coding with objc & swift

使用NSSecureCoding协议进行编解码

| Comments

译自:Object Encoding and Decoding with NSSecureCoding Protocol

在iOS和Mac OS上,NSCoding是一种极其简单方便的用来存储数据的方法。它可以直接将你的数据模型对象写入一个文件,之后又可以直接将它们读入内存而不需要编写任何文件解析和序列化的逻辑。将任何对象(假设它已经实现了NSCoding协议)保存至一个文件,你只需要这样做:

1
2
Foo *someFoo = [[Foo alloc] init];
[NSKeyedArchiver archiveRootObject:someFoo toFile:someFile];

之后要加载它,只需要这样:

1
Foo *someFoo = [NSKeyedUnarchiver unarchiveObjectWithFile:someFile];

那些编译进你的应用里面的资源采用这种方式很不错(比如nib文件,它其实就是用的NSCoding的方式),但是使用NSCoding读写用户数据文件的问题是,由于你是将整个类编码进一个文件,因此在你的应用中就隐式的赋予了该文件实例化类对象的权限(可能待查)。

虽然你不能在一个被NSCoding的文件中存储可执行代码(至少在iOS上不行),但黑客可能会用一个特制的文件欺骗你的应用,来实例化一个你绝对想象不到的类对象,或者在一个你想象不到的环境下实例化类对象。虽然这样做很难带来任何真正的伤害,但这肯定会导致应用崩溃或用户数据丢失。

iOS6中,苹果引入了一个基于NSCoding的新协议,叫做NSSecureCoding。NSSecureCoding和NSCoding几乎完全相同,除了解码的时候你需要指定要解码的对象的key和类,并且如果指定的类和从文件解码到的对象的类不匹配的时候,NSCoder会抛出一个异常来告诉你该数据已经被篡改。

大多数支持NSCoding的系统对象都已经升级到支持NSSecureCoding,所以通过给你的NSKeyedUnarchiver激活安全编码(secure coding)功能,就可以确保你加载到的数据文件是安全的。你可以这样做:

1
2
3
4
5
6
7
// Set up NSKeyedUnarchiver to use secure coding
NSData *data = [NSData dataWithContentsOfFile:someFile];
NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
[unarchiver setRequiresSecureCoding:YES];

// Decode object
Foo *someFoo = [unarchiver decodeObjectForKey:NSKeyedArchiveRootObjectKey];

注意,如果你的NSKeyedUnarchiver激活了安全编码,那么存储在这个文件中的所有对象都必须遵循NSSecureCoding协议,否则你会得到一个异常。为了表明你的类支持NSSecureCoding协议,你必须要在initWithCoder:方法里面实现新的解码逻辑,并且要让supportsSecureCoding方法返回YES。encodeWithCoder:方法不需要任何改变,因为安全问题在读取过程中才可能出现,不会出现在保存的过程中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@interface Foo : NSObject

@property (nonatomic, strong) NSNumber *property1;
@property (nonatomic, copy) NSArray *property2;
@property (nonatomic, copy) NSString *property3;

@end

@implementation Foo

+ (BOOL)supportsSecureCoding
{
  return YES;
}

- (id)initWithCoder:(NSCoder *)coder
{
  if ((self = [super init]))
  {
    // Decode the property values by key, specifying the expected class
    _property1 = [coder decodeObjectOfClass:[NSNumber class] forKey:@"property1"];
    _property2 = [coder decodeObjectOfClass:[NSArray class] forKey:@"property2"];
    _property3 = [coder decodeObjectOfClass:[NSString class] forKey:@"property3"];
  }
  return self;
}

- (void)encodeWithCoder:(NSCoder *)coder
{
  // Encode our ivars using string keys as normal
  [coder encodeObject:_property1 forKey:@"property1"];
  [coder encodeObject:_property2 forKey:@"property2"];
  [coder encodeObject:_property3 forKey:@"property3"];
}

@end

几个星期前,我写了如何在运行时通过自我检查机制(introspection)来检测类的属性,以实现自动NSCoding

这是一个非常棒的方法,它能一下子让你的所有的模型对象都支持NSCoding,而无需重复的编写initWithCoder:和encodeWithCoder:方法,从而也减少了出错的几率。但是我们使用的这个方法不支持NSSecureCoding,因为我们无法对正在加载的对象进行类型验证。

那么,如何增强我们的自动NSCoding系统以彻底支持NSSecureCoding呢?

