2420dff358c19b4514b057d2dc7e05d8
Swift 5 之后 "Method Swizzling"?

Swift 5 之后 "Method Swizzling"?

引子

随着六月份的 WWDC 上对 SwiftUI 的发布,感觉 Swift 有变成了炽手可热的话题。在大会结束后,发现了有这么几条 Twitter 在讨论一个叫做 @_dynamicReplacement(for:) 的新特性。

这是一个什么东西呢,于是我在 Swift 社区中也检索了对应的关键字,看到一个 Dynamic Method Replacement 的帖子在爬了多层楼之后,大概看到了使用的方式(环境是 macOS 10.14.5,Swift 版本是 5.0,注意以下 Demo 只能在工程中跑,Playground 会报 error: Couldn't lookup symbols: 错误)。

class Test {
    dynamic func foo() {
        print("bar")
    }
}

extension Test {
    @_dynamicReplacement(for: foo())
    func foo_new() {
        print("bar new")
    }
}

Test().foo() // bar new

看到这里是不是眼前一亮?我们期待已久的 Method Swizzling 仿佛又回来了?

开始的时候只是惊喜,但是在平时的个人开发中,其实很少会用到 hook 逻辑(当然这里说的不是公司项目)。直到有一天,朋友遇到了一个问题,于是又对这个东西做了一次较为深入的研究 ....

Method Swizzling in Objective-C

首先我们先写一段 ObjC 中 Method Swizzling 的场景:

//
//  PersonObj.m
//  MethodSwizzlingDemo
//
//  Created by Harry Duan on 2019/7/26.
//  Copyright © 2019 Harry Duan. All rights reserved.
//

#import "PersonObj.h"
#import <objc/runtime.h>

@implementation PersonObj

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        SEL oriSelector = @selector(sayWords);
        SEL swiSelector = @selector(sayWordsB);
        Method oriMethod = class_getInstanceMethod(class, oriSelector);
        Method swiMethod = class_getInstanceMethod(class, swiSelector);
        method_exchangeImplementations(oriMethod, swiMethod);

        SEL swi2Selector = @selector(sayWorkdsC);
        Method swi2Method = class_getInstanceMethod(class, swi2Selector);
        method_exchangeImplementations(oriMethod, swi2Method);
    });
}

- (void)sayWords {
    NSLog(@"A");
}

- (void)sayWordsB {
    NSLog(@"B");
    [self sayWordsB];
}

- (void)sayWorkdsC {
    NSLog(@"C");
    [self sayWorkdsC];
}

@end

上述代码我们声明了 - (void)sayWords 方法,然后再 + (void)load 过程中,使用 Method Swizzling 进行了两次 Hook。

在执行处,我们来调用一下 - sayWords 方法:

PersonObj *p = [PersonObj new];
[p sayWords];

// log
2019-07-26 16:04:49.231045+0800 MethodSwizzlingDemo[9859:689451] C
2019-07-26 16:04:49.231150+0800 MethodSwizzlingDemo[9859:689451] B
2019-07-26 16:04:49.231250+0800 MethodSwizzlingDemo[9859:689451] A

正如我们所料,结果会输出 CBA,因为 - sayWords 方法首先被替换成了 - sayWordsB ,其替换后的结果又被替换成了 - sayWordsC 。进而由于 Swizze 的方法都调用了原方法,所以会输出 CBA。

来复习一下 Method Swizzling 在 Runtime 中的原理,我们可以概括成一句话来描述它:方法指针的交换。以下是 ObjC 的 Runtime 750 版本的源码:

void method_exchangeImplementations(Method m1, Method m2)
{
    if (!m1  ||  !m2) return;

    mutex_locker_t lock(runtimeLock);

        // 重点在这里,将两个方法的实例对象 m1 和 m2 传入后做了一次啊 imp 指针的交换
    IMP m1_imp = m1->imp;
    m1->imp = m2->imp;
    m2->imp = m1_imp;


    // RR/AWZ updates are slow because class is unknown
    // Cache updates are slow because class is unknown
    // fixme build list of classes whose Methods are known externally?

    flushCaches(nil);

        // 来更新每个方法中 RR/AWZ 的 flags 信息
        // RR/AWZ = Retain Release/Allow With Zone(神奇的缩写)
    updateCustomRR_AWZ(nil, m1);
    updateCustomRR_AWZ(nil, m2);
}

由于 ObjC 对于实例方法的存储方式是以方法实例表,那么我们只要能够访问到其指定的方法实例,修改 imp 指针对应的指向,再对引用计数和内存开辟等于 Class 相关的信息做一次更新就实现了 Method Swizzling。

一个连环 Hook 的场景

上面的输出 ABC 场景,是我朋友遇到的。在制作一个根据动态库来动态加载插件的研发工具链的时候,在主工程会开放一些接口,模拟 Ruby 的 alias_method 写法,这样就可以将自定义的实现注入到下层方法中,从而扩展实现。当然这种能力暴露的方案不是很好,只是一种最粗暴的插件方案实现方法。

当然我们今天要说的不是 ObjC,因为 ObjC 在 Runtime 机制上都是可以预期的。如果我们使用 Swift 5.0 中 Dynamic Method Replacement 方案在 Swift 工程中实现这种场景。

```swift
import UIKit

class Person {
dynamic func sayWords() {
print("A")
}
}

extension Person {
@_dynamicReplacement(for: sayWords())
func sayWordsB() {
print("B")
sayWords()
}
}

top Created with Sketch.