iOS Runtime簡單介紹,以及不同類的Method Swizzling

茉莉兒發表於2017-07-02

(同步簡書更新)

Runtime介紹:

runtime顧名思義就是執行時,其實我們的App從你按下command+R開始一直到App執行起來經歷了大致兩個階段,1:編譯時,2:執行時。還記得一道很經典的面試題

這裡給大家解釋下:首先, * testObject 是告訴編譯器,testObject是一個指向某個Objective-C物件的指標。因為不管指向的是什麼型別的物件,一個指標所佔的記憶體空間都是固定的,所以這裡宣告成任何型別的物件,最終生成的可執行程式碼都是沒有區別的。這裡限定了NSString只不過是告訴編譯器,請把testObject當做一個NSString來檢查,如果後面呼叫了非NSString的方法,會產生警告。接著,你建立了一個NSData物件,然後把這個物件所在的記憶體地址儲存在testObject裡。那麼執行時(從這段程式碼執行開始,到程式結束),testObject指向的記憶體空間就是一個NSData物件。你可以把testObject當做一個NSData物件來用。 所以編譯時是NSString,執行時是NSData。
runtime是什麼:
在runtime中,所有的類在OC中都會被定義成一個結構體,像這樣
類在runtime中的表示
struct objc_class {
Class isa;//指標,顧名思義,表示是一個什麼, //例項的isa指向類物件,類物件的isa指向元類

#if !__OBJC2__
        Class super_class;  //指向父類
        const char *name;  //類名
        long version;     //類的版本資訊,預設初始化為 0。我們可以在執行期對其進行修改(class_setVersion)或獲取(class_getVersion)。
        long info;   /*供執行期使用的一些位標識。有如下一些位掩碼:
                        CLS_CLASS (0x1L) 表示該類為普通 class ,其中包含例項方法和變數;
                      CLS_META (0x2L) 表示該類為 metaclass,其中包含類方法;
                      CLS_INITIALIZED (0x4L) 表示該類已經被執行期初始化了,這個標識位只被 objc_addClass 所設定;
                      CLS_POSING (0x8L) 表示該類被 pose 成其他的類;(poseclass 在ObjC 2.0中被廢棄了);
                      CLS_MAPPED (0x10L) 為ObjC執行期所使用
                      CLS_FLUSH_CACHE (0x20L) 為ObjC執行期所使用
                      CLS_GROW_CACHE (0x40L) 為ObjC執行期所使用
                      CLS_NEED_BIND (0x80L) 為ObjC執行期所使用
                      CLS_METHOD_ARRAY (0x100L) 該標誌位指示 methodlists 是指向一個 objc_method_list 還是一個包含 objc_method_list 指標的陣列;*/
        long instance_size  //該類的例項變數大小(包括從父類繼承下來的例項變數);
        struct objc_ivar_list *ivars //成員變數列表
        struct objc_method_list **methodLists; //方法列表
        struct objc_cache *cache;//快取   一種優化,呼叫過的方法存入快取列表,下次呼叫先找快取
        struct objc_protocol_list *protocols //協議列表
        #endif
} OBJC2_UNAVAILABLE;複製程式碼

相關的定義
/// 描述類中的一個方法
typedef struct objc_method *Method;

/// 例項變數
typedef struct objc_ivar *Ivar;

/// 類別Category
typedef struct objc_category *Category;

/// 類中宣告的屬性
typedef struct objc_property *objc_property_t;

ObjC 為每個類的定義生成兩個 objc_class ,一個即普通的 class,另一個即 metaclass。我們可以在執行期建立這兩個 objc_class 資料結構,然後使用 objc_addClass 動態地建立新的類定義。

runtime能幹什麼:
  • :1:獲取一個類中的列表比如方法列表、屬性列表、協議列表、成員變數列表像如下這樣 其中獲取到的屬性、方法都是可以獲取public和private的。
unsigned int count;
    Class clas = [WKWebViewController class]; //是我自己的類,之所以不用系統的類是因為系統的類方法屬性太多了

    objc_property_t * propertyList = class_copyPropertyList(clas, &count);
    for (int i = 0; i < count; i++) {
        const char *propertyName = property_getName(propertyList[i]);
        NSLog(@"  %@  屬性(包括私有) -------->>>>>    %@",clas,[NSString stringWithUTF8String:propertyName]);
    }
    NSLog(@"-------------------------------------------------------------------------------------------------------------- ");

    Method * methodList = class_copyMethodList(clas, &count);
    for (int i = 0; i < count; i++) {
        Method methodName = methodList[i];
        NSLog(@"  %@ 方法(包括私有)  -------->>>>>    %@",clas,NSStringFromSelector(method_getName(methodName)));
    }
    NSLog(@"-------------------------------------------------------------------------------------------------------------- ");


    Ivar *ivarList = class_copyIvarList(clas, &count);
    for (int i = 0; i>>>> %@",clas, [NSString stringWithUTF8String:ivarName]);
    }
    NSLog(@"-------------------------------------------------------------------------------------------------------------- ");


    //獲取協議列表
    __unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);
    for (int i = 0; i>>>> %@",clas, [NSString stringWithUTF8String:protocolName]);
    }複製程式碼

