轉自公眾號《讓技術一瓜公食》,文章底部掃碼關注。
引子
隨著六月份的 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 工程中實現這種場景。
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 的場景呢?從程式碼層面我沒有想到任何辦法。
從 Swift 原始碼來猜測 @_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 在呼叫時機做了哪些事情。
通過 Unit Test 解決問題
以下思路是朋友 @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)也是如此。這種反社群未經同意的情況下,為了自己的技術產品而定製語言是一件好事嗎?
這裡腦補一個黑人問號,也許明年的我會說出“真香”!