Runtime是什麼?見名知意,其概念無非就是“因為 Objective-C 是一門動態語言,所以它需要一個運行時系統……這就是 Runtime 系統”云云。對博主這種菜鳥而言,Runtime 在實際開發中,其實就是一組C語言的函數。胡適說:“多研究些問題,少談些主義”,雲山霧罩的概念聽多了總是容易 ...
Runtime是什麼?見名知意,其概念無非就是“因為 Objective-C 是一門動態語言,所以它需要一個運行時系統……這就是 Runtime 系統”云云。對博主這種菜鳥而言,Runtime 在實際開發中,其實就是一組C語言的函數。胡適說:“多研究些問題,少談些主義”,雲山霧罩的概念聽多了總是容易頭暈,接下來我們直接從代碼入手學習 Runtime。
1、由objc_msgSend說開去
Objective-C 中的方法調用,不是簡單的方法調用,而是發送消息,也就是說,其實 [receiver message] 會被編譯器轉化為: objc_msgSend(receiver, selector),何以證明?新建一個類 MyClass,其.m文件如下:
1 2 3 4 5 6 7 8 9 10 11 |
#import "MyClass.h"
@implementation MyClass
-(instancetype)init{
if (self = [ super init]) {
[self showUserName];
}
return self;
}
-(void)showUserName{
NSLog(@ "Dave Ping" );
}
|
使用 clang 重寫命令:
1 |
$ clang -rewrite-objc MyClass.m
|
然後在同一目錄下會多出一個 MyClass.cpp 文件,雙擊打開,可以看到 init 方法已經被編譯器轉化為下麵這樣:
1 2 3 4 5 6 |
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;
}
|
我們要找的就是它:
1 |
((void (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName( "showUserName" ))
|
objc_msgSend 函數被定義在 objc/message.h 目錄下,其函數原型是醬紫滴:
1 |
OBJC_EXPORT void objc_msgSend(void /* id self, SEL op, ... */ )
|
該函數有兩個參數,一個 id 類型,一個 SEL 類型。
2、SEL
SEL 被定義在 objc/objc.h 目錄下:
1 |
typedef struct objc_selector *SEL;
|
其實它就是個映射到方法的C字元串,你可以用 Objective-C 編譯器命令 @selector() 或者 Runtime 系統的 sel_registerName 函數來獲得一個 SEL 類型的方法選擇器。
3、id
與 SEL 一樣,id 也被定義在 objc/objc.h 目錄下:
1 |
typedef struct objc_object *id;
|
id 是一個結構體指針類型,它可以指向 Objective-C 中的任何對象。objc_object 結構體定義如下:
1 |
struct objc_object { Class isa OBJC_ISA_AVAILABILITY;};
|
我們通常所說的對象,就長這個樣子,這個結構體只有一個成員變數 isa,對象可以通過 isa 指針找到其所屬的類。isa 是一個 Class 類型的成員變數,那麼 Class 又是什麼呢?
4、Class
Class 也是一個結構體指針類型:
1 |
typedef struct objc_class *Class;
|
objc_class 結構體是醬紫滴:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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,添加如下方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
-(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 依次調用這些方法:
1 2 3 4 5 6 7 8 |
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 方法改成這樣:
1 |
objc_msgSend(objct, sel_registerName( "showAge" ));
|
Xcode 就會報錯:
1 |
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 函數原型如下:
1 |
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 結構體定義如下:
1 2 3 4 5 6 7 8 9 10 11 12 |
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。
舉個慄子,問下麵的代碼輸出什麼:
1 2 3 4 5 6 7 8 9 10 11 12 |
@implementation Son : Father
- (id)init
{
self = [ super init];
if (self)
{
NSLog(@ "%@" , NSStringFromClass([self class]));
NSLog(@ "%@" , NSStringFromClass([ super class]));
}
return self;
}
@end
|
答案是全部輸出 Son。
使用 clang 重寫命令,發現上述代碼被轉化為:
1 2 |
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 中添加自定義的屬性:
1 |
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 是一個枚舉值,表示關聯對象的行為,從命名就能看出各個枚舉值的含義:
1 2 3 4 5 6 7 8 9 10 11 |
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 即可:
1 |
objc_setAssociatedObject(self, associatedKey, nil, OBJC_ASSOCIATION_COPY_NONATOMIC);
|
objc_removeAssociatedObjects 方法將會移除源對象中所有的關聯對象.
舉個慄子,假如我們要給 UIButton 添加一個監聽單擊事件的 block 屬性,新建 UIButton 的 Category,其.m文件如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#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 的屬性來監聽單擊事件了:
1 2 3 4 5 6 |
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
button.frame = self.view.bounds;
[self.view addSubview:button];
button.click = ^{
NSLog(@ "buttonClicked" );
};
|
完整的對象關聯代碼點這裡。
8、自動歸檔
博主在學習 Runtime 之前,歸檔的時候是醬紫寫的:
1 2 3 4 5 6 7 8 9 10 11 |
- (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行這種代碼:
1 |
[aCoder encodeObject:self.name forKey:@ "name" ];
|
想想都頭疼,通過 Runtime 我們就可以輕鬆解決這個問題:
1.使用 class_copyIvarList 方法獲取當前 Model 的所有成員變數.
2.使用 ivar_getName 方法獲取成員變數的名稱.
3.通過 KVC 來讀取 Model 的屬性值(encodeWithCoder:),以及給 Model 的屬性賦值(initWithCoder:).
舉個慄子,新建一個 Model 類,其.m文件如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
#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 賦值的:
1 2 3 4 5 6 7 |
-(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)
代碼如下: