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

Runtime介紹:

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


這里給大家解釋下:首先, * testObject 是告訴編譯器,testObject是一個指向某個Objective-C對象的指針。因為不管指向的是什么類型的對象,一個指針?biāo)嫉膬?nèi)存空間都是固定的,所以這里聲明成任何類型的對象,最終生成的可執(zhí)行代碼都是沒有區(qū)別的。這里限定了NSString只不過是告訴編譯器,請把testObject當(dāng)做一個NSString來檢查,如果后面調(diào)用了非NSString的方法,會產(chǎn)生警告。接著,你創(chuàng)建了一個NSData對象,然后把這個對象所在的內(nèi)存地址保存在testObject里。那么運行時(從這段代碼執(zhí)行開始,到程序結(jié)束),testObject指向的內(nèi)存空間就是一個NSData對象。你可以把testObject當(dāng)做一個NSData對象來用。 所以編譯時是NSString,運行時是NSData。
runtime是什么:
在runtime中,所有的類在OC中都會被定義成一個結(jié)構(gòu)體,像這樣
類在runtime中的表示
struct objc_class {
Class isa;//指針,顧名思義,表示是一個什么, //實例的isa指向類對象,類對象的isa指向元類

#if !__OBJC2__
        Class super_class;  //指向父類
        const char *name;  //類名
        long version;     //類的版本信息,默認(rèn)初始化為 0。我們可以在運行期對其進(jìn)行修改(class_setVersion)或獲?。╟lass_getVersion)。
        long info;   /*供運行期使用的一些位標(biāo)識。有如下一些位掩碼:
                        CLS_CLASS (0x1L) 表示該類為普通 class ,其中包含實例方法和變量;
                      CLS_META (0x2L) 表示該類為 metaclass,其中包含類方法;
                      CLS_INITIALIZED (0x4L) 表示該類已經(jīng)被運行期初始化了,這個標(biāo)識位只被 objc_addClass 所設(shè)置;
                      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) 該標(biāo)志位指示 methodlists 是指向一個 objc_method_list 還是一個包含 objc_method_list 指針的數(shù)組;*/
        long instance_size  //該類的實例變量大小(包括從父類繼承下來的實例變量);
        struct objc_ivar_list *ivars //成員變量列表
        struct objc_method_list **methodLists; //方法列表
        struct objc_cache *cache;//緩存   一種優(yōu)化,調(diào)用過的方法存入緩存列表,下次調(diào)用先找緩存
        struct objc_protocol_list *protocols //協(xié)議列表
        #endif
} OBJC2_UNAVAILABLE;

相關(guān)的定義
/// 描述類中的一個方法
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。我們可以在運行期創(chuàng)建這兩個 objc_class 數(shù)據(jù)結(jié)構(gòu),然后使用 objc_addClass 動態(tài)地創(chuàng)建新的類定義。

runtime能干什么:
  • :1:獲取一個類中的列表比如方法列表、屬性列表、協(xié)議列表、成員變量列表像如下這樣 其中獲取到的屬性、方法都是可以獲取public和private的。
unsigned int count;
    Class clas = [WKWebViewController class]; //是我自己的類,之所以不用系統(tǒng)的類是因為系統(tǒng)的類方法屬性太多了
    
    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<count; i++) {
        Ivar myIvar = ivarList[i];
        const char *ivarName = ivar_getName(myIvar);
        NSLog(@"%@ 成員變量(包括私有) -------->>>>> %@",clas, [NSString stringWithUTF8String:ivarName]);
    }
    NSLog(@"-------------------------------------------------------------------------------------------------------------- ");
    
    
    //獲取協(xié)議列表
    __unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);
    for (int i = 0; i<count; i++) {
        Protocol *myProtocal = protocolList[i];
        const char *protocolName = protocol_getName(myProtocal);
        NSLog(@"%@ 協(xié)議 -------->>>>> %@",clas, [NSString stringWithUTF8String:protocolName]);
    }

輸出后的結(jié)果是


image.png

其中也包括了私有方法。

  • 2:攔截方法調(diào)用
    有的時候我們用一個類或者一個實例變量去調(diào)用一個方法,由于操作失誤或者是其他原因,導(dǎo)致這個所被調(diào)用的方法并不存在,報出這樣的錯誤,然后閃退!


    image.png

