iOS探索 runtime面試題分析

我是好寶寶發表於2020-03-02

歡迎閱讀iOS探索系列(按序閱讀食用效果更加)

寫在前面

本文涉及的面試題如下:

  • 什麼是Runtime
  • 方法的本質是什麼
  • SEL和IMP的關係
  • 能否向運⾏時建立的類中新增例項變數
  • 利用runtime-API建立物件
  • 關聯物件分析——分類中建立屬性
  • weak置空原理
  • method swizzing坑點

一、什麼是Runtime

  • runtime是由CC++彙編實現的一套API,為OC語言加入了物件導向、執行時的功能
  • 執行時(runtime)將資料型別的確定由編譯時推遲到了執行時
  • 平時編寫的OC程式碼,在程式執行過程中,最終會轉換成runtime的C語言程式碼——runtimeObjective-C 的幕後⼯作者

如類結構中的rorw屬性

  • ro(read-only)在編譯時已經確定
  • rw(read-write)在執行時才確定,因此可以使用runtime進行修改

二、方法的本質是什麼

方法的本質是傳送訊息objc_msgSend,即尋找IMP的過程

傳送訊息會有以下⼏個流程:

  1. 快速查詢流程——通過彙編objc_msgSend查詢快取cache_t是否有imp實現
  2. 慢速查詢流程——通過C++中lookUpImpOrForward遞迴查詢當前類和父類的rwmethodlist的方法
  3. 動態方法解析——通過呼叫resolveInstanceMethodresolveClassMethod來動態方法決議——實現訊息動態處理
  4. 快速轉發流程——通過CoreFoundation來觸發訊息轉發流程,forwardingTargetForSelector實現快速轉發,由其他物件來實現處理方法
  5. 慢速轉發流程——先呼叫methodSignatureForSelector獲取到方法的簽名,生成對應的invocation;再通過forwardInvocation來進行處理
  6. 以上流程均無法挽救就崩潰並報錯

三、SEL和IMP的關係

遇到這種問題先要解釋兩者分別是什麼?再解釋兩者的關係

SEL是方法編號,也是方法名,在dyld載入映象到記憶體時,通過_read_image方法載入到記憶體的表中了

IMP是函式實現指標,找IMP就是找函式實現的過程

SELIMP的關係就可以解釋為:

  • SEL就相當於書本的⽬錄標題
  • IMP就是書本的⻚碼
  • 函式就是具體頁碼對應的內容

比如我們想在《程式設計師的自我修養——連結、裝載與庫》一書中找到“動態連結”(SEL),肯定會翻到179頁(IMP),179頁會開始講述具體內容(函式實現)

iOS探索 runtime面試題分析

四、能否向運⾏時建立的類中新增例項變數

具體情況具體分析:

  1. 編譯好的類不能新增例項變數
  2. 執行時建立的類可以新增例項變數,但若已註冊到記憶體中就不行了

原因:

  • 編譯好的例項變數儲存的位置在ro,而ro是在編譯時就已經確定了的
  • ⼀旦編譯完成,記憶體結構就完全確定就⽆法修改
  • 只能修改rw中的方法或者可以通過關聯物件的方式來新增屬性

五、利用runtime-API建立物件

這題對runtime-API要求程度比較高

1.API介紹

  1. 動態建立類
/**
 *建立類
 *
 *superClass: 父類,傳Nil會建立一個新的根類
 *name: 類名
 *extraBytes: 額外的記憶體空間,一般傳0
 *return:返回新類,建立失敗返回Nil,如果類名已經存在,則建立失敗
 */
Class FXPerson = objc_allocateClassPair([NSObject class], "LGPerson", 0);
複製程式碼
  1. 新增成員變數
/**
*新增成員變數
*這個函式只能在objc_allocateClassPair和objc_registerClassPair之前呼叫。不支援向現有類新增一個例項變數
*這個類不能是元類,不支援在元類中新增一個例項變數
*例項變數的最小對齊為1 << align,例項變數的最小對齊依賴於ivar的型別和機器架構。對於任何指標型別的變數,請通過log2(sizeof(pointer_type))
*
*cls 往哪個類新增
*name 新增的名字
*size 大小
*alignment 對齊處理方式
*types 簽名
*/
class_addIvar(FXPerson, "fxName", sizeof(NSString *), log2(sizeof(NSString *)), "@");
複製程式碼
  1. 註冊到記憶體
