Reanudar NSUrlSession en iOS10

iOS 10 se lanzará pronto, por lo que vale la pena probar las aplicaciones para que sean compatibles con él. Durante dicha testing, descubrimos que nuestra aplicación no puede reanudar descargas de background en iOS10. El código que funcionó bien en las versiones anteriores no funciona en uno nuevo, tanto en un emulador como en un dispositivo.

En lugar de networkingucir nuestro código a un caso de testing de trabajo mínimo, he buscado en Internet tutoriales de NSUrlSession y los he probado. El comportamiento es el mismo: la reanudación funciona en las versiones previas de iOS pero rompe el 10.

Pasos para reproducir:

  1. Descargue un formulario de proyecto NSUrlSession tutorial https://www.raywenderlich.com/110458/nsurlsession-tutorial-getting-started
  2. Enlace directo: http://www.raywenderlich.com/wp-content/uploads/2016/01/HalfTunes-Final.zip
  3. Constrúyalo e inicie en iOS 10. Busca algo, por ejemplo, "rápido". Inicie una descarga y luego presione "Pausa" y luego "Reanudar"

Resultados previstos:

La descarga se reanuda. Puede verificar cómo funciona con las versiones anteriores a iOS10.

Resultados actuales:

La descarga falla. En la console xcode puedes ver:

2016-09-02 16:11:24.913 HalfTunes[35205:2279228] *** -[NSKeyedUnarchiver initForReadingWithData:]: data is NULL 2016-09-02 16:11:24.913 HalfTunes[35205:2279228] *** -[NSKeyedUnarchiver initForReadingWithData:]: data is NULL 2016-09-02 16:11:24.913 HalfTunes[35205:2279228] Invalid resume data for background download. Background downloads must use http or https and must download to an accessible file. 

Más escenarios:

Si activa el modo fuera de línea mientras se descarga un file, obtendrá

 Url session completed with error: Error Domain=NSURLErrorDomain Code=-1002 "unsupported URL" UserInfo={NSLocalizedDescription=unsupported URL} { NSLocalizedDescription = "unsupported URL"; } 

cuando la networking está apagada y la descarga nunca se recupera cuando la networking vuelve a funcionar. Otros casos de uso con pausa, como reiniciar, tampoco funcionan.

Investigación adicional:

He intentado comprobar si devuelve resumeData es válido usando el código sugerido en

¿Cómo puedo comprobar que un blob NSData es válido como resumeData para un NSURLSessionDownloadTask?

pero el file de destino está en su lugar. Aunque el formatting resumeData ha cambiado y ahora el nombre del file se almacena en NSURLSessionResumeInfoTempFileName y debe agregarle NSTemporaryDirectory ().

Además de eso, he cargado un informe de error a Apple, pero todavía no han respondido.

La pregunta (de la vida, el universo y todo):

¿Se reanuda la reanudación de NSUrlSession en todas las demás aplicaciones? ¿Se puede arreglar en el lado de la aplicación?

Este problema surgió de currentRequest y originalRequest NSKeyArchived codificado con una raíz inusual de "NSKeyedArchiveRootObjectKey" en lugar de la constante NSKeyedArchiveRootObjectKey que es "raíz", literalmente, y algunos otros comportamientos negativos en el process de encoding de la request NSURL (mutable).

Lo detecté en la beta 1 y archivé un error (n. ° 27144153 en caso de que quiera duplicar). Incluso envié un correo electrónico a "Quinn the Eskimo" (eskimo1 en apple dot com), que es el tipo de apoyo del equipo de NSURLSession, para confirmar que lo recibieron y dijo que lo sabían y están al tanto del problema.

ACTUALIZACIÓN: finalmente descubrí cómo resolver este problema. Proporcione datos a la function correctResumeData () y devolverá datos de reanudación utilizables

