我曾听人说过,这种方法是一种危险的做法。就连“swizzling”这个名字也暗示着这是一种欺骗。
方法狂饮正在修改映射,以便调用选择器A将实际调用实现b。这样做的一个用途是扩展闭源类的行为。
我们是否可以将风险正式化,以便决定是否使用swizzling的人可以做出明智的决定,是否值得他们尝试这样做。
如。
我觉得最大的危险是完全无意中产生许多不必要的副作用。这些副作用可能会以“bug”的形式出现,从而导致你走上错误的道路去寻找解决方案。根据我的经验,危险在于难以辨认、令人困惑和令人沮丧的代码。有点像有人在c++中过度使用函数指针。
真正危险的不是搅拌本身。正如您所说,问题在于它经常被用来修改框架类的行为。假设你知道这些私人课程是如何运作的,这是“危险的”。即使你今天的修改工作,苹果在未来总是有机会改变类,并导致你的修改失败。此外,如果许多不同的应用程序都这样做,那么苹果就很难在不破坏大量现有软件的情况下改变框架。
谨慎而明智地使用它,可以得到优雅的代码,但通常情况下,它只会导致令人困惑的代码。
我认为应该禁止它,除非您碰巧知道它为特定的设计任务提供了非常好的机会,但您需要清楚地知道为什么它很适合这种情况,以及为什么替代方案不能很好地适用于这种情况。
例如,方法swizzling的一个很好的应用是isa swizzling,这就是ObjC实现键值观察的方式。
一个糟糕的例子可能是依赖于方法变换作为扩展类的一种手段,这会导致极高的耦合。
首先,我将准确定义方法swizzling的含义:
方法搅拌比这更普遍,但这是我感兴趣的情况。
危险:
原类的变化。我们并不拥有我们正在搅拌的类。如果类改变,我们的搅拌可能会停止工作。
难以维持. .你不仅要编写和维护swizzled方法。您必须编写和维护进行混合的代码
难以调试。很难跟上搅拌的流程,有些人甚至没有意识到搅拌已经准备好了。如果从swizzle引入了错误(可能是由于原始类的更改),它们将很难解决。
总之,您应该将混合保持在最低限度,并考虑原始类的更改可能如何影响您的混合。此外,你应该清楚地评论和记录你正在做的事情(或者完全避免它)。
我认为这是一个非常好的问题,但遗憾的是,大多数答案并没有解决真正的问题,而是绕过了这个问题,只是简单地说不要使用swizzling。
使用方法滋滋作响就像在厨房里使用锋利的刀。有些人害怕锋利的刀,因为他们认为他们会割伤自己,但事实是……
方法变换可以用来编写更好、更高效、更可维护的代码。它也可能被滥用并导致可怕的bug。
与所有设计模式一样,如果我们充分意识到模式的后果,我们就能够就是否使用它做出更明智的决定。单身人士是一个很有争议的例子,而且有很好的理由。它们真的很难正确地执行。尽管如此,许多人仍然选择使用单例。这同样适用于搅拌。一旦你完全理解了好的和坏的,你就应该形成自己的观点。
下面是方法变换的一些陷阱:
这些观点都是有效的,在解决它们的过程中,我们可以提高对方法混合的理解,以及用于实现结果的方法。我一个一个来。
我还没有看到一个方法swizzling的实现是安全的,可以并发使用__abc3。在95%的情况下,这实际上不是问题,你想要使用方法搅拌。通常,您只是想替换方法的实现,并且希望在程序的整个生命周期中使用该实现。这意味着你应该在+(void)load中执行你的方法swizzling。load类方法在应用程序开始时串行执行。如果您在这里进行搅拌,就不会遇到任何并发性问题。然而,如果你要在+(void)initialize中进行swizzling,你可能会在你的swizzling实现中出现一个竞态条件,运行时可能会以一种奇怪的状态结束。
+(void)load
load
+(void)initialize
这是搅拌的一个问题,但这是整个问题的关键。我们的目标是能够改变这些代码。人们指出这是一个大问题的原因是,你不仅仅是在为你想要改变的NSButton的一个实例改变事情,而是在你的应用程序中所有的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:是在其他地方定义的,会发生什么?这个问题并不是搅拌所特有的,但我们可以解决它。该解决方案还有一个额外的好处,即解决了其他缺陷。我们是这样做的:
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:
_cmd
有一个简单的解决办法。使用上面定义的另一种搅拌技术。论点不变!
方法被打乱的顺序很重要。假设setFrame:只在NSView上定义,想象一下事情的顺序:
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实现)。
NSControl
当你在一个按钮上调用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中进行swizle,并且只对正在加载的类进行更改,那么你是安全的。load方法保证父类加载方法在任何子类之前被调用。我们会得到完全正确的顺序!
查看传统定义的swizzled方法,我认为很难判断发生了什么。但看看我们上面所做的另一种搅拌方式,就很容易理解了。这个问题已经解决了!
调试过程中的困惑之一是看到一个奇怪的回溯,其中混淆的名称和您头脑中的一切都很混乱。同样,替代实现解决了这个问题。您将在回溯中清楚地看到有名称的函数。尽管如此,调试混水还是很困难,因为很难记住混水会产生什么影响。很好地记录您的代码(即使您认为您是唯一会看到它的人)。遵循良好的实践,你会没事的。它并不比多线程代码更难调试。
如果使用得当,方法搅拌是安全的。你可以采取的一个简单的安全措施是只在load中搅拌。像编程中的许多事情一样,它可能是危险的,但了解其后果将使您能够正确地使用它。
1使用上面定义的swizzling方法,如果你要使用蹦床,你可以让事情线程安全。你需要两个蹦床。在方法开始时,必须将函数指针store赋值给一个函数,该函数旋转到store所指向的地址发生变化为止。这将避免任何竞态条件,即在能够设置store函数指针之前调用swizzled方法。然后,在类中还没有定义实现的情况下,需要使用蹦床,并进行蹦床查找并正确调用超类方法。将方法定义为动态查找超实现,将确保调用的顺序无关紧要。
store
虽然我使用了这种技巧,但我想指出:
方法变换在单元测试中非常有用。
它允许您编写一个模拟对象,并使用该模拟对象而不是实际对象。代码保持干净,单元测试具有可预测的行为。假设您想要测试一些使用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魔法有关。