如果你还记得,原来的实现是使用了class_copyPropertyList()和property_getName()这两个运行时函数来生成了一组属性名字,然后我们将它们存入了一个数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//引入Objective-C运行时的头文件
#import <objc/runtime.h> 

- (NSArray *)propertyNames
{
  //获得属性列表
  unsigned int propertyCount;
  objc_property_t *properties = class_copyPropertyList([self class],
    &propertyCount);
  NSMutableArray *array = [NSMutableArray arrayWithCapacity:propertyCount];
  for (int i = 0; i < propertyCount; i++)
  {
    //获得属性名字
    objc_property_t property = properties[i];
    const char *propertyName = property_getName(property);
    NSString *key = @(propertyName);

    //加入数组中
    [array addObject:key];
  }

  //记得释放属性列表,因为ARC不会替我们释放它
  free(properties);

  return array;
}

通过使用KVC,我们就能够通过名字来设置和获取一个对象的所有属性,并在一个NSCoder对象中将它们编码或解码。

为了实现NSSecureCoding,我们将遵循同样的原则,但除了要获取属性的名字外,还需要获取到它们的类型。幸运的是,Objective-C的运行时存储了类属性的类型信息,所以获取名字的同时,我们也可以很容易地获得这些数据。

类的属性可以是基本数据类型(如整型、布尔型和结构体),也可以是对象(如NSString、NSArray等等)。KVC的valueForKey:方法和setValue:forKey:方法实现了对基本数据类型的自动化”装箱(boxing)”操作。也就是说,它们会将整型、布尔型和结构体这些基本数据类型各自转换成NSNumber和NSValue对象。这样,对我们来说就简单许多了,因为我们只需要处理装箱后的对象就可以了。因此我们就能够将我们所有的属性类型按照类来处理,而不必为了不同的属性类型调用不同的解码方法。

虽然,运行时不会把每个属性装箱后对应的类名给我们,但它会给我们对应的类型编码信息——一个包含了类型信息的特殊格式的C字符串(与@encode(var);语法返回的字符串格式相同)。由于没有能够自动获取基本数据类型的等价类的方法,所以我们需要解析这个字符串,然后自己指定相应的类。

记录了类型编码字符串的格式的文档在这里

第一个字符就代表了对应的基本数据类型。Objective-C为每个支持的基本类型都定义了一个唯一的字符,比如’i’表示一个整数,’f’表示浮点数,’d’表示double,等等。对象被表示为’@’(后接类名),还有另外一些生僻的类型,如’:’表示selector,或’#’表示类。

