2015-10-02 2 views
28

NSKeyedUnarchiver.decodeObject приведет к сбою/SIGABRT, если исходный класс неизвестен. Единственное решение, которое я видел, чтобы поймать этот вопрос, относится к ранней истории Swift и требует использования Objective C (также предваряется внедрением Swift 2 guard, throws, try & catch). Я мог бы выяснить маршрут Objective C, но, если возможно, я предпочел бы понять решение Swift.Быстрый способ только предотвратить отказ NSKeyedUnarchiver.decodeObject?

Например, данные были закодированы NSPropertyListFormat.XMLFormat_v1_0. Следующий код будет сбой при unarchiver.decodeObject(), если класс закодированных данных неизвестен.

//... 
let dat = NSData(contentsOfURL: url)! 
let unarchiver = NSKeyedUnarchiver(forReadingWithData: dat) 

//it will crash after this if the class in the xml file is not known 

if let newListCollection = (unarchiver.decodeObject()) as? List { 
    return newListCollection 
} else { 
    return nil 
} 
//... 

Ищу для Swift 2 единственный способ проверить, является ли данные, прежде чем пытаться .decodeObject - так .decodeObject не имеет throws - это значит, что try - catch, кажется, не быть одним из вариантов в Swift (методы без throws нельзя обернуть AFAIK). Или альтернативный способ декодирования данных, которые будут вызывать ошибку, я могу поймать, если декодирование завершится неудачно. Я хочу, чтобы пользователь мог импортировать файл с диска iCloud или Dropbox, поэтому он должен быть правильно проверен. Я не могу предположить, что закодированные данные безопасны.

NSKeyedUnarchiver В методы .unarchiveTopLevelObjectWithData & .validateValue оба имеют throws. Возможно ли, что они могут быть использованы? Я не могу понять, как даже начать пытаться реализовать validateValue в этом контексте. Это даже возможный маршрут? Или я должен смотреть на один из других методов решения?

Или кто-нибудь знает альтернативный вариант Swift 2 только для решения этой проблемы? Я считаю, что ключ, который меня интересует, вероятно, имеет название $classname - но TBH Я не в своей глубине, пытаясь выяснить, как реализовать validateValue - или даже будет ли это правильным путем для продолжения. У меня есть ощущение, что я упускаю что-то очевидное.


EDIT: Вот решение - спасибо, чтобы Ринтаро большой ответ (ов) ниже

Первоначальный ответ решен вопрос для меня - то есть реализации делегата.

На данный момент, однако я пошел с раствором, построенного вокруг дополнительного редактируемого ответ Ринтаро в следующем виде:

//... 
let dat = NSData(contentsOfURL: url)! 
let unarchiver = NSKeyedUnarchiver(forReadingWithData: dat) 

do { 
    let decodedDataObject = try unarchiver.decodeTopLevelObject() 
    if let newListCollection = decodedDataObject as? List { 
     return newListCollection 
    } else { 
     return nil 
    } 
} 
catch { 
    return nil 
} 
//... 

ответ

22

NSKeyedUnarchiver Когда встречает неизвестные классы, unarchiver(_:cannotDecodeObjectOfClassName:originalClasses:) метод делегата вызывается.

Делегат может, например, загружать код, чтобы ввести класс во время выполнения и возвращает класс, или заменить другой объект класса. Если делегат возвращает nil, разборки прерываются, и метод вызывает NSInvalidUnarchiveOperationException.

Таким образом, вы можете реализовать делегат вроде этого:

class MyUnArchiverDelegate: NSObject, NSKeyedUnarchiverDelegate { 

    // This class is placeholder for unknown classes. 
    // It will eventually be `nil` when decoded. 
    final class Unknown: NSObject, NSCoding { 
     init?(coder aDecoder: NSCoder) { super.init(); return nil } 
     func encodeWithCoder(aCoder: NSCoder) {} 
    } 

    func unarchiver(unarchiver: NSKeyedUnarchiver, cannotDecodeObjectOfClassName name: String, originalClasses classNames: [String]) -> AnyClass? { 
     return Unknown.self 
    } 
} 

Тогда:

let unarchiver = NSKeyedUnarchiver(forReadingWithData: dat) 
let delegate = MyUnArchiverDelegate() 
unarchiver.delegate = delegate 

