iOS 13 NSKeyedUnarchiver EXC_BAD_ACCESS - PullRequest
1 голос
/ 03 мая 2020

Я потратил большую часть дня и половину, пытаясь отладить эту проблему, которую я вижу, когда пытаюсь разархивировать большой двоичный объект данных, хранящийся локально (эта проблема также появляется при извлечении его через iCloud, но так как они запускаются через тот же код, я предполагаю, что они связаны).

Фон

Я изначально создавал это приложение четыре года go, и по причинам, которые с тех пор были потерянное время (но, вероятно, потому что я тогда был скорее новичком), я полагался на библиотеку AutoCoding , чтобы получить объекты в моей модели данных для автоматического принятия NSCoding (хотя в некоторых случаях я сам реализовывал протокол места - как я уже сказал, я был новичком) и FCFileManager для сохранения этих объектов в локальном каталоге документов. Сама модель данных довольно проста: пользовательские объекты NSObject, которые имеют различные свойства NSString, NSArray и другие пользовательские классы NSObject (но я отмечу, что существует ряд циклических ссылок; большинство из них объявлены как сильные и ненатомные c в заголовке файлы). Эта комбинация хорошо работает (и все еще работает) в рабочей версии приложения.

Однако в будущем обновлении я планирую добавить сохранение / загрузку файлов из 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 = 2 сбой при получении выполнения выполнения через эта линия. Отслеживание стека никогда не бывает особенно полезным; обычно это ~ 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;  
}  

Каждый объект Game имеет Команду дома и на выезде; каждая команда имеет NSMutableArray объектов Game, называемый 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 имеет значение, когда происходит cra sh (см. Скриншот: 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  

Этот класс также импортирует автокодирование, но не имеет пользовательской реализации initWithCoder: или setWithCoder:

Любопытно, что заменяет метод загрузки Я написал, что версия FCFileManager также аварийно завершает свою работу, поэтому это может быть больше связано с тем, как данные архивируются, чем с тем, как они извлекаются. Но опять же, все это прекрасно работает при использовании методов FCFileManager для загрузки / сохранения файлов, поэтому я предполагаю, что существует некоторая разница на низком уровне между реализацией архивов в iOS 11 (когда последнее обновление FCFileManager) и iOS 12 + (когда обновлялись API-интерфейсы NSKeyedArchiver).

В соответствии с некоторыми предложениями, которые я нашел в Интернете (например, в этом), я также попробовал следующее:

    @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 при попытке декодирования.

Кто-нибудь знает, где я могу ошибаться здесь? Я уверен, что это что-то простое, но я просто не могу понять это. У меня нет проблем с предоставлением большего количества исходного кода, если это необходимо для более глубокого погружения.

Спасибо за вашу помощь!

1 Ответ

0 голосов
/ 03 мая 2020

Я (каким-то образом) преодолел это, полагаясь на AutoCoding для управления реализацией NSCoding для класса Game. Отбрасывая слои, кажется, что была некоторая проблема с использованием -[NSMutableArray arrayWithObjects:] в -[Game initWithCoder:], которая вызвала EXC_BAD_ACCESS, но даже это казалось go, когда возвращался к AutoCoding. Не уверен, какое влияние это окажет на обратную совместимость, но, думаю, я перейду этот мост, когда доберусь туда.

...