【问题标题】:iOS 13 NSKeyedUnarchiver EXC_BAD_ACCESSiOS 13 NSKeyedUnarchiver EXC_BAD_ACCESS
【发布时间】:2020-08-17 11:22:25
【问题描述】:

我已经花了一天半的时间来调试这个问题,我在尝试取消归档本地存储的数据 blob 时看到了这个问题(通过 iCloud 检索它时也会出现这个问题,但因为它们运行通过相同的代码路径,我认为它们是相关的)。

背景

我最初是在四年前构建了这个应用程序,但由于时间已久的原因(但可能是因为那时我还是个新手),我依靠 AutoCoding 库来获取我的数据中的对象模型自动采用 NSCoding(尽管我确实在某些地方自己实现了该协议——就像我说的,我是一个新手)和 FCFileManager 用于将这些对象保存到本地文档目录。数据模型本身相当简单:具有 NSString、NSArray 和其他自定义 NSObject 类的各种属性的自定义 NSObject(但我会注意到有许多循环引用;其中大多数在头文件中声明为强且非原子)。这种组合已经(并且仍然)在应用的生产版本中运行良好。

但是,在未来的更新中,我计划从 iCloud 添加保存/加载文件。虽然我一直在构建它,但我一直在寻找减少我的第三方依赖项列表并将旧代码更新到 iOS 13+ API。碰巧 FCFileManager 依赖于现已弃用的 +[NSKeyedUnarchiver unarchiveObjectWithFile:]+[NSKeyedArchiver archiveRootObject:toFile:],因此我专注于使用更现代的 API 从该库重写我需要的内容。

我可以很容易地使用这个来保存文件:

    @objc static func save(_ content: NSCoding, at fileName: String, completion: ((Bool, Error?) -> ())?) {  
        CFCSerialQueue.processingQueue.async { // my own serial queue  
            measureTime(operation: "[LocalService Save] Saving") { // just measures the time it takes for the logic in the closure to process  
                do {  
                    let data: Data = try NSKeyedArchiver.archivedData(withRootObject: content, requiringSecureCoding: false)  
                    // targetDirectory here is defined earlier in the class as the local documents directory  
                    try data.write(to: targetDirectory!.appendingPathComponent(fileName), options: .atomicWrite)  
                    if (completion != nil) {  
                        completion!(true, nil)  
                    }  
                } catch {  
                    if (completion != nil) {  
                        completion!(false, error)  
                    }  
                }  
            }  
        }  
    }  

这很好用——非常快,并且仍然可以由 FCFileManager 的最小包装器加载 +[NSKeyedUnarchiver unarchiveObjectWithFile:]

问题

但是从本地文档目录返回加载这个文件被证明是一个巨大的挑战。这是我现在正在使用的:

    @objc static func load(_ fileName: String, completion: @escaping ((Any?, Error?) -> ())) {  
        CFCSerialQueue.processingQueue.async {// my own serial queue  
            measureTime(operation: "[LocalService Load] Loading") {  
                do {  
                    // targetDirectory here is defined earlier in the class as the local documents directory  
                    let combinedUrl: URL = targetDirectory!.appendingPathComponent(fileName)   
                    if (FileManager.default.fileExists(atPath: combinedUrl.path)) {  
                        let data: Data = try Data(contentsOf: combinedUrl)  
                        let obj: Any? = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data)  
                        completion(obj, nil)  
                    } else {  
                        completion(nil, ServiceError.generic(message: "Data not found at URL \(combinedUrl)"))  
                    }  
                } catch {  
                    completion(nil, error)  
                }  
            }  
        }  
    }  

我已将 FCFileManager 的 +[NSKeyedUnarchiver unarchiveObjectWithFile:] 替换为新的 +[NSKeyedUnarchiver unarchiveTopLevelObjectWithData:],但是当执行流经该行时,我遇到了 EXC_BAD_ACCESS code=2 崩溃。堆栈跟踪从来都不是特别有用。它通常长约 1500 帧,并在各种自定义 -[NSObject initWithCoder:] 实现之间跳转。这是一个示例(为上下文、清晰和简洁添加了 cmets):

