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

原文連接:http://www.javashuo.com/article/p-olcodhgo-dp.htmlhtml

若是對方法交換已經比較熟悉,能夠跳過總體介紹,直接看常見問題部分程序員

總體介紹

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

原理

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是一個不透明指針,咱們不可以經過結構體指針的方式來訪問它的成員,只能經過暴露的接口來操做。this

接口以下,很簡單,一目瞭然:spa

#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];
}

交換自己簡單:原理簡單,接口方法也少並且好理解,由於結構體定義也就三個成員變量,也難不到哪裏去!指針

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

下面就詳細介紹下幾種常見坑,也是爲啥網上已有不少文章介紹方法交換,爲何還要再寫一篇的緣由:再也不有盲點

常見問題1、被屢次調用(屢次交換)

"簡單使用"中的代碼用於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,在程序退到後臺時,想中止監控,則再進行一次(偶數)交換也是一種取消監控的方式。當再次進入前臺時,則再次(奇數)交換,實現監控。(經過標誌位實現用的更多,更簡單)

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

咱們仍是在分類裏面添加方法來交換

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

咱們本意交換的是子類方法,可是子類沒有實現,父類實現了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時,都要認真推演一遍,計算下可能產生的影響。

相關文章
相關標籤/搜索