前言:
關于Runtime的資料網上一搜很多,但總是寫的只言片語,不太全面。最近花了一個星期的時間重新學習Runtime,并整理了一個系列文章,并發表出來,同時也感謝開源貢獻的開發者。這里共有三篇文章:
Runtime系列一:Runtime的前世今生
Runtime系列二:Runtime的原理
Runtime系列三:Runtime在項目中使用場景
一、方法交換Swizzling
使用場景:系統自帶的方法功能不夠,給系統自帶的方法擴展一些功能,并且保持原有的功能。
方式一:繼承系統的類,重寫方法.
方式二:使用runtime,交換方法.
在Objective-C中調用一個方法,其實是向一個對象發送消息,而查找消息的唯一依據是selector的名字。所以,我們可以利用Objective-C的runtime機制,實現在運行時交換selector對應的方法實現以達到我們的目的。每個類都有一個方法列表,存放著selector的名字和方法實現的映射關系。IMP有點類似函數指針,指向具體的Method實現
我們先看看SEL與IMP之間的關系圖:
從上圖可以看出來,每一個SEL與一個IMP一一對應,正常情況下通過SEL可以查找到對應消息的IMP實現。
但是,現在我們要做的就是把鏈接線解開,然后連到我們自定義的函數的IMP上。當然,交換了兩個SEL的IMP,還是可以再次交換回來了。交換后變成這樣的,如下圖
從圖中可以看出,我們通過swizzling特性,將selectorC的方法實現IMPc與selectorN的方法實現IMPn交換了,當我們調用selectorC,也就是給對象發送selectorC消息時,所查找到的對應的方法實現就是IMPn而不是IMPc了。
#import"UIViewController+swizzling.h"
#import@implementationUIViewController(swizzling)//load方法會在類第一次加載的時候被調用//調用的時間比較靠前,適合在這個方法里做方法交換
+ (void)load{//方法交換應該被保證,在程序中只會執行一次staticdispatch_once_tonceToken;dispatch_once(&onceToken, ^{
//獲得viewController的生命周期方法的selector
SEL systemSel =@selector(viewWillAppear:);
//自己實現的將要被交換的方法的selector
SEL swizzSel =@selector(swiz_viewWillAppear:);
//兩個方法的Method
Method ? systemMethod = class_getInstanceMethod([selfclass], systemSel); ? ??
? Method swizzMethod = class_getInstanceMethod([selfclass], swizzSel);
//首先動態添加方法,實現是被交換的方法,返回值表示添加成功還是失敗BOOLisAdd = class_addMethod(self, systemSel, method_getImplementation(swizzMethod), method_getTypeEncoding(swizzMethod));
if(isAdd) {
//如果成功,說明類中不存在這個方法的實現//將被交換方法的實現替換到這個并不存在的實現class_replaceMethod(self,swizzSel,method_getImplementation(systemMethod), method_getTypeEncoding(systemMethod));? ? ? ? }
else{
//否則,交換兩個方法的實現method_exchangeImplementations(systemMethod, swizzMethod);? ? ? ? }? ? });}
- (void)swiz_viewWillAppear:(BOOL)animated{/
/這時候調用自己,看起來像是死循環//但是其實自己的實現已經被替換了
[self ?swiz_viewWillAppear:animated];
NSLog(@"swizzle");}
@end
在一個自己定義的viewController中重寫viewWillAppear
- (void)viewWillAppear:(BOOL)animated{
? ? [super viewWillAppear:animated];? ?
? ? NSLog(@"viewWillAppear");
}
二、設置關聯值
使用場景:現在你準備用一個系統的類,但是系統的類并不能滿足你的需求,你需要額外添加一個屬性。給一個類聲明屬性,其實本質就是給這個類添加關聯,并不是直接把這個值的內存空間添加到類存空間。分類只能添加方法
1.設置關聯值
這種情況的一般解決辦法就是繼承。
但是,只增加一個屬性,就去繼承一個類,總是覺得太麻煩類。
這個時候,runtime的關聯屬性就發揮它的作用了。
1、添加關聯對象
-(void)addAssociatedObject:(id)object{objc_setAssociatedObject(self,@selector(getAssociatedObject), object, OBJC_ASSOCIATION_RETAIN_NONATOMIC);}//獲取關聯對象-(id)getAssociatedObject{returnobjc_getAssociatedObject(self, _cmd);}
注意:這里面我們把getAssociatedObject方法的地址作為唯一的key,_cmd代表當前調用方法的地址。
參數說明:
object:與誰關聯,通常是傳self
key:唯一鍵,在獲取值時通過該鍵獲取,通常是使用static
const void *來聲明
value:關聯所設置的值
policy:內存管理策略,比如使用copy
voidobjc_setAssociatedObject(idobject,constvoid*key, idvalue, objc _AssociationPolicy policy)
2.獲取關聯值
參數說明:
object:與誰關聯,通常是傳self,在設置關聯時所指定的與哪個對象關聯的那個對象
key:唯一鍵,在設置關聯時所指定的鍵
idobjc_getAssociatedObject(idobject,constvoid*key)
3.取消關聯
voidobjc_removeAssociatedObjects(idobject)
關聯策略
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy){OBJC_ASSOCIATION_ASSIGN =0,// 表示弱引用關聯,通常是基本數據類型OBJC_ASSOCIATION_RETAIN_NONATOMIC =1,// 表示強引用關聯對象,是線程安全的OBJC_ASSOCIATION_COPY_NONATOMIC =3,// 表示關聯對象copy,是線程安全的OBJC_ASSOCIATION_RETAIN =01401,// 表示強引用關聯對象,不是線程安全的OBJC_ASSOCIATION_COPY =01403// 表示關聯對象copy,不是線程安全的};
@implementationViewController
- (void)viewDidLoad {?
? [super viewDidLoad];// Do any additional setup after loading the view, typically from a nib.
// 給系統NSObject類動態添加屬性name
NSObject*objc = [[NSObjectalloc] init];?
? objc.name =@"123";NSLog(@"%@",objc.name);}
@end
// 定義關聯的key static const char*key ="name";
@implementationNSObject(Property)
- (NSString*)name{// 根據關聯的key,獲取關聯的值。returnobjc_getAssociatedObject(self, key);}
- (void)setName:(NSString*)name{
// 第一個參數:給哪個對象添加關聯/
/ 第二個參數:關聯的key,通過這個key獲取
// 第三個參數:關聯的value
// 第四個參數:關聯的策略objc_setAssociatedObject(self, key, name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);}
@end
三、動態添加方法
使用場景:如果一個類方法非常多,加載類到內存的時候也比較耗費資源,需要給每個方法生成映射表,可以使用動態給某個類,添加方法解決。
@implementationViewController
- (void)viewDidLoad {?
? [superviewDidLoad];// Do any additional setup after loading the view, typically from a nib.
Person *p = [[Person alloc] init];// 默認person,沒有實現eat方法,可以通過performSelector調用,但是會報錯。
// 動態添加方法就不會報錯[p performSelector:@selector(eat)];}
@end
@implementationPerson//
void(*)()// 默認方法都有兩個隱式參數,
void eat(idself,SEL sel){NSLog(@"%@ %@",self,NSStringFromSelector(sel));}
// 當一個對象調用未實現的方法,會調用這個方法處理,并且會把對應的方法列表傳過來.// 剛好可以用來判斷,未實現的方法是不是我們想要動態添加的方法
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if(sel ==@selector(eat)) {
// 動態添加eat方法// 第一個參數:給哪個類添加方法
// 第二個參數:添加方法的方法編號
// 第三個參數:添加方法的函數實現(函數地址)
// 第四個參數:函數的類型,(返回值+參數類型) v:void @:對象->self :表示SEL->_cmdclass_addMethod(self,@selector(eat), eat,"v@:");? ? }return[superresolveInstanceMethod:sel];}
@end
四、字典轉模型
設計模型
模型屬性,通常需要跟字典中的key一一對應
問題:一個一個的生成模型屬性,很慢?
需求:能不能自動根據一個字典,生成對應的屬性。
解決:提供一個分類,專門根據字典生成對應的屬性字符串。
@implementation ?NSObject(Log)// 自動打印屬性字符串
+ (void)resolveDict:(NSDictionary*)dict{
// 拼接屬性字符串代碼NSMutableString*strM = [NSMutableString string];
// 1.遍歷字典,把字典中的所有key取出來,生成對應的屬性代碼
[dict enumerateKeysAndObjectsUsingBlock:^(id_Nonnull key,id_Nonnull obj,BOOL* _Nonnull stop) {
// 類型經常變,抽出來
NSString*type;
if([obj isKindOfClass:NSClassFromString(@"__NSCFString")]) {? ?
? ? ? ? type =@"NSString";? ? ? ? }
else if([obj isKindOfClass:NSClassFromString(@"__NSCFArray")]){?
? ? ? ? ? type =@"NSArray";? ? ? ? }
else if([obj isKindOfClass:NSClassFromString(@"__NSCFNumber")]){? ? ? ?
? ? type =@"int";? ? ? ? }
else if([obj isKindOfClass:NSClassFromString(@"__NSCFDictionary")]){? ? ? ?
? ? type =@"NSDictionary";? ? ? ? }
// 屬性字符串NSString*str;
if([type containsString:@"NS"]) {? ?
? str = [NSStringstringWithFormat:@"@property (nonatomic, strong) %@ *%@;",type,key];? ? ? ? }
else{? ? ? ? ? ?
str = [NSStringstringWithFormat:@"@property (nonatomic, assign) %@ %@;",type,key];? ? ? ? }
// 每生成屬性字符串,就自動換行。
[strM appendFormat:@"\n%@\n",str];? ? }];
// 把拼接好的字符串打印出來,就好了。NSLog(@"%@",strM);}
@end
字典轉模型的方式一:KVC
@implementation Status
+ (instancetype)statusWithDict:(NSDictionary *)dict{
?Status*status= [[self alloc] init];?
? [status setValuesForKeysWithDictionary:dict];
return ?status;
}
@end
KVC字典轉模型弊端:必須保證,模型中的屬性和字典中的key一一對應。
如果不一致,就會調用[ setValue:forUndefinedKey:]
報key找不到的錯。
分析:模型中的屬性和字典的key不一一對應,系統就會調用setValue:forUndefinedKey:報錯。
解決:重寫對象的setValue:forUndefinedKey:,把系統的方法覆蓋,
就能繼續使用KVC,字典轉模型了。
-(void)setValue:(id)valueforUndefinedKey:(NSString*)key{}
字典轉模型的方式二:Runtime
思路:利用運行時,遍歷模型中所有屬性,根據模型的屬性名,去字典中查找key,取出對應的值,給模型的屬性賦值。
步驟:提供一個NSObject分類,專門字典轉模型,以后所有模型都可以通過這個分類轉。
@implementation ?ViewController
- (void)viewDidLoad {? ?
[superviewDidLoad];// Do any additional setup after loading the view, typically from a nib.//
解析Plist文件
NSString*filePath = [[NSBundlemainBundle]pathForResource:@"status.plist"ofType:nil];
NSDictionary*statusDict = [NSDictionary ?dictionaryWithContentsOfFile:filePath];
// 獲取字典數組NSArray*dictArr = statusDict[@"statuses"];
// 自動生成模型的屬性字符串//?
? [NSObject resolveDict:dictArr[0][@"user"]];
_statuses = [NSMutableArray array];/
/ 遍歷字典數組
for(NSDictionary*dictindict Arr)
{? ? ? ? Status *status = [Status modelWithDict:dict];? ? ?
? [_statuses addObject:status];? ?
}
// 測試數據NSLog(@"%@ %@",_statuses,[_statuses[0] user]);}
@end
@implementation ?NSObject(Model)
+ (instancetype)modelWithDict:(NSDictionary*)dict
{
// 思路:遍歷模型中所有屬性-》使用運行時
// 0.創建對應的對象id objc = [[selfalloc] init];
// 1.利用runtime給對象中的成員屬性賦值
// class_copyIvarList:獲取類中的所有成員屬性
// Ivar:成員屬性的意思
// 第一個參數:表示獲取哪個類中的成員屬性
// 第二個參數:表示這個類有多少成員屬性,傳入一個Int變量地址,會自動給這個變量賦值
// 返回值Ivar *:指的是一個ivar數組,會把所有成員屬性放在一個數組中,通過返回的數組就能全部獲取到。/* 類似下面這種寫法
Ivar ivar;
Ivar ivar1;
Ivar ivar2;
// 定義一個ivar的數組a
Ivar a[] = {ivar,ivar1,ivar2};
// 用一個Ivar *指針指向數組第一個元素
Ivar *ivarList = a;
// 根據指針訪問數組第一個元素
ivarList[0];
*/unsignedintcount;
// 獲取類中的所有成員屬性Ivar *ivarList = class_copyIvarList(self, &count);
for(inti =0; i < count; i++) {
// 根據角標,從數組取出對應的成員屬性Ivar ivar = ivarList[i];
// 獲取成員屬性名
NSString*name = [NSString stringWithUTF8String:ivar_getName(ivar)];
// 處理成員屬性名->字典中的key
// 從第一個角標開始截取
NSString*key = [name substringFromIndex:1];
// 根據成員屬性名去字典中查找對應的value
id value = dict[key];
// 二級轉換:如果字典中還有字典,也需要把對應的字典轉換成模型
// 判斷下value是否是字典
if([value isKindOfClass:[NSDictionaryclass]]) {
// 字典轉模型// 獲取模型的類對象,調用modelWithDict
// 模型的類名已知,就是成員屬性的類型
// 獲取成員屬性類型
NSString*type = [NSString ?stringWithUTF8String:ivar_getTypeEncoding(ivar)];
// 生成的是這種@"@\"User\"" 類型 -》 @"User"? 在OC字符串中 \" -> ",\是轉義的意思,不占用字符// 裁剪類型字符串NSRangerange = [type rangeOfString:@"\""];? ? ?
? ? type = [type substringFromIndex:range.location + range.length];? ? ?
? ? ? range = [type rangeOfString:@"\""];// 裁剪到哪個角標,不包括當前角標type = [type substringToIndex:range.location];
// 根據字符串類名生成類對象
Class modelClass =NSClassFromString(type);
if(modelClass) {
// 有對應的模型才需要轉
// 把字典轉模型value? =? [modelClass modelWithDict:value];? ? ? ? ? ? }? ? ? ? }
// 三級轉換:NSArray中也是字典,把數組中的字典轉換成模型
// 判斷值是否是數組
if([value isKindOfClass:[NSArrayclass]]) {
// 判斷對應類有沒有實現字典數組轉模型數組的協議
if([self respondsToSelector:@selector(arrayContainModelClass)]) {/
/ 轉換成id類型,就能調用任何對象的方法id
id Self =self;
// 獲取數組中字典對應的模型
NSString*type =? [id ?Self arrayContainModelClass][key];
// 生成模型
Class classModel =NSClassFromString(type);
NSMutableArray*arrM = [NSMutableArray ?array];
// 遍歷字典數組,生成模型數組for(NSDictionary*dict in value) {
// 字典轉模型id model =? [classModel modelWithDict:dict];? ? ? ? ? ? ?
? ?[arrM addObject:model];? ? ? ? ? ? ? ? }
// 把模型數組賦值給value ?value = arrM;? ? ? ? ? ? }? ? ? ? }
if(value) {
// 有值,才需要給模型的屬性賦值
// 利用KVC給模型中的屬性賦值[objc setValue:value forKey:key];? ? ? ? }? ?
}return ? objc;}
@end
五、參考文章
http://www.lxweimin.com/p/e071206103a4
http://www.lxweimin.com/p/adf0d566c887
http://www.lxweimin.com/p/927c8384855a
http://chun.tips/2014/11/05/objc-runtime-1/#more
http://blog.sunnyxx.com/2016/08/13/reunderstanding-runtime-0/
http://blog.csdn.net/wzzvictory/article/details/8624057
http://www.cocoachina.com/ios/20151208/14595.html
http://www.lxweimin.com/p/46dd81402f63
六、后記
說實話,剛開始做開發是后,也一直聽說runtime,但項目中用的少,直到最近項目不是太忙,才重新看看蘋果的runtime機制,有一種驀然回首,茅塞頓開的感覺,學會runtime,一是可以更好的幫助你理解OC的運行機制。還有一點就是可以裝-B