@implementation Game  
@synthesize AwayKStats,AwayQBStats,AwayRB1Stats,AwayRB2Stats,AwayWR1Stats,AwayWR2Stats,AwayWR3Stats,awayTOs,awayTeam,awayScore,awayYards,awayQScore,awayStarters,gameName,homeTeam,hasPlayed,homeYards,HomeKStats,superclass,HomeQBStats,HomeRB1Stats,HomeRB2Stats,homeStarters,HomeWR1Stats,HomeWR2Stats,HomeWR3Stats,homeScore,homeQScore,homeTOs,numOT,AwayTEStats,HomeTEStats, gameEventLog,HomeSStats,HomeCB1Stats,HomeCB2Stats,HomeCB3Stats,HomeDL1Stats,HomeDL2Stats,HomeDL3Stats,HomeDL4Stats,HomeLB1Stats,HomeLB2Stats,HomeLB3Stats,AwaySStats,AwayCB1Stats,AwayCB2Stats,AwayCB3Stats,AwayDL1Stats,AwayDL2Stats,AwayDL3Stats,AwayDL4Stats,AwayLB1Stats,AwayLB2Stats,AwayLB3Stats,homePlays,awayPlays,playEffectiveness, homeStarterSet, awayStarterSet;  

-(id)initWithCoder:(NSCoder *)aDecoder {  
    self = [super init];  
    if (self) {  
        // ...lots of other decoding...  

        // stack trace says the BAD_ACCESS is flowing through these decoding lines  
        // @property (atomic) Team *homeTeam;  
        homeTeam = [aDecoder decodeObjectOfClass:[Team class] forKey:@"homeTeam"];  
        // @property (atomic) Team *awayTeam;  
        // there's no special reason for this line using a different decoding method;  
        // I was just trying to test out both  
        awayTeam = [aDecoder decodeObjectForKey:@"awayTeam"];   

        // ...lots of other decoding...  
    }  
    return self;  
}  

每个游戏对象都有一个主客队;每个团队都有一个名为 gameSchedule 的游戏对象 NSMutableArray,定义如下:

@property (strong, atomic) NSMutableArray<Game*> *gameSchedule;  

这是团队的 initWithCoder: 实现:

-(id)initWithCoder:(NSCoder *)coder {  
    self = [super initWithCoder:coder];  
    if (self) {  
        if (teamHistory.count > 0) {  
           if (teamHistoryDictionary == nil) {  
               teamHistoryDictionary = [NSMutableDictionary dictionary];  
           }  
           if (teamHistoryDictionary.count < teamHistory.count) {  
               for (int i = 0; i < teamHistory.count; i++) {  
                   [teamHistoryDictionary setObject:teamHistory[i] forKey:[NSString stringWithFormat:@"%ld",(long)([HBSharedUtils currentLeague].baseYear + i)]];  
               }  
           }  
        }  

        if (state == nil) {  
           // set the home state here  
        }  

        if (playersTransferring == nil) {  
           playersTransferring = [NSMutableArray array];  
        }  

        if (![coder containsValueForKey:@"projectedPollScore"]) {  
           if (teamOLs != nil && teamQBs != nil && teamRBs != nil && teamWRs != nil && teamTEs != nil) {  
               FCLog(@"[Team Attributes] Adding Projected Poll Score to %@", self.abbreviation);  
               projectedPollScore = [self projectPollScore];  
           } else {  
               projectedPollScore = 0;  
           }  
        }  

        if (![coder containsValueForKey:@"teamStrengthOfLosses"]) {  
           [self updateStrengthOfLosses];  
        }  

        if (![coder containsValueForKey:@"teamStrengthOfSchedule"]) {  
           [self updateStrengthOfSchedule];  
        }  

        if (![coder containsValueForKey:@"teamStrengthOfWins"]) {  
           [self updateStrengthOfWins];  
        }  
    }  
    return self;  
}  

除了一些属性的回填之外,非常简单。然而,这个类导入了 AutoCoding,它像这样连接到 -[NSObject initWithCoder:]

- (void)setWithCoder:(NSCoder *)aDecoder  
{  
    BOOL secureAvailable = [aDecoder respondsToSelector:@selector(decodeObjectOfClass:forKey:)];  
    BOOL secureSupported = [[self class] supportsSecureCoding];  
    NSDictionary *properties = self.codableProperties;  
    for (NSString *key in properties)  
    {  
        id object = nil;  
        Class propertyClass = properties[key];  
        if (secureAvailable)  
        {  
            object = [aDecoder decodeObjectOfClass:propertyClass forKey:key]; // where the EXC_BAD_ACCESS seems to be coming from  
        }  
        else  
        {  
            object = [aDecoder decodeObjectForKey:key];  
        }  
        if (object)  
        {  
            if (secureSupported && ![object isKindOfClass:propertyClass] && object != [NSNull null])  
            {  
                [NSException raise:AutocodingException format:@"Expected '%@' to be a %@, but was actually a %@", key, propertyClass, [object class]];  
            }  
            [self setValue:object forKey:key];  
        }  
    }  
}  

