iOS底層-runtime應(yīng)用之方法交換(method swizzling)

method swizzling 就是常說(shuō)的方法交換,也常被成為黑魔法。
簡(jiǎn)單點(diǎn)說(shuō)就是 定義并實(shí)現(xiàn)了方法A和B,在調(diào)用方法A的時(shí)候,執(zhí)行的確實(shí)方法B

基本概念

在了解method swizzling之前先來(lái)了解幾個(gè)概念:

SEL/@selector(方法名)

SEL 又叫選擇器,但是一般我們將它稱之為方法編號(hào)。源碼中的定義為
typedef struct objc_selector *SEL;

以下關(guān)于方法編號(hào)的解釋來(lái)自這篇文章 (未得專業(yè)驗(yàn)證)

方法以 selector 作為索引. selector 的數(shù)據(jù)類型是 SEL. 雖然 SEL 定義成 char*, 我們可 以把它想象成 int. 每個(gè)方法的名字對(duì)應(yīng)一個(gè)唯一的 int 值.比如, 方法 addObject: 可能 對(duì)應(yīng)的是 12. 當(dāng)尋找該方法是, 使用的是 selector,而不是名字 @"addObject:"

Objective-C 數(shù)據(jù)結(jié)構(gòu)中,存在一個(gè) name - selector 的映射表如下圖


映射關(guān)系

在編譯的時(shí)候, 只要有方法的調(diào)用, 編譯器都會(huì)通過(guò) selector 來(lái)查找,所以 (假設(shè) addObject 的 selector 為 12)

[myObject addObject:yourObject];
將會(huì)編譯變成

objc_msgSend(myObject, 12, yourObject);

這里,objec_msgSend()函數(shù)將會(huì)使用 myObjec 的 isa 指針來(lái)找到 myObject 的類空間結(jié)構(gòu)并 在類空間結(jié)構(gòu)中查找 selector 12 所對(duì)應(yīng)的方法.如果沒(méi)有找到,那么將使用指向父類的指 針找到父類空間結(jié)構(gòu)進(jìn)行 selector 12 的查找. 如果仍然沒(méi)有找到,就繼續(xù)往父類的父類一 直找,直到找到為止, 如果到了根類 NSObject 中仍然找不到,將會(huì)拋出異常.

我們可以看到, 這是一個(gè)很動(dòng)態(tài)的查找過(guò)程.類的結(jié)構(gòu)可以在運(yùn)行的時(shí)候改變,這樣可以很 容易來(lái)進(jìn)行功能擴(kuò)展Objective-C 語(yǔ)言是動(dòng)態(tài)語(yǔ)言, 支持動(dòng)態(tài)綁定.

??文章摘要結(jié)束

IMP

IMP指向方法實(shí)現(xiàn)的首地址,類似C語(yǔ)言的函數(shù)指針。IMP是消息最終調(diào)用的執(zhí)行代碼,是方法真正的實(shí)現(xiàn)代碼 。
源碼中的定義:
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ );
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...);
#endif

每一個(gè)實(shí)現(xiàn)了的方法都存在一個(gè)SEL和IMP(這不廢話,不然怎么可以成功調(diào)用?),寫(xiě)句的原因是可能剛理解這些概念的時(shí)候有些繞:

只有聲明,沒(méi)有實(shí)現(xiàn)的方法 有沒(méi)有SEL/IMP?比如只是在.h文件中寫(xiě)入- (void)test?
只有實(shí)現(xiàn)沒(méi)有聲明的方法,有沒(méi)有SEL/IMP? 比如只是在.m文件中寫(xiě)- (void)test{}?

上面問(wèn)題的答案就是:
只有聲明,沒(méi)有實(shí)現(xiàn):SEL和IMP都沒(méi)有
沒(méi)有聲明,只有實(shí)現(xiàn):SEL和IMP都有。
在只有聲明,沒(méi)有實(shí)現(xiàn)的情況下,打印類結(jié)構(gòu)信息。在ro里面的信息是 baseMethodList = 0x0000000000000000

Method

主要包含三部分

方法名:方法名為此方法的簽名,有著相同函數(shù)名和參數(shù)名的方法有著相同的方法名。
方法類型:方法類型描述了參數(shù)的類型。
IMP: IMP即函數(shù)指針,為方法具體實(shí)現(xiàn)代碼塊的地址,可像普通C函數(shù)調(diào)用一樣使用IMP。
實(shí)際上相當(dāng)于在SEL和IMP之間作了一個(gè)映射。有了SEL,我們便可以找到對(duì)應(yīng)的IMP。
源碼中的定義:
struct objc_method {
SEL _Nonnull method_name;
char * _Nullable method_types;
IMP _Nonnull method_imp;
}

