【问题标题】:What are the Dangers of Method Swizzling in Objective-C?Objective-C 中方法混用的危险是什么?
【发布时间】:2011-07-17 09:25:15
【问题描述】:

我听说有人说方法混合是一种危险的做法。甚至 swizzling 这个名字也暗示它有点作弊。

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

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

例如

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

【问题讨论】:

  • 从你读过的代码中得到你期望的其他东西已经不是很好了
  • 这听起来像是一种不干净的方式来做 python 装饰器非常干净地做的事情

标签: ios objective-c swizzling


【解决方案1】:

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

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

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

背景

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

讨论

以下是方法调配的一些陷阱:

  • 方法混合不是原子的
  • 更改未拥有代码的行为
  • 可能的命名冲突
  • Swizzling 改变了方法的参数
  • 调酒的顺序很重要
  • 难以理解(看起来是递归的)
  • 难以调试

这些观点都是正确的,在解决它们时,我们可以提高我们对方法混合的理解以及用于实现结果的方法。我会一次接一个。

方法调配不是原子的

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

改变非拥有代码的行为

这是 swizzling 的一个问题,但它是重点。目标是能够更改该代码。人们指出这很重要的原因是因为您不仅要更改要更改的 NSButton 的一个实例,还要更改应用程序中的所有 NSButton 实例。出于这个原因,你在调酒时应该小心,但你不需要完全避免它。

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

可能的命名冲突

命名冲突是整个 Cocoa 中的一个问题。我们经常在类别中添加类名和方法名的前缀。不幸的是,命名冲突是我们语言中的瘟疫。但是,在 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 方法是这样定义的:

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

通过重命名方法来改变方法的参数

这是我心目中的大事。这就是不应该进行标准方法混合的原因。您正在更改传递给原始方法实现的参数。这就是它发生的地方:

[self my_setFrame:frame];

这一行的作用是:

objc_msgSend(self, @selector(my_setFrame:), frame);

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

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

调酒的顺序很重要

方法混合的顺序很重要。假设 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 上的方法被混用时会发生什么?好吧,大多数 swizzling 将确保它不会替换所有视图的 setFrame: 的实现,因此它会拉起实例方法。这将使用现有实现在NSButton 类中重新定义setFrame:,以便交换实现不会影响所有视图。现有的实现是在NSView 上定义的。在NSControl 上调配时也会发生同样的事情(再次使用NSView 实现)。

当你在一个按钮上调用setFrame: 时,它会因此调用你的swizzled 方法,然后直接跳转到最初在NSView 上定义的setFrame: 方法。不会调用 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:)];

由于视图调动首先发生,控件调动将能够上拉正确的方法。同样,由于控件调动在按钮调动之前,按钮将上拉控件的调动实现setFrame:。这有点令人困惑,但这是正确的顺序。我们如何才能确保这种顺序?

同样,只需使用load 来调配东西。如果您在load 中调动,并且只对正在加载的类进行更改,那么您将是安全的。 load 方法保证超类加载方法将在任何子类之前被调用。我们会得到完全正确的订单!

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

查看传统定义的 swizzled 方法,我认为很难判断发生了什么。但是看看我们在上面进行的调动的另一种方式,它很容易理解。这个已经解决了!

难以调试

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

结论

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


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

【讨论】:

  • 惊人的信息和技术上合理的答案。讨论真的很有趣。感谢您花时间写这篇文章。
  • 您能否指出在应用商店中是否可以接受混搭您的体验。我也将您转至stackoverflow.com/questions/8834294
  • Swizzling 可以在 App Store 上使用。许多应用程序和框架都可以(包括我们的)。上面的一切仍然有效,你不能调配私有方法(实际上,你在技术上可以,但你会冒被拒绝的风险,这样做的危险在于不同的线程)。
  • 惊人的答案。但是为什么在 class_swizzleMethodAndStore() 中而不是直接在 +swizzle:with:store: 中进行调配呢?为什么要增加这个功能?
  • @Frizlab 好问题!这真的只是一种风格。如果您正在编写一堆直接使用 Objective-C 运行时 API(C 语言)的代码,那么能够将其称为 C 风格以保持一致性是很不错的。除此之外,我能想到的唯一原因是,如果你用纯 C 编写了一些东西,那么它仍然是可调用的。不过,您没有理由不能在 Objective-C 方法中完成所有操作。
【解决方案2】:

你可能会得到看起来很奇怪的代码