/**
 *往記憶體註冊類
 *
 * cls 要註冊的類
 */
 objc_registerClassPair(FXPerson);
複製程式碼
  1. 新增屬性變數
/**
*往類裡面新增屬性
*
*cls 要新增屬性的類
*name 屬性名字
*attributes 屬性的屬性陣列。
*attriCount 屬性中屬性的數量。
*/
class_addProperty(targetClass, propertyName, attrs, 4);
複製程式碼
  1. 新增方法
/**
 *往類裡面新增方法
 *
 *cls 要新增方法的類
 *sel 方法編號
 *imp 函式實現指標
 *types 簽名
 */
class_addMethod(FXPerson, @selector(setHobby), (IMP)fxSetter, "v@:@");
複製程式碼

2.整體使用

// hobby的setter-IMP
void fxSetter(NSString *value) {
    printf("%s/n",__func__);
}

// hobby的getter-IMP
NSString *fxHobby() {
    return @"iOS";
}

// 新增屬性變數的封裝方法
void fx_class_addProperty(Class targetClass, const char *propertyName) {
    objc_property_attribute_t type = { "T", [[NSString stringWithFormat:@"@\"%@\"",NSStringFromClass([NSString class])] UTF8String] }; //type
    objc_property_attribute_t ownership0 = { "C", "" }; // C = copy
    objc_property_attribute_t ownership = { "N", "" }; //N = nonatomic
    objc_property_attribute_t backingivar  = { "V", [NSString stringWithFormat:@"_%@",[NSString stringWithCString:propertyName encoding:NSUTF8StringEncoding]].UTF8String };  //variable name
    objc_property_attribute_t attrs[] = {type, ownership0, ownership, backingivar};
    class_addProperty(targetClass, propertyName, attrs, 4);
}

// 列印屬性變數的封裝方法
void fx_printerProperty(Class targetClass){
    unsigned int outCount, i;
    objc_property_t *properties = class_copyPropertyList(targetClass, &outCount);
    for (i = 0; i < outCount; i++) {
        objc_property_t property = properties[i];
        fprintf(stdout, "%s %s\n", property_getName(property), property_getAttributes(property));
    }
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 動態建立類
        Class FXPerson = objc_allocateClassPair([NSObject class], "FXPerson", 0);
        // 新增成員變數
        class_addIvar(FXPerson, "name", sizeof(NSString *), log2(sizeof(NSString *)), "@");
        // 註冊到記憶體
        objc_registerClassPair(FXPerson);
        // 新增屬性變數
        fx_class_addProperty(FXPerson, "hobby");
        fx_printerProperty(FXPerson);
        // 新增方法(為屬性方法新增setter、getter方法)
        class_addMethod(FXPerson, @selector(setHobby:), (IMP)fxSetter, "v@:@");
        class_addMethod(FXPerson, @selector(hobby), (IMP)fxHobby, "@@:");

        // 開始使用
        id person = [FXPerson alloc];
        [person setValue:@"Felix" forKey:@"name"];
        NSLog(@"FXPerson的名字是:%@ 愛好是:%@", [person valueForKey:@"name"], [person valueForKey:@"hobby"]);
    }
    return 0;
}
複製程式碼

3.注意事項

  • 記得匯入<objc/runtime.h>
  • 新增成員變數class_addIvar必須在objc_registerClassPair前,因為註冊到記憶體時ro已經確定了,不能再往ivars新增(同第四個面試題)
  • 新增屬性變數class_addProperty可以在註冊記憶體前後,因為是往rw中新增的
  • class_addProperty中“屬性的屬性”——nonatomic/copy是根據屬性的型別變化而變化的
  • class_addProperty不會自動生成setter和getter方法,因此直接呼叫KVC會崩潰

不只可以通過KVC列印來檢驗,也可以下斷點檢視ro、rw的結構來檢驗

六、關聯物件分析

實則是為了解決分類建立屬性的問題

1.分類直接新增屬性的後果

  • 編譯會出現警告:沒有setter方法和getter方法
  • 執行會報錯:-[FXPerson setName:]: unrecognized selector sent to instance 0x100f61180'
    iOS探索 runtime面試題分析

2.為什麼不能直接新增屬性

Categoryruntime中是用一個結構體表示的:

struct category_t {
    const char *name;
    classref_t cls;
    struct method_list_t *instanceMethods;
    struct method_list_t *classMethods;
    struct protocol_list_t *protocols;
    struct property_list_t *instanceProperties;
    // Fields below this point are not always present on disk.
    struct property_list_t *_classProperties;
    ...
};
複製程式碼

裡面雖然可以新增屬性變數,但是這些properties並不會自動生成Ivar,也就是不會有 @synthesize的作用,dyld載入期間,這些分類會被載入並patch到相應的類中。這是一個動態過程,Ivar不能動態新增

3.解決方案

手動實現setter、getter方法,關聯物件

- (void)setName:(NSString *)name {
    /**
    引數一:id object : 給哪個物件新增屬性,這裡要給自己新增屬性,用self。
    引數二:void * == id key : 屬性名,根據key獲取關聯物件的屬性的值,在objc_getAssociatedObject中通過次key獲得屬性的值並返回。
    引數三:id value : 關聯的值,也就是set方法傳入的值給屬性去儲存。
    引數四:objc_AssociationPolicy policy : 策略,屬性以什麼形式儲存。
    */
    objc_setAssociatedObject(self, @"name", name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (NSString *)name {
    /**
    引數一:id object : 獲取哪個物件裡面的關聯的屬性。
    引數二:void * == id key : 什麼屬性,與objc_setAssociatedObject中的key相對應,即通過key值取出value。
    */
    return objc_getAssociatedObject(self, @"name");
}
複製程式碼

4.關聯物件原理

  1. setter方法——objc_setAssociatedObject分析

蘋果設計介面時往往會加個中間層——即使底層實現邏輯發生變化也不會影響到對外介面

void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) {
    _object_set_associative_reference(object, (void *)key, value, policy);
}
複製程式碼

跟進去看看_object_set_associative_reference實現

void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
    // This code used to work when nil was passed for object and key. Some code
    // probably relies on that to not crash. Check and handle it explicitly.
    // rdar://problem/44094390
    if (!object && !value) return;
    
    assert(object);
    
    if (object->getIsa()->forbidsAssociatedObjects())
        _objc_fatal("objc_setAssociatedObject called on instance (%p) of class %s which does not allow associated objects", object, object_getClassName(object));
    
    // retain the new value (if any) outside the lock.
    // 在鎖之外保留新值(如果有)。
    ObjcAssociation old_association(0, nil);
    // acquireValue會對retain和copy進行操作,
    id new_value = value ? acquireValue(value, policy) : nil;
    {
        // 關聯物件的管理類
        AssociationsManager manager;
        // 獲取關聯的 HashMap -> 儲存當前關聯物件
        AssociationsHashMap &associations(manager.associations());
        // 對當前的物件的地址做按位去反操作 - 就是 HashMap 的key (雜湊函式)
        disguised_ptr_t disguised_object = DISGUISE(object);
        if (new_value) {
            // break any existing association.
            // 獲取 AssociationsHashMap 的迭代器 - (物件的) 進行遍歷
            AssociationsHashMap::iterator i = associations.find(disguised_object);
            if (i != associations.end()) {
                // secondary table exists
                ObjectAssociationMap *refs = i->second;
                // 根據key去獲取關聯屬性的迭代器
                ObjectAssociationMap::iterator j = refs->find(key);
                if (j != refs->end()) {
                    old_association = j->second;
                    // 替換設定新值
                    j->second = ObjcAssociation(policy, new_value);
                } else {
                    // 到最後了 - 直接設定新值
                    (*refs)[key] = ObjcAssociation(policy, new_value);
                }
            } else {
                // create the new association (first time).
                // 如果AssociationsHashMap從沒有物件的關聯資訊表,
                // 那麼就建立一個map並通過傳入的key把value存進去
                ObjectAssociationMap *refs = new ObjectAssociationMap;
                associations[disguised_object] = refs;
                (*refs)[key] = ObjcAssociation(policy, new_value);
                object->setHasAssociatedObjects();
            }
        } else {
            // setting the association to nil breaks the association.
            // 如果傳入的value是nil,並且之前使用相同的key儲存過關聯物件,
            // 那麼就把這個關聯的value移除(這也是為什麼傳入nil物件能夠把物件的關聯value移除)
            AssociationsHashMap::iterator i = associations.find(disguised_object);
            if (i !=  associations.end()) {
                ObjectAssociationMap *refs = i->second;
                ObjectAssociationMap::iterator j = refs->find(key);
                if (j != refs->end()) {
                    old_association = j->second;
                    refs->erase(j);
                }
            }
        }
    }
    // release the old value (outside of the lock).
    // 最後把之前使用傳入的這個key儲存的關聯的value釋放(OBJC_ASSOCIATION_SETTER_RETAIN策略儲存的)
    if (old_association.hasValue()) ReleaseValue()(old_association);
}
複製程式碼
  • ObjcAssociation old_association(0, nil)處理傳進來的值得到new_value
  • 獲取到管理所有關聯物件的hashmap總表的管理者AssociationsManager,然後拿到hashmap總表AssociationsHashMap
  • DISGUISE(object)對關聯物件的地址進行取反操作得到雜湊表對應的下標index
  • 如果new_value為空(即對屬性賦值為nil)就直接找到相應的表進行刪除
  • 如果new_value不為空,就拿到總表的迭代器通過拿到的下標index進行遍歷查詢;如果找到管理物件的關聯屬性雜湊map表,然後再通過key去遍歷取值
    • 如果取到了,就先把新值設定到key上,再將舊值釋放掉
    • 如果沒取到,就直接將新值設定在key上