method swizzling 的應(yīng)用

在實(shí)際開(kāi)發(fā)中,經(jīng)常會(huì)遇到這樣的情況

NSArray *array = @[@"1",@"2",@"3"];
NSLog(@"%@",array[4]);

一般來(lái)說(shuō),在使用下標(biāo)取值之前,都需要先判斷
if (array.count < 4) {
NSLog(@"%@",array[4]);
}
但是總有漏掉判斷或其他情況導(dǎo)致crash
為了避免這種情況或者少寫(xiě)重復(fù)代碼,我們可以使用動(dòng)態(tài)方法交換的方式來(lái)處理
定義一個(gè)方法JERuntimeTool
在.h中定義方法并在.m中實(shí)現(xiàn)

#import <objc/runtime.h>
/**
交換方法

@param cls 交換對(duì)象
@param oriSEL 原始方法編號(hào)
@param swizzledSEL 交換的方法編號(hào)
*/
+ (void)je_methodSwizzlingWithClass:(Class)cls
                             oriSEL:(SEL)oriSEL
                        swizzledSEL:(SEL)swizzledSEL{
    
    if (!cls){
        NSLog(@"傳入的交換類不能為空");
        return;
    }
    Method oriMethod = class_getInstanceMethod(cls, oriSEL);
    Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
    method_exchangeImplementations(oriMethod, swiMethod);
}

定義一個(gè)拓展NSArray的JE類NSArray+JE.h

針對(duì)下標(biāo)取值的情況,有多個(gè)方法,都需要進(jìn)行異常捕獲

- (id)je_objectAtIndex:(NSUInteger)index{
    if (index > self.count-1) {
        //異常處理或記錄打印
        return nil;
    }
    return [self lg_objectAtIndex:index];
}

- (id)je_objectAtIndexedSubscript:(NSUInteger)index{
     if (index > self.count-1) {
        //異常處理或記錄打印
        return nil;
    }
    return [self lg_objectAtIndexedSubscript:index];
}

進(jìn)行方法交換

+ (void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        
        [JERuntimeTool je_methodSwizzlingWithClass:objc_getClass("__NSArrayI")
                                            oriSEL:@selector(objectAtIndex:)
                                       swizzledSEL:@selector(je_objectAtIndex:)];
        
        [JERuntimeTool je_methodSwizzlingWithClass:objc_getClass("__NSArrayI")
                                            oriSEL:@selector(objectAtIndexedSubscript:)
                                       swizzledSEL:@selector(je_objectAtIndexedSubscript:)];
    });
}

這里有幾點(diǎn)需要注意:
1、在+ (void)load 方法中調(diào)用
2、調(diào)用的時(shí)候,用單例的方式
3、對(duì)于系統(tǒng)的有些方法應(yīng)該交換哪個(gè)方法?
4、交換的類應(yīng)該是那個(gè)?
在crash的時(shí)候有一個(gè)錯(cuò)誤信息,其中有一段:reason: '*** -[__NSArrayI objectAtIndexedSubscript:]: index 4 beyond bounds [0 .. 2],其中 __NSArrayI是需要交換的類, objectAtIndexedSubscript:是需要交換的方法。
對(duì)于我們自定義的類一般來(lái)說(shuō)直接就是類名和自定義的方法SEL,但是系統(tǒng)的抽象類類是不能直接作為類對(duì)象傳入的,比如NSArray/NSMutableArrya、NSDictionary/NSMutableDictionary、 NSData/NSMutableData等。

上面的方法看似很完美,但是下面坑就是在你不經(jīng)意間就出現(xiàn)在腳下。

會(huì)出現(xiàn)的問(wèn)題

問(wèn)題1

背景:子類沒(méi)有實(shí)現(xiàn)父類的方法 但是對(duì)子類的方法進(jìn)行了交換
比如:Person:NSObjec Student:Person
Person定義并實(shí)現(xiàn)了方法:-(void)personMethod;
Studen 沒(méi)有實(shí)現(xiàn) -(void)personMethod;

在Student的分類中中交換方法

Student+JE.h

