在上一篇文章中,学习了runtime中的各个重要的知识点,接下来就是要开始运用了。主要是分析一些优秀开源库是如何运用runtime,提高工作效率的。
AutoCoding
AutoCoding 是一个NSObject的类别,它提供归档解档(即自动序列化和反序列化)对象。在介绍这个开源库之前,先简单过一下iOS中对象归档解档,这个并不是重点,只是为了抛出问题,所以不会详讲。在iOS中对象序归档解档会使用到NSKeyedArchiver和NSKeyedUnarchiver,接下来是示例代码:
创建一个需要归档解档的对象的类,它需要遵循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); } .......
运行代码,输出结果为,结果是毫无疑问的
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:接口内写
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说的归档存档。那么它是怎么做到的?
首先,在NSObject (AutoCoding) 类别中实现以下两个接口:
- (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。