在 Objective-C 的世界里,Runtime 扮演着至关重要的角色。它不仅是 OC 面向对象特性的基石,更是实现动态性、灵活性的关键。今天我们聚焦 Objective-C 初阶 中两个核心概念:方法交换 (Method Swizzling) 与 消息传递 (Message Passing),深入探讨它们的工作原理、应用场景以及需要注意的坑。
方法交换 (Method Swizzling):偷偷摸摸换实现
问题场景重现
假设我们有一个第三方的 SDK,它提供了一个 UIButton 的子类 CustomButton,并且有一个 setTitle: 方法。现在我们需要在不修改 SDK 源码的情况下,为 setTitle: 方法添加额外的日志记录功能。这就是方法交换大显身手的时候。
底层原理剖析
方法交换的核心在于利用 Objective-C 的 Runtime 机制,动态地改变方法的实现。具体来说,就是交换两个方法的 IMP (Implementation Pointer),也就是函数指针。这样,调用原本的方法名,实际上执行的是交换后的实现。
代码解决方案
#import <objc/runtime.h>
@implementation UIButton (Swizzling)
+ (void)load { // 在类加载时进行方法交换
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
SEL originalSelector = @selector(setTitle:forState:);
SEL swizzledSelector = @selector(my_setTitle:forState:);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
// 若原方法未实现,先添加原方法,再将新方法替换为原方法
BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod); // 交换两个方法的实现
}
});
}
// 我们自己的方法实现,添加了日志
- (void)my_setTitle:(NSString *)title forState:(UIControlState)state {
NSLog(@"Setting title: %@", title); // 添加日志
[self my_setTitle:title forState:state]; // 调用原始的 setTitle: 方法
}
@end
实战避坑经验总结
load方法的线程安全: 使用dispatch_once确保方法交换只执行一次,避免多线程竞争导致的问题。- 命名冲突: 为 Swizzled 方法添加前缀,避免与 SDK 中其他方法冲突。例如,使用
my_或者类名的缩写。 - 调用原始方法: 在 Swizzled 方法中一定要调用原始方法,否则会丢失原始功能。
- 父类方法: 如果需要 Swizzle 父类的方法,需要特别小心。确保只 Swizzle 子类自身的方法,避免影响其他子类。
- 谨慎使用: 方法交换是一种强大的技术,但过度使用会增加代码的复杂性,降低可维护性。
消息传递 (Message Passing):OC 的灵魂
问题场景重现
想象一下,你调用了一个对象的方法,但这个对象并没有实现这个方法。在静态语言中,这通常会导致编译错误。但是在 Objective-C 中,得益于消息传递机制,程序并不会立即崩溃,而是会尝试将这个消息转发给其他对象处理。
底层原理剖析
Objective-C 的消息传递机制基于 objc_msgSend 函数。当我们调用一个对象的方法时,实际上是调用了 objc_msgSend 函数,并将消息 (方法名) 和参数传递给它。Runtime 会根据对象的 isa 指针找到对应的类,然后在类的方法列表中查找对应的方法。如果找到了,就执行该方法;如果找不到,就会进入消息转发流程。
消息转发流程大致分为三个步骤:
resolveInstanceMethod:/resolveClassMethod:: 询问类是否可以动态地为这个 selector 提供实现。forwardingTargetForSelector:: 如果第一步无法解决,则询问是否有其他对象可以处理这个消息。如果返回一个对象,则将消息转发给该对象。methodSignatureForSelector:和forwardInvocation:: 如果第二步也无法解决,则会调用methodSignatureForSelector:方法获取方法签名,然后调用forwardInvocation:方法进行完整的消息转发。我们可以在forwardInvocation:方法中实现自定义的消息转发逻辑。
代码解决方案
以下是一个简单的消息转发的例子:
#import <Foundation/Foundation.h>
@interface MyClass : NSObject
- (void)unknownMethod;
@end
@interface AnotherClass : NSObject
- (void)handleUnknownMethod;
@end
@implementation AnotherClass
- (void)handleUnknownMethod {
NSLog(@"AnotherClass handled the unknown method!");
}
@end
@implementation MyClass
// 第一步:尝试动态添加方法实现
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(unknownMethod)) {
// 动态添加 handleUnknownMethod 的实现
class_addMethod([self class], sel, (IMP)dynamicMethodIMP, "v@:");
return YES;
} else {
return [super resolveInstanceMethod:sel];
}
}
void dynamicMethodIMP(id self, SEL _cmd) {
NSLog(@"Dynamic method implementation!");
}
// 第二步:尝试将消息转发给其他对象
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(unknownMethod)) {
return [[AnotherClass alloc] init]; // 将消息转发给 AnotherClass
} else {
return [super forwardingTargetForSelector:aSelector];
}
}
// 第三步:完整的消息转发
- (void)forwardInvocation:(NSInvocation *)anInvocation {
SEL selector = [anInvocation selector];
if ([[AnotherClass new] respondsToSelector:selector]) {
[anInvocation invokeWithTarget:[AnotherClass new]];
} else {
[super forwardInvocation:anInvocation];
}
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
if (!signature) {
signature = [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return signature;
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
MyClass *obj = [[MyClass alloc] init];
[obj unknownMethod]; // 调用未实现的方法
}
return 0;
}
实战避坑经验总结
- 避免无限循环: 在消息转发过程中,要避免出现无限循环。例如,A 对象将消息转发给 B 对象,B 对象又将消息转发回 A 对象,导致程序崩溃。
- 性能损耗: 消息转发会带来一定的性能损耗,因为它需要多次查找方法列表。因此,应该尽量避免过度使用消息转发。
- 方法签名: 在
forwardInvocation:方法中,需要手动创建方法签名。如果方法签名不正确,会导致程序崩溃。
希望这篇文章能帮助你更好地理解 Objective-C Runtime 中方法交换和消息传递这两个重要的概念。 掌握它们能让你在 OC 的世界里更加游刃有余,解决各种疑难杂症。 记得在使用 Objective-C Runtime 时,要特别小心,避免出现一些难以调试的问题。
冠军资讯
键盘上的咸鱼