iOS Runtime 運行時機制

runtime(「runtime&runloop 面試、工作」

runtime(簡稱運行時),是一套 純C(C和匯編寫的) 的API。而 OC 就是 運行時機制,也就是在運行時候的一些機制,其中最主要的是 消息機制。
OC的函數(shù)調(diào)用成為消息發(fā)送,屬于 動態(tài)調(diào)用過程。在編譯的時候并不能決定真正調(diào)用哪個函數(shù),只有在真正運行的時候才會根據(jù)函數(shù)的名稱找到對應的函數(shù)來調(diào)用。

runtime 常見作用

  • 發(fā)送消息
  • 動態(tài)添加屬性 objc_setAssociatedObject/objc_getAssociatedObject
  • 動態(tài)添加方法 resolveInstanceMethod
  • 攔截并替換方法 method_exchangeImplementations
  • 字典轉(zhuǎn)模型KVC實現(xiàn)(必須保證,模型中的屬性和字典中的key 一一對應)
    利用運行時,遍歷模型中所有屬性,根據(jù)模型的屬性名,去字典中查找key,取出對應的值,給模型的屬性賦值
  • 實現(xiàn) NSCoding 的自動歸檔和解檔
- (void)encodeWithCoder:(NSCoder *)encoder

{
    unsigned int count = 0;
    Ivar *ivars = class_copyIvarList([Movie class], &count);

    for (int i = 0; i<count; i++) {
        // 取出i位置對應的成員變量
        Ivar ivar = ivars[i];
        // 查看成員變量
        const char *name = ivar_getName(ivar);
        // 歸檔
        NSString *key = [NSString stringWithUTF8String:name];
        id value = [self valueForKey:key];
        [encoder encodeObject:value forKey:key];
    }
    free(ivars);
}

isa、屬性列表、方法列表、協(xié)議列表

isa

一個objc 對象的 isa 的指針指向什么?有什么作用?
1、每一個對象本質(zhì)上都是一個類的實例。其中類定義了成員變量和成員方法的列表。對象通過對象的isa指針指向所屬類
2、每一個類本質(zhì)上都是一個對象,類其實是元類(meteClass)的實例。元類定義了類方法的列表。類通過類的isa指針指向元類。
3、元類保存了類方法的列表。當類方法被調(diào)用時,先會從本身查找類方法的實現(xiàn),如果沒有,元類會向他父類查找該方法。同時注意的是:元類(meteClass)也是類,它也是對象。元類通過isa指針最終指向的是一個根元類(root meteClass)。
4、根元類的isa指針指向本身,這樣形成了一個封閉的內(nèi)循環(huán)。

image.png

屬性/方法/協(xié)議列表

// 獲取協(xié)議列表
__unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);

