《YYModel源碼分析(一)YYClassInfo》

NO IMAGE

YYModel大家肯定很熟悉,其非侵入性,易用性都使得它成為json-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我們下一章再說。小弟不才,如有誤區請一定及時指出。

相關文章

如何實現一個JavaClass解析器

自己動手實現一個UnixShell

SpringIoC容器初始化—Resource定位源碼分析

《YYModel源碼分析(二)NSObject+YYModel》