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


原文鏈接: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

最后補充一點:C函數 實現交換

這里講的是用C函數交換系統類的方法。而不是fishhook的hook C的函數,目標不一樣。原理也不一樣

還以hook UIViewControllerviewDidLoad為例

上面說到,oc方法調用會轉換為objc_msgSend(self,_cmd,param)這種形式,這里再補充一點,objc_msgSend找到imp函數指針后,最終會是imp(self,_cmd,param)調用C函數,imp其實就是個C函數指針。

那么我們可以定義一個C函數,讓sel和我們新建的C函數(imp)形成映射。另外還需要記錄之前的imp實現,可以定義一個函數指針來保存sel之前的imp實現;大概示意:

之前:
pOriImp = NULL
vcSel -> vcImp
Cfun(){};

之后:

pOriImp = vcImp;
vcSel -> cFun;// 函數名即為函數指針

詳細如下:

/// 准備1. 定義一個函數指針,用於記錄系統原本的IMP實現,並初始化為NULL
void (*origin_test_viewDidload)(id,SEL) = NULL;

/// 准備2. 定義要交換的函數,里面會調用系統的IMP
static void swizzle_test_viewDidload(id self, SEL _cmd)
{
    // 這里打印的self為UIViewController或者子類實例
    NSLog(@"%@",self);
    if (origin_test_viewDidload) {
        origin_test_viewDidload(self, _cmd);
    }
}

/// 開始交換。startHook可以是某個類的方法或實例方法或C函數都可以
+ (void)startHook {
   static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class target = [UIViewController class];
        SEL oriSel = @selector(viewDidLoad);
        // 要交換的函數
        IMP swiImp = (IMP)swizzle_test_viewDidload;
        Method origMethod = class_getInstanceMethod(target, oriSel);
        // 替換之前的先保留
        origin_test_viewDidload = (void *)method_getImplementation(origMethod);
        if (origin_test_viewDidload) {
            // 最后替換,這里用到了set
            method_setImplementation(origMethod, swiImp);
        }
    });
}

這種hook,沒有給類的MethodList新增Method,只是替換了實現,對原類改動最小。

和其它hook方式一樣,這種對第三方庫 的hook,也是不影響。如果第三方庫也交換了,均會得到調用

最后,如果你想取消hook,很簡單,method_setImplementation為原來的IMP即可。記着把origin_test_viewDidload也置為NULL.

總結

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

最后,大概三類hook,至於想用哪種,其實無所謂了,看具體場景。但是原理一定要清楚,每次hook時,都要認真推演一遍,計算下可能產生的影響。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM