ios nsurlsession ios10 nsurlsessiondownloadtask
http://www.raywenderlich.com/wp-content/uploads/2016/01/HalfTunes-Final.zip

Reanudar NSUrlSession en iOS10



nsurlsessiondownloadtask (3)

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

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

Primero corrige los datos del currículum en la forma 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, solo 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"]; } }

iOS 10 se lanzará pronto, por lo que vale la pena probar las aplicaciones para verificar su compatibilidad. Durante dicha prueba, descubrimos que nuestra aplicación no puede reanudar las descargas de fondo en iOS10. El código que funcionó bien en versiones anteriores no funciona en uno nuevo, tanto en un emulador como en un dispositivo.

En lugar de reducir nuestro código al mínimo caso de prueba de trabajo, busqué en Internet tutoriales de NSUrlSession y los probé. El comportamiento es el mismo: reanudar trabajos en versiones anteriores de iOS pero se rompe en décimo.

Pasos para reproducir:

  1. Descargue un formulario de proyecto del tutorial NSUrlSession 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. Compilarlo e iniciarlo en iOS 10. Buscar algo, por ejemplo, "rápido". Comience una descarga y luego presione "Pausa" y luego "Reanudar"

Resultados previstos:

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

Resultados actuales:

La descarga falla En la consola 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 sin conexión mientras se descarga un archivo, obtiene

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

cuando la red se apaga y la descarga no se recupera cuando la red vuelve a funcionar. Otros casos de uso con pausa, como el reinicio, tampoco funcionan.

Investigación adicional:

Intenté comprobar si el curriculum vitae devuelto es válido utilizando el código sugerido en

¿Cómo puedo verificar que un blob de NSData sea válido como resumeData para un NSURLSessionDownloadTask?

pero el archivo de destino está en su lugar. Aunque el formato resumeData ha cambiado y ahora el nombre de archivo se almacena en NSURLSessionResumeInfoTempFileName y debe anexarle NSTemporaryDirectory ().

Además de eso, he llenado un informe de error para Apple, pero todavía no me han contestado.

La pregunta (de la Vida, el Universo y Todo):

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


En cuanto a la parte de la pregunta sobre el error de unsupported URL y la pérdida de curriculum vitae en la red fallan, u otro error, he registrado una TSI con Apple y la última respuesta de Quinn:

En primer lugar, el comportamiento que está 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 en cuanto a cuándo la solución se enviará a los usuarios normales de iOS.

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

Publicaré las actualizaciones a medida que suceden, pero estoy seguro de que esto da a las personas la esperanza de que, al menos, el problema está siendo examinado por Apple.

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

ACTUALIZAR:

De Quinn,

En efecto. Desde la última vez que hablamos (y me disculpo por haber tardado tanto en contactarte aquí, he sido enterrado en incidentes recientemente) He profundizado en este tema en nombre de otros desarrolladores y descubrí que: A. Este problema se manifiesta en dos contextos, que se caracterizan por los errores NSURLErrorCannotWriteToFile y NSURLErrorUnsupportedURL. B. Podemos trabajar alrededor del primero pero no del segundo. He adjuntado una actualización a mi documento que completa los detalles. Lamentablemente, no pudimos encontrar una solución para el segundo síntoma. La única forma de avanzar es que iOS Engineering solucione ese error. Esperamos que eso 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 la unsupported URL no funciona y tenemos que esperar a que se solucione el error.

El problema de NSURLErrorCannotWriteToFile es manejado por el código de Mousavian arriba.

OTRA ACTUALIZACIÓN:

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

¿Esto fue visto en 10.2?

Sí. La solución para este problema se incluyó en la primera versión beta 10.2. Varios desarrolladores con los que he trabajado han informado que este parche se ha quedado atascado, pero aún así recomiendo que lo intentes en la última versión beta (actualmente iOS 10.2 beta 2, 14C5069c). Avísame si tocas algún inconveniente.


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 algunas otras fallas en el proceso de codificación de NSURL (Mutable) Request.

Detecté eso en la versión beta 1 y archivé un error (No. 27144153 en caso de que quieras duplicar). Incluso envié un correo electrónico a "Quinn the Eskimo" (eskimo1 en apple dot com), quien es el tipo de soporte del equipo de NSURLSession, para confirmar que lo recibieron y dijo que lo entendieron y que están al tanto del problema.

ACTUALIZACIÓN: finalmente descubrí cómo resolver este problema. Dar datos a la función correctaResumeData () y devolverá datos de currículum utilizables

ACTUALIZACIÓN 2: puede usar la función NSURLSession.correctedDownloadTaskWithResumeData () / URLSession.correctedDownloadTask (withResumeData :) para obtener una tarea con las variables originales de una solicitud original y de una solicitud actual

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

(Para el código Swift 3, desplácese hacia abajo, para Objective C vea la publicación LeavesStar pero no la 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 } }