OC方法交換swizzle詳細介紹——不再有盲點

来源:https://www.cnblogs.com/mddblog/archive/2019/06/29/11105450.html
-Advertisement-
Play Games

原文鏈接: "https://www.cnblogs.com/mddblog/p/11105450.html" 如果已經比較熟悉,可以跳過整體介紹,直接看常見問題部分 整體介紹 方法交換是runtime的重要體現,也是"消息語言"的核心。OC給開發者開放了很多介面,讓開發者也能全程參與這一過程。 原 ...


原文鏈接:https://www.cnblogs.com/mddblog/p/11105450.html

如果已經比較熟悉,可以跳過整體介紹,直接看常見問題部分

整體介紹

方法交換是runtime的重要體現,也是"消息語言"的核心。OC給開發者開放了很多介面,讓開發者也能全程參與這一過程。

原理

oc的方法調用,比如[self test]會轉換為objc_msgSend(self,@selfector(test))。objc_msgsend會以@selector(test)作為標識,在方法接收者(self)所屬類(以及所屬類繼承層次)方法列表找到Method,然後拿到imp函數入口地址,完成方法調用。

typedef struct objc_method *Method;

// oc2.0已廢棄,可以作為參考
struct objc_method {
    SEL _Nonnull method_name;
    char * _Nullable method_types;
    IMP _Nonnull method_imp;
}

基於以上鋪墊,那麼有兩種辦法可以完成交換:

  • 一種是改變@selfector(test),不太現實,因為我們一般都是hook系統方法,我們拿不到系統源碼,不能修改。即便是我們自己代碼拿到源碼修改那也是編譯期的事情,並非運行時(跑題了。。。)
  • 所以我們一般修改imp函數指針。改變sel與imp的映射關係;
系統為我們提供的介面

typedef struct objc_method *Method;Method是一個不透明指針,我們不能夠通過結構體指針的方式來訪問它的成員,只能通過暴露的介面來操作。

介面如下,很簡單,一目瞭然:

#import <objc/runtime.h>

/// 根據cls和sel獲取實例Method
Method _Nonnull * _Nullable class_getInstanceMethod(Class _Nullable cls, SEL _Nonnull name);

/// 給cls新增方法,需要提供結構體的三個成員,如果已經存在則返回NO,不存在則新增並返回成功
BOOL class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, 
                const char * _Nullable types)

/// method->imp
IMP _Nonnull method_getImplementation(Method _Nonnull m);

/// 替換
IMP _Nullable class_replaceMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, 
                    const char * _Nullable types)

/// 跟定兩個method,交換它們的imp:這個好像就是我們想要的
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2);
簡單使用

假設交換UIViewController的viewDidLoad方法

/// UIViewController 某個分類

+ (void)swizzleInstanceMethod:(Class)target original:(SEL)originalSelector swizzled:(SEL)swizzledSelector {
    Method originMethod = class_getInstanceMethod(target, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(target, swizzledSelector);
    method_exchangeImplementations(originMethod, swizzledMethod);
}

+ (void)load {
    [self swizzleInstanceMethod:[UIViewController class] original:@selector(viewDidLoad) swizzled:@selector(swizzle_viewDidLoad)];
}
/// hook
- (void)swizzle_viewDidLoad {
    [self swizzle_viewDidLoad];
}

交換本身簡單:原理簡單,介面方法也少而且好理解,因為結構體定義也就三個成員變數,也難不到哪裡去!

但是,具體到使用場景,疊加上其它外部的不穩定因素,想要穩定的寫出通用或者半通用交換方法,上面的"簡單使用"遠遠不夠的。

下麵就詳細介紹下幾種常見坑,也是為啥網上已有很多文章介紹方法交換,為什麼還要再寫一篇的原因:不再有盲點

常見問題一、被多次調用(多次交換)

"簡單使用"中的代碼用於hook viewDidload一般是沒問題的,+load 方法一般也執行一次。但是如果一些程式員寫法不規範時,會造成多次調用。

比如寫了UIViewController的子類,在子類裡面實現+load方法,又習慣性的調用了super方法

+ (void)load {
    // 這裡會引起UIViewController父類load方法多次調用
    [super load];
}

又或者更不規範的調用,直接調用load,類似[UIViewController load]

為了沒盲點,我們擴展下load的調用:
  • load方法的調用時機在dyld映射image時期,這也符合邏輯,載入完調用load。
  • 類與類之間的調用順序與編譯順序有關,先編譯的優先調用,繼承層次上的調用順序則是先父類再子類;
  • 類與分類的調用順序是,優先調用類,然後是分類;
  • 分類之間的順序,與編譯順序有關,優先編譯的先調用;
  • 系統的調用是直接拿到imp調用,沒有走消息機制;

手動的[super load]或者[UIViewController load]則走的是消息機制,分類的會優先調用,如果你運氣好,另外一個程式員也實現了UIViewController的分類,且實現+load方法,還後編譯,則你的load方法也只執行一次;(分類同名方法後編譯的會“覆蓋”之前的)

為了保險起見,還是:

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self swizzleInstanceMethod:[UIViewController class] original:@selector(viewDidLoad) swizzled:@selector(swizzle_viewDidLoad)];
    });
}
繼續擴展:多次調用的副作用是什麼呢?
  • 根據原理,如果是偶數次

