Swift5 Method Swizzling 方法交换

最近看了一下关于Swift5的特性,发现了一个叫 @_dynamicReplacement(for:) 的新特性比较有趣,所以研究了一下。
新建一个Swift 5.0的工程,不能用playground!
然后输入以下代码,这里有一个key word叫dynamic,这个关键字在Swift3中就出现了,代表动态派发,只有加了dynamic修饰的方法才能被@_dynamicReplacement(for:) :

import Foundation

class Test {
    dynamic func testA() {
        print("testA")
    }
}

extension Test {
    @_dynamicReplacement(for: testA())
    func testB() {
        print("testB")
    }
}

Test().testA()

运行一下发现控制台输出了:

testB
Program ended with exit code: 0

( ⊙ o ⊙ )啊?我们期待已久的 Method Swizzling 又回来了?

看到这里,我们不禁要与OC的Method Swizzling做一个比较:
先写一个OC的类:

//Test.h
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface Test : NSObject
- (void)testA;
- (void)testB;
- (void)testC;
@end

NS_ASSUME_NONNULL_END

//Test.m
#import "Test.h"
#import <objc/runtime.h>

@implementation Test

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        SEL testASelector = @selector(testA);
        SEL testBSelector = @selector(testB);
        Method testAMethod = class_getInstanceMethod(class, testASelector);
        Method testBMethod = class_getInstanceMethod(class, testBSelector);
        method_exchangeImplementations(testAMethod, testBMethod);

        SEL testCSelector = @selector(testC);
        Method testCMethod = class_getInstanceMethod(class, testCSelector);
        method_exchangeImplementations(testAMethod, testCMethod);
    });
}

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

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

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

//main.m
#import <Foundation/Foundation.h>
#import "Test.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Test * t = [[Test alloc] init];
        [t testA];
    }
    return 0;
}

上面的程序很明显,运行结果是:

2019-09-26 23:12:39.513796+0800 OC[87404:218117] C
2019-09-26 23:12:39.514014+0800 OC[87404:218117] B
2019-09-26 23:12:39.514036+0800 OC[87404:218117] A
Program ended with exit code: 0

OC中方法交换是一个一个来的,所以结果会输出 CBA,因为 testA 方法首先被替换成了 testB ,其替换后的结果又被替换成了 testC 。进而由于 Swizze 的方法都调用了原方法,所以会输出 CBA。

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

对于这个例子,我们用Swift的@_dynamicReplacement(for:) 写一遍:

import Foundation

class Test {
    dynamic func testA() {
        print("A")
    }
}

extension Test {
    @_dynamicReplacement(for: testA())
    func testB() {
        print("B")
        testA()
    }
}

extension Test {
    @_dynamicReplacement(for: testA())
    func testC() {
        print("C")
        testA()
    }
}

Test().testA()

从视觉角度上来看,通过对 Swift Functions 的显式声明,我们完成了对于 Method Swizzling 的实现。
但是跑一下代码,发现运行结果却是:

C
A
Program ended with exit code: 0

为什么结果只显示两个?我交换了一下两个 extension 的顺序继续尝试,其打印结果又变成了 BA 。所以可以断定在执行顺序上,后声明的将会生效。那么应该如何实现这种连环 Hook 的场景呢?经过多次的尝试,从代码层面应该是不可能办到了,总会以最后一次的方法为准。

按照正常程序员的逻辑,如果我们在重构一个模块的代码,新的模块代码无论从功能还是效率上,都应该优于之前的方式、覆盖之前所有的逻辑场景。如果 Swift 支持这种连环修改的场景,那这个新的 Feature 放出其实是功能不完备的!于是我们开始翻看 Swift 这个 Feature 的 PR 代码,来一探 Dynamic Method Replacement 的原理。

首先来看这个 Dynamic Method Replacement 特性的 Issue-20333[2],作者上来就贴了两段很有意思的代码:

/// 片段一
// Module A
struct Foo {
 dynamic func bar() {}
}
// Module B
extension Foo {
  @_dynamicReplacement(for: bar())
  func barReplacement() {
    ...
    // Calls previously active implementation of bar()
    bar()
  }
}

/// 片段二
dynamic_replacement_scope AGroupOfReplacements {
   extension Foo {
     func replacedFunc() {}
   }
   extension AnotherType {
     func replacedFunc() {}
   }
}

AGroupOfReplacements.enable()
...
AGroupOfReplacements.disable()

大概意思就是,他希望这种动态替换的特性,通过一些关键字标记和内部标记,兼容动态替换和启用开关。既然他们有规划 enable 和 disable 这两个方法来控制启用,那么就来检索它们的实现。

通过关键字搜索,我在 MetadataLookup.cpp 这个文件的 L1523-L1536 中找到了 enable 和 disable 两个方法的实现源码:

// Metadata.h#L4390-L4394:
// https://github.com/apple/swift/blob/659c49766be5e5cfa850713f43acc4a86f347fd8/include/swift/ABI/Metadata.h#L4390-L4394

/// dynamic replacement functions 的链表实现
/// 只有一个 next 指针和 imp 指针
struct DynamicReplacementChainEntry {
  void *implementationFunction;
  DynamicReplacementChainEntry *next;
};

// MetadataLookup.cpp#L1523-L1563
// https://github.com/aschwaighofer/swift/blob/fff13330d545b914d069aad0ef9fab2b4456cbdd/stdlib/public/runtime/MetadataLookup.cpp#L1523-L1563
void DynamicReplacementDescriptor::enableReplacement() const {
    // 拿到根节点
  auto *chainRoot = const_cast<DynamicReplacementChainEntry *>(
      replacedFunctionKey->root.get());

  // 通过遍历链表来保证这个方法是 enabled 的
  for (auto *curr = chainRoot; curr != nullptr; curr = curr->next) {
    if (curr == chainEntry.get()) {
            // 如果在 Replacement 链中发现了这个方法,说明已经 enable,中断操作
      swift::swift_abortDynamicReplacementEnabling();
    }
  }

  // 将 Root 节点的 imp 保存到 current,并将 current 头插
  auto *currentEntry =
      const_cast<DynamicReplacementChainEntry *>(chainEntry.get());
  currentEntry->implementationFunction = chainRoot->implementationFunction;
  currentEntry->next = chainRoot->next;

  // Root 继续进行头插操作
  chainRoot->next = chainEntry.get();
    // Root 的 imp 换成了 replacement 实现
  chainRoot->implementationFunction = replacementFunction.get();
}

// 同理 disable 做逆操作
void DynamicReplacementDescriptor::disableReplacement() const {
  const auto *chainRoot = replacedFunctionKey->root.get();
  auto *thisEntry =
      const_cast<DynamicReplacementChainEntry *>(chainEntry.get());

  // Find the entry previous to this one.
  auto *prev = chainRoot;
  while (prev && prev->next != thisEntry)
    prev = prev->next;
  if (!prev) {
    swift::swift_abortDynamicReplacementDisabling();
    return;
  }

  // Unlink this entry.
  auto *previous = const_cast<DynamicReplacementChainEntry *>(prev);
  previous->next = thisEntry->next;
  previous->implementationFunction = thisEntry->implementationFunction;
}

我们发现 Swift 中处理每一个 dynamic 方法,会为其建立一个 dynamicReplacement 链表来记录实现记录。
那么也就是说不管我们对原来的 dynamic 做了多少次 @_dynamicReplacement ,其实现原则上都会被记录下来,这一点也说明Swift并未抛弃OC中连环hook的场景。
但是调用方法后的执行代码我始终没有找到对应的逻辑,所以无法判断 Swift 在调用时机做了哪些事情。

但是总会有大神想出办法,比如 Whirlwind ,既然我们无法找到调用的实现,那么另辟蹊径:既然 Swift 已经通过链式记录了所有的实现,那么在单元测试的时候应该会有这种逻辑测试

在根据关键字和文件后缀搜索了大量的单元测试文件后,我们发现了这个文件dynamic_replacement_chaining.swift 。我们注意到 L13 的执行命令:

// RUN: %target-build-swift-dylib(%t/%target-library-name(B)) -I%t -L%t -lA %target-rpath(%t) -module-name B -emit-module -emit-module-path %t -swift-version 5 %S/Inputs/dynamic_replacement_chaining_B.swift -Xfrontend -enable-dynamic-replacement-chaining

在命令参数中增加了 -Xfrontend -enable-dynamic-replacement-chaining ,第一反应:这个东西像 Build Settings 中的 Flags。
翻看了 Build Settings 中所有的 Compile Flags,将其尝试写入 Other Swift Flags 中:
在这里插入图片描述
重新运行一遍,奇迹发生了:
在这里插入图片描述
说明我们的实验和猜想是正确的,Swift 在处理 dynamic 是将所有实现的 imp 保存,并且也有办法根据记录的链表来触发实现。

@_dynamicReplacement 虽然在 Swift 5 中就已经带入了 Swift 中,但是在官方论坛和官方仓库中并未找到 Release 日志痕迹。
这个 PR 虽然已经开了一年之久,苹果在今年的 WWDC 之前就偷偷的 Merge 到了 master 分支上,不得不猜,Apple 是为了实现 SwiftUI,而专门定制的 Feature。

发布了249 篇原创文章 · 获赞 926 · 访问量 149万+

猜你喜欢

转载自blog.csdn.net/youshaoduo/article/details/101482771