目标C中的方法摇摆的危险是什么?
我听说有人说这种方法是一种危险的做法。 甚至连这个名字也不知道这是一个骗子。
方法Swizzling正在修改映射,以便调用select器A实际上会调用实现B.这样做的一个用途是扩展闭源类的行为。
我们是否可以将风险正式化,以便任何决定是否使用混搭的人都能够做出明智的决定,是否值得为自己的努力做点什么。
例如
- 命名冲突 :如果该类稍后扩展其function以包含已添加的方法名称,则会导致大量问题。 通过明智地命名混杂的方法来降低风险。
我认为这是一个非常好的问题,而不是解决真正的问题,大部分答案都是围绕着这个问题的,而不是使用虚拟的。
使用方法嘶嘶作响就像在厨房使用锋利的刀。 有些人害怕刀锋,因为他们认为自己会严重削减自己,但事实是刀锋更安全 。
方法swizzling可以用来编写更好,更高效,更可维护的代码。 它也可能被滥用,并导致可怕的错误。
背景
就像所有的devise模式一样,如果我们充分意识到模式的后果,我们就能够做出更明智的决定是否使用模式。 单身人士是很有争议的一个很好的例子,有很好的理由 – 他们真的很难正确实施。 不过,许多人仍然select使用单身。 同样可以说关于swizzling。 一旦你完全了解好坏,你应该形成自己的观点。
讨论
以下是方法调整的一些缺陷:
- 方法swizzling不是primefaces的
- 更改非拥有代码的行为
- 可能的命名冲突
- Swizzling改变了方法的参数
- swizzles的顺序很重要
- 很难理解(看起来recursion)
- 难以debugging
这些观点都是有效的,在解决这些问题时,我们可以提高对方法调整的理解以及用于实现结果的方法。 我会一次拿走每一个。
方法swizzling不是primefaces的
我还没有看到可以同时安全使用的方法debugging的实现1 。 在95%的情况下,这实际上不是一个问题,您想使用方法调整。 通常,您只需要replace方法的实现,并且希望在您的程序的整个生命周期中使用该实现。 这意味着你应该做你的方法在+(void)load
。 load
类方法在应用程序的开始处连续执行。 如果您在这里进行调整,您将不会遇到任何并发问题。 但是,如果你在+(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:
方法。 NSControl
和NSView
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魔术。