輸出後的結果是

image.png
image.png

其中也包括了私有方法。

  • 2:攔截方法呼叫
    有的時候我們用一個類或者一個例項變數去呼叫一個方法,由於操作失誤或者是其他原因,導致這個所被呼叫的方法並不存在,報出這樣的錯誤,然後閃退!
    image.png
    image.png

這個時候如果我們想避免這些崩潰,我們就需要在執行時對其做一些手腳。iOS中方法呼叫的流程:其實呼叫方法就是傳送訊息,所有呼叫方法的程式碼例如 [obj aaa] 在執行時runtime會將這段程式碼轉換為objc_msgSend(obj, [@selector]);(本質就是傳送訊息)然後obj會通過其中isa指標去該類的快取中(cache)查詢對應函式的Method, 如果沒有找到,再去該類的方法列表(methodList)中查詢,如果沒有找到再去該類的父類找,如果找到了,就先將方法新增到快取中,以便下次查詢,然後通過method中的指標定位到指定方法執行。如果一直沒有找到,便會走完如下四個方法之後崩潰。

/**
    如果呼叫的是不存在的例項方法則會在奔潰前進入該方法,防止崩潰可以在此處做處理
 */
+(BOOL)resolveInstanceMethod:(SEL)sel {
    return YES;
}

/**
 如果呼叫的是不存在的類方法則會在奔潰前進入該方法,防止崩潰可以在此處做處理
 */
+(BOOL)resolveClassMethod:(SEL)sel {
    return YES;
}

/**
 這個方法會把你所呼叫的不存在的方法重定向到一個宣告瞭該方法的類中,只需要你返回一個有該方法的
 類就可以,如果你重定向的這個類仍然不具有該方法那麼會繼續崩潰
 */
-(id)forwardingTargetForSelector:(SEL)aSelector {

}

/**
 將你不存在的方法打包成NSInvocation物件,做完你自己的處理之後
 呼叫invokeWithTarget讓某個target來處理該方法
 */
-(void)forwardInvocation:(NSInvocation *)anInvocation {
    [anInvocation invokeWithTarget:self];
}複製程式碼

3:動態新增方法
因為我們呼叫了一個不存在的方法導致崩潰,那麼我們在判斷出不存在後就動態新增上一個方法吧 這樣不就不會蹦了嗎?我們先寫一個方法用來給我們做出提示

- (void) errorMethod {
    NSLog(@"no method!!!!!!!");
}複製程式碼

如果呼叫了沒有的方法,那麼就把這個方法新增進去,然後把被呼叫的方法的指標指向這個error1:,那麼一旦呼叫了沒有的方法就會走這個。我們來看程式碼

+(BOOL)resolveInstanceMethod:(SEL)sel {
    Method errorMethod =  class_getInstanceMethod([self class], @selector(errorMethod));
    if ([NSStringFromSelector(sel) isEqualToString:@"testMethod"]) {
        BOOL isAdd =  class_addMethod([self class], sel, method_getImplementation(errorMethod), method_getTypeEncoding(errorMethod));
        NSLog(@"tinajia  = %d",isAdd);
    }
    //Do something
    return YES;
}複製程式碼

主要用到

/**
    新增方法
     @param class] 在哪個類裡新增
     @param sel 新增的方法的名字
     @param errorMethod 新增的方法的實現IMP指
     @param types 方法的標示符
     @return 是否新增成功
         */
BOOL isAdd =  class_addMethod([self class], sel, method_getImplementation(errorMethod), method_getTypeEncoding(errorMethod));複製程式碼

然後執行下:

WKWebViewController * vc= [[WKWebViewController alloc] init];
[vc performSelector:@selector(testMethod)];複製程式碼

我呼叫了並不存在的testMethod方法並沒有崩潰並且方法成功新增了

image.png
image.png

  • 4:動態交換方法(也叫iOS黑魔法,慎用)
    沒什麼好例子,用一個網上說的例子(引用別人的東西,懶得複製了,就接了圖)
    其實本質即使SEL和IMP的交換,原理是這樣的:在iOS中每一個類中都有一個叫dispatch table的東西,裡面存放在SEL 和他所對應的IMP指標,之前也說過方法呼叫就是通過sel找IMP指標然後指標定位呼叫方法。方法交換就是對這個dispatch table進行操作。讓A的SEL去對應B的IMP,B的SEL對應A的IMP,如圖
    這樣就達到方法交換的目的,下面看程式碼:
