#iOS底層探索 -- Method-Swizzling 的應(yīng)用與坑點(diǎn)

在我們開始接觸到runtime之后,我們經(jīng)常能見到Method-Swizzling這個(gè)被稱作 黑魔法 的東西,那么到底什么是Method-Swizzling 怎么使用、使用過程中又有哪些坑點(diǎn),我們今天來探究一下

什么是Method-Swizzling

字面上: 方法調(diào)配 、方法交換
實(shí)現(xiàn)上: 我們常說的方法、方法名sel 通過其指向的IMP指針(方法的實(shí)現(xiàn)),通過這樣的對(duì)應(yīng)關(guān)系在使用時(shí)去調(diào)用對(duì)應(yīng)的方法。
這里通過 objc/runtime.h中提供的api:method_exchangeImplementations(Method m1, Method m2)來交換IMP,通過這樣的方法重新綁定selIMP對(duì)應(yīng)關(guān)系。

Method-Swizzling原理.jpg

Method-Swizzling有什么作用

那么上面這樣的方法交換在我們開發(fā)中到底有什么作用?

舉個(gè)栗子??
我們?cè)陂_發(fā)過程中一直都會(huì)有一個(gè)無法避開的需求--埋點(diǎn)比如記錄用戶在操作APP期間到底訪問了哪些頁(yè)面。

  1. 最基礎(chǔ)的,我們?cè)诿總€(gè)VC的ViewDidLoad中去預(yù)留方法
  2. 高一點(diǎn)的,我們實(shí)現(xiàn)一個(gè)VC的基類,在基類中我們對(duì)ViewDidLoad重寫
  3. 再或者,我們可以通過category去實(shí)現(xiàn)
    但這些我們都需要寫大量代碼,如果2、3方法是在項(xiàng)目后期去做時(shí),也需要去調(diào)整很多類信息。

甚至,我們有可能是需要再SDK中,在不接觸頁(yè)面代碼的情況下去實(shí)現(xiàn)這個(gè)埋點(diǎn)的需求,我們就可以通過Method-Swizzling 去直接hook ViewDidLoad方法,直接實(shí)現(xiàn)埋點(diǎn)。
這也就是AOP(Aspect Oriented Programming) 面向切面編程 思想

通過預(yù)編譯方式和運(yùn)行期間動(dòng)態(tài)代理實(shí)現(xiàn)程序功能的統(tǒng)一維護(hù)的一種技術(shù)。AOPOOP的延續(xù),是軟件開發(fā)中的一個(gè)熱點(diǎn),也是Spring框架中的一個(gè)重要內(nèi)容,是函數(shù)式編程的一種衍生范型。利用AOP可以對(duì)業(yè)務(wù)邏輯的各個(gè)部分進(jìn)行隔離,從而使得業(yè)務(wù)邏輯各部分之間的耦合度降低,提高程序的可重用性,同時(shí)提高了開發(fā)的效率。(摘自百度百科)

除了上面說的埋點(diǎn),還有一下crash收集我們也可以通過這樣的方法實(shí)現(xiàn)。

Method-Swizzling具體使用 及 坑點(diǎn)

接下來,我們實(shí)戰(zhàn)看一下Method-Swizzling的具體應(yīng)用

/// 實(shí)例方法交換
/// @param cls 方法交換的類
/// @param oriSEL 需要被交換的方法編號(hào)
/// @param swizzledSEL 用來交換的方法編號(hào)
+ (void)lg_methodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
    
    if (!cls) NSLog(@"傳入的交換類不能為空");

    Method oriMethod = class_getInstanceMethod(cls, oriSEL);
    Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
    method_exchangeImplementations(oriMethod, swiMethod);
}

這是最基礎(chǔ)的Method-Swizzling應(yīng)用,其中會(huì)有一些問題,比如

重復(fù)調(diào)用問題

因?yàn)?code>+(void)load方法調(diào)用的最早,所以一般我們放在其中去做方法交換。而+(void)load并不能確定只調(diào)用一次,如果發(fā)生多次調(diào)用,那么方法交換也會(huì)發(fā)生多次,IMP就會(huì)反復(fù)的交換。