還是不明白就LLDB斷點除錯唄

iOS探索 runtime面試題分析

  1. getter方法——objc_getAssociatedObject分析
id objc_getAssociatedObject(id object, const void *key) {
    return _object_get_associative_reference(object, (void *)key);
}
複製程式碼
id _object_get_associative_reference(id object, void *key) {
    id value = nil;
    uintptr_t policy = OBJC_ASSOCIATION_ASSIGN;
    {
        // 關聯物件的管理類
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.associations());
        // 生成偽裝地址。處理引數 object 地址
        disguised_ptr_t disguised_object = DISGUISE(object);
        // 所有物件的額迭代器
        AssociationsHashMap::iterator i = associations.find(disguised_object);
        if (i != associations.end()) {
            ObjectAssociationMap *refs = i->second;
            // 內部物件的迭代器
            ObjectAssociationMap::iterator j = refs->find(key);
            if (j != refs->end()) {
                // 找到 - 把值和策略讀取出來
                ObjcAssociation &entry = j->second;
                value = entry.value();
                policy = entry.policy();
                // OBJC_ASSOCIATION_GETTER_RETAIN - 就會持有一下
                if (policy & OBJC_ASSOCIATION_GETTER_RETAIN) {
                    objc_retain(value);
                }
            }
        }
    }
    if (value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) {
        objc_autorelease(value);
    }
    return value;
}
複製程式碼

objc_getAssociatedObjectobjc_setAssociatedObject的逆過程

iOS探索 runtime面試題分析

七、weak置空原理

當面試官問你weak置空原理是什麼,你可能只知道weak怎麼用卻不知道怎麼答吧

weak一行打下斷點執行專案

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        FXPerson *person = [[FXPerson alloc] init];
        id __weak person = object;
    }
    return 0;
}
複製程式碼

Xcode選單欄Debug->Debug Workflow->Always show Disassembly打上勾檢視彙編——彙編程式碼會來到libobjc庫objc_initWeak

iOS探索 runtime面試題分析

1.weak建立過程

①objc_initWeak

  • location:表示__weak指標的地址(我們研究的就是__weak指標指向的內容怎麼置為nil)
  • newObj:所引用的物件,即例子中的person
id
objc_initWeak(id *location, id newObj)
{
    if (!newObj) {
        *location = nil;
        return nil;
    }

    return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>
        (location, (objc_object*)newObj);
}
複製程式碼

②storeWeak

  • HaveOld:weak指標之前是否已經指向了一個弱引用
  • HaveNew:weak指標是否需要指向一個新引用
  • CrashIfDeallocating:如果被弱引用的物件正在析構,此時再弱引用該物件,是否應該crash

storeWeak最主要的兩個邏輯點(原始碼太長,這裡不貼了)

iOS探索 runtime面試題分析

由於是第一次呼叫,所以走haveNew分支——獲取到的是新的雜湊表SideTable,主要執行了weak_register_no_lock方法來進行插入

③weak_register_no_lock

  • 主要進行了isTaggedPointerdeallocating條件判斷
  • 將被弱引用物件所在的weak_table中的weak_entry_t雜湊陣列中取出對應的weak_entry_t
  • 如果weak_entry_t不存在,則會新建一個並插入
  • 如果存在就將指向被弱引用物件地址的指標referrer通過函式append_referrer插入到對應的weak_entry_t引用陣列

