Objective C中方法调配的危险是什么?

Rob*_*ert 232 objective-c swizzling ios

我听说有人说方法调整是一种危险的做法.即使是名字调整也会让人觉得这有点像作弊.

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

我们能否将风险正式化,以便任何决定是否使用调酒的人都可以做出明智的决定是否值得他们尝试做什么.

例如

  • 命名冲突:如果该类稍后扩展其功能以包括您添加的方法名称,则会导致大量问题.通过明智地命名混合方法来降低风险.

wby*_*ung 433

我认为这是一个非常好的问题,令人遗憾的是,大多数答案都没有解决这个问题,而是简单地说不要使用调酒问题.

使用方法嘶嘶声就像在厨房里使用锋利的刀具.有些人害怕锋利的刀,因为他们认为他们会严重削减自己,但事实是锋利的刀更安全.

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

背景

与所有设计模式一样,如果我们完全了解模式的后果,我们就能否使用它做出更明智的决策.单身人士是一个很有争议的事情的好例子,并且有充分的理由 - 他们真的很难正确实施.不过,很多人仍然选择使用单身人士.关于调配也是如此.一旦你完全理解了好的和坏的,你应该形成自己的意见.

讨论

以下是方法调配的一些缺陷:

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

这些要点都是有效的,在解决这些问题时,我们可以改进对方法调整的理解以及用于实现结果的方法.我会一次拿走每一个.

方法调配不是原子的

我还没有看到方法调配的实现,可以安全地同时使用1.在95%的情况下,你想要使用方法调配,这实际上不是问题.通常,您只是想替换方法的实现,并且希望在程序的整个生命周期中使用该实现.这意味着你应该调整你的方法+(void)load.将load在您的应用程序的启动串行执行类方法.如果你在这里进行调整,你将不会遇到任何并发问题.+(void)initialize但是,如果你是混淆的话,你可能会在你的混合实现中遇到竞争条件,并且运行时可能会以一种奇怪的状态结束.

更改非拥有代码的行为

这是一个混合的问题,但这是重点.目标是能够更改该代码.人们认为这是一个大问题的原因是因为你不只是改变你想要改变的东西的一个实例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
Run Code Online (Sandbox Code Playgroud)

这很好用,但如果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
Run Code Online (Sandbox Code Playgroud)

虽然这看起来不像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
Run Code Online (Sandbox Code Playgroud)

通过重命名方法进行调配会改变方法的参数

这是我脑海中最重要的一个.这就是不应该进行标准方法调配的原因.您正在更改传递给原始方法实现的参数.这就是它发生的地方:

[self my_setFrame:frame];
Run Code Online (Sandbox Code Playgroud)

这条线的作用是:

objc_msgSend(self, @selector(my_setFrame:), frame);
Run Code Online (Sandbox Code Playgroud)

哪个将使用运行时查找实现my_setFrame:.一旦找到实现,它就会使用给定的相同参数调用实现.它找到的实现是原始的实现setFrame:,所以它继续并调用它,但是_cmd参数不是setFrame:应该的.它现在my_setFrame:.原始实现正在调用它从未预期会收到的参数.这不好.

有一个简单的解决方案 - 使用上面定义的替代调配技术.论点将保持不变!

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:)];
Run Code Online (Sandbox Code Playgroud)

当方法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:)];
Run Code Online (Sandbox Code Playgroud)

由于视图调整首先发生,控件调整将能够拉出正确的方法.同样地,由于控制旋转是在按钮旋转之前,按钮将拉动控件的混合实现setFrame:.这有点令人困惑,但这是正确的顺序.我们怎样才能确保这种秩序?

再一次,只是load用来调配东西.如果你进入load并且只对正在加载的类进行更改,那么你将是安全的.该load方法保证在任何子类之前调用​​超类加载方法.我们会得到准确的订单!

难以理解(看起来递归)

