Runtime-iOS執行時應用篇
在上篇文章iOS執行時Runtime基礎後,本篇將會總結Rutime的具體應用例項,結合其動態特性,Runtime在開發中的應用大致分為以下幾個方面:
一、動態方法交換:Method Swizzling
實現動態方法交換(Method Swizzling )是Runtime中最具盛名的應用場景,其原理是:通過Runtime獲取到方法實現的地址,進而動態交換兩個方法的功能。使用到關鍵方法如下:
//獲取類方法的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,然後執行交換
- (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,並在其中添用以替換的方法。
+ (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的分類中攔截系統方法,將其替換為我們自定義的方法,程式碼如下:
//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);
}
新增一段測試程式碼,切換不同的模擬器,觀察在不同機型上文字的大小:
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.給物件設定關聯屬性
@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(圖片網路連結屬性),相關程式碼如下:
//UIImage+Tools.h檔案中
UIImage+Tools.m
@interface UIImage (Tools)
//新增一個新屬性:圖片網路連結
@property(nonatomic,copy)NSString *urlString;
@end
//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
測試檔案中:
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.獲取屬性列表
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.獲取所有成員變數
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.獲取所有方法
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.獲取當前遵循的所有協議
__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作為函式指標,指向方法的實現。通過它,我們可以繞開傳送訊息的過程來提高函式呼叫的效率。當我們需要持續大量重複呼叫某個方法的時候,會十分有用,具體程式碼示例如下:
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:利用懶載入思路,若一個類有很多個方法,同時載入到記憶體中會耗費資源,可以使用動態解析新增方法。方法動態解析主要用到的方法如下:
//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方法其實就是一個傳送訊息的過程,若方法未實現,我們可以利用方法動態解析與訊息轉發來避免程式崩潰,這主要涉及下面一個處理未實現訊息的過程:
除了上述的方法動態解析,還使用到的相關方法如下:
訊息接收者重定向
//重定向類方法的訊息接收者,返回一個類
- (id)forwardingTargetForSelector:(SEL)aSelector
//重定向例項方法的訊息接受者,返回一個例項物件
- (id)forwardingTargetForSelector:(SEL)aSelector
訊息重定向
- (void)forwardInvocation:(NSInvocation *)anInvocation;
- (NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector;
六、動態操作屬性
1.動態修改屬性變數
現在假設這樣一個情況:我們使用第三方框架裡的Person類,在特殊需求下想要更改其私有屬性nickName,這樣的操作我們就可以使用Runtime可以動態修改物件屬性。
基本思路:首先使用Runtime獲取Peson物件的所有屬性,找到nickName,然後使用ivar的方法修改其值。具體的程式碼示例如下:
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來改進它們,關鍵的程式碼示例如下:
//原理:使用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); //釋放指標
}
下面是有關歸檔的測試程式碼:
//--測試歸檔
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資料用於測試:
{
"id":"2462079046",
"name": "梧雨北辰",
"age":"18",
"weight":140,
"address":{
"country":"中國",
"province": "河南"
},
"courses":[{
"name":"Chinese",
"desc":"語文課"
},{
"name":"Math",
"desc":"數學課"
},{
"name":"English",
"desc":"英語課"
}
]
}
具體的程式碼實現流程如下:
步驟1:建立NSObject的類目NSObject+ZSModel,用於實現字典轉模型
@interface NSObject (ZSModel)
+ (instancetype)zs_modelWithDictionary:(NSDictionary *)dictionary;
@end
//ZSModel協議,協議方法可以返回一個字典,表明特殊欄位的處理規則
@protocol ZSModel<NSObject>
@optional
+ (nullable NSDictionary<NSString *, id> *)modelContainerPropertyGenericClass;
@end;
#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類:
//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
#import "StudentModel.h"
@implementation StudentModel
+ (NSDictionary *)modelContainerPropertyGenericClass {
//需要特別處理的屬性
return @{@"courses" : [CourseModel class],@"uid":@"id"};
}
@end
Address類:
//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類:
//讀取JSON資料
NSDictionary *jsonData = [FileTools getDictionaryFromJsonFile:@"Student"];
NSLog(@"%@",jsonData);
//字典轉模型
StudentModel *student = [StudentModel zs_modelWithDictionary:jsonData];
CourseModel *courseModel = student.courses[0];
NSLog(@"%@",courseModel.name);
步驟4:測試字典轉模型操作
//讀取JSON資料
NSDictionary *jsonData = [FileTools getDictionaryFromJsonFile:@"Student"];
NSLog(@"%@",jsonData);
//字典轉模型
StudentModel *student = [StudentModel zs_modelWithDictionary:jsonData];
CourseModel *courseModel = student.courses[0];
NSLog(@"%@",courseModel.name);
效果如下:
最後總結
以上就是我們在實際開發中常用的Runtime的操作了,Runtime的強大作用遠不止如此。深入的瞭解和學習Runtime,不僅僅有助於iOS開發,而且對於理解程式語言的底層原理也十分有用,Keep Learning!~
參考連結:
1.Objective-C Runtime Programming Guide
2.Method Swizzling
3.iOS資料持久化儲存:歸檔
4.YYModel原始碼
相關文章
- HttpRuntime應用程式的執行時HTTP
- 【譯】Vue 的小奇技(第二篇):衡量 Vue 應用的執行時效能Vue
- 分散式應用執行時 Dapr 1.7 釋出分散式
- 執行時應用自我保護(RASP):應用安全的自我修養
- iOS runtime執行時的作用和應用場景iOS
- 多執行緒應用執行緒
- 應用執行時 Layotto 進入 CNCF 雲原生全景圖
- 自適應查詢執行:在執行時提升Spark SQL執行效能SparkSQL
- 釋出.NET應用程式,不單獨安裝執行時
- 雲原生應用程式執行時 Kyma 的主要特性介紹
- 關於 Angular Universal 應用執行時需要 Browser API 的問題AngularAPI
- 使用SAP BSP應用執行VueVue
- kubernetes執行應用1之Deployment
- 在 OpenFunction 中執行 Serverless 應用FunctionServer
- 利用神器BTrace 追蹤線上 Spring Boot應用執行時資訊Spring Boot
- Hummingbird: 在Web上執行Flutter應用WebFlutter
- 可本地執行大模型的應用大模型
- APC 篇—— APC 執行
- 多個 Laravel 應用 queue 佇列執行時會互串的問題Laravel佇列
- 關於Vulkan應用程式執行時編譯GLSL Shader檔案的方法編譯
- Flink 批作業的執行時自適應執行管控
- 【JVM之記憶體與垃圾回收篇】執行時資料區概述及執行緒JVM記憶體執行緒
- 多執行緒應用初探(一)----(概念,安全)執行緒
- MapReduce如何作為Yarn應用程式執行?Yarn
- Docker容器中執行.Net Core應用程式Docker
- kubernetes執行應用2之DaemonSet詳解
- NCF的Dapr應用例項的執行
- 在 WASI 上執行 .NET 7 應用程式
- PlayCover for Mac(全屏執行ios應用軟體)MaciOS
- Java執行緒篇——執行緒的開啟Java執行緒
- 程式執行緒篇——執行緒切換(上)執行緒
- 程式執行緒篇——執行緒切換(下)執行緒
- 程式執行緒篇——程式執行緒基礎執行緒
- SAP Fiori Elements 應用的 manifest.json 檔案執行時如何被解析的JSON
- 【Azure 應用服務】一個 App Service 同時部署執行兩個及多個 Java 應用程式(Jar包)APPJavaJAR
- RocketMQ - 應用篇MQ
- SpringSecurity應用篇SpringGse
- 釋出 .NET 5 帶執行時單檔案應用時優化檔案體積的方法優化