解決方案

通過單例的思路,我們通過 dispatch_once(&onceToken, ^{})去做數(shù)據(jù)保護(hù),避免反復(fù)橫跳的發(fā)生

或者我們可以通過手動(dòng)觸發(fā),只調(diào)用一次

子類交換了繼承于父類方法

- (void)viewDidLoad {
    [super viewDidLoad];

    ///LGStudent 繼承于 LGPerson
    LGStudent *s = [[LGStudent alloc] init];
    [s personInstanceMethod];

    LGPerson *p = [[LGPerson alloc] init];
    [p personInstanceMethod];
}
#import "LGPerson.h"

@implementation LGPerson
- (void)personInstanceMethod{
    NSLog(@"person對(duì)象方法:%s",__func__);
}
+ (void)personClassMethod{
    NSLog(@"person類方法:%s",__func__);
}
@end
@implementation LGStudent (LG)

+ (void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [LGRuntimeTool lg_methodSwizzlingWithClass:self oriSEL:@selector(personInstanceMethod) swizzledSEL:@selector(lg_studentInstanceMethod)];
    });
}
- (void)lg_studentInstanceMethod{
    NSLog(@"LGStudent分類添加的lg對(duì)象方法:%s",__func__);
}

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


Method-Swizzling坑點(diǎn)2.jpg

我們?cè)谥暗奶剿髦芯椭?,?dāng)子類沒有實(shí)現(xiàn)方法時(shí),方法會(huì)遍歷到父類的方法列表中返回IMP,而Method-Swizzling是直接修改的IMP,所以被交換的其實(shí)就是父類的方法

那么問題就來了,如果交換后的方法父類本身不存在,那就找不到對(duì)應(yīng)方法,就會(huì)出現(xiàn)崩潰。

解決方案

+ (void)lg_betterMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
    
    if (!cls) NSLog(@"傳入的交換類不能為空");
    // oriSEL       personInstanceMethod
    // swizzledSEL  lg_studentInstanceMethod
    Method oriMethod = class_getInstanceMethod(cls, oriSEL);
    Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
    //加一層保護(hù)措施,如果添加成功,則表示該方法不存在于本類,而是存在于父類中,不能交換父類的方法,否則父類的對(duì)象調(diào)用該方法會(huì)crash;添加失敗則表示本類存在該方法
    BOOL success = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(oriMethod));
    if (success) {// 自己沒有 - 交換 - 沒有父類進(jìn)行處理 (重寫一個(gè))
        //再將原有的實(shí)現(xiàn)替換到swizzledMethod方法上,從而實(shí)現(xiàn)方法的交換,并且未影響到父類方法的實(shí)現(xiàn)
        class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
    }else{ // 自己有
        method_exchangeImplementations(oriMethod, swiMethod);
    }
}

在交換前判斷下是否是自己的方法,如果不是,那就改為替換,不直接交換,避免影響父類的方法實(shí)現(xiàn)。

探究 在交換的方法中再次調(diào)用原方法,是否會(huì)發(fā)生遞歸

// 是否遞歸
- (void)lg_studentInstanceMethod{
    [self lg_studentInstanceMethod]; 
    NSLog(@"LGStudent分類添加的lg對(duì)象方法:%s",__func__);
}

答案是不會(huì)。


遞歸探尋.jpg

此時(shí)lg_studentInstanceMethod 內(nèi)部實(shí)現(xiàn)是通過personInstanceMethod 方法名去調(diào)用的,而中間lg_studentInstanceMethod的方法,則指向了personInstanceMethodIMP,并不會(huì)行成一個(gè)遞歸循環(huán)。
但如果改為

// 是否遞歸
- (void)lg_studentInstanceMethod{
    [self personInstanceMethod]; 
    NSLog(@"LGStudent分類添加的lg對(duì)象方法:%s",__func__);
}

還是會(huì)一直在遞歸循環(huán)中,所以在這里要清楚理解SELIMP的關(guān)系和區(qū)別

?著作權(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)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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