這個時候如果我們想避免這些崩潰,我們就需要在運行時對其做一些手腳。iOS中方法調(diào)用的流程:其實調(diào)用方法就是發(fā)送消息,所有調(diào)用方法的代碼例如 [obj aaa] 在運行時runtime會將這段代碼轉(zhuǎn)換為objc_msgSend(obj, [@selector]);(本質(zhì)就是發(fā)送消息)然后obj會通過其中isa指針去該類的緩存中(cache)查找對應(yīng)函數(shù)的Method, 如果沒有找到,再去該類的方法列表(methodList)中查找,如果沒有找到再去該類的父類找,如果找到了,就先將方法添加到緩存中,以便下次查找,然后通過method中的指針定位到指定方法執(zhí)行。如果一直沒有找到,便會走完如下四個方法之后崩潰。

/**
    如果調(diào)用的是不存在的實例方法則會在奔潰前進(jìn)入該方法,防止崩潰可以在此處做處理
 */
+(BOOL)resolveInstanceMethod:(SEL)sel {
    return YES;
}

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

/**
 這個方法會把你所調(diào)用的不存在的方法重定向到一個聲明了該方法的類中,只需要你返回一個有該方法的
 類就可以,如果你重定向的這個類仍然不具有該方法那么會繼續(xù)崩潰
 */
-(id)forwardingTargetForSelector:(SEL)aSelector {
    
}

/**
 將你不存在的方法打包成NSInvocation對象,做完你自己的處理之后
 調(diào)用invokeWithTarget讓某個target來處理該方法
 */
-(void)forwardInvocation:(NSInvocation *)anInvocation {
    [anInvocation invokeWithTarget:self];
}

3:動態(tài)添加方法
因為我們調(diào)用了一個不存在的方法導(dǎo)致崩潰,那么我們在判斷出不存在后就動態(tài)添加上一個方法吧 這樣不就不會蹦了嗎?我們先寫一個方法用來給我們做出提示

- (void) errorMethod {
    NSLog(@"no method!!!!!!!");
}

如果調(diào)用了沒有的方法,那么就把這個方法添加進(jìn)去,然后把被調(diào)用的方法的指針指向這個error1:,那么一旦調(diào)用了沒有的方法就會走這個。我們來看代碼

+(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 添加的方法的實現(xiàn)IMP指
     @param types 方法的標(biāo)示符
     @return 是否添加成功
         */
BOOL isAdd =  class_addMethod([self class], sel, method_getImplementation(errorMethod), method_getTypeEncoding(errorMethod));

然后運行下:

WKWebViewController * vc= [[WKWebViewController alloc] init];
[vc performSelector:@selector(testMethod)];

我調(diào)用了并不存在的testMethod方法并沒有崩潰并且方法成功添加了

image.png
  • 4:動態(tài)交換方法(也叫iOS黑魔法,慎用)

    沒什么好例子,用一個網(wǎng)上說的例子(引用別人的東西,懶得復(fù)制了,就接了圖)
    其實本質(zhì)即使SEL和IMP的交換,原理是這樣的:在iOS中每一個類中都有一個叫dispatch table的東西,里面存放在SEL 和他所對應(yīng)的IMP指針,之前也說過方法調(diào)用就是通過sel找IMP指針然后指針定位調(diào)用方法。方法交換就是對這個dispatch table進(jìn)行操作。讓A的SEL去對應(yīng)B的IMP,B的SEL對應(yīng)A的IMP,如圖

    這樣就達(dá)到方法交換的目的,下面看代碼:

+ (void)changeMethod {
    //  如果是類方法 要使用 !
    //  如果是系統(tǒng)的集合類的屬性要用元類 比如 __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); 這兩個參數(shù)很簡單,就是兩個需要交換的方法。
最后我調(diào)用了m1但是實際上走了m2。

動態(tài)交換方法的原理以及交換過程中指針的變化

在通常的方法交換中我們通常有兩種情景,一種是我會針對被交換的類建一個category,然后hook的方法會寫在category中。另一種是自己創(chuàng)建一個Tool類里面放些常用的工具方法其中包含了方法交換。可能大家普遍選擇第一種方法,但是如果你需要hook的類非常多的(我實際項目中就遇到這樣的問題)那你就需要針對不同的類創(chuàng)建category,就會導(dǎo)致文件過多,且每一個文件中只有一個hook方法,這樣一來左側(cè)一堆文件,所以我用了第二種方法,但是在使用過程中出現(xiàn)一個問題,先看下我的代碼機構(gòu)


image.png

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

  • ViewController中將要被替換的系統(tǒng)方法


    被替換的方法(系統(tǒng)方法)
  • Category中將要用來替換的自定義方法


    用來替換的方法(自定義方法)
  • 然后在ViewController中的load中做方法替換


    進(jìn)行方法替換

    運行一下的輸出結(jié)果想必大家已經(jīng)猜到了先執(zhí)行custom再執(zhí)行system,這是通常情況下大家的做法。


    結(jié)果

    下面再來看下如果我將替換方法寫在不同類中會怎樣,調(diào)用Tool中的交換方法
    執(zhí)行Tool中的交換方法

    然后直接看結(jié)果了,因為代碼都是一模一樣的我直接復(fù)制過去的


    結(jié)果

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

    解決的辦法是我們在交換方法之前要先像其中添加方法,也就是說把customMethod添加到SystemClass中,但是注意要把customMethod的實現(xiàn)指向syetemMethod的實現(xiàn)。這樣一來就可以達(dá)到SystemClass調(diào)用customMethod卻執(zhí)行systemMethod的代碼的效果,實現(xiàn)以上要求我們需要在交換之前執(zhí)行這個方法。
class_addMethod(systemClass, sel_Custom, imp_System, method_getTypeEncoding(method_System))

其中第一個參數(shù)是需要往哪個類添加;第二個參數(shù)是要添加的方法的方法名;第三個參數(shù)是所添加的方法的方法實現(xiàn),第四個是方法的標(biāo)識符。經(jīng)過就該之后我們的代碼是這樣

.
.
之前的都一樣就省略
.
.
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);
    }