- (instancetype)initWithCoder:(NSCoder *)aDecoder  
{  
    [self setWithCoder:aDecoder];  
    return self;  
}  

我做了一些代码跟踪,发现执行流向了上面的-[NSCoder decodeObject:forKey:] 调用。根据我添加的一些日志记录,似乎 propertyClass 在传递给-[NSCoder decodeObjectOfClass:forKey:] 之前以某种方式被释放。但是,Xcode 显示当崩溃发生时 propertyClass 有一个值(见截图:https://imgur.com/a/J0mgrvQ

定义了该框架中的相关属性:

@property (strong, nonatomic) Record *careerFgMadeRecord; 

并且本身具有以下属性:

@interface Record : NSObject  
@property (strong, nonatomic) NSString *title;  
@property (nonatomic) NSInteger year;  
@property (nonatomic) NSInteger statistic;  
@property (nonatomic) Player *holder;  
@property (nonatomic) HeadCoach *coachHolder;  
// … some functions  
@end  

该类也导入 AutoCoding,但没有自定义 initWithCoder: 或 setWithCoder: 实现。

奇怪的是,用 FCFileManager 的版本替换我编写的加载方法也会以同样的方式崩溃,因此这可能更多是数据归档方式的问题,而不是数据的检索方式。但同样,当使用 FCFileManager 的方法加载/保存文件时,这一切都很好,所以我的猜测是 iOS 11(FCFileManager 上次更新时)和 iOS 12+(当NSKeyedArchiver API 已更新)。

根据我在网上找到的一些建议(比如这个),我也试过这个:

    @objc static func load(_ fileName: String, completion: @escaping ((Any?, Error?) -> ())) {  
        CFCSerialQueue.processingQueue.async {  
            measureTime(operation: "[LocalService Load] Loading") {  
                do {  
                    let combinedUrl: URL = targetDirectory!.appendingPathComponent(fileName)  
                    if (FileManager.default.fileExists(atPath: combinedUrl.path)) {  
                        let data: Data = try Data(contentsOf: combinedUrl)  
                        let unarchiver: NSKeyedUnarchiver = try NSKeyedUnarchiver(forReadingFrom: data)  
                        unarchiver.requiresSecureCoding = false;  
                        let obj: Any? = try unarchiver.decodeTopLevelObject(forKey: NSKeyedArchiveRootObjectKey)  
                        completion(obj, nil)  
                    } else {  
                        completion(nil, ServiceError.generic(message: "Data not found at URL \(combinedUrl)"))  
                    }  
                } catch {  
                    completion(nil, error)  
                }  
            }  
        }  
    } 

但是,这仍然会在尝试解码时抛出相同的 EXC_BAD_ACCESS。

有没有人知道我在这里可能出错的地方?我确信这很简单,但我似乎无法弄清楚。如果需要深入研究,我可以提供更多源代码。

感谢您的帮助!

【问题讨论】:

  • 尝试去掉所有的自定义对象(没有 NS 前缀的东西),看看你是否仍然会崩溃。提醒一下,NSCoding 与 Codable 不同。
  • @glotcha 根据您的提示,我自己在 Team 类上实现了 NSCoder,从编码标准 NSObjects 开始。似乎 EXC_BAD_ACCESS 来自self.gameSchedule = [coder decodeObjectOfClasses:[NSSet setWithArray:@[[NSMutableArray class], [Game class]]] forKey:@"gameSchedule"];,堆栈跟踪在该行和-[Game initWithCoder:] 内的团队引用解码之间反弹。这让我觉得数据模型中的循环引用可能是根本原因,但 NSCoder 不应该开箱即用地为您处理这些吗?
  • 对不起,我没有时间在心里调试你的代码。游戏是一门课吗?我不明白您为什么要使用该方法 decodeObjectOfClasses,即:为什么您需要用一个新类代替您存档的类。慢慢来,保持简单。让基础知识发挥作用并从那里开始构建。

标签: ios swift ios13 nskeyedarchiver nskeyedunarchiver


【解决方案1】:

我(不知何故)通过依靠 AutoCoding 来管理 Game 类的 NSCoding 实现来解决这个问题。剥离层,似乎在-[Game initWithCoder:] 中使用-[NSMutableArray arrayWithObjects:] 存在一些问题,导致EXC_BAD_ACCESS,但是当回到自动编码时,即使这样似乎也消失了。不确定这会对向后兼容性产生什么影响,但我想当我到达那里时我会越过那座桥。

【讨论】:

    猜你喜欢
    • 2021-11-10
    • 2021-11-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-09-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多