+ (void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [JERuntimeTool je_methodSwizzlingWithClass:self oriSEL:@selector(personInstanceMethod) swizzledSEL:@selector(je_studentInstanceMethod)];
    });
}

- (void)je_studentInstanceMethod{
    NSLog(@"do something");
    [self je_studentInstanceMethod];
}

執(zhí)行代碼

Student *s = [Student new];
[s personInstanceMethod];
    
Person *p = [Person new];
[p personInstanceMethod];

運(yùn)行結(jié)果:

do something
person對(duì)象方法:-[Person personInstanceMethod]

do something
[Person je_studentInstanceMethod]: unrecognized selector sent to instance 0x600002e5c440

在person調(diào)用 personInstanceMethod 方法的時(shí)候 出現(xiàn)了問(wèn)題,原因是:

1、person 調(diào)用 personInstanceMethod(SEL) ,由于進(jìn)行了交換,將執(zhí)行
je_studentInstanceMethod(IMP)
2、繼續(xù)調(diào)用[self je_studentInstanceMethod],person 調(diào)用 je_studentInstanceMethod (SEL)
3、由于je_studentInstanceMethod(SEL) 屬于Student(調(diào)用SEL需要執(zhí)行相對(duì)應(yīng)的IMP,這個(gè)時(shí)候,在Person中沒(méi)有相應(yīng)的IMP) 所以person 調(diào)用的時(shí)候 出現(xiàn)了 [Person je_studentInstanceMethod]: unrecognized的錯(cuò)誤

知道了原因,我們需要針對(duì)性解決問(wèn)題:
在Student中添加一個(gè) 方法,對(duì)應(yīng)關(guān)系為:swizzleSEL + Ori IMP。(可見(jiàn)下圖)
在這里,采用的是 :向Student中添加
je_studentInstanceMethod(SEL) + personInstanceMethod(IMP),如果添加成功,說(shuō)明Student沒(méi)有實(shí)現(xiàn)這個(gè)方法,這樣來(lái)達(dá)到通用性,代碼如下

+ (void)je_methodSwizzlingWithClass:(Class)cls
                             oriSEL:(SEL)oriSEL
                        swizzledSEL:(SEL)swizzledSEL{
    
    if (!cls){
        NSLog(@"傳入的交換類不能為空");
        return;
    }
    
    Method oriMethod = class_getInstanceMethod(cls, oriSEL);
    Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
    
    BOOL didAddMethod = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
    if (didAddMethod) {
        class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
    }else{
        method_exchangeImplementations(oriMethod, swiMethod);
    }
}

執(zhí)行步驟如下圖


交換代碼中的執(zhí)行過(guò)程

問(wèn)題2

背景:父類和子類都沒(méi)有實(shí)現(xiàn)
比如:子類Student 只申明一個(gè)方法readBook;

在Student的分類中中交換方法

Student+JE.h

+ (void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [JERuntimeTool je_methodSwizzlingWithClass:self oriSEL:@selector(readBook) swizzledSEL:@selector(je_readBook)];
    });
}

- (void)je_readBook{
    NSLog(@"do something");
    [self je_readBook];
}

執(zhí)行代碼

Student *s = [Student new];
[s personInstanceMethod];

運(yùn)行結(jié)果:

不停的調(diào)用   NSLog(@"do something"); 這一句

這里產(chǎn)生了遞歸,原因是交換不完全。

步驟解析:

1、交換前:


交換前

2、執(zhí)行class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));

執(zhí)行添加方法后結(jié)構(gòu)

3、執(zhí)行 class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));

這里需要注意的是·將 swizzleSEL (也就是圖中的5) 對(duì)應(yīng)的 swizzleIMP (圖中的6) 替換成 一個(gè)nil,這里需要注意的是,從底層源碼中看:
class_replaceMethod —>
addMethod —>
_method_setImplementation

如果reply一個(gè)nil IMP,那么是不會(huì)執(zhí)行的,所以最終指向結(jié)構(gòu)還是2圖的結(jié)構(gòu)

static IMP 
_method_setImplementation(Class cls, method_t *m, IMP imp)
{
    runtimeLock.assertLocked();

    if (!m) return nil;
    if (!imp) return nil;

    IMP old = m->imp;
    m->imp = imp;
....
    return old;
}

執(zhí)行過(guò)程:
結(jié)合圖2:
調(diào)用9 -》 執(zhí)行 10;
10 調(diào)用 5 -》 執(zhí)行 6
6 調(diào)用5 -》 執(zhí)行6
....
所以發(fā)生了遞歸