// 得到類的所有方法  
Method allMethods = class_copyMethodList([Person class], &count);  
// 獲得某個類的實例對象方法
Method class_getInstanceMethod(Class cls , SEL name)
// 獲得某個類的類方法
Method class_getClassMethod(Class cls , SEL name)
// 動態(tài)添加方法:   
// 第一個參數(shù)表示Class cls 類型;   
// 第二個參數(shù)表示待調(diào)用的方法名稱;   
// 第三個參數(shù)(IMP)myAddingFunction,IMP是一個函數(shù)指針,這里表示指定具體實現(xiàn)方法myAddingFunction;   
// 第四個參數(shù)表方法的參數(shù),0代表沒有參數(shù);   
class_addMethod([_per class], @selector(sayHi), (IMP)myAddingFunction, 0);  
// 替換原方法實現(xiàn) 
class_replaceMethod(toolClass, cusSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
// 交換兩個方法  
method_exchangeImplementations(method1, method2);

// 得到所有成員變量  
Ivar allVariables = class_copyIvarList([Person class], &count);  
// 得到所有屬性  
objc_property_t properties = class_copyPropertyList([Person class], &count);  
// 根據(jù)名字得到實例變量的Ivar指針  Ivar oneIVIvar = class_getInstanceVariable([Person class], name);  
// 找到后可以直接對私有變量賦值  
object_setIvar(_per, oneIVIvar, @"Mike");// 強制修改name屬性  

// 關(guān)聯(lián)兩個對象
// id object:表示關(guān)聯(lián)者,是一個對象,變量名理所當然也是object 
// const void key :獲取被關(guān)聯(lián)者的索引key 
// id value :被關(guān)聯(lián)者,這里是一個block 
// objc_AssociationPolicy policy : 關(guān)聯(lián)時采用的協(xié)議,有assign,retain,copy等協(xié)議,一般使用OBJC_ASSOCIATION_RETAIN_NONATOMIC
objc_setAssociatedObject(id object, const void key, id value, objc_AssociationPolicy policy)

// 利用參數(shù)key 將對象object中存儲的對應值取出來id 
objc_getAssociatedObject(id object , const void *key)

// 獲得成員變量的名字
const char *ivar_getName(Ivar v)
// 獲得成員變量的類型
const char *ivar_getTypeEndcoding(Ivar v)

消息傳遞機制如何查找方法

根據(jù)對象的 isa 指針找到類對象 id,在查詢類對象里面的 methodLists 方法函數(shù)列表,如果沒有在好到,在沿著 superClass ,尋找父類,再在父類 methodLists 方法列表里面查詢,最終找到 SEL ,根據(jù) id 和 SEL 確認 IMP(指針函數(shù)),在發(fā)送消息;

當發(fā)送消息的時候,我們會根據(jù)類里面的 methodLists 列表去查詢我們要動用的SEL,當查詢不到的時候,我們會一直沿著父類查詢,當最終查詢不到的時候我們會報 unrecognized selector 錯誤,當系統(tǒng)查詢不到方法的時候,會調(diào)用 +(BOOL)resolveInstanceMethod:(SEL)sel 動態(tài)解釋的方法來給我一次機會來添加,調(diào)用不到的方法?;蛘呶覀兛梢栽俅问褂?-(id)forwardingTargetForSelector:(SEL)aSelector 重定向的方法來告訴系統(tǒng),該調(diào)用什么方法,一來保證不會崩潰。

Category實現(xiàn)原理

在objc_class結(jié)構(gòu)體中:ivars是objc_ivar_list指針;methodLists是指向objc_method_list指針的指針。也就是說可以動態(tài)修改*methodLists的值來添加成員方法,這也是Category實現(xiàn)的原理

method swizzling原理

method swizzling,簡單說就是進行方法交換。
在Objective-C中調(diào)用一個方法,其實是向一個對象發(fā)送消息,查找消息的唯一依據(jù)是selector的名字。利用Objective-C的動態(tài)特性,可以實現(xiàn)在運行時偷換selector對應的方法實現(xiàn),達到給方法掛鉤的目的
selector的本質(zhì)其實就是方法名,IMP有點類似函數(shù)指針,指向具體的Method實現(xiàn),通過selector就可以找到對應的IMP。
交換方法的幾種實現(xiàn)方式
利用 method_exchangeImplementations 交換兩個方法的實現(xiàn)
利用 class_replaceMethod 替換方法的實現(xiàn)
利用 method_setImplementation 來直接設置某個方法的IMP。

SEL/IMP(Method = SEL + IMP)

image.png

SEL

SEL 本質(zhì)上是一個整型,是 oc 編譯時對函數(shù)的編號。相同的函數(shù)名具有一樣的函數(shù)編號,而同名函數(shù)在不同類中有不同實現(xiàn),所以 SEL 和 IMP是一對多的關(guān)系。
結(jié)構(gòu)如下:

typedef struct objc_selector *SEL;

Objective-C 在編譯的時候,會依據(jù)方法的名字、參數(shù)序列、生成一個整型標識的地址( int 類型的地址):這個標識就是 SEL
只要方法相同, SEL 就是一樣的。
獲取 SEL 值:
a、sel_registerName函數(shù)
b、Objective-C編譯器提供的@selector()
c、NSSelectorFromString()方法

IMP

IMP 本質(zhì)上是一個函數(shù)指針,指向函數(shù)的實現(xiàn)。
結(jié)構(gòu)如下:

/*
    第一個參數(shù):是指向self的指針(如果是實例方法,則是類實例的內(nèi)存地址;如果是類方法,則是指向元類的指針)
    第二個參數(shù):是方法選擇器(selector)
    接下來的參數(shù):方法的參數(shù)列表。
*/
id (*IMP)(id, SEL,...)

Method 用于表示類定義中的方法
結(jié)構(gòu)如下

typedef struct objc_method *Method
struct objc_method{
    SEL method_name      OBJC2_UNAVAILABLE; // 方法名
    char *method_types   OBJC2_UNAVAILABLE;
    IMP method_imp       OBJC2_UNAVAILABLE; // 方法實現(xiàn)
}

我們可以看到該結(jié)構(gòu)體中包含一個SEL和IMP,實際上相當于在SEL和IMP之間作了一個映射。有了SEL,我們便可以找到對應的IMP,從而調(diào)用方法的實現(xiàn)代碼。

IMP和SEL關(guān)系

在 objective-c的方法調(diào)用

[receiver message];

運行時轉(zhuǎn)為:

objc_msgSend(receiver, selector);

系統(tǒng)根據(jù) 接收者receiver和函數(shù)編號selctor查在類的dispatch table查對應的函數(shù)實現(xiàn)imp,然后執(zhí)行函數(shù)并返回執(zhí)行結(jié)果。

dispatch table 查找 imp 的流程及原理

在類的dispatch table 中存放著 sel 和 imp的對一一應關(guān)系。類似于字典通過 key 可以知道 value。若 imp 在當前類的dispatch table 中未被找到,則會通過 isa指針在其父類中的 dispatch table 中查找,若在當前類的父類中未被找到,則會在父類的父類中查找,最終回溯到NSObject類中。

image.png

若每次函數(shù)調(diào)用都要通過 sel 在 dispatch table中一層一層的查找對應的 imp, 這將是一個很大的時間開銷,因此objective-c的設計者們使用了緩存技術(shù),cache dispatch table 用于緩存調(diào)用過方法, 使查找 imp 的過程更加高效。

SEL selector = @selector(foo);
IMP imp = [receiver methodForSelector: selector];
imp();
為什么不直接獲得函數(shù)指針,而要從SEL這個編號走一圈再回到函數(shù)指針呢?

有了SEL這個中間過程,我們可以對一個編號和什么方法映射做些操作,也就是說我們可以一個SEL指向不同的函數(shù)指針,這樣就可以完成一個方法名在不同時候執(zhí)行不同的函數(shù)體。另外可以將SEL作為參數(shù)傳遞給不同的類執(zhí)行。也就是說我們某些業(yè)務我們只知道方法名但需要根據(jù)不同的情況讓不同類執(zhí)行的時候,SEL可以幫助我們。

獲取IMP

runtime提供了兩種方法

IMP class_getMethodImplementation(Class cls, SEL name);
IMP method_getImplementation(Method m)

第一種方法:class_getMethodImplementation

- (void)getIMP_class_getMethodImplementationFromSelector:(SEL)aSelector{
    
    const char *className = object_getClassName([self class]);
    // 獲取實例的IMP
    IMP instanceIMP = class_getMethodImplementation(objc_getClass(className), aSelector);
    // 獲取類的IMP
    IMP classIMP = class_getMethodImplementation(objc_getMetaClass(className), aSelector);
    
    NSLog(@"instanceIMP:%p classIMP:%p",instanceIMP,classIMP);
}

對于第一種方法而言,類方法和實例方法實際上都是通過調(diào)用class_getMethodImplementation()來尋找IMP地址的

第二種方法:method_getImplementation

- (void)getIMP_method_getImplementationFromSelector:(SEL)aSelector{
    
    const char *className = object_getClassName([self class]);
    // 獲取類中的某個實例方法
    Method instanceMethod = class_getInstanceMethod(objc_getClass(className), aSelector);
    // 獲取類中的某個類方法
    Method classMethod = class_getClassMethod(objc_getClass(className), aSelector);
    
    // 獲取實例的IMP
    IMP instanceIMP = method_getImplementation(instanceMethod);
    // 獲取類的IMP
    IMP classIMP = method_getImplementation(classMethod);
    
    NSLog(@"instanceIMP:%p classIMP:%p",instanceIMP,classIMP);
}

而method_getImplementation而言,傳入的參數(shù)只有method,區(qū)分類方法和實例方法在于封裝method的函數(shù)
類方法
Method class_getClassMethod(Class cls, SEL name)
實例方法
Method class_getInstanceMethod(Class cls, SEL name)
最后調(diào)用IMP method_getImplementation(Method m) 獲取IMP地址

其他問答

下面的代碼輸出什么?

@implementation Son : NSObject
- (id)init
{
    self = [super init];
    if (self) {
        NSLog(@"%@", NSStringFromClass([self class]));
        NSLog(@"%@", NSStringFromClass([super class]));
    }
    return self;
}
@end

思考一下,會打印出來什么?

答案:都輸出 Son

class 獲取當前方法的調(diào)用者的類,superClass 獲取當前方法的調(diào)用者的父類,super 僅僅是一個編譯指示器,就是給編譯器看的,不是一個指針。
本質(zhì):只要編譯器看到super這個標志,就會讓當前對象去調(diào)用父類方法,本質(zhì)還是當前對象在調(diào)用

這個題目主要是考察關(guān)于objc中對 self 和 super 的理解:
self 是類的隱藏參數(shù),指向當前調(diào)用方法的這個類的實例。而 super 本質(zhì)是一個編譯器標示符,和 self 是指向的同一個消息接受者
當使用 self 調(diào)用方法時,會從當前類的方法列表中開始找,如果沒有,就從父類中再找;
而當使用 super時,則從父類的方法列表中開始找。然后調(diào)用父類的這個方法
調(diào)用 [self class] 時,會轉(zhuǎn)化成 objc_msgSend 函數(shù)

id objc_msgSend(id self, SEL op, ...)
- 調(diào)用 `[super class]`時,會轉(zhuǎn)化成 `objc_msgSendSuper` 函數(shù).

id objc_msgSendSuper(struct objc_super *super, SEL op, ...)
第一個參數(shù)是 objc_super 這樣一個結(jié)構(gòu)體,其定義如下
 struct objc_super {
 __unsafe_unretained id receiver;
 __unsafe_unretained Class super_class;
 };

第一個成員是 receiver, 類似于上面的 objc_msgSend函數(shù)第一個參數(shù)self
第二個成員是記錄當前類的父類是什么,告訴程序從父類中開始找方法,找到方法后,最后內(nèi)部是使用 objc_msgSend(objc_super->receiver, @selector(class))去調(diào)用, 此時已經(jīng)和[self class]調(diào)用相同了,故上述輸出結(jié)果仍然返回 Son

objc Runtime 開源代碼對- (Class)class方法的實現(xiàn)
-(Class)class { return object_getClass(self); 
}

問題: 能否向編譯后得到的類中增加實例變量?能否向運行時創(chuàng)建的類中添加實例變量?為什么?
解答: 1、不能向編譯后得到的類增加實例變量 2、能向運行時創(chuàng)建的類中添加實例變量?!窘忉尅浚?. 編譯后的類已經(jīng)注冊在 runtime 中,類結(jié)構(gòu)體中的 objc_ivar_list 實例變量的鏈表和 instance_size 實例變量的內(nèi)存大小已經(jīng)確定,runtime會調(diào)用 class_setvarlayout 或 class_setWeaklvarLayout 來處理strong weak 引用.所以不能向存在的類中添加實例變量。2. 運行時創(chuàng)建的類是可以添加實例變量,調(diào)用class_addIvar函數(shù). 但是的在調(diào)用 objc_allocateClassPair 之后,objc_registerClassPair 之前,原因同上.

問題: runtime如何實現(xiàn)weak變量的自動置nil?
解答: 對于 weak 對象會放入一個 hash 表中。 用 weak 指向的對象內(nèi)存地址作為 key,當此對象的引用計數(shù)為0的時候會 dealloc,假如 weak 指向的對象內(nèi)存地址是a,那么就會以a為鍵, 在這個 weak 表中搜索,找到所有以a為鍵的 weak 對象,從而設置為 nil。

問題: 給類添加一個屬性后,在類結(jié)構(gòu)體里哪些元素會發(fā)生變化?
解答: instance_size :實例的內(nèi)存大??;objc_ivar_list *ivars:屬性列表

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

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