結果就是方法交換不生效,但是有遺留問題,這時手動調用

- (void)swizzle_viewDidLoad {
    [self swizzle_viewDidLoad];
}

會引起死迴圈。

其實,方法交換後,任何時候都不要嘗試手動調用,特別是交換的系統方法。實際開發中,也沒人會手動調用,這裡我們只討論這種場景的技術及後果,幫助理解

  • 奇數次調用

奇數次之後一切正常。但是,奇數次之前,它會先經歷偶數次。

比如,第一次交換,正常,第二次交換,那麼相當於沒有交換,如果你手動調用了swizzle_viewDidLoad,很明顯死迴圈了,然後你又在其它線程進行第三次交換,又不死迴圈了。哈哈,好玩,但你要保重,別玩失火了玩到線上了!!!

這種情況還是有可能發生的,比如交換沒有放在load方法,又沒有dispatch_once,而是自己寫了個類似start的開始方法,被自己或者他人誤調用。

最後:為了防止多次交換始終加上dispatch_once,除非你清楚你自己在幹啥。

再次擴展:常見的多次交換

這裡說的多次交換,和上面說的不一樣,交換方法不一樣,比如我們開發中經常遇到的。

我們自己交換了viewDidLoad,然後第三方庫也交換了viewDidLoad,那麼交換前(箭頭代表映射關係):

sysSel -> sysImp
ourSel -> ourImp
thirdSel -> thirdImp

第一步,我們與系統交換:

sysSel -> ourImp
ourSel -> sysImp
thirdSel -> thirdImp

第二步,第三方與系統交換:

sysSel -> thirdImp
ourSel -> sysImp
thirdSel -> ourImp

假設,push了一個VC,首先是系統的sysSel,那麼調用順序:

thirdImp、ourImp、sysImp

沒毛病!

多次交換這種場景是真實存在的,比如我們監控viewDidload/viewWillappear,在程式退到後臺時,想停止監控,則再進行一次(偶數)交換也是一種取消監控的方式。當再次進入前臺時,則再次(奇數)交換,實現監控。(通過標誌位實現用的更多,更簡單)

問題二、被交換的類沒有實現該方法

我們還是在分類裡面添加方法來交換

情況一:父類實現了被交換方法

我們本意交換的是子類方法,但是子類沒有實現,父類實現了class_getInstanceMethod(target, swizzledSelector);執行的結果返回父類的Method,那麼後續交換就相當於和父類的方法實現了交換。

一般情況下也不會出問題,可是埋下了一系列隱患。如果其它程式員也繼承了這個父類。舉例代碼如下

/// 父類
@interface SuperClassTest : NSObject
- (void)printObj;
@end
@implementation SuperClassTest
- (void)printObj {
    NSLog(@"SuperClassTest");
}
@end

/// 子類1
@interface SubclassTest1 : SuperClassTest
@end
@implementation SubclassTest1
- (void)printObj {
    NSLog(@"printObj");
}
@end

/// 子類2
@interface SubclassTest2 : SuperClassTest
@end
@implementation SubclassTest2
/// 有沒有重寫此方法,會呈現不同的結果
- (void)printObj {
    // 有沒有調用super  也是不同的結果
    [super printObj];
    NSLog(@"printObj");
}
@end

/// 子類1 分類實現交換

+ (void)swizzleInstanceMethod:(Class)target original:(SEL)originalSelector swizzled:(SEL)swizzledSelector {
    Method originMethod = class_getInstanceMethod(target, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(target, swizzledSelector);
    method_exchangeImplementations(originMethod, swizzledMethod);
}
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self swizzleInstanceMethod:[SubclassTest1 class] original:@selector(printObj) swizzled:@selector(swiprintObj)];
    });
}

- (void)swiprintObj {
    NSLog(@"swi1:%@",self);
    [self swiprintObj];
}

