自己最近在研究Runtime,研究好久才知道了一些大概和簡單的應用。在這里做一個筆記。RunTime被稱為iOS開發的黑魔法,功能之強大,簡直就是裝逼神器啊。自己也是摸索著前人的步伐,一步一步探索Runtime機制在開發中的使用。
1.什么是Runtime
Runtime是一套底層的C語言API,包含很多強大實用的C語言數據類型和C語言函數。平時我們編寫的OC代碼,都是基于Runtime實現的。OC是運行時語言,也只有在運行的時候才可以確定對象的類型,并調用類的對象的相應的方法,其中最主要的是消息機制。所以利用Runtime機制可以在程序運行的時候動態修改類的方法、類的對象的屬性、方法、創建類別。這些應該是Runtime的基本用法吧,也是我們在平時的開發中用到的。
例如下邊的這個方法在運行時會被轉化:
/* OC方法調用 */
[obj makeTest];
/* 編譯時Runtime會將上面的代碼轉為下面的消息發送 */
objc_msgSend(obj, @selector(makeText));
iOS的頂層基類NSObject含有一個指向objc_class結構體的isa指針:
@interface NSObject{
Class isa;
}
typedef struct objc_class *Class;
struct objc_class {
Class isa; // 指向metaclass,也就是靜態的Class
Class super_class ; // 指向其父類
const char *name ; // 類名
long version ; // 類的版本信息,初始化默認為0
/* 一些標識信息,如CLS_CLASS(0x1L)表示該類為普通class;
CLS_META(0x2L)表示該類為metaclass */
long info;
long instance_size ; // 該類的實例變量大小(包括從父類繼承下來的實例變量);
struct objc_ivar_list *ivars; // 用于存儲每個成員變量的地址
/* 與info的一些標志位有關,如是普通class則存儲對象方法,如是metaclass則存儲類方法; */
struct objc_method_list **methodLists ;
struct objc_cache *cache; // 指向最近使用的方法的指針,用于提升效率;
struct objc_protocol_list *protocols; // 存儲該類遵守的協議
}
在objc_msgSend函數的調用過程:
1.首先通過obj的isa指針找到obj對應的Class。
2.在Class中先去cache中通過SEL查找對應函數method
3.若cache中未找到,再去methodLists中查找
4.若methodLists中未找到,則進入superClass按前面的步驟進行遞歸查找
5.若找到method,則將method加入到cache中,以方便下次查找,并通過method中的函數指針跳轉到對應的函數中去執行。
6.如果一直查找到NSObject還沒查找到,則會進入消息動態處理流程。
消息動態處理流程:
/* 1. 時機處理之一,在這個方法中我們可以利用runtime的特性動態添加方法來處理 */
+ (BOOL)resolveInstanceMethod:(SEL)sel;
/* 2. 時機處理之二,在這個方法中看代理能不能處理,如果代理對象能處理,則轉接給代理對象 */
- (id)forwardingTargetForSelector:(SEL)aSelector;
/* 3. 消息轉發之一,該方法返回方法簽名,如果返回nil,則轉發流程終止,拋出異常 */
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
/* 4. 消息轉發之二,在該方法中我們可以對調用方法進行重定向 */
- (void)forwardInvocation:(NSInvocation *)anInvocation;
2.Runtime的應用場景
1>.程序運行過程中動態創建類,如:KVO的實現
2>.程序運行過程中動態修改對象的屬性、方法
3>.遍歷一個類的所有成員變量、方法
4>.交換方法
5>.運行時創建類
3.場景舉例:
(1).動態創建類---實現自己的KVO監聽
KVO監聽相信大家在平時開發中都有用到過,但是它是怎么監聽到屬性變化的呢?懶加載大家都有用到吧,自己重寫屬性的set方法。對,KVO就是監聽屬性的set方法。讓我們先看第一張圖:
圖中實例對象p的isa指針指向的是Person類,讓我們單步往下走
現在p的isa指針指向的是NSKVONotifying_Person這個類。在程序運行的時候,蘋果利用runtime機制動態的創建了一個繼承Person類的NSKVONotifying_Person這個類,并將isa指針動態指向子類方法。蘋果在動態創建的NSKVONotifying_Person這個類中重寫父類屬性的set方法,方法實現中再調用父類的set方法。
現在我們自己利用Runtime實現自己的KVO監聽,關鍵代碼如下:
-(void)LR_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context{
//獲取類名
NSString* oldClass = NSStringFromClass([self class]);
NSString* newClass = [@"LR" stringByAppendingString:oldClass];
const char* name = [newClass UTF8String];
//1.動態生成一個類
Class myClass = objc_allocateClassPair([self class], name, 0);
//添加一個方法
class_addMethod(myClass, @selector(setName:), (IMP)setName, "");
//2.注冊這個類
objc_registerClassPair(myClass);
//3.修改isa指針
object_setClass(self, myClass);
//4.實現setName方法
void setName(id self, SEL _cmd,NSString* newName){
NSLog(@"我來餓了");
}
(2).程序運行過程中動態修改對象的屬性、方法
我們在demo中調用Person類一個沒有實現的方法,然后command+R會怎樣?Crash?。。?/p>
Person* p = [[Person alloc] init];
[p performSelector:@selector(run)];
現在我們可以利用runtime機制實現方法的懶加載。
關鍵代碼如下:
+(BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(run)) {
//1.cls 類類型
//2.name 方法編號
//3.imp 方法實現。函數指針指向一個實現
//4.types 返回值類型
class_addMethod([Person class], sel, (IMP)haha, "v");
}
/*
第四個參數的含義:
v表示void,@表示id類型,:表示SEL類型
"v@:@":表示返回值為void,接受一個id類型、一個SEL類型、一個id類型的方法
"@@:":表示返回值為id類型,接受一個id類型和一個SEL類型參數的方法
*/
return [super resolveInstanceMethod:sel];
}
實現動態添加的方法的實現
void haha(id self ,SEL _cmd){
NSLog(@"%@===%@",self,NSStringFromSelector(_cmd));
NSLog(@"你說啥");
}
當一個類調用了沒有實現的方法,就會來到runtime的這個方法resolveInstanceMethod,進行方法的尋找,如果子類中沒有方法的實現,就會在父類中尋找,如果父類也沒有,就往父類的父類取尋找。所以在這個方法中我們使用class_addMethod方法動態的為Person類添加方法。
注:在所有的方法中都會隱式接收2個參數----self,_cmd。self調用的類,_cmd方法的編碼名。這兩個參數只有你傳入之后才可以在方法實現中拿到。
(3).遍歷一個類的所有成員變量、方法
主要用到的方法如下:
class_copyIvarList--->獲取類的成員變量列表-->多用于字典轉模型,歸解檔的操作。(有興趣的可以研究一下MJExtension的內部實現,受益匪淺)
class_copyPropertyList--->獲取類的屬性列表
代碼如圖:
(4).交換方法
通過runtime的method_exchangeImplementations方法來實現方法的互換(實際是利用runtime改變了兩個方法的isa指針指向)。一般用自己寫的方法(常用在自己封裝的類或寫的框架中,添加某些防錯措施)來替換系統的方法,如:
在數組中的越界訪問或數組使用addObject方法添加元素為nil時導致的程序崩潰??梢孕陆ㄒ粋€分類實現方法的交換來防止程序的崩潰,如圖:
(5).動態添加一個類
4.Runtime的簡單應用
當我們的項目越做越大越復雜的時候,建立的控制器也會越來越多,相應的跳轉也會增加。特別是你接收一個大項目的時候,對整體的業務邏輯不熟悉,整體的架構體系也是一頭霧水,然而你又要修復某個頁面的BUG,估計要找到對應的頁面都要找好久。有沒有一種方式可以快速找到某個頁面對應的ciewController呢?
方案一、在整個項目建立初期構建一個基類viewController,此后創建的VC均繼承于基類。我們只需要重寫基類的viewWillAppear方法。
- (void)viewWillAppear:(BOOL)animated {
[super?viewWillAppear:animated];
NSString?*className?=?NSStringFromClass([self?class]);
NSLog(@"%@?will?appear",?className);
}
方案二、給viewController創建一個分類,在分類里邊進行方法的替換。這里所說的方法替換并不是使用method_exchangeImplementations改變兩個方法的isa指針指向。而是先拿到系統方法的IMP實現,然后構建一個新的符合我們需求的IMP實現來替代系統方法的IMP實現。如圖:
具體代碼如下:
5.Runtime使用心得
Runtime很強大,這只是我的一個初步的了解,對于很多東西不是很了解。這應該算是Runtime的一個基礎用法吧。不過黑魔法就是用著酸爽。要理解透徹,并且在開發中熟練應用感覺任重道遠啊。