iOS runtime (二)(runtime學習之AutoCoding源碼分析)

  在上一篇文章中,學習了runtime中的各個重要的知識點,接下來就是要開始運用了。主要是分析一些優秀開源庫是如何運用runtime,提升工做效率的。html


   AutoCodinggit

  AutoCoding 是一個NSObject的類別,它提供歸檔解檔(即自動序列化和反序列化)對象。在介紹這個開源庫以前,先簡單過一下iOS中對象歸檔解檔,這個並非重點,只是爲了拋出問題,因此不會詳講。在iOS中對象序歸檔解檔會使用到NSKeyedArchiver和NSKeyedUnarchiver,接下來是示例代碼:github

建立一個須要歸檔解檔的對象的類,它須要遵循NSSecureCoding協議並實現相應接口,決定它是如何歸檔解檔對象的成員變量。算法

//MyDataInfo.h
#import <Foundation/Foundation.h>

@interface MyDataInfo : NSObject <NSSecureCoding>
@property (nonatomic, strong) NSString *name;
@property (nonatomic, assign) NSInteger age;
@end
//MyDataInfo.m
#import "MyDataInfo.h"
NSString *const kNameKey = @"name";
NSString *const kAgeKey = @"age";
@implementation MyDataInfo
- (void)encodeWithCoder:(NSCoder *)aCoder
{
    [aCoder encodeObject:self.name forKey:kNameKey];
    [aCoder encodeInteger:self.age forKey:kAgeKey];
}

- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder
{
    self.name = [aDecoder decodeObjectForKey:kNameKey];
    self.age = [aDecoder decodeIntegerForKey:kAgeKey];
    return self;
}

+ (BOOL)supportsSecureCoding
{
    return YES;
}
@end

接下來就是對這個類進行歸檔解檔的過程緩存

//ViewController.m
.......
- (void)viewDidLoad {
    [super viewDidLoad];
    MyDataInfo *dataInfo = [[MyDataInfo alloc] init];
    dataInfo.name = @"Davi";
    dataInfo.age = 100;
    
    NSData *archivedData = [NSKeyedArchiver archivedDataWithRootObject:dataInfo];
    
    MyDataInfo *unArchiveDataInfo = [NSKeyedUnarchiver unarchiveObjectWithData:archivedData];
    
    NSLog(@"unArchiveDataInfo name:%@, age:%ld", unArchiveDataInfo.name, (long)unArchiveDataInfo.age);
}
.......

運行代碼,輸出結果爲,結果是毫無疑問的app

2016-06-30 20:16:10.482 TestAutoCoding[59320:54257381] unArchiveDataInfo name:Davi, age:100

那麼要拋出的問題什麼呢?在平常開發當中,咱們要保存的對象,它們可能會有組合的關係,也有繼承的關係,也有可能有N多屬性,而且也會屬於不少不一樣的類,那麼此時若是要歸檔解檔這些對象,都須要像MyDataInfo同樣遵循NSSecureCoding協議並實現相應接initWithCoder:,encodeWithCoder:,爲每一個自屬性定義一個key,相似於NSString *const kNameKey = @"name",而後在encodeWithCoder:接口內寫encode代碼,在initWithCoder:接口內寫ide

decode代碼。當須要歸檔解檔的類有不少不少,這部分就是一個重複的苦力活,並且會容易出錯。AutoCoding就是爲解決這個問題出現的。github上對它的描述是這樣的學習

 

AutoCoding is a category on NSObject that provides automatic support for NSCoding to any object. This means that rather than having to implement the initWithCoder: and encodeWithCoder: methods yourself, all the model classes in your app can be saved or loaded from a file without you needing to write any additional code.

 意思就是你不須要爲要歸檔解檔的model類寫任何代碼,就能實現上面MyDataInfo說的歸檔存檔。那麼它是怎麼作到的?atom

首先,在NSObject (AutoCoding) 類別中實現如下兩個接口:spa

- (instancetype)initWithCoder:(NSCoder *)aDecoder
{
    [self setWithCoder:aDecoder];    //後續講解
    return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder
{
    for (NSString *key in [self codableProperties])  //後續講解
    {
        id object = [self valueForKey:key];
        if (object) [aCoder encodeObject:object forKey:key];
    }
}

這樣完成了第一步,要歸檔解檔的model類只要沒有實現這兩個方法,都會被調用到NSObject 類別以上這兩個方法。接下先說歸檔,在encodeWithCoder:接口內能夠看到[self codableProperties],它的做用是拿到對象(它所屬類的繼承體系中除NSObject外)的全部屬性名及類型。

- (NSDictionary *)codableProperties
{
    //添加一個字典關聯到這個類上,目的是作緩存,只要作一次獲取成員變量名字,和類型。其中名字爲key,類型爲對應值value
    __autoreleasing NSDictionary *codableProperties = objc_getAssociatedObject([self class], _cmd);
    if (!codableProperties)
    {
        codableProperties = [NSMutableDictionary dictionary];
        Class subclass = [self class];
        //從當前類開始向父類遍歷,一直到NSObject類就中止
        while (subclass != [NSObject class])
        {
            //獲取當前類全部的變量名-類型,添加到字典codableProperties裏
            [(NSMutableDictionary *)codableProperties addEntriesFromDictionary:[subclass codableProperties]/*緊跟着講*/];
            subclass = [subclass superclass];
        }
        codableProperties = [NSDictionary dictionaryWithDictionary:codableProperties];
        
        objc_setAssociatedObject([self class], _cmd, codableProperties, OBJC_ASSOCIATION_RETAIN);
    }
    return codableProperties;
}

接下來就是[subclass codableProperties],它的做用獲取類中全部的變量名、類型,以字典做爲返回值

+ (NSDictionary *)codableProperties
{
    //deprecated
    SEL deprecatedSelector = NSSelectorFromString(@"codableKeys");
    if ([self respondsToSelector:deprecatedSelector] || [self instancesRespondToSelector:deprecatedSelector])
    {
        NSLog(@"AutoCoding Warning: codableKeys method is no longer supported. Use codableProperties instead.");
    }
    deprecatedSelector = NSSelectorFromString(@"uncodableKeys");
    if ([self respondsToSelector:deprecatedSelector] || [self instancesRespondToSelector:deprecatedSelector])
    {
        NSLog(@"AutoCoding Warning: uncodableKeys method is no longer supported. Use ivars, or synthesize your properties using non-KVC-compliant names to avoid coding them instead.");
    }
    deprecatedSelector = NSSelectorFromString(@"uncodableProperties");
    NSArray *uncodableProperties = nil;
    if ([self respondsToSelector:deprecatedSelector] || [self instancesRespondToSelector:deprecatedSelector])
    {
        uncodableProperties = [self valueForKey:@"uncodableProperties"];
        NSLog(@"AutoCoding Warning: uncodableProperties method is no longer supported. Use ivars, or synthesize your properties using non-KVC-compliant names to avoid coding them instead.");
    }
    
    unsigned int propertyCount;
    __autoreleasing NSMutableDictionary *codableProperties = [NSMutableDictionary dictionary];
    objc_property_t *properties = class_copyPropertyList(self, &propertyCount);
    for (unsigned int i = 0; i < propertyCount; i++)
    {
        //獲取屬性名
        objc_property_t property = properties[i];
        const char *propertyName = property_getName(property);
        __autoreleasing NSString *key = @(propertyName);

        //檢查是否可coding
        if (![uncodableProperties containsObject:key])
        {
            //拿到property的Type Encodings,這部分不瞭解可去看官方文檔Type Encodings瞭解下規則及runtime中對應的相關接口,這裏都用到,也能夠看我上一篇文章
            Class propertyClass = nil;
            char *typeEncoding = property_copyAttributeValue(property, "T");
            switch (typeEncoding[0])
            {
          //表示是個對象
case '@': {
            //根據Type Encodings規則得出來的算法而已,目的截取出屬性類型
if (strlen(typeEncoding) >= 3) { char *className = strndup(typeEncoding + 2, strlen(typeEncoding) - 3); __autoreleasing NSString *name = @(className); NSRange range = [name rangeOfString:@"<"]; if (range.location != NSNotFound) { name = [name substringToIndex:range.location]; } propertyClass = NSClassFromString(name) ?: [NSObject class]; free(className); } break; } case 'c': case 'i': case 's': case 'l': case 'q': case 'C': case 'I': case 'S': case 'L': case 'Q': case 'f': case 'd': case 'B': {
            //都當成是NSNumber型 propertyClass
= [NSNumber class]; break; } case '{': { propertyClass = [NSValue class]; break; } } free(typeEncoding); if (propertyClass) { //獲取變量名 char *ivar = property_copyAttributeValue(property, "V"); if (ivar) { //檢查屬性是否有編譯器幫忙生成的成員變量名帶_線的 __autoreleasing NSString *ivarName = @(ivar); if ([ivarName isEqualToString:key] || [ivarName isEqualToString:[@"_" stringByAppendingString:key]]) { codableProperties[key] = propertyClass; } free(ivar); } else { //檢查屬性是不是 dynamic 和 readwrite的 char *dynamic = property_copyAttributeValue(property, "D"); char *readonly = property_copyAttributeValue(property, "R"); if (dynamic && !readonly) { codableProperties[key] = propertyClass; } free(dynamic); free(readonly); } } } } free(properties); return codableProperties; }

再看回encodeWithCoder:(NSCoder *)aCoder

- (void)encodeWithCoder:(NSCoder *)aCoder
{
    for (NSString *key in [self codableProperties])  
    {
        id object = [self valueForKey:key];
        if (object) [aCoder encodeObject:object forKey:key];
    }
}

不難知道,其實就是遍歷全部屬性名字,並以屬性名字做爲key,利用KVC先把屬性值拿到,再屬性名字做爲key調用encodeObject。原理就是這樣,解檔也差很少就再也不寫了,關鍵是拿到對象的全部屬性名稱及類型,利用KVC獲取和設置屬性值,再以屬性名稱做爲Key避免手動定義。

使用AutoCoding以後,MyDataInfo直接變成下面這樣就能夠了,結果與手動寫是同樣的,大大減少工做量,提升開發效率,又減小出錯率。

//MyDataInfo.h
#import <Foundation/Foundation.h>
@interface MyDataInfo : NSObject 
@property (nonatomic, strong) NSString *name;
@property (nonatomic, assign) NSInteger age;
@end

//MyDataInfo.m
#import "MyDataInfo.h"
@implementation MyDataInfo
@end

這裏運用到runtime相關知識主要有我上篇文章介紹過的成員變量與屬性、Type Encodings。

相關文章
相關標籤/搜索