ObjC 中的黑魔法 Swizzling

Aug 07, 2022 • 预计阅读时间 3 分钟

ObjC 是动态语言,方法调用都是在运行期间通过 objc_msgSend 向对象发送消息来实现。

而且 ObjC 支持动态增加/交互方法的实现,这个技术称之为 Swizzle。

Swizzle 本质上是改变了方法映射表中的 SELMethod 的对应关系。

  • 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/

参考资料

https://pspdfkit.com/blog/2019/swizzling-in-swift/

iOS
版权声明:如果转发请带上本文链接和注明来源。

lvv.me

iOS/macOS Developer

weak-strong dance 的注意事项

C/C++ 中的 static 和 inline 的作用