Objective-C 中方法混用的危险是什么? [英] What are the Dangers of Method Swizzling in Objective-C?

查看:365
本文介绍了Objective-C 中方法混用的危险是什么?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我听说人们说方法混合是一种危险的做法.甚至 swizzling 这个名字都暗示它有点作弊.

I have heard people state that method swizzling is a dangerous practice. Even the name swizzling suggests that it is a bit of a cheat.

Method Swizzling 正在修改映射这样调用选择器 A 将实际调用实现 B.它的一种用途是扩展封闭源类的行为.

Method Swizzling is modifying the mapping so that calling selector A will actually invoke implementation B. One use of this is to extend behavior of closed source classes.

我们能否将风险形式化,以便决定是否使用 swizzling 的任何人都可以做出明智的决定,是否值得他们尝试做的事情.

Can we formalise the risks so that anyone who is deciding whether to use swizzling can make an informed decision whether it is worth it for what they are trying to do.

例如

  • 命名冲突:如果类稍后扩展其功能以包含您添加的方法名称,则会导致大量问题.通过合理命名混合方法来降低风险.
  • Naming Collisions: If the class later extends its functionality to include the method name that you have added, it will cause a huge manner of problems. Reduce the risk by sensibly naming swizzled methods.

推荐答案

我认为这是一个非常棒的问题,可惜大多数答案没有解决真正的问题,而是绕开了这个问题,只是说不要使用混搭.

I think this is a really great question, and it's a shame that rather than tackling the real question, most answers have skirted the issue and simply said not to use swizzling.

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

Using method sizzling is like using sharp knives in the kitchen. Some people are scared of sharp knives because they think they'll cut themselves badly, but the truth is that sharp knives are safer.

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

Method swizzling can be used to write better, more efficient, more maintainable code. It can also be abused and lead to horrible bugs.

与所有设计模式一样,如果我们完全了解该模式的后果,我们就能够就是否使用它做出更明智的决定.单身人士是颇具争议的事物的一个很好的例子,而且有充分的理由——它们真的很难正确实施.不过,许多人仍然选择使用单例.关于调酒也可以这样说.一旦你充分了解了好与坏,你应该形成自己的观点.

As with all design patterns, if we are fully aware of the consequences of the pattern, we are able to make more informed decisions about whether or not to use it. Singletons are a good example of something that's pretty controversial, and for good reason — they're really hard to implement properly. Many people still choose to use singletons, though. The same can be said about swizzling. You should form your own opinion once you fully understand both the good and the bad.

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

Here are some of the pitfalls of method swizzling:

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

这些观点都是正确的,在解决它们时,我们可以提高我们对方法混合的理解以及用于实现结果的方法.我会一次接一个.

These points are all valid, and in addressing them we can improve both our understanding of method swizzling as well as the methodology used to achieve the result. I'll take each one at a time.

我还没有看到可以安全地同时使用的方法混合实现1.这实际上在 95% 的情况下都不是问题,您希望使用方法调配.通常,您只是想替换一个方法的实现,并且希望该实现在程序的整个生命周期中都使用.这意味着你应该在 +(void)load 中调配你的方法.load 类方法在应用程序开始时串行执行.如果您在这里进行调配,您将不会遇到任何并发问题.但是,如果您要在 +(void)initialize 中进行 swizzle,那么您的 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.

这是 swizzling 的一个问题,但它是重点.目标是能够更改该代码.人们指出这很重要的原因是因为您不仅要更改要更改的 NSButton 的一个实例,而且要更改所有 NSButton 应用程序中的实例.出于这个原因,你在调酒时应该小心,但你不需要完全避免它.

This is an issue with swizzling, but it's kind of the whole point. The goal is to be able to change that code. The reason that people point this out as being a big deal is because you're not just changing things for the one instance of NSButton that you want to change things for, but instead for all NSButton instances in your application. For this reason, you should be cautious when you swizzle, but you don't need to avoid it altogether.

这样想……如果你在一个类中重写了一个方法,而你没有调用超类的方法,你可能会导致问题出现.在大多数情况下,超类期望调用该方法(除非另有说明).如果你将同样的想法应用到 swizzling 上,你已经涵盖了大多数问题.始终调用原始实现.如果你不这样做,你可能改变太多而不安全.

Think of it this way... if you override a method in a class and you don't call the super class method, you may cause problems to arise. In most cases, the super class is expecting that method to be called (unless documented otherwise). If you apply this same thought to swizzling, you've covered most issues. Always call the original implementation. If you don't, you're probably changing too much to be safe.

命名冲突是整个 Cocoa 中的一个问题.我们经常在类别中添加类名和方法名的前缀.不幸的是,命名冲突是我们语言中的瘟疫.但是,在 swizzling 的情况下,它们不必如此.我们只需要稍微改变一下我们对方法混杂的看法.大多数 swizzling 是这样完成的:

