ObjC 是动态语言,方法调用都是在运行期间通过 objc_msgSend
向对象发送消息来实现。
而且 ObjC 支持动态增加/交互方法的实现,这个技术称之为 Swizzle。
Swizzle 本质上是改变了方法映射表中的 SEL
与 Method
的对应关系。
selector1
->Imp1
selector2
->Imp2
经过 Swizzle 以后,交换了两个方法的实现,对应关系变为:
selector1
->Imp2
selector2
->Imp1
最简单的实现是这样的:
void swizzleInstanceMethod(Class cls, SEL original, SEL swizzled) {
Method originalMethod = class_getInstanceMethod(cls, original);
Method swizzledMethod = class_getInstanceMethod(cls, swizzled);
method_exchangeImplementations(originalMethod, swizzledMethod);
}
以上是交换实例方法,如果是类的静态方法则需要通过 object_getClass
先获取元类:
void swizzleClassMethod(Class cls, SEL original, SEL swizzled) {
swizzleInstanceMethod(object_getClass(cls), original, swizzled);
}
在使用的时候,一般是在目标类的分类中的 + (void)load
方法里进行方法交换:
@implementation MyObject (Swizzling)
+ (void)load {
swizzleInstanceMethod(self, @selector(myMethod), @selector(swizzled_myMethod));
}
- (void)swizzled_myMethod {
[self swizzled_myMethod];
NSLog(@"swizzled instance method.");
}
@end
一般情况下,这样做没有问题,但是由于不知道目标方法是怎么实现的,会存在风险。因为方法交换实际上是偷偷的改变了 _cmd
,
如果原来的方法实现里用到了 _cmd
那么就会得到一个意料之外的值:
@interface MyObject : NSObject
@property(nonatomic) NSInteger count;
@end
@implementation MyObject
- (NSInteger)count {
NSLog(@"_cmd: %@", NSStringFromSelector(_cmd));
NSInteger count = [objc_getAssociatedObject(self, _cmd) integerValue];
NSLog(@"original instance getCount: %ld", count);
return count;
}
- (void)setCount:(NSInteger)count {
objc_setAssociatedObject(self, @selector(count), @(count), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
NSLog(@"original instance setCount.");
}
@end
以上类中的 count
属性,实现上是使用了关联值的方式,而且使用了 _cmd
作为关联的 key,作为开发者这样写代码的方式当然没有问题。
当 count
的 getter 方法被 Swizzle 以后,情况就不同了:
@implementation MyObject (Swizzling)
+ (void)load {
swizzleInstanceMethod(self, @selector(count), @selector(swizzled_count));
}
- (NSInteger)swizzled_count {
NSInteger count = [self swizzled_count];
NSLog(@"swizzled instance getCount: %ld", count);
return count;
}
@end
count 属性的 getter 方法中的 _cmd
已经被偷偷的替换了,通过调试打印出来的值是 swizzled_count
,
因为 setter 方法使用的 key 是 @selector(count)
的值是 count
,两边的 key 不一样导致 getter 得到的值永远是 0 。
作为开发者要避免这种情况发生,就不要使用 _cmd
这个不稳定的关键字。使用一个固定的值,比如开源项目里一般会这么写:
static char kAssociatedObjectKey;
@implementation MyObject
- (NSInteger)count {
NSInteger count = [objc_getAssociatedObject(self, &kAssociatedObjectKey) integerValue];
return count;
}
- (void)setCount:(NSInteger)count {
objc_setAssociatedObject(self, &kAssociatedObjectKey, @(count), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end
使用一个静态的变量 static char kAssociatedObjectKey
,变量的地址在运行期间是固定的,这样就保证了 key 的唯一性,使用 char
类型纯粹是为了节省空间。
进一步完善 Swizzle
因为 class_getInstanceMethod
会先查找当前类的方法,如果没有再从父类中查找。这样就带来一个问题:如果是在子类中 Swizzle 它未重写的方法,
实际上被替换的就是父类中的方法,也就是方法映射表中的父类方法被替换为了子类中的方法:
selector1
->Imp2
(子类的方法)selector2
->Imp1
(父类的方法)
当父类的对象实例调用到被 Swizzle 的方法时,由于该方法的实现只存在子类中,就会出现 unrecognized selector
的崩溃。
@interface MySonObject : MyObject
@end
@implementation MySonObject
@end
在子类中进行 Swizzle:
@interface MySonObject (Swizzling)
@end
@implementation MySonObject (Swizzling)
+ (void)load {
swizzleInstanceMethod(self, @selector(count), @selector(swizzled_count));
swizzleInstanceMethod(self, @selector(setCount:), @selector(swizzled_setCount:));
}
- (void)swizzled_setCount:(NSInteger)count {
NSLog(@"swizzled instance setCount.");
[self swizzled_setCount:count];
}
- (NSInteger)swizzled_count {
NSInteger count = [self swizzled_count];
NSLog(@"swizzled instance getCount: %ld", count);
return count;
}
@end
当父类的对象实例调用到被 Swizzle 的方法,就会产生崩溃:
MyObject *my = [[MyObject alloc] init];
my.count = 10; // Crash !!
这种崩溃是很常见的,对于不是自己实现的子类,很难知道它是否实现了父类的方法,而且依赖这个条件才能 Swizzle 的话,那么 Swizzle 本身的技术价值就很小了。
解决方案是:先用 class_addMethod
给类添加目标方法,如果添加成功说明子类没有重载这个方法,再使用 class_replaceMethod
进行方法交换。
void swizzleInstanceMethod(Class cls, SEL original, SEL swizzled) {
Method originalMethod = class_getInstanceMethod(cls, original);
Method swizzledMethod = class_getInstanceMethod(cls, swizzled);
const char *types = method_getTypeEncoding(originalMethod);
if (class_addMethod(cls,
original,
method_getImplementation(swizzledMethod),
types)) {
class_replaceMethod(cls,
swizzled,
method_getImplementation(originalMethod),
types);
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
以上,就是 Swizzle 的一般写法。
更高级的 Swizzle
高级是指 Swizzle 的姿势更花哨,倾向于追求完美的方法。
当被 Swizzle 的方法只在父类中实现的时候,比较优雅的做法是在 Swizzle 方法里调用 super
而不是做方法交换,
而且这样不会改变被 Swizzle 方法的 _cmd
,但是这个方法会稍微复杂一些: https://petersteinberger.com/blog/2014/a-story-about-swizzling-the-right-way-and-touch-forwarding/