方法替换(Method Swizzling)实战

Mike Ash Friday Q&A 中文译文:方法替换(Method Swizzling)实战

作者 TommyWu
封面圖片: 方法替换(Method Swizzling)实战

译文 · 原文: Friday Q&A 2010-01-29: Method Replacement for Fun and Profit · 作者 Mike Ash

原文:https://www.mikeash.com/pyblog/friday-qa-2010-01-29-method-replacement-for-fun-and-profit.html 发布:2010-01-29 作者:Mike Ash 译者:MiMo(mimo-v2.5-pro);代码块保留英文原样


又到了每周一次的周五问答时间。本周的 Friday Q & A 中,Mike Shields 建议我谈谈 Objective-C 中的方法替换(method replacement)与方法混写(method swizzling)。

重写方法
在几乎任何面向对象语言中,重写方法都是常见任务。多数情况下我们通过子类化 —— 一种由来已久的技术 —— 来完成这项工作。你创建子类,在子类中实现该方法,必要时实例化子类,而子类实例将使用重写的方法。人人都知道如何操作。

但有时你需要重写那些你无法控制其实例化的对象中的方法。此时子类化就不够用了,因为代码不会实例化你的子类。你的方法重写只能闲置一旁,无所作为。

Posing(姿态子类化)

Posing(姿态子类化)是一项有趣的技术,但遗憾的是,它如今已过时,因为苹果在” 新” 的(64 位和 iPhone)Objective-C 运行时(runtime)中不再支持它。通过姿态子类化,你可以创建一个子类,然后让该子类” 伪装” 成它的超类。运行时会进行一些” 魔法” 操作,于是该子类便被到处使用,方法覆盖(method overrides)也因此再次变得有效。由于这已不再被支持,我将不深入探讨其细节。

Categories(分类)

通过使用 category(分类),你可以轻松地覆盖现有类中的方法:

@implementation NSView (MyOverride)
- (void)drawRect: (NSRect)r
{
// this runs instead of the normal -[NSView drawRect:]
[[NSColor blueColor] set];
NSRectFill(r);
}
@end
  • 无法调用原有方法的实现。新的实现会完全替换原有实现,而原有实现就直接丢失了。大多数覆盖本意是添加功能,而非完全替换,但使用类目(category)无法做到这一点。

  • 涉及的类也可能在另一个类目中实现了同名方法,而运行时(runtime)无法保证当两个类目包含同名方法时哪一个实现会 “胜出”。

首先,你需要用不同的名称来实现覆盖:

@implementation NSView (MyOverride)
- (void)override_drawRect: (NSRect)r
{
// call through to the original, really
[self override_drawRect: r];
[[NSColor blueColor] set];
NSRectFill(r);
}
@end

要交换方法,需要一些代码将新的方法实现(method implementation)置入,同时将旧的方法实现移出:

void MethodSwizzle(Class c, SEL origSEL, SEL overrideSEL)
{
Method origMethod = class_getInstanceMethod(c, origSEL);
Method overrideMethod = class_getInstanceMethod(c, overrideSEL);

对于方法仅存在于父类的情况,第一步是向当前类添加一个新方法,并使用该重写作为其实现。完成此步骤后,再将重写方法替换为原始方法。

添加新方法的这一步骤同样可用于检查实际存在哪种情况。运行时函数(runtime function)class_addMethod 在方法已存在时会失败,因此可用于此检查:

if(class_addMethod(c, origSEL, method_getImplementation(overrideMethod), method_getTypeEncoding(overrideMethod)))
{
class_replaceMethod(c, overrideSEL, method_getImplementation(origMethod), method_getTypeEncoding(origMethod));
}
else
{
method_exchangeImplementations(origMethod, overrideMethod);
}
}

这段代码需要两种情况的原因在于,class_getInstanceMethod 实际上会返回包含该方法实现的父类的 Method。替换该实现将错误地替换了其他类的方法!