示例代碼,實現了printObjswiprintObj的交換。

  • 問題1:父類的實例對象正常調用printObj,也會造成swiprintObj優先調用,然後再調用printObj,這不是我們想要的,如果你想監控父類,那麼完全可以直接交換父類的方法;
  • 問題2:假設sub2(子類2)沒有實現printObj,但它的實例對象也調用了printObj,正常應該是能夠調用父類的printObj方法,但是由於被交換,會調用sub1的swiprintObj,swiprintObj的實現裡面有[self swiprintObj],這裡的self是sub2,sub2是沒有實現swiprintObj的,直接崩潰。
  • 問題3:sub2子類重寫了printObj,一切正常,sub2實例對象調用正常,但是如果在printObj裡面調用super方法就。。。

那麼如何避免這種情況呢?

使用class_addMethod方法來避免。再次優化後的結果:

+ (void)swizzleInstanceMethod:(Class)target original:(SEL)originalSelector swizzled:(SEL)swizzledSelector {
    Method originMethod = class_getInstanceMethod(target, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(target, swizzledSelector);
    if (class_addMethod(target, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))) {
        class_replaceMethod(target, swizzledSelector, method_getImplementation(originMethod), method_getTypeEncoding(originMethod));
    }
    else {
        method_exchangeImplementations(originMethod, swizzledMethod);
    }
}

分步驟詳細解析如下:

  • class_addMethod 執行前

superSel -> superImp
sub1SwiSel -> sub1SwiImp

  • class_addMethod 執行後,給子類增加了sel,但是對應的imp實現還是swizzledMethod的imp即交換方法的imp

superSel -> superImp
sub1Sel -> sub1SwiImp
sub1SwiSel -> sub1SwiImp

被交換的方法sub1Sel已經指向了交換方法的imp實現,下一步將交換方法的sel 指向被交換方法的imp即可。被交換方法不是沒有實現嗎??? 有的,OC繼承關係,父類的實現就是它的實現superImp

  • class_replaceMethod,將sub1SwiSel的實現替換為superImp

superSel -> superImp
sub1Sel -> sub1SwiImp
sub1SwiSel -> superImp

系統在給對象發送sel消息時,執行sub1SwiImp,sub1SwiImp裡面發送sub1SwiSel,執行superImp,完成hook。

我們說的給子類新增method,其實並不是一個全新的,而是會共用imp,函數實現沒有新增。這樣的好處是superSel對應的imp沒有改變,它自己的以及它的其它子類不受影響,完美解決此問題;但是繼續往下看其它問題

情況2:父類也沒有實現

尷尬了,都沒有實現方法,那還交換個錘子???

先說結果吧,交換函數執行後,方法不會被交換,但是手動調用下麵這些,同樣會死迴圈。

- (void)swiprintObj {
    NSLog(@"swi1:%@",self);
    [self swiprintObj];
}

所以我們要加判斷,然後返回給方法調用者一個bool值,或者更直接一點,拋出異常。

/// 交換類方法的註意獲取meta class, object_getClass。class_getClassMethod
+ (void)swizzleInstanceMethod:(Class)target original:(SEL)originalSelector swizzled:(SEL)swizzledSelector {
    Method originMethod = class_getInstanceMethod(target, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(target, swizzledSelector);
    if (originMethod && swizzledMethod) {
        if (class_addMethod(target, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))) {
            class_replaceMethod(target, swizzledSelector, method_getImplementation(originMethod), method_getTypeEncoding(originMethod));
        }
        else {
            method_exchangeImplementations(originMethod, swizzledMethod);
        }
    }
    else {
        @throw @"originalSelector does not exit";
    }
}

再加上 dispatch_once 上面已經算是比較完美了,但是並沒有完美,主要是場景不同,情況就不同。我們只有理解原理,不同場景不同對待。

最後補充一點:新建類來交換系統方法

上面說的都是在分類裡面實現交換方法,這裡新建"私有類"來交換系統方法。

在寫SDK時,分類有重名覆蓋問題,編譯選項還要加-ObjC。出問題編譯階段還查不出來。那麼我們可以用新建一個私有類實現交換,類重名則直接編譯報錯。交換方法和上面的分類交換稍不一樣

比如hook viewDidload,代碼如下:

@interface SwizzleClassTest : NSObject
@end

@implementation SwizzleClassTest
+ (void)load {
    /// 私有類,可以不用dispatch_once
    Class target = [UIViewController class];
    Method swiMethod = class_getInstanceMethod(self, @selector(swi_viewDidLoad));
    Method oriMethod = class_getInstanceMethod(target, @selector(viewDidLoad));
    if (swiMethod && oriMethod) {
        if (class_addMethod(target, @selector(swi_viewDidLoad), method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod))) {
            // 這裡獲取給UIViewController新增的method
            swiMethod = class_getInstanceMethod(target, @selector(swi_viewDidLoad));
            method_exchangeImplementations(oriMethod, swiMethod);
        }
    }
}