我們來看下執(zhí)行完add操作之后此時的方法和類的對應(yīng)關(guān)系(紅色的為add的修改)

關(guān)系

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

class_replaceMethod(systemClass, sel_System, imp_Custom, method_getTypeEncoding(method_System));

第一個參數(shù):需要修改的方法的所在的類;第二個參數(shù):需要替換其實現(xiàn)的方法名;第三個參數(shù):需要把哪個實現(xiàn)替換給他;第四個參數(shù):方法標(biāo)識符。此時看下我們做完replace之后的類與方法名以及他們實現(xiàn)的關(guān)系(紅色的為replace的修改)。


關(guān)系

此時大家已經(jīng)看出來了,雖然沒有執(zhí)行exchange方法,但是我已經(jīng)達(dá)到了方法交換的目的。系統(tǒng)執(zhí)行systemMethod時候會走customMethod的實現(xiàn)但是因為在customMethod方法中我會遞歸執(zhí)行[self customMethod],所以又會走到systemMethod的實現(xiàn),因為之前進(jìn)行了方法添加,所以此時A類中有了customMethod方法,不會再發(fā)生之前的crash。達(dá)到一個不同類進(jìn)行Method Swizzling的目的。

綜上來看一個完整嚴(yán)謹(jǐn)?shù)腗ethodSwizzling應(yīng)該在交換前先add,并且add方法的參數(shù)不能錯
+ (void)changeMethod {

    Class systemClass = NSClassFromString(@"你的類");
    
    SEL sel_System = @selector(系統(tǒng)方法);
    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中都是沒有問題的。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 轉(zhuǎn)至元數(shù)據(jù)結(jié)尾創(chuàng)建: 董瀟偉,最新修改于: 十二月 23, 2016 轉(zhuǎn)至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 2,051評論 0 9
  • 這篇文章完全是基于南峰子老師博客的轉(zhuǎn)載 這篇文章完全是基于南峰子老師博客的轉(zhuǎn)載 這篇文章完全是基于南峰子老師博客的...
    西木閱讀 30,886評論 33 466
  • 我們常常會聽說 Objective-C 是一門動態(tài)語言,那么這個「動態(tài)」表現(xiàn)在哪呢?我想最主要的表現(xiàn)就是 Obje...
    Ethan_Struggle閱讀 2,333評論 0 7
  • 文中的實驗代碼我放在了這個項目中。 以下內(nèi)容是我通過整理[這篇博客] (http://yulingtianxia....
    茗涙閱讀 1,027評論 0 6
  • 本文轉(zhuǎn)載自:http://yulingtianxia.com/blog/2014/11/05/objective-...
    ant_flex閱讀 886評論 0 1

友情鏈接更多精彩內(nèi)容