背景
由于4月之后苹果要求不能使用老本版的Xcode
打包提审,因此最近一次上线更新升级成了Xcode 11.3.1
版本。iOS13适配要点总结有一些大佬已经总结很全面了,这里补充记录一个归档解档的坑。
问题代码
- (void)updateCache {
NSMutableDictionary *cache = [NSMutableDictionary dictionary];
if (self.viewModel.data1) {
[cache setObject:self.viewModel.data1 forKey:@"data1"];
}
if (self.viewModel.data2) {
[cache setObject:self.viewModel.data2 forKey:@"data2"];
}
if (self.viewModel.data3) {
[cache setObject:self.viewModel.data3 forKey:@"data3"];
}
NSData *archiverData = [NSKeyedArchiver archivedDataWithRootObject:[cache copy]];
NSString *archiverString = [archiverData base64EncodedStringWithOptions:0];
[[NSUserDefaults standardUserDefaults] setObject:archiverString forKey:@"cache"];
[[NSUserDefaults standardUserDefaults] synchronize];
}
- (void)loadCache {
NSString *archiverString = [[NSUserDefaults standardUserDefaults] objectForKey:@"cache"];
if (archiverString) {
@try {
NSData *archiverData = [[NSData alloc] initWithBase64EncodedString:archiverString options:0];
NSDictionary *cacheDic = [NSKeyedUnarchiver unarchiveObjectWithData:archiverData];
self.viewModel.data1 = cacheDic[@"data1"];
self.viewModel.data2 = cacheDic[@"data2"];
self.viewModel.data3 = cacheDic[@"data3"];
} @catch (NSException *exception) {
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"cache"];
}
}
}
我们的缓存策略是第一次进入页面返回数据后进行updateCache
操作,后续刷新接口时比对数据MD5
跟之前是否一致,不一致使用新数据展示并重新进行updateCache
,一致的话加载之前缓存数据loadCache
。
问题就是在loadCache
方法中解档出来的cacheDic
虽热归档进去的每个对象都存在,但是对象对应的属性值全部都为nil
。
寻找原因很痛苦毕竟除了升级Xcode
其他什么都没改。最后在官方方法中看到了端倪。
+ (NSData *)archivedDataWithRootObject:(id)rootObject API_DEPRECATED("Use +archivedDataWithRootObject:requiringSecureCoding:error: instead", macosx(10.2,10.14), ios(2.0,12.0), watchos(2.0,5.0), tvos(9.0,12.0));
+ (nullable id)unarchiveObjectWithData:(NSData *)data API_DEPRECATED("Use +unarchivedObjectOfClass:fromData:error: instead", macosx(10.2,10.14), ios(2.0,12.0), watchos(2.0,5.0), tvos(9.0,12.0));
iOS12
之后两个归档解档的方法被废弃了,iOS11
之后提供了新的方法。
+ (nullable NSData *)archivedDataWithRootObject:(id)object requiringSecureCoding:(BOOL)requiresSecureCoding error:(NSError **)error API_AVAILABLE(macos(10.13), ios(11.0), watchos(4.0), tvos(11.0));
+ (nullable id)unarchivedObjectOfClass:(Class)cls fromData:(NSData *)data error:(NSError **)error API_AVAILABLE(macos(10.13), ios(11.0), watchos(4.0), tvos(11.0)) NS_REFINED_FOR_SWIFT;
+ (nullable id)unarchivedObjectOfClasses:(NSSet<Class> *)classes fromData:(NSData *)data error:(NSError **)error API_AVAILABLE(macos(10.13), ios(11.0), watchos(4.0), tvos(11.0)) NS_REFINED_FOR_SWIFT;
注意到官方新的API
中归档方法里面有个requiringSecureCoding
参数,对应归档数据是否遵循NSSecureCoding
协议。可以看出新的API
更加安全。
替换后代码
- (void)updateCache {
NSMutableDictionary *cache = [NSMutableDictionary dictionary];
if (self.viewModel.data1) {
[cache setObject:self.viewModel.data1 forKey:@"data1"];
}
if (self.viewModel.data2) {
[cache setObject:self.viewModel.data2 forKey:@"data2"];
}
if (self.viewModel.data3) {
[cache setObject:self.viewModel.data3 forKey:@"data3"];
}
NSData *archiverData = nil;
if (@available(iOS 11.0, *)) {
NSError *error = nil;
archiverData = [NSKeyedArchiver archivedDataWithRootObject:[cache copy] requiringSecureCoding:YES error:&error];
} else {
archiverData = [NSKeyedArchiver archivedDataWithRootObject:[cache copy]];
}
NSString *archiverString = [archiverData base64EncodedStringWithOptions:0];
[[NSUserDefaults standardUserDefaults] setObject:archiverString forKey:@"cacheData"];
[[NSUserDefaults standardUserDefaults] synchronize];
}
- (void)loadCache
{
NSString *archiverString = [[NSUserDefaults standardUserDefaults] objectForKey:@"cacheData"];
if (archiverString) {
@try {
NSData *archiverData = [[NSData alloc] initWithBase64EncodedString:archiverString options:0];
NSDictionary *cacheDic = nil;
NSError *error = nil;
if (@available(iOS 11.0, *)) {
NSSet *set = [[NSSet alloc] initWithArray:@[[Data1Class class], [Data2Class class], [Data3Class class], [Data3Class class], [NSArray class], [NSDictionary class]]];
cacheDic = [NSKeyedUnarchiver unarchivedObjectOfClasses:set fromData:archiverData error:&error];
} else {
cacheDic = [NSKeyedUnarchiver unarchiveObjectWithData:archiverData];
}
self.viewModel.data1 = homeCacheDic[@"data1"];
self.viewModel.data2 = homeCacheDic[@"data2"];
self.viewModel.data3 = homeCacheDic[@"data3"];
} @catch (NSException *exception) {
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"cacheData"];
}
}
}
最初的更改是直接替换了API
,发现不管requiringSecureCoding
设为true
或false
都还是之前的效果(解档出来的每个对象都存在,但是对象对应的属性值全部都为nil
)。
最终的解决方案是归档时requiringSecureCoding
设为true
,归档的自定义数据都遵守NSSecureCoding
协议,并实现对应方法。解档时unarchivedObjectOfClasses
对应的NSSet
要包括归档时数据结构的所有类名。
NSSecureCoding
协议对应要实现的方法有3个:
public protocol NSSecureCoding : NSCoding {
static var supportsSecureCoding: Bool { get }
}
public protocol NSCoding {
func encode(with coder: NSCoder)
init?(coder: NSCoder)
}
由于我们是OC
和Swift
混编,并使用了ObjectMapper
做数据模型转换。所以伪代码大概是这样:
import UIKit
import ObjectMapper
@objc(Data1)
@objcMembers class Data1: NSObject, Mappable, NSSecureCoding {
required init?(coder: NSCoder) {
param1 = coder.decodeObject(forKey: "param1") as? String
param2 = coder.decodeObject(forKey: "param2") as? String
param3 = coder.decodeObject(forKey: "param3") as? String
param4 = coder.decodeBool(forKey: "param4")
}
static var supportsSecureCoding: Bool {
return true
}
func encode(with coder: NSCoder) {
coder.encode(param1, forKey: "param1")
coder.encode(param2, forKey: "param2")
coder.encode(param3, forKey: "param3")
coder.encode(param4, forKey: "param4")
}
var param1: String?
var param2: String?
var param3: String?
var param4: Bool = false
required init?(map: Map) { }
func mapping(map: Map) {
param1 <- map["param1"]
param2 <- map["param2"]
param3 <- map["param3"]
param4 <- map["param4"]
}
}
新API
在归档中用到的所有自定义数据模型类全部实现NSSecureCoding
之后,发现解档出来的对象对应的属性已经有正确的值了。
总结
踩这个坑感觉有几个点需要注意:
- 本地存储归档字符串的
Key
需要更改一下,防止新代码读取老缓存失败的问题。 - 如果归档时传入的数据结构包含了
NSArray
、NSDictionary
,那么在解档时unarchivedObjectOfClasses
对应的NSSet
中也应该添加对应的类名,否则解档出来的值为nil
。 - 新
API
是iOS11
之后出的,所以要做好之前系统版本的兼容。 - 如果你的数据模型类是多个自定义类嵌套的话,记得解档时所有涉及的所有类都要添加在
unarchivedObjectOfClasses
对应的NSSet
中。