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


235

我听说人们说方法混乱是一种危险的做法。甚至连起草的名字都暗示这有点作弊。

方法Swizzling正在修改映射,以便调用选择器A实际上将调用实现B。此方法的一种用法是扩展封闭源类的行为。

我们是否可以将风险正式化,以便任何决定是否使用毛毛雨的人都可以做出明智的决定,以决定这样做是否值得。

例如

  • 命名冲突:如果该类以后扩展其功能以包括您添加的方法名称,则将引起大量问题。通过合理命名混淆方法来降低风险。

它已经不是很好,让你从代码指望你读过别的东西
马丁Babacaev

这听起来像是一种不干净的方式来做python装饰器做得很干净的方式
Claudiu 2013年

Answers:


439

我认为这是一个非常好的问题,很遗憾,大多数答案都没有解决实际问题,而是大多数回答都绕过了这个问题,并简单地说不要使用麻烦。

使用铁板烧的方法就像在厨房里用锋利的刀。有些人害怕锋利的刀,因为他们认为自己会严重割伤自己,但事实是锋利的刀更安全

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

背景

与所有设计模式一样,如果我们完全了解该模式的后果,则可以就是否使用它做出更明智的决定。单例是一个很有争议的很好的例子,并且有充分的理由-它们确实很难正确实现。但是,许多人仍然选择使用单例。关于下垂也可以这样说。一旦完全了解好与坏,就应该形成自己的见解。

讨论区

这是方法混乱的一些陷阱:

  • 方法混乱不是原子的
  • 更改未拥有代码的行为
  • 可能的命名冲突
  • 混乱更改方法的参数
  • 麻烦的顺序很重要
  • 难以理解(看起来是递归的)
  • 调试困难

这些要点都是有效的,在解决这些问题时,我们可以提高我们对方法混乱的理解以及用于获得结果的方法的理解。我一次接一个。

方法混乱不是原子的

我还没有看到可以安全地同时使用方法swizzling的实现1。实际上,在95%的情况下,您都不希望使用方法混乱。通常,您只想替换一个方法的实现,并且希望在程序的整个生命周期中都使用该实现。这意味着您应该使方法陷入混乱+(void)load。将load在您的应用程序的启动串行执行类方法。如果您在此处进行复杂处理,那么并发不会有任何问题。如果你要陷入困境+(void)initialize但是,,则可能会在混乱的实现中遇到竞争条件,并且运行时可能会陷入怪异的状态。

更改未拥有代码的行为

这是一个令人毛骨悚然的问题,但这很重要。目标是能够更改该代码。人们指出这很重要的原因是NSButton,您不仅要更改要为其更改内容的一个实例,还需要更改NSButton应用程序中的所有实例。出于这个原因,您在打喷嚏时应格外小心,但不必完全避免。

这样想吧...如果您在类中重写了一个方法而没有调用超类方法,则可能会引起问题。在大多数情况下,超类期望该方法被调用(除非另有说明)。如果您将同样的想法应用到问题上,则涵盖了大多数问题。始终调用原始实现。如果您不这样做,则可能是为了安全起见,所做的更改太多。

可能的命名冲突

命名冲突是整个可可中的一个问题。我们经常在类别中为类名和方法名加上前缀。不幸的是,命名冲突在我们的语言中是一个困扰。但是,如果出现混乱,则不必如此。我们只需要改变对方法略微混乱的思考方式即可。大部分的处理都是这样的:

@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失效时会发生什么?很好的解决方法是确保不会替换setFrame:所有视图的实现,因此它将拉起实例方法。这将使用现有的实现setFrame:NSButton类中重新定义,以便交换实现不会影响所有视图。现有的实现是在上定义的NSView。当NSControl继续使用时(再次使用NSView实现),将会发生相同的事情。

因此,当您调用setFrame:按钮时,它将调用您的混乱方法,然后直接跳至setFrame:最初在上定义的方法NSView。在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而仅对正在加载的类进行更改,那将是安全的。该load方法保证将在任何子类之前调用​​超类加载方法。我们将获得正确的订单!

难以理解(看起来是递归的)

