NSCoding是把資料儲存在iOS和Mac OS上的一種極其簡單和方便的方式,它把模型物件直接轉變成一個檔案,然後再把這個檔案重新載入到記憶體裡,並不需要任何檔案解析和序列化的邏輯。如果要把物件儲存到一個資料檔案中(假設這個物件實現了NSCoding協議),那麼你可以像下面這樣做:
1 2 |
Foo *someFoo = [[Foo alloc] init]; [NSKeyedArchiver archiveRootObject:someFoo toFile:someFile]; |
稍後再載入它:
1 |
Foo *someFoo = [NSKeyedUnarchiver unarchiveObjectWithFile:someFile]; |
這樣做對於編譯進APP裡的資源來說是可以的(例如nib檔案,它在底層使用了NSCoding),但是使用NSCoding來讀寫使用者資料檔案的問題在於,把全部的類編碼到一個檔案裡,也就間接地給了這個檔案訪問你APP裡面例項類的許可權。
雖然你不能在一個NSCoded檔案裡(至少在iOS中的)儲存可執行程式碼,但是一名黑客可以使用特製地檔案騙過你的APP進入到例項化類中,這是你從沒打算做的,或者是你想要在另一個不同的上下文時才做的。儘管以這種方式造成實際性的破壞很難,但是無疑會導致使用者的APP崩潰掉或者資料丟失。
在iOS6中,蘋果引入了一個新的協議,是基於NSCoding的,叫做NSSecureCoding。NSSecureCoding和NSCoding是一樣的,除了在解碼時要同時指定key和要解碼的物件的類,如果要求的類和從檔案中解碼出的物件的類不匹配,NSCoder會丟擲異常,告訴你資料已經被篡改了。
大部分支援NSCoding的系統物件都已經升級到支援NSSecureCoding了,所以能安全地寫有關歸檔的程式碼,你可以確保正在載入的資料檔案是安全的。實現的方式如下:
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]; |
注意一下,如果要讓編寫歸檔的程式碼是安全的,那麼儲存在檔案中的每一個物件都要實現NSSecureCoding協議,否則會有異常丟擲。如果要告訴框架自定義的類支援NSSecureCoding協議,那麼你必須在initWithCoder: method方法中實現新的解碼邏輯,並且supportsSecureCodin方法要返回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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
@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 |
幾周前,我寫了一篇關於如何自動實現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 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
// Import the Objective-C runtime headers #import <objc/runtime.h> - (NSArray *)propertyNames { // Get the list of properties unsigned int propertyCount; objc_property_t *properties = class_copyPropertyList([self class], &propertyCount); NSMutableArray *array = [NSMutableArray arrayWithCapacity: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); // Add to array [array addObject:key]; } // Remember to free the list because ARC doesn't do that for us free(properties); return array; } |
使用KVC(鍵-值編碼),我們能夠利用名稱設定和獲取一個物件的所有屬性,並且在一個NSCoder物件中對這些屬性進行編碼/解碼。
為了要實現NSSecureCoding,我們要遵循同樣的原則,但是不僅僅是獲取屬性名,還需要獲取它們的型別。幸運地是,Objective C執行時儲存了類的屬性型別的詳細資訊,所以可以很容易和名字一起取到這些資料。
一個類的屬性可以是基本資料型別(例如整型、布林型別和結構體),或者物件(例如字串、陣列等等)。KVC中的valueForKey: and setValue:forKey:方法實現了對基本型別的自動“裝箱”,也就是說它們會把整型、布林型和結構體各自轉變成NSNumber和NSValue物件。這使事情變得簡單了很多,因為我們只要處理裝箱過的型別(物件)即可,所以我們可以宣告屬性型別為類,而不用為不同的屬性型別呼叫不同的解碼方法。
儘管執行時方法沒有提供已裝箱的類名,但是它們提供了型別編碼—一種特殊格式化的C風格的字串,它包含了型別資訊(與@encode(var);返回的形式一樣)。因為沒有方法自動獲取到基本型別對應的裝箱過的類,所以我們需要解析這個字串,然後指定其合適的型別。
型別編碼字串形式的文件說明在這裡。
第一個字母代表了基本型別。Objective C使用一個唯一的字母表示每一個支援的基本型別,例如’i’表示integer,’f’表示float,’d’表示double,等等。物件用’@’表示(緊跟著的是類名),還有其他一些不常見的型別,例如’:’表示selectors,’#’表示類。
結構體和聯合體表示為大括號裡面的表示式。只有幾種型別是KVC機制所支援的,但是支援的那些類通常被裝箱為NSValue物件,所以可用一種方式處理以’{’開頭的任何值。
如果根據字串的首字母來轉換,那麼我們可以處理所有已知的型別:
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 |
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 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
case '@': { // The objcType for classes will always be at least 3 characters long if (strlen(typeEncoding) >= 3) { // Copy the class name as a C-String char *cName = strndup(typeEncoding + 2, strlen(typeEncoding) - 3); // Convert to an NSString for easier manipulation NSString *name = @(cName); // Strip out and protocols from the end of the class name NSRange range = [name rangeOfString:@"<"]; if (range.location != NSNotFound) { name = [name substringToIndex:range.location]; } // Get class from name, or default to NSObject if no name is found propertyClass = NSClassFromString(name) ?: [NSObject class]; free(cName); } break; } |
最後,把上面的解析過程和前面實現的propertyNames方法結合起來,建立一個方法返回屬性類的字典,屬性名稱作為字典的鍵。下面是完成的實現過程:
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 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 |
- (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 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
+ (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擴充套件,它利用這種方法自動給沒有實現NSCoding 和 NSSecureCoding協議的物件新增對它們的支援。