unarchiver.decodeObjectForKey("root") 
// -> `nil` if the root object is unknown class. 

ДОБАВЛ:

я не заметил, что NSCoder ха s extension с более методами Swifty:

extension NSCoder { 
    @warn_unused_result 
    public func decodeObjectOfClass<DecodedObjectType : NSCoding where DecodedObjectType : NSObject>(cls: DecodedObjectType.Type, forKey key: String) -> DecodedObjectType? 
    @warn_unused_result 
    @nonobjc public func decodeObjectOfClasses(classes: NSSet?, forKey key: String) -> AnyObject? 
    @warn_unused_result 
    public func decodeTopLevelObject() throws -> AnyObject? 
    @warn_unused_result 
    public func decodeTopLevelObjectForKey(key: String) throws -> AnyObject? 
    @warn_unused_result 
    public func decodeTopLevelObjectOfClass<DecodedObjectType : NSCoding where DecodedObjectType : NSObject>(cls: DecodedObjectType.Type, forKey key: String) throws -> DecodedObjectType? 
    @warn_unused_result 
    public func decodeTopLevelObjectOfClasses(classes: NSSet?, forKey key: String) throws -> AnyObject? 
} 

Вы можете:

do { 
    try unarchiver.decodeTopLevelObjectForKey("root") 
    // OR `unarchiver.decodeTopLevelObject()` depends on how you archived. 
} 
catch let (err) { 
    print(err) 
} 
// -> emits something like: 
// Error Domain=NSCocoaErrorDomain Code=4864 "*** -[NSKeyedUnarchiver decodeObjectForKey:]: cannot decode object of class (MyProject.MyClass) for key (root); the class may be defined in source code or a library that is not linked" UserInfo={NSDebugDescription=*** -[NSKeyedUnarchiver decodeObjectForKey:]: cannot decode object of class (MyProject.MyClass) for key (root); the class may be defined in source code or a library that is not linked} 
+0

Большое спасибо. Это отличный ответ. – simons

+1

См. Мой обновленный ответ. – rintaro

+0

Еще раз спасибо. 'if try unarchiver.decodeTopLevelObject()! = nil' работает для меня так, как я его реализовал. Если это не «ноль», тогда мне хорошо идти. Я пропустил это при поиске методов, которые бросают. Ваш оригинальный ответ также работает для меня. – simons

15

другой способ исправить имя класса, используемого для NSCoding. Вы просто должны использовать:

  • NSKeyedArchiver.setClassName("List", forClass: List.self перед сериализацией
  • NSKeyedUnarchiver.setClass(List.self, forClassName: "List") перед тем десериализации

везде, где это необходимо.

Похоже, что расширения iOS префиксное имя класса с именем расширения.

+1

Это отлично подходит для нового приложения, однако будьте осторожны, если ваше приложение уже находится в магазине, так как при первом запуске после обновления любые существующие данные не будут заархивированы с новым именем класса, что приведет к авария. Убедитесь, что у вас есть код для обратной совместимости. Один из способов сделать это - использовать делегат для возвращения правильного имени класса. –

+0

@JamesKuang Я думаю, что он говорит о случае, когда он никогда не будет работать без этого, т.е. вы кодируете в классе и расшифровываете в расширении, поэтому имена классов не совпадают. – xaphod

+0

Как ни странно, мне пришлось использовать NSKeyedUnarchiver.setClass для этого, чтобы работать (на вложенном классе Swift). Но только при получении от TestFlight. Запуск его из Xcode (8.3.1) на моем тестовом телефоне работал нормально. И использование @objc() не делало никаких очевидных различий – Kristoffer

0

Собственно, это причина, по которой мы должны глубоко разбираться. Возможно, вы создаете путь к архиву с именем xxx.archive, затем вы удаляете с пути (xxx.archive), теперь все в порядке. Но если изменить целевое имя, когда вы распаковываете, произошел сбой !!! Это потому, что архив & разблокирует другой объект (правда, мы архивируем & unarchive target.obj, а не только объект). так просто удалить путь к архиву или просто использовать другой путь к архиву. И тогда мы должны подумать о том, как избежать аварии, try-catch - наш помощник, упомянутый rintaro.