錯(cuò)誤原因:查看替換后的結(jié)構(gòu)


按照之前的方法替換后的結(jié)構(gòu)

知道了原因,針對(duì)這個(gè)問(wèn)題來(lái)解決:
解決思路就是:判斷有沒(méi)有IMP,如果沒(méi)有IMP,就添加一個(gè)默認(rèn)的IMP(在這里就是 - (void)readBook {})
代碼如下:

+ (void)je_methodSwizzlingWithClass:(Class)cls
                             oriSEL:(SEL)oriSEL
                        swizzledSEL:(SEL)swizzledSEL{
    
    if (!cls){
        NSLog(@"傳入的交換類不能為空");
        return;
    }
    
    Method oriMethod = class_getInstanceMethod(cls, oriSEL);
    Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
    if (!oriMethod) {
        class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
        method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){ }));
    }

    BOOL didAddMethod = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
    if (didAddMethod) {
        class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
    }else{
        method_exchangeImplementations(oriMethod, swiMethod);
    }
}

按照最終的方案替換后的結(jié)構(gòu)


最終交換后的結(jié)構(gòu)

類方法交換

上面說(shuō)了這么多,都是針對(duì)對(duì)象方法(實(shí)例方法)來(lái)講的,那么如果想交換類方法要怎么處理?
如果搞明白了 對(duì)象 -> 類 —> 元類 —> 根源類 的關(guān)系,并且知道實(shí)例方法和類方法的存儲(chǔ)位置,這個(gè)問(wèn)題就很容易解決。

知識(shí)點(diǎn)補(bǔ)充:
1、類的isa 指向 元類 ,元類的isa指向 根源類 參照之前的文章
2、實(shí)例方法存儲(chǔ)在類對(duì)象中,類方法存儲(chǔ)在元類對(duì)象中

解決上面的問(wèn)題,只需要在交換的時(shí)候 傳入的類 傳入元類就好了。
代碼如下:

+ (void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = object_getClass(self); 
        [JERuntimeTool je_methodSwizzlingWithClass:class oriSEL:@selector(personMethod) swizzledSEL:@selector(je_readBook)];
    });
}

method swizzling 總結(jié)和注意事項(xiàng)

一、方法交換的調(diào)用時(shí)機(jī):

在+ (void)load方法中調(diào)用。
為什么:
1)、自動(dòng)調(diào)用 2)、調(diào)用的早。 load方法在app啟動(dòng)的時(shí)候,就由系統(tǒng)自動(dòng)調(diào)用了。
2、保證交換的唯一性(需要用單例的形式)

二、load方法的加載順序

1)、有繼承關(guān)系的類 先加載父類(不包含其拓展)再加載子類
2)、不同類之間的load是按照編譯順序來(lái)決定的(即使是有繼承關(guān)系的類 他們的拓展之間也是按照編譯順序來(lái)的)
3)、推展類的調(diào)用是在所有的類加載完成之后,(可參照源碼 中的map_images方法: 類 > protocol > category)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

  • 本文轉(zhuǎn)載自:http://yulingtianxia.com/blog/2014/11/05/objective-...
    ant_flex閱讀 883評(píng)論 0 1
  • 我們常常會(huì)聽(tīng)說(shuō) Objective-C 是一門(mén)動(dòng)態(tài)語(yǔ)言,那么這個(gè)「動(dòng)態(tài)」表現(xiàn)在哪呢?我想最主要的表現(xiàn)就是 Obje...
    Ethan_Struggle閱讀 2,327評(píng)論 0 7
  • 本文詳細(xì)整理了 Cocoa 的 Runtime 系統(tǒng)的知識(shí),它使得 Objective-C 如虎添翼,具備了靈活的...
    lylaut閱讀 865評(píng)論 0 4
  • Method Swizzling參考資料 1.用到的運(yùn)行時(shí)基礎(chǔ)知識(shí)介紹 SEL : 方法選擇器,SEL是函數(shù)ob...
    shannoon閱讀 1,450評(píng)論 0 7
  • 文中的實(shí)驗(yàn)代碼我放在了這個(gè)項(xiàng)目中。 以下內(nèi)容是我通過(guò)整理[這篇博客] (http://yulingtianxia....
    茗涙閱讀 1,026評(píng)論 0 6

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