iOS探索 runtime面試題分析

④append_referrer

找到弱引用物件的對應的weak_entry雜湊陣列中插入

iOS探索 runtime面試題分析

2.weak建立流程

iOS探索 runtime面試題分析

3.weak銷燬過程

由於弱引用在析構dealloc時自動置空,所以檢視dealloc的底層實現並LLVM除錯

  • _objc_rootDealloc->rootDealloc
  • rootDealloc->object_dispose
  • object_dispose->objc_destructInstance
  • objc_destructInstance->clearDeallocating
  • clearDeallocating->sidetable_clearDeallocating
  • sidetable_clearDeallocating3->table.refcnts.erase(it)

4.weak銷燬流程

(非本人做圖)具體可查閱iOS底層學習 - 記憶體管理之weak原理探究

iOS探索 runtime面試題分析

八、Method Swizzing坑點

method swizzing不瞭解的可以閱讀iOS逆向 程式碼注入+Hook

1.黑魔法應用

在日常開發中,再好的程式設計師都會犯錯,比如陣列越界

NSArray *array = @[@"F", @"e", @"l", @"i", @"x"];
NSLog(@"%@", array[5]);
NSLog(@"%@", [array objectAtIndex:5]);
複製程式碼

因此為了避免陣列越界這種問題,大神們開始玩起了黑魔法——method swizzing

  • 新建NSArray分類
  • 匯入runtime標頭檔案——<objc/runtime.h>
  • 寫下新的方法
  • +load利用黑魔法交換方法
#import "NSArray+FX.h"
#import <objc/runtime.h>

@implementation NSArray (FX)

+ (void)load {
    // 交換objectAtIndex方法
    Method oriMethod1 = class_getInstanceMethod(self, @selector(objectAtIndex:));
    Method swiMethod1 = class_getInstanceMethod(self, @selector(fx_objectAtIndex:));
    method_exchangeImplementations(oriMethod1, swiMethod1);
    
    // 交換取下標方法
    Method oriMethod2 = class_getInstanceMethod(self, @selector(objectAtIndexedSubscript:));
    Method swiMethod2 = class_getInstanceMethod(self, @selector(fx_objectAtIndexedSubscript:));
    method_exchangeImplementations(oriMethod2, swiMethod2);
}

- (void)fx_objectAtIndex:(NSInteger)index {
    if (index > self.count - 1) {
        NSLog(@"objectAtIndex————————陣列越界");
        return;
    }
    return [self fx_objectAtIndex:index];
}

- (void)fx_objectAtIndexedSubscript:(NSInteger)index {
    if (index > self.count - 1) {
        NSLog(@"取下標————————陣列越界");
        return;
    }
    return [self fx_objectAtIndexedSubscript:index];
}

@end
複製程式碼

然而程式還是無情的崩了...

iOS探索 runtime面試題分析
其實在iOS中NSNumber、NSArray、NSDictionary等這些類都是類簇(Class Clusters),一個NSArray的實現可能由多個類組成。所以如果想對NSArray進行方法交換,必須獲取到其真身進行方法交換,直接對NSArray進行操作是無效的

以下是NSArrayNSDictionary本類的類名

iOS探索 runtime面試題分析

這樣就好辦了,可以使用runtime取出本類

iOS探索 runtime面試題分析

2.坑點一

黑魔法最好寫成單例,避免多次交換

比如說新增了[NSArray load]程式碼,方法實現又交換回去了導致了崩潰

iOS探索 runtime面試題分析

+load方法改寫成單例(雖然不常見,但也要避免)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        // 交換objectAtIndex方法
        Method oriMethod1 = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
        Method swiMethod1 = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(fx_objectAtIndex:));
        method_exchangeImplementations(oriMethod1, swiMethod1);
        
        // 交換取下標方法
        Method oriMethod2 = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndexedSubscript:));
        Method swiMethod2 = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(fx_objectAtIndexedSubscript:));
        method_exchangeImplementations(oriMethod2, swiMethod2);
    });
}
複製程式碼

3.坑點二

①子類交換父類實現的方法

  • 父類FXPerson類中有-doInstance方法,子類FXSon類沒有重寫
  • FXSon類新建分類做了方法交換,新方法中呼叫舊方法
  • FXPerson類FXSon類呼叫-doInstance

