ios - 在 Objective-C 中方法 Swizzling 的危险是什么?

标签 ios objective-c swizzling

我听说有人说方法调配是一种危险的做法。甚至名称 swizzling 也表明它有点作弊。

Method Swizzling正在修改映射,以便调用选择器 A 将实际调用实现 B。它的一种用途是扩展闭源类的行为。

我们能否将风险形式化,以便任何决定是否使用 swizzling 的人都可以做出明智的决定,是否值得他们尝试做的事情。

例如

  • 命名冲突 : 如果该类后来扩展其功能以包含您添加的方法名称,则会导致大量问题。通过明智地命名混合方法来降低风险。
  • 最佳答案

    我认为这是一个非常好的问题,令人遗憾的是,大多数答案没有解决真正的问题,而是绕过了这个问题,只是说不要使用 swizzling。

    使用方法嘶嘶声就像在厨房里使用锋利的刀。有些人害怕锋利的刀,因为他们认为它们会严重割伤自己,但事实是sharp knives are safer .

    方法 swizzling 可用于编写更好、更高效、更易于维护的代码。它也可能被滥用并导致可怕的错误。

    背景

    与所有设计模式一样,如果我们完全了解该模式的后果,我们就能够就是否使用它做出更明智的决定。单例是一个很好的例子,它非常有争议,并且有充分的理由 - 它们真的很难正确实现。尽管如此,许多人仍然选择使用单例。关于 swizzling 也是如此。一旦你完全理解了好与坏,你就应该形成自己的观点。

    讨论

    以下是方法 swizzling 的一些陷阱:

  • 方法 swizzling 不是原子的
  • 更改无主代码的行为
  • 可能的命名冲突
  • Swizzling 更改方法的参数
  • swizzles 的顺序很重要
  • 难以理解(看起来是递归的)
  • 调试困难

  • 这些观点都是有效的,通过解决它们,我们可以提高我们对方法调配以及用于实现结果的方法的理解。我会一个一个地拿走。

    方法调配不是原子的

    我还没有看到可以安全地同时使用的方法 swizzling 的实现。在 95% 的情况下,这实际上不是问题,您想使用方法 swizzling。通常,您只想替换方法的实现,并且希望该实现在程序的整个生命周期中使用。这意味着你应该在 +(void)load 中做你的方法 swizzling . load class 方法在应用程序开始时串行执行。如果您在这里进行 swizzling,您将不会遇到任何并发问题。如果您要混入 +(void)initialize ,但是,您最终可能会在 swizzling 实现中遇到竞争条件,并且运行时可能会以一种奇怪的状态结束。

    改变非拥有代码的行为

    这是 swizzling 的一个问题,但这就是重点。目标是能够更改该代码。人们指出这是一件大事的原因是因为您不仅仅是为 NSButton 的一个实例改变事物。你想要改变的东西,而是为了所有 NSButton应用程序中的实例。出于这个原因,你在调配时应该小心,但你不需要完全避免它。

    这么想吧……如果你重写了一个类中的一个方法,而不调用父类(super class)的方法,可能会导致出现问题。在大多数情况下,父类(super class)期望调用该方法(除非另有说明)。如果您将同样的想法应用于 swizzling,那么您已经涵盖了大多数问题。始终调用原始实现。如果你不这样做,你可能会改变太多以确保安全。

    可能的命名冲突

    命名冲突是整个 Cocoa 的一个问题。我们经常在类别中添加类名和方法名的前缀。不幸的是,命名冲突在我们的语言中是一种瘟疫。但是,在 swizzling 的情况下,它们不一定是。我们只需要稍微改变我们对方法 swizzling 的看法。大多数 swizzling 是这样完成的:
    @interface NSView : NSObject
    - (void)setFrame:(NSRect)frame;
    @end
    
    @implementation NSView (MyViewAdditions)
    
    - (void)my_setFrame:(NSRect)frame {
        // do custom work
        [self my_setFrame:frame];
    }
    
    + (void)load {
        [self swizzle:@selector(setFrame:) with:@selector(my_setFrame:)];
    }
    
    @end
    

    这工作得很好,但是如果 my_setFrame: 会发生什么是在别处定义的?这个问题并不是 swizzling 所独有的,但无论如何我们都可以解决它。该解决方法还有一个额外的好处,即解决其他陷阱。这是我们所做的:
    @implementation NSView (MyViewAdditions)
    
    static void MySetFrame(id self, SEL _cmd, NSRect frame);
    static void (*SetFrameIMP)(id self, SEL _cmd, NSRect frame);
    
    static void MySetFrame(id self, SEL _cmd, NSRect frame) {
        // do custom work
        SetFrameIMP(self, _cmd, frame);
    }
    
    + (void)load {
        [self swizzle:@selector(setFrame:) with:(IMP)MySetFrame store:(IMP *)&SetFrameIMP];
    }
    
    @end
    

    虽然这看起来有点不像 Objective-C(因为它使用函数指针),但它避免了任何命名冲突。原则上,它和标准的 swizzling 做的事情完全一样。对于已经使用 swizzling 的人来说,这可能有点改变,因为它已经定义了一段时间,但最终,我认为它更好。 swizzling 方法定义如下:
    typedef IMP *IMPPointer;
    
    BOOL class_swizzleMethodAndStore(Class class, SEL original, IMP replacement, IMPPointer store) {
        IMP imp = NULL;
        Method method = class_getInstanceMethod(class, original);
        if (method) {
            const char *type = method_getTypeEncoding(method);
            imp = class_replaceMethod(class, original, replacement, type);
            if (!imp) {
                imp = method_getImplementation(method);
            }
        }
        if (imp && store) { *store = imp; }
        return (imp != NULL);
    }
    
    @implementation NSObject (FRRuntimeAdditions)
    + (BOOL)swizzle:(SEL)original with:(IMP)replacement store:(IMPPointer)store {
        return class_swizzleMethodAndStore(self, original, replacement, store);
    }
    @end
    

    通过重命名方法进行 Swizzling 会更改方法的参数

    这是我心中的大事。这就是不应该进行标准方法调配的原因。您正在更改传递给原始方法实现的参数。这是它发生的地方:
    [self my_setFrame:frame];
    

    这条线的作用是:
    objc_msgSend(self, @selector(my_setFrame:), frame);
    

    它将使用运行时查找 my_setFrame: 的实现.一旦找到实现,它就会使用给定的相同参数调用实现。它找到的实现是 setFrame: 的原始实现,所以它继续调用,但是 _cmd参数不是 setFrame:应该是这样。现在是 my_setFrame: .原始实现被调用时带有一个它从未预料到会收到的参数。这不好。

    有一个简单的解决方案 - 使用上面定义的替代 swizzling 技术。论据将保持不变!

    调酒的顺序很重要

    方法被混淆的顺序很重要。假设 setFrame:仅在 NSView 上定义,想象一下事情的顺序:
    [NSButton swizzle:@selector(setFrame:) with:@selector(my_buttonSetFrame:)];
    [NSControl swizzle:@selector(setFrame:) with:@selector(my_controlSetFrame:)];
    [NSView swizzle:@selector(setFrame:) with:@selector(my_viewSetFrame:)];
    

    NSButton 上的方法时会发生什么是 swizzled?好吧,大多数 swizzling 将确保它不会取代 setFrame: 的实现对于所有 View ,所以它会拉起实例方法。这将使用现有的实现来重新定义 setFrame:NSButton类,以便交换实现不会影响所有 View 。现有的实现是在 NSView 上定义的实现。 .在 NSControl 上 swizzling 时也会发生同样的事情(再次使用 NSView 实现)。

    当您拨打 setFrame:在按钮上,它会因此调用您的 swizzled 方法,然后直接跳转到 setFrame:方法最初定义于 NSView . NSControlNSView不会调用 swizzled 的实现。

    但是如果订单是:
    [NSView swizzle:@selector(setFrame:) with:@selector(my_viewSetFrame:)];
    [NSControl swizzle:@selector(setFrame:) with:@selector(my_controlSetFrame:)];
    [NSButton swizzle:@selector(setFrame:) with:@selector(my_buttonSetFrame:)];
    

    由于 View swizzling 首先发生,因此控件 swizzling 将能够调出正确的方法。同样,由于控件 swizzling 在按钮 swizzling 之前,按钮将拉起控件的 swizzled 实现 setFrame: .这有点令人困惑,但这是正确的顺序。我们怎样才能保证这个顺序呢?

    同样,只需使用 load调和东西。如果您混入 load并且您只对正在加载的类进行更改,您就安全了。 load方法保证在任何子类之前调用​​父类(super class)加载方法。我们会得到完全正确的订单!

    难以理解(看起来是递归的)

    看看传统定义的 swizzled 方法,我认为很难说清楚发生了什么。但是看看我们在上面完成 swizzling 的另一种方式,它很容易理解。这个已经解决了!

    调试困难

    调试过程中的一个困惑是看到一个奇怪的回溯,其中混淆了名称,一切都在你的脑海中变得困惑。同样,替代实现解决了这个问题。您将在回溯中看到明确命名的函数。尽管如此,swizzling 可能很难调试,因为很难记住 swizzling 有什么影响。很好地记录您的代码(即使您认为您是唯一会看到它的人)。遵循良好的做法,你会没事的。调试并不比多线程代码难。

    结论

    如果使用得当,方法 swizzling 是安全的。您可以采取的一个简单安全措施是只在 load 中混合。 .像编程中的许多事情一样,它可能很危险,但了解后果将使您能够正确使用它。

    1 使用上面定义的 swizzling 方法,如果您要使用蹦床,您可以使事情线程安全。你需要两个蹦床。在方法开始时,您必须分配函数指针,store , 到一个函数,该函数一直旋转到 store 的地址指着变了。这将避免在您能够设置 store 之前调用 swizzled 方法的任何竞争条件。函数指针。然后,在类中尚未定义实现的情况下,您需要使用蹦床,并进行蹦床查找并正确调用父类(super class)方法。定义方法以便动态查找 super 实现将确保 swizzling 调用的顺序无关紧要。

    关于ios - 在 Objective-C 中方法 Swizzling 的危险是什么?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/5339276/

    相关文章:

    ios - 由于 NodeJs 服务器,我的 iOS 聊天应用程序会被应用程序商店拒绝吗?

    iphone - 创建简单 UIAlertView 时的 EXC_BAD_ACCESS

    objective-c - ios 警告调用 uiviewcontroller 的 init 方法

    ios - 使用 Delphi XE5 在 UIView 类 [Swizzling] 中开始 Hook

    ios - 如何调配 NSURLSession 类方法 dataTaskWithUrl

    ios - UIView 方法 swizzling swift 3

    iphone - 在 iphone 中从数据库中检索图像花费的时间太长

    ios - redirect_uri_mismatch iOS上的Unity Google API

    ios - UITableView 单元格在调整大小后不响应输入

    ios - 转到另一个选项卡中的详细 View Controller ?