Runtime全方位裝逼指南

?楔子
Runtime是什么?見名知意,其概念無非就是“因為 Objective-C 是一門動態(tài)語言,所以它需要一個運行時系統(tǒng)……這就是 Runtime 系統(tǒng)”云云。對博主這種菜鳥而言,Runtime 在實際開發(fā)中,其實就是一組C語言的函數(shù)。胡適說:“多研究些問題,少談些主義”,云山霧罩的概念聽多了總是容易頭暈,接下來我們直接從代碼入手學習 Runtime。
?
1、由objc_msgSend說開去:
Objective-C 中的方法調(diào)用,不是簡單的方法調(diào)用,而是發(fā)送消息,也就是說,其實 [receiver message] 會被編譯器轉(zhuǎn)化為: objc_msgSend(receiver, selector),何以證明?新建一個類 MyClass,其.m文件如下:

#import "MyClass.h"
@implementation MyClass

-(instancetype)init{
    if (self = [super init]) {
        [self showUserName];
    }
    return self;
}

-(void)showUserName{
    NSLog(@"Dave Ping");
}

使用 clang 重寫命令:

$ clang -rewrite-objc MyClass.m

然后在同一目錄下會多出一個 MyClass.cpp 文件,雙擊打開,可以看到 init 方法已經(jīng)被編譯器轉(zhuǎn)化為下面這樣:

static instancetype _I_MyClass_init(MyClass * self, SEL _cmd) {
    if (self = ((MyClass *(*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("MyClass"))}, sel_registerName("init"))) {
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("showUserName"));
    }
    return self;
}

我們要找的就是它:

((void (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("showUserName"))

objc_msgSend 函數(shù)被定義在 objc/message.h 目錄下,其函數(shù)原型是醬紫滴:

OBJC_EXPORT void objc_msgSend(void /* id self, SEL op, ... */ )

?該函數(shù)有兩個參數(shù),一個 id 類型,一個 SEL 類型。

2、SEL
SEL 被定義在 ?objc/objc.h 目錄下:

typedef struct objc_selector *SEL;

其實它就是個映射到方法的C字符串,你可以用 Objective-C 編譯器命令 @selector() 或者 Runtime 系統(tǒng)的 sel_registerName 函數(shù)來獲得一個 SEL 類型的方法選擇器。

3、id
與 SEL 一樣,id 也被定義在 ?objc/objc.h 目錄下:

typedef struct objc_object *id;

id 是一個結構體指針類型,它可以指向 Objective-C 中的任何對象。objc_object 結構體定義如下:

struct objc_object { Class isa OBJC_ISA_AVAILABILITY;};

我們通常所說的對象,就長這個樣子,這個結構體只有一個成員變量? isa,?對象可以通過 ?isa 指針找到其所屬的類。isa 是一個 ?Class 類型的成員變量,那么 Class 又是什么呢?

4、Class
Class 也是一個結構體指針類型:

typedef struct objc_class *Class;

objc_class 結構體是醬紫滴:

struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class super_class                                        OBJC2_UNAVAILABLE;
    const char *name                                         OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;
    struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache *cache                                 OBJC2_UNAVAILABLE;
    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;

我們通常說的?類就長這樣子:
·Class 也有一個 isa 指針,指向其所屬的元類(meta).
·super_class:指向其超類.
·name:是類名.
·version:是類的版本信息.
·info:是類的詳情.
·instance_size:是該類的實例對象的大小.
·ivars:指向該類的成員變量列表.
·methodLists:指向該類的實例方法列表,它將方法選擇器和方法實現(xiàn)地址聯(lián)系起來。methodLists 是指向 ·objc_method_list 指針的指針,也就是說可以動態(tài)修改 *methodLists 的值來添加成員方法,這也是 Category 實現(xiàn)的原理,同樣解釋了 Category 不能添加屬性的原因.
·cache:Runtime 系統(tǒng)會把被調(diào)用的方法存到 ?cache 中(理論上講一個方法如果被調(diào)用?,那么它有可能今后還會被調(diào)用),下次查找的時候效率更高.
·protocols:指向該類的協(xié)議列表.


說到這里有點亂了,我們來捋一下,當我們調(diào)用一個方法時,其運行過程大致如下:

首先,Runtime 系統(tǒng)會把方法調(diào)用轉(zhuǎn)化為消息發(fā)送,即 objc_msgSend,并且把方法的調(diào)用者,和方法選擇器,當做參數(shù)傳遞過去.

此時,方法的調(diào)用者會通過 isa 指針來找到其所屬的類,然后在 cache 或者 methodLists 中查找該方法,找得到就跳到對應的方法去執(zhí)行.

如果在中沒有找到該方法,則通過 super_class 往上一級超類查找(如果一直找到 NSObject 都沒有找到該方法的話,這種情況,我們放到后面消息轉(zhuǎn)發(fā)的時候再說).

前面我們說 methodLists 指向該類的實例方法列表,實例方法-方法,那么類方法(+方法)存儲在哪兒呢?類方法被存儲在元類中,Class 通過 isa 指針即可找到其所屬的元類.


上圖實線是 super_class 指針,虛線是 isa 指針。根元類的超類是NSObject,而 isa 指向了自己。NSObject 的超類為 nil,也就是它沒有超類。

5、使用objc_msgSend
前面我們使用 clang 重寫命令,看到 Runtime 是如何將方法調(diào)用轉(zhuǎn)化為消息發(fā)送的。我們也可以依樣畫葫蘆,來學習使用一下 objc_msgSend。新建一個類 TestClass,添加如下方法:

-(void)showAge{
    NSLog(@"24");
}

-(void)showName:(NSString *)aName{
    NSLog(@"name is %@",aName);
}

-(void)showSizeWithWidth:(float)aWidth andHeight:(float)aHeight{
    NSLog(@"size is %.2f * %.2f",aWidth, aHeight);
}

-(float)getHeight{
    return 187.5f;
}

-(NSString *)getInfo{
    return @"Hi, my name is Dave Ping, I'm twenty-four years old in the year, I like apple, nice to meet you.";
}

我們可以像下面這樣,使用 objc_msgSend 依次調(diào)用這些方法:

    TestClass *objct = [[TestClass alloc] init];
    
    ((void (*) (id, SEL)) objc_msgSend) (objct, sel_registerName("showAge"));
    
    ((void (*) (id, SEL, NSString *)) objc_msgSend) (objct, sel_registerName("showName:"), @"Dave Ping");
    
    ((void (*) (id, SEL, float, float)) objc_msgSend) (objct, sel_registerName("showSizeWithWidth:andHeight:"), 110.5f, 200.0f);
    
    float f = ((float (*) (id, SEL)) objc_msgSend_fpret) (objct, sel_registerName("getHeight"));
    NSLog(@"height is %.2f",f);
    
    NSString *info = ((NSString* (*) (id, SEL)) objc_msgSend) (objct, sel_registerName("getInfo"));
    NSLog(@"%@",info);

也許你已經(jīng)注意到,objc_msgSend 在使用時都被強制轉(zhuǎn)換了一下,這是因為 objc_msgSend 這個函數(shù)至少要有兩個參數(shù),一個id消息接受者,一個SEL消息名稱。后面三個點代表參數(shù),是變參。也就是說方法攜帶的參數(shù),可以沒有,可以有多個。如果我們把調(diào)用 showAge 方法改成這樣:

objc_msgSend(objct, sel_registerName("showAge"));

Xcode 就會報錯:

Too many arguments to function call, expected 0, have 2.

完整的 objc_msgSend 使用代碼在?這里

6、objc_msgSendSuper
編譯器會根據(jù)情況在 objc_msgSend,objc_msgSend_stret,objc_msgSendSuper,objc_msgSendSuper_stret 或 objc_msgSend_fpret 五個方法中選擇一個來調(diào)用。如果消息是傳遞給超類,那么會調(diào)用 objc_msgSendSuper 方法,如果消息返回值是數(shù)據(jù)結構,就會調(diào)用 objc_msgSendSuper_stret 方法,如果返回值是浮點數(shù),則調(diào)用 objc_msgSend_fpret 方法。

這里我們重點說一下 objc_msgSendSuper,objc_msgSendSuper 函數(shù)原型如下:

OBJC_EXPORT void objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )

?當我們調(diào)用 [super selector] 時,Runtime 會調(diào)用 objc_msgSendSuper 方法,objc_msgSendSuper 方法有兩個參數(shù),super 和 op,Runtime 會把 selector 方法選擇器賦值給 op。而 super 是一個 objc_super 結構體指針,objc_super 結構體定義如下:

struct objc_super {
    /// Specifies an instance of a class.
    __unsafe_unretained id receiver;

    /// Specifies the particular superclass of the instance to message. 
#if !defined(__cplusplus)  &&  !__OBJC2__
    /* For compatibility with old objc-runtime.h header */
    __unsafe_unretained Class class;
#else
    __unsafe_unretained Class super_class;
#endif
    /* super_class is the first class to search */
};

Runtime 會創(chuàng)建一個 objc_spuer 結構體變量,將其地址作為參數(shù)(super)傳遞給 objc_msgSendSuper,并且將 self 賦值給 receiver:super—>receiver=self.
舉個栗子,問下面的代碼輸出什么:

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

答案是全部輸出 Son.
使用 clang 重寫命令,發(fā)現(xiàn)上述代碼被轉(zhuǎn)化為:

NSLog((NSString *)&__NSConstantStringImpl__var_folders_gm_0jk35cwn1d3326x0061qym280000gn_T_main_a5cecc_mi_0, NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("class"))));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_gm_0jk35cwn1d3326x0061qym280000gn_T_main_a5cecc_mi_1, NSStringFromClass(((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){ (id)self, (id)class_getSuperclass(objc_getClass("Son")) }, sel_registerName("class"))));