看一下传统定义的混合方法,我认为很难说出发生了什么.但是看看我们已经完成的替代方式,它很容易理解.这个已经解决了!

很难调试

在调试过程中遇到的一个困惑是看到一个奇怪的回溯,其中混乱的名字混在一起,一切都在脑海中混乱.同样,替代实现解决了这个问题.你会在回溯中看到明确命名的函数.尽管如此,调配仍然很难调试,因为很难记住调酒的影响.记录你的代码(即使你认为你是唯一一个会看到它的人).遵循良好的做法,你会没事的.调试并不比多线程代码困难.

结论

如果使用得当,方法调配是安全的.您可以采取的一个简单的安全措施是只能调整load.像编程中的许多东西一样,它可能很危险,但了解后果将允许您正确使用它.


1使用上面定义的混合方法,如果你使用蹦床,你可以使线程安全.你需要两个蹦床.在方法开始时,您必须将函数指针分配store给一个旋转的函数,直到store指向更改的地址.这样可以避免在能够设置store函数指针之前调用swizzled方法的任何竞争条件.然后,如果类中尚未定义实现并且具有trampoline查找并正确调用超类方法,则需要使用trampoline.定义方法以便动态查找超级实现将确保调配调用的顺序无关紧要.

  • 惊人的信息和技术上合理的答案.讨论非常有趣.感谢您抽出宝贵时间来写这篇文章. (22认同)
  • Swizzling可以在App商店中使用.许多应用程序和框架都做(包括我们的).上面的所有东西仍然存在,你不能混淆私人方法(实际上,你在技术上可以,但你会冒被拒绝的风险,这是针对不同的线程的危险). (3认同)
  • @Frizlab好问题!这真的只是一种风格.如果您正在编写一堆直接使用Objective-C运行时API(在C中)的代码,那么能够将其称为C样式以保持一致性是很好的.除此之外我能想到的唯一原因是如果你用纯C写一些东西,那么它仍然可以调用.但是,没有理由你不能在Objective-C方法中完成所有这些工作. (2认同)

Rob*_*ert 11

首先,我将通过方法调配确切地定义我的意思:

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

方法调配比这更普遍,但这是我感兴趣的情况.

危险:

  • 原班级的变化.我们不拥有我们调情的课程.如果班级改变我们的混战可能会停止工作.

  • 很难维护.你不仅要编写和维护混合方法.你必须编写和维护预先形成混合的代码

  • 很难调试.很难跟随混合的流动,有些人甚至可能没有意识到已经进行了混合.如果从混合中引入了错误(可能是原始类中的变化),它们将很难解决.

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


Cal*_*leb 7

真正危险的不是调酒本身.正如您所说,问题是它经常用于修改框架类的行为.假设你知道那些私人课程是如何运作的,这是"危险的".即使您今天的修改有效,Apple也有可能在将来更改课程并导致您的修改中断.此外,如果有许多不同的应用程序,它会让Apple更难以在不破坏大量现有软件的情况下更改框架.


Ara*_*ion 5

仔细而明智地使用,它可以导致优雅的代码,但通常,它只会导致令人困惑的代码.

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

例如,方法调配的一个很好的应用是混合,这是ObjC实现Key Value Observing的方式.

一个糟糕的例子可能是依靠方法调配作为扩展类的方法,这导致极高的耦合.


use*_*724 5

虽然我使用过这种技术,但我想指出:

  • 它会混淆您的代码,因为它可能会导致未记录的,但需要的副作用.当一个人读取代码时,他/她可能不知道所需的副作用行为,除非他/她记得搜索代码库以查看它是否已被调整.我不确定如何缓解这个问题,因为并不总是能够记录代码依赖于副作用混合行为的每个地方.
  • 它可以使您的代码更少可重用,因为找到一段代码依赖于他们想在其他地方使用的混合行为的人不能简单地将其剪切并粘贴到其他代码库中,而无需查找和复制混合方法.