ios - Swift única manera de prevenir NSKeyedUnarchiver.decodeObject crash?
(3)
NSKeyedUnarchiver.decodeObject
causará un bloqueo / SIGABRT
si la clase original es desconocida. La única solución que he encontrado para detectar este problema data de la historia temprana de Swift y requería usar Objective C (también anterior a la implementación de Swift 2 de guard
, throws
, try
& catch
). Podría averiguar la ruta del Objetivo C, pero preferiría entender una solución Swift-only si es posible.
Por ejemplo, los datos se han codificado con NSPropertyListFormat.XMLFormat_v1_0
. El siguiente código fallará en unarchiver.decodeObject()
si la clase de los datos codificados es desconocida.
//...
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
}
//...
Estoy buscando una forma única de Swift 2 para comprobar si los datos son válidos antes de intentar .decodeObject
, ya que .decodeObject
no tiene throws
, lo que significa que try
- catch
no parece ser una opción en Swift (los métodos sin throws
no pueden ser ajustados HASTA DONDE SE). O bien, una forma alternativa de descodificar los datos que arrojará un error que puedo detectar si falla la descodificación. Quiero que el usuario pueda importar un archivo desde la unidad iCloud o Dropbox, por lo que debe validarse correctamente. No puedo asumir que los datos codificados son seguros.
Los métodos de .unarchiveTopLevelObjectWithData
y .validateValue
tienen throws
. ¿Hay alguna forma de que estos puedan ser utilizados? No puedo encontrar la manera de comenzar a intentar implementar validateValue
en este contexto. ¿Es esto incluso una ruta posible? ¿O debería estar buscando uno de los otros métodos para encontrar una solución?
¿O alguien sabe una forma alternativa de Swift 2 de abordar este problema? Creo que la clave en la que estoy interesado probablemente tenga el nombre de $classname
, pero TBH estoy fuera de mi alcance con respecto a tratar de averiguar cómo implementar validateValue
, o incluso si esa sería la ruta correcta para perseverar. Tengo la sensación de que me falta algo obvio.
EDITAR: Aquí hay una solución - gracias a la (s) gran (s) respuesta (s) de rintaro a continuación
La respuesta inicial resolvió el problema para mí, es decir, implementar un delegado.
Por ahora, sin embargo, me he ido con una solución basada en la respuesta editada adicional de rintaro de la siguiente manera:
//...
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
}
//...
Cuando NSKeyedUnarchiver
encuentra clases desconocidas, se llama al método de delegado unarchiver(_:cannotDecodeObjectOfClassName:originalClasses:)
.
El delegado puede, por ejemplo, cargar algún código para introducir la clase en el tiempo de ejecución y devolver la clase, o sustituir un objeto de clase diferente . Si el delegado devuelve
nil
, el proceso de anulación aborta y el método genera unaNSInvalidUnarchiveOperationException
.
Entonces, puedes implementar al delegado de esta manera:
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
}
}
Entonces:
let unarchiver = NSKeyedUnarchiver(forReadingWithData: dat)
let delegate = MyUnArchiverDelegate()
unarchiver.delegate = delegate
unarchiver.decodeObjectForKey("root")
// -> `nil` if the root object is unknown class.
AÑADIDO :
No me di cuenta de que NSCoder
tiene extension
con métodos más rápidos:
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?
}
Usted puede:
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}
En realidad, es la razón por la que debemos cavar profundamente las cuestiones. Existe la posibilidad de crear una ruta de archivo llamada xxx.archive, luego se desarchiva de la ruta (xxx.archive), ahora todo está bien. ¡Pero si cambia el nombre del objetivo, cuando se desarchiva, se produjo el bloqueo! Es porque archivamos y desarchivamos el objeto diferente (la verdad es que archivamos y desarchivamos target.obj, no solo el obj). de manera tan simple es eliminar la ruta del archivo o simplemente usar una ruta de archivo diferente. Y luego deberíamos considerar cómo evitar el choque, try-catch es nuestro ayudante mencionado por .
Otra forma es arreglar el nombre de la clase utilizada para NSCoding. Simplemente tienes que usar:
-
NSKeyedArchiver.setClassName("List", forClass: List.self
antes de serializar -
NSKeyedUnarchiver.setClass(List.self, forClassName: "List")
antes de deserializar
donde sea necesario
Parece que las extensiones de iOS prefijan el nombre de clase con el nombre de la extensión.