當調(diào)用 [super class] 時,會轉(zhuǎn)換成 objc_msgSendSuper 函數(shù):

第一步先構造 objc_super 結構體,結構體第一個成員就是 self。第二個成員是 (id)class_getSuperclass(objc_getClass(“Son”)).

第二步是去 Father 這個類里去找 - (Class)class,沒有,然后去 NSObject 類去找,找到了。最后內(nèi)部是使用 objc_msgSend(objc_super->receiver, @selector(class)) 去調(diào)用,此時已經(jīng)和 [self class] 調(diào)用相同了,所以兩個輸出結果都是 Son。

7、?對象關聯(lián)
對象關聯(lián)允許開發(fā)者對已經(jīng)存在的類在 Category 中添加自定義的屬性

OBJC_EXPORT void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_1);

·object 是源對象.
·value 是被關聯(lián)的對象.
·key 是關聯(lián)的鍵,objc_getAssociatedObject 方法通過不同的 key 即可取出對應的被關聯(lián)對象.
·policy 是一個枚舉值,表示關聯(lián)對象的行為,從命名就能看出各個枚舉值的含義:

typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    OBJC_ASSOCIATION_ASSIGN = 0,           /**< Specifies a weak reference to the associated object. */
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object. 
                                            *   The association is not made atomically. */
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,   /**< Specifies that the associated object is copied. 
                                            *   The association is not made atomically. */
    OBJC_ASSOCIATION_RETAIN = 01401,       /**< Specifies a strong reference to the associated object.
                                            *   The association is made atomically. */
    OBJC_ASSOCIATION_COPY = 01403          /**< Specifies that the associated object is copied.
                                            *   The association is made atomically. */
};

要取出被關聯(lián)的對象使用 objc_getAssociatedObject 方法即可,要刪除一個被關聯(lián)的對象,使用 objc_setAssociatedObject 方法將對應的 key 設置成 nil 即可:

objc_setAssociatedObject(self, associatedKey, nil, OBJC_ASSOCIATION_COPY_NONATOMIC);

objc_removeAssociatedObjects 方法將會移除源對象中所有的關聯(lián)對象.
舉個栗子,假如我們要給 UIButton 添加一個監(jiān)聽單擊事件的 block 屬性,新建 UIButton 的 Category,其.m文件如下:

#import "UIButton+ClickBlock.h"
#import <objc/runtime.h>

static const void *associatedKey = "associatedKey";

@implementation UIButton (ClickBlock)

//Category中的屬性,只會生成setter和getter方法,不會生成成員變量

-(void)setClick:(clickBlock)click{
    objc_setAssociatedObject(self, associatedKey, click, OBJC_ASSOCIATION_COPY_NONATOMIC);
    [self removeTarget:self action:@selector(buttonClick) forControlEvents:UIControlEventTouchUpInside];
    if (click) {
        [self addTarget:self action:@selector(buttonClick) forControlEvents:UIControlEventTouchUpInside];
    }
}

-(clickBlock)click{
    return objc_getAssociatedObject(self, associatedKey);
}

-(void)buttonClick{
    if (self.click) {
        self.click();
    }
}

