iOS底層探索19、方法交換 swizzing

runtimemethod swizzing 其本質(zhì)是方法交換,即 imp 的交換。但其使用過程中可能出現(xiàn)一些問題,本文對method swizzing進行簡單的探究。

一、方法交換的問題場景與處理方案

1、+load方法中交換的問題

任意創(chuàng)建一個iOS工程,代碼準(zhǔn)備. 子類MySubPerson添加分類,在分類中的+load調(diào)方法交換的方法:

// 1、+load 中交換方法
+ (void)load {
    
    NSLog(@"%s",__func__);
    [My_RunTimeTool my_methodSwizzlingWithClass:self oriSEL:@selector(personInstanceOne) swizzledSEL:@selector(my_subPersonInstanceOne)];
}

/// My_RunTimeTool 工具類
// 1、
+ (void)my_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);
}

ViewController:

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    
    MySubPerson *subPerson = [[MySubPerson alloc] init];
    [subPerson personInstanceOne];
    [MySubPerson load];
    [subPerson personInstanceOne];

    MyPerson *person = [[MyPerson alloc] init];
    [person personInstanceOne];
}

運行工程,輸出如下:

Demo_appload[25656:2481009] +[ViewController load]
Demo_appload[25656:2481009] +[MySubPerson(forSwizz) load]
Demo_appload[57169:6022736] 來了 C++ : myFunc 
Demo_appload[25656:2481009] main 函數(shù)進入
Demo_appload[25656:2481009] -[MySubPerson personInstanceOne]
Demo_appload[25656:2481009] MySubPerson 分類添加的對象方法:-[MySubPerson(forSwizz) my_subPersonInstanceOne]
Demo_appload[25656:2481009] +[MySubPerson(forSwizz) load]
Demo_appload[25656:2481009] -[MySubPerson personInstanceOne]
Demo_appload[25656:2481009] -[MyPerson personInstanceOne]

這里除了個問題,我們其實要實現(xiàn)MySubPerson執(zhí)行方法:my_subPersonInstanceOne,但是load的多次調(diào)用,使方法多次交換,交換成了并非我們所需要的。so 要進行 once處理:

+ (void)load {
    
    NSLog(@"%s",__func__);
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [My_RunTimeTool my_methodSwizzlingWithClass:self oriSEL:@selector(personInstanceOne) swizzledSEL:@selector(my_subPersonInstanceOne)];
    });
}

再次運行工程,MySubPerson每次都是執(zhí)行my_subPersonInstanceOne,實現(xiàn)需求:

Demo_appload[25656:2481009] -[MySubPerson personInstanceOne]
Demo_appload[25656:2481009] MySubPerson 分類添加的對象方法:-[MySubPerson(forSwizz) my_subPersonInstanceOne]
Demo_appload[25656:2481009] +[MySubPerson(forSwizz) load]
Demo_appload[25656:2481009] MySubPerson 分類添加的對象方法:-[MySubPerson(forSwizz) my_subPersonInstanceOne]
Demo_appload[25656:2481009] -[MyPerson personInstanceOne]

但是,我們一直+load方法的實現(xiàn),會使類提前加載,對啟動造成慢。
so 使用在+initialize而非+load進行一系列處理。

+ (void)initialize
{
    if (self == [super class]) {
        NSLog(@"%s",__func__);
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            [My_RunTimeTool my_methodSwizzlingWithClass:self oriSEL:@selector(personInstanceOne) swizzledSEL:@selector(my_subPersonInstanceOne)];
        });
    }
}

2、父類實現(xiàn) - 子類中不實現(xiàn)oriMethod方法的場景

運行工程,crash 了,信息如下:

Demo_appload[26037:2504023] -[MyPerson personInstanceOne]
Demo_appload[26037:2504023] MySubPerson 分類添加的對象方法:-[MySubPerson(forSwizz) my_subPersonInstanceOne]
Demo_appload[25970:2499447] -[MyPerson my_subPersonInstanceOne]: unrecognized selector sent to instance 0x600002b3b6c0
Demo_appload[25970:2499447] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[MyPerson my_subPersonInstanceOne]: unrecognized selector sent to instance 0x600002b3b6c0'

