iOS-RunTime,不再只是聽說
一. RunTime簡介
RunTime簡稱運行時。OC就是運行時機制,也就是在運行時候的一些機制,其中最主要的是消息機制。
對于C語言,函數的調用在編譯的時候會決定調用哪個函數,如果調用未實現的函數就會報錯。
對于OC語言,屬于動態調用過程,在編譯的時候并不能決定真正調用哪個函數,只有在真正運行的時候才會根據函數的名稱找到對應的函數來調用。在編譯階段,OC可以調用任何函數,即使這個函數并未實現,只要聲明過就不會報錯。
二. RunTime消息機制
消息機制是運行時里面最重要的機制,OC中任何方法的調用,本質都是發送消息。
使用運行時,發送消息需要導入框架 <objc/message.h> 并且xcode5之后,蘋果不建議使用底層方法,如果想要使用運行時,需要關閉嚴格檢查objc_msgSend的調用,BuildSetting->搜索msg 改為NO。
下來看一下實例方法調用底層實現
Person *p = [[Person alloc] init];
[p eat];
// 底層會轉化成
//SEL:方法編號,根據方法編號就可以找到對應方法的實現。
[p performSelector:@selector(eat)];
//performSelector本質即為運行時,發送消息,誰做事情就調用誰
objc_msgSend(p, @selector(eat));
// 帶參數
objc_msgSend(p, @selector(eat:),10);
類方法的調用底層
// 本質是會將類名轉化成類對象,初始化方法其實是在創建類對象。
[Person eat];
// Person只是表示一個類名,并不是一個真實的對象。只要是方法必須要對象去調用。
// RunTime 調用類方法同樣,類方法也是類對象去調用,所以需要獲取類對象,然后使用類對象去調用方法。
Class personclass = [Persion class];
[[Persion class] performSelector:@selector(eat)];
// 類對象發送消息
objc_msgSend(personclass, @selector(eat));
<p>@selector (SEL):是一個SEL方法選擇器。SEL其主要作用是快速的通過方法名字查找到對應方法的函數指針,然后調用其函數。SEL其本身是一個Int類型的地址,地址中存放著方法的名字。</p>
對于一個類中。每一個方法對應著一個SEL。所以一個類中不能存在2個名稱相同的方法,即使參數類型不同,因為SEL是根據方法名字生成的,相同的方法名稱只能對應一個SEL。
運行時發送消息的底層實現
每一個類都有一個方法列表 Method List,保存這類里面所有的方法,根據SEL傳入的方法編號找到方法,相當于value - key的映射。然后找到方法的實現。去方法的實現里面去實現。如圖所示。
運行時發送消息的底層實現
那么內部是如何動態查找對應的方法的?
首先我們知道所有的類中都繼承自NSObject類,在NSObjcet中存在一個Class的isa指針。
typedef struct objc_class *Class;
@interface NSObject <NSObject> {
Class isa OBJC_ISA_AVAILABILITY;
}
我們來到objc_class中查看,其中包含著類的一些基本信息。
struct objc_class {
Class isa; // 指向metaclass
Class super_class ; // 指向其父類
const char name ; // 類名
long version ; // 類的版本信息,初始化默認為0,可以通過runtime函數class_setVersion和class_getVersion進行修改、讀取
long info; // 一些標識信息,如CLS_CLASS (0x1L) 表示該類為普通 class ,其中包含對象方法和成員變量;CLS_META (0x2L) 表示該類為 metaclass,其中包含類方法;
long instance_size ; // 該類的實例變量大小(包括從父類繼承下來的實例變量);
struct objc_ivar_list ivars; // 用于存儲每個成員變量的地址
struct objc_method_list methodLists ; // 與 info 的一些標志位有關,如CLS_CLASS (0x1L),則存儲對象方法,如CLS_META (0x2L),則存儲類方法;
struct objc_cache cache; // 指向最近使用的方法的指針,用于提升效率;
struct objc_protocol_list protocols; // 存儲該類遵守的協議
}</code></pre>
下面我們就以p實例的eat方法來看看具體消息發送之后是怎么來動態查找對應的方法的。
- 實例方法 [p eat]; 底層調用 [p performSelector:@selector(eat)]; 方法,編譯器在將代碼轉化為 objc_msgSend(p, @selector(eat));
- 在 objc_msgSend 函數中。首先通過 p 的 isa 指針找到 p 對應的 class 。在 Class 中先去 cache 中通過 SEL 查找對應函數 method ,如果找到則通過 method 中的函數指針跳轉到對應的函數中去執行。
- 若 cache 中未找到。再去 methodList 中查找。若能找到,則將 method 加入到 cache 中,以方便下次查找,并通過 method 中的函數指針跳轉到對應的函數中去執行。
- 若 methodlist 中未找到,則去 superClass 中查找。若能找到,則將 method 加入到 cache 中,以方便下次查找,并通過 method 中的函數指針跳轉到對應的函數中去執行。
三. 使用RunTime交換方法:
當系統自帶的方法功能不夠,需要給系統自帶的方法擴展一些功能,并且保持原有的功能時,可以使用RunTime交換方法實現。
這里要實現image添加圖片的時候,自動判斷image是否為空,如果為空則提醒圖片不存在。
方法一:使用分類
+ (nullable UIImage *)xx_ccimageNamed:(NSString *)name
{
// 加載圖片 如果圖片不存在則提醒或發出異常
UIImage *image = [UIImage imageNamed:name];
if (image == nil) {
NSLog(@"圖片不存在");
}
return image;
}
缺點:每次使用都需要導入頭文件,并且如果項目比較大,之前使用的方法全部需要更改。
方法二 :RunTime交換方法
交換方法的本質其實是交換兩個方法的實現,即調換xx_ccimageNamed和imageName方法,達到調用xx_ccimageNamed其實就是調用imageNamed方法的目的
那么首先需要明白方法在哪里交換,因為交換只需要進行一次,所以在分類的load方法中,當加載分類的時候交換方法即可。
+(void)load
{
// 獲取要交換的兩個方法
// 獲取類方法 用Method 接受一下
// class :獲取哪個類方法
// SEL :獲取方法編號,根據SEL就能去對應的類找方法。
Method imageNameMethod = class_getClassMethod([UIImage class], @selector(imageNamed:));
// 獲取第二個類方法
Method xx_ccimageNameMrthod = class_getClassMethod([UIImage class], @selector(xx_ccimageNamed:));
// 交換兩個方法的實現 方法一 ,方法二。
method_exchangeImplementations(imageNameMethod, xx_ccimageNameMrthod);
// IMP其實就是 implementation的縮寫:表示方法實現。
}
交換方法內部實現:
- 根據SEL方法編號在Method中找到方法,兩個方法都找到
- 交換方法的實現,指針交叉指向。如圖所示:
交換方法內部實現
注意:交換方法時候 xx_ccimageNamed方法中就不能再調用imageNamed方法了,因為調用imageNamed方法實質上相當于調用 xx_ccimageNamed方法,會循環引用造成死循環。
RunTime也提供了獲取對象方法和方法實現的方法。
// 獲取方法的實現
class_getMethodImplementation(<#__unsafe_unretained Class cls#>, <#SEL name#>)
// 獲取對象方法
class_getInstanceMethod(<#__unsafe_unretained Class cls#>, <#SEL name#>)
此時,當調用imageNamed:方法的時候就會調用xx_ccimageNamed:方法,為image添加圖片,并判斷圖片是否存在,如果不存在則提醒圖片不存在。
四. 動態添加方法
如果一個類方法非常多,其中可能許多方法暫時用不到。而加載類方法到內存的時候需要給每個方法生成映射表,又比較耗費資源。此時可以使用RunTime動態添加方法
動態給某個類添加方法,相當于懶加載機制,類中許多方法暫時用不到,那么就先不加載,等用到的時候再去加載方法。
動態添加方法的方法:
首先我們先不實現對象方法,當調用performSelector: 方法的時候,再去動態加載方法。
這里同上創建Person類,使用performSelector: 調用Person類對象的eat方法。
Person *p = [[Person alloc]init];
// 當調用 P中沒有實現的方法時,動態加載方法
[p performSelector:@selector(eat)];
此時編譯的時候是不會報錯的,程序運行時才會報錯,因為Person類中并沒有實現eat方法,當去類中的Method List中發現找不到eat方法,會報錯找不到eat方法。

報錯信息:未被選擇器發送到實例
而當找不到對應的方法時就會來到攔截調用,在找不到調用的方法程序崩潰之前調用的方法。
當調用了沒有實現的對象方法的時,就會調用 +(BOOL)resolveInstanceMethod:(SEL)sel 方法。
當調用了沒有實現的類方法的時候,就會調用 +(BOOL)resolveClassMethod:(SEL)sel 方法。
首先我們來到API中看一下蘋果的說明,搜索 Dynamic Method Resolution 來到動態方法解析。

Dynamic Method Resolution
Dynamic Method Resolution的API中已經講解的很清晰,我們可以實現方法 resolveInstanceMethod: 或者 resolveClassMethod: 方法,動態的給實例方法或者類方法添加方法和方法實現。
所以通過這兩個方法就可以知道哪些方法沒有實現,從而動態添加方法。參數sel即表示沒有實現的方法。
一個objective - C方法最終都是一個C函數,默認任何一個方法都有兩個參數。
self : 方法調用者 _cmd : 調用方法編號。我們可以使用函數class_addMethod為類添加一個方法以及實現。
這里仿照API給的例子,動態的為P實例添加eat對象
+(BOOL)resolveInstanceMethod:(SEL)sel
{
// 動態添加eat方法
// 首先判斷sel是不是eat方法 也可以轉化成字符串進行比較。
if (sel == @selector(eat)) {
/
第一個參數: cls:給哪個類添加方法
第二個參數: SEL name:添加方法的編號
第三個參數: IMP imp: 方法的實現,函數入口,函數名可與方法名不同(建議與方法名相同)
第四個參數: types :方法類型,需要用特定符號,參考API
/
class_addMethod(self, sel, (IMP)eat , "v@:");
// 處理完返回YES
return YES;
}
return [super resolveInstanceMethod:sel];
}</code></pre>
重點來看一下class_addMethod方法
class_addMethod(__unsafe_unretained Class cls, SEL name, IMP imp, const char
types)</code></pre>
class_addMethod中的四個參數。第一,二個參數比較好理解,重點是第三,四個參數。
- cls : 表示給哪個類添加方法,這里要給Person類添加方法,self即代表Person。
- SEL name : 表示添加方法的編號。因為這里只有一個方法需要動態添加,并且之前通過判斷確定sel就是eat方法,所以這里可以使用sel。
- IMP imp : 表示方法的實現,函數入口,函數名可與方法名不同(建議與方法名相同)需要自己來實現這個函數。每一個方法都默認帶有兩個隱式參數
self : 方法調用者 _cmd : 調用方法的標號 ,可以寫也可以不寫。
void eat(id self ,SEL _cmd)
{
// 實現內容
NSLog(@"%@的%@方法動態實現了",self,NSStringFromSelector(_cmd));
}
types : 表示方法類型,需要用特定符號。系統提供的例子中使用的是 "v@:" ,我們來到API中看看 "v@:" 指定的方法是什么類型的。

Objective-C type encodings
從圖中可以看出
v -> void 表示無返回值
<p>@ -> object 表示id參數</p>
: -> method selector 表示SEL
至此已經完成了P實例eat方法的動態添加。當P調用eat方法時輸出

p調用eat方法時輸出
動態添加有參數的方法
如果是有參數的方法,需要對方法的實現和class_addMethod方法內方法類型參數做一些修改。
方法實現:因為在C語言函數中,所以對象參數類型只能用id代替。
方法類型參數:因為添加了一個id參數,所以方法類型應該為 "v@:@"
來看一下代碼
+(BOOL)resolveInstanceMethod:(SEL)sel
{
if (sel == @selector(eat:)) {
class_addMethod(self, sel, (IMP)aaaa , "v@:@");
return YES;
}
return [super resolveInstanceMethod:sel];
}
void aaaa(id self ,SEL _cmd,id Num)
{
// 實現內容
NSLog(@"%@的%@方法動態實現了,參數為%@",self,NSStringFromSelector(_cmd),Num);
}
調用 eat: 函數
Person *p = [[Person alloc]init];
[p performSelector:@selector(eat:)withObject:@"xx_cc"];
輸出為

p調用eat:方法時輸出
五. RunTime動態添加屬性
使用RunTime給系統的類添加屬性,首先需要了解對象與屬性的關系。

對象與屬性的關系
對象一開始初始化的時候其屬性name為nil,給屬性賦值其實就是讓name屬性指向一塊存儲字符串的內存,使這個對象的屬性跟這塊內存產生一種關聯,個人理解對象的屬性就是一個指針,指向一塊內存區域。
那么如果想動態的添加屬性,其實就是動態的產生某種關聯就好了。而想要給系統的類添加屬性,只能通過分類。
這里給NSObject添加name屬性,創建NSObject的分類
我們可以使用@property給分類添加屬性
@property(nonatomic,strong)NSString *name;
雖然在分類中可以寫@property
添加屬性,但是不會自動生成私有屬性,也不會生成set,get方法的實現,只會生成set,get的聲明,需要我們自己去實現。
方法一:我們可以通過使用靜態全局變量給分類添加屬性
static NSString *_name;
-(void)setName:(NSString *)name
{
_name = name;
}
-(NSString *)name
{
return _name;
}
但是這樣_name靜態全局變量與類并沒有關聯,無論對象創建與銷毀,只要程序在運行_name變量就存在,并不是真正意義上的屬性。
方法二:使用RunTime動態添加屬性
RunTime提供了動態添加屬性和獲得屬性的方法。
-(void)setName:(NSString *)name
{
objc_setAssociatedObject(self, @"name",name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
-(NSString *)name
{
return objc_getAssociatedObject(self, @"name");
}
- 動態添加屬性
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
參數一: id object : 給哪個對象添加屬性,這里要給自己添加屬性,用self。
參數二: void * == id key : 屬性名,根據key獲取關聯對象的屬性的值,在 objc_getAssociatedObject 中通過次key獲得屬性的值并返回。
參數三: id value : 關聯的值,也就是set方法傳入的值給屬性去保存。
參數四: objc_AssociationPolicy policy : 策略,屬性以什么形式保存。
有以下幾種
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
OBJC_ASSOCIATION_ASSIGN = 0, // 指定一個弱引用相關聯的對象
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, // 指定相關對象的強引用,非原子性
OBJC_ASSOCIATION_COPY_NONATOMIC = 3, // 指定相關的對象被復制,非原子性
OBJC_ASSOCIATION_RETAIN = 01401, // 指定相關對象的強引用,原子性
OBJC_ASSOCIATION_COPY = 01403 // 指定相關的對象被復制,原子性
};
- 獲得屬性
objc_getAssociatedObject(id object, const void *key);
參數一: id object : 獲取哪個對象里面的關聯的屬性。
參數二: void * == id key : 什么屬性,與 objc_setAssociatedObject 中的key相對應,即通過key值取出value。
此時已經成功給NSObject添加name屬性,并且NSObject對象可以通過點語法為屬性賦值。
NSObject *objc = [[NSObject alloc]init];
objc.name = @"xx_cc";
NSLog(@"%@",objc.name);
六. RunTime字典轉模型
為了方便以后重用,這里通過給NSObject添加分類,聲明并實現使用RunTime字典轉模型的類方法。
+ (instancetype)modelWithDict:(NSDictionary *)dict
首先來看一下KVC字典轉模型和RunTime字典轉模型的區別
KVC:KVC字典轉模型實現原理是遍歷字典中所有Key,然后去模型中查找相對應的屬性名,要求屬性名與Key必須一一對應,字典中所有key必須在模型中存在。
RunTime:RunTime字典轉模型實現原理是遍歷模型中的所有屬性名,然后去字典查找相對應的Key,也就是以模型為準,模型中有哪些屬性,就去字典中找那些屬性。
RunTime字典轉模型的優點:當服務器返回的數據過多,而我們只使用其中很少一部分時,沒有用的屬性就沒有必要定義成屬性浪費不必要的資源。只保存最有用的屬性即可。
RunTime字典轉模型過程
首先需要了解,屬性定義在類里面,那么類里面就有一個屬性列表,屬性列表以數組的形式存在,根據屬性列表就可以獲得類里面的所有屬性名,所以遍歷屬性列表,也就可以遍歷模型中的所有屬性名。
所以RunTime字典轉模型過程就很清晰了。
- 創建模型對象
id objc = [[self alloc] init];
- 使用 class_copyIvarList 方法拷貝成員屬性列表
unsigned int count = 0;
Ivar *ivarList = class_copyIvarList(self, &count);
參數一: __unsafe_unretained Class cls : 獲取哪個類的成員屬性列表。這里是self,因為誰調用分類中類方法,誰就是self。
參數二: unsigned int *outCount : 無符號int型指針,這里創建unsigned int型count,&count就是他的地址,保證在方法中可以拿到count的地址為count賦值。傳出來的值為成員屬性總數。
返回值: Ivar * : 返回的是一個Ivar類型的指針 。指針默認指向的是數組的第0個元素,指針+1會向高地址移動一個Ivar單位的字節,也就是指向第一個元素。Ivar表示成員屬性。
- 遍歷成員屬性列表,獲得屬性列表
for (int i = 0 ; i < count; i++) {
// 獲取成員屬性
Ivar ivar = ivarList[i];
}
- 使用 ivar_getName(ivar) 獲得成員屬性名,因為成員屬性名返回的是C語言字符串,將其轉化成OC字符串
NSString *propertyName = [NSString stringWithUTF8String:ivar_getName(ivar)];
通過 ivar_getTypeEncoding(ivar) 也可以獲得成員屬性類型。
- 因為獲得的是成員屬性名,是帶_的成員屬性,所以需要將下劃線去掉,獲得屬性名,也就是字典的key。
// 獲取key
NSString *key = [propertyName substringFromIndex:1];
- 獲取字典中key對應的Value。
// 獲取字典的value
id value = dict[key];
- 給模型屬性賦值,并將模型返回
if (value) {
// KVC賦值:不能傳空
[objc setValue:value forKey:key];
}
return objc;
至此已成功將字典轉為模型。
七. RunTime字典轉模型的二級轉換
在開發過程中經常用到模型嵌套,也就是模型中還有一個模型,這里嘗試用RunTime進行模型的二級轉換,實現思路其實比較簡單清晰。
- 首先獲得一級模型中的成員屬性的類型
// 成員屬性類型
NSString *propertyType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
- 判斷當一級字典中的value是字典,并且一級模型中的成員屬性類型不是NSDictionary的時候才需要進行二級轉化。
首先value是字典才進行轉化是必須的,因為我們通常將字典轉化為模型,其次,成員屬性類型不是系統類,說明成員屬性是我們自定義的類,也就是要轉化的二級模型。而當成員屬性類型就是NSDictionary的話就表明,我們本就想讓成員屬性是一個字典,不需要進行模型的轉換。
id value = dict[key];
if ([value isKindOfClass:[NSDictionary class]] && ![propertyType containsString:@"NS"])
{
// 進行二級轉換。
}
- 獲取要轉換的模型類型,這里需要對propertyType成員屬性類型做一些處理,因為propertyType返回給我們成員屬性類型的是 @\"Mode\" ,我們需要對他進行截取為 Mode 。這里需要注意的是\只是轉義符,不占位。
// @\"Mode\"去掉前面的@\"
NSRange range = [propertyType rangeOfString:@"\""];
propertyType = [propertyType substringFromIndex:range.location + range.length];
// Mode\"去掉后面的\"
range = [propertyType rangeOfString:@"\""];
propertyType = [propertyType substringToIndex:range.location];
- 獲取需要轉換類的類對象,將字符串轉化為類名。
Class modelClass = NSClassFromString(propertyType);
- 判斷如果類名不為空則調用分類的modelWithDict方法,傳value字典,進行二級模型轉換,返回二級模型在賦值給value。
if (modelClass) {
value = [modelClass modelWithDict:value];
}
這里可能有些繞,重新理一下,我們通過判斷value是字典并且需要進行二級轉換,然后將value字典轉化為模型返回,并重新賦值給value,最后給一級模型中相對應的key賦值模型value即可完成二級字典對模型的轉換。
最后附上二級轉換的完整方法
+ (instancetype)modelWithDict:(NSDictionary *)dict{
// 1.創建對應類的對象
id objc = [[self alloc] init];
// count:成員屬性總數
unsigned int count = 0;
// 獲得成員屬性列表和成員屬性數量
Ivar *ivarList = class_copyIvarList(self, &count);
for (int i = 0 ; i < count; i++) {
// 獲取成員屬性
Ivar ivar = ivarList[i];
// 獲取成員名
NSString *propertyName = [NSString stringWithUTF8String:ivar_getName(ivar)];
// 獲取key
NSString *key = [propertyName substringFromIndex:1];
// 獲取字典的value key:屬性名 value:字典的值
id value = dict[key];
// 獲取成員屬性類型
NSString *propertyType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
// 二級轉換
// value值是字典并且成員屬性的類型不是字典,才需要轉換成模型
if ([value isKindOfClass:[NSDictionary class]] && ![propertyType containsString:@"NS"]) {
// 進行二級轉換
// 獲取二級模型類型進行字符串截取,轉換為類名
NSRange range = [propertyType rangeOfString:@"\""];
propertyType = [propertyType substringFromIndex:range.location + range.length];
range = [propertyType rangeOfString:@"\""];
propertyType = [propertyType substringToIndex:range.location];
// 獲取需要轉換類的類對象
Class modelClass = NSClassFromString(propertyType);
// 如果類名不為空則進行二級轉換
if (modelClass) {
// 返回二級模型賦值給value
value = [modelClass modelWithDict:value];
}
}
if (value) {
// KVC賦值:不能傳空
[objc setValue:value forKey:key];
}
}
// 返回模型
return objc;
}
以上只是對RunTime淺顯的理解,可以看出RunTime非常強大。也許我們只是簡單調用了一個方法,系統底層卻幫我們做了很多很多事情。
文中如果有不對的地方歡迎指出。我是 xx_cc ,一只長大很久但還沒有二夠的家伙。
來自:http://www.jianshu.com/p/8acdedf9c1af