Runtime 前言 從字面意思看,就是運行時。但是這個運行時究竟什麼意思?可以把它理解成:不是在編譯期也不是在鏈接期,而是在運行時。那究竟在運行期間做了什麼呢?按照蘋果官方的說法,就是把一些決策(方法的調用,類的添加等)推遲,推遲到運行期間。只要有可能,程式就可以動態的完成任務,而不是我們在編譯期 ...
Runtime
前言
從字面意思看,就是運行時。但是這個運行時究竟什麼意思?可以把它理解成:不是在編譯期也不是在鏈接期,而是在運行時。那究竟在運行期間做了什麼呢?按照蘋果官方的說法,就是把一些決策(方法的調用,類的添加等)推遲,推遲到運行期間。只要有可能,程式就可以動態的完成任務,而不是我們在編譯期已經決定它要完成什麼任務。這就意味了OC不僅僅需要編譯器,還需要一個運行時的系統來支撐。
目錄
接下來就對Runtime做一個系統的介紹,主要內容包括:
- 簡介
- 涉及到的數據結構
- runtime.h解析
- 如何可以觸及到RunTime?
- 消息
- 動態消息解析
- 消息轉發
- Runtime的使用場景
1.簡介
根據前言,你已經瞭解了Runtime大概是個什麼鬼,在OC發展歷程中,它主要有兩個版本:Legacy和Modern。Legacy版本採用的是OC1.0版本;Modern版本採用的OC2.0版本,而且相比Legacy也添加了一些新特性。最明顯的區別在於:
- 在legacy版本,如果你改變了類的佈局,那麼你必須重新編譯繼承自它的類。
- 在modern版本,如果你改變了類的佈局,你不必重新編輯繼承自它的類。
平臺
iPhone的應用程式以及OS X v10.5版本的64位機器使用的是modern版本的runtime。
其他(OS X桌面應用32位程式)使用的是legacy版本的runtime。
2.涉及到的數據結構
這裡主要介紹一下在runtime.h裡面涉及到的一些數據結構。
Ivar
Ivar從字面意思來講,它就是代表的實例變數,它也是一個結構體指針,包含了變數的名稱、類型、偏移量以及所占空間。
SEL
選擇器,每個方法都有自己的選擇器,其實就是方法的名字,但是不僅僅是方法的名字,在objc.h中,我們可以看到它的定義:
/// An opaque type that represents a method selector.一個不透明類型,用來代表一個方法選擇器
typedef struct objc_selector *SEL;
由定義可知它是一個objc_selector的結構體指針,尷尬的是在runtime源碼中並沒有找到該結構體。猜想它內部應該就是一個char 的字元串。
你可以使用:
NSLog(@"%s",@selector(description)); //%s用來輸出一個字元串
列印出來description。
在這裡你可以把它理解成一個選擇器,可以標識某個方法。
IMP
它是一個函數指針,指向方法的實現,在objc.h裡面它的定義是這樣的:
/// A pointer to the function of a method implementation.
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ );
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...);
#endif
id
id是一個我們經常使用的類型,可用於作為類型轉換的中介者。它類似於Java裡面的Object,可以轉換為任何的數據類型。它在objc.h裡面是這樣定義的:
/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
/// A pointer to an instance of a class.
typedef struct objc_object *id;
它其實是一個objc _ object的結構體指針,而在後面將要提到的Class其實是個objc _ class的指針,而objc _ class是繼承自objc _o bject的,因此可以相互轉換,這也是為什麼id可以轉換為其他任何的數據類型的原因。
Method
方法,它其實是一個objc_method的結構體指針,其定義如下:
/// An opaque type that represents a method in a class definition.
typedef struct objc_method *Method;
struct objc_method {
SEL _Nonnull method_name OBJC2_UNAVAILABLE;
char * _Nullable method_types OBJC2_UNAVAILABLE;
IMP _Nonnull method_imp OBJC2_UNAVAILABLE;
}
這個就比較好理解了,該結構體包含了方法的名稱(SEL),方法的類型以及方法的IMP。
Class
它是一個objc_class的結構體指針,在runtime.h中的定義如下:
/// An opaque type that represents an Objective-C class.一個不透明類型,代表OC的類
typedef struct objc_class *Class;
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
該結構體中各部分介紹如下:
- isa:是一個Class類型的指針,每個對象的實例都有isa指針,他指向對象的類。而Class裡面也有個isa指針,它指向meteClass(元類),元類保存了類方法的列表。
- name:對象的名字
- version:類的版本號,必須是0
- info:供運行期間使用的位標識
- instance_size:該類的實例大小
- ivars:成員變數數組,包含了該類包含的成員變數
- methodLists:包含方法的數組列表,也是一個結構體,該結構體裡面還包含了一個obsolete的指針,表示廢棄的方法的列表
- cache:緩存。這個比較複雜,在後面會提到,這裡先忽略。
- protocols:協議列表,也是一個數組
而在objc-runtime-new.h中,你會發現這樣的定義(在runtime中並沒有完全暴露objc_class的實現):
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
class_rw_t *data() {
return bits.data();
}
//其他的省略
其實objc _ class繼承自objc _ object。所以這也說明瞭為什麼id能夠轉換為其他的類型。
3.runtime.h解析
我們先看一下在usr/include/objc/runtime.h,這個是任何一個工程都可以直接找到的,它是SDK的一部分。主要定義了以下內容:
- 定義了一些類型,例如Method/Ivar/Category等,還有一些結構體。
- 函數。函數裡面有分了幾大類:
- 關於對象實例的方法,例如object _ getClass、object _ setClass以及object _ getIvar等。這些函數大多以object開頭。 用來獲取屬性或者對對象進行操作。
- 獲得類定義的方法,例如objc _ getClass/objc _ getMetaClass等,這些方法更多的是獲取Class或者在Class級別上進行操作。 多以objc開頭
- 和類相關的方法。例如class _ getName/class _ isMetaClass等,這些更多的是獲取Class的一些屬性。比如該類的屬性列表、方法列表、協議列表等。傳參大多為Class。 多以class開頭
- 實例化類的一些方法。例如class _ createInstance方法,就是相當於平時的alloc init。
- 添加類的方法。例如你可以使用這些方法冬天的註冊一個類。使用objc _ allocateClassPair創建一個新類,使用 objc _ registerClassPair對類進行註冊
- 等等。。。
- 關於對象實例的方法,例如object _ getClass、object _ setClass以及object _ getIvar等。這些函數大多以object開頭。 用來獲取屬性或者對對象進行操作。
- 另外就是一些廢棄的方法和類型。
4. 如何可以觸及到RunTime?
有三種不同的方式可以讓OC編程和runtime系統交互。
OC源代碼
大多數情況下,我們寫的OC代碼,其實它底層的實現就是runtime。runtime系統在背後自動幫我們處理了操作。例如我們編譯一個類,編譯器器會創建一個結構體,然後這個結構體會從類中捕獲信息,包括方法、屬性、Protocol等。
NSObject的一些方法
在Foundation框架裡面有個NSObject.h,在usr/include/objc裡面也有一個NSObject.h。而我們平時用到的類的基類是/usr/include/objc裡面的這個NSObject.h,Foundation裡面的NSObject.h只是NSObject的一個Category。所以這裡我們更關註一下/usr/include/objc裡面的NSObject.h。
由於大多數對象都是NSObject的子類,所以在NSObject.h裡面定義的方法都可以使用。
在這些方法裡面,有一些方法能夠查詢runtime系統的信息,例如:
- (BOOL)isKindOfClass:(Class)aClass; //用來檢測一個對象是否是某各類的實例對象,aClass也有可能是父類,同樣可以檢測出來。
- (BOOL)isMemberOfClass:(Class)aClass; //而該方法只能檢測一個對象是否是某各類的實例對象。但如果aClass不能為該類的父類,如果是父類則該方法返回NO
- (BOOL)respondsToSelector:(SEL)aSelector;
- (BOOL)conformsToProtocol:(Protocol *)aProtocol;
- (IMP)methodForSelector:(SEL)aSelector;
這裡用代碼對isKindOfClass和isMemberOfClass做個簡單介紹:
//stu是Student的實例對象,Student的父類為Person,Person的父類為NSObject。
[stu isKindOfClass:[Student class]]; //YES
[stu isKindOfClass:[Person class]]; //YES
[stu isKindOfClass:[NSObject class]]; //YES
[stu isMemberOfClass:[Student class]]; //YES
[stu isMemberOfClass:[Person class]]; //NO
[stu isMemberOfClass:[NSObject class]]; //NO
我們可以在objc源代碼中的NSObject.mm中看到相應的實現:
+ (BOOL)isMemberOfClass:(Class)cls {
return object_getClass((id)self) == cls;
}
- (BOOL)isMemberOfClass:(Class)cls {
return [self class] == cls;
}
+ (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}
- (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}
從具體實現可知,為什麼isKindOfClass能夠檢測出superclass。另外,在NSObject.h中,並沒有看到兩個方法的類方法聲明,但是在實現裡面卻包含了類方法的實現。這裡有個疑問:為什麼沒有對外聲明的兩個類方法依然可以在外部調用呢?(比如我可以直接使用[Student isMemberOfClass:[NSObject class]])。
這裡還用到了class方法,這個方法聲明如下:
+ (Class)class OBJC_SWIFT_UNAVAILABLE("use 'aClass.self' instead");
- (Class)class OBJC_SWIFT_UNAVAILABLE("use 'type(of: anObject)' instead");
+ (Class)class { //返回當前的self
return self;
}
- (Class)class {
return object_getClass(self);
}
這裡重要的是理解self究竟代表著什麼:
- 當self為實例對象的時候,[self class] 和 object_getClass(self)是等價的。object_getClass([self class])得到的是元類。
- 當self為類對象的時候,[self class]返回的是自身,還是self。object_getClass(self) 與object_getClass([self class])等價。拿到的是元類。
Runtime函數
runtime系統其實就是一個動態共用的Library,它是由在/usr/include/objc目錄的公共介面中的函數和數據結構組成。
5. 消息
在Objective-C中,消息直到運行時才將其與消息的實現綁定,編譯器會將
[receiver message];
轉換成
objc_msgSend(receiver,selector); //1
objc_msgSend(receiver,selector,arg1,arg2,...); //2
如果包含參數,那麼就會執行2方法。其實除了該方法,還有以下幾個方法:
objc_msgSend_stret
objc_msgSendSuper
objc_msgSendSuper_stret
當想一個對象的父類發送message時,會使用
objc_msgSendSuper
如果方法的返回值是一個結構體,那麼就會使用
objc_msgSend_stret
objc_msgSendSuper_stret
這裡我們可以打開objc源碼,然後你會發現裡面有多個.s文件:
這裡之所以有objc-msg-類的不同文件,我猜想應該是對不同的CPU指令集(指令不一樣)做了分別處理。因為這些.s文件名稱中包含的是不同的arm指令集。而且打開.s文件你會發現裡面的實現是彙編語言,所以蘋果為了效率還是蠻拼的,直接用彙編語言實現。
其中就能找到objc _ msgSend的實現(objc-msg-i386.s中):
雖然對彙編瞭解不是太多,但是這個文件中的註釋很詳細,從註釋可以看出objc_msgSend方法的執行過程:
- 先載入receiver和selector到寄存器,然後判斷receiver是否為空,如果為空,則函數執行結束;
- 如果receiver不為空,開始搜索緩存,查看方法緩存列表裡面是否有改selector,如果有則執行;
- 如果沒有緩存,則搜索方法列表,如果在方法列表中找到,則跳轉到具體的imp實現。沒有則執行結束。
使用了隱藏參數
在發送一個消息的時候,會被編譯成objc_msgSend,此時該消息的參數將會傳入objc_msgSend方法裡面。除此之外,還會包含兩個隱藏的參數:
- receiver
- method的selector
這兩個參數在上面也有提到。其中的receiver就是消息的發送方,而selector就是選擇器,也可以直接用 _ cmd來指代( _ cmd用來代表當前所在方法的SEL)。之所以隱蔽是因為在方法聲明中並沒有被明確聲明,在源代碼中我們仍然可以引用它們。
獲取方法地址
我們每次發送消息都會走objc_msgSend()方法,那麼有沒有辦法避開消息綁定直接獲取方法的地址並調用方法呢?答案當然是有的。我們上面簡單介紹了IMP,其實我們可以使用NSObject的
- (IMP)methodForSelector:(SEL)aSelector;
方法,通過該方法獲得IMP,然後調用該方法。但是避開消息綁定而直接調用的使用並不常見,但是如果你要多次迴圈調用的話,直接獲取方法地址並調用不失為一個省時操作。看下麵的代碼:
void (*setter)(id,SEL,BOOL);
setter = (void(*)(id,SEL,BOOL))[stu2 methodForSelector:@selector(learning)];
NSDate *startDate = [NSDate date];
for (int i = 0;i<100000;i++) {
setter(stu2,@selector(learning),YES);
}
double deltaTime = [[NSDate date] timeIntervalSinceDate:startDate];
NSLog(@"----%f",deltaTime);
NSDate *startDate1 = [NSDate date];
for (int i = 0;i<100000;i++) {
[stu2 learning];
}
double deltaTime1 = [[NSDate date] timeIntervalSinceDate:startDate1];
NSLog(@"----%f",deltaTime1);
你可以自行跑一下,看一下時間差異。你會發現:獲取方法地址直接調用更省時間,但請註意使用場景。
6. 動態消息解析
這裡介紹一下如果動態地提供方法的實現。
動態方法解析
在開發過程中,你可能想動態地提供一個方法的實現。比如我們對一個對象聲明瞭一個屬性,然後我們使用了 @dynamic 標識符:
@dynamic propertyName;
該標識符的目的就是告訴編譯器:和這個屬性相關的getter和setter方法會動態地提供(當然你也可以直接手動在代碼裡面實現)。這個時候你就會用到NSObject.h裡面的兩個方法
+ (BOOL)resolveClassMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
+ (BOOL)resolveInstanceMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
來提供方法的實現。
其實OC方法就是一個簡單的C函數,它至少包含了兩個參數self和 _ cmd,你可以自己聲明一個方法:
void dynamicMethodIMP(id self, SEL _cmd) {
//這裡是方法的具體實現
}
此時我們可以在聲明屬性的類中實現上面提到的兩個方法(一個是解析類方法,一個是解析實例方法),例如我在Person裡面這樣寫:
@dynamic address; //也就意味著我們需要手動/動態實現該屬性的getter和setter方法。
你會發現當我們運行下麵的代碼時,程式會crash:
Person *zhangsan = [[Person alloc] init];
zhangsan.address = @"he nan xinxiang ";
NSLog(@"%@",zhangsan.address);
// crash reason
// -[Person setAddress:]: unrecognized selector sent to instance 0x1d4449630
這裡簡單的做一個動態方法解析:
void setter(id self,SEL _cmd) {
NSLog(@"set address");
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSString *selStr = NSStringFromSelector(sel);
if ([selStr hasPrefix:@"set"]) {
class_addMethod([self class], sel, (IMP)setter, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
所以我們需要自己去實現setAddress: 方法。(這裡判斷用hasPrefix不太準確,開發者可以自行根據需求調整)。轉發消息(下麵會講到)和動態解析是正交的。也就是說一個class有機會再消息轉發機制前去動態解析此方法,也可以將動態解析方法返回NO,然後將操作轉發給消息轉發。
動態載入
OC編程也允許我們在程式運行的時候動態去創建和鏈接一個類或者分類。這些創建的類或者分類將會和運行app前創建的類一樣,沒有差別。
動態載入在開發的過程中可以做好多事情,例如系統設置中的不同模塊就是動態載入的。
在Cocoa環境中,最經典的就是Xcode,它可以安裝不同的插件,這個也是動態載入的方式實現的。
7. 消息轉發
發送一個消息給對象,如果對象不能處理,那麼就會產生錯誤。然而,在產生錯誤之前,runtime 系統會給對象第二次機會去處理該消息。這裡詳細已經在深入淺出理解消息的傳遞和轉發文章中做了介紹,這裡就不再介紹了。
8. Runtime的使用場景
Runtime的使用幾乎無處不在,OC本身就是一門運行時語言,Class的生成、方法的調用等等,都是Runtime。另外,我們可以用Runtime做一些其他的事情。
字典轉換Model
平時我們從服務端拿到的數據是json字元串,我們可以將其轉換成成NSDictionary,然後通過runtime中的一些方法做一個轉換:
先拿到model的所有屬性或者成員變數,然後將其和字典中的key做映射,然後通過KVC對屬性賦值即可。更多可參見class_copyIvarList方法獲取實例變數問題引發的思考中的例子。
熱更新(JSPatch的實現)
JSPatch能做到JS調用和改寫OC方法的根本原因就是OC是動態語言,OC上的所有方法的調用/類的生成都通過OC Runtime在運行時進行,我們可以根據名稱/方法名反射得到相應的類和方法。例如
Class class = NSClassFromString("UIViewController");
id viewController = [[class alloc] init];
SEL selector = NSSelectorFromString("viewDidLoad");
[viewController performSelector:selector];
也正是鑒於此,才實現了熱更新。
給Category添加屬性
我們可以使用runtime在Category中給類添加屬性,這個主要使用了兩個runtime鐘的方法:
OBJC_EXPORT void
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,id _Nullable value, objc_AssociationPolicy policy);
OBJC_EXPORT id _Nullable
objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key);
具體使用可參見:給分類(Category)添加屬性。
Method Swizzling
它是改變一個已存在的selector的實現的技術,比如你想將viewDidload方法替換為我們自定義的方法,給系統的方法添加一些需要的功能,來實現某些需求。比如你想跟蹤每個ViewController展示的次數,你可以使用該技術重寫ViewDidAppear方法,然後做一些自己的處理。可以參見Method Swizzling裡面的講解。
總結
Objective-c本身就是一門冬天語言,所以瞭解runtime有助於我們更加深入地瞭解其內部的實現原理。也會把一些看似很難的問題通過runtime很快解決。
參考鏈接:
1.Objective-C Runtime Programming Guide
2.Objective-C Runtime
3.objc4
4.深入淺出理解消息的傳遞和轉發
5.class_copyIvarList方法獲取實例變數問題引發的思考
6.JSPatch 實現原理詳解
7.給分類(Category)添加屬性
8.Method Swizzling
轉載請註明來源:http://www.cnblogs.com/zhanggui/p/8243316.html