- (void)addSubview:(UIView *)view atIndex:(NSInteger)index {
    //this looks like an infinite loop but we're swizzling so default will get called
    [self addSubview:view atIndex:index];

来自与一些 UI 魔术相关的实际生产代码。

【讨论】:

    【解决方案3】:

    方法调配在单元测试中非常有用。

    它允许您编写一个模拟对象并使用该模拟对象代替真实对象。您的代码要保持干净并且您的单元测试具有可预测的行为。假设您要测试一些使用 CLLocationManager 的代码。您的单元测试可以调配 startUpdatingLocation 以便它将预定的一组位置提供给您的委托,并且您的代码不必更改。

    【讨论】:

    • isa-swizzling 会是更好的选择。
    【解决方案4】:

    虽然我使用过这种技术,但我想指出:

    • 它会混淆您的代码,因为它可能会导致未记录但需要的副作用。当一个人阅读代码时,他/她可能不知道所需的副作用行为,除非他/她记得搜索代码库以查看它是否已被混合。我不确定如何缓解这个问题,因为并非总是可以记录代码依赖于副作用混杂行为的每个地方。
    • 它可能会降低您的代码的可重用性,因为如果有人发现一段代码取决于他们想在其他地方使用的 swizzled 行为,就不能简单地将其剪切并粘贴到其他代码库中,而无需找到并复制 swizzled 方法.

    【讨论】:

      【解决方案5】:

      首先我将准确定义我所说的方法调配:

      • 将最初发送到方法(称为 A)的所有调用重新路由到新方法(称为 B)。
      • 我们拥有方法 B
      • 我们没有方法 A
      • 方法 B 做了一些工作,然后调用方法 A。

      Method swizzling 比这更通用,但这是我感兴趣的情况。

      危险:

      • 原始类的变化。我们不拥有我们正在调配的课程。如果班级发生变化,我们的 swizzle 可能会停止工作。

      • 难以维护。您不仅要编写和维护 swizzled 方法。您必须编写和维护执行 swizzle 的代码

      • 难以调试。很难跟随调酒的流程,有些人甚至可能没有意识到调酒已经完成。如果 swizzle 引入了错误(可能是由于原始类的更改),它们将很难解决。

      总之,您应该尽量减少调酒,并考虑原始类中的更改可能会如何影响您的调酒。此外,您应该清楚地评论并记录您正在做的事情(或完全避免这样做)。

      【讨论】:

      • @everyone 我已将您的答案组合成一个很好的格式。随意编辑/添加它。感谢您的意见。
      • 我是你心理学的粉丝。 “强大的调配能力伴随着巨大的调配责任。”
      【解决方案6】:

      谨慎而明智地使用它可以产生优雅的代码,但通常只会导致代码混乱。

      我说它应该被禁止,除非你碰巧知道它为特定的设计任务提供了一个非常优雅的机会,但你需要清楚地知道它为什么很好地适用于这种情况,以及为什么替代方案不能优雅地工作情况。

      例如,method swizzling 的一个很好的应用是 isa swizzling,这就是 ObjC 实现 Key Value Observing 的方式。

      一个不好的例子可能是依赖方法调配作为扩展类的一种方式,这会导致极高的耦合度。

      【讨论】:

      • -1 用于提及 KVO——在 method swizzling 的上下文中。 isa swizzling 是另一回事。
      • @NikolaiRuhe:请随时提供参考,以便我开导。
      • 这里引用了implementation details of KVOMethod swizzling 在 CocoaDev 上有描述。
      【解决方案7】:

      真正危险的不是调酒本身。正如您所说,问题在于它通常用于修改框架类的行为。假设您对这些私人课程的工作方式有所了解,这是“危险的”。即使您的修改今天有效,Apple 将来也有可能更改类并导致您的修改中断。此外,如果许多不同的应用程序都这样做,Apple 很难在不破坏大量现有软件的情况下更改框架。

      【讨论】:

      • 我会说这样的开发者应该收获他们所播种的——他们将他们的代码与特定的实现紧密耦合。因此,如果该实现以一种不应该破坏其代码的微妙方式发生变化,如果不是因为他们明确决定像他们一样紧密地耦合他们的代码,那是他们的错。
      • 当然,但是说一个非常流行的应用程序在下一个 iOS 版本下会中断。很容易说这是开发人员的错,他们应该知道得更好,但这让苹果在用户眼中看起来很糟糕。如果你想这样做,我认为混合你自己的方法甚至类没有任何害处,除了你的观点是它会使代码有些混乱。当您混合由其他人控制的代码时,它开始变得更加冒险 - 而这正是混合似乎最诱人的情况。
      • Apple 不是唯一可以通过 siwzzling 修改的基类的提供者。应该编辑您的答案以包括所有人。
      • @A.R.也许吧,但这个问题被标记为 iOS 和 iPhone,所以我们应该在这种情况下考虑它。鉴于 iOS 应用程序必须静态链接除 iOS 提供的库和框架之外的任何库和框架,Apple 实际上是唯一可以在没有应用程序开发人员参与的情况下更改框架类实现的实体。但我确实同意类似问题会影响其他平台的观点,而且我想说这些建议也可以推广到 swizzling 之外。一般而言,建议是:自己动手。避免修改其他人维护的组件。
      • 方法调配可能是那么危险。但是随着框架的出现,它们并没有完全忽略以前的框架。你仍然需要更新你的应用所期望的基本框架。并且该更新会破坏您的应用程序。当 ios 更新时,它可能不是那么大的问题。我的使用模式是覆盖默认的重用标识符。由于我无法访问 _reuseIdentifier 变量并且不想替换它,除非没有提供它,我唯一的解决方案是对每个变量进行子类化并提供重用标识符,或者在 nil 时进行调整和覆盖。实现类型是关键...
      【解决方案8】:

      我觉得最大的危险是产生许多不想要的副作用,完全是偶然的。这些副作用可能会以“错误”的形式出现,从而导致您走上错误的道路来寻找解决方案。根据我的经验,危险是难以辨认、令人困惑和令人沮丧的代码。有点像有人在 C++ 中过度使用函数指针。

      【讨论】:

      • 好的,问题是很难调试。造成这些问题的原因是审阅者没有意识到/忘记了代码已被调配,并且在调试时正在寻找错误的位置。
      • 是的。此外,过度使用时,您可能会陷入无尽的连锁店或调酒网络。想象一下,当你在未来的某个时候只改变其中一个时会发生什么?我将这种体验比作叠茶杯或玩“积木密码”
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-07-13
      • 2013-01-31
      • 2011-08-04
      • 2012-11-18
      相关资源
      最近更新 更多