举一个具体的例子,假设要替换 -[NSView description]。如果 NSView 没有实现 -description(这很有可能),那么你得到的将是 NSObject 的 Method。如果你对该 Method 调用 method_exchangeImplementations,你就会用你自己的代码替换 NSObject 的 -description 方法,而这并非你想要的结果!

(当出现这种情况时,使用简单的 category(分类)方法就足够了,因此不需要这段代码。问题在于你无法知道一个类是否重写了其父类的方法,这甚至可能随系统版本而变化,因此你必须假设该类可能自行实现了该方法,并编写能够处理这种情况的代码。)

最后,我们只需要确保这段代码在程序启动时确实被调用。这可以通过向 MyOverride category 添加一个 +load 方法轻松实现:

+ (void)load
{
MethodSwizzle(self, @selector(drawRect:), @selector(override_drawRect:));
}

事实证明,无需保留原方法的方法特性。[self override_drawRect: r] 中涉及的动态派发(dynamic dispatch)是完全不必要的。我们从一开始就清楚需要调用哪个实现。

与其将原方法迁移到新方法中,不如直接将其实现(implementation)提取到一个全局函数指针中:

void (*gOrigDrawRect)(id, SEL, NSRect);
+ (void)load
{
Method origMethod = class_getInstanceMethod(self, @selector(drawRect:));
gOrigDrawRect = (void *)method_getImplementation(origMethod);

接下来,替换原始方法。和之前一样,需要考虑两种情况,因此我会先添加新方法,然后如果发现已存在同名方法,则替换它:

if(!class_addMethod(self, @selector(drawRect:), (IMP)OverrideDrawRect, method_getTypeEncoding(origMethod)))
method_setImplementation(origMethod, (IMP)OverrideDrawRect);
}
static void OverrideDrawRect(NSView *self, SEL _cmd, NSRect r)
{
gOrigDrawRect(self, _cmd, r);
[[NSColor blueColor] set];
NSRectFill(r);
}

必要的警告
覆盖(overriding)不属于自己的类中的方法是危险行为。你的覆盖可能会打破该类原有的假设,从而引发问题。只要有可能,应避免这样做。若必须覆盖,请务必极其谨慎地编写代码。

总结
本周内容到此结束。现在你已了解了 Objective-C 中方法覆盖的全部可能性,包括一种我在其他地方很少见到讨论的变体。请善用此能力,勿作恶用!

七天后再会。届时欢迎继续发送你的主题建议。Friday Q & A 依赖读者投稿运作,若有想在此探讨的话题,请投稿吧!


#Original (English)

Source: https://www.mikeash.com/pyblog/friday-qa-2010-01-29-method-replacement-for-fun-and-profit.html

It’s that time of the week again. For this week’s Friday Q&A Mike Shields has suggested that I talk about method replacement and method swizzling in Objective-C.

Overriding Methods Overriding methods is a common task in just about any object oriented language. Most of the time you do this by subclassing, a time-honored technique. You subclass, you implement the method in the subclass, you instantiate the subclass when necessary, and instances of the subclass use the overridden method. Everybody knows how to do this.

Sometimes, though, you need to override methods that are in objects whose instantiation you don’t control. Subclassing doesn’t suffice in that case, because you can’t make that code instantiate your subclass. Your method override sits there, twiddling its thumbs, accomplishing nothing.

Posing Posing is an interesting technique but, alas, is now obsolete, since Apple no longer supports it in the “new” (64-bit and iPhone) Objective-C runtime. With posing, you subclass, then pose the subclass as its superclass. The runtime does some magic and suddenly the subclass is used everywhere, and method overrides become useful again. Since this is no longer supported, I won’t go into details.

Categories Using a category, you can easily override a method in an existing class:

@implementation NSView (MyOverride)
- (void)drawRect: (NSRect)r
{
// this runs instead of the normal -[NSView drawRect:]
[[NSColor blueColor] set];
NSRectFill(r);
}
@end
  • It’s impossible to call through to the original implementation of the method. The new implementation replaces the original, which is simply lost. Most overrides want to add functionality, not completely replace it, but it’s not possible with a category.

  • The class in question could implement the method in question in a category too, and the runtime doesn’t guarantee which implementation “wins” when two categories contain methods with the same name.