@end

然后在代碼中,就可以使用 UIButton 的屬性來監(jiān)聽單擊事件了:

    UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
    button.frame = self.view.bounds;
    [self.view addSubview:button];
    button.click = ^{
        NSLog(@"buttonClicked");
    };

完整的對象關聯(lián)代碼點這里

8、?自動歸檔
博主在學習 Runtime 之前,歸檔的時候是醬紫寫的:

- (void)encodeWithCoder:(NSCoder *)aCoder{
    [aCoder encodeObject:self.name forKey:@"name"];
    [aCoder encodeObject:self.ID forKey:@"ID"];
}

- (id)initWithCoder:(NSCoder *)aDecoder{
    if (self = [super init]) {
        self.ID = [aDecoder decodeObjectForKey:@"ID"];
        self.name = [aDecoder decodeObjectForKey:@"name"];
    }
    return self;
}

那么問題來了,如果當前 Model 有100個屬性的話,就需要寫100行這種代碼:

[aCoder encodeObject:self.name forKey:@"name"];

想想都頭疼,通過 Runtime 我們就可以輕松解決這個問題:
?1.使用 class_copyIvarList 方法獲取當前 Model 的所有成員變量.
2.使用 ivar_getName 方法獲取成員變量的名稱.
3.通過 KVC 來讀取 Model 的屬性值(encodeWithCoder:),以及給 Model 的屬性賦值(initWithCoder:).

舉個栗子,新建一個 Model 類,其.m文件如下:

#import "TestModel.h"
#import <objc/runtime.h>
#import <objc/message.h>

@implementation TestModel
- (void)encodeWithCoder:(NSCoder *)aCoder{
    unsigned int outCount = 0;
    Ivar *vars = class_copyIvarList([self class], &outCount);
    for (int i = 0; i < outCount; i ++) {
        Ivar var = vars[i];
        const char *name = ivar_getName(var);
        NSString *key = [NSString stringWithUTF8String:name];
        
        // 注意kvc的特性是,如果能找到key這個屬性的setter方法,則調(diào)用setter方法
        // 如果找不到setter方法,則查找成員變量key或者成員變量_key,并且為其賦值
        // 所以這里不需要再另外處理成員變量名稱的“_”前綴
        id value = [self valueForKey:key];
        [aCoder encodeObject:value forKey:key];
    }
}

- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder{
    if (self = [super init]) {
        unsigned int outCount = 0;
        Ivar *vars = class_copyIvarList([self class], &outCount);
        for (int i = 0; i < outCount; i ++) {
            Ivar var = vars[i];
            const char *name = ivar_getName(var);
            NSString *key = [NSString stringWithUTF8String:name];
            id value = [aDecoder decodeObjectForKey:key];
            [self setValue:value forKey:key];
        }
    }
    return self;
}
@end

完整的自動歸檔代碼在這里

9、字典與模型互轉(zhuǎn)
最開始博主是這樣用字典給 Model 賦值的:

-(instancetype)initWithDictionary:(NSDictionary *)dict{
    if (self = [super init]) {
        self.age = dict[@"age"];
        self.name = dict[@"name"];
    }
    return self;
}

可想而知,遇到的問題跟歸檔時候一樣(后來使用MJExtension),這里我們稍微來學習一下其中原理,字典轉(zhuǎn)模型的時候:
1.?根據(jù)字典的 key 生成 setter 方法.
2.使用 objc_msgSend 調(diào)用 setter 方法為 Model 的屬性賦值(或者 KVC).

模型轉(zhuǎn)字典的時候:
?1.調(diào)用 class_copyPropertyList 方法獲取當前 Model 的所有屬性.
2.調(diào)用 property_getName 獲取屬性名稱.
3.根據(jù)屬性名稱生成 getter 方法.
4.使用 objc_msgSend 調(diào)用 getter 方法獲取屬性值(或者 KVC).

代碼如下:

#import "NSObject+KeyValues.h"
#import <objc/runtime.h>
#import <objc/message.h>

@implementation NSObject (KeyValues)

//字典轉(zhuǎn)模型
+(id)objectWithKeyValues:(NSDictionary *)aDictionary{
    id objc = [[self alloc] init];
    for (NSString *key in aDictionary.allKeys) {
        id value = aDictionary[key];
        
        /*判斷當前屬性是不是Model*/
        objc_property_t property = class_getProperty(self, key.UTF8String);
        unsigned int outCount = 0;
        objc_property_attribute_t *attributeList = property_copyAttributeList(property, &outCount);
        objc_property_attribute_t attribute = attributeList[0];
        NSString *typeString = [NSString stringWithUTF8String:attribute.value];
        if ([typeString isEqualToString:@"@\"TestModel\""]) {
            value = [self objectWithKeyValues:value];
        }
        /**********************/
        
        //生成setter方法,并用objc_msgSend調(diào)用
        NSString *methodName = [NSString stringWithFormat:@"set%@%@:",[key substringToIndex:1].uppercaseString,[key substringFromIndex:1]];
        SEL setter = sel_registerName(methodName.UTF8String);
        if ([objc respondsToSelector:setter]) {
            ((void (*) (id,SEL,id)) objc_msgSend) (objc,setter,value);
        }
    }
    return objc;
}

//模型轉(zhuǎn)字典
-(NSDictionary *)keyValuesWithObject{
    unsigned int outCount = 0;
    objc_property_t *propertyList = class_copyPropertyList([self class], &outCount);
    NSMutableDictionary *dict = [NSMutableDictionary dictionary];
    for (int i = 0; i < outCount; i ++) {
        objc_property_t property = propertyList[i];
        
        //生成getter方法,并用objc_msgSend調(diào)用
        const char *propertyName = property_getName(property);
        SEL getter = sel_registerName(propertyName);
        if ([self respondsToSelector:getter]) {
            id value = ((id (*) (id,SEL)) objc_msgSend) (self,getter);
            
            /*判斷當前屬性是不是Model*/
            if ([value isKindOfClass:[self class]] && value) {
                value = [value keyValuesWithObject];
            }
            /**********************/
            
            if (value) {
                NSString *key = [NSString stringWithUTF8String:propertyName];
                [dict setObject:value forKey:key];
            }
        }
        
    }
    return dict;
}
@end

完整代碼在這里

10、?動態(tài)方法解析
前面我們留下了一點東西沒說,那就是如果某個對象調(diào)用了不存在的方法時會怎么樣,一般情況下程序會crash,錯誤信息類似下面這樣:

unrecognized selector sent to instance 0x7fd0a141afd0

但是在程序crash之前,Runtime 會給我們動態(tài)方法解析的機會,消息發(fā)送的步驟大致如下:

1.檢測這個 selector 是不是要忽略的。比如 Mac OS X 開發(fā),有了垃圾回收就不理會 retain,release 這些函數(shù)了.

2.檢測這個 target 是不是 nil 對象。ObjC 的特性是允許對一個 nil 對象執(zhí)行任何一個方法不會 Crash,因為會被忽略掉.

3.如果上面兩個都過了,那就開始查找這個類的 IMP,先從 cache 里面找,完了找得到就跳到對應的函數(shù)去執(zhí)行.
如果 cache 找不到就找一下方法分發(fā)表.

4.如果分發(fā)表找不到就到超類的分發(fā)表去找,一直找,直到找到NSObject類為止.

如果還找不到就要開始進入消息轉(zhuǎn)發(fā)了,消息轉(zhuǎn)發(fā)的大致過程如圖:
這里寫圖片描述

1.進入 resolveInstanceMethod: 方法,指定是否動態(tài)添加方法。若返回NO,則進入下一步,若返回YES,則通過 class_addMethod 函數(shù)動態(tài)地添加方法,消息得到處理,此流程完畢.

2.resolveInstanceMethod: 方法返回 NO 時,就會進入 forwardingTargetForSelector: 方法,這是 Runtime 給我們的第二次機會,用于指定哪個對象響應這個 selector。返回nil,進入下一步,返回某個對象,則會調(diào)用該對象的方法.

3.若 forwardingTargetForSelector: 返回的是nil,則我們首先要通過 methodSignatureForSelector: 來指定方法簽名,返回nil,表示不處理,若返回方法簽名,則會進入下一步.

4當?shù)?methodSignatureForSelector: 方法返回方法簽名后,就會調(diào)用 forwardInvocation: 方法,我們可以通過 anInvocation 對象做很多處理,比如修改實現(xiàn)方法,修改響應對象等.

如果到最后,消息還是沒有得到響應,程序就會crash,詳細代碼在這里。


Objective-C Runtime

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

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

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