讀完這篇文章你可以自己寫一個 YYModel 這樣的神器,這篇文章類似一個源碼解析,但不同的是,它不光光是解析,更是實戰,因為我覺得學習一個東西必須要自己寫一遍才算是真的學了一遍,否則即便是讀完了源碼印象還是不會太深刻,so,開始吧。
注:為了簡單起見,我的例子只是實現了一個精簡的版本,YYModel 有很多功能,我這裡就實現了一個核心的功能,JSON -> Model。
注:文章的最後有完整的代碼
從JSON映射到Model的原理
想一下平時我們是怎麼使用類似這樣子的庫的,當我們有一個JSON的時候,我們把所有JSON的字段(比如name、page)全部寫成對應的類中的屬性。然後庫會自動把你JSON對應字段的值賦值到這些對應的屬性裡去。屬性我們用 @property 來定義,就意味著編譯器會幫你生成對應的get``set方法,我們使用的 . 其實也是在調用get``set方法來實現賦值的。在 Objective-C 中有一個著名的函數 objc_msgSend(...) 我們所有的類似 [obj method] 的方法調用(發送消息)都會被轉換成 objc_msgSend(...) 的方式來調用。(具體這個函數怎麼用後面再說)
所以對於一個庫來說,要調用你這個 Model 的 set 方法,用 objc_msgSend(...) 會容易的多,所以JSON映射到Model的原理其實就是調用這個函數而已。
所以整個流程就是,你給我一個 Model 類,我會用 runtime 提供的各種函數來拿到你所有的屬性和對應的get``set,判斷完相應的類型以後,調用objc_msgSend(...)。說起來真的非常簡單,做起來就有點麻煩...
前期的准備工作
為了後面的方便,我們需要把一些關鍵的東西封裝起來,我們需要單獨封裝 ivar property method,也就是實例變量、屬性、方法,但事實上我們的這個精簡版的YYModel並不需要 method ivar 的封裝,為了保證完整性,我還是打算寫下來。
封裝 ivar
先來封裝 ivar,看一下頭文件 CPClassIvarInfo.h(YYModel只有4個文件,兩個 .h 兩個 .m 我為了讓代碼看起來更清楚,所以我自己在重寫 YYModel 的時候把所有可以拆出來的類都分別拆成了一對.h .m)並把前綴改成了 CP 意思是 copy。
#import #import #import "CPCommon.h" @interface CPClassIvarInfo : NSObject @property (nonatomic, assign, readonly) Ivar ivar; @property (nonatomic, strong, readonly) NSString *name; @property (nonatomic, strong, readonly) NSString *typeEncoding; @property (nonatomic, assign, readonly) CPEncodingType type; - (instancetype)initWithIvar:(Ivar)ivar; @end
Ivar 代表一個實例變量,你所有和實例變量有關的操作都必須要把 Ivar 傳進去,等一下就能看到。
name 是這個實例變量的變量名
typeEncoding 是對類型的編碼,具體可以看這裡 對於不同的類型就會有對應的編碼,比如 int 就會變編碼成 i,可以用 @encode(int)這樣的操作來看一個類型的編碼。
type 是一個自定義的枚舉,它描述了 YYMode 規定的類型。
一個強大的枚舉
然後重新再創建一個文件(CPCommon),作為一個公共的文件 CPEncodingType 這個枚舉就寫在這裡。
我們要創建的這個枚舉需要一口氣表示三種不同的類型,一種用於普通的類型上(int double object),一種用來表示關鍵詞(const),一種表示 Property 的屬性(Nonatomic weak retain)。
我們可以用位運算符來搞定這三種類型,用8位的枚舉值來表示第一種,16位的表示第二種,24位的表示第三種,然後為了區別這三種類型都屬於多少位的,我們可以分別搞三個 mask ,做一個該類型和某一個 mask 的與(&)的操作就可以知道這個類型是具體是哪一個類型了,例子在後面。
這個枚舉我們可以這樣定義:
typedef NS_OPTIONS(NSUInteger, CPEncodingType) { CPEncodingTypeMask = 0xFF, //8 bit CPEncodingTypeUnknown = 0, CPEncodingTypeVoid = 1, CPEncodingTypeBool = 2, CPEncodingTypeInt8 = 3, CPEncodingTypeUInt8 = 4, CPEncodingTypeInt16 = 5, CPEncodingTypeUInt16 = 6, CPEncodingTypeInt32 = 7, CPEncodingTypeUInt32 = 8, CPEncodingTypeInt64 = 9, CPEncodingTypeUInt64 = 10, CPEncodingTypeFloat = 11, CPEncodingTypeDouble = 12, CPEncodingTypeLongDouble = 13, CPEncodingTypeObject = 14, CPEncodingTypeClass = 15, CPEncodingTypeSEL = 16, CPEncodingTypeBlock = 17, CPEncodingTypePointer = 18, CPEncodingTypeStruct = 19, CPEncodingTypeUnion = 20, CPEncodingTypeCString = 21, CPEncodingTypeCArray = 22, CPEncodingTypeQualifierMask = 0xFF00, //16 bit CPEncodingTypeQualifierConst = 1 << 8, CPEncodingTypeQualifierIn = 1 << 9, CPEncodingTypeQualifierInout = 1 << 10, CPEncodingTypeQualifierOut = 1 << 11, CPEncodingTypeQualifierBycopy = 1 << 12, CPEncodingTypeQualifierByref = 1 << 13, CPEncodingTypeQualifierOneway = 1 << 14, CPEncodingTypePropertyMask = 0xFF0000, // 24 bit CPEncodingTypePropertyReadonly = 1 << 16, CPEncodingTypePropertyCopy = 1 << 17, CPEncodingTypePropertyRetain = 1 << 18, CPEncodingTypePropertyNonatomic = 1 << 19, CPEncodingTypePropertyWeak = 1 << 20, CPEncodingTypePropertyCustomGetter = 1 << 21, CPEncodingTypePropertyCustomSetter = 1 << 22, CPEncodingTypePropertyDynamic = 1 << 23, };
比如有一個類型是這樣的
CPEncodingType type = CPEncodingTypeDouble;
假設我們並不知道它是 CPEncodingTypeDouble 類型,那我們要怎麼樣才能知道它是什麼類型呢?只要這樣:
NSLog(@"%lu",type & CPEncodingTypeMask);
輸出: 12
在枚舉的定義中
CPEncodingTypeDouble = 12,
假設這個枚舉值有很多種混在一起
CPEncodingType type = CPEncodingTypeDouble | CPEncodingTypePropertyRetain; NSLog(@"%lu",type & CPEncodingTypePropertyMask); //輸出 262144 (1<<18的十進制表示) NSLog(@"%lu",type & CPEncodingTypeMask); //輸出 12
可能有人知道這種神奇的用法,但在我讀YYModel之前我沒用過這種方法(技術比較菜)。
然後還有一個函數,這個函數可以把類型編碼(Type Encoding)轉換成剛才的枚舉值,很簡單卻很長的一個函數:
CPEncodingType CPEncodingGetType(const char *typeEncoding) { char *type = (char *)typeEncoding; if (!type) return CPEncodingTypeUnknown; size_t len = strlen(type); if (len == 0) return CPEncodingTypeUnknown; CPEncodingType qualifier = 0; bool prefix = true; while (prefix) { switch (*type) { case 'r': { qualifier |= CPEncodingTypeQualifierConst; type++; } break; case 'n': { qualifier |= CPEncodingTypeQualifierIn; type++; } break; case 'N': { qualifier |= CPEncodingTypeQualifierInout; type++; } break; case 'o': { qualifier |= CPEncodingTypeQualifierOut; type++; } break; case 'O': { qualifier |= CPEncodingTypeQualifierBycopy; type++; } break; case 'R': { qualifier |= CPEncodingTypeQualifierByref; type++; } break; case 'V': { qualifier |= CPEncodingTypeQualifierOneway; type++; } break; default: { prefix = false; } break; } } len = strlen(type); if (len == 0) return CPEncodingTypeUnknown | qualifier; switch (*type) { case 'v': return CPEncodingTypeVoid | qualifier; case 'B': return CPEncodingTypeBool | qualifier; case 'c': return CPEncodingTypeInt8 | qualifier; case 'C': return CPEncodingTypeUInt8 | qualifier; case 's': return CPEncodingTypeInt16 | qualifier; case 'S': return CPEncodingTypeUInt16 | qualifier; case 'i': return CPEncodingTypeInt32 | qualifier; case 'I': return CPEncodingTypeUInt32 | qualifier; case 'l': return CPEncodingTypeInt32 | qualifier; case 'L': return CPEncodingTypeUInt32 | qualifier; case 'q': return CPEncodingTypeInt64 | qualifier; case 'Q': return CPEncodingTypeUInt64 | qualifier; case 'f': return CPEncodingTypeFloat | qualifier; case 'd': return CPEncodingTypeDouble | qualifier; case 'D': return CPEncodingTypeLongDouble | qualifier; case '#': return CPEncodingTypeClass | qualifier; case ':': return CPEncodingTypeSEL | qualifier; case '*': return CPEncodingTypeCString | qualifier; case '^': return CPEncodingTypePointer | qualifier; case '[': return CPEncodingTypeCArray | qualifier; case '(': return CPEncodingTypeUnion | qualifier; case '{': return CPEncodingTypeStruct | qualifier; case '@': { if (len == 2 && *(type + 1) == '?') return CPEncodingTypeBlock | qualifier; else return CPEncodingTypeObject | qualifier; } default: return CPEncodingTypeUnknown | qualifier; } }
很簡單,不用多講了。
回到 CPClassIvarInfo 剛才我們只給出了頭文件,現在看一下實現。
- (instancetype)initWithIvar:(Ivar)ivar { if (!ivar){ return nil; } self = [super init]; if (self){ _ivar = ivar; const char *name = ivar_getName(ivar); if (name){ _name = [NSString stringWithUTF8String:name]; } const char *typeEncoding = ivar_getTypeEncoding(ivar); if (typeEncoding){ _typeEncoding = [NSString stringWithUTF8String:typeEncoding]; _type = CPEncodingGetType(typeEncoding); } } return self; }
只有一個方法,這裡就用到了兩個 runtime 函數 ivar_getName(ivar) 和 ivar_getTypeEncoding(ivar) 傳入 ivar 就行。
封裝Method
然後看一下對於 Method 的封裝,看一下頭文件(CPClassMethodInfo.h)
#import #import NS_ASSUME_NONNULL_BEGIN @interface CPClassMethodInfo : NSObject @property (nonatomic, assign, readonly) Method method; @property (nonatomic, strong, readonly) NSString *name; @property (nonatomic, assign, readonly) SEL sel; @property (nonatomic, assign, readonly) IMP imp; @property (nonatomic, strong, readonly) NSString *typeEncoding; @property (nonatomic, strong, readonly) NSString *returnTypeEncoding; @property (nullable, nonatomic, strong, readonly) NSArray*argumentTypeEncodings; - (instancetype)initWithMethod:(Method)method; NS_ASSUME_NONNULL_END @end
Objective-C 的 Optional
NS_ASSUME_NONNULL_BEGIN 和 NS_ASSUME_NONNULL_END 是成對出現的,因為 Swift 可以和 Objective-C 混用,但是 Swift 有 Optional 類型,而 Objective-C 沒有這樣的概念,為了和 Swift 保持一致,現在 Objective-C 有了 _Nullable 可空 _Nonnull不可空這樣的關鍵字,這兩個關鍵字可以在變量、方法返回值、方法參數上使用,比如:
@property (nonatomic, strong) NSString * _Nonnull string; - (NSString * _Nonnull)method:(NSString *_Nonnull)string;
還有另外一對 nullable nonnull,它們可以這樣用
@property (nullable, nonatomic, strong) NSString *string; - (nullable NSString *)method:(nullable NSString *)string;
對了,這些關鍵詞只能用在指針上,其他類型是不能用的。
當你一旦在某個地方寫上關鍵詞 nullable的時候,編譯器就會提出警告,Pointer is missing a nullability type specifier (_Nonnull, _Nullable, or _Null_unspecified) 然後你就可以加上NS_ASSUME_NONNULL_BEGIN 和 NS_ASSUME_NONNULL_END來表示只有我標記為 nullable 的地方才可空,其余地方都是 nonnull。
回到剛才的頭文件代碼,method 表示一個方法
name 很明顯就是方法名了
sel 和 imp 是一個對應關系,一個對象的所有方法都會保存在一張表裡,通過 sel 就能找到這個方法的 imp,我講的可能有點簡單,如果想要深入的了解可以查一下文檔或者博客。
typeEncoding 又是一個編碼,這裡是參數和返回值的編碼
returnTypeEncoding 返回值的編碼
argumentTypeEncodings 所有參數的編碼
實現還是很簡單
- (instancetype)initWithMethod:(Method)method { if (!method) { return nil; } self = [super init]; if (self){ _method = method; _sel = method_getName(method); _imp = method_getImplementation(method); const char *name = sel_getName(_sel); if (name) { _name = [NSString stringWithUTF8String:name]; } const char *typeEncoding = method_getTypeEncoding(method); if (typeEncoding) { _typeEncoding = [NSString stringWithUTF8String:typeEncoding]; } char *returnTypeEncoding = method_copyReturnType(method); if (returnTypeEncoding) { _returnTypeEncoding = [NSString stringWithUTF8String:returnTypeEncoding]; free(returnTypeEncoding); } //得到參數的數目,遍歷取得所有參數的類型 unsigned int count = method_getNumberOfArguments(method); if (count > 0) { NSMutableArray *types = [[NSMutableArray alloc] initWithCapacity:10]; for (unsigned int i = 0; i < count; i++) { char *argumentsType = method_copyArgumentType(method, i); NSString *type = argumentsType ? [NSString stringWithUTF8String:argumentsType] : nil; [types addObject:type ? type : @""]; if (argumentsType) { free(argumentsType); } } _argumentTypeEncodings = types; } } return self; }
和前面套路一樣。
封裝 Property
老樣子,看頭
#import #import #import "CPCommon.h" NS_ASSUME_NONNULL_BEGIN @interface CPClassPropertyInfo : NSObject @property (nonatomic, assign, readonly) objc_property_t property; @property (nonatomic, strong, readonly) NSString *name; @property (nonatomic, assign, readonly) CPEncodingType type; @property (nonatomic, strong, readonly) NSString *typdEncoding; @property (nonatomic, strong, readonly) NSString *ivarName; @property (nullable, nonatomic, assign, readonly) Class cls; @property (nonatomic, assign, readonly) SEL getter; @property (nonatomic, assign, readonly) SEL setter; - (instancetype)initWithProperty:(objc_property_t)property; NS_ASSUME_NONNULL_END @end
這是在精簡版的YYModel中會用到的一個類,這裡尤其要注意的是type和typdEncoding兩個屬性,希望讀者能夠仔細調試一下,看一下主要的一段代碼:
CPEncodingType type = 0; unsigned int outCount; objc_property_attribute_t *attrs = property_copyAttributeList(property, &outCount); //遍歷所有的Property的屬性 for (unsigned int i = 0; i < outCount; i++) { switch (attrs[i].name[0]) { case 'T': if (attrs[i].value) { _typdEncoding = [NSString stringWithUTF8String:attrs[i].value]; type = CPEncodingGetType(attrs[i].value); if((type & CPEncodingTypeMask) == CPEncodingTypeObject){ //如果該類型為一個對象 比如 @"NSString" ,截取中間的,結果為 NSString,目的是為了得到這個類的 Class size_t len = strlen(attrs[i].value); if (len > 3) { char name[len - 2]; name[len - 3] = '\0'; memcpy(name, attrs[i].value + 2, len - 3); _cls = objc_getClass(name); } } } break; case 'V': if (attrs[i].value) { _ivarName = [NSString stringWithUTF8String:attrs[i].value]; } break; case 'R': type |= CPEncodingTypePropertyReadonly; break; case 'C': type |= CPEncodingTypePropertyCopy; break; case '&': type |= CPEncodingTypePropertyRetain; break; case 'N': type |= CPEncodingTypePropertyNonatomic; break; case 'D': type |= CPEncodingTypePropertyDynamic; break; case 'W': type |= CPEncodingTypePropertyWeak; break; case 'G': type |= CPEncodingTypePropertyCustomGetter; if (attrs[i].value) { _getter = NSSelectorFromString([NSString stringWithUTF8String:attrs[i].value]); } break; case 'S': type |= CPEncodingTypePropertyCustomSetter; if (attrs[i].value) { _setter = NSSelectorFromString([NSString stringWithUTF8String:attrs[i].value]); } break; default: break; } }
我們通過property_copyAttributeList這個函數得到一個指向一個結構體objc_property_attribute_t的指針,這個結構體的結構如下:
typedef struct { const char *name; /**< The name of the attribute */ const char *value; /**< The value of the attribute (usually empty) */ } objc_property_attribute_t;
說是一個指針,其實它是一個結構體數組,指針指向的其實是這個數組第一個元素。
這個結構體表示的是一個 Property 的屬性,關於 Property 的類型編碼可以看這裡
要說清這個數組裡每一個結構體元素的name和value都存了什麼,我們可以看一下下面這段代碼:
Class cls = objc_getClass("CPBook"); objc_property_t property = class_getProperty(cls, "name"); const char* attr = property_getAttributes(property); NSLog(@"%s",attr);
這裡比如有一個類是 CPBook ,我們通過這個類的 Class 來拿到一個叫做 name 的 Property,然後在拿到這個 Property 所有屬性,輸出的結果是 T@"NSString",&,N,V_name
其實,我們用和上面一樣返回一個結構體數組的方式來獲取這個 Property 的屬性的話,那麼這個結構體應該會有4個元素。
第一個元素 name = T,value = @"NSString",第二個元素 name = &,value 沒有值,第三個元素 name = N,value 仍然沒有值,第四個元素 name = V,value = _name。不信可以運行一下下面的代碼來看看。
Class cls = objc_getClass("CPBook"); unsigned int acount; objc_property_t *prop = class_copyPropertyList(cls, &acount); objc_property_attribute_t *attr1 = property_copyAttributeList(prop[2], &acount); NSLog(@"%s",attr1[0].name); NSLog(@"%s",attr1[0].value); NSLog(@"-------------------"); NSLog(@"%s",attr1[1].name); NSLog(@"%s",attr1[1].value); NSLog(@"-------------------"); NSLog(@"%s",attr1[2].name); NSLog(@"%s",attr1[2].value); NSLog(@"-------------------"); NSLog(@"%s",attr1[3].name); NSLog(@"%s",attr1[3].value);
至於 V N & 這樣的符號是什麼意思,可以打開上面給出的鏈接自己看一下文檔,一看便知。
這樣一來在 switch 分支中,只要匹配到 T 就能得到這個 Property 的類型是什麼,這樣就可以得到這個類型的 Type Encoding,並且能夠得到該類的 Class。只要匹配到 V 就能得到這個 Property 實例變量名。
該類全部代碼如下:
- (instancetype)initWithProperty:(objc_property_t)property { if (!property) { return nil; } self = [super init]; if (self) { _property = property; const char *name = property_getName(property); if (name) { _name = [NSString stringWithUTF8String:name]; } CPEncodingType type = 0; unsigned int outCount; objc_property_attribute_t *attrs = property_copyAttributeList(property, &outCount); //遍歷所有的Property的屬性 for (unsigned int i = 0; i < outCount; i++) { switch (attrs[i].name[0]) { case 'T': if (attrs[i].value) { _typdEncoding = [NSString stringWithUTF8String:attrs[i].value]; type = CPEncodingGetType(attrs[i].value); if((type & CPEncodingTypeMask) == CPEncodingTypeObject){ //如果該類型為一個對象 比如 @"NSString" ,截取中間的,結果為 NSString,目的是為了得到這個類的 Class size_t len = strlen(attrs[i].value); if (len > 3) { char name[len - 2]; name[len - 3] = '\0'; memcpy(name, attrs[i].value + 2, len - 3); _cls = objc_getClass(name); } } } break; case 'V': if (attrs[i].value) { _ivarName = [NSString stringWithUTF8String:attrs[i].value]; } break; case 'R': type |= CPEncodingTypePropertyReadonly; break; case 'C': type |= CPEncodingTypePropertyCopy; break; case '&': type |= CPEncodingTypePropertyRetain; break; case 'N': type |= CPEncodingTypePropertyNonatomic; break; case 'D': type |= CPEncodingTypePropertyDynamic; break; case 'W': type |= CPEncodingTypePropertyWeak; break; case 'G': type |= CPEncodingTypePropertyCustomGetter; if (attrs[i].value) { _getter = NSSelectorFromString([NSString stringWithUTF8String:attrs[i].value]); } break; case 'S': type |= CPEncodingTypePropertyCustomSetter; if (attrs[i].value) { _setter = NSSelectorFromString([NSString stringWithUTF8String:attrs[i].value]); } break; default: break; } } if (attrs) { free(attrs); attrs = NULL; } _type = type; if (_name.length) { if (!_getter) { _getter = NSSelectorFromString(_name); } if (!_setter) { _setter = NSSelectorFromString([NSString stringWithFormat:@"set%@%@:",[_name substringToIndex:1].uppercaseString, [_name substringFromIndex:1]]); } } } return self; }
這樣一來,我們就有了 ivar Method Property 的封裝類。接下來,我們需要一個叫做CPClassInfo的類,來封裝一些類的信息,並且把以上三個類也封裝進去,用來描述整個類。
封裝 Class
繼續看頭:
#import #import @class CPClassIvarInfo; @class CPClassMethodInfo; @class CPClassPropertyInfo; NS_ASSUME_NONNULL_BEGIN @interface CPClassInfo : NSObject @property (nonatomic, assign, readonly) Class cls; @property (nonatomic, assign, readonly) Class superClass; @property (nonatomic, assign, readonly) Class metaClass; @property (nonatomic, readonly) BOOL isMeta; @property (nonatomic, strong, readonly) NSString *name; @property (nullable, nonatomic, strong, readonly) CPClassInfo *superClassInfo; @property (nullable, nonatomic, strong, readonly) NSDictionary*ivarInfos; @property (nullable, nonatomic, strong, readonly) NSDictionary*methodInfos; @property (nullable, nonatomic, strong, readonly) NSDictionary*propertyInfos; - (void)setNeedUpadte; - (BOOL)needUpdate; + (nullable instancetype)classInfoWithClass:(Class)cls; NS_ASSUME_NONNULL_END @end
Class 類型用來描述一個類,你可以使用
model.class [model class] [CPTestModel class] object_getClass(model)
等方法來取到這個 Class。·注意object_getClass()和其他方式 有些不同具體看這裡
其余的 Property 不用多介紹了,看到它們的名字就大概能猜到干嘛的了。
最後的幾個 NSDictionary 用來存所有的 ivar Method Property。
有些時候,一個類有可能被更改,可能改掉了方法或者是 Property,那麼這時候應該通知CPClassInfo來重新獲取到更改過後的類的信息。所以我們有兩個相關的方法來實現這個目的。
- (void)setNeedUpadte; - (BOOL)needUpdate;
先來看一下初始化方法
- (instancetype)initWithClass:(Class)cls{ if (!cls) { return nil; } self = [super init]; if (self) { _cls = cls; _superClass = class_getSuperclass(cls); _isMeta = class_isMetaClass(cls); if (_isMeta) { _metaClass = objc_getMetaClass(class_getName(cls)); } _name = NSStringFromClass(cls); [self _update]; _superClassInfo = [self.class classInfoWithClass:_superClass]; } return self; }
你沒看錯,這和頭文件定義的classInfoWithClass:不是一個方法,頭文件定義的那個方法用來緩存,因為實例化這個方法還是有點開銷的,所以沒有必要每一次都去實例化。
這裡有一個 _update 方法,剛才說過,如果這個類會在某一個時刻發生變化,應該通知,收到通知後,我們去執行一些更新的操作,所以把會發生變化的一部分代碼單獨拿出來更好,現在看一下 _update 方法。
- (void)_update{ _ivarInfos = nil; _propertyInfos = nil; _methodInfos = nil; unsigned int ivarCount = 0; Ivar *ivars = class_copyIvarList(self.cls, &ivarCount); if (ivars) { _ivarInfos = [NSMutableDictionary new]; for (unsigned int i = 0; i < ivarCount; i++) { CPClassIvarInfo *ivarInfo = [[CPClassIvarInfo alloc] initWithIvar:ivars[i]]; if (ivarInfo.name) { [_ivarInfos setValue:ivarInfo forKey:ivarInfo.name]; } } free(ivars); } unsigned int propertyCount = 0; objc_property_t *properties = class_copyPropertyList(self.cls, &propertyCount); if (properties) { _propertyInfos = [NSMutableDictionary new]; for (unsigned int i = 0; i < propertyCount; i++) { CPClassPropertyInfo *propertyInfo = [[CPClassPropertyInfo alloc] initWithProperty:properties[i]]; if (propertyInfo.name) { [_propertyInfos setValue:propertyInfo forKey:propertyInfo.name]; } } free(properties); } unsigned int methodCount = 0; Method *methods = class_copyMethodList(self.cls, &methodCount); if (methods) { _methodInfos = [NSMutableDictionary new]; for (unsigned int i = 0; i < methodCount; i++) { CPClassMethodInfo *methodInfo = [[CPClassMethodInfo alloc] initWithMethod:methods[i]]; if (methodInfo.name) { [_methodInfos setValue:methodInfo forKey:methodInfo.name]; } } free(methods); } if (!_ivarInfos) { _ivarInfos = @{}; } if (!_methodInfos) { _methodInfos = @{}; } if (!_propertyInfos) { _propertyInfos = @{}; } _needUpdate = NO; }
其實這個方法就是拿到一個類所有的 ivar Method Property ,一個類發生變化是不是主要就是這三個玩意的變化?
最後一行的 _needUpdate 是一個全局變量,用來標識是否發生的變化,它被定義在這裡,以免暴露給外面。
@implementation CPClassInfo{ BOOL _needUpdate; }
當外界需要通知自己已經發生變化或者查一下是否發生變化時就調用這兩個相關方法
- (BOOL)needUpdate { return _needUpdate; } - (void)setNeedUpadte { _needUpdate = YES; } 現在來看一下classInfoWithClass: + (instancetype)classInfoWithClass:(Class)cls{ if (!cls) { return nil; } static NSMutableDictionary *metaCache; static NSMutableDictionary *classCache; static dispatch_semaphore_t lock; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ metaCache = [NSMutableDictionary dictionary]; classCache = [NSMutableDictionary dictionary]; lock = dispatch_semaphore_create(1); }); dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER); CPClassInfo *info; if (class_isMetaClass(cls)) { info = [metaCache valueForKey:NSStringFromClass(cls)]; } else { info = [classCache valueForKey:NSStringFromClass(cls)]; } if (info && info->_needUpdate) { [info _update]; } dispatch_semaphore_signal(lock); if (!info) { info = [[CPClassInfo alloc] initWithClass:cls]; if (info) { dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER); if (info.isMeta) { [metaCache setValue:info forKey:NSStringFromClass(cls)]; } else { [classCache setValue:info forKey:NSStringFromClass(cls)]; } dispatch_semaphore_signal(lock); } } return info; }
兩個 NSMutableDictionary 都是用來緩存的,並聲明在了靜態區,並且使用dispatch_once()來確保只會被初始化一次,然後我們需要保證線程安全,因為有可能會在多線程的場景裡被用到,所以使用信號量dispatch_semaphore_t來搞定,信號量就像停車這樣的場景一樣,如果發現車滿了,就等待,一有空位就放行,也就是說,當一個線程要進入臨界區的時候,必須獲取一個信號量,如果沒有問題就進入臨界區,這時另一個線程進來了,也要獲取,發現信號量並沒有釋放,就繼續等待,直到前面一個信號量被釋放後,該線程才准許進入。我們可以使用dispatch_semaphore_wait()來獲取信號量,通過dispatch_semaphore_signal()來釋放信號量。
在這段代碼裡,我們首先確保要實例化的這個對象有沒有被緩存,用傳進來的 cls 作為 key,如果緩存命中,那直接取出緩存,然後判斷一下,有沒有更新,如果有更新,調用_update刷新一遍,返回,否則直接返回。緩存沒有命中的話,還是乖乖的調用實例化方法,然後緩存起來。
繼續封裝
CPModelPropertyMeta
先建一個文件,叫做 CPMeta.h 和 CPMeta.m,我們要在這裡寫兩個類,一個是對 Property 的再次封裝,一個是對 Class 的再次封裝。
我直接把頭文件代碼全拿出來了:
#import #import #import "CPCommon.h" @class CPClassInfo; @class CPClassPropertyInfo; typedef NS_ENUM (NSUInteger, CPEncodingNSType) { CPEncodingTypeNSUnknown = 0, CPEncodingTypeNSString, CPEncodingTypeNSMutableString, CPEncodingTypeNSValue, CPEncodingTypeNSNumber, CPEncodingTypeNSDecimalNumber, CPEncodingTypeNSData, CPEncodingTypeNSMutableData, CPEncodingTypeNSDate, CPEncodingTypeNSURL, CPEncodingTypeNSArray, CPEncodingTypeNSMutableArray, CPEncodingTypeNSDictionary, CPEncodingTypeNSMutableDictionary, CPEncodingTypeNSSet, CPEncodingTypeNSMutableSet, }; @interface CPModelMeta : NSObject{ @package CPClassInfo *_clsInfo; NSDictionary *_mapper; NSArray *_allPropertyMetas; NSUInteger _keyMappedCount; CPEncodingNSType _nsType; } + (instancetype)metaWithClass:(Class)cls; @end @interface CPModelPropertyMeta : NSObject{ @package NSString *_name; CPEncodingType _type; CPEncodingNSType _nsType; BOOL _isCNumber; Class _cls; Class _genericCls; SEL _getter; SEL _setter; BOOL _isKVCCompatible; NSString *_mappedToKey; CPClassPropertyInfo *_info; } + (instancetype)modelWithClassInfo:(CPClassInfo *)clsInfo propretyInfo:(CPClassPropertyInfo *)propertyInfo generic:(Class)generic; @end
可以看到這裡有兩個類,姑且叫做 CPModelPropertyMeta 和 CPModelMeta 以及一個枚舉,這個枚舉表示一個NS的類型,因為在上一個枚舉當中,我們對於對象只定義了 CPEncodingTypeObject 這一個類型,沒法區分它到底是 NSString 還是別的,所以這裡要細化一下,類型判斷清楚很重要,如果不把這部分做好,那麼在JSON轉換的時候,類型上出錯就直接蹦了。
先來看一下 CPModelPropertyMeta 。(在 YYModel 中,這兩個類其實是和一個叫做NSObject+CPModel的擴展放在一起的,但是我強制把它們拆出來了,為了看起來清楚,所以我把 @package 的成員變量都寫到了 interface 裡面,這麼做是不合理的,但這裡為了清晰和學習起見,所以我亂來了。)這個類中多了幾個成員變量,我就說幾個看起來不那麼清楚的成員變量。
_isCNumber 這裡變量表示是不是一個C語言的類型,比如int這樣的。
_genericCls這個變量在精簡版裡沒用到,我只是放在這裡,YYModel 可以給容器型的屬性轉換,具體可以看YY大神的文檔。
_isKVCCompatible 能不能支持 KVC
_mappedToKey 要映射的 key,把 JSON 轉成 Model 的時會根據這個 key 把相同字段的 JSON 值賦值給這個 Property。
為了判斷 NS 的類型和是否是 C 類型,在 .m 裡有兩個函數
#define force_inline __inline__ __attribute__((always_inline)) static force_inline CPEncodingNSType CPClassGetNSType(Class cls) { if (!cls) return CPEncodingTypeNSUnknown; if ([cls isSubclassOfClass:[NSMutableString class]]) return CPEncodingTypeNSMutableString; if ([cls isSubclassOfClass:[NSString class]]) return CPEncodingTypeNSString; if ([cls isSubclassOfClass:[NSDecimalNumber class]]) return CPEncodingTypeNSDecimalNumber; if ([cls isSubclassOfClass:[NSNumber class]]) return CPEncodingTypeNSNumber; if ([cls isSubclassOfClass:[NSValue class]]) return CPEncodingTypeNSValue; if ([cls isSubclassOfClass:[NSMutableData class]]) return CPEncodingTypeNSMutableData; if ([cls isSubclassOfClass:[NSData class]]) return CPEncodingTypeNSData; if ([cls isSubclassOfClass:[NSDate class]]) return CPEncodingTypeNSDate; if ([cls isSubclassOfClass:[NSURL class]]) return CPEncodingTypeNSURL; if ([cls isSubclassOfClass:[NSMutableArray class]]) return CPEncodingTypeNSMutableArray; if ([cls isSubclassOfClass:[NSArray class]]) return CPEncodingTypeNSArray; if ([cls isSubclassOfClass:[NSMutableDictionary class]]) return CPEncodingTypeNSMutableDictionary; if ([cls isSubclassOfClass:[NSDictionary class]]) return CPEncodingTypeNSDictionary; if ([cls isSubclassOfClass:[NSMutableSet class]]) return CPEncodingTypeNSMutableSet; if ([cls isSubclassOfClass:[NSSet class]]) return CPEncodingTypeNSSet; return CPEncodingTypeNSUnknown; } static force_inline BOOL CPEncodingTypeIsCNumber(CPEncodingType type) { switch (type & CPEncodingTypeMask) { case CPEncodingTypeBool: case CPEncodingTypeInt8: case CPEncodingTypeUInt8: case CPEncodingTypeInt16: case CPEncodingTypeUInt16: case CPEncodingTypeInt32: case CPEncodingTypeUInt32: case CPEncodingTypeInt64: case CPEncodingTypeUInt64: case CPEncodingTypeFloat: case CPEncodingTypeDouble: case CPEncodingTypeLongDouble: return YES; default: return NO; } }
這兩個函數不用多說了,很簡單,要說明一下宏定義 force_inline 所有標記了 force_inline 的函數叫做內聯函數,在調用的時候都不是一般的調用,而是在編譯的時候就已經整個丟進了調用這個函數的方法或函數裡去了,這和平時定義一個宏一樣,你在哪裡使用到了這個宏,那麼在編譯的時候編譯器就會把你使用這個宏的地方替換成宏的值。為什麼要這麼做呢?因為效率,調用一個函數也是有開銷的,調用一個函數有壓棧彈棧等操作。如果你的函數很小,你這麼一弄就免去了這些操作。
然後看一下CPModelPropertyMeta的初始化方法
+ (instancetype)modelWithClassInfo:(CPClassInfo *)clsInfo propretyInfo:(CPClassPropertyInfo *)propertyInfo generic:(Class)generic{ CPModelPropertyMeta *meta = [self new]; meta->_name = propertyInfo.name; meta->_type = propertyInfo.type; meta->_info = propertyInfo; meta->_genericCls = generic; if ((meta->_type & CPEncodingTypeMask) == CPEncodingTypeObject) { meta->_nsType = CPClassGetNSType(propertyInfo.cls); } else { meta->_isCNumber = CPEncodingTypeIsCNumber(meta->_type); } meta->_cls = propertyInfo.cls; if (propertyInfo.getter) { if ([clsInfo.cls instancesRespondToSelector:propertyInfo.getter]) { meta->_getter = propertyInfo.getter; } } if (propertyInfo.setter) { if ([clsInfo.cls instancesRespondToSelector:propertyInfo.setter]) { meta->_setter = propertyInfo.setter; } } if (meta->_setter && meta->_getter) { switch (meta->_type & CPEncodingTypeMask) { case CPEncodingTypeBool: case CPEncodingTypeInt8: case CPEncodingTypeUInt8: case CPEncodingTypeInt16: case CPEncodingTypeUInt16: case CPEncodingTypeInt32: case CPEncodingTypeUInt32: case CPEncodingTypeInt64: case CPEncodingTypeUInt64: case CPEncodingTypeFloat: case CPEncodingTypeDouble: case CPEncodingTypeObject: case CPEncodingTypeClass: case CPEncodingTypeBlock: case CPEncodingTypeStruct: case CPEncodingTypeUnion: { meta->_isKVCCompatible = YES; } break; default: break; } } return meta; }
判斷一下是否是 object 的類型,然後拿到具體的 NS 類型,或者判斷一下是不是 C 類型,然後拿到 getter setter 最後判斷一下能不能 KVC。
CPModelPropertyMeta
這個類主要是生成一個映射表,這個映射表就是 _mapper 這個變量,這個類也需要被緩存起來,套路和上面講到的緩存套路一樣
+ (instancetype)metaWithClass:(Class)cls { if (!cls) return nil; static CFMutableDictionaryRef cache; static dispatch_once_t onceToken; static dispatch_semaphore_t lock; dispatch_once(&onceToken, ^{ cache = CFDictionaryCreateMutable(CFAllocatorGetDefault(), 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); lock = dispatch_semaphore_create(1); }); dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER); CPModelMeta *meta = CFDictionaryGetValue(cache, (__bridge const void *)(cls)); dispatch_semaphore_signal(lock); if (!meta || meta->_clsInfo.needUpdate) { meta = [[CPModelMeta alloc] initWithClass:cls]; if (meta) { dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER); CFDictionarySetValue(cache, (__bridge const void *)(cls), (__bridge const void *)(meta)); dispatch_semaphore_signal(lock); } } return meta; }
緩存沒命中就調用 initWithClass: 來進行初始化
- (instancetype)initWithClass:(Class)cls{ if (!cls) { return nil; } self = [super init]; if (self) { CPClassInfo *clsInfo = [CPClassInfo classInfoWithClass:cls]; NSMutableDictionary *allPropertyMetas = [NSMutableDictionary new]; CPClassInfo *curClsInfo = clsInfo; //連同當前的類和其父類的屬性一起放入allPropertyMetas數組,(NSObject和NSProxy是沒有父類的) while (curClsInfo && curClsInfo.superClass != nil) { for (CPClassPropertyInfo *propertyInfo in curClsInfo.propertyInfos.allValues) { if (!propertyInfo.name)continue; CPModelPropertyMeta *meta = [CPModelPropertyMeta modelWithClassInfo:clsInfo propretyInfo:propertyInfo generic:nil]; if (!meta || !meta->_name)continue; if (!meta->_setter || !meta->_getter)continue; if (allPropertyMetas[meta->_name])continue; allPropertyMetas[meta->_name] = meta; } curClsInfo = clsInfo.superClassInfo; } if (allPropertyMetas.count) { _allPropertyMetas = allPropertyMetas.allValues.copy; } NSMutableDictionary *mapper = [NSMutableDictionary new]; [allPropertyMetas enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull name, CPModelPropertyMeta * _Nonnull meta, BOOL * _Nonnull stop) { meta->_mappedToKey = name; mapper[name] = meta; }]; if (mapper.count) _mapper = mapper; _clsInfo = clsInfo; _keyMappedCount = _allPropertyMetas.count; _nsType = CPClassGetNSType(cls); } return self; }
把 CPClassInfo 裡所有的 propertyInfo 遍歷出來,實例化成一個 CPModelPropertyMeta ,還順便把 CPClassInfo 父類的所有 propertyInfo 也拿出來,這樣一來,你的 Model 即便有一個父類也能把父類的 Property 賦值。
然後生成一個映射表,就基本完成了初始化工作了,這張映射表是關鍵,等一下所有的 JSON 的轉換都依賴這一張表。
從 JSON 到 Model 的轉換
現在進入正餐環節,我們剛才已經把所有的准備工作完成了,現在要開始正式的完成從 JSON 到 Model 的轉換了。
首先,先建一個 Category,取名 CPModel,因為我們只完成整個 YYMode 的一個主要功能,所以我們只給出一個接口就行了,所以頭文件很簡單。
#import @interface NSObject (CPModel) + (instancetype)modelWithJSON:(id)json; @end
使用者只需要調用 + modelWithJSON: 即可完成轉換的操作。
現在看看這個方法要怎麼實現:
+ (instancetype)modelWithJSON:(id)json { NSDictionary *dic = [self _cp_dictionaryWithJSON:json]; if (!dic || dic == (id)kCFNull) { return nil; } if (![dic isKindOfClass:[NSDictionary class]]) { return nil; } Class cls = [self class]; NSObject *one = [cls new]; if ([one modelSetWithDictionary:dic]) { return one; } return nil; }
首先先把 JSON 轉換成 NSDictionary ,然後得到該 Model 的 Class 去實例化這個 Model,接著調用一個叫做- modelSetWithDictionary: 的方法。
把 JSON 轉換成 NSDictionary 的方法很簡單
+ (NSDictionary *)_cp_dictionaryWithJSON:(id)json{ if (!json || json == (id)kCFNull) { return nil; } NSDictionary *dic = nil; NSData *data = nil; if ([json isKindOfClass:[NSDictionary class]]) { dic = json; }else if ([json isKindOfClass:[NSString class]]) { data = [(NSString *)json dataUsingEncoding:NSUTF8StringEncoding]; }else if ([json isKindOfClass:[NSData class]]) { data = json; } if (data) { dic = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableLeaves error:nil]; if (![dic isKindOfClass:[NSDictionary class]]) { dic = nil; } } return dic; }
然後看一下 - modelSetWithDictionary:
- (BOOL)modelSetWithDictionary:(NSDictionary *)dic{ if (!dic || dic == (id)kCFNull) { return NO; } if (![dic isKindOfClass:[NSDictionary class]]) { return NO; } CPModelMeta *meta = [CPModelMeta metaWithClass:object_getClass(self)]; //① if (meta->_keyMappedCount == 0) { return NO; } ModelSetContext context = {0}; context.modelMeta = (__bridge void *)(meta); context.model = (__bridge void *)(self); context.dictionary = (__bridge void *)(dic); if (meta->_keyMappedCount >= dic.count) { CFDictionaryApplyFunction((CFDictionaryRef)dic, ModelSetWithDictionaryFunction, &context); } return YES; }
這裡有一個結構體,這個結構體用來存儲 model(因為是給這個Model 裡的 Property 賦值)、modelMeta(剛才也看到了,這裡存放了映射表)、dictionary(這是由 JSON 轉換過來的),這個結構體的定義如下:
typedef struct { void *modelMeta; void *model; void *dictionary; } ModelSetContext;
然後在- modelSetWithDictionary:有這麼一行代碼
CFDictionaryApplyFunction((CFDictionaryRef)dic, ModelSetWithDictionaryFunction, &context);
這個代碼的作用是,把一對 Key - Value 拿出來,然後調用你傳進去的函數ModelSetWithDictionaryFunction(),你有多少對Key - Value,它就會調用多少次這個函數,相當於便利所有的Key - Value,為什麼要這樣做,而不用一個循環呢?在作者的博客裡有這麼一段
遍歷容器類時,選擇更高效的方法
相對於 Foundation 的方法來說,CoreFoundation 的方法有更高的性能,用 CFArrayApplyFunction() 和 CFDictionaryApplyFunction() 方法來遍歷容器類能帶來不少性能提升,但代碼寫起來會非常麻煩。
然後我們來看一下ModelSetWithDictionaryFunction()的實現
static void ModelSetWithDictionaryFunction(const void *key, const void *value, void *context) { ModelSetContext *ctx = context; __unsafe_unretained CPModelMeta *modelMeta = (__bridge CPModelMeta *)(ctx->modelMeta); __unsafe_unretained CPModelPropertyMeta *propertyMeta = [modelMeta->_mapper objectForKey:(__bridge id)(key)]; __unsafe_unretained id model = (__bridge id)(ctx->model); if (propertyMeta->_setter) { ModelSetValueForProperty(model, (__bridge __unsafe_unretained id)value, propertyMeta); } }
為什麼在變量前都加了__unsafe_unretained,作者也說了
避免多余的內存管理方法
在 ARC 條件下,默認聲明的對象是 strong 類型的,賦值時有可能會產生 retain/release 調用,如果一個變量在其生命周期內不會被釋放,則使用 unsafe_unretained 會節省很大的開銷。
訪問具有 weak 屬性的變量時,實際上會調用 objc_loadWeak() 和 objc_storeWeak() 來完成,這也會帶來很大的開銷,所以要避免使用 weak 屬性。
繼續,根據 key(這個 key 就是 JSON 裡的字段,應該和你 Model 定義的 Property 名相同,否則就匹配不了,在 YYMode 中有一個自定義映射表的支持,我把它去掉了,有興趣的可以下載 YYMode 的源碼看一下) 取出映射表裡的 propertyMeta。現在我們有了要轉換的 model 對象,和一個和 JSON 裡字段對應的 propertyMeta 對象,已經該 JSON 字段的值,現在要賦值的條件全部具備了,我們只需要調用propertyMeta中的setter方法,然後把值傳進去就完成了,這部分的工作由 ModelSetValueForProperty()函數完成,這個函數裡有大量的類型判斷,為了簡單起見,我就判斷了NSString NSNumber 和普通C語言類型,代碼如下:
static void ModelSetValueForProperty(__unsafe_unretained id model, __unsafe_unretained id value, __unsafe_unretained CPModelPropertyMeta *meta) { if (meta->_isCNumber) { NSNumber *num = CPNSNumberCreateFromID(value); ModelSetNumberToProperty(model, num, meta); if (num) [num class]; } else if (meta->_nsType) { if (value == (id)kCFNull) { ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, (id)nil); } else { switch (meta->_nsType) { case CPEncodingTypeNSString: case CPEncodingTypeNSMutableString: { if ([value isKindOfClass:[NSString class]]) { if (meta->_nsType == CPEncodingTypeNSString) { ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, value); } else { ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, ((NSString *)value).mutableCopy); } } else if ([value isKindOfClass:[NSNumber class]]) { ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, (meta->_nsType == CPEncodingTypeNSString) ? ((NSNumber *)value).stringValue : ((NSNumber *)value).stringValue.mutableCopy); } else if ([value isKindOfClass:[NSData class]]) { NSMutableString *string = [[NSMutableString alloc] initWithData:value encoding:NSUTF8StringEncoding]; ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, string); } else if ([value isKindOfClass:[NSURL class]]) { ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, (meta->_nsType == CPEncodingTypeNSString) ? ((NSURL *)value).absoluteString : ((NSURL *)value).absoluteString.mutableCopy); } else if ([value isKindOfClass:[NSAttributedString class]]) { ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, (meta->_nsType == CPEncodingTypeNSString) ? ((NSAttributedString *)value).string : ((NSAttributedString *)value).string.mutableCopy); } } break; case CPEncodingTypeNSNumber:{ if ([value isKindOfClass:[NSNumber class]]) { if (meta->_nsType == CPEncodingTypeNSNumber) { ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model,meta->_setter,value); } } } break; default: break; } } } }
關於 objc_msgSend() 我們隨便拿一行例子來舉例,比如這個:
((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, value);
這是一個可以調用者決定返回值和參數的函數,一般的函數是做不到的,默認情況下這個函數是長這樣
objc_msgSend(id, SEL)
id 是指調用某一個方法的對象,在這裡這個對象就是你的 Model
SEL 是指你這個對象要調用的方法是什麼,在這裡這個方法就是 setter方法
然而,setter 方法是有參數的,這個參數怎麼傳進去?這就需要強制類型轉換了,我們把這個函數強制轉換成這個模樣:
((void (*)(id, SEL, id))(void *) objc_msgSend)
這樣代表這個函數是一個沒有返回值,並且有3個參數的函數,分別是 id SEL id,前面兩個參數之前講過了,第三個參數就是你要調用的這個 setter 方法需要的參數,所以經過強制類型轉換之後的變異版就成了一開始的那種樣子。
其余的都沒有什麼好講的了,都很簡單,都是一些煩人的類型判斷,只要仔細一點一行行看就能看懂了。
全部搞定以後,和原版的 YYModel 一樣,你可以這麼來測試
CPTestModel *model = [CPTestModel modelWithJSON:@"{\"name\": \"Harry Potter\",\"index\": 512,\"number\": 10,\"num\": 100}"];
結尾
如果你自己親自動手寫完了這個精簡版的 YYMode ,你再去看完整版的會容易很多,我寫的這篇文章是把我從讀 YYModel 源碼中學到的一些有用的東西分享給大家,如有什麼寫錯的地方,歡迎指正。
完整代碼
點擊這裡