目标C中的方法摇摆的危险是什么?

我听说有人说这种方法是一种危险的做法。 甚至连这个名字也不知道这是一个骗子。

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

我们是否可以将风险正式化,以便任何决定是否使用混搭的人都能够做出明智的决定,是否值得为自己的努力做点什么。

例如

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

我认为这是一个非常好的问题,而不是解决真正的问题,大部分答案都是围绕着这个问题的,而不是使用虚拟的。

使用方法嘶嘶作响就像在厨房使用锋利的刀。 有些人害怕刀锋,因为他们认为自己会严重削减自己,但事实是刀锋更安全 。

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

背景

就像所有的devise模式一样,如果我们充分意识到模式的后果,我们就能够做出更明智的决定是否使用模式。 单身人士是很有争议的一个很好的例子,有很好的理由 – 他们真的很难正确实施。 不过,许多人仍然select使用单身。 同样可以说关于swizzling。 一旦你完全了解好坏,你应该形成自己的观点。

讨论

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

  • 方法swizzling不是primefaces的
  • 更改非拥有代码的行为
  • 可能的命名冲突
  • Swizzling改变了方法的参数
  • swizzles的顺序很重要
  • 很难理解(看起来recursion)
  • 难以debugging

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

方法swizzling不是primefaces的

我还没有看到可以同时安全使用的方法debugging的实现1 。 在95%的情况下,这实际上不是一个问题,您想使用方法调整。 通常,您只需要replace方法的实现,并且希望在您的程序的整个生命周期中使用该实现。 这意味着你应该做你的方法在+(void)loadload类方法在应用程序的开始处连续执行。 如果您在这里进行调整,您将不会遇到任何并发问题。 但是,如果你在+(void)initialize ,那么你最终可能会在竞争环境中出现竞争条件,并且运行时可能会以奇怪的状态结束。

更改非拥有代码的行为

这是一个棘手的问题,但这是一个重点。 目标是能够更改该代码。 人们指出这是一个大问题的原因是因为你不只是改变NSButton的一个实例,你想改变的东西,而是你的应用程序中的所有NSButton实例。 出于这个原因,你应该小心,当你swizzle,但你不需要完全避免它。

想想这样…如果你在一个类中重写一个方法,并且你没有调用超类的方法,你可能会引起问题。 在大多数情况下,超类预计将调用该方法(除非另有说明)。 如果你把这个想法应用到混合,你已经覆盖了大部分的问题。 始终调用原始实现。 如果你不这样做,你可能会变得太安全了。

可能的命名冲突

命名冲突是cocoa所有的问题。 我们经常在类别中添加类名和方法名。 不幸的是,命名冲突是我们语言中的一个瘟疫。 然而,在混合的情况下,它们不一定是。 我们只需要改变我们对微调方法的思考方式。 大多数调配都是这样完成的:

 @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:被定义在其他地方会发生什么? 这个问题不是独一无二的,但我们可以解决它。 解决方法还有解决其他陷阱的好处。 以下是我们所做的:

 @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(因为它使用函数指针),它避免了任何命名冲突。 原则上,它和标准的混合做同样的事情。 对于已经定义了一段时间的用户来说,这可能会有些改变,但是最终我认为它会更好。 混合方法是这样定义的:

 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:的实现my_setFrame: 。 一旦find该实现,就会使用与给定的参数相同的参数来调用实现。 它find的实现是setFrame:的原始实现,所以它继续并调用它,但_cmd参数不是setFrame:就像它应该是。 现在是my_setFrame: 原来的实现被调用,它从来没有预料到它会收到的参数。 这不好。

有一个简单的解决scheme – 使用上面定义的替代swizzling技术。 论点将保持不变!

swizzles的顺序很重要

方法的顺序问题。 假设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上的方法被debugging时会发生什么? 那么大多数swizzling将确保它不会取代setFrame:的实现setFrame:对于所有视图,所以它将拉起实例方法。 这将使用现有的实现来重新定义setFrame:类中的setFrame:以便交换实现不会影响所有视图。 现有的实现是在NSView定义的实现。 在NSControl再次调用时会发生同样的事情(再次使用NSView实现)。

当你调用setFrame:在一个button上,它会调用你的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:)]; 

由于视图首先发生混合,控制混合将能够拉出正确的方法。 同样,由于控件混合是在button调配之前,button将拉起控件的setFrame setFrame: 这有点令人困惑,但这是正确的顺序。 我们怎样才能保证这个顺序呢?

