我曾听人说过,这种方法是一种危险的做法。就连“swizzling”这个名字也暗示着这是一种欺骗。
方法Swizzling正在修改映射,以便调用选择器A将实际调用实现b。这样做的一个用途是扩展闭源类的行为。
我们是否可以将风险正式化,以便决定是否使用swizzling的人可以做出明智的决定,是否值得他们尝试这样做。
E.g.
命名冲突:如果类后来扩展其功能以包含您添加的方法名,将会导致大量的问题。通过合理地命名swizzled方法来降低风险。
我曾听人说过,这种方法是一种危险的做法。就连“swizzling”这个名字也暗示着这是一种欺骗。
方法Swizzling正在修改映射,以便调用选择器A将实际调用实现b。这样做的一个用途是扩展闭源类的行为。
我们是否可以将风险正式化,以便决定是否使用swizzling的人可以做出明智的决定,是否值得他们尝试这样做。
E.g.
命名冲突:如果类后来扩展其功能以包含您添加的方法名,将会导致大量的问题。通过合理地命名swizzled方法来降低风险。
我觉得最大的危险是完全无意中产生许多不必要的副作用。这些副作用可能会以“bug”的形式出现,从而导致你走上错误的道路去寻找解决方案。根据我的经验,危险在于难以辨认、令人困惑和令人沮丧的代码。有点像有人在c++中过度使用函数指针。
It's not the swizzling itself that's really dangerous. The problem is, as you say, that it's often used to modify the behavior of framework classes. It's assuming that you know something about how those private classes work that's "dangerous." Even if your modifications work today, there's always a chance that Apple will change the class in the future and cause your modification to break. Also, if many different apps do it, it makes it that much harder for Apple to change the framework without breaking a lot of existing software.
谨慎而明智地使用它,可以得到优雅的代码,但通常情况下,它只会导致令人困惑的代码。
我认为应该禁止它,除非您碰巧知道它为特定的设计任务提供了非常好的机会,但您需要清楚地知道为什么它很适合这种情况,以及为什么替代方案不能很好地适用于这种情况。
例如,方法swizzling的一个很好的应用是isa swizzling,这就是ObjC实现键值观察的方式。
一个糟糕的例子可能是依赖于方法变换作为扩展类的一种手段,这会导致极高的耦合。
首先,我将准确定义方法swizzling的含义:
将最初发送到方法(称为a)的所有调用重新路由到新方法(称为B)。 我们拥有方法B 我们没有A方法 方法B做了一些工作,然后调用方法A。
方法搅拌比这更普遍,但这是我感兴趣的情况。
危险:
Changes in the original class. We dont own the class that we are swizzling. If the class changes our swizzle may stop working. Hard to maintain. Not only have you got to write and maintain the swizzled method. you have to write and maintain the code that preforms the swizzle Hard to debug. It is hard to follow the flow of a swizzle, some people may not even realise the swizzle has been preformed. If there are bugs introduced from the swizzle (perhaps dues to changes in the original class) they will be hard to resolve.
总之,您应该将混合保持在最低限度,并考虑原始类的更改可能如何影响您的混合。此外,你应该清楚地评论和记录你正在做的事情(或者完全避免它)。
我认为这是一个非常好的问题,但遗憾的是,大多数答案并没有解决真正的问题,而是绕过了这个问题,只是简单地说不要使用swizzling。
使用方法滋滋作响就像在厨房里使用锋利的刀。有些人害怕锋利的刀,因为他们认为他们会割伤自己,但事实是,锋利的刀更安全。
方法变换可以用来编写更好、更高效、更可维护的代码。它也可能被滥用并导致可怕的bug。
背景
与所有设计模式一样,如果我们充分意识到模式的后果,我们就能够就是否使用它做出更明智的决定。单例是一个很有争议的例子,而且有很好的理由——它们真的很难正确地实现。尽管如此,许多人仍然选择使用单例。这同样适用于搅拌。一旦你完全理解了好的和坏的,你就应该形成自己的观点。
讨论
下面是方法变换的一些陷阱:
方法搅拌不是原子性的 更改非拥有代码的行为 可能的命名冲突 Swizzling改变方法的参数 搅拌的顺序很重要 难以理解(看起来是递归的) 调试困难
这些观点都是有效的,在解决它们的过程中,我们可以提高对方法混合的理解,以及用于实现结果的方法。我一个一个来。
方法搅拌不是原子性的
I have yet to see an implementation of method swizzling that is safe to use concurrently1. This is actually not a problem in 95% of cases that you'd want to use method swizzling. Usually, you simply want to replace the implementation of a method, and you want that implementation to be used for the entire lifetime of your program. This means that you should do your method swizzling in +(void)load. The load class method is executed serially at the start of your application. You won't have any issues with concurrency if you do your swizzling here. If you were to swizzle in +(void)initialize, however, you could end up with a race condition in your swizzling implementation and the runtime could end up in a weird state.
更改非拥有代码的行为
这是搅拌的一个问题,但这是整个问题的关键。我们的目标是能够改变这些代码。人们指出这是个大问题的原因是因为你不仅仅是在为你想要改变的一个NSButton实例改变东西,而是在你的应用程序中所有的NSButton实例。出于这个原因,你在搅拌时应该小心,但你不需要完全避免它。
这样想吧……如果重写类中的方法而不调用超类方法,则可能会引起问题。在大多数情况下,超类期望调用该方法(除非另有文档)。如果您将同样的想法应用于搅拌,那么您已经涵盖了大多数问题。始终调用原始实现。如果你不这样做,你可能改变太多而不安全。
可能的命名冲突
命名冲突是贯穿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(因为它使用函数指针),但它避免了任何命名冲突。原则上,这和标准的搅拌是一样的。对于那些已经使用了一段时间的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上的方法被swizzled时会发生什么?大多数swizzling将确保它不会替换setFrame:对于所有视图的实现,因此它将拉出实例方法。这将使用现有的实现在NSButton类中重新定义setFrame:,以便交换实现不会影响所有视图。现有的实现是在NSView中定义的。同样的事情也会发生在NSControl上(再次使用NSView实现)。
当你在一个按钮上调用setFrame:时,它会因此调用你的swizzled方法,然后直接跳转到最初在NSView中定义的setFrame:方法。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方法将在任何子类之前被调用。我们会得到完全正确的顺序!
难以理解(看起来是递归的)
查看传统定义的swizzled方法,我认为很难判断发生了什么。但看看我们上面所做的另一种搅拌方式,就很容易理解了。这个问题已经解决了!
调试困难
One of the confusions during debugging is seeing a strange backtrace where the swizzled names are mixed up and everything gets jumbled in your head. Again, the alternative implementation addresses this. You'll see clearly named functions in backtraces. Still, swizzling can be difficult to debug because it's hard to remember what impact the swizzling is having. Document your code well (even if you think you're the only one who will ever see it). Follow good practices, and you'll be alright. It's not harder to debug than multi-threaded code.
结论
如果使用得当,方法搅拌是安全的。你可以采取的一个简单的安全措施是只在负载时搅拌。像编程中的许多事情一样,它可能是危险的,但了解其后果将使您能够正确地使用它。
1 Using the above defined swizzling method, you could make things thread safe if you were to use trampolines. You would need two trampolines. At the start of the method, you would have to assign the function pointer, store, to a function that spun until the address to which store pointed to changed. This would avoid any race condition in which the swizzled method was called before you were able to set the store function pointer. You would then need to use a trampoline in the case where the implementation isn't already defined in the class and have the trampoline lookup and call the super class method properly. Defining the method so it dynamically looks up the super implementation will ensure that the order of swizzling calls does not matter.
虽然我使用了这种技巧,但我想指出:
It obfuscates your code because it can cause un-documented, though desired, side effects. When one reads the code he/she may be unaware of the side effect behavior that is required unless he/she remembers to search the code base to see if it has been swizzled. I'm not sure how to alleviate this problem because it is not always possible to document every place where the code is dependent upon the side effect swizzled behavior. It can make your code less reusable because someone who finds a segment of code which depends upon the swizzled behavior that they would like to use elsewhere cannot simply cut and paste it into some other code base without also finding and copying the swizzled method.
方法变换在单元测试中非常有用。
它允许您编写一个模拟对象,并使用该模拟对象而不是实际对象。代码保持干净,单元测试具有可预测的行为。假设您想要测试一些使用CLLocationManager的代码。你的单元测试可以混合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魔法有关。