前言
好久沒有寫過新文章了,最近一直在忙工作的事情,我的新浪微博開源專案也停止了一週時間,目前完成了60%,就先寫一篇關於JSON轉Model的文章和大家聊聊天吧,為什麼會寫一個這個小工具呢,請看文末?
核心方法Runtime的介紹
1. Runtime是什麼?
顧名思義:Runtime就是執行時的意思,是系統在執行時的一些機制,其中最主要的就是訊息機制,舉個常用的例子,在物件導向程式設計的語言中,萬物皆物件,物件如何呼叫方法呢,
[target excuteSEL]
,需要一個物件,需要一個方法名,系統在執行時會自動轉換成以下的形式:
objc_msgSend(target,@selector(excuteSEL:))
關於Runtime的詳細介紹,網上有很多,這裡就不做過多描述了。
2.Runtime的常見用法
注:使用時需要#import
* 1 方法替換(黑魔法)
舉個例子來說明一下:
將呼叫A方法替換為呼叫B方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class_replaceMethod([self class], @selector(sendAMessage:), (IMP)changeAtoB, NULL); //MARK: 方法替換_1 - (void)sendAMessage:(NSString *)message { NSLog(@"A_message: %@",message); } - (void)sendBMessage:(NSString *)message { NSLog(@"B_Message: %@",message); } ViewController * changeAtoB(ViewController *SELF, SEL _cmd, NSString *message) { if ([NSStringFromSelector(_cmd) isEqualToString:@"sendAMessage:"]) { //將方法進行替換 [SELF sendBMessage:message]; } return SELF; } //這裡IMP可以理解為魔法通道,將源方法通過IMP指標轉換為目標方法 |
* 2 獲取物件的屬性和方法
注:獲取物件的屬性,這個方法在JSON轉Model可以說是核心方法了
舉例說明:
1 2 3 4 5 6 7 |
//class_copyPropertyList 這個方法會獲取到一個類.h和.m檔案中interface中的所有屬性 //獲取CMGCD類的屬性名稱 unsigned int count; objc_property_t *propertyList = class_copyPropertyList([CMGCD class], &count); for (unsigned int i = 0; i %@",[NSString stringWithUTF8String:propertyName]); } |
1 2 3 4 5 |
// unsigned int count; Method *methodList = class_copyMethodList([CMGCD class], &count); for (unsigned int i; i %@",NSStringFromSelector(method_getName(method))); } |
* 2 設定物件關聯
定義:關聯是指把兩個物件相互關聯起來,使其中的一個物件作為另外一個物件的一部分
再舉個例子,我在物件中定義了一個屬性
1 |
@property (strong, nonatomic) NSString *content; |
1 2 3 4 5 |
//設定物件屬性關聯 static char associatedKey; objc_setAssociatedObject(self, &associatedKey, @"content_yeah", OBJC_ASSOCIATION_RETAIN_NONATOMIC); NSString *get_content = objc_getAssociatedObject(self, &associatedKey); NSLog(@"content = %@",get_content); |
Tips:設定物件關聯需要以下幾個要點:
源物件
、關鍵字
、關聯的物件
關聯策略
解釋一下:這裡我將@"content_yeah"
這個物件與self
使用OBJC_ASSOCIATION_RETAIN_NONATOMIC
策略關聯到一起,意思就是在self
的生命週期之內關聯的物件都不會被釋放,通過這個方法,可以實現動態向類裡面新增屬性
另外還有一些關聯的方法,如
- 斷開關聯: 設定關聯物件為
nil
即可
objc_setAssociatedObject(self, &associatedKey, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
- 斷開這個物件的所有關聯關係
1 2 |
//斷開所有關聯 objc_removeAssociatedObjects(self); |
JSON轉Model工具的主要介紹
1. 為什麼會寫這樣一個工具
很多時候我們並不是缺少實力,而且是缺少一種彼可取而代之
的勇氣,為什麼會有MJExtension
、YYModel
的產生,檢視原始碼的過程中我有種想死的感覺,但是知道實現的原理後,為什麼不能自己去實現一個呢?這個工具的原始碼非常簡單,我寫這個工具的目的只是為了告訴朋友們,真的不復雜,不要因為看著複雜就放棄了自己動手的衝動
2. 工具的整體步驟簡介
著重介紹一下我的思考過程
- 1 核心方法?
利用Runtime可以遍歷出物件的所有屬性,然後利用遞迴的思想逐層解析JSON - 2 怎麼去做?
基本所有的Model繼承NSObject
,我們可以寫一個NSObject
的Category
,然後在其中寫一些解析方法,我們需要一個對照JSON字串的解析路徑字典,比如說JSON
的狗
的屬性名稱為dog
,我們的物件屬性名稱想定義為xiaogou
,這就需要手寫一個字典將解析中遇到的dog
都給對映為xiaogou
- 3 開始動手吧
3. 主要程式碼介紹:
* 1 NSObject+CMModel
介紹
1 2 3 4 5 6 7 8 |
//NSObject+CMModel.h // 單個物件 - (NSDictionary *)dict_CMModelWithClass; // 物件陣列 - (NSDictionary *)dict_CMModelWIthArrayClass; - (instancetype)cm_initWithJSONString:(NSString *)jsonString; |
下面著重解釋一下.m檔案中的內容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
//NSObject+CMModel.m #import #import "CMObject.h" #import "CMProperty.h" //返回單個物件的解析字典,預設為nil - ( NSDictionary * _Nullable )dict_CMModelWithClass { return nil; } //返回物件陣列的解析字典,預設為nil - (NSDictionary *)dict_CMModelWIthArrayClass { return nil; } //呼叫方法 - (instancetype)cm_initWithJSONString:(NSString *)jsonString { if (self) { [self analysisWitnJsonString:jsonString]; } return self; } |
取得物件的所有屬性及其對應的型別
Tips: 這裡自己寫了一個類,將物件的屬性及其名稱封裝到一個型別為CMProperty
的陣列
- 1 CMProperty類簡介
1 2 3 4 5 6 7 8 |
//屬性名稱 @property (strong, nonatomic) NSString *propertyName; //屬性的類 @property (strong, nonatomic) Class propertyClass; //是否基本型別 @property (assign, nonatomic) BOOL isBasicType; |
- 2 獲取物件屬性及其型別,並且將其封裝為型別為
CMProperty
的陣列
1 2 3 4 5 6 7 |
//將該類的屬性和對應的型別進行封裝 - (NSArray *)propertyArray { NSMutableArray *propertyArray = [NSMutableArray array]; unsigned int count; objc_property_t *propertyList = class_copyPropertyList([self class], &count); for (unsigned int i = 0; i |
開始解析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
//進行解析 - (void)analysisWitnJsonString:(NSString *)json { //存在解析字典 if ([self dict_CMModelWithClass] != nil || [self dict_CMModelWIthArrayClass] != nil) { CMObject *analysisTools = [[CMObject alloc] initWithGoalObject:self CMPropertyArray:[self propertyArray]]; if ([self dict_CMModelWIthArrayClass]) { analysisTools.analysisObjectArrayDict = [self dict_CMModelWIthArrayClass]; } if ([self dict_CMModelWithClass]) { analysisTools.analysisDict = [self dict_CMModelWithClass]; } analysisTools.jsonString = json; } } |
* 2 CMObject
介紹
這個類為實際進行解析工具的類,或者可以稱之為工具,這裡我們需要特殊對待NSArray
、NSDictionary
、int、float等基本型別
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
//CMObject.h //要解析的物件 @property (strong, nonatomic) id object; //解析key對應列表 單個物件 @property (strong, nonatomic) NSDictionary *analysisDict; //解析key對應列表 物件陣列 @property (strong, nonatomic) NSDictionary *analysisObjectArrayDict; //屬性列表 @property (strong, nonatomic) NSArray *propertyArray; //init方法 - (instancetype)initWithGoalObject:(id)object CMPropertyArray:(NSArray *)propertyArray; //解析的源JSON @property (strong, nonatomic) NSString *jsonString; BOOL Exist_(id obj,NSArray *existArray); |
Tips: 下面程式碼可能看著會不舒服,我說一下整體的思路
* 1 NSObject+CMModel
中將封裝的屬性陣列傳遞過來,我們一個接一個的對屬性進行遍歷構造
2 遍歷詳解:舉例,碰到NSArray
的屬性時,我們去看要解析的類中實現的- (NSDictionary
)dict_CMModelWIthArrayClass這個方法,找到目的物件的型別
OBJClass,然後將
JSON字典拆分後利用
- (instancetype)cm_initWithJSONString:(NSString *)jsonString這個方法建立一個
OBJClass型別的物件,並且新增到陣列中,建立完後,使用
KVC將陣列賦給源物件,具體程式碼看下面
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 |
//CMObject.m //這裡說一下解析方法, 程式碼較多 //開始進行model解析 - (void)analysisModel { NSDictionary *jsonDict = [NSJSONSerialization JSONObjectWithData:[_jsonString dataUsingEncoding:NSUTF8StringEncoding] options:NSJSONReadingAllowFragments error:nil]; //將`NSObject+CMModel`中獲取的屬性陣列進行遍歷構造,並賦值給對應的屬性 [_propertyArray enumerateObjectsUsingBlock:^(CMProperty * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { //單獨處理陣列型別 if (obj.propertyClass == [NSArray class]) { // 物件為陣列型別, NSString *key = [_analysisObjectArrayDict objectForKey:obj.propertyName]; Class OBJClass = NSClassFromString(key); if (![jsonDict objectForKey:key]) { //名稱與json中的名稱並不相符 if ([_analysisDict objectForKey:obj.propertyName]) { NSMutableArray *objectArray = [NSMutableArray array]; NSArray *obj_json_array = [jsonDict objectForKey:[_analysisDict objectForKey:obj.propertyName]]; [obj_json_array enumerateObjectsUsingBlock:^(NSDictionary * _Nonnull obj_dict, NSUInteger idx, BOOL * _Nonnull stop) { id objx = [[OBJClass alloc] cm_initWithJSONString:DataToJSONString(obj_dict)]; //解析好物件之後,存到資料中 [objectArray addObject:objx]; }]; [_object setValue:objectArray forKey:obj.propertyName]; }else { } } }else if (Exist_(obj.propertyClass, @[[NSDictionary class],[NSMutableDictionary class]])) { NSDictionary *obj_dict = [jsonDict valueForKey:[_analysisDict objectForKey:obj.propertyName]]; if (!obj_dict) { [_object setValue:[NSNull null] forKey:obj.propertyName]; }else { [_object setValue:obj_dict forKey:obj.propertyName]; } }else if (obj.isBasicType){ //基本型別 id num = [jsonDict valueForKey:[_analysisDict objectForKey:obj.propertyName]]; if (!num || num == [NSNull null]) { //為空判斷 //這裡我們預設設定為 0.00大小的NSNumber物件 [_object setValue:NULL_NUM forKey:obj.propertyName]; }else { NSNumber *number = (NSNumber *)num; [_object setValue:number forKey:obj.propertyName]; } }else { Class OBJClass = obj.propertyClass; if(!Exist_(OBJClass, foundationClasses_)) { //如果型別為自定義類 id obj_dict = [jsonDict valueForKey:[_analysisDict objectForKey:obj.propertyName]]; //非空判斷 if (!obj_dict) { id objx = [[OBJClass alloc] cm_initWithJSONString:DataToJSONString(obj_dict)]; [_object setValue:objx forKey:obj.propertyName]; }else { //這裡可以賦值為[NSNull null] 可以賦值為一個新物件 // [_object setValue:[NSNull null] forKey:obj.propertyName]; [_object setValue:[[OBJClass alloc] init] forKey:obj.propertyName]; } }else { [_object setValue:[jsonDict valueForKey:[_analysisDict objectForKey:obj.propertyName]] forKey:obj.propertyName]; } } }]; } |
4. 如何使用
舉例說明:
這裡有三個類
Animals(動物)
1 2 |
@property (strong, nonatomic) NSArray *dogs; @property (strong, nonatomic) NSArray *pigs; |
—實現方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
- (NSDictionary *)dict_CMModelWithClass { return @{ @"dogs" : @"dog", @"pigs" : @"pig", }; } - (NSDictionary *)dict_CMModelWIthArrayClass { return @{ @"dogs" : @"Dog", @"pigs" : @"Pig", }; } |
Dog(狗)
1 2 3 4 5 |
@property (strong, nonatomic) NSString *dog_name; @property (assign, nonatomic) NSUInteger dog_age; //狗養的豬 @property (strong, nonatomic) Pig *dog_pig; |
—實現方法
1 2 3 4 5 6 7 |
- (NSDictionary *)dict_CMModelWithClass { return @{ @"dog_age" : @"age", @"dog_name" : @"name", @"dog_pig" : @"dog_pig" }; } |
Pig(豬)
1 2 |
@property (strong, nonatomic) NSString *pig_name; @property (assign, nonatomic) NSUInteger pig_age; |
—實現方法
1 2 3 4 5 6 |
- (NSDictionary *)dict_CMModelWithClass { return @{ @"pig_age" : @"age", @"pig_name" : @"name" }; } |
呼叫方法
1 2 3 |
NSString *testJSON = @"{\"dog\":[{\"name\":\"dog_1\",\"age\":15,\"dog_pig\":{\"name\":\"dogAndPig1\",\"age\":666}},{\"name\":\"dog_2\",\"age\":null,\"dog_pig\":null}],\"pig\":[{\"name\":\"pig_1\",\"age\":10},{\"name\":\"pig_2\",\"age\":12}]}"; Animals *animals = [[Animals alloc] cm_initWithJSONString:testJSON]; |
寫在後面的話
這個專案並不完善,比如說對於其中日期的格式化,非空的一些判斷等,其中也有一些bug,本文權當是拋磚引玉,利用Runtime
可以做很多事情,比如你可以實現,一句話完成歸檔與解歸檔,不會再出現Model屬性過多時重寫initWithCoder
和encodeWithCoder
的尷尬了,so,有時候我們更缺的是一種思考問題的方式,共勉!
PS:歡迎來我的簡書、Github、個人部落格交流?