method swizzling
每個類都有自己的方法列表,一個 Method = SEL + IMP + 方法簽名,SEL = 方法名稱字符串,IMP = 一個函數的起始地址。
method swizzling 就是把 SEL 對應的 IMP 和另一個 SEL 的 IMP 交換。操作要保證唯一性、原子性。在 +load 方法中實現;使用 dispatch_once。因爲沒有編譯器的安全保證,操作一定要仔細。
+load 與 +initialize:
+load 在類初次加載時調用(比main函數還早),initialize 在類內方法被第一次調用前調用。
使用場景比如 統計 view controller 顯示次數,直接寫在 viewWillAppear 會入侵業務代碼,一般會用 category,但是如果有很多類需要統計,就需要很多 category。可以只封裝一個工具類。
先介紹一下用 category (下面的代碼有一大部分可以簡化,只是爲了後面做鋪墊;只以 viewWillAppear 爲例,其他集合類、類方法有邏輯上相同):
// 這個函數在 +load 中調用,且 dispatch_once
+ (void)swizzleUIViewControllerViewWillAppear {
Class systemClass = NSClassFromString(@"UIViewController");
SEL sel_System = NSSelectorFromString(viewWillAppear:);
SEL sel_Custom = @selector(swizzle_viewWillAppear:); // 後面有定義
Method method_System = class_getInstanceMethod(systemClass, sel_System);
Method method_Custom = class_getInstanceMethod([self class], sel_Custom);
IMP imp_System = method_getImplementation(method_System);
IMP imp_Custom = method_getImplementation(method_Custom);
method_exchangeImplementations(method_System, method_Custom);
}
- (void)swizzle_viewWillAppear:(BOOL)animated {
// 這裏寫統計有關的代碼
// 標記1
// 實際調用系統的 viewWillAppear: (已交換地址)
[self swizzle_viewWillAppear:animated];
}
現在考慮用一個工具類。把上述代碼複製到 Tool 類中,實際使用中會報錯:unrecognized selector。考慮:vc 中調用 viewWillAppear(SEL)時,它的 IMP 實際指向 swizzle_viewWillAppear,因此會進入 Tool 類的這個函數。問題出現了:此時壓入棧中的 self 指針是 vc 類的實例指針,vc 類中根本就找不到 swizzle_viewWillAppear 這個 SEL!
解決辦法:給 vc 的類加上這個 Method 入口不就行了麼。最後一行改爲:
// 添加 custom_sel -> system_imp 的 Method
// 如果添加成功,只剩下把 system_sel 的 imp 替換爲 custom_imp,實現交換。
if (class_addMethod(systemClass, sel_Custom, imp_System, method_getTypeEncoding(method_System))) {
class_replaceMethod(systemClass, sel_System, imp_Custom, method_getTypeEncoding(method_System));
}
問題又來了,如果添加這個方法失敗了呢?也就是上面的代碼的 else 部分怎麼寫?
很多博客裏寫的是
else {
// 原 vc 類中已存在 custom_sel,直接交換實現:
method_exchangeImplementations(method_System, method_Custom);
}
好,現在假設 vc 中已存在 swizzle_viewWillAppear 這個函數:
vc 執行 viewWillAppear,由於已交換 IMP,代碼會運行至 標記1(前面有標註);
現在執行:[self swizzle_viewWillAppear:animated];
此時的 self 指針本質上是 vc,給 vc 發這條消息會走到 vc 類的 swizzle_viewWillAppear,而不是 Tool 類的。
因此最終代碼會走到 vc 類的 swizzle_viewWillAppear: 處,而不是目的地 viewWillAppear。
那麼 else 該怎麼寫?首先不能與其他業務有衝突。直接拋異常,提醒改名爲 swizzle_viewWillAppear23456。
KVO(isa swizzling)
https://www.dazhuanlan.com/2019/11/19/5dd39d302450d/