First, you implement the override with a different name:

@implementation NSView (MyOverride)
- (void)override_drawRect: (NSRect)r
{
// call through to the original, really
[self override_drawRect: r];
[[NSColor blueColor] set];
NSRectFill(r);
}
@end

To swap the method, you need a bit of code to move the new implementation in and the old implementation out:

void MethodSwizzle(Class c, SEL origSEL, SEL overrideSEL)
{
Method origMethod = class_getInstanceMethod(c, origSEL);
Method overrideMethod = class_getInstanceMethod(c, overrideSEL);

For the case where the method only exists in a superclass, the first step is to add a new method to this class, using the override as the implementation. Once that’s done, then the override method is replaced with the original one.

The step of adding the new method can also double as a check to see which case is actually present. The runtime function class_addMethod will fail if the method already exists, and so can be used for the check:

if(class_addMethod(c, origSEL, method_getImplementation(overrideMethod), method_getTypeEncoding(overrideMethod)))
{
class_replaceMethod(c, overrideSEL, method_getImplementation(origMethod), method_getTypeEncoding(origMethod));
}
else
{
method_exchangeImplementations(origMethod, overrideMethod);
}
}

The reason the code needs the two cases is because class_getInstanceMethod will actually return the Method for the superclass if that’s where the implementation lies. Replacing that implementation will replace the method for the wrong class!

As a concrete example, imagine replacing -[NSView description]. If NSView doesn’t implement -description (which is probable) then you’ll get NSObject’s Method instead. If you called method_exchangeImplementations on that Method, you’d replace the -description method on NSObject with your own code, which is not what you want to do!

(When that’s the case, a simple category method would work just fine, so this code wouldn’t be needed. The problem is that you can’t know whether a class overrides a method from its superclass or not, and that could even change from one OS release to the next, so you have to assume that the class may implement the method itself, and write code that can handle that.)

Finally we just need to make sure that this code actually gets called when the program starts up. This is easily done by adding a +load method to the MyOverride category:

+ (void)load
{
MethodSwizzle(self, @selector(drawRect:), @selector(override_drawRect:));
}

It turns out that there’s no need to preserve the method-ness of the original method. The dynamic dispatch involved in [self override_drawRect: r] is completely unnecessary. We know which implementation we want right from the start.

Instead of moving the original method into a new one, just move its implementation into a global function pointer:

void (*gOrigDrawRect)(id, SEL, NSRect);
+ (void)load
{
Method origMethod = class_getInstanceMethod(self, @selector(drawRect:));
gOrigDrawRect = (void *)method_getImplementation(origMethod);

Next, replace the original. Like before, there are two cases to worry about, so I’ll first add the method, then replace the existing one if it turns out that there is one:

if(!class_addMethod(self, @selector(drawRect:), (IMP)OverrideDrawRect, method_getTypeEncoding(origMethod)))
method_setImplementation(origMethod, (IMP)OverrideDrawRect);
}
static void OverrideDrawRect(NSView *self, SEL _cmd, NSRect r)
{
gOrigDrawRect(self, _cmd, r);
[[NSColor blueColor] set];
NSRectFill(r);
}

The Obligatory Warning Overriding methods on classes you don’t own is a dangerous business. Your override could cause problems by breaking the assumptions of the class in question. Avoid it if it’s at all possible. If you must do it, code your override with extreme care.

Conclusion That’s it for this week. Now you know the full spectrum of method override possibilities in Objective-C, including one variation that I haven’t seen discussed much elsewhere. Use this power for good, not for evil!

Come back in seven days for the next edition. Until then, keep sending in your suggestions for topics. Friday Q&A is powered by reader submissions, so if you have an idea for a topic to cover here, send it in!