【发布时间】: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