+ (void)changeMethod {
    //  如果是類方法 要使用 !
    //  如果是系統的集合類的屬性要用元類 比如 __NSSetM = NSMutableSet
    //  Class  class = NSClassFromString(@"__NSSetM");
    //  Class metaClass = objc_getMetaClass([NSStringFromClass(class) UTF8String]);
    Class systemClass = NSClassFromString(__NSSetM);

    SEL sel_System = NSSelectorFromString(addObject:);
    SEL sel_Custom = @selector(swizzle_addObject:);

    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_addObject:(id) obj {
    if (obj) {
        [self swizzle_addObject:obj];
    }
}複製程式碼

主要程式碼 method_exchangeImplementations(method1, method2); 這兩個引數很簡單,就是兩個需要交換的方法。
最後我呼叫了m1但是實際上走了m2。

#####動態交換方法的原理以及交換過程中指標的變化
在通常的方法交換中我們通常有兩種情景,一種是我會針對被交換的類建一個category,然後hook的方法會寫在category中。另一種是自己建立一個Tool類裡面放些常用的工具方法其中包含了方法交換。可能大家普遍選擇第一種方法,但是如果你需要hook的類非常多的(我實際專案中就遇到這樣的問題)那你就需要針對不同的類建立category,就會導致檔案過多,且每一個檔案中只有一個hook方法,這樣一來左側一堆檔案,所以我用了第二種方法,但是在使用過程中出現一個問題,先看下我的程式碼機構

image.png
image.png
我要hook的是ViewController中的viewDidLoad方法,我建立了兩個類一個是ViewController的category,另一個是Tool類,為了一會區別演示不同類hook的不同(兩個類中hook的程式碼完全一樣)

  • ViewController中將要被替換的系統方法
    被替換的方法(系統方法)
    被替換的方法(系統方法)
  • Category中將要用來替換的自定義方法
    用來替換的方法(自定義方法)
    用來替換的方法(自定義方法)
  • 然後在ViewController中的load中做方法替換
    進行方法替換
    進行方法替換

    執行一下的輸出結果想必大家已經猜到了先執行custom再執行system,這是通常情況下大家的做法。
    結果
    結果

    下面再來看下如果我將替換方法寫在不同類中會怎樣,呼叫Tool中的交換方法
    執行Tool中的交換方法
    執行Tool中的交換方法

    然後直接看結果了,因為程式碼都是一模一樣的我直接複製過去的
    結果
    結果

    發生了crash,原因是ViewController中沒有swizzel_viewDidLoad_custom這個方法,為什麼不同類的交換會出現這種問題,我們用個圖來說明下
    image.png
    image.png

    解決的辦法是我們在交換方法之前要先像其中新增方法,也就是說把customMethod新增到SystemClass中,但是注意要把customMethod的實現指向syetemMethod的實現。這樣一來就可以達到SystemClass呼叫customMethod卻執行systemMethod的程式碼的效果,實現以上要求我們需要在交換之前執行這個方法。
    class_addMethod(systemClass, sel_Custom, imp_System, method_getTypeEncoding(method_System))複製程式碼
    其中第一個引數是需要往哪個類新增;第二個引數是要新增的方法的方法名;第三個引數是所新增的方法的方法實現,第四個是方法的識別符號。經過就該之後我們的程式碼是這樣
.
.
之前的都一樣就省略
.
.
if (class_addMethod(systemClass, sel_Custom, imp_System, method_getTypeEncoding(method_System))) {
        class_replaceMethod(systemClass, sel_System, imp_Custom, method_getTypeEncoding(method_System));
    } else {
        method_exchangeImplementations(method_System, method_Custom);
    }複製程式碼

我們來看下執行完add操作之後此時的方法和類的對應關係(紅色的為add的修改)

關係
關係

因為SystemClass中本身不包含customMethod所以add一定是成功的,也就是說會進入判斷執行replace方法。

class_replaceMethod(systemClass, sel_System, imp_Custom, method_getTypeEncoding(method_System));複製程式碼

第一個引數:需要修改的方法的所在的類;第二個引數:需要替換其實現的方法名;第三個引數:需要把哪個實現替換給他;第四個引數:方法識別符號。此時看下我們做完replace之後的類與方法名以及他們實現的關係(紅色的為replace的修改)。

關係
關係

此時大家已經看出來了,雖然沒有執行exchange方法,但是我已經達到了方法交換的目的。系統執行systemMethod時候會走customMethod的實現但是因為在customMethod方法中我會遞迴執行[self customMethod],所以又會走到systemMethod的實現,因為之前進行了方法新增,所以此時A類中有了customMethod方法,不會再發生之前的crash。達到一個不同類進行Method Swizzling的目的。

綜上來看一個完整嚴謹的MethodSwizzling應該在交換前先add,並且add方法的引數不能錯
+ (void)changeMethod {

    Class systemClass = NSClassFromString(@"你的類");

    SEL sel_System = @selector(系統方法);
    SEL sel_Custom = @selector(你自己的方法);

    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);

    if (class_addMethod(systemClass, sel_Custom, imp_System, method_getTypeEncoding(method_System))) {
        class_replaceMethod(systemClass, sel_System, imp_Custom, method_getTypeEncoding(method_System));
    } else {
        method_exchangeImplementations(method_System, method_Custom);
    }
}複製程式碼
以上程式碼無論是寫在工具類中還是category中都是沒有問題的。

相關文章