《YYModel原始碼分析(一)YYClassInfo》

我是繁星發表於2019-04-14

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我們下一章再說。小弟不才,如有誤區請一定及時指出。

相關文章