Reprendre NSUrlSession sur iOS10
iOS 10 va bientôt sortir, il vaut donc la peine de tester la compatibilité des applications avec. Au cours d'un tel test, nous avons découvert que notre application ne peut pas reprendre les téléchargements en arrière-plan sur iOS10. Le code qui fonctionnait bien sur les versions précédentes ne fonctionne pas sur les nouvelles, à la fois sur un émulateur et sur un périphérique.
Au lieu de réduire notre code à un cas de test de travail minimal, j'ai recherché sur Internet des tutoriels NSUrlSession et les ai testés. Le comportement est le même: la reprise des travaux sur les versions précédentes d'iOS mais s'arrête le 10.
Étapes à reproduire:
- Téléchargez un formulaire de projet NSUrlSession tutorial https://www.raywenderlich.com/110458/nsurlsession-tutorial-getting-started
- Lien direct: http://www.raywenderlich.com/wp-content/uploads/2016/01/HalfTunes-Final.Zip
- Construisez-le et lancez-le sous iOS 10. Recherchez quelque chose, par exemple "Swift". Démarrez un téléchargement, puis appuyez sur "Pause" puis sur "Reprendre"
Résultats attendus:
Le téléchargement reprend. Vous pouvez vérifier comment cela fonctionne avec les versions antérieures à iOS10.
Résultats actuels:
Le téléchargement échoue. Dans la console xcode, vous pouvez voir:
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.
Plus de scénarios:
Si vous activez le mode hors ligne pendant le téléchargement d'un fichier, vous obtenez
Url session completed with error: Error Domain=NSURLErrorDomain Code=-1002 "unsupported URL" UserInfo={NSLocalizedDescription=unsupported URL} {
NSLocalizedDescription = "unsupported URL";
}
lorsque le réseau est arrêté et que le téléchargement ne se rétablit jamais lorsque le réseau est à nouveau opérationnel. Les autres cas d'utilisation avec pause, comme le redémarrage, ne fonctionnent pas non plus.
Enquête supplémentaire:
J'ai essayé de vérifier si les données de retour retournées sont valides en utilisant le code suggéré dans
mais le fichier cible est en place. Le format resumeData a changé et le nom du fichier est désormais stocké dans NSURLSessionResumeInfoTempFileName et vous devez lui ajouter NSTemporaryDirectory ().
À côté de cela, j'ai rempli un rapport de bogue à Apple, mais ils n'ont pas encore répondu.
La question (de la vie, de l'univers et de tout):
La reprise de NSUrlSession est-elle interrompue dans toutes les autres applications? Peut-il être fixé côté application?
Ce problème provient de currentRequest et originalRequest NSKeyArchived encodé avec une racine inhabituelle de "NSKeyedArchiveRootObjectKey" au lieu de NSKeyedArchiveRootObjectKey constante qui est "root" littéralement et d'autres comportements incorrects dans le processus de codage de la requête NSURL (Mutable).
J'ai détecté cela en version bêta 1 et déposé un bogue (n ° 27144153 au cas où vous souhaiteriez le dupliquer). Même moi, j'ai envoyé un e-mail à "Quinn the Eskimo" (eskimo1 at Apple dot com) qui est le gars du support de l'équipe NSURLSession, pour confirmer qu'ils l'ont reçu et il a dit qu'ils l'avaient obtenu et qu'ils étaient au courant de problème.
MISE À JOUR: J'ai enfin compris comment résoudre ce problème. Donnez des données à la fonction correctResumeData () et elle renverra des données de reprise utilisables
MISE À JOUR 2: Vous pouvez utiliser la fonction NSURLSession.correctedDownloadTaskWithResumeData ()/URLSession.correctedDownloadTask (withResumeData :) pour obtenir une tâche avec les variables originalRequest et currentRequest correctes
MISE À JOUR 3: Quinn dit que ce problème est résolu dans iOS 10.2, vous pouvez continuer à utiliser ce code pour être compatible avec iOS 10.0 et 10.1 et cela fonctionnera avec nouvelle version sans aucun problème.
(Pour Swift 3, faites défiler ci-dessous, pour l'Objectif C, voir leavesstar post mais je ne l'ai pas testé)
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
}
}
Concernant la partie de la question sur le unsupported URL
erreur et reprise de perte de données en cas d'échec du réseau, ou autre échec, j'ai enregistré un TSI avec Apple, et la dernière réponse de Quinn:
Premièrement, le comportement que vous voyez est certainement un bogue dans NSURLSession. Nous espérons résoudre ce problème dans une future mise à jour logicielle. Ce travail est suivi par. Je n'ai aucune information à partager sur le moment où le correctif sera expédié aux utilisateurs iOS normaux.
En ce qui concerne les solutions de contournement, j'ai creusé cette question en détail hier et je comprends maintenant parfaitement l'échec. OMI, il existe un moyen raisonnable de contourner ce problème, mais je dois exécuter mes idées après l'ingénierie NSURLSession avant de pouvoir les partager. J'espère avoir de leurs nouvelles dans un jour ou deux. Veuillez patienter.
Je publierai des mises à jour au fur et à mesure, mais je suis sûr que cela donne aux gens un certain espoir qu'au moins le problème est examiné par Apple.
(Accessoires massifs à la solution de contournement de Mousavian pour le comportement de suspension/reprise)
MISE À JOUR:
De Quinn,
Effectivement. Depuis la dernière fois que nous avons parlé (et je m'excuse d'avoir mis si longtemps à vous revenir ici; j'ai été enterré récemment dans des incidents), j'ai approfondi les choses au nom d'autres développeurs et découvert que: A. Ce problème se manifeste dans deux contextes, caractérisés par les erreurs NSURLErrorCannotWriteToFile et NSURLErrorUnsupportedURL. B. Nous pouvons contourner le premier mais pas le second. J'ai joint une mise à jour à mon document qui remplit les détails. Malheureusement, nous n'avons pas pu trouver de solution de contournement pour le deuxième symptôme. La seule voie à suivre consiste pour iOS Engineering à corriger ce bogue. Nous espérons que cela se produira dans une mise à jour du logiciel iOS 10 mais je n'ai pas de détails concrets à partager (à part que ce correctif semble avoir manqué le bus 10.1) -:
Donc, malheureusement, le unsupported URL
le problème ne fonctionne pas et nous devons attendre que le bogue soit corrigé.
Le problème NSURLErrorCannotWriteToFile
est géré par le code de Mousavian ci-dessus.
UNE AUTRE MISE À JOUR:
Quinn a confirmé les dernières tentatives de bêta 10.2 pour résoudre ces problèmes.
Est-ce que cela a permis de voir 10.2?
Oui. Le correctif de ce problème a été inclus dans la première version bêta 10.2. Un certain nombre de développeurs avec lesquels j'ai travaillé ont signalé que ce correctif est bloqué, mais je vous recommande tout de même de l'essayer par vous-même sur la dernière version bêta (actuellement iOS 10.2 beta 2, 14C5069c). Faites-moi savoir si vous rencontrez des problèmes.
Voici le code Objective - C pour la réponse de Mousavian.
Cela fonctionne bien dans iOS 9.3.5 (appareil) et iOS 10.1 (simulateur).
Corrigez d'abord les données de CV à la manière 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;
}
Je n'ai pas créé de catégorie pour NSURLSession, je crée juste dans My Singleton. voici le code pour créer 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"];
}
}