升級 Xcode 11 踩坑歸檔解檔

背景

因爲4月以後蘋果要求不能使用老本版的Xcode打包提審,所以最近一次上線更新升級成了Xcode 11.3.1版本。iOS13適配要點總結有一些大佬已經總結很全面了,這裏補充記錄一個歸檔解檔的坑。ios

問題代碼

- (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,一致的話加載以前緩存數據loadCachemacos

問題就是在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以後提供了新的方法。bash

+ (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更加安全。markdown

替換後代碼

- (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設爲truefalse都仍是以前的效果(解檔出來的每一個對象都存在,可是對象對應的屬性值所有都爲nil)。數據結構

最終的解決方案是歸檔時requiringSecureCoding設爲true,歸檔的自定義數據都遵照NSSecureCoding協議,並實現對應方法。解檔時unarchivedObjectOfClasses對應的NSSet要包括歸檔時數據結構的全部類名。app

NSSecureCoding協議對應要實現的方法有3個:post

public protocol NSSecureCoding : NSCoding {
    static var supportsSecureCoding: Bool { get }
}

public protocol NSCoding {
    func encode(with coder: NSCoder)
    init?(coder: NSCoder)
}
複製代碼

因爲咱們是OCSwift混編,並使用了ObjectMapper作數據模型轉換。因此僞代碼大概是這樣:ui

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以後,發現解檔出來的對象對應的屬性已經有正確的值了。

總結

踩這個坑感受有幾個點須要注意:

  1. 本地存儲歸檔字符串的Key須要更改一下,防止新代碼讀取老緩存失敗的問題。
  2. 若是歸檔時傳入的數據結構包含了NSArrayNSDictionary,那麼在解檔時unarchivedObjectOfClasses對應的NSSet中也應該添加對應的類名,不然解檔出來的值爲nil
  3. APIiOS11以後出的,因此要作好以前系統版本的兼容。
  4. 若是你的數據模型類是多個自定義類嵌套的話,記得解檔時全部涉及的全部類都要添加在unarchivedObjectOfClasses對應的NSSet中。
相關文章
相關標籤/搜索