Objective-C:调配方法调用的方法应该调用原始实现

标签 objective-c inheritance method-swizzling

我正在修复其他人的闭源应用程序中的错误。

在 macOS 中,可以在“系统偏好设置”中设置滚动条以显示“始终”(NSScrollerStyleLegacy)、“滚动时”(NSScrollerStyleOverlay) 或“自动基于鼠标或触控板”(如果连接了触控板,则为 NSScrollerStyleOverlay,否则为 NSScrollerStyleLegacy) .要检查正在使用哪种样式,应用程序应该执行以下操作:

if ([NSScroller preferredScrollerStyle] == NSScrollerStyleLegacy)
    addPaddingForLegacyScrollbars();

不幸的是,出于某种原因,此应用正在读取 NSUserDefaults 的值(已使用反编译器确认)。

NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
if ([[defaults objectForKey:@"AppleShowScrollBars"] isEqual: @"Always"])
    addPaddingForLegacyScrollbars();

此代码错误地假定 AppleShowScrollBars 的除“Always”以外的任何值都等同于 NSScrollerStyleOverlay。如果默认设置为“自动”并且没有连接触控板,这将是错误的。

为了解决这个问题,我使用了 ZKSwizzle调配 NSUserDefaults 的库 objectForKey:方法:

- (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"] 来检查用户的偏好。经过一番搜索,我遇到了 this answer关于如何获取调用者的类名,并写道:

- (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,但事实并非如此,所以我只能方法边界的变化。

我根本上想要的是仅当在堆栈中在我上方调用此方法时才调配此方法。堆栈下方的任何调用都应使用原始实现。

有没有办法做到这一点,或者我目前的解决方案是否合理?

最佳答案

这种方法可能没问题(在“我已经决定调配”的上下文中),但它确实感觉有点脆弱,而且 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 变量。这在这里不重要,因为你'正在调配一个特定的对象。)

如果重新进入这个函数,那么它会直接跳到原来的实现。

关于Objective-C:调配方法调用的方法应该调用原始实现,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/72850882/

相关文章:

iphone - 应用程序仅在iPhone设备上崩溃而不在模拟器中崩溃

ios - View 的宽度发生了奇怪的变化

javascript继承奇怪

ios - Swift - 仅当类符合使用方法调配的协议(protocol)时才扩展类

objective-c - 使用 Mail.app 从 Cocoa 发送 HTML 邮件

objective-c - 将自定义 CSS 和 javascript 注入(inject) WebView

java多重继承ActionBarActivity

python - 如何更好的基于配置文件实例化不同的子类?

ios - 调试核心数据 __NSCFSet addObject nil 异常

ios - 使用 swizzling 更改 UIAlertView