Я потратил большую часть дня и половину, пытаясь отладить эту проблему, которую я вижу, когда пытаюсь разархивировать большой двоичный объект данных, хранящийся локально (эта проблема также появляется при извлечении его через 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 при попытке декодирования.
Кто-нибудь знает, где я могу ошибаться здесь? Я уверен, что это что-то простое, но я просто не могу понять это. У меня нет проблем с предоставлением большего количества исходного кода, если это необходимо для более глубокого погружения.
Спасибо за вашу помощь!