本文授權轉載,作者:戴尼瑪(簡書)
楔子
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 指針即可找到其所屬的元類。
上圖實線是 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類為止
如果還找不到就要開始進入消息轉發了,消息轉發的大致過程如圖:
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