观察传统定义的混淆方法,我认为很难说出正在发生什么。但是,看看我们在上面做过的另一种选择,这很容易理解。这个已经解决了!

调试困难

调试过程中的困惑之一是看到一个奇怪的回溯,在回溯中,混乱的名字混在一起,一切都变得混乱。同样,替代实现解决了这个问题。您将在回溯中看到名称明确的函数。仍然很难调试,因为很难记住调试产生的影响。很好地记录代码(即使您认为自己是唯一会看到它的人)。遵循良好做法,您会没事的。调试不比多线程代码难。

结论

如果使用正确,方法冲洗是安全的。您可以采取的一种简单的安全措施是只进入load。像编程中的许多事情一样,它可能很危险,但是了解其后果将使您能够正确使用它。


1如果使用蹦床,可以使用上述定义的旋转方法使线程安全。您将需要两个蹦床。在方法开始时,您必须将函数指针分配给store旋转的函数,直到store指向的地址更改为止。这样可以避免在设置store功能指针之前调用swizzled方法的任何竞争情况。然后,如果尚未在类中定义实现,则需要使用蹦床,并需要进行蹦床查找并正确调用超类方法。定义该方法,以便它动态查找超级实现,将确保无效调用的顺序无关紧要。


24
内容丰富且技术上合理的答案。讨论非常有趣。感谢您抽出宝贵的时间写这篇文章。
里卡多·桑切斯·塞兹

您能否指出应用商店中出现的烦恼是可以接受的。我还将您也定向到stackoverflow.com/questions/8834294
Dickey Singh

3
Swizzling可以在App Store上使用。许多应用程序和框架都可以(包括我们的)。上面的所有内容仍然有效,您不能随意使用私有方法(实际上,从技术上讲可以,但是您可能会面临拒绝的风险,并且这样做的危险在于使用其他线程)。
wbyoung 2013年

惊人的答案。但是,为什么要在class_swizzleMethodAndStore()中而不是直接在+ swizzle:with:store:中进行调整呢?为什么要增加此功能?
Frizlab 2014年

2
@Frizlab好问题!这实际上只是一种风格。如果您正在编写一堆直接与Objective-C运行时API(在C语言中)一起工作的代码,那么能够以C样式实现一致性很不错。除此以外,我能想到的唯一原因是,如果您使用纯C语言编写了某些内容,那么它仍然可以被调用。但是,没有理由不能用Objective-C方法来完成所有这些工作。
wbyoung 2014年

12

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

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

方法混淆比这更笼统,但这是我感兴趣的情况。

危险:

  • 原班的变化。我们没有自己正在上课的班级。如果班级改变,我们的麻烦可能会停止。

  • 难以维护。您不仅需要编写和维护复杂的方法。您必须编写和维护执行麻烦的代码

  • 难以调试。很难跟踪麻烦的发生,有些人甚至可能没有意识到已经进行了麻烦。如果有麻烦带来的错误(可能是由于原始类的更改所致),将很难解决。

总而言之,您应该尽量减少麻烦,并考虑原始类的更改如何影响您的麻烦。另外,您应该清楚地评论和记录您的工作(或完全避免这样做)。


@每个人,我都将您的答案组合成一个不错的格式。随时进行编辑/添加。感谢您的输入。
罗伯特

我是你的心理学迷。“强大的领导能力带来了巨大的领导责任。”
杰克逊(Jacksonkr)

7

真正危险的不是漩涡本身。正如您所说,问题是它通常用于修改框架类的行为。假设您对那些“危险”的私有类如何工作有所了解。即使您的修改在今天起作用,Apple仍然有可能会在将来更改类,并导致您的修改失败。同样,如果有许多不同的应用程序执行此操作,则在不破坏大量现有软件的情况下,Apple很难更改框架。


我想说这样的开发人员应该收获他们的收获-他们将代码紧密耦合到特定的实现。因此,如果实现方式以一种微妙的方式更改,而这不是他们的明确决定像他们那样紧紧地耦合代码,则不应破坏代码,这是他们的错。
Arafangion 2011年

可以,但是可以说,一个非常受欢迎的应用程序会在下一版iOS下崩溃。可以很容易地说这是开发人员的错,他们应该知道得更多,但是这会使Apple在用户眼中看起来很糟糕。如果您愿意的话,我认为混淆自己的方法甚至类没有任何危害,除了您可能会使代码有些混乱之外。当您混淆由其他人控制的代码时,它的风险就会开始增加一些,这正是混淆似乎最诱人的情况。
Caleb

Apple不是唯一可以通过siwzzling进行修改的基类提供程序。您的答案应该进行编辑以包含所有人。
AR

@AR也许,但是这个问题被标记为iOS和iPhone,所以我们应该在这种情况下考虑它。鉴于iOS应用程序必须静态链接除iOS提供的库和框架以外的任何库和框架,因此Apple实际上是唯一可以更改框架类的实现而无需应用程序开发人员参与的实体。但是,我确实同意类似问题会影响其他平台的观点,并且我想说,建议也可以泛滥成灾。一般而言,建议是:双手握住自己。避免修改其他人维护的组件。
Caleb

方法混乱可能会很危险。但是随着框架的出现,它们并没有完全省略先前的框架。您仍然必须更新应用程序期望的基本框架。该更新将破坏您的应用。在更新ios时,它可能不会成为一个大问题。我的使用模式是覆盖默认的redirectIdentifier。因为我无法访问_reuseIdentifier变量,并且不想替换它,除非未提供该变量,所以我唯一的解决方案是对每个变量进行子类化并提供重用标识符,或者如果没有,则覆盖并重写。实现类型是关键...
The Lazy Coder 2012年

5

谨慎而明智地使用它可以生成优美的代码,但是通常,它只会导致混乱的代码。

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

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

一个不好的例子可能是依靠方法混乱作为扩展类的一种方法,这导致了极高的耦合。


1
对于方法调用,在KVO提及时为-1 。伊萨(Isa)泛滥只是另外一回事。
Nikolai Ruhe 2013年

@NikolaiRuhe:请随时提供参考,以便给我启发。
Arafangion

这是对KVO实现细节的参考。方法混淆在CocoaDev中进行了描述。
Nikolai Ruhe 2013年

5

尽管我使用了这种技术,但我想指出:

  • 它会使您的代码模糊不清,因为它可能会导致未记录的(尽管有希望的)副作用。当人们阅读代码时,他/她可能不知道所需要的副作用,除非他/她记得搜索代码库以查看其是否被混淆。我不确定如何减轻此问题,因为并非总是可以记录每个代码依赖于副作用混乱行为的地方。
  • 这样做会使代码的可重用性降低,因为找到要依赖于他们在其他地方使用的混乱行为的代码段的人不能简单地将其剪切并粘贴到其他代码库中,而无需也找到并复制该混乱方法。

4

我觉得最大的危险是完全无意中产生了许多不良副作用。这些副作用可能会以“错误”的形式出现,从而导致您走错了道路,无法找到解决方案。以我的经验,这种危险难以辨认,令人困惑且令人沮丧。有点像有人过度使用C ++中的函数指针。


好的,所以问题在于它很难调试。造成此问题的原因是,审阅者没有意识到/忘记了代码已经混乱,并且在调试时在错误的位置寻找它们。
罗伯特

3
是的,差不多。另外,当使用过度时,您会陷入无尽的连锁或混乱的网络。想象一下,当您在将来某个时候只更改其中之一时会发生什么?我利肯的经验堆叠茶杯或打“码积木”
AR

4

您可能最终得到看起来很奇怪的代码,例如

- (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魔术有关的实际生产代码。


3

方法混淆可能对单元测试很有帮助。

它允许您编写一个模拟对象,并使用该模拟对象代替实际对象。您的代码保持整洁,并且单元测试具有可预测的行为。假设您要测试一些使用CLLocationManager的代码。您的单元测试可能会使startUpdatingLocation混乱,因此它将给您的委托提供一组预定的位置,并且您的代码不必更改。


isa-swizzling将是一个更好的选择。
vikingosegundo
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.