本文用來介紹 iOS 開發中,如何通過『Runtime』獲取類詳細屬性、方法。通過本文,您將瞭解到:
- 獲取類詳細屬性、方法簡述
- 獲取類詳細屬性、方法(成員變數列表、屬性列表、方法列表、所遵循的協議列表)
- 應用場景 3.1 修改私有屬性 3.2 萬能控制器跳轉 3.3 實現字典轉模型 3.4 改進 iOS 歸檔和解檔
文中示例程式碼在: bujige / YSC-Class-DetailList-Demo
1. 獲取類詳細屬性、方法簡述
在蘋果官方為我們提供的類中,只能獲取一小部分公開的屬性和方法。有些我們恰好需要的屬性和方法,可能會被官方隱藏了起來,沒有直接提供給我們。
那應該如何才能獲取一個類中所有的變數和方法,用來查詢是否有對我們有用的變數和方法呢?
幸好 Runtime 中為我們提供了一系列 API 來獲取 Class (類)的 成員變數( Ivar )、屬性( Property )、方法( Method )、協議( Protocol ) 等。我們可以通過這些方法來遍歷一個類中的成員變數列表、屬性列表、方法列表、協議列表。從而查詢我們需要的變數和方法。
比如說遇到這樣一個需求:更改 UITextField 佔位文字的顏色和字號。實現程式碼參考 3.1 修改私有屬性 中的例子。
下面我們先來講解一下如何通過程式碼獲取類詳細屬性、方法。
2. 獲取類詳細屬性、方法
注意:標頭檔案中需引入
#import <objc/runtime.h>
。
2.1 獲取類的成員變數列表
// 列印成員變數列表
- (void)printIvarList {
unsigned int count;
Ivar *ivarList = class_copyIvarList([self class], &count);
for (unsigned int i = 0; i < count; i++) {
Ivar myIvar = ivarList[i];
const char *ivarName = ivar_getName(myIvar);
NSLog(@"ivar(%d) : %@", i, [NSString stringWithUTF8String:ivarName]);
}
free(ivarList);
}
複製程式碼
2.2 獲取類的屬性列表
// 列印屬性列表
- (void)printPropertyList {
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.3 獲取類的方法列表
// 列印方法列表
- (void)printMethodList {
unsigned int count;
Method *methodList = class_copyMethodList([self class], &count);
for (unsigned int i = 0; i < count; i++) {
Method method = methodList[i];
NSLog(@"method(%d) : %@", i, NSStringFromSelector(method_getName(method)));
}
free(methodList);
}
複製程式碼
2.4 獲取類所遵循的協議列表
// 列印協議列表
- (void)printProtocolList {
unsigned int count;
__unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);
for (unsigned int i = 0; i < count; i++) {
Protocol *myProtocal = protocolList[i];
const char *protocolName = protocol_getName(myProtocal);
NSLog(@"protocol(%d) : %@", i, [NSString stringWithUTF8String:protocolName]);
}
free(protocolList);
}
複製程式碼
3. 應用場景
3.1 修改私有屬性
需求:更改 UITextField 佔位文字的顏色和字號
先來想想又幾種做法:
方法 1:通過 attributedPlaceholder 屬性修改
我們知道 UITextField 中有 placeholder 屬性和 attributedPlaceholder 屬性。通過 placeholder 屬性只能更改佔位文字,無法修改佔位文字的字型和顏色。而通過 attributedPlaceholder 屬性我們就可以修改 UITextField 佔位文字的顏色和字號了。
方法 2:重寫 UITextField 的 drawPlaceholderInRect: 方法修改
實現步驟:
- 自定義一個 XXTextField 繼承自 UITextField;
- 重寫自定義 XXTextField 的 drawPlaceholderInRect: 方法;
- 在 drawPlaceholderInRect 方法中設定 placeholder 的屬性。
- (void)drawPlaceholderInRect:(CGRect)rect {
// 計算佔位文字的 Size
NSDictionary *attributes = @{
NSForegroundColorAttributeName : [UIColor lightGrayColor],
NSFontAttributeName : [UIFont systemFontOfSize:15]
};
CGSize placeholderSize = [self.placeholder sizeWithAttributes:attributes];
[self.placeholder drawInRect:CGRectMake(0, (rect.size.height - placeholderSize.height)/2, rect.size.width, rect.size.height) withAttributes: attributes];
}
複製程式碼
方法 3:利用 Runtime,找到並修改 UITextfield 的私有屬性
實現步驟:
- 通過獲取類的屬性列表和成員變數列表的方法列印 UITextfield 所有屬性和成員變數;
- 找到私有的成員變數
_placeholderLabel
; - 利用 KVC 對
_placeholderLabel
進行修改。
// 列印 UITextfield 的所有屬性和成員變數
- (void)printUITextFieldList {
unsigned int count;
Ivar *ivarList = class_copyIvarList([UITextField class], &count);
for (unsigned int i = 0; i < count; i++) {
Ivar myIvar = ivarList[i];
const char *ivarName = ivar_getName(myIvar);
NSLog(@"ivar(%d) : %@", i, [NSString stringWithUTF8String:ivarName]);
}
free(ivarList);
objc_property_t *propertyList = class_copyPropertyList([UITextField 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);
}
// 通過修改 UITextfield 的私有屬性更改佔位顏色和字型
- (void)createLoginTextField {
UITextField *loginTextField = [[UITextField alloc] init];
loginTextField.frame = CGRectMake(15,(self.view.bounds.size.height-52-50)/2, self.view.bounds.size.width-60-18,52);
loginTextField.delegate = self;
loginTextField.font = [UIFont systemFontOfSize:14];
loginTextField.contentVerticalAlignment = UIControlContentVerticalAlignmentCenter;
loginTextField.textColor = [UIColor blackColor];
loginTextField.placeholder = @"使用者名稱/郵箱";
[loginTextField setValue:[UIFont systemFontOfSize:15] forKeyPath:@"_placeholderLabel.font"];
[loginTextField setValue:[UIColor lightGrayColor]forKeyPath:@"_placeholderLabel.textColor"];
[self.view addSubview:loginTextField];
}
複製程式碼
3.2 萬能控制器跳轉
需求:
- 某個頁面的不同 banner 圖,點選可以跳轉到不同頁面。
- 推送通知,點選跳轉到指定頁面。
- 二維碼掃描,根據不同內容,跳轉不同頁面。
- WebView 頁面,根據 URL 點選不同,跳轉不同的原生頁面。
先來思考一下幾種解決方法。
方法 1:在每個需要跳轉的地方寫一堆判斷語句以及跳轉語句。
方法 2:將判斷語句和跳轉語句抽取出來,寫到基類,或者對應的 Category 中。
方法 3:利用 Runtime,定製一個萬能跳轉控制器工具。
實現步驟:
- 事先和伺服器端商量好,定義跳轉不同控制器的規則,讓伺服器傳回對應規則的相關引數。 比如:跳轉到 A 控制器,需要伺服器傳回 A 控制器的類名,控制器 A 需要傳入的屬性引數(id、type 等等)。
- 根據伺服器傳回的類名,建立對應的控制器物件;
- 遍歷伺服器傳回的引數,利用 Runtime 遍歷控制器物件的屬性列表;
- 如果控制器物件存在該屬性,則利用 KVC 進行賦值;
- 進行跳轉。
首先,定義跳轉規則,如下所示。XXViewController
是將要跳轉的控制器類名。property
字典中儲存的是控制器所需的屬性引數。
// 定義的規則
NSDictionary *params = @{
@"class" : @"XXViewController",
@"property" : @{
@"ID" : @"123",
@"type" : @"XXViewController1"
}
};
複製程式碼
然後,新增一個工具類 XXJumpControllerTool
,新增跳轉相關的類方法。
/********************* XXJumpControllerTool.h 檔案 *********************/
#import <Foundation/Foundation.h>
@interface XXJumpControllerTool : NSObject
+ (void)pushViewControllerWithParams:(NSDictionary *)params;
@end
/********************* XXJumpControllerTool.m 檔案 *********************/
#import "XXJumpControllerTool.h"
#import <UIKit/UIKit.h>
#import <objc/runtime.h>
@implementation XXJumpControllerTool
+ (void)pushViewControllerWithParams:(NSDictionary *)params {
// 取出控制器類名
NSString *classNameStr = [NSString stringWithFormat:@"%@", params[@"class"]];
const char *className = [classNameStr cStringUsingEncoding:NSASCIIStringEncoding];
// 根據字串返回一個類
Class newClass = objc_getClass(className);
if (!newClass) {
// 建立一個類
Class superClass = [NSObject class];
newClass = objc_allocateClassPair(superClass, className, 0);
// 註冊你建立的這個類
objc_registerClassPair(newClass);
}
// 建立物件(就是控制器物件)
id instance = [[newClass alloc] init];
NSDictionary *propertys = params[@"property"];
[propertys enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
// 檢測這個物件是否存在該屬性
if ([XXJumpControllerTool checkIsExistPropertyWithInstance:instance verifyPropertyName:key]) {
// 利用 KVC 對控制器物件的屬性賦值
[instance setValue:obj forKey:key];
}
}];
// 跳轉到對應的控制器
[[XXJumpControllerTool topViewController].navigationController pushViewController:instance animated:YES];
}
// 檢測物件是否存在該屬性
+ (BOOL)checkIsExistPropertyWithInstance:(id)instance verifyPropertyName:(NSString *)verifyPropertyName {
unsigned int count, i;
// 獲取物件裡的屬性列表
objc_property_t *properties = class_copyPropertyList([instance class], &count);
for (i = 0; i < count; i++) {
objc_property_t property =properties[i];
// 屬性名轉成字串
NSString *propertyName = [[NSString alloc] initWithCString:property_getName(property) encoding:NSUTF8StringEncoding];
// 判斷該屬性是否存在
if ([propertyName isEqualToString:verifyPropertyName]) {
free(properties);
return YES;
}
}
free(properties);
return NO;
}
// 獲取當前顯示在螢幕最頂層的 ViewController
+ (UIViewController *)topViewController {
UIViewController *resultVC = [XXJumpControllerTool _topViewController:[[UIApplication sharedApplication].keyWindow rootViewController]];
while (resultVC.presentedViewController) {
resultVC = [XXJumpControllerTool _topViewController:resultVC.presentedViewController];
}
return resultVC;
}
+ (UIViewController *)_topViewController:(UIViewController *)vc {
if ([vc isKindOfClass:[UINavigationController class]]) {
return [XXJumpControllerTool _topViewController:[(UINavigationController *)vc topViewController]];
} else if ([vc isKindOfClass:[UITabBarController class]]) {
return [XXJumpControllerTool _topViewController:[(UITabBarController *)vc selectedViewController]];
} else {
return vc;
}
return nil;
}
@end
複製程式碼
測試程式碼:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 萬能跳轉控制器
[self jumpController];
}
複製程式碼
3.3 實現字典轉模型
在日常開發中,將網路請求中獲取的 JSON 資料轉為資料模型,是我們開發中必不可少的操作。通常我們會選用諸如 YYModel
、JSONModel
或者 MJExtension
等第三方框架來實現這一過程。這些框架實現原理的核心就是 Runtime
和 KVC
,以及 Getter / Setter
。
實現的大體思路如下:藉助 Runtime
可以動態獲取成員列表的特性,遍歷模型中所有屬性,然後以獲取到的屬性名為 key
,在 JSON
字典中尋找對應的值 value
;再使用 KVC
或直接呼叫 Getter / Setter
將每一個對應 value
賦值給模型,就完成了字典轉模型的目的。
需求:將伺服器返回的 JSON 字典轉為資料模型。
先準備一份待解析的 JSON 資料,內容如下:
{
"id": "123412341234",
"name": "行走少年郎",
"age": "18",
"weight": 120,
"address": {
"country": "中國",
"province": "北京"
},
"courses": [
{
"name": "Chinese",
"desc": "語文課"
},
{
"name": "Math",
"desc": "數學課"
},
{
"name": "English",
"desc": "英語課"
}
]
}
複製程式碼
假設這就是伺服器返回的 JSON 資料,內容是一個學生的資訊。現在我們需要將該 JSON 字典轉為方便開發的資料模型。
從這份 JSON 中可以看出,字典中取值除了字串之外,還有陣列和字典。那麼在將字典轉換成資料模型的時候,就要考慮 模型巢狀模型、模型巢狀模型陣列 的情況了。具體步驟如下:
3.3.1 建立模型
經過分析,我們總共需要三個模型: XXStudentModel、XXAdressModel、XXCourseModel。
/********************* XXStudentModel.h 檔案 *********************/
#import <Foundation/Foundation.h>
#import "NSObject+XXModel.h"
@class XXAdressModel, XXCourseModel;
@interface XXStudentModel : NSObject <XXModel>
/* 姓名 */
@property (nonatomic, copy) NSString *name;
/* 學生號 id */
@property (nonatomic, copy) NSString *uid;
/* 年齡 */
@property (nonatomic, assign) NSInteger age;
/* 體重 */
@property (nonatomic, assign) NSInteger weight;
/* 地址(巢狀模型) */
@property (nonatomic, strong) XXAdressModel *address;
/* 課程(巢狀模型陣列) */
@property (nonatomic, strong) NSArray *courses;
@end
/********************* XXStudentModel.m 檔案 *********************/
#import "XXStudentModel.h"
#import "XXCourseModel.h"
@implementation XXStudentModel
+ (NSDictionary *)modelContainerPropertyGenericClass {
//需要特別處理的屬性
return @{
@"courses" : [XXCourseModel class],
@"uid" : @"id"
};
}
@end
/********************* XXAdressModel.h 檔案 *********************/
#import <Foundation/Foundation.h>
@interface XXAdressModel : NSObject
/* 國籍 */
@property (nonatomic, copy) NSString *country;
/* 省份 */
@property (nonatomic, copy) NSString *province;
/* 城市 */
@property (nonatomic, copy) NSString *city;
@end
/********************* XXAdressModel.m 檔案 *********************/
#import "XXAdressModel.h"
@implementation XXAdressModel
@end
/********************* XXCourseModel.h 檔案 *********************/
#import <Foundation/Foundation.h>
@interface XXCourseModel : NSObject
/* 課程名 */
@property (nonatomic, copy) NSString *name;
/* 課程介紹 */
@property (nonatomic, copy) NSString *desc;
@end
/********************* XXCourseModel.m 檔案 *********************/
#import "XXCourseModel.h"
@implementation XXCourseModel
@end
複製程式碼
3.3.2 在 NSObject 分類中實現字典轉模型
細心的你可能已經發現:上面的 XXStudentModel.h
檔案中匯入了 #import "NSObject+XXModel.h"
檔案,並且遵循了 <XXModel>
協議,並且在 XXStudentModel.m
檔案中實現了協議的 + (NSDictionary *)modelContainerPropertyGenericClass
方法。
NSObject+XXModel.h
、NSObject+XXModel.m
就是我們用來解決字典轉模型所建立的分類,協議中的 + (NSDictionary *)modelContainerPropertyGenericClass
方法用來告訴分類特殊欄位的處理規則,比如 id --> uid
。
/********************* NSObject+XXModel.h 檔案 *********************/
#import <Foundation/Foundation.h>
// XXModel 協議
@protocol XXModel <NSObject>
@optional
// 協議方法:返回一個字典,表明特殊欄位的處理規則
+ (nullable NSDictionary<NSString *, id> *)modelContainerPropertyGenericClass;
@end;
@interface NSObject (XXModel)
// 字典轉模型方法
+ (instancetype)xx_modelWithDictionary:(NSDictionary *)dictionary;
@end
複製程式碼
/********************* NSObject+XXModel.m 檔案 *********************/
#import "NSObject+XXModel.h"
#import <objc/runtime.h>
@implementation NSObject (XXModel)
+ (instancetype)xx_modelWithDictionary:(NSDictionary *)dictionary {
// 建立當前模型物件
id object = [[self alloc] init];
unsigned int count;
// 獲取當前物件的屬性列表
objc_property_t *propertyList = class_copyPropertyList([self class], &count);
// 遍歷 propertyList 中所有屬性,以其屬性名為 key,在字典中查詢 value
for (unsigned int i = 0; i < count; i++) {
// 獲取屬性
objc_property_t property = propertyList[i];
const char *propertyName = property_getName(property);
NSString *propertyNameStr = [NSString stringWithUTF8String:propertyName];
// 獲取 JSON 中屬性值 value
id value = [dictionary objectForKey:propertyNameStr];
// 獲取屬性所屬類名
NSString *propertyType;
unsigned int attrCount;
objc_property_attribute_t *attrs = property_copyAttributeList(property, &attrCount);
for (unsigned int i = 0; i < attrCount; i++) {
switch (attrs[i].name[0]) {
case 'T': { // Type encoding
if (attrs[i].value) {
propertyType = [NSString stringWithUTF8String:attrs[i].value];
// 去除轉義字元:@\"NSString\" -> @"NSString"
propertyType = [propertyType stringByReplacingOccurrencesOfString:@"\"" withString:@""];
// 去除 @ 符號
propertyType = [propertyType stringByReplacingOccurrencesOfString:@"@" withString:@""];
}
} break;
default: break;
}
}
// 對特殊屬性進行處理
// 判斷當前類是否實現了協議方法,獲取協議方法中規定的特殊屬性的處理方式
NSDictionary *perpertyTypeDic;
if([self respondsToSelector:@selector(modelContainerPropertyGenericClass)]){
perpertyTypeDic = [self performSelector:@selector(modelContainerPropertyGenericClass) withObject:nil];
}
// 處理:字典的 key 與模型屬性不匹配的問題,如 id -> uid
id anotherName = perpertyTypeDic[propertyNameStr];
if(anotherName && [anotherName isKindOfClass:[NSString class]]){
value = dictionary[anotherName];
}
// 處理:模型巢狀模型的情況
if ([value isKindOfClass:[NSDictionary class]] && ![propertyType hasPrefix:@"NS"]) {
Class modelClass = NSClassFromString(propertyType);
if (modelClass != nil) {
// 將被巢狀字典資料也轉化成Model
value = [modelClass xx_modelWithDictionary:value];
}
}
// 處理:模型巢狀模型陣列的情況
// 判斷當前 value 是一個陣列,而且存在協議方法返回了 perpertyTypeDic
if ([value isKindOfClass:[NSArray class]] && perpertyTypeDic) {
Class itemModelClass = perpertyTypeDic[propertyNameStr];
//封裝陣列:將每一個子資料轉化為 Model
NSMutableArray *itemArray = @[].mutableCopy;
for (NSDictionary *itemDic in value) {
id model = [itemModelClass xx_modelWithDictionary:itemDic];
[itemArray addObject:model];
}
value = itemArray;
}
// 使用 KVC 方法將 value 更新到 object 中
if (value != nil) {
[object setValue:value forKey:propertyNameStr];
}
}
free(propertyList);
return object;
}
@end
複製程式碼
3.3.3 測試程式碼
- (void)parseJSON {
NSString *filePath = [[NSBundle mainBundle] pathForResource:@"Student" ofType:@"json"];
NSData *jsonData = [NSData dataWithContentsOfFile:filePath];
// 讀取 JSON 資料
NSDictionary *json = [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingMutableContainers error:nil];
NSLog(@"%@",json);
// JSON 字典轉模型
XXStudentModel *student = [XXStudentModel xx_modelWithDictionary:json];
NSLog(@"student.uid = %@", student.uid);
NSLog(@"student.name = %@", student.name);
for (unsigned int i = 0; i < student.courses.count; i++) {
XXCourseModel *courseModel = student.courses[i];
NSLog(@"courseModel[%d].name = %@ .desc = %@", i, courseModel.name, courseModel.desc);
}
}
複製程式碼
效果如下:
當然,如若需要考慮快取機制、效能問題、物件型別檢查等,建議還是使用例如 YYModel
之類的知名第三方框架,或者自己造輪子。
3.4 改進 iOS 歸檔和解檔
『歸檔』是一種常用的輕量型檔案儲存方式,在專案中,如果需要將資料模型本地化儲存,一般就會用到歸檔和解檔。但是如果資料模型中有多個屬性的話,我們不得不對每個屬性進行處理,這個過程非常繁瑣。
這裡我們可以參考之前『字典轉模型』 的程式碼。通過 Runtime 獲取類的屬性列表,實現自動歸檔和解檔。歸檔操作和解檔操作主要會用到了兩個方法: encodeObject: forKey:
和 decodeObjectForKey:
。
首先在 NSObject 的分類 NSObject+XXModel.h
、NSObject+XXModel.m
中新增以下程式碼:
// 解檔
- (instancetype)xx_modelInitWithCoder:(NSCoder *)aDecoder {
if (!aDecoder) return self;
if (!self) {
return self;
}
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]);
NSString *name = [NSString stringWithUTF8String:propertyName];
id value = [aDecoder decodeObjectForKey:name];
[self setValue:value forKey:name];
}
free(propertyList);
return self;
}
// 歸檔
- (void)xx_modelEncodeWithCoder:(NSCoder *)aCoder {
if (!aCoder) return;
if (!self) {
return;
}
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]);
NSString *name = [NSString stringWithUTF8String:propertyName];
id value = [self valueForKey:name];
[aCoder encodeObject:value forKey:name];
}
free(propertyList);
}
複製程式碼
然後在需要實現歸檔解檔的模型中,新增 -initWithCoder:
和 -encodeWithCoder:
方法。
#import "XXPerson.h"
#import "NSObject+XXModel.h"
@implementation XXPerson
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super init];
if (self) {
[self xx_modelInitWithCoder:aDecoder];
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder {
[self xx_modelEncodeWithCoder:aCoder];
}
@end
複製程式碼
測試一下歸檔解檔程式碼:
XXPerson *person = [[XXPerson alloc] init];
person.uid = @"123412341234";
person.name = @"行走少年郎";
person.age = 18;
person.weight = 120;
// 歸檔
NSString *path = [NSString stringWithFormat:@"%@/person.plist", NSHomeDirectory()];
[NSKeyedArchiver archiveRootObject:person toFile:path];
// 解檔
XXPerson *personObject = [NSKeyedUnarchiver unarchiveObjectWithFile:path];
NSLog(@"personObject.uid = %@", personObject.uid);
NSLog(@"personObject.name = %@", personObject.name);
複製程式碼
當然,上邊程式碼只是演示一下 Runtime 對於歸檔和解檔的優化,真正用在開發中的邏輯遠比上邊的樣例要負責,具體也參考 YYModel
的實現。
參考資料
- CoyoteK : runtime從入門到精通(九)—— 萬能介面跳轉
- 梧雨北辰 : Runtime-iOS執行時應用篇
- 雷曼同學 : https://www.jianshu.com/p/361c9136cf3a
- ibireme : iOS JSON 模型轉換庫評測
iOS 開發:『Runtime』詳解 系列文章:
- iOS 開發:『Runtime』詳解(一)基礎知識
- iOS 開發:『Runtime』詳解(二)Method Swizzling
- iOS 開發:『Runtime』詳解(三)Category 底層原理
- iOS 開發:『Runtime』詳解(四)獲取類詳細屬性、方法
尚未完成:
- iOS 開發:『Runtime』詳解(五)Crash 防護系統
- iOS 開發:『Runtime』詳解(六)Objective-C 2.0 結構解析
- iOS 開發:『Runtime』詳解(七)KVO 底層實現