http://blog.jobbole.com/67655/
NSCoding是把數據存儲在iOS和Mac
OS上的一種極其簡單和方便的方式,它把模型對象直接轉變成一個文件,然后再把這個文件重新加載到內存里,并不需要任何文件解析和序列化的邏輯。如果要把對象保存到一個數據文件中(假設這個對象實現了NSCoding協議),那么你可以像下面這樣做:
C++
Foo *someFoo = [[Foo alloc] init];
[NSKeyedArchiver archiveRootObject:someFoo toFile:someFile];
1
2Foo*someFoo=[[Fooalloc]init];
[NSKeyedArchiverarchiveRootObject:someFootoFile:someFile];
稍后再加載它:
C++
Foo *someFoo = [NSKeyedUnarchiver unarchiveObjectWithFile:someFile];
1
Foo*someFoo=[NSKeyedUnarchiverunarchiveObjectWithFile:someFile];
這樣做對于編譯進APP里的資源來說是可以的(例如nib文件,它在底層使用了NSCoding),但是使用NSCoding來讀寫用戶數據文件的問題在于,把全部的類編碼到一個文件里,也就間接地給了這個文件訪問你APP里面實例類的權限。
雖然你不能在一個NSCoded文件里(至少在iOS中的)存儲可執行代碼,但是一名黑客可以使用特制地文件騙過你的APP進入到實例化類中,這是你從沒打算做的,或者是你想要在另一個不同的上下文時才做的。盡管以這種方式造成實際性的破壞很難,但是無疑會導致用戶的APP崩潰掉或者數據丟失。
在iOS6中,蘋果引入了一個新的協議,是基于NSCoding的,叫做NSSecureCoding。NSSecureCoding和NSCoding是一樣的,除了在解碼時要同時指定key和要解碼的對象的類,如果要求的類和從文件中解碼出的對象的類不匹配,NSCoder會拋出異常,告訴你數據已經被篡改了。
大部分支持NSCoding的系統對象都已經升級到支持NSSecureCoding了,所以能安全地寫有關歸檔的代碼,你可以確保正在加載的數據文件是安全的。實現的方式如下:
C++
// 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];
1
2
3
4
5
6
7// Set up NSKeyedUnarchiver to use secure coding
NSData*data=[NSDatadataWithContentsOfFile:someFile];
NSKeyedUnarchiver*unarchiver=[[NSKeyedUnarchiveralloc]initForReadingWithData:data];
[unarchiversetRequiresSecureCoding:YES];
// Decode object
Foo*someFoo=[unarchiverdecodeObjectForKey:NSKeyedArchiveRootObjectKey];
注意一下,如果要讓編寫歸檔的代碼是安全的,那么存儲在文件中的每一個對象都要實現NSSecureCoding協議,否則會有異常拋出。如果要告訴框架自定義的類支持NSSecureCoding協議,那么你必須在initWithCoder:
method方法中實現新的解碼邏輯,并且supportsSecureCodin方法要返回YES。encodeWithCoder:方法沒有變化,因為與安全相關的事是圍繞加載進行的,而不是保存:
C++
@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
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@interfaceFoo:NSObject
@property(nonatomic,strong)NSNumber*property1;
@property(nonatomic,copy)NSArray*property2;
@property(nonatomic,copy)NSString*property3;
@end
@implementationFoo
+(BOOL)supportsSecureCoding
{
returnYES;
}
-(id)initWithCoder:(NSCoder*)coder
{
if((self=[superinit]))
{
// Decode the property values by key, specifying the expected class
_property1=[coderdecodeObjectOfClass:[NSNumberclass]forKey:@"property1"];
_property2=[coderdecodeObjectOfClass:[NSArrayclass]forKey:@"property2"];
_property3=[coderdecodeObjectOfClass:[NSStringclass]forKey:@"property3"];
}
returnself;
}
-(void)encodeWithCoder:(NSCoder*)coder
{
// Encode our ivars using string keys as normal
[coderencodeObject:_property1forKey:@"property1"];
[coderencodeObject:_property2forKey:@"property2"];
[coderencodeObject:_property3forKey:@"property3"];
}
@end
幾周前,我寫了一篇關于如何自動實現NSCoding的文章,它利用反射機制確定運行時類的屬性。
這是一種給所有的模型對象添加NSCoding支持的很好的方式,在initWithCoder:/encodeWithCoder:
方法中,你不再需要寫重復的并且容易出錯的代碼了。但是我們使用的方法沒有支持NSSecureCoding,因為我們不打算在對象被加載時校驗其類型。
那么怎么改善這個自動NSCoding系統,使其以正確的方式支持NSSecureCoding呢?
回想一下,最開始的實現原理是利用class_copyPropertyList() 和 property_getName()這樣兩個運行時方法,產生屬性名稱列表,我們再把它們在數組中排序:
C++
// Import the Objective-C runtime headers
#import
- (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;
}
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
-(NSArray*)propertyNames
{
// Get the list of properties
unsignedintpropertyCount;
objc_property_t*properties=class_copyPropertyList([selfclass],
&propertyCount);
NSMutableArray*array=[NSMutableArrayarrayWithCapacity:propertyCount];
for(inti=0;i
{
// Get property name
objc_property_tproperty=properties[i];
constchar*propertyName=property_getName(property);
NSString*key=@(propertyName);
// Add to array
[arrayaddObject:key];
}
// Remember to free the list because ARC doesn't do that for us
free(properties);
returnarray;
}
使用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對象,所以可用一種方式處理以’{’開頭的任何值。
如果根據字符串的首字母來轉換,那么我們可以處理所有已知的類型:
C++
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);
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
99ClasspropertyClass=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=[NSNumberclass];
break;
}
case'*':// C-String
{
propertyClass=[NSStringclass];
break;
}
case'@':// Object
{
//TODO: get class name
break;
}
case'{':// Struct
{
propertyClass=[NSValueclass];
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得到類:
C++
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;
}
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
40case'@':
{
// 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
NSRangerange=[namerangeOfString:@"<"];
if(range.location!=NSNotFound)
{
name=[namesubstringToIndex:range.location];
}
// Get class from name, or default to NSObject if no name is found
propertyClass=NSClassFromString(name)?:[NSObjectclass];
free(cName);
}
break;
}
最后,把上面的解析過程和前面實現的propertyNames方法結合起來,創建一個方法返回屬性類的字典,屬性名稱作為字典的鍵。下面是完成的實現過程:
- (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;
}
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([selfclass],_cmd);
if(dictionary)
{
returndictionary;
}
// Loop through our superclasses until we hit NSObject
dictionary=[NSMutableDictionarydictionary];
Classsubclass=[selfclass];
while(subclass!=[NSObjectclass])
{
unsignedintpropertyCount;
objc_property_t *properties=class_copyPropertyList(subclass,
&propertyCount);
for(inti=0;i<propertyCount;i++)
{
// Get property name
objc_property_tproperty=properties[i];
constchar*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([ivarNameisEqualToString:key]||
[ivarNameisEqualToString:[@"_"stringByAppendingString:key]])
{
// Get type
ClasspropertyClass=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=[NSNumberclass];
break;
}
case'*': // C-String
{
propertyClass=[NSStringclass];
break;
}
case'@': // Object
{
//TODO: get class name
break;
}
case'{': // Struct
{
propertyClass=[NSValueclass];
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=[subclasssuperclass];
}
// Cache and return dictionary
objc_setAssociatedObject([selfclass],_cmd,dictionary,
OBJC_ASSOCIATION_RETAIN_NONATOMIC);
returndictionary;
}
最難的部分已經完成了?,F在,要實現NSSecureCoding,只要將initWithCoder:方法中之前寫的自動編碼實現的部分,改為在解析時考慮到屬性的類就可以了。此外,還需讓supportsSecureCoding方法返回YES:
C++
+ (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];
}
}
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
{
returnYES;
}
-(id)initWithCoder:(NSCoder*)coder
{
if((self=[superinit]))
{
// Decode the property values by key, specifying the expected class
[[selfpropertyClassesByName]enumerateKeysAndObjectsUsingBlock:(void(^)(NSString*key,ClasspropertyClass,BOOL*stop)){
idobject=[aDecoderdecodeObjectOfClass:propertyClassforKey:key];
if(object)[selfsetValue:objectforKey:key];
}];
}
returnself;
}
-(void)encodeWithCoder:(NSCoder*)aCoder
{
for(NSString*keyin[selfpropertyClassesByName])
{
idobject=[selfvalueForKey:key];
if(object)[aCoderencodeObject:objectforKey:key];
}
}
這樣就得到了一個用于描述模型對象的簡單的基類,并且它以正確的方式支持NSSecureCoding。此外,你可以使用我的AutoCoding擴展,它利用這種方法自動給沒有實現NSCoding 和 NSSecureCoding協議的對象添加對它們的支持。