Naming conflicts are an issue all throughout Cocoa. We frequently prefix class names and method names in categories. Unfortunately, naming conflicts are a plague in our language. In the case of swizzling, though, they don't have to be. We just need to change the way that we think about method swizzling slightly. Most swizzling is done like this:

@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: 被定义在其他地方会发生什么?这个问题并不是 swizzling 所独有的,但无论如何我们都可以解决它.该解决方法还具有解决其他陷阱的额外好处.下面是我们的做法:

This works just fine, but what would happen if my_setFrame: was defined somewhere else? This problem isn't unique to swizzling, but we can work around it anyway. The workaround has an added benefit of addressing other pitfalls as well. Here's what we do instead:

@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 的人来说,这可能有点改变,因为它已经定义了一段时间,但最后,我认为它更好.swizzling 方法是这样定义的:

While this looks a little less like Objective-C (since it's using function pointers), it avoids any naming conflicts. In principle, it's doing the exact same thing as standard swizzling. This may be a bit of a change for people who have been using swizzling as it has been defined for a while, but in the end, I think that it's better. The swizzling method is defined thusly:

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

通过重命名方法来改变方法的参数

这是我心目中的大事.这就是不应该进行标准方法混合的原因.您正在更改传递给原始方法实现的参数.这就是它发生的地方:

Swizzling by renaming methods changes the method's arguments

This is the big one in my mind. This is the reason that standard method swizzling should not be done. You are changing the arguments passed to the original method's implementation. This is where it happens:

[self my_setFrame:frame];

这一行的作用是:

objc_msgSend(self, @selector(my_setFrame:), frame);

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

Which will use the runtime to look up the implementation of my_setFrame:. Once the implementation is found, it invokes the implementation with the same arguments that were given. The implementation it finds is the original implementation of setFrame:, so it goes ahead and calls that, but the _cmd argument isn't setFrame: like it should be. It's now my_setFrame:. The original implementation is being called with an argument it never expected it would receive. This is no good.

有一个简单的解决方案 —使用上面定义的替代混合技术.参数将保持不变!

There's a simple solution — use the alternative swizzling technique defined above. The arguments will remain unchanged!

方法混合的顺序很重要.假设 setFrame: 只定义在 NSView 上,想象一下这个顺序:

The order in which methods get swizzled matters. Assuming setFrame: is only defined on NSView, imagine this order of things:

[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 实现).

What happens when the method on NSButton is swizzled? Well most swizzling will ensure that it's not replacing the implementation of setFrame: for all views, so it will pull up the instance method. This will use the existing implementation to re-define setFrame: in the NSButton class so that exchanging implementations doesn't affect all views. The existing implementation is the one defined on NSView. The same thing will happen when swizzling on NSControl (again using the NSView implementation).

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

When you call setFrame: on a button, it will therefore call your swizzled method, and then jump straight to the setFrame: method originally defined on NSView. The NSControl and NSView swizzled implementations will not be called.

但是如果订单是:

[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:实现.这有点令人困惑,但这是正确的顺序.我们如何才能确保这种顺序?

Since the view swizzling takes place first, the control swizzling will be able to pull up the right method. Likewise, since the control swizzling was before the button swizzling, the button will pull up the control's swizzled implementation of setFrame:. This is a bit confusing, but this is the correct order. How can we ensure this order of things?

同样,只需使用 load 来调配东西.如果您在 load 中调动,并且只对正在加载的类进行更改,那么您将是安全的.load 方法保证超类加载方法将在任何子类之前被调用.我们会得到完全正确的订单!

Again, just use load to swizzle things. If you swizzle in load and you only make changes to the class being loaded, you'll be safe. The load method guarantees that the super class load method will be called before any subclasses. We'll get the exact right order!

查看传统定义的 swizzled 方法,我认为很难判断发生了什么.但是看看我们在上面进行的调动的另一种方式,它很容易理解.这个已经解决了!

Looking at a traditionally defined swizzled method, I think it's really hard to tell what's going on. But looking at the alternative way we've done swizzling above, it's pretty easy to understand. This one's already been solved!

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

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.

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

Method swizzling is safe if used properly. A simple safety measure you can take is to only swizzle in load. Like many things in programming, it can be dangerous, but understanding the consequences will allow you use it properly.

1 使用上面定义的 swizzling 方法,如果您要使用蹦床,您可以使事情线程安全.你需要两个蹦床.在方法开始时,您必须将函数指针 store 分配给一个旋转的函数,直到 store 指向的地址发生变化.这将避免在您能够设置 store 函数指针之前调用 swizzled 方法的任何竞争条件.然后,您需要在类中尚未定义实现的情况下使用蹦床,并进行蹦床查找并正确调用超类方法.定义方法使其动态查找超级实现将确保调动调用的顺序无关紧要.

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.

这篇关于Objective-C 中方法混用的危险是什么?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