花括弧{…}包含起来的表达式代表struct和union类型。这些类型仅有一部分受KVC机制的支持,但是受到支持的这部分类型总是被装箱成NSValue对象的,因此我们可以将任何以’{‘开头的值都做同样的处理。

使用switch,基于字符串的第一个字符,我们就能够处理所有的已知类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
Class propertyClass = nil;
char *typeEncoding = property_copyAttributeValue(property, "T");
switch (typeEncoding[0])
{
  case 'c': // Numeric types
  case 'i':
  case 's':
  case 'l':
  case 'q':
  case 'C':
  case 'I':
  case 'S':
  case 'L':
  case 'Q':
  case 'f':
  case 'd':
  case 'B':
  {
    propertyClass = [NSNumber class];
    break;
  }
  case '*': // C-String
  {
    propertyClass = [NSString class];
    break;
  }
  case '@': // Object
  {
    //TODO: get class name
    break;
  }
  case '{': // Struct
  {
    propertyClass = [NSValue class];
    break;
  }
  case '[': // C-Array
  case '(': // Enum
  case '#': // Class
  case ':': // Selector
  case '^': // Pointer
  case 'b': // Bitfield
  case '?': // Unknown type
  default:
  {
    propertyClass = nil; // Not supported by KVC
    break;
  }
}
free(typeEncoding);

要处理’@’类型,我们需要获得类名。类名可能包含了协议名(但无需担心这点),因此我们将把字符串进行分割以只提取出类名,然后使用NSClassFromString函数来获得对应的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
case '@':
{
  //类的objcType只少3个字符长度
  if (strlen(typeEncoding) >= 3)
  {
    //拷贝得到C字符串形式的类名
    char *cName = strndup(typeEncoding + 2, strlen(typeEncoding) - 3);

    //转换为一个NSString,以便后续操作的处理
    NSString *name = @(cName);

    //剔除类名后面的协议名字
    NSRange range = [name rangeOfString:@"<"];
    if (range.location != NSNotFound)
    {
      name = [name substringToIndex:range.location];
    }

    //根据类名获取对应的类,如果没有对应的类则默认为NSObject
    propertyClass = NSClassFromString(name) ?: [NSObject class];
    free(cName);
  }
  break;
}

最后,我们可以将这种解析逻辑与前面的实现中的propertyNames方法的逻辑组合起来,以属性名字作为key,创建一个返回包含属性对应类的字典的方法。下面是完整的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
- (NSDictionary *)propertyClassesByName
{
  // Check for a cached value (we use _cmd as the cache key, 
  // which represents @selector(propertyNames))
  NSMutableDictionary *dictionary = objc_getAssociatedObject([self class], _cmd);
  if (dictionary)
  {
      return dictionary;
  }

  // Loop through our superclasses until we hit NSObject
  dictionary = [NSMutableDictionary dictionary];
  Class subclass = [self class];
  while (subclass != [NSObject class])
  {
    unsigned int propertyCount;
    objc_property_t *properties = class_copyPropertyList(subclass,
      &propertyCount);
    for (int i = 0; i < propertyCount; i++)
    {
      // Get property name
      objc_property_t property = properties[i];
      const char *propertyName = property_getName(property);
      NSString *key = @(propertyName);

      // Check if there is a backing ivar
      char *ivar = property_copyAttributeValue(property, "V");
      if (ivar)
      {
        // Check if ivar has KVC-compliant name
        NSString *ivarName = @(ivar);
        if ([ivarName isEqualToString:key] ||
          [ivarName isEqualToString:[@"_" stringByAppendingString:key]])
        {
          // Get type
          Class propertyClass = nil;
          char *typeEncoding = property_copyAttributeValue(property, "T");
          switch (typeEncoding[0])
          {
            case 'c': // Numeric types
            case 'i':
            case 's':
            case 'l':
            case 'q':
            case 'C':
            case 'I':
            case 'S':
            case 'L':
            case 'Q':
            case 'f':
            case 'd':
            case 'B':
            {
              propertyClass = [NSNumber class];
              break;
            }
            case '*': // C-String
            {
              propertyClass = [NSString class];
              break;
            }
            case '@': // Object
            {
              //TODO: get class name
              break;
            }
            case '{': // Struct
            {
              propertyClass = [NSValue class];
              break;
            }
            case '[': // C-Array
            case '(': // Enum
            case '#': // Class
            case ':': // Selector
            case '^': // Pointer
            case 'b': // Bitfield
            case '?': // Unknown type
            default:
            {
              propertyClass = nil; // Not supported by KVC
              break;
            }
          }
          free(typeEncoding);

          // If known type, add to dictionary
          if (propertyClass) dictionary[propertyName] = propertyClass;
        }
        free(ivar);
      }
    }
    free(properties);
    subclass = [subclass superclass];
  }

  // Cache and return dictionary
  objc_setAssociatedObject([self class], _cmd, dictionary,
    OBJC_ASSOCIATION_RETAIN_NONATOMIC);
  return dictionary;
}

最困难的部分都已经完成了。现在,要实现NSSecureCoding,我们只需要修改前面实现自动化逻辑的代码中的initWithCoder:方法,接收属性对应类以进行解析操作。同样,我们还需要让supportsSecureCoding方法返回YES:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
+ (BOOL)supportsSecureCoding
{
  return YES;
}

- (id)initWithCoder:(NSCoder *)coder
{
  if ((self = [super init]))
  {
    // Decode the property values by key, specifying the expected class
    [[self propertyClassesByName] enumerateKeysAndObjectsUsingBlock:(void (^)(NSString *key, Class propertyClass, BOOL *stop)) {
      id object = [aDecoder decodeObjectOfClass:propertyClass forKey:key];
      if (object) [self setValue:object forKey:key];
    }];
  }
  return self;
}

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

好了,现在你的模型类已经拥有了一个可以开箱即用的支持NSSecureCoding的简单基类。或者,你也可以直接使用我的一个叫做AutoCoding的分类(category),它正是使用了这种方法来为任何一个即便没有实现NSCoding和NSSecureCoding协议的对象加入自动NSCoding和NSSecureCoding能力的分类。

本文原文作者为Nick Lockwood,他是《iOS Core Animation: Advanced Techniques》一书的作者,也是iCarousel、iRate等开源项目的作者。

Comments