Objective C中方法调配的危险是什么?

时间:2011-03-17 12:54:40

标签: ios objective-c swizzling

我听到有人说方法调整是一种危险的做法。即使是名字调整也会让人觉得这有点像作弊。

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

我们能否将风险正式化,以便任何决定是否使用调酒的人都能做出明智的决定是否值得他们尝试做什么。

E.g。

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

8 个答案:

答案 0 :(得分:423)

我认为这是一个非常好的问题,而且很遗憾的是,大多数答案都没有解决问题,而是简单地说不要使用调酒问题。

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

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

背景

与所有设计模式一样,如果我们完全了解模式的后果,我们可以就是否使用模式做出更明智的决策。单身人士是一个很有争议的事情的好例子,并且有充分的理由 - 他们真的很难正确实施。不过,很多人仍然选择使用单身人士。关于调配也是如此。一旦你完全理解了好的和坏的,你应该形成自己的意见。

讨论

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

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

这些要点都是有效的,在解决这些问题时,我们可以改进对方法调配的理解以及用于实现结果的方法。我会一次拿走每一个。

方法调配不是原子的

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

更改非拥有代码的行为

这是一个混乱的问题,但这是一个重点。目标是能够更改该代码。人们认为这是一个大问题的原因是因为您不仅要为NSButton的一个实例更改内容,而是要为所有NSButton更改内容。@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 您的申请中的实例。出于这个原因,你在调整时应该保持谨慎,但是你不需要完全避免它。

以这种方式思考......如果你在类中覆盖一个方法并且你不调用超类方法,你可能会出现问题。在大多数情况下,超类期望调用该方法(除非另有说明)。如果你将这同样的想法应用于调配,那么你已经涵盖了大多数问题。始终调用原始实现。如果你不这样做,你可能会改变太多而不安全。

可能的命名冲突

命名冲突在整个Cocoa中都是一个问题。我们经常在类别中为类名和方法名添加前缀。不幸的是,命名冲突在我们的语言中是一个瘟疫。然而,在变相的情况下,他们并非必须如此。我们只需要改变我们认为方法稍微调整的方式。大多数调配都是这样完成的:

my_setFrame:

这很好用,但如果在其他地方定义@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 会怎么样?这个问题对于调配来说并不是独一无二的,但无论如何我们都可以解决它。解决方法还有一个额外的好处,即解决其他陷阱。以下是我们的工作:

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

虽然这看起来不像Objective-C(因为它使用了函数指针),但它避免了任何命名冲突。原则上,它与标准调配完全相同。对于那些一直使用调配的人来说,这可能有点变化,但最终,我认为它会更好。因此定义了混合方法:

[self my_setFrame:frame];

通过重命名方法进行调配会改变方法的参数

这是我脑海中最重要的一个。这就是不应该进行标准方法调配的原因。您正在更改传递给原始方法实现的参数。这就是它发生的地方:

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

这一行的作用是:

my_setFrame:

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

这是一个简单的解决方案 - 使用上面定义的替代调配技术。论点将保持不变!

swizzles的顺序

方法变得混乱的顺序很重要。假设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

setFrame:上的方法被淘汰时会发生什么?大多数调配都会确保它不会替换所有视图的setFrame:实现,因此它将拉出实例方法。这将使用现有实现在NSButton类中重新定义NSView,以便交换实现不会影响所有视图。现有的实现是在NSControl上定义的实现。在NSView上混淆时会发生同样的事情(再次使用setFrame:实现)。

当您在按钮上调用setFrame:时,它会调用您的混合方法,然后直接跳转到最初在NSView上定义的NSControl方法。不会调用NSView[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中徘徊,并且只对正在加载的班级进行更改,那么您将是安全的。 load方法保证在任何子类之前调用​​超类加载方法。我们会得到准确的订单!

难以理解(看起来递归)

考虑一种传统定义的混合方法,我认为很难说出发生了什么。但是看看我们上面已经调整过的替代方式,它很容易理解。这个问题已经解决了!

难以调试

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

结论

如果使用得当,方法调配是安全的。您可以采取的一个简单的安全措施是仅在store中进行调整。像编程中的许多东西一样,它可能是危险的,但了解后果将允许您正确使用它。


1 使用上面定义的混合方法,如果您使用蹦床,可以使线程安全。你需要两个蹦床。在方法开始时,您必须将函数指针store分配给一个旋转的函数,直到store指向的地址发生变化。这样可以避免在能够设置{{1}}函数指针之前调用swizzled方法的任何竞争条件。然后,如果在类中已经定义了实现并且具有trampoline查找并正确调用超类方法,则需要使用trampoline。定义方法以便动态查找超级实现将确保调配调用的顺序无关紧要。

答案 1 :(得分:11)

首先,我将通过方法调配确切地定义我的意思:

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

方法调整比这更普遍,但我感兴趣的是这种情况。

危险:

  • 原始课程的变化。我们不拥有我们调情的课程。如果班级改变我们的混战可能会停止工作。

  • 难以维护。你不仅要编写和维护混合方法。你必须编写和维护预先形成混合的代码

  • 难以调试。很难跟随混合的流动,有些人甚至可能没有意识到已经进行了混合。如果从混合中引入了错误(可能是原始类别中的变化),则很难解决。

总之,您应该保持最低限度的调整,并考虑原始类中的更改可能会如何影响您的混合。此外,您应该清楚地评论和记录您正在做的事情(或者完全避免它。)

答案 2 :(得分:7)

真正危险的不是调酒本身。正如您所说,问题是它经常用于修改框架类的行为。假设你知道那些私人课程是如何运作的,这是“危险的”。即使您今天的修改有效,Apple也有可能在将来更改课程并导致您的修改中断。此外,如果许多不同的应用程序都这样做,那么苹果公司在不破坏大量现有软件的情况下更改框架将会更加困难。

答案 3 :(得分:5)

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

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

例如,方法调配的一个很好的应用就是混合,这就是ObjC实现Key Value Observing的方法。

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

答案 4 :(得分:5)

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

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

答案 5 :(得分:4)

我觉得最大的危险在于产生许多不必要的副作用,完全是偶然的。这些副作用可能会表现为“错误”,从而导致您走错路径找到解决方案。根据我的经验,危险是难以辨认,令人困惑和令人沮丧的代码。有点像有人在C ++中重写函数指针。

答案 6 :(得分:4)

你最终可能会看到奇怪的代码,比如

- (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魔术相关的实际生产代码。

答案 7 :(得分:3)

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

它允许您编写模拟对象并使用该模拟对象而不是真实对象。您的代码保持干净,您的单元测试具有可预测的行为。假设您要测试一些使用CLLocationManager的代码。您的单元测试可以调整startUpdatingLocation,以便它可以向您的代理提供一组预定的位置,而您的代码也不必更改。