這是什么原因呢?
還記得方法查找流程嗎?子類未實現(xiàn)便會一直找向父類,所以,方法交換時,父類MyPersonimp變了,它指向了my_subPersonInstanceOne,子類中有實現(xiàn)正常執(zhí)行,但父類中并未實現(xiàn)my_subPersonInstanceOne,so 找不到方法-->崩潰。
處理方案:

/// My_RunTimeTool 工具類
// 2、父類實現(xiàn) 子類未實現(xiàn)方法
+ (void)my_resolve_methodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL {
    
    if (!cls) NSLog(@"傳入的交換類不能為空");

    Method oriMethod = class_getInstanceMethod(cls, oriSEL);
    Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
    
    // 嘗試添加我們要交換的方法 swiMethod: my_subPersonInstanceOne
    BOOL success = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(oriMethod));
    
    if (success) {// 加成功了,那么就是自己沒有方法:swiMethod - 用父類的自己的方法替換
        class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
    }else{ // 自己有,正常交換
        method_exchangeImplementations(oriMethod, swiMethod);
    }
}

再次運行工程,成功執(zhí)行:

來了 C++ : myFunc 
Demo_appload[26168:2512856] main 函數(shù)進入
Demo_appload[26168:2512856] +[MySubPerson(forSwizz) initialize]
Demo_appload[26168:2512856] -[MyPerson personInstanceOne]
Demo_appload[26168:2512856] MySubPerson 分類添加的對象方法:-[MySubPerson(forSwizz) my_subPersonInstanceOne]
Demo_appload[26168:2512856] -[MyPerson personInstanceOne]

3、父類子類都未實現(xiàn)oriMethod

運行工程,執(zhí)行到下面位置:

來了 C++ : myFunc 
Demo_appload[26233:2520202] main 函數(shù)進入
Demo_appload[26233:2520202] +[MySubPerson(forSwizz) initialize]

過了一會兒崩潰了在了下圖位置:

image.png

遞歸循環(huán)溢出了,但是為何上面2中場景未造成 crash 呢?
--> 1、文章上面的場景 1、2中,my_subPersonInstanceOneimp是指向personInstanceOne的,方法查找時找的是personInstanceOne,找到與否都會拋出結(jié)果;
--> 2、但場景3personInstanceOne并未實現(xiàn),沒有imp,my_subPersonInstanceOne一直自己指自己,遞歸至溢出崩潰。
如何處理這中場景呢?代碼如下:

// 3、父類子類未實現(xiàn) oriSEL
+ (void)my_best_methodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL {
    
    if (!cls) NSLog(@"傳入的交換類不能為空");

    Method oriMethod = class_getInstanceMethod(cls, oriSEL);
    Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
    
    if (!oriMethod) {// 避免沒有 oriMethod 導(dǎo)致無意義交換
        // 在 oriMethod 為 nil 時,替換后將 swizzledSEL 賦值一個不做任何事的空實現(xiàn)(這里做個打印),代碼如下:
        class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
        method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){
            NSLog(@"來了一個空的 imp");
        }));
    }
    
    // 嘗試添加我們要交換的方法 swiMethod: my_subPersonInstanceOne
    BOOL success = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(oriMethod));
    
    if (success) {// 加成功了,那么就是自己沒有方法:swiMethod - 用自己替換
        class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
    }else{ // 自己有,正常交換
        method_exchangeImplementations(oriMethod, swiMethod);
    }
}

判斷一下oriMethod,若不存在,給swiMethod一個空的實現(xiàn),再次運行結(jié)果如下,崩在MyPerson找不到personInstanceOne