ACTUALIZACIÓN 2: puede utilizar la function NSURLSession.correctedDownloadTaskWithResumeData () / URLSession.correctedDownloadTask (withResumeData :)) para get una tarea con las variables correctas originalRequest y currentRequest

ACTUALIZACIÓN 3: Quinn dice que este problema se resuelve en iOS 10.2, puede seguir usando este código para tener compatibilidad con iOS 10.0 y 10.1 y funcionará con una nueva versión sin ningún problema.

(Para el código Swift 3, desplácese a continuación, para el Objetivo C vea el post de leavesstar, pero no lo probé)

Swift 2.3:

 func correctRequestData(data: NSData?) -> NSData? { guard let data = data else { return nil } // return the same data if it's correct if NSKeyedUnarchiver.unarchiveObjectWithData(data) != nil { return data } guard let archive = (try? NSPropertyListSerialization.propertyListWithData(data, options: [.MutableContainersAndLeaves], format: nil)) as? NSMutableDictionary else { return nil } // Rectify weird __nsurlrequest_proto_props objects to $number pattern var k = 0 while archive["$objects"]?[1].objectForKey("$\(k)") != nil { k += 1 } var i = 0 while archive["$objects"]?[1].objectForKey("__nsurlrequest_proto_prop_obj_\(i)") != nil { let arr = archive["$objects"] as? NSMutableArray if let dic = arr?[1] as? NSMutableDictionary, let obj = dic["__nsurlrequest_proto_prop_obj_\(i)"] { dic.setObject(obj, forKey: "$\(i + k)") dic.removeObjectForKey("__nsurlrequest_proto_prop_obj_\(i)") arr?[1] = dic archive["$objects"] = arr } i += 1 } if archive["$objects"]?[1].objectForKey("__nsurlrequest_proto_props") != nil { let arr = archive["$objects"] as? NSMutableArray if let dic = arr?[1] as? NSMutableDictionary, let obj = dic["__nsurlrequest_proto_props"] { dic.setObject(obj, forKey: "$\(i + k)") dic.removeObjectForKey("__nsurlrequest_proto_props") arr?[1] = dic archive["$objects"] = arr } } // Rectify weird "NSKeyedArchiveRootObjectKey" top key to NSKeyedArchiveRootObjectKey = "root" if archive["$top"]?.objectForKey("NSKeyedArchiveRootObjectKey") != nil { archive["$top"]?.setObject(archive["$top"]?["NSKeyedArchiveRootObjectKey"], forKey: NSKeyedArchiveRootObjectKey) archive["$top"]?.removeObjectForKey("NSKeyedArchiveRootObjectKey") } // Reencode archived object let result = try? NSPropertyListSerialization.dataWithPropertyList(archive, format: NSPropertyListFormat.BinaryFormat_v1_0, options: NSPropertyListWriteOptions()) return result } func getResumeDictionary(data: NSData) -> NSMutableDictionary? { var iresumeDictionary: NSMutableDictionary? = nil // In beta versions, resumeData is NSKeyedArchive encoded instead of plist if #available(iOS 10.0, OSX 10.12, *) { var root : AnyObject? = nil let keyedUnarchiver = NSKeyedUnarchiver(forReadingWithData: data) do { root = try keyedUnarchiver.decodeTopLevelObjectForKey("NSKeyedArchiveRootObjectKey") ?? nil if root == nil { root = try keyedUnarchiver.decodeTopLevelObjectForKey(NSKeyedArchiveRootObjectKey) } } catch {} keyedUnarchiver.finishDecoding() iresumeDictionary = root as? NSMutableDictionary } if iresumeDictionary == nil { do { iresumeDictionary = try NSPropertyListSerialization.propertyListWithData(data, options: [.MutableContainersAndLeaves], format: nil) as? NSMutableDictionary; } catch {} } return iresumeDictionary } func correctResumeData(data: NSData?) -> NSData? { let kResumeCurrentRequest = "NSURLSessionResumeCurrentRequest" let kResumeOriginalRequest = "NSURLSessionResumeOriginalRequest" guard let data = data, let resumeDictionary = getResumeDictionary(data) else { return nil } resumeDictionary[kResumeCurrentRequest] = correctRequestData(resumeDictionary[kResumeCurrentRequest] as? NSData) resumeDictionary[kResumeOriginalRequest] = correctRequestData(resumeDictionary[kResumeOriginalRequest] as? NSData) let result = try? NSPropertyListSerialization.dataWithPropertyList(resumeDictionary, format: NSPropertyListFormat.XMLFormat_v1_0, options: NSPropertyListWriteOptions()) return result } extension NSURLSession { func correctedDownloadTaskWithResumeData(resumeData: NSData) -> NSURLSessionDownloadTask { let kResumeCurrentRequest = "NSURLSessionResumeCurrentRequest" let kResumeOriginalRequest = "NSURLSessionResumeOriginalRequest" let cData = correctResumeData(resumeData) ?? resumeData let task = self.downloadTaskWithResumeData(cData) // a compensation for inability to set task requests in CFNetwork. // While you still get -[NSKeyedUnarchiver initForReadingWithData:]: data is NULL error, // this section will set them to real objects if let resumeDic = getResumeDictionary(cData) { if task.originalRequest == nil, let originalReqData = resumeDic[kResumeOriginalRequest] as? NSData, let originalRequest = NSKeyedUnarchiver.unarchiveObjectWithData(originalReqData) as? NSURLRequest { task.setValue(originalRequest, forKey: "originalRequest") } if task.currentRequest == nil, let currentReqData = resumeDic[kResumeCurrentRequest] as? NSData, let currentRequest = NSKeyedUnarchiver.unarchiveObjectWithData(currentReqData) as? NSURLRequest { task.setValue(currentRequest, forKey: "currentRequest") } } return task } } 

Swift 3:

 func correct(requestData data: Data?) -> Data? { guard let data = data else { return nil } if NSKeyedUnarchiver.unarchiveObject(with: data) != nil { return data } guard let archive = (try? PropertyListSerialization.propertyList(from: data, options: [.mutableContainersAndLeaves], format: nil)) as? NSMutableDictionary else { return nil } // Rectify weird __nsurlrequest_proto_props objects to $number pattern var k = 0 while ((archive["$objects"] as? NSArray)?[1] as? NSDictionary)?.object(forKey: "$\(k)") != nil { k += 1 } var i = 0 while ((archive["$objects"] as? NSArray)?[1] as? NSDictionary)?.object(forKey: "__nsurlrequest_proto_prop_obj_\(i)") != nil { let arr = archive["$objects"] as? NSMutableArray if let dic = arr?[1] as? NSMutableDictionary, let obj = dic["__nsurlrequest_proto_prop_obj_\(i)"] { dic.setObject(obj, forKey: "$\(i + k)" as NSString) dic.removeObject(forKey: "__nsurlrequest_proto_prop_obj_\(i)") arr?[1] = dic archive["$objects"] = arr } i += 1 } if ((archive["$objects"] as? NSArray)?[1] as? NSDictionary)?.object(forKey: "__nsurlrequest_proto_props") != nil { let arr = archive["$objects"] as? NSMutableArray if let dic = arr?[1] as? NSMutableDictionary, let obj = dic["__nsurlrequest_proto_props"] { dic.setObject(obj, forKey: "$\(i + k)" as NSString) dic.removeObject(forKey: "__nsurlrequest_proto_props") arr?[1] = dic archive["$objects"] = arr } } /* I think we have no reason to keep this section in effect for item in (archive["$objects"] as? NSMutableArray) ?? [] { if let cls = item as? NSMutableDictionary, cls["$classname"] as? NSString == "NSURLRequest" { cls["$classname"] = NSString(string: "NSMutableURLRequest") (cls["$classes"] as? NSMutableArray)?.insert(NSString(string: "NSMutableURLRequest"), at: 0) } }*/ // Rectify weird "NSKeyedArchiveRootObjectKey" top key to NSKeyedArchiveRootObjectKey = "root" if let obj = (archive["$top"] as? NSMutableDictionary)?.object(forKey: "NSKeyedArchiveRootObjectKey") as AnyObject? { (archive["$top"] as? NSMutableDictionary)?.setObject(obj, forKey: NSKeyedArchiveRootObjectKey as NSString) (archive["$top"] as? NSMutableDictionary)?.removeObject(forKey: "NSKeyedArchiveRootObjectKey") } // Reencode archived object let result = try? PropertyListSerialization.data(fromPropertyList: archive, format: PropertyListSerialization.PropertyListFormat.binary, options: PropertyListSerialization.WriteOptions()) return result } func getResumeDictionary(_ data: Data) -> NSMutableDictionary? { // In beta versions, resumeData is NSKeyedArchive encoded instead of plist var iresumeDictionary: NSMutableDictionary? = nil if #available(iOS 10.0, OSX 10.12, *) { var root : AnyObject? = nil let keyedUnarchiver = NSKeyedUnarchiver(forReadingWith: data) do { root = try keyedUnarchiver.decodeTopLevelObject(forKey: "NSKeyedArchiveRootObjectKey") ?? nil if root == nil { root = try keyedUnarchiver.decodeTopLevelObject(forKey: NSKeyedArchiveRootObjectKey) } } catch {} keyedUnarchiver.finishDecoding() iresumeDictionary = root as? NSMutableDictionary } if iresumeDictionary == nil { do { iresumeDictionary = try PropertyListSerialization.propertyList(from: data, options: PropertyListSerialization.ReadOptions(), format: nil) as? NSMutableDictionary; } catch {} } return iresumeDictionary } func correctResumeData(_ data: Data?) -> Data? { let kResumeCurrentRequest = "NSURLSessionResumeCurrentRequest" let kResumeOriginalRequest = "NSURLSessionResumeOriginalRequest" guard let data = data, let resumeDictionary = getResumeDictionary(data) else { return nil } resumeDictionary[kResumeCurrentRequest] = correct(requestData: resumeDictionary[kResumeCurrentRequest] as? Data) resumeDictionary[kResumeOriginalRequest] = correct(requestData: resumeDictionary[kResumeOriginalRequest] as? Data) let result = try? PropertyListSerialization.data(fromPropertyList: resumeDictionary, format: PropertyListSerialization.PropertyListFormat.xml, options: PropertyListSerialization.WriteOptions()) return result } extension URLSession { func correctedDownloadTask(withResumeData resumeData: Data) -> URLSessionDownloadTask { let kResumeCurrentRequest = "NSURLSessionResumeCurrentRequest" let kResumeOriginalRequest = "NSURLSessionResumeOriginalRequest" let cData = correctResumeData(resumeData) ?? resumeData let task = self.downloadTask(withResumeData: cData) // a compensation for inability to set task requests in CFNetwork. // While you still get -[NSKeyedUnarchiver initForReadingWithData:]: data is NULL error, // this section will set them to real objects if let resumeDic = getResumeDictionary(cData) { if task.originalRequest == nil, let originalReqData = resumeDic[kResumeOriginalRequest] as? Data, let originalRequest = NSKeyedUnarchiver.unarchiveObject(with: originalReqData) as? NSURLRequest { task.setValue(originalRequest, forKey: "originalRequest") } if task.currentRequest == nil, let currentReqData = resumeDic[kResumeCurrentRequest] as? Data, let currentRequest = NSKeyedUnarchiver.unarchiveObject(with: currentReqData) as? NSURLRequest { task.setValue(currentRequest, forKey: "currentRequest") } } return task } } 

En cuanto a la parte de la pregunta sobre el error de unsupported URL y el CVS perdido en fallas en la networking u otro error, he registrado una TSI con Apple y la última respuesta de Quinn:

En primer lugar, el comportamiento que estás viendo es definitivamente un error en NSURLSession. Esperamos solucionar este problema en una futura actualización de software. Ese trabajo está siendo rastreado por. No tengo ninguna información para compartir sobre cuándo se enviará la solución a los usuarios normales de iOS.

En cuanto a las soluciones, ayer expliqué este tema en detalle y ahora entiendo completamente el fracaso. En la OMI existe una forma razonable de solucionar este problema, pero necesito dirigir mis ideas más allá de la ingeniería de NSURLSession antes de poder compartirlas. Espero recibir noticias de ellos en el día siguiente o dos. Por favor espere.

Publicaré las actualizaciones a medida que sucedan, pero estoy seguro de que esto les da a las personas la esperanza de que al less la manzana esté parsing el problema.

(Accesos masivos a la solución de Mousavian para suspender / reanudar el comportamiento)

ACTUALIZAR:

De Quinn,

En efecto. Desde la última vez que hablamos (y pido disculpas por el hecho de que he tardado tanto en contactarte aquí, he sido enterrado en incidentes recientemente). He investigado esto en nombre de otros desarrolladores y he descubierto que: A. Este problema se manifiesta en dos contexts, caracterizados por los errores NSURLErrorCannotWriteToFile y NSURLErrorUnsupportedURL. B. Podemos trabajar alnetworkingedor del primero pero no el segundo. Adjunto una actualización a mi documento que llena los detalles. Lamentablemente, no pudimos encontrar una solución para el segundo síntoma. La única manera de avanzar es que iOS Engineering solucione ese error. Esperamos que esto suceda en una actualización de software de iOS 10, pero no tengo detalles concretos para compartir (aparte de que esta solución parece que se perdió el bus 10.1):

Por lo tanto, desafortunadamente, el problema de unsupported URL no funciona y tenemos que esperar a que se corrija el error.

El problema NSURLErrorCannotWriteToFile se maneja por el código de Mousavian arriba.

OTRA ACTUALIZACIÓN:

Quinn confirmó los últimos 10.2 bashs beta para resolver estos problemas.

¿Esto se echó un vistazo en 10.2?

Sí. La solución para este problema se incluyó en el primer 10.2 beta. Una serie de desarrolladores con los que he trabajado han informado de que este parche se ha atascado, pero aún así recomiendo que lo pruebe usted mismo en la versión beta más reciente (actualmente iOS 10.2 beta 2, 14C5069c). Déjame saber si golpeas cualquier inconveniente.

Aquí está el código Objective – C para la respuesta de Mousavian.

Funciona bien en iOS 9.3.5 (Dispositivo) e iOS 10.1 (simulador).

Primero corrija los datos del curriculum vitae en el modo de Mousavian

  - (NSData *)correctRequestData:(NSData *)data { if (!data) { return nil; } if ([NSKeyedUnarchiver unarchiveObjectWithData:data]) { return data; } NSMutableDictionary *archive = [NSPropertyListSerialization propertyListWithData:data options:NSPropertyListMutableContainersAndLeaves format:nil error:nil]; if (!archive) { return nil; } int k = 0; while ([[archive[@"$objects"] objectAtIndex:1] objectForKey:[NSString stringWithFormat:@"$%d", k]]) { k += 1; } int i = 0; while ([[archive[@"$objects"] objectAtIndex:1] objectForKey:[NSString stringWithFormat:@"__nsurlrequest_proto_prop_obj_%d", i]]) { NSMutableArray *arr = archive[@"$objects"]; NSMutableDictionary *dic = [arr objectAtIndex:1]; id obj; if (dic) { obj = [dic objectForKey:[NSString stringWithFormat:@"__nsurlrequest_proto_prop_obj_%d", i]]; if (obj) { [dic setObject:obj forKey:[NSString stringWithFormat:@"$%d",i + k]]; [dic removeObjectForKey:[NSString stringWithFormat:@"__nsurlrequest_proto_prop_obj_%d", i]]; arr[1] = dic; archive[@"$objects"] = arr; } } i += 1; } if ([[archive[@"$objects"] objectAtIndex:1] objectForKey:@"__nsurlrequest_proto_props"]) { NSMutableArray *arr = archive[@"$objects"]; NSMutableDictionary *dic = [arr objectAtIndex:1]; if (dic) { id obj; obj = [dic objectForKey:@"__nsurlrequest_proto_props"]; if (obj) { [dic setObject:obj forKey:[NSString stringWithFormat:@"$%d",i + k]]; [dic removeObjectForKey:@"__nsurlrequest_proto_props"]; arr[1] = dic; archive[@"$objects"] = arr; } } } id obj = [archive[@"$top"] objectForKey:@"NSKeyedArchiveRootObjectKey"]; if (obj) { [archive[@"$top"] setObject:obj forKey:NSKeyedArchiveRootObjectKey]; [archive[@"$top"] removeObjectForKey:@"NSKeyedArchiveRootObjectKey"]; } NSData *result = [NSPropertyListSerialization dataWithPropertyList:archive format:NSPropertyListBinaryFormat_v1_0 options:0 error:nil]; return result; } - (NSMutableDictionary *)getResumDictionary:(NSData *)data { NSMutableDictionary *iresumeDictionary; if ([[NSProcessInfo processInfo] operatingSystemVersion].majorVersion >= 10) { NSMutableDictionary *root; NSKeyedUnarchiver *keyedUnarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data]; NSError *error = nil; root = [keyedUnarchiver decodeTopLevelObjectForKey:@"NSKeyedArchiveRootObjectKey" error:&error]; if (!root) { root = [keyedUnarchiver decodeTopLevelObjectForKey:NSKeyedArchiveRootObjectKey error:&error]; } [keyedUnarchiver finishDecoding]; iresumeDictionary = root; } if (!iresumeDictionary) { iresumeDictionary = [NSPropertyListSerialization propertyListWithData:data options:0 format:nil error:nil]; } return iresumeDictionary; } static NSString * kResumeCurrentRequest = @"NSURLSessionResumeCurrentRequest"; static NSString * kResumeOriginalRequest = @"NSURLSessionResumeOriginalRequest"; - (NSData *)correctResumData:(NSData *)data { NSMutableDictionary *resumeDictionary = [self getResumDictionary:data]; if (!data || !resumeDictionary) { return nil; } resumeDictionary[kResumeCurrentRequest] = [self correctRequestData:[resumeDictionary objectForKey:kResumeCurrentRequest]]; resumeDictionary[kResumeOriginalRequest] = [self correctRequestData:[resumeDictionary objectForKey:kResumeOriginalRequest]]; NSData *result = [NSPropertyListSerialization dataWithPropertyList:resumeDictionary format:NSPropertyListXMLFormat_v1_0 options:0 error:nil]; return result; } 

No creé una categoría para NSURLSession, simplemente creo en My Singleton. aquí está el código para crear NSURLSessionDownloadTask:

  NSData *cData = [self correctResumData:self.resumeData]; if (!cData) { cData = self.resumeData; } self.downloadTask = [self.session downloadTaskWithResumeData:cData]; if ([self getResumDictionary:cData]) { NSDictionary *dict = [self getResumDictionary:cData]; if (!self.downloadTask.originalRequest) { NSData *originalData = dict[kResumeOriginalRequest]; [self.downloadTask setValue:[NSKeyedUnarchiver unarchiveObjectWithData:originalData] forKey:@"originalRequest"]; } if (!self.downloadTask.currentRequest) { NSData *currentData = dict[kResumeCurrentRequest]; [self.downloadTask setValue:[NSKeyedUnarchiver unarchiveObjectWithData:currentData] forKey:@"currentRequest"]; } }