你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> Runtime全方位裝逼指南

Runtime全方位裝逼指南

編輯:IOS開發基礎

Time-Photo.jpg

本文授權轉載,作者:戴尼瑪(簡書)

楔子

Runtime是什麼?見名知意,其概念無非就是“因為 Objective-C 是一門動態語言,所以它需要一個運行時系統……這就是 Runtime 系統”雲雲。對博主這種菜鳥而言,Runtime 在實際開發中,其實就是一組C語言的函數。胡適說:“多研究些問題,少談些主義”,雲山霧罩的概念聽多了總是容易頭暈,接下來我們直接從代碼入手學習 Runtime。

1、由objc_msgSend說開去

Objective-C 中的方法調用,不是簡單的方法調用,而是發送消息,也就是說,其實 [receiver message] 會被編譯器轉化為: 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 方法已經被編譯器轉化為下面這樣:

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 函數被定義在 objc/message.h 目錄下,其函數原型是醬紫滴:

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

該函數有兩個參數,一個 id 類型,一個 SEL 類型。

2、SEL

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

typedef struct objc_selector *SEL;

其實它就是個映射到方法的C字符串,你可以用 Objective-C 編譯器命令 @selector() 或者 Runtime 系統的 sel_registerName 函數來獲得一個 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:指向該類的實例方法列表,它將方法選擇器和方法實現地址聯系起來。methodLists 是指向 ·objc_method_list 指針的指針,也就是說可以動態修改 *methodLists 的值來添加成員方法,這也是 Category 實現的原理,同樣解釋了 Category 不能添加屬性的原因。

  • ·cache:Runtime 系統會把被調用的方法存到 cache 中(理論上講一個方法如果被調用,那麼它有可能今後還會被調用),下次查找的時候效率更高。

  • ·protocols:指向該類的協議列表。

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

首先,Runtime 系統會把方法調用轉化為消息發送,即 objc_msgSend,並且把方法的調用者,和方法選擇器,當做參數傳遞過去.

此時,方法的調用者會通過 isa 指針來找到其所屬的類,然後在 cache 或者 methodLists 中查找該方法,找得到就跳到對應的方法去執行。

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

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

1396900-1b94ff9a3905ba68.jpg

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

5、使用objc_msgSend

前面我們使用 clang 重寫命令,看到 Runtime 是如何將方法調用轉化為消息發送的。我們也可以依樣畫葫蘆,來學習使用一下 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 依次調用這些方法:

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);

也許你已經注意到,objc_msgSend 在使用時都被強制轉換了一下,這是因為 objc_msgSend 函數可以hold住各種不同的返回值以及多個參數,但默認情況下是沒有參數和返回值的。如果我們把調用 showAge 方法改成這樣:

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

Xcode 就會報錯:

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

完整的 objc_msgSend 使用代碼在這裡。

6、objc_msgSendSuper

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

這裡我們重點說一下 objc_msgSendSuper,objc_msgSendSuper 函數原型如下:

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

當我們調用 [super selector] 時,Runtime 會調用 objc_msgSendSuper 方法,objc_msgSendSuper 方法有兩個參數,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 會創建一個 objc_spuer 結構體變量,將其地址作為參數(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 重寫命令,發現上述代碼被轉化為:

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"))));

當調用 [super class] 時,會轉換成 objc_msgSendSuper 函數:

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

  • 第二步是去 Father 這個類裡去找 - (Class)class,沒有,然後去 NSObject 類去找,找到了。最後內部是使用 objc_msgSend(objc_super->receiver, @selector(class)) 去調用,此時已經和 [self class] 調用相同了,所以兩個輸出結果都是 Son。

7、對象關聯

對象關聯允許開發者對已經存在的類在 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 是被關聯的對象

·key 是關聯的鍵,objc_getAssociatedObject 方法通過不同的 key 即可取出對應的被關聯對象

·policy 是一個枚舉值,表示關聯對象的行為,從命名就能看出各個枚舉值的含義:

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. */
};

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

objc_setAssociatedObject(self, associatedKey, nil, OBJC_ASSOCIATION_COPY_NONATOMIC);

objc_removeAssociatedObjects 方法將會移除源對象中所有的關聯對象.

舉個栗子,假如我們要給 UIButton 添加一個監聽單擊事件的 block 屬性,新建 UIButton 的 Category,其.m文件如下:

#import "UIButton+ClickBlock.h"
#import 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 的屬性來監聽單擊事件了:

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

完整的對象關聯代碼點這裡。

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 #import @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方法,則調用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、字典與模型互轉

最開始博主是這樣用字典給 Model 賦值的:

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

可想而知,遇到的問題跟歸檔時候一樣(後來使用MJExtension),這裡我們稍微來學習一下其中原理,字典轉模型的時候:

1.根據字典的 key 生成 setter 方法

2.使用 objc_msgSend 調用 setter 方法為 Model 的屬性賦值(或者 KVC)

模型轉字典的時候:

1.調用 class_copyPropertyList 方法獲取當前 Model 的所有屬性

2.調用 property_getName 獲取屬性名稱

3.根據屬性名稱生成 getter 方法

4.使用 objc_msgSend 調用 getter 方法獲取屬性值(或者 KVC)

代碼如下:

#import "NSObject+KeyValues.h"
#import #import @implementation NSObject (KeyValues)
//字典轉模型
+(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調用
        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;
}
//模型轉字典
-(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調用
        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、動態方法解析

前面我們留下了一點東西沒說,那就是如果某個對象調用了不存在的方法時會怎麼樣,一般情況下程序會crash,錯誤信息類似下面這樣:

unrecognized selector sent to instance 0x7fd0a141afd0

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

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

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

3.如果上面兩個都過了,那就開始查找這個類的 IMP,先從 cache 裡面找,完了找得到就跳到對應的函數去執行

如果 cache 找不到就找一下方法分發表

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

如果還找不到就要開始進入消息轉發了,消息轉發的大致過程如圖:

1396900-19c5eb4913f49e33.jpg

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

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

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

4.當第 methodSignatureForSelector: 方法返回方法簽名後,就會調用 forwardInvocation: 方法,我們可以通過 anInvocation 對象做很多處理,比如修改實現方法,修改響應對象等。

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

參考文章:

Objective-C Runtime

標哥的技術博客 Runtime系列文章

刨根問底Objective-C Runtime(1)- Self & Super

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved