[iOS 理解] swizzling

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/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章