iOS 13 NSKeyedUnarchiver EXC_BAD_ACCESS

问题描述 投票:1回答:1

我花了一天半的大部分时间来尝试调试此问题,当我尝试取消存档本地存储的数据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:]最小包装程序加载。

问题

但是从本地文档目录加载该文件back已证明是一个巨大的挑战。这是我现在正在使用的内容:

    @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)  
                }  
            }  
        }  
    }  

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

@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;  
}  

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

@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。

有人对我可能在这里出问题的地方有任何见识吗?我敢肯定这很简单,但似乎无法弄清楚。如果需要更深入地研究,我可以提供更多源代码。

感谢您的帮助!

ios swift ios13 nskeyedarchiver nskeyedunarchiver
1个回答
0
投票

我(以某种方式)通过依靠AutoCoding来管理Game类的NSCoding实现来克服了这一点。剥离图层,似乎在-[NSMutableArray arrayWithObjects:]中使用-[Game initWithCoder:]会导致EXC_BAD_ACCESS出现一些问题,但是当退回到AutoCoding时,这种问题似乎也消失了。不知道这会对向后兼容性产生什么影响,但是我想到达那儿后我会越过那座桥梁。

© www.soinside.com 2019 - 2024. All rights reserved.