Objective-C中方法混合的危险是什么?

我曾听人说过,这种方法是一种危险的做法。就连“swizzling”这个名字也暗示着这是一种欺骗。

方法狂饮正在修改映射,以便调用选择器A将实际调用实现b。这样做的一个用途是扩展闭源类的行为。

我们是否可以将风险正式化,以便决定是否使用swizzling的人可以做出明智的决定,是否值得他们尝试这样做。

如。

  • 命名冲突:如果类后来扩展了它的功能来包含你添加的方法名,它将导致大量的问题。通过合理地命名swizzled方法来降低风险。
41608 次浏览

我觉得最大的危险是完全无意中产生许多不必要的副作用。这些副作用可能会以“bug”的形式出现,从而导致你走上错误的道路去寻找解决方案。根据我的经验,危险在于难以辨认、令人困惑和令人沮丧的代码。有点像有人在c++中过度使用函数指针。

真正危险的不是搅拌本身。正如您所说,问题在于它经常被用来修改框架类的行为。假设你知道这些私人课程是如何运作的,这是“危险的”。即使你今天的修改工作,苹果在未来总是有机会改变类,并导致你的修改失败。此外,如果许多不同的应用程序都这样做,那么苹果就很难在不破坏大量现有软件的情况下改变框架。

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

我认为应该禁止它,除非您碰巧知道它为特定的设计任务提供了非常好的机会,但您需要清楚地知道为什么它很适合这种情况,以及为什么替代方案不能很好地适用于这种情况。

例如,方法swizzling的一个很好的应用是isa swizzling,这就是ObjC实现键值观察的方式。

一个糟糕的例子可能是依赖于方法变换作为扩展类的一种手段,这会导致极高的耦合。

首先,我将准确定义方法swizzling的含义:

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

方法搅拌比这更普遍,但这是我感兴趣的情况。

危险:

  • 原类的变化。我们并不拥有我们正在搅拌的类。如果类改变,我们的搅拌可能会停止工作。

  • 难以维持. .你不仅要编写和维护swizzled方法。您必须编写和维护进行混合的代码

  • 难以调试。很难跟上搅拌的流程,有些人甚至没有意识到搅拌已经准备好了。如果从swizzle引入了错误(可能是由于原始类的更改),它们将很难解决。

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

我认为这是一个非常好的问题,但遗憾的是,大多数答案并没有解决真正的问题,而是绕过了这个问题,只是简单地说不要使用swizzling。

使用方法滋滋作响就像在厨房里使用锋利的刀。有些人害怕锋利的刀,因为他们认为他们会割伤自己,但事实是……

方法变换可以用来编写更好、更高效、更可维护的代码。它也可能被滥用并导致可怕的bug。

背景

与所有设计模式一样,如果我们充分意识到模式的后果,我们就能够就是否使用它做出更明智的决定。单身人士是一个很有争议的例子,而且有很好的理由。它们真的很难正确地执行。尽管如此,许多人仍然选择使用单例。这同样适用于搅拌。一旦你完全理解了好的和坏的,你就应该形成自己的观点。

讨论

下面是方法变换的一些陷阱:

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

这些观点都是有效的,在解决它们的过程中,我们可以提高对方法混合的理解,以及用于实现结果的方法。我一个一个来。

方法搅拌不是原子性的

我还没有看到一个方法swizzling的实现是安全的,可以并发使用__abc3。在95%的情况下,这实际上不是问题,你想要使用方法搅拌。通常,您只是想替换方法的实现,并且希望在程序的整个生命周期中使用该实现。这意味着你应该在+(void)load中执行你的方法swizzling。load类方法在应用程序开始时串行执行。如果您在这里进行搅拌,就不会遇到任何并发性问题。然而,如果你要在+(void)initialize中进行swizzling,你可能会在你的swizzling实现中出现一个竞态条件,运行时可能会以一种奇怪的状态结束。

更改非拥有代码的行为

这是搅拌的一个问题,但这是整个问题的关键。我们的目标是能够改变这些代码。人们指出这是一个大问题的原因是,你不仅仅是在为你想要改变的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上的方法被打乱时会发生什么?大多数swizzling将确保它不会替换所有视图的setFrame:的实现,因此它将拉起实例方法。这将使用现有的实现在NSButton类中重新定义setFrame:,以便交换实现不会影响所有视图。现有的实现是在NSView上定义的实现。同样的事情也会发生在NSControl上(同样使用NSView实现)。

当你在一个按钮上调用setFrame:时,它会因此调用你的swizzled方法,然后直接跳转到最初在NSView上定义的setFrame:方法。NSControlNSView混合实现将不会被调用。

但如果顺序是:

[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方法。然后,在类中还没有定义实现的情况下,需要使用蹦床,并进行蹦床查找并正确调用超类方法。将方法定义为动态查找超实现,将确保调用的顺序无关紧要。

虽然我使用了这种技巧,但我想指出:

  • 它会混淆您的代码,因为它可能会导致未记录的副作用(尽管希望如此)。当一个人阅读代码时,他/她可能不知道需要的副作用行为,除非他/她记得搜索代码库,看看它是否被混淆了。我不确定如何缓解这个问题,因为不可能总是记录代码依赖于副作用的每一个地方。
  • 它会降低代码的可重用性,因为如果有人发现一段依赖于swizzled行为的代码,并且希望在其他地方使用,那么如果不找到并复制swizzled方法,就不能简单地将其剪切并粘贴到其他代码库中。

方法变换在单元测试中非常有用。

它允许您编写一个模拟对象,并使用该模拟对象而不是实际对象。代码保持干净,单元测试具有可预测的行为。假设您想要测试一些使用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魔法有关。