我正在修复别人的闭源应用程序中的错误。
在macOS中,滚动条可以在"系统偏好设置"中设置为显示"始终";(nsscrollerstylecacy), "(NSScrollerStyleOverlay),或"自动基于鼠标或触控板";(如果连接了触控板,则为NSScrollerStyleOverlay,否则为nsscrollerstyleelegacy)。要检查使用的是哪种样式,应用程序应该这样做:
if ([NSScroller preferredScrollerStyle] == NSScrollerStyleLegacy)
addPaddingForLegacyScrollbars();
不幸的是,由于某种原因,这个应用程序从NSUserDefaults
读取值(使用反编译器确认)。
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
if ([[defaults objectForKey:@"AppleShowScrollBars"] isEqual: @"Always"])
addPaddingForLegacyScrollbars();
此代码错误地假设AppleShowScrollBars
的任何值,而不是"Always"等于NSScrollerStyleOverlay
。如果默认设置为"Automatic"并且没有连接Trackpad
为了解决这个问题,我使用ZKSwizzle库来切换NSUserDefaults objectForKey: method:
- (id)objectForKey:(NSString *)defaultName {
if ([defaultName isEqual: @"AppleShowScrollBars"]) {
if ([NSScroller preferredScrollerStyle] == NSScrollerStyleLegacy) {
return @"Always";
} else {
return @"WhenScrolling";
}
}
return ZKOrig(id, defaultName);
}
不幸的是,这会导致堆栈溢出,因为[NSScroller preferredScrollerStyle]
本身最初会调用[NSUserDefaults objectForKey:@"AppleShowScrollBars"]
来检查用户的首选项。经过一番搜索,我找到了如何获取调用者的类名的答案,并写道:
- (id)objectForKey:(NSString *)defaultName {
if ([defaultName isEqual: @"AppleShowScrollBars"]) {
NSString *caller = [[[NSThread callStackSymbols] objectAtIndex:1] substringWithRange:NSMakeRange(4, 6)];
if (![caller isEqualToString:@"AppKit"]) {
if ([NSScroller preferredScrollerStyle] == NSScrollerStyleLegacy) {
return @"Always";
} else {
return @"WhenScrolling";
}
}
}
return ZKOrig(id, defaultName);
}
这工作完美!然而,获取调用者使用backtrace_symbols
API用于调试,对上述答案的评论表明这是一个非常糟糕的主意。而且,一般来说,根据调用者的不同返回不同的值会让人感觉很恶心。
显然,如果这是我自己的代码,我会重写它,首先使用preferredScrollerStyle
而不是NSUserDefaults
,但它不是,所以我只能在方法边界进行更改。
我根本想要的是这个方法只有当它在堆栈中被调用时才会被搅拌。
是否有办法做到这一点,或者我目前的解决方案是合理的?
这种方法可能是可以的(在"我已经决定swizzle"的上下文中),但它确实感觉有点脆弱,您注意到,callStackSymbols
可以非常慢,可用的信息取决于调试符号是否可用(这可能永远不会破坏这个特定的用例,但如果它这样做,错误将非常令人困惑)。
我认为你可以通过使用一个静态变量来缩短递归,使它更健壮,更快。
- (id)objectForKey:(NSString *)defaultName {
static BOOL isRunning = false;
if (!isRunning && [defaultName isEqual: @"AppleShowScrollBars"]) {
isRunning = true;
NSScrollerStyle scrollerStyle = [NSScroller preferredScrollerStyle];
isRunning = false;
if (scrollerStyle == NSScrollerStyleLegacy) {
return @"Always";
} else {
return @"WhenScrolling";
}
}
return ZKOrig(id, defaultName);
}
函数中的static
变量在调用之间保留其值,因此可以使用它来检测递归是否发生。(这不是线程安全的,但在这个用例中应该不是问题。还要注意,这个类的所有实例共享同一个static
变量。这在这里应该不重要,因为你正在搅拌一个特定的对象。)
如果重新输入这个函数,那么它将直接跳转到原来的实现。
Rob的回答很好,但是如果我正确理解了你的需求,可能有一个替代解决方案可以简化一些事情。你可以通过搅拌app方法来避免重入(并避免在所有上下文中搅拌NSUserDefaults
),使用它作为入口点来知道应用程序何时要从NSUserDefaults
中读取,并在调用期间临时地搅拌NSUserDefaults
:
// This can be an ivar, static variable, etc.
// You can initialize this with `dispatch_once` if the method only reads the scroller
// style only once (e.g. on initialization), or leave this mutable if you want to check
// every time the app method is called.
static NSScrollerStyle effectiveScrollerStyle = NSScrollerStyleLegacy;
// Replace this dummy method with whatever the actual interface is for the app method
// in question.
- (void)whateverTheInterfaceIsForTheAppMethod:(id)whatever {
// Call this _prior_ to swizzling `NSUserDefaults`.
effectiveScrollerStyle = [NSScrollerStyle preferredScrollerStyle];
/* swizzle NSUserDefalts with the implementation below */
ZKOrid(void, whatever);
/* restore NSUserDefaults */
}
// --------------------------------------------------- //
- (id)objectForKey:(NSString *)defaultName {
if ([defaultName isEqual:@"AppleShowScrollBars"]) {
if (effectiveScrollerStyle == NSScrollerStyleLegacy) {
return @"Always";
} else {
return @"WhenScrolling";
}
}
return ZKOrig(id, defaultName);
}
我不熟悉ZKSwizzle
,所以我不确定你用来搅拌NSUserDefaults
的确切语法,但希望概念是清楚的,并做你想要的。