问题描述
我听说人们指出方法混淆是一种危险的做法.甚至连起草的名字都暗示这有点作弊.
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.
方法混乱正在修改映射因此调用选择器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.
我们可以对风险进行形式化处理,以便任何决定是否使用毛毛雨的人都可以做出明智的决定,以决定这样做是否值得.
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:
- 方法混乱不是原子性的
- 更改未拥有代码的行为
- 可能的命名冲突
- 混乱更改方法的参数
- 麻烦的顺序很重要
- 难以理解(看起来是递归的)
- 难以调试
这些要点都是有效的,在解决这些问题时,我们可以提高我们对方法混乱的理解以及用于实现结果的方法的理解.我一次接一个.
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.
我还没有看到可以安全地同时使用的方法转换的实现.实际上,在您要使用方法混乱的情况下,有95%的情况都不是问题.通常,您只想替换一个方法的实现,并且希望在程序的整个生命周期中都使用该实现.这意味着您应该在+(void)load
中执行您的方法. load
类方法是在应用程序开始时顺序执行的.如果您在此处进行复杂处理,那么并发不会有任何问题.但是,如果要在+(void)initialize
中陷入混乱,则可能会在混乱的实现中遇到竞争条件,并且运行时可能会陷入怪异的状态.
I have yet to see an implementation of method swizzling that is safe to use concurrently. 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.
这是一个令人毛骨悚然的问题,但这很重要.目的是能够更改该代码.人们指出这一点很重要的原因是,您不仅要更改要为其更改内容的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.
以这种方式思考...如果您重写类中的方法而没有调用超类方法,则可能会引起问题.在大多数情况下,超类期望该方法被调用(除非另有说明).如果您将相同的想法应用到问题上,则涵盖了大多数问题.始终调用原始实现.如果您不这样做,则可能是为了安全起见,所做的更改太多了.
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.
命名冲突是整个可可中的一个问题.我们经常在类别中为类名和方法名加上前缀.不幸的是,命名冲突在我们的语言中是一个困扰.但是,如果出现混乱,则不必一定要这样做.我们只需要改变对方法略微混乱的思考方式即可.最令人毛骨悚然的是这样的:
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:
会发生什么呢?这个问题并不是唯一的问题,但是我们仍然可以解决它.解决方法还有一个额外的好处,就是可以解决其他陷阱.这是我们要做的:
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(因为它使用的是函数指针),但它避免了任何命名冲突.原则上讲,它所做的与标准筛选完全相同.对于已经定义了一段时间的使用毛毛雨的人来说,这可能是一个变化,但最后,我认为它会更好.这样就定义了毛毛雨的方法:
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
上的方法混乱时会发生什么?很好的解决方法是确保不会替换所有视图的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:
时,它将因此调用您陷入混乱的方法,然后直接跳转到最初在NSView
上定义的setFrame:
方法. NSControl
和NSView
混乱的实现将不会被调用.
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!
考虑到传统上定义的混淆方法,我认为很难告诉发生了什么.但是,看看我们在上面做过的另一种选择,这很容易理解.这个已经解决了!
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.
使用上面定义的旋转方法,如果要使用蹦床,可以使线程安全.您将需要两个蹦床.在方法开始时,您必须将函数指针store
分配给要旋转的函数,直到store
指向的地址更改为止.这样可以避免在设置store
函数指针之前调用swizzled方法的任何竞争情况.然后,如果尚未在类中定义实现,则需要使用蹦床,并需要进行蹦床查找并正确调用超类方法.定义该方法,以便它动态查找超级实现,将确保混乱的调用顺序无关紧要.
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中方法混乱的危险是什么?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!