转自公众号《让技术一瓜公食》,文章底部扫码关注。git
随着六月份的 WWDC 上对 SwiftUI 的发布,感受 Swift 有变成了炽手可热的话题。在大会结束后,发现了有这么几条 Twitter 在讨论一个叫作 @_dynamicReplacement(for:)
的新特性。程序员
这是一个什么东西呢,因而我在 Swift 社区中也检索了对应的关键字,看到一个 Dynamic Method Replacement 的帖子**。**在爬了多层楼以后,大概看到了使用的方式(环境是 macOS 10.14.5,Swift 版本是 5.0,注意如下 Demo 只能在工程中跑,Playground 会报 error: Couldn't lookup symbols:
错误)。github
class Test {
dynamic func foo() {
print("bar")
}
}
extension Test {
@_dynamicReplacement(for: foo())
func foo_new() {
print("bar new")
}
}
Test().foo() // bar new
复制代码
看到这里是否是眼前一亮?咱们期待已久的 Method Swizzling 仿佛又回来了?swift
开始的时候只是惊喜,可是在平时的我的开发中,其实不多会用到 hook 逻辑(固然这里说的不是公司项目)。直到有一天,朋友遇到了一个问题,因而又对这个东西作了一次较为深刻的研究 ....bash
首先咱们先写一段 ObjC 中 Method Swizzling 的场景:app
//
// 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。frontend
在执行处,咱们来调用一下 - sayWords
方法:ide
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。
上面的输出 ABC
场景,是我朋友遇到的。在制做一个根据动态库来动态加载插件的研发工具链的时候,在主工程会开放一些接口,模拟 Ruby 的 alias_method
写法,这样就能够将自定义的实现注入到下层方法中,从而扩展实现。固然这种能力暴露的方案不是很好,只是一种最粗暴的插件方案实现方法。
固然咱们今天要说的不是 ObjC,由于 ObjC 在 Runtime 机制上都是能够预期的。若是咱们使用 Swift 5.0 中 Dynamic Method Replacement 方案在 Swift 工程中实现这种场景。
import UIKit
class Person {
dynamic func sayWords() {
print("A")
}
}
extension Person {
@_dynamicReplacement(for: sayWords())
func sayWordsB() {
print("B")
sayWords()
}
}
extension Person {
@_dynamicReplacement(for: sayWords())
func sayWordsC() {
print("C")
sayWords()
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
Person().sayWords()
}
}
复制代码
从视觉角度上来看,经过对 Swift Functions 的显式声明(有种 Swift 真香的感受),咱们完成了对于 Method Swizzling 的实现。跑一下代码,发现运行结果却不如咱们的预期:
C
A
复制代码
为何结果只显示两个?我交换了一下两个 extension
的顺序继续尝试,其打印结果又变成了 BA
。因而差很少能够总结一下规律,在执行顺序上,后声明的将会生效。那么应该如何实现这种连环 Hook 的场景呢?从代码层面我没有想到任何办法。
@_dynamicReplacement
实现按照正常程序员的逻辑,若是咱们在重构一个模块的代码,新的模块代码不管从功能仍是效率上,都应该优于以前的方式、覆盖以前全部的逻辑场景。若是 Swift 支持这种连环修改的场景,那这个新的 Feature 放出实际上是功能不完备的!因而咱们开始翻看 Swift 这个 Feature 的 PR 代码,来一探 Dynamic Method Replacement 的原理。
首先来看这个 Dynamic Method Replacement 特性的 Issue-20333,做者上来就贴了两段颇有意思的代码:
/// 片断一
// 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 在调用时机作了哪些事情。
如下思路是朋友 @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 日志痕迹,而我是从 Twitter 友的口头才了解到的。而且这个 PR 虽然已经开了一年之久,苹果在今年的 WWDC 以前就偷偷的 Merge 到了 master
分支上。
**不得不猜,Apple 是为了实现 SwiftUI,而专门定制的 Feature。**另外,在其余的一些特性中也能看到这种现象,例如 Function Builder(为了实现 SwiftUI 的 DSL),Combine(为了实现 SwiftUI 中的 Dataflow & Binding)也是如此。这种反社区未经赞成的状况下,为了本身的技术产品而定制语言是一件好事吗?
这里脑补一个黑人问号,也许明年的我会说出“真香”!