- (void)swi_viewDidLoad {
    // 不能調用,這裡的self是UIViewController類或者子類的實例,調用test的話直接崩潰。或者做類型判斷 [self isKindOfClass:[SwizzleClassTest class]],然後再調用
    // [self test];
    [self swi_viewDidLoad];
}

- (void)test {
    NSLog(@"Do not do this");
}

@end

這裡也用到class_addMethod,給UIViewController新增了一個swi_viewDidLoad sel及其imp實現,共用了SwizzleClassTest 的imp實現。

另外系統發送viewdidload消息進而調用swi_viewDidLoad方法,裡面的self是UIViewController,所以不能再[self test],否則崩潰。也不能在其它地方手動[self swi_viewDidLoad];會死迴圈,因為這時候self是SwizzleClassTest,而它的method是沒有被交換的,好處是我們可以通過self的類型判斷來避免。

可以比較下交換前後,

交換前:

SwizzleClassTest_swi_viewDidLoadSel -> SwizzleClassTest_swi_viewDidLoadImp

UIViewController_viewDidLoadSel -> UIViewController_viewDidLoadImp

交換後:

SwizzleClassTest_swi_viewDidLoadSel -> SwizzleClassTest_swi_viewDidLoadImp

UIViewController_swi_viewDidLoadSel -> UIViewController_viewDidLoadImp
UIViewController_viewDidLoadSel -> UIViewController_swi_viewDidLoadImp

可以看出 SwizzleClassTest 沒有受影響,映射關係不變。

這種想取消的話,也很簡單method_exchangeImplementations

總結

  • 首先要知道方法交換的原理;
  • 熟悉它常用介面;
  • 被交換方法不存在引發的 父類、子類問題;
  • 以及oc中方法的繼承、“覆蓋”問題;
  • 可能引發重覆交換的問題,以及後果;
  • 理解self只是個隱藏參數,並不一定是當前方法所在的類的實例對象

最後,每次使用方法交換時,都要認真推演一遍,計算下可能產生的影響。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 工廠顧名思義就是創建產品,根據產品是具體產品還是具體工廠可分為簡單工廠模式和工廠方法模式,根據工廠的抽象程度可分為工廠方法模式和抽象工廠模式。該模式用於封裝和管理對象的創建,是一種創建型模式。本文從一個具體的例子逐步深入分析,來體會三種工廠模式的應用場景和利弊。 1. 簡單工廠模式 該模式對對象創建 ...
  • 1、簡介 首先,他是一個全新的基於容器技術的分散式架構領先方案。Kubernetes(k8s)是Google開源的容器集群管理系統(谷歌內部:Borg)。在Docker技術的基礎上,為容器化的應用提供部署運行、資源調度、服務發現和動態伸縮等一系列完整功能,提高了大規模容器集群管理的便捷性。 Kube ...
  • 各位讀者朋友們,好久不見了! 最近博主一直在忙於工作以及寫《Flutter入門與應用實戰》的書,所以沒有時間打理博客。今天來給大家分享一個博主在GitChat上發起的一場Chat。 下麵是本場Chat的簡介: Flutter 是 Google 發佈的 UI 框架,可以快速在 iOS 和 Androi ...
  • 如果第二次看到我的文章,歡迎右側掃碼訂閱我喲~ 👉 每周五早8點 按時送達。當然了,也會時不時加個餐~ 這篇是「分散式系統理論」系列的第22篇,也是最後一篇。我們來聊聊分散式系統中的最後一道保障——監控。 監控這個事情,有點像我們平時對人的健康體檢。想要效果好、結果靠譜,就得“全面體檢”,每一項都 ...
  • 如果第二次看到我的文章,歡迎右側掃碼訂閱我喲~ 👉 每周五早8點 按時送達。當然了,也會時不時加個餐~ 是的,這份禮物最佳受眾是程式員。但是,如果你不是程式員,相信這些能使你更懂程式員,能更好的與他們交流。 有些小伙伴們應該知道了,之前的《分散式系統關註點——360°的全方位監控》是我去年開始寫的 ...
  • 在springMVC controller中返回json數據出現亂碼問題,因為沒有進行編碼,只需要簡單的註解就可以了 在@RequestMapping()中加入produces="text/html;charset=UTF-8"屬性即可,如下: ...
  • 1.Java 發展史 1991年01月 Sun公司成立了Green項目小組,專攻智能家電的嵌入式控制系統 1991年02月 放棄C++,開 發新語言,命名為“Oak” 1991年06月 JamesGosling開發了Oak的解釋器 1992年01月 Green完成了Green操作系 統、Oak語言、 ...
  • 本文續接上一章ArrayList原理及使用,對ArrayList中的常用方法subList進行了剖析,從源碼的角度對通過subList方法得到的集合和原集合有何關係,有何不同點,從而避免工作中遇到各種坑 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...