Runtime-iOS運行時應用
本篇將會總結Rutime的具體應用實體,結合其動態特性,Runtime在開發中的應用大致分為以下幾個方面(Runtime應用圖):

相關文章:iOS運行時Runtime基礎
一、動態方法交換:Method Swizzling
實作動態方法交換(Method Swizzling )是Runtime中最具盛名的應用場景,其原理是:通過Runtime獲取到方法實作的地址,進而動態交換兩個方法的功能,使用到關鍵方法如下:
| 1 2 3 4 5 6 | //獲取類方法的Mthod Method _Nullable class_getClassMethod(Class _Nullable cls, SEL _Nonnull name) //獲取實體物件方法的Mthod Method _Nullable class_getInstanceMethod(Class _Nullable cls, SEL _Nonnull name) //交換兩個方法的實作 void method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2) |
1.動態方法交換示例
現在演示一個代碼示例:在視圖控制中,定義兩個實體方法printA與printB,然后執行交換
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | - (void)printA{ NSLog(@"列印A......"); } - (void)printB{ NSLog(@"列印B......"); } //交換方法的實作,并測驗列印 Method methodA = class_getInstanceMethod([self class], @selector(printA)); Method methodB = class_getInstanceMethod([self class], @selector(printB)); method_exchangeImplementations(methodA, methodB); [self printA]; //列印B...... [self printB]; //列印A...... |
2.攔截并替換系統方法
Runtime動態方法交換更多的是應用于系統類別庫和第三方框架的方法替換,在不可見原始碼的情況下,我們可以借助Rutime交換方法實作,為原有方法添加額外功能,這在實際開發中具有十分重要的意義,
下面將展示一個攔截并替換系統方法的示例:為了實作不同機型上的字體都按照比例適配,我們可以攔截系統UIFont的systemFontOfSize方法,具體操作如下:
步驟1:在當前工程中添加UIFont的分類:UIFont +Adapt,并在其中添用以替換的方法,
| 1 2 3 4 5 6 7 8 | + (UIFont *)zs_systemFontOfSize:(CGFloat)fontSize{ //獲取設備螢屏寬度,并計算出比例scale CGFloat width = [[UIScreen mainScreen] bounds].size.width; CGFloat scale = width/375.0; //注意:由于方法交換,系統的方法名已變成了自定義的方法名,所以這里使用了 //自定義的方法名來獲取UIFont return [UIFont zs_systemFontOfSize:fontSize * scale]; } |
步驟2:在UIFont的分類中攔截系統方法,將其替換為我們自定義的方法,代碼如下:
| 1 2 3 4 5 6 7 8 9 | //load方法不需要手動呼叫,iOS會在應用程式啟動的時候自動調起load方法,而且執行時間較早,所以在此方法中執行交換操作比較合適, + (void)load{ //獲取系統方法地址 Method sytemMethod = class_getClassMethod([UIFont class], @selector(systemFontOfSize:)); //獲取自定義方法地址 Method customMethod = class_getClassMethod([UIFont class], @selector(zs_systemFontOfSize:)); //交換兩個方法的實作 method_exchangeImplementations(sytemMethod, customMethod); } |
添加一段測驗代碼,切換不同的模擬器,觀察在不同機型上文字的大小:
| 1 2 3 4 | UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(0, 100, 300, 50)]; label.text = @"測驗Runtime攔截方法"; label.font = [UIFont systemFontOfSize:20]; [self.view addSubview:label]; |
二、實作分類添加新屬性
我們在開發中常常使用類目Category為一些已有的類擴展功能,雖然繼承也能夠為已有類增加新的方法,而且相比類目更是具有增加屬性的優勢,但是繼承畢竟是一個重量級的操作,添加不必要的繼承關系無疑增加了代碼的復雜度,
遺憾的是,OC的類目并不支持直接添加屬性,如果我們直接在分類的宣告中寫入Property屬性,那么只能為其生成set與get方法宣告,卻不能生成成員變數,直接呼叫這些屬性還會造成崩潰,
所以為了實作給分類添加屬性,我們還需借助Runtime的關聯物件(Associated Objects)特性,它能夠幫助我們在運行階段將任意的屬性關聯到一個物件上,下面是相關的三個方法:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | /** 1.給物件設定關聯屬性 @param object 需要設定關聯屬性的物件,即給哪個物件關聯屬性 @param key 關聯屬性對應的key,可通過key獲取這個屬性, @param value 給關聯屬性設定的值 @param policy 關聯屬性的存盤策略(對應Property屬性中的assign,copy,retain等) OBJC_ASSOCIATION_ASSIGN @property(assign), OBJC_ASSOCIATION_RETAIN_NONATOMIC @property(strong, nonatomic), OBJC_ASSOCIATION_COPY_NONATOMIC @property(copy, nonatomic), OBJC_ASSOCIATION_RETAIN @property(strong,atomic), OBJC_ASSOCIATION_COPY @property(copy, atomic), */ void objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key, id _Nullable value, objc_AssociationPolicy policy) /** 2.通過key獲取關聯的屬性 @param object 從哪個物件中獲取關聯屬性 @param key 關聯屬性對應的key @return 回傳關聯屬性的值 */ id _Nullable objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key) /** 3.移除物件所關聯的屬性 @param object 移除某個物件的所有關聯屬性 */ void objc_removeAssociatedObjects(id _Nonnull object) |
注意:key與關聯屬性一一對應,我們必須確保其全域唯一性,常用我們使用@selector(methodName)作為key,
現在演示一個代碼示例:為UIImage增加一個分類:UIImage+Tools,并為其設定關聯屬性urlString(圖片網路鏈接屬性),相關代碼如下:
| 1 2 3 4 5 6 | //UIImage+Tools.h檔案中 UIImage+Tools.m @interface UIImage (Tools) //添加一個新屬性:圖片網路鏈接 @property(nonatomic,copy)NSString *urlString; @end |
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | //UIImage+Tools.m檔案中 #import "UIImage+Tools.h" #import <objc/runtime.h> @implementation UIImage (Tools) //set方法 - (void)setUrlString:(NSString *)urlString{ objc_setAssociatedObject(self, @selector(urlString), urlString, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } //get方法 - (NSString *)urlString{ return objc_getAssociatedObject(self, @selector(urlString)); } //添加一個自定義方法,用于清除所有關聯屬性 - (void)clearAssociatedObjcet{ objc_removeAssociatedObjects(self); } @end |
測驗檔案中:
| 1 2 3 4 5 6 7 8 9 | UIImage *image = [[UIImage alloc] init]; image.urlString = @"http://www.image.png"; NSLog(@"獲取關聯屬性:%@",image.urlString); [image clearAssociatedObjcet]; NSLog(@"獲取關聯屬性:%@",image.urlString); //列印: //獲取關聯屬性:http://www.image.png // 獲取關聯屬性:(null) |
三、獲取類的詳細資訊
1.獲取屬性串列
| 1 2 3 4 5 6 7 | unsigned int count; objc_property_t *propertyList = class_copyPropertyList([self class], &count); for (unsigned int i = 0; i<count; i++) { const char *propertyName = property_getName(propertyList[i]); NSLog(@"PropertyName(%d): %@",i,[NSString stringWithUTF8String:propertyName]); } free(propertyList); |
2.獲取所有成員變數
| 1 2 3 4 5 6 7 | Ivar *ivarList = class_copyIvarList([self class], &count); for (int i= 0; i<count; i++) { Ivar ivar = ivarList[i]; const char *ivarName = ivar_getName(ivar); NSLog(@"Ivar(%d): %@", i, [NSString stringWithUTF8String:ivarName]); } free(ivarList); |
3.獲取所有方法
| 1 2 3 4 5 6 7 | Method *methodList = class_copyMethodList([self class], &count); for (unsigned int i = 0; i<count; i++) { Method method = methodList[i]; SEL mthodName = method_getName(method); NSLog(@"MethodName(%d): %@",i,NSStringFromSelector(mthodName)); } free(methodList); |
4.獲取當前遵循的所有協議
| 1 2 3 4 5 6 7 | __unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count); for (int i=0; i<count; i++) { Protocol *protocal = protocolList[i]; const char *protocolName = protocol_getName(protocal); NSLog(@"protocol(%d): %@",i, [NSString stringWithUTF8String:protocolName]); } free(propertyList); |
注意:C語言中使用Copy操作的方法,要注意釋放指標,防止記憶體泄漏
四、解決同一方法高頻率呼叫的效率問題
Runtime原始碼中的IMP作為函式指標,指向方法的實作,通過它,我們可以繞開發送訊息的程序來提高函式呼叫的效率,當我們需要持續大量重復呼叫某個方法的時候,會十分有用,具體代碼示例如下:
| 1 2 3 4 5 6 | void (*setter)(id, SEL, BOOL); int i; setter = (void (*)(id, SEL, BOOL))[target methodForSelector:@selector(setFilled:)]; for ( i = 0 ; i < 1000 ; i++ ) setter(targetList[i], @selector(setFilled:), YES); |
五、方法動態決議與訊息轉發
其實該部分可以參考基礎篇中內容,這里不再重復贅述,只是大概做出一些總結,
1.動態方法決議:動態添加方法
Runtime足夠強大,能夠讓我們在運行時動態添加一個未實作的方法,這個功能主要有兩個應用場景:
場景1:動態添加未實作方法,解決代碼中因為方法未找到而報錯的問題;
場景2:利用懶加載思路,若一個類有很多個方法,同時加載到記憶體中會耗費資源,可以使用動態決議添加方法,方法動態決議主要用到的方法如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | //OC方法: //類方法未找到時調起,可于此添加類方法實作 + (BOOL)resolveClassMethod:(SEL)sel //實體方法未找到時調起,可于此添加實體方法實作 + (BOOL)resolveInstanceMethod:(SEL)sel //Runtime方法: /** 運行時方法:向指定類中添加特定方法實作的操作 @param cls 被添加方法的類 @param name selector方法名 @param imp 指向實作方法的函式指標 @param types imp函式實作的回傳值與引數型別 @return 添加方法是否成功 */ BOOL class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types) |
2.解決方法無回應崩潰問題
執行OC方法其實就是一個發送訊息的程序,若方法未實作,我們可以利用方法動態決議與訊息轉發來避免程式崩潰,這主要涉及下面一個處理未實作訊息的程序:
訊息轉發流程圖:
除了上述的方法動態決議,還使用到的相關方法如下:
訊息接收者重定向
| 1 2 3 4 5 | //重定向類方法的訊息接收者,回傳一個類 - (id)forwardingTargetForSelector:(SEL)aSelector //重定向實體方法的訊息接受者,回傳一個實體物件 - (id)forwardingTargetForSelector:(SEL)aSelector |
訊息重定向
| 1 2 3 | - (void)forwardInvocation:(NSInvocation *)anInvocation; - (NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector; |
六、動態操作屬性
1.動態修改屬性變數
現在假設這樣一個情況:我們使用第三方框架里的Person類,在特殊需求下想要更改其私有屬性nickName,這樣的操作我們就可以使用Runtime可以動態修改物件屬性,
基本思路:首先使用Runtime獲取Peson物件的所有屬性,找到nickName,然后使用ivar的方法修改其值,具體的代碼示例如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | Person *ps = [[Person alloc] init]; NSLog(@"ps-nickName: %@",[ps valueForKey:@"nickName"]); //null //第一步:遍歷物件的所有屬性 unsigned int count; Ivar *ivarList = class_copyIvarList([ps class], &count); for (int i= 0; i<count; i++) { //第二步:獲取每個屬性名 Ivar ivar = ivarList[i]; const char *ivarName = ivar_getName(ivar); NSString *propertyName = [NSString stringWithUTF8String:ivarName]; if ([propertyName isEqualToString:@"_nickName"]) { //第三步:匹配到對應的屬性,然后修改;注意屬性帶有下劃線 object_setIvar(ps, ivar, @"梧雨北辰"); } } NSLog(@"ps-nickName: %@",[ps valueForKey:@"nickName"]); // |
總結:此程序類似KVC的取值和賦值
2.實作 NSCoding 的自動歸檔和解檔
歸檔是一種常用的輕量型檔案存盤方式,但是它有個弊端:在歸檔程序中,若一個Model有多個屬性,我們不得不對每個屬性進行處理,非常繁瑣,
歸檔操作主要涉及兩個方法:encodeObject 和 decodeObjectForKey,現在,我們可以利用Runtime來改進它們,關鍵的代碼示例如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | //原理:使用Runtime動態獲取所有屬性 //解檔操作 - (instancetype)initWithCoder:(NSCoder *)aDecoder{ self = [super init]; if (self) { unsigned int count = 0; Ivar *ivarList = class_copyIvarList([self class], &count); for (int i = 0; i < count; i++) { Ivar ivar = ivarList[i]; const char *ivarName = ivar_getName(ivar); NSString *key = [NSString stringWithUTF8String:ivarName]; id value = [aDecoder decodeObjectForKey:key]; [self setValue:value forKey:key]; } free(ivarList); //釋放指標 } return self; } //歸檔操作 - (void)encodeWithCoder:(NSCoder *)aCoder{ unsigned int count = 0; Ivar *ivarList = class_copyIvarList([self class], &count); for (NSInteger i = 0; i < count; i++) { Ivar ivar = ivarList[i]; NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)]; id value = [self valueForKey:key]; [aCoder encodeObject:value forKey:key]; } free(ivarList); //釋放指標 } |
下面是有關歸檔的測驗代碼:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | //--測驗歸檔 Person *ps = [[Person alloc] init]; ps.name = @"梧雨北辰"; ps.age = 18; NSString *temp = NSTemporaryDirectory(); NSString *fileTemp = [temp stringByAppendingString:@"person.archive"]; [NSKeyedArchiver archiveRootObject:ps toFile:fileTemp]; //--測驗解檔 NSString *temp = NSTemporaryDirectory(); NSString *fileTemp = [temp stringByAppendingString:@"person.henry"]; Person *person = [NSKeyedUnarchiver unarchiveObjectWithFile:fileTemp]; NSLog(@"person-name:%@,person-age:%ld",person.name,person.age); //person-name:梧雨北辰,person-age:18 |
3.實作字典與模型的轉換
字典資料轉模型的操作在專案開發中很常見,通常我們會選擇第三方如YYModel;其實我們也可以自己來實作這一功能,主要的思路有兩種:KVC、Runtime,總結字典轉化模型程序中需要解決的問題如下(字典轉模型圖):

現在,我們使用Runtime來實作字典轉模型的操作,大致的思路是這樣:
借助Runtime可以動態獲取成員串列的特性,遍歷模型中所有屬性,然后以獲取到的屬性名為key,在JSON字典中尋找對應的值value;再將每一個對應Value賦值給模型,就完成了字典轉模型的目的,
首先準備下面的JSON資料用于測驗:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | { "id":"2462079046", "name": "梧雨北辰", "age":"18", "weight":140, "address":{ "country":"中國", "province": "河南" }, "courses":[{ "name":"Chinese", "desc":"幼ò肝" },{ "name":"Math", "desc":"數學課" },{ "name":"English", "desc":"英語課" } ] } |
具體的代碼實作流程如下:
步驟1:創建NSObject的類目NSObject+ZSModel,用于實作字典轉模型
| 1 2 3 4 5 6 7 8 9 | @interface NSObject (ZSModel) + (instancetype)zs_modelWithDictionary:(NSDictionary *)dictionary; @end //ZSModel協議,協議方法可以回傳一個字典,表明特殊欄位的處理規則 @protocol ZSModel<NSObject> @optional + (nullable NSDictionary<NSString *, id> *)modelContainerPropertyGenericClass; @end; |
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | #import "NSObject+ZSModel.h" #import <objc/runtime.h> @implementation NSObject (ZSModel) + (instancetype)zs_modelWithDictionary:(NSDictionary *)dictionary{ //創建當前模型物件 id object = [[self alloc] init]; //1.獲取當前物件的成員變數串列 unsigned int count = 0; Ivar *ivarList = class_copyIvarList([self class], &count); //2.遍歷ivarList中所有成員變數,以其屬性名為key,在字典中查找Value for (int i= 0; i<count; i++) { //2.1獲取成員屬性 Ivar ivar = ivarList[i]; NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)] ; //2.2截取成員變數名:去掉成員變數前面的"_"號 NSString *propertyName = [ivarName substringFromIndex:1]; //2.3以屬性名為key,在字典中查找value id value = dictionary[propertyName]; //3.獲取成員變數型別, 因為ivar_getTypeEncoding獲取的型別是"@\"NSString\""的形式 //所以我們要做以下的替換 NSString *ivarType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];// 替換: //3.1去除轉義字符:@\"name\" -> @"name" ivarType = [ivarType stringByReplacingOccurrencesOfString:@"\"" withString:@""]; //3.2去除@符號 ivarType = [ivarType stringByReplacingOccurrencesOfString:@"@" withString:@""]; //4.對特殊成員變數進行處理: //判斷當前類是否實作了協議方法,獲取協議方法中規定的特殊變數的處理方式 NSDictionary *perpertyTypeDic; if([self respondsToSelector:@selector(modelContainerPropertyGenericClass)]){ perpertyTypeDic = [self performSelector:@selector(modelContainerPropertyGenericClass) withObject:nil]; } //4.1處理:字典的key與模型屬性不匹配的問題,如id->uid id anotherName = perpertyTypeDic[propertyName]; if(anotherName && [anotherName isKindOfClass:[NSString class]]){ value = dictionary[anotherName]; } //4.2.處理:模型嵌套模型 if ([value isKindOfClass:[NSDictionary class]] && ![ivarType hasPrefix:@"NS"]) { Class modelClass = NSClassFromString(ivarType); if (modelClass != nil) { //將被嵌套字典資料也轉化成Model value = [modelClass zs_modelWithDictionary:value]; } } //4.3處理:模型嵌套模型陣列 //判斷當前Vaue是一個陣列,而且存在協議方法回傳了perpertyTypeDic if ([value isKindOfClass:[NSArray class]] && perpertyTypeDic) { Class itemModelClass = perpertyTypeDic[propertyName]; //封裝陣列:將每一個子資料轉化為Model NSMutableArray *itemArray = @[].mutableCopy; for (NSDictionary *itemDic in value) { id model = [itemModelClass zs_modelWithDictionary:itemDic]; [itemArray addObject:model]; } value = itemArray; } //5.使用KVC方法將Vlue更新到object中 if (value != nil) { [object setValue:value forKey:propertyName]; } } free(ivarList); //釋放C指標 return object; } @end |
步驟2:分別創建各個資料模型Student、Address、Course
Student類:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | //Student.h檔案 #import "NSObject+ZSModel.h" #import "AddressModel.h" #import "CourseModel.h" @interface StudentModel : NSObject<ZSModel> //遵循協議 //普通屬性 @property (nonatomic, copy) NSString *uid; @property(nonatomic,copy)NSString *name; @property (nonatomic, assign) NSInteger age; //嵌套模型 @property (nonatomic, strong) AddressModel *address; //嵌套模型陣列 @property (nonatomic, strong) NSArray *courses; @end |
| 1 2 3 4 5 6 7 | #import "StudentModel.h" @implementation StudentModel + (NSDictionary *)modelContainerPropertyGenericClass { //需要特別處理的屬性 return @{@"courses" : [CourseModel class],@"uid":@"id"}; } @end |
Address類:
| 1 2 3 4 5 6 7 8 9 10 11 12 | //AddressModel.h檔案 @interface AddressModel : NSObject @property (nonatomic, copy) NSString *country; //國籍 @property (nonatomic, copy) NSString *province; //省份 @property (nonatomic, copy) NSString *city; //城市 @end //-----------------優美的分割線------------------------ //AddressModel.m檔案 #import "AddressModel.h" @implementation AddressModel @end |
Course類:
| 1 2 3 4 5 6 7 8 | //讀取JSON資料 NSDictionary *jsonData = [FileTools getDictionaryFromJsonFile:@"Student"]; NSLog(@"%@",jsonData); //字典轉模型 StudentModel *student = [StudentModel zs_modelWithDictionary:jsonData]; CourseModel *courseModel = student.courses[0]; NSLog(@"%@",courseModel.name); |
步驟4:測驗字典轉模型操作
| 1 2 3 4 5 6 7 8 | //讀取JSON資料 NSDictionary *jsonData = [FileTools getDictionaryFromJsonFile:@"Student"]; NSLog(@"%@",jsonData); //字典轉模型 StudentModel *student = [StudentModel zs_modelWithDictionary:jsonData]; CourseModel *courseModel = student.courses[0]; NSLog(@"%@",courseModel.name); |
效果如下(測驗字典轉模型操作圖):