iOS探索 runtime面試題分析

子類列印出結果,而父類呼叫卻崩潰了,為什麼會這樣呢?

因為FXSon類交換方法時取得doInstance先在本類搜尋方法,再往父類裡查詢,在FXFather中找到了方法實現就把它跟新方法進行交換了。可是新方法是在FXSon分類中的,FXFather找不到imp就unrecognized selector sent to instance 0x600002334250

所以這種情況下應該只交換子類的方法,不動父類的方法

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Method oriMethod = class_getInstanceMethod(self, @selector(doInstance));
        Method swiMethod = class_getInstanceMethod(self, @selector(fx_doInstance));
        
        BOOL didAddMethod = class_addMethod(self, @selector(doInstance), method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
        if (didAddMethod) {
            class_replaceMethod(self, @selector(fx_doInstance), method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
        } else {
            method_exchangeImplementations(oriMethod, swiMethod);
        }
    });
}
複製程式碼
  • 通過class_addMethodFXSon類新增方法(class_addMethod不會取代本類中已存在的實現,只會覆蓋本類中繼承父類的方法實現)
    • 取得新方法swiMethod的實現和方法型別
    • 往方法名@selector(fx_doInstance)新增方法
    • class_addMethod 把新方法實現放到舊方法名中,此刻呼叫doInstance就是呼叫fx_doInstance,但是呼叫fx_doInstance會崩潰
  • 根據didAddMethod判斷是否新增成功
    • 新增成功說明之前本類沒有實現——class_replaceMethod替換方法
    • 新增失敗說明之前本類已有實現——method_exchangeImplementations交換方法
    • class_replaceMethoddoInstance方法實現替換掉fx_doInstance中的方法實現

iOS探索 runtime面試題分析
iOS探索 runtime面試題分析

FXPerson類只寫了方法宣告,沒有方法實現,卻做了方法交換——會造成死迴圈

iOS探索 runtime面試題分析

  • doInstance方法中新增了新的方法實現
  • fx_doInstance方法中想用舊的方法實現替代之前的方法實現,可是找不到doInstance實現,導致class_replaceMethod無效->在fx_doInstance中呼叫fx_doInstance就會死迴圈

iOS探索 runtime面試題分析

因此改變程式碼邏輯如下

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Method oriMethod = class_getInstanceMethod(self, @selector(doInstance));
        Method swiMethod = class_getInstanceMethod(self, @selector(fx_doInstance));
        
        if (!oriMethod) {
            class_addMethod(self, @selector(doInstance), method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
            method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd) {
                NSLog(@"方法未實現");
            }));
        }
        
        BOOL didAddMethod = class_addMethod(self, @selector(doInstance), method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
        if (didAddMethod) {
            class_replaceMethod(self, @selector(fx_doInstance), method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
        } else {
            method_exchangeImplementations(oriMethod, swiMethod);
        }
    });
}
複製程式碼
  • 未實現方法時用新的方法實現新增方法,此時呼叫doInstance就是呼叫fx_doInstance
  • 由於此時fx_doInstance方法內部還是呼叫自己,用block修改fx_doInstance的實現,就可以斷開死迴圈了
  • 由於oriMethod(0x0),method_exchangeImplementations交換失敗

iOS探索 runtime面試題分析

4.注意事項

使用Method Swizzling有以下注意事項:

  • 儘可能在+load方法中交換方法
  • 最好使用單例保證只交換一次
  • 自定義方法名不能產生衝突
  • 對於系統方法要呼叫原始實現,避免對系統產生影響
  • 做好註釋(因為方法交換比較繞)
  • 迫不得已情況下才去使用方法交換

這是一份做好封裝的Method Swizzling交換方法

+ (void)FXMethodSwizzlingWithClass:(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) {
        class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
        method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd) {
            NSLog(@"方法未實現");
        }));
    }

    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);
    }
}
複製程式碼

寫在後面

現在iOS面試都喜歡問些底層的問題,這可以非常直觀的看出你對runtime的理解,而且在知識點上繼續推敲、挖坑,當你答不上來時只能任人宰割——壓低薪資或不錄用。所以還是要在平時多加練習,只要懂了原理就能舉一反三了,即便面試的時候不能答的十全十美,也能給面試官留下個好印象

相關文章