YYModel大家肯定很熟悉,其非侵入性,易用性都使得它成為josn-Model的新寵,接下來我們們分析下他的原理。
必須要了解的知識
-
先看YYClassInfo這個類,他是一個runtime中Class在OC層的封裝,並且解析增加了很多描述,所以想了解YYModel原理必須對runtime有一定了解。
-
在runtime層型別其實是一個結構體objc_class,objc_class中儲存著指向超類的superClass、指向所屬型別的ISA、指向class_rw_t的指標,class_rw_t中儲存著這個類的成員變數列表、屬性列表和方法列表。所以其實我們是可以通過runtime的api去讀取類的這些資訊的。
-
編譯器會以一定規則對型別進行編碼,並且儲存在runtime資料結構中,所以我們可以根據規則解析出屬性、成員變數和方法引數的型別 《官方文件》
YYEncodingType
YYEncodingType 代表的是typeEncoding所代表的型別。通過YYEncodingType YYEncodingGetType(const char *typeEncoding)
方法可以將字串轉換成一個代表具體型別的列舉值。
YYEncodingType不僅僅代表型別,他是一個按位列舉,還儲存類一些屬性描述資訊。
YYClassIvarInfo
成員變數在runtime層表現為Ivar這個型別,通過Ivar可以讀取變數名,等等資訊
@interface YYClassIvarInfo : NSObject
//注意這裡用assign修飾了,因為runtime層的資料結構都不歸引用計數管理
@property (nonatomic, assign, readonly) Ivar ivar; ///runtime中成員變數
@property (nonatomic, strong, readonly) NSString * name; // 成員變數名字
@property (nonatomic, assign, readonly) ptrdiff_t offset; ///偏移量
@property (nonatomic, strong, readonly) NSString *typeEncoding; //型別編碼
@property (nonatomic, assign, readonly) YYEncodingType type; //由型別編碼解析出的資訊
//解析Ivar資訊
-(instancetype)initWithIvar:(Ivar)ivar;
@end
複製程式碼
這個類中主要用到的runtime介面有
ivar_getName() //獲取成員變數名
ivar_getOffset() //獲取偏移量
ivar_getTypeEncoding() //獲取成員變數的型別編碼
複製程式碼
YYClassMethodInfo
methodInfo中包含的資訊要多一點
@interface YYClassMethodInfo : NSObject
@property (nonatomic, assign, readonly) Method method; //runtime Method資料
@property (nonatomic, assign, 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 (nonatomic, nullable, strong, readonly) NSArray<NSString *> *argumentTypeEncoding; //引數型別陣列,用陣列表示
- (instancetype)initWithMethod:(Method)method;
@end
複製程式碼
主要用到的runtimeApi有
method_getName()
method_getImplementation()
method_getTypeEncoding()
method_copyReturnMethod()
method_getNumberOfArguments() //獲取引數數量
method_copyArgumentType(method,i) //獲取方法第I個引數的型別編碼
複製程式碼
YYClassPropertyInfo
我們先分析一下類的屬性中包含什麼樣的資訊,包含了成員變數、遵循的協議、還有描述屬性的關鍵字例如記憶體管理方面的copy、strong、weak等,還要讀寫的readOnly等。還有預設生成的setter和getter方法。這些都需要解析出來
@interface YYClassPropertyInfo : NSObject
@property (nonatomic, assign, readonly) objc_property_t property; //runtime中屬性資料
@property (nonatomic, strong, readonly) NSString * name; //屬性名
@property (nonatomic, assign, readonly) YYEncodingType type; //型別列舉
@property (nonatomic, strong, readonly) NSString * typeEncoding; //型別編碼
@property (nonatomic, strong, readonly) NSString * ivarName; //成員變數名字
@property (nonatomic, nullable, assign, readonly) Class cls; //所屬型別,這裡需要直接解析出來
@property (nonatomic, nullable, strong, readonly) NSArray<NSString *> *protocols; //遵循的協議
@property (nonatomic, assign, readonly) SEL getter; //生成的getter方法
@property (nonatomic, assign, readonly) SEL setter; //生成的setter方法
- (instancetype)initWithProperty:(objc_property_t)property;
@end
複製程式碼
屬性的解析過程是這裡面最長的,因為涉及到encoding字串的解析。比如型別的解析。我們可以通過property_copyAttributeList
方法獲取屬性的描述objc_property_attribute_t資料結構大概是一個陣列,陣列的元素是一個map,而且不用的key對應的是不同的含義,比如"T"代表的屬性的型別編碼,"V"代表的是成員變數的名字,等等。我們著重看一下解析型別和協議的位置。
case 'T'://代表type encoding
if (attrs[i].value){
_typeEncoding = [NSString stringWithUTF8String:attrs[i].value];
//轉成YYEncodingType
type = YYEncodingGetType(attrs[i].value);
//如果型別是oc物件,把OC物件解析出來,例如:@"NSString"
if ((type & YYEncodingTypeMask) == YYEncodingTypeObject && _typeEncoding.length){
NSScanner * scanner = [NSScanner scannerWithString:_typeEncoding];
//先把掃描位置移動到"
if (![scanner scanString:@"@\"" intoString:NULL]) continue;
NSString *clsName = nil;
//然後三秒至存在"或者<的位置,這是因為如果遵循了協議typeEncode就是@"NSString<NSCopy>"了
if ([scanner scanUpToCharactersFromSet:[NSCharacterSet characterSetWithCharactersInString:@"\"<"] intoString:&clsName]){
if (clsName.length) _cls = objc_getClass(clsName.UTF8String);
}
NSMutableArray *protocols = nil;
//如果遵循多個協議typecoding是這樣的@"NSString<NSCopy><NSObject>",所以將掃描位置移動到<位置,迴圈擷取
while ([scanner scanString:@"<" intoString:NULL]) {
NSString * protocol = nil;
if ([scanner scanUpToString:@">" intoString:&protocol]){
if (protocol.length){
if (!protocols) protocols = [NSMutableArray new];
[protocols addObject:protocol];
}
[scanner scanString:@">" intoString:NULL];
}
protocols = protocols;
}
}
}
break;
複製程式碼
一個OC型別編碼之後是這樣的以NSString為例,@"NSString"
,如果遵循了協議是這樣的,@"NSString"如果遵循了多個協議是這樣的@"NSString",所以以上程式碼也是依據與此展開的。
我們再看一下如何獲取的get和set方法
if (_name.length){
if (!_getter){
_getter = NSSelectorFromString(_name);
}
if (!_setter){
_setter = NSSelectorFromString([NSString stringWithFormat:@"set%@%@",[_name substringFromIndex:1].uppercaseString,[_name substringFromIndex:1]]);
}
}
複製程式碼
這個很簡單其實就是根據屬性名,首字元大些,然後在前面加上get和set,哈哈,是不是很厲害。
YYClassInfo
那麼其實上面最重要的三部分都看完了,YYClassInfo就很簡單了
@interface YYClassInfo : NSObject
@property (nonatomic, assign, readonly) Class cls; //所屬型別
@property (nullable, nonatomic, assign, readonly) Class superCls; //超類
@property (nullable, nonatomic, assign, readonly) Class metaCls; //元類
@property (nonatomic, readonly) BOOL isMetal; //是否是元類
@property (nonatomic, strong, readonly) NSString * name; //類名
@property (nullable, nonatomic, strong, readonly) YYClassInfo * superClassInfo; //超類的classInfo
@property (nullable, nonatomic, strong, readonly) NSDictionary<NSString *, YYClassIvarInfo *> *ivarInfos; //屬性集合,以字典的形式儲存,key是成員變數名
@property (nullable, nonatomic, strong, readonly) NSDictionary<NSString *, YYClassMethodInfo *> *methodInfos; //方法集合,以字典形式儲存,key是方法名
@property (nullable, nonatomic, strong, readonly) NSDictionary<NSString *, YYClassPropertyInfo *> *propertyInfos; //屬性名,以字典形式儲存,key是屬性名
//設定需要update類資訊。
- (void)setNeedUpdate;
//獲取是否需要update類資訊
- (BOOL)needUpdate;
//根據cls獲取解析資料YYClassInfo
+ (nullable instancetype)classInfoWithClass:(Class)cls;
//根據className獲取解析資料YYClassInfo
+ (nullable instancetype)classInfoWithClassName:(NSString *)className;
@end
複製程式碼
那麼我們通過classInfo解析一個型別都經過了哪些過程呢,首先會從快取中讀取是否有快取資料,這個快取是一個靜態全域性變數,如果快取中有判斷是否需要更新類資料,如果需要更新重新解析,如果快取中沒有資料,那麼解析類資料,然後遞迴解析超類資料,直到超類為nil,NSObject的superClass就為nil。這個地方看似需要遞迴很多,但是我們通常的model都是直接繼承自NSObject的,所以基本就兩次左右。 我們看一下核心程式碼,基本都是呼叫的runtimeApi
- (void)_update{
_ivarInfos = nil;
_methodInfos = nil;
_propertyInfos = nil;
Class cls = self.cls;
unsigned int methodCount = 0;
Method *methods = class_copyMethodList(cls, &methodCount);
if (methods){
NSMutableDictionary * methodInfo = [NSMutableDictionary new];
_methodInfos = _methodInfos;
for (unsigned int i = 0; i < methodCount; i++){
YYClassMethodInfo * info = [[YYClassMethodInfo alloc] initWithMethod:methods[i]];
if (info.name) methodInfo[info.name] = info;
}
free(methods);
}
unsigned int propertyCount = 0;
objc_property_t * properties = class_copyPropertyList(cls, &propertyCount);
if (properties){
NSMutableDictionary * propertyInfos = [NSMutableDictionary new];
_propertyInfos = propertyInfos;
for (unsigned int i = 0; i<propertyCount; i++){
YYClassPropertyInfo * info = [[YYClassPropertyInfo alloc] initWithProperty:properties[i]];
if (info.name) propertyInfos[info.name] = info;
}
free(properties);
}
unsigned int ivarCount = 0;
Ivar *ivars = class_copyIvarList(cls, &ivarCount);
if (ivars){
NSMutableDictionary * ivarInfos = [NSMutableDictionary new];
_ivarInfos = ivarInfos;
for (unsigned int i = 0; i<ivarCount; i++){
YYClassIvarInfo * info = [[YYClassIvarInfo alloc] initWithIvar:ivars[i]];
if (info.name) ivarInfos[info.name] = info;
}
free(ivars);
}
if (!_ivarInfos) _ivarInfos = @{};
if (!_methodInfos) _methodInfos = @{};
if (!_propertyInfos) _propertyInfos = @{};
_needUpdate = NO;
}
複製程式碼
總結
由YYClassInfo我們能看出作者對runtime的理解之深,通過對runtime類結構的封裝,我們可以方便的獲取到一個類的各種資訊。json轉model也就沒有那麼難了,關於NSObject+YYModel我們下一章再說。小弟不才,如有誤區請一定及時指出。