Demo_appload[26619:2549842] main 函數(shù)進入
Demo_appload[26619:2549842] +[MySubPerson(forSwizz) initialize]
Demo_appload[26619:2549842] -[MySubPerson(forSwizz) my_subPersonInstanceOne]
Demo_appload[26619:2549842] 來了一個空的 imp
Demo_appload[26619:2549842] MySubPerson 分類添加的對象方法:-[MySubPerson(forSwizz) my_subPersonInstanceOne]
Demo_appload[26619:2549842] -[MyPerson personInstanceOne]: unrecognized selector sent to instance 0x6000021843b0
Demo_appload[26619:2549842] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[MyPerson personInstanceOne]: unrecognized selector sent to instance 0x6000021843b0'

二、 api 源碼

1、method_exchangeImplementations()源碼:

void method_exchangeImplementations(Method m1, Method m2)
{
    if (!m1  ||  !m2) return;

    mutex_locker_t lock(runtimeLock);
    // imp 交換
    IMP m1_imp = m1->imp;
    m1->imp = m2->imp;
    m2->imp = m1_imp;


    // RR/AWZ updates are slow because class is unknown
    // Cache updates are slow because class is unknown
    // fixme build list of classes whose Methods are known externally?

    flushCaches(nil);

    adjustCustomFlagsForMethodChange(nil, m1);
    adjustCustomFlagsForMethodChange(nil, m2);
}

2、class_replaceMethod()源碼:

IMP 
class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)
{
    if (!cls) return nil;

    mutex_locker_t lock(runtimeLock);
    // 添加 Method
    return addMethod(cls, name, imp, types ?: "", YES);
}

addMethod()

/**********************************************************************
* addMethod
* fixme
* Locking: runtimeLock must be held by the caller
**********************************************************************/
static IMP 
addMethod(Class cls, SEL name, IMP imp, const char *types, bool replace)
{
    IMP result = nil;

    runtimeLock.assertLocked();

    checkIsKnownClass(cls);
    
    ASSERT(types);
    ASSERT(cls->isRealized());

    method_t *m;
    if ((m = getMethodNoSuper_nolock(cls, name))) {
        // already exists
        if (!replace) {
            result = m->imp;
        } else {
            // 已存在,要替換
            // m: my_subPersonInstanceOne
            // imp: personInstanceOne
            result = _method_setImplementation(cls, m, imp);
        }
    } else {
        auto rwe = cls->data()->extAllocIfNeeded();

        // fixme optimize
        method_list_t *newlist;
        newlist = (method_list_t *)calloc(sizeof(*newlist), 1);
        newlist->entsizeAndFlags = 
            (uint32_t)sizeof(method_t) | fixed_up_method_list;
        newlist->count = 1;
        newlist->first.name = name;
        newlist->first.types = strdupIfMutable(types);
        newlist->first.imp = imp;

        prepareMethodLists(cls, &newlist, 1, NO, NO);
        rwe->methods.attachLists(&newlist, 1);
        flushCaches(cls);

        result = nil;
    }

    return result;
}

_method_setImplementation()

/***********************************************************************
* method_setImplementation
* Sets this method's implementation to imp.
* The previous implementation is returned.
**********************************************************************/
static IMP 
_method_setImplementation(Class cls, method_t *m, IMP imp)
{
    runtimeLock.assertLocked();

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

    // m: my_subPersonInstanceOne
    // imp: personInstanceOne
    IMP old = m->imp;
    m->imp = imp;// 使 m 指向 personInstanceOne 的 imp

    // Cache updates are slow if cls is nil (i.e. unknown)
    // RR/AWZ updates are slow if cls is nil (i.e. unknown)
    // fixme build list of classes whose Methods are known externally?

    flushCaches(cls);

    adjustCustomFlagsForMethodChange(cls, m);

    return old;
}

class_replaceMethod執(zhí)行流程:
class_replaceMethod() --> addMethod() --> _method_setImplementation() - 使 swizzMethod 的 imp指向 personInstanceOne 的 imp.

最后編輯于
?著作權(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ù)。

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