再次,只是使用load来搅拌的东西。 如果你在load而只是对正在加载的类进行更改,那么你将是安全的。 load方法保证在任何子类之前调用​​超类加载方法。 我们会得到确切的顺序!

很难理解(看起来recursion)

看一个传统定义的混合方法,我认为很难说出是怎么回事。 但是看看我们上面所做的另一种方式,这很容易理解。 这个已经解决了!

难以debugging

debugging过程中的混乱之一是看到一个奇怪的回溯,混乱的名字混在一起,一切都混乱在你的脑海。 再一次,替代实现解决了这个问题。 你会在回溯中看到明确命名的函数。 尽pipe如此,搅拌可能很难debugging,因为很难记住搅拌的影响。 很好地logging你的代码(即使你认为你是唯一一个看到它的人)。 遵循良好的做法,你会没事的。 debugging比multithreading代码更难。

结论

如果使用正确的话,方法swizzling是安全的。 一个简单的安全措施,你可以采取的只是调整load 。 像编程中的许多事情一样,这可能是危险的,但理解后果将允许您正确使用它。


1使用上面定义的混搭方法,如果要使用蹦床,可以使线程安全。 你需要两个蹦床。 在方法开始的时候,你必须把函数指针store赋给一个函数,直到store指向的地址改变为止。 这可以避免在设置store函数指针之前调用调用方法的任何竞争条件。 如果实施还没有在课堂上定义的情况下,你需要使用一个蹦床,并有蹦床查找和正确调用超级方法。 定义方法以便dynamic查找超级实现将确保调用调用的顺序无关紧要。

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

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

方法swizzling比这更一般,但这是我感兴趣的情况。

危险:

  • 原class上的变化 。 我们不拥有我们正在调整的课程。 如果class级改变我们的swizzle可能会停止工作。

  • 很难维护 。 你不仅需要编写和维护混合的方法。 你必须编写和维护预调整的代码

  • 很难debugging 。 很难跟随stream动,有些人甚至可能不会意识到已经进行了swizzle。 如果从swizzle中引入错误(也许会导致原来的类别发生变化),它们将很难解决。

总之,你应该保持最低限度的调整,并考虑原来的class级变化如何影响你的调整。 你也应该清楚地评论和logging你在做什么(或者完全避免)。

这不是自相矛盾的,这真的很危险。 正如你所说,问题在于它经常用来修改框架类的行为。 假设你知道这些私人课程是如何工作的,这是“危险的”。 即使您今天的修改工作,苹果公司总是有机会在将来改变课程,并导致您的修改中断。 而且,如果有许多不同的应用程序可以实现这一点,那么苹果公司就很难在不破坏现有软件的情况下改变框架。

谨慎小心地使用,可以导致优雅的代码,但通常只会导致代码混淆。

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

例如,方法调配的一个很好的应用就是混合,这就是ObjC如何实现关键值观察。

一个不好的例子可能是依靠方法调整来扩展你的类,这会导致非常高的耦合度。

虽然我使用了这个技术,但我想指出:

  • 它混淆了你的代码,因为它可能会导致没有logging的,尽pipe所需的副作用。 当人们读取代码时,他/她可能不知道所需的副作用行为,除非他/她记得search代码库以查看是否已经被debugging过。 我不确定如何缓解这个问题,因为并不总是能够logging代码依赖于副作用混淆行为的每个地方。
  • 它可以让你的代码更less的可重用性,因为有人发现一段代码依赖于他们想在其他地方使用的混淆行为,不能简单地将其切割并粘贴到其他代码库中,而不会find并复制混搭的方法。

我觉得最大的危险是造成许多不必要的副作用,完全是偶然的。 这些副作用可能performance为“错误”,从而导致错误的pathfind解决scheme。 根据我的经验,危险是难以辨认的,令人困惑的,令人沮丧的代码。 有点像C ++中过度使用函数指针的时候。

在unit testing中,方法swizzling可以是非常有帮助的。

它允许你写一个模拟对象,并使用该模拟对象而不是真实的对象。 您的代码保持清洁,您的unit testing具有可预测的行为。 假设你想testing一些使用CLLocationManager的代码。 你的unit testing可以改变startUpdatingLocation,这样它就可以将一组预定的位置提供给你的代理,你的代码就不需要改变。

你可能会看到类似奇怪的代码

 - (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魔术。

Interesting Posts