在上一篇文章中,學習了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。