關于runtime,網上的資料都很全了,這里是根據自己的理解寫一個學習總結報告。主要借鑒文章如下:
http://www.lxweimin.com/p/db6dc23834e3 神經病院Objective-C Runtime出院第三天——如何正確使用Runtime
https://www.cnblogs.com/ioshe/p/5489086.html [iOS開發-Runtime詳解]
1.runtime是用來做什么的?
1.1 與runtime相關最大的,就是OC語言的動態綁定機制。
動態綁定是指一個對象發送消息后,該消息的實現(實際執行的函數)根據運行環境的不同而不同(此處只針對OC,Swift中已經不是運行時加載方法,而是和C語言類似,在編譯階段就確定了)。實現該機制,常用的就是分類(categor)、類擴展(extension)、子類(subclass)繼承等我們每個人都會使用的設計模式。
正常情況下,我們使用OC的這些特性就能夠解決大部分問題。但是有些情況下,為了優雅、高效的解決問題,我們有時候希望從更底層的層面進行操縱。
1.2. 一個經典案例
有一個業務需求,我們希望統計某個頁面viewController被點擊的次數,或者在進入某些頁面的時候添加引導圖。常規的做法是在這些對應的頁面的viewDidLoad中進行對應的需求作業。但是,如果項目比較大,頁面非常多,或者層級很復雜,這樣操作就效率很低,需要到不同的界面去進行分散的操作,日后新增、修改、維護或者調整也很麻煩。一個比較高效、優雅的做法是在基類UIViewContoller的viewDidLoad中實現該方法,因為所有的頁面都會繼承基類的viewDidLoad方法,在該基類中實現之后我們只需要在此處維護和新增就夠了。
所以我們需要給UIViewContoller基類添加一個category分類,在分類中重寫viewDidLoad方法。但是如果直接在分類中重寫,會導致基類代碼中的viewDidLoad不執行。此時,我們就需要使用runtime相關的方法來解決該問題(具體方式見第4節,第2條方法交換,如果需要深入了解,可以看看動態埋點統計的實例)。
1.3 常用runtime實現的強大功能
OC本質上是C的擴展和封裝。我們的OC代碼運行時,底層調用的實際上是c語言的代碼。runtime(翻譯過來即運行時)就是蘋果暴露給用戶的一個偏底層的可以操作底層代碼的API接口,是對常用的設計模式的一個必要補充。通過該接口的一些函數,我們可以直接干預消息發送過程,從而實現很多強大的功能。比如
- (1) 實現多繼承Multiple Inheritance (利用消息轉發機制)
- (2) 在分類中重寫原類方法而又不失去原類方法中的功能 (利用class的Method Swizzling)
- (3) Aspect Oriented Programming (切片編程)
- (4) 重寫class方法(Isa Swizzling)
- (5) 給分類添加屬性變量( 利用Associated Object給分類添加關聯對象)
- (6) 動態的增加方法 (利用消息轉發機制,在運行時實現方法)
- (7) NSCoding的自動歸檔和自動解檔(利用類底層的結構查詢函數,批量給所有屬性自動添加相同的解檔、歸檔方法)
2. runtime API中主要內容
2.1 對象、類的定義
從下表可以看到,本質上類是一個指向類結構體的指針,而對象是一個指向對象結構體的指針,對象結構體中存儲有一個isa類,它動態的指向該對象的類。類結構體中存儲有類的名字,父類名字,類的成員變量(無論是通過@property還是直接定義的成員變量都存儲在這里),類的實例變量大小(我們定義實例變量的時候變量空間大小就已經確定了),類的方法鏈表(普通類里面存儲著該類的實例方法,元類中存儲中該類的類方法),協議鏈表,方法緩存表(我們發送消息時第一個查詢的結構體)等。
//對象結構體中存儲有一個isa類
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;//Isa標識對象的類;它指向一個結構的類定義編譯。
};
//所有的對象本質上都是一個id,而id是一個指向對象結構體的指針
typedef struct objc_object *id;
//類是一個指向類結構體的指針
typedef struct objc_class *Class;`
`//類結構體中存儲有該類定義的所有相關數據;`
`struct objc_class {`
`Class isa OBJC_ISA_AVAILABILITY;//Isa標識對象的類;它指向一個結構的類定義編譯。`
`#if !__OBJC2__`
`Class super_class;``//父類`
`const char *name;``//類名`
`long version;``//類的版本信息,默認為0`
`long info;``//類信息,供運行期使用的一些位標識`
`long instance_size;``//類的實例變量大小`
`struct objc_ivar_list *ivars;``// 類的成員變量鏈表`
`struct objc_method_list **methodLists;``// 方法鏈表`
`struct objc_cache *cache;``//方法緩存`
`struct objc_protocol_list *protocols;``//協議鏈表#`
`endif} `
`OBJC2_UNAVAILABLE;`
因為類也有一個isa 指針,所以類本質上也是一個對象,稱為類對象。類對象Isa指針標識的類為該類的元類(meta class),每一個類都是這個元類的唯一實例對象。元類對象Isa指針標識的類為根元類,根元類(root meta Class)在整個系統中只有一個,所有的元類的isa指針都指向根元類,根元類的Isa指針標識的類為自己。具體如下所示,圖中虛線代表類的isa指針指向,實線代表類的父類。根元類的父類是根類,同時根元類的實例對象也是根類(root class),這里形成了一個閉環。
isa指針指向:實例對象->類->元類->(不經過父元類)直接到根元類(NSObject的元類),根元類的isa指向自己;
2.2 Method、IMP、SEL的定義
把他們拿出來說,是因為容易他們之間存在相關性和差異,非常容易產生誤解,而且他們對我們理解消息機制很有幫助,我們可以看一下方法Method的定義如下:
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;
}
Method 是一個指向結構體的指針,它包含了IMP和SEL,還包含了方法類型定義、方法的參數等。
SEL是方法的指針,但不同于C語言中的函數指針,函數指針直接保存了方法的地址,但SEL只是方法編號;
IMP是方法的具體實現函數指針,在runtime里,我們可以使用函數改變或者設置IMP來更改一個函數的具體實現,例如:
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2) //交換兩個方法的實現
method_setImplementation(Method _Nonnull m, IMP _Nonnull imp) // 給一個方法設置實現
2.3 runtime中常用的的一些函數
runtime中的函數,一般按照結構體的層級結構來操縱。對類中成員進行操作的,以class開頭,對方法中成員進行操作的以method開頭,其他的以此類推。常見的函數如下:
class_getProperty(Class _Nullable cls, const char * _Nonnull name) //獲取類的所有屬性列表
class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp,
const char * _Nullable types) //給類添加方法
class_replaceMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp,
const char * _Nullable types) //替換類方法
class_addIvar(Class _Nullable cls, const char * _Nonnull name, size_t size,
uint8_t alignment, const char * _Nullable types) //增加類變量
method_getImplementation(Method _Nonnull m) //獲取方法的實現
method_setImplementation(Method _Nonnull m, IMP _Nonnull imp) //設置方法的實現
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2) //交換方法的實現
imp_implementationWithBlock(id _Nonnull block) //使用一個block創建一個實現
sel_getName(SEL _Nonnull sel) //獲取方法的名稱
sel_registerName(const char * _Nonnull str) //注冊一個方法
3.消息傳遞、轉發機制
想要合理的利用runtime中相關API接口,必須理解runtime中的消息傳遞、轉發機制。
(1)當一個對象發送消息時,首先,底層會執行一個消息發送函數,函數長這樣
objc_msgSend(void /* id self, SEL op, ... */
如果是使用super發送消息,函數長這樣:
objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */
(2)底層會從該對象所屬的類中(isa指針所指的類)的方法緩存列表查找對應的實現
(3)如果2找不到,會從該類的方法鏈表中繼續查找
(4)如果3找不到,會跳轉到該類的父類查找,父類步驟和子類一樣,具體如下圖所示。
(5)一直向上到根類,如果根類仍然找不到,就開始準備進行消息轉發。轉發第一步:動態消息解析。查看當前類是否實現了resolveInstanceMethod方法(如果是類方法,會看是否實現了resolveClassMethod方法)。如果該方法返回了YES,消息轉發終止。我們可以在這個方法中動態添加方法實現,不實現也不要緊,只要返回YES消息發送就不會報錯。
+(BOOL)resolveClassMethod:(SEL)sel
{
NSString * selStr = NSStringFromSelector(sel);
if ([selStr isEqualToString:@"runTest"]) {
//注意,想要給類添加方法,必須添加到它的metaClass上,所以在class_addMethod中添加的類都要是原類!!!
// 確定metaClass的方法是objc_getMetaClass(object_getClassName(self));
if (class_addMethod(objc_getMetaClass(object_getClassName(self)), sel,class_getMethodImplementation(objc_getMetaClass(object_getClassName(self)), @selector(runTestFunction)), "s@:")) {
return YES;
}
return [super resolveClassMethod:sel];
}
return [super resolveClassMethod:sel];
}
(6)如果第5步返回NO,就開始消息重定向。查看是否指定了其他對象來執行該方法。具體是查看當前類是否實現了forwardingTargetForSelector方法;如果該方法返回了一個對象,就在該對象上執行該selctor方法(該對象上執行該方法時步驟與本對象一致);
-(id)forwardingTargetForSelector:(SEL)aSelector
(7)如果第6步返回nil,就需要進行真正的消息轉發機制。具體是查看當前類是否實現了methodSignatureForSelector方法,如果該方法返回不為nil,就執行forwardInvocation方法。如果forwardInvocation實現了,消息轉發終止(但不見得消息轉發完成,forwardInvocation只是一個消息的分發中心,將這些不能識別的消息轉發給不同的接收對象,或者轉發給同一個對象,再或者將消息翻譯成另外的消息,亦或者簡單的“吃掉”某些消息,因此沒有響應也不會報錯。)。
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
if ([someOtherObject respondsToSelector:
[anInvocation selector]])
[anInvocation invokeWithTarget:someOtherObject];
else
[super forwardInvocation:anInvocation];
}
(8)上述步驟,如果到第7部都沒有實現,系統就會報錯,提示unrecognized selector sent to instance
注意:上述任何一步,都要在前一步驟沒有完成的基礎上 。
4.如何使用runtime
基于runtime提供的數據結構,以及上述消息傳遞、轉發機制,runtime提供了豐富的函數讓我們來實現我們第1節中提到的強大的功能,我們這里簡單梳理下實現方式:
-
(1) 實現多繼承Multiple Inheritance (利用消息轉發機制),如下圖所示。
1330553-c7ef6392ecc9ee9d.gif
我們在Warrior中頭文件中定義一個方法negotiate,但是不實現它,而在forwardingTargetForSelector方法中,針對該selecotr,指定一個Diplomat對象,就可以將該方法實現交給diplomat類來實現。看起來就像是Warrior也繼承了了Diplomat的方法一樣(注意,像respondsToSelector:
和 isKindOfClass:
這類方法只會考慮繼承體系,不會考慮轉發鏈。也就是說如果[Warrior respondsToSelector:negotiate]會返回NO)。
-
(2) 在分類中重寫原類方法而又不失去原類方法中的功能 (利用class的Method Swizzling)
實現方式是通過runtime中的實現交換函數method_exchangeImplementations。首先,在本類中定義另一個待交換的方法exchage_ViewDidLoad;待交換的方法中需要調用原方法,然后添加需要額外實現的功能(例如第1節中提到的數據統計方法)。在恰當的時機(一般是在load方法中),交換該兩個方法的實現。實際執行代碼的使用,調用原類方法會執行待交換的方法的實現,待交換的方法實現中又會調用原來的方法實現,從而保留了原來的方法的實現。
+(void)exchangeOriginMethodWithMethodExchangeMethod
{
// 防止方法被多次調用后交換失效;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SEL originSEL = @selector(viewDidLoad);
SEL swizzSEL = @selector(exchage_ViewDidLoad);
Method viewDidLoad = class_getInstanceMethod([self class], originSEL);
Method exchang_viewDidLoad = class_getInstanceMethod([self class], swizzSEL);
// 測試原來的選擇子是否已經添加了方法(是否已經交換了方法);
Boolean didAddMethod = class_addMethod([self class], originSEL,method_getImplementation(exchang_viewDidLoad),method_getTypeEncoding(exchang_viewDidLoad));
if (!didAddMethod) {
//
// 如果沒有添加方法,就直接交換
method_exchangeImplementations(viewDidLoad, exchang_viewDidLoad);
}else{
// 如果已經添加了,就同時更換交換后的方法實現;
class_replaceMethod([self class], swizzSEL, method_getImplementation(viewDidLoad), method_getTypeEncoding(viewDidLoad));
}
});
}
-(void)exchage_ViewDidLoad
{
NSLog(@"%@ did load",self);
[self exchage_ViewDidLoad];//注意,exchage_ViewDidLoad的實現現在是viewDidLoad了,所以沒有循環調用
}
(3) Aspect Oriented Programming (切片編程,內容太多,暫不展開,可以看這里)
-
(4) 重寫class方法(Isa Swizzling)
蘋果著名的KVO技術和NSNotificationCenter就使用的該方法,在我們給一個對象添加了KVO鍵值觀察方法后
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)contex。
后臺會重新創建一個NSKVONotifying_Object類,然后偷偷將原來的類的isa指針指向該類。該類中會在屬性變量修改時候,調用
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
方法,并發出相應的通知
-
(5) 給分類添加屬性變量( 利用Associated Object給分類添加關聯對象)
我們可以給分類添加屬性,但是分類不會自動給我們生成成員變量。因為類的成員變量在編譯器已經決定了(寫入了類的結構體中,具體見前面結構體的定義),但是category是在運行期才決議的。所以如果要給分類添加成員變量,需要用runtime里面函數在運行期實現。一般使用objc_setAssociatedObject和objc_getAssociatedObject函數來實現。這兩個函數都是成對的出現,一個給對象添加關聯對象,一個獲取關聯對象。具體代碼如下。
@property(nonatomic,strong)id associatedObjcet;
-(void)setAssociatedObjcet:(id)associatedObjcet{
objc_setAssociatedObject(self, @selector(associatedObjcet), associatedObjcet, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
-(id)associatedObjcet{
return objc_getAssociatedObject(self, @selector(associatedObjcet));
}
-
(6) 動態的增加方法 (利用消息轉發機制,在運行時實現方法)
動態的增加方法和多重繼承有些類似,都是調用的方法在類中并沒有實現代碼,而是在消息轉發機制的某一步才動態的添加實現代碼。消息轉發機制本身有多步驟,所以根據需要,可以在不同的步驟實現動態添加,常見的一般在方法動態解析resolveInstanceMethod或者在消息轉發forwardInvocation的時候進行。
(7) NSCoding的自動歸檔和自動解檔(利用類底層的結構查詢函數,批量給所有屬性自動添加相同的解檔、歸檔方法)
NSCoding其實就是對所有的屬性調用encode和decode方法。使用手動操作有一個缺陷,如果屬性多起來,要寫好多行相似的代碼,雖然功能是可以完美實現,但是看上去不是很優雅。用runtime實現的思路就比較簡單,我們循環依次找到每個成員變量的名稱,然后利用KVC讀取和賦值就可以完成encodeWithCoder和initWithCoder了,部分代碼如下:
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];
}