iOS Mise en réseau lorsque l'application n'est pas en cours d'exécution

image


Les utilisateurs s'attendent à ce que le réseau fonctionne «comme par magie» et passe inaperçu. Cette magie dépend des développeurs du système et des applications. Il est difficile d'influencer le système, nous nous limiterons donc à l'application.


Ce sujet est complexe et il existe d'innombrables problèmes. Nous discuterons de ceux que nous avons rencontrés au cours des derniers mois. Je m'excuse tout de suite pour le volume. Bref, pas du tout, trop de petites choses qui méritent qu'on y prête attention.


Pour commencer, parlons de la terminologie.


Le transfert de données se produit dans deux directions:


  • téléchargement (téléchargement, téléchargement de données depuis le serveur),
  • téléchargement (envoi de données au serveur).

L'application peut être active, mais peut fonctionner en arrière-plan. Formellement, il a d' autres états , mais nous ne sommes intéressés que par ceux-ci:


  • fond (lorsque l'application est minimisée),
  • active (lorsque l'application est active, à l'écran).

Modèles utiles: rappel , délégué ( Cocoa Design Patterns , à propos du rappel sur Wikipedia ). Vous devez également savoir URLSession (dans l'article, le lien mentionne également le travail en arrière-plan avec le réseau, mais en passant).


Tous les exemples sont écrits dans Swift 5 , fonctionnent sur iOS 11 et plus récent (testé sur iOS 11 et 12) et supposent l'utilisation de requêtes HTTP régulières. Pour la plupart, tout cela fonctionnera, à commencer par iOS 9, mais il y a des "nuances".


Le schéma général de travail avec le réseau. URLSession


Travailler avec le réseau n'est pas particulièrement difficile:


  • créer la configuration URLSessionConfiguration ;
  • créer une instance de configuration de URLSession ;
  • créer une tâche (en utilisant session.dataTask(…) et des méthodes similaires);
  • abonnez-vous aux mises à jour des tâches. Les mises à jour viennent de manière asynchrone, elles peuvent venir au délégué, qui est enregistré lors de la création de la session, ou elles peuvent venir dans le rappel, qui est créé lorsque la tâche est créée;
  • lorsque nous avons vu que la tâche est terminée, nous revenons à la logique d'application.

Un exemple simple ressemble à ceci:


 let session = URLSession(configuration: .default) let url = URL(...) let dataTask = session.dataTask(with: url) { data, response, error in ... //     //  callback,    } 

Ce schéma est similaire pour diverses tâches, seules les petites choses changent. Et tant que nous n'avons pas besoin de continuer à travailler avec le réseau après que l'utilisateur a fermé l'application, tout est relativement simple.


Je constate tout de suite que même dans ce scénario, il y a beaucoup de choses intéressantes. Parfois, vous devez travailler avec des redirections délicates, parfois vous avez besoin d'une autorisation, d'un épinglage SSL ou d'un seul coup. Vous pouvez en lire beaucoup à ce sujet. Pour une raison quelconque, travailler avec le réseau en arrière-plan est beaucoup moins décrit.

Création d'une session pour travailler en arrière-plan


Quelle est la différence entre la session URL en arrière- plan et la session habituelle? Il fonctionne en dehors du processus de demande, quelque part à l'intérieur du système. Par conséquent, il ne "meurt" pas lorsque le processus de demande est terminé. Cela s'appelle une session d'arrière-plan (ainsi que l'état de l'application, ce qui est un peu déroutant) et nécessite des paramètres spécifiques. Par exemple, ceci:


 let configuration = URLSessionConfiguration.background(withIdentifier: "com.my.app") configuration.sessionSendsLaunchEvents = true configuration.isDiscretionary = true configuration.allowsCellularAccess = true configuration.shouldUseExtendedBackgroundIdleMode = true configuration.waitsForConnectivity = true URLSession(configuration: configuration, delegate: self, delegateQueue: nil) 

La configuration a de nombreux autres paramètres, mais ceux-ci se rapportent directement aux sessions d'arrière-plan:


  • l'identifiant (passé dans l'initialiseur) est une chaîne utilisée pour faire correspondre les sessions d'arrière-plan au redémarrage de l'application. Si l'application redémarre et que vous créez une session d'arrière-plan avec un identifiant déjà utilisé dans une autre session d'arrière-plan, la nouvelle aura accès aux tâches de la précédente. La conclusion de cela est simple. Pour un fonctionnement correct, vous avez besoin que cet identifiant soit unique pour votre application et permanent (vous pouvez utiliser, par exemple, un dérivé des applications bundleId );
  • sessionSendsLaunchEvents indique si la session d'arrière-plan doit démarrer l'application une fois le transfert de données terminé. Si ce paramètre est défini sur false, le déclencheur ne se produira pas et l'application recevra tous les événements la prochaine fois qu'elle se lancera. Si le paramètre est true , une fois le transfert de données terminé, le système lance l'application et appelle la méthode AppDelegate: application(_:handleEventsForBackgroundURLSession:completionHandler:) correspondante AppDelegate: application(_:handleEventsForBackgroundURLSession:completionHandler:) ;
  • isDiscretionary permet au système de planifier des tâches plus rarement. Cela, d'une part, améliore la durée de vie de la batterie et, d'autre part, cela peut ralentir la tâche. Ou peut-être l'accélérer. Par exemple, si un grand volume est téléchargé, le système pourra suspendre la tâche jusqu'à ce qu'il se connecte au WiFi, puis télécharger rapidement tout sans dépenser Internet mobile lent (si cela est autorisé, quelle est la prochaine). Si la tâche est créée alors que l'application est déjà en arrière-plan, ce paramètre est automatiquement défini sur true ;
  • permetCellularAccess - un paramètre qui montre que vous pouvez utiliser la communication cellulaire pour travailler avec le réseau. Je n'ai pas joué avec lui avec soin, mais selon les critiques, il y a (avec un commutateur de système similaire) un grand nombre de râteaux;
  • shouldUseExtendedBackgroundIdleMode. Un paramètre utile qui montre que le système doit maintenir une connexion avec le serveur plus longtemps lorsque l'application passe en arrière-plan. Sinon, la connexion sera rompue.
  • waitsForConnectivity Dans un appareil mobile, les communications peuvent disparaître pendant de courtes périodes. Les tâches créées à ce moment peuvent soit être suspendues jusqu'à ce qu'une connexion apparaisse, soit renvoyer immédiatement une erreur «pas de connexion». Le paramètre vous permet de contrôler ce comportement. Si elle est false, en l'absence de communication, la tâche s'arrêtera immédiatement avec une erreur. Si true , attendez qu'un lien apparaisse.
  • la dernière ligne (initialiseur de session) contient un paramètre important, delegate. À propos de lui - un peu plus.

Délégué vs rappels


Comme je l'ai dit ci-dessus, il existe deux façons d'obtenir des événements à partir d'une tâche / d'une session. Le premier est le rappel:


 session.dataTask(with: request) { data, response, error in ...   } 

Dans ce cas, l'événement de fin de tâche sera envoyé à la fermeture, où vous devez vérifier s'il y a une erreur, ce qui est dans la réponse et quelles données sont arrivées.


La deuxième option pour travailler avec une session consiste à déléguer. Dans ce cas, nous devons créer une classe qui implémente les protocoles URLSessionDataDelegate et (ou) d'autres à proximité (pour différents types de tâches, les protocoles sont légèrement différents). Une référence à une instance de cette classe se trouve dans une session et ses méthodes sont appelées lorsque des événements sont transmis au délégué. Le lien peut être enregistré dans la session par l'initialiseur. Dans l'exemple, self.


 URLSession(configuration: configuration, delegate: self, delegateQueue: nil) 

Pour les sessions régulières, les deux méthodes sont disponibles. Les sessions d'arrière-plan ne peuvent être utilisées que par un délégué.


Donc, nous avons configuré la session, l'avons créée, regardons comment télécharger quelque chose.


Schéma général de téléchargement des données en arrière-plan


Pour télécharger des données, vous devez généralement former une (URLRequest) , y enregistrer les paramètres / en-têtes / données nécessaires, créer une URLSessionDownloadTask et l'exécuter pour exécution. Quelque chose comme ça:


 var request = URLRequest(...) //  request,   let task = session.downloadTask(with: request) if #available(iOS 11, *) { task.countOfBytesClientExpectsToSend = [approximate size of request] task.countOfBytesClientExpectsToReceive = [approximate size of response] } task.resume() 

À ce stade, rien n'est très différent de la tâche de téléchargement habituelle. Certes, deux paramètres sont apparus countOfBytesClientExpectsToSend / countOfBytesClientExpectsToReceive , ils montrent la quantité de données que nous prévoyons d'envoyer dans la demande et de revenir dans la réponse. Cela est nécessaire pour que le système puisse mieux planifier le travail avec la tâche, le télécharger plus rapidement, sans surcharger. Ces valeurs n'ont pas besoin d'être précises.


Après resume() tâche ira à l'exécution. Pendant le transfert de données, les progrès seront transmis (à ce sujet - lire ci-dessous, il y a aussi des options là-bas), et après l'achèvement, plusieurs méthodes de délégué seront exécutées. Parmi eux, il y en a un très important:


 urlSession(_:downloadTask:didFinishDownloadingTo:) 

Le fait est que le téléchargement a lieu dans un fichier temporaire, après quoi l'application a la possibilité de déplacer ce fichier quelque part ou de faire autre chose avec. Ce fichier temporaire n'est disponible qu'à l'intérieur de cette méthode, après en être sorti, le fichier est supprimé et rien ne peut être fait avec.


Après cette méthode importante, une autre méthode sera appelée où l'erreur tombera si elle se produit. S'il n'y a pas d' error , l' error sera nil.


 urlSession(_:task:didCompleteWithError:) 

Et que se passe-t-il à la fin, si la demande est passée en arrière-plan ou a été complétée? Comment appeler des méthodes de délégué? Ce n'est pas facile ici.


Si le téléchargement de quelque chose qui a été lancé par l'application est terminé et que l'indicateur sessionSendsLaunchEvents dans la configuration de la session, le système lancera l'application (en arrière-plan) et appellera l' application (_: handleEventsForBackgroundURLSession: achèvementHandler :) dans AppDelegate, .


Dans cette méthode, l'application doit:


  • save completionHandler (il devra être appelé après un certain temps, de manière asynchrone et dans le thread principal);
  • recréer une session d'arrière-plan avec le même identifiant que précédemment (et qui est passée à cette méthode en cas de plusieurs sessions d'arrière-plan);
  • dans une session nouvellement créée, les événements arriveront au délégué (en particulier, la très importante urlSession(_:downloadTask:didFinishDownloadingTo:) ), vous devez les traiter, copier les fichiers où vous voulez;
  • Une fois toutes les méthodes appelées, une autre méthode déléguée est appelée, appelée urlSessionDidFinishEvents(forBackgroundURLSession:) et dans laquelle vous devrez appeler le completionHandler. stocké précédemment completionHandler.

C'est important. Il est nécessaire d'appeler completionHandler dans le thread principal à l'aide de DispatchQueue.main.async(...) .

Dans le même temps, vous devez vous rappeler que tout cela se produit dans une application qui fonctionne en arrière-plan. Et cela signifie que les ressources (temps d'exécution) sont limitées. Enregistrez rapidement les fichiers là où vous en avez besoin, modifiez les états nécessaires dans l'application et arrêtez - c'est à peu près tout ce qui peut être fait. Si vous voulez en faire plus, vous pouvez utiliser UIApplication.beginBackgroundTask() ou les nouveaux BackgroundTasks .


Schéma général d'envoi des données de base


Le téléchargement de fichiers sur le serveur fonctionne également avec des restrictions. Cependant, tout commence de la même manière: nous URLSessionUploadTask) une demande, créons une tâche (maintenant ce sera URLSessionUploadTask) , URLSessionUploadTask) la tâche. Quel est le problème?


Le problème est de savoir comment nous créons la demande. Habituellement, nous formons les données envoyées en tant que Data . URLSession, arrière- URLSession, ne sait pas comment travailler avec cela. Et avec une demande de streaming ( uploadTask(withStreamedRequest:) ) ne sait pas non plus comment. Il est nécessaire d'écrire tout ce qui doit être envoyé dans un fichier et de créer une tâche d'envoi à partir du fichier. Cela se révèle en quelque sorte comme ceci:


 var fileUrl = methodThatSavesFileAndRetursItsUrl(...) var request = URLRequest(...) let task = session.uploadTask(with: request, fromFile: fileUrl) task.resume() 

Mais il n'est pas nécessaire d'enregistrer la taille, URLSession peut la consulter elle-même. Après l'envoi, la même méthode déléguée urlSession(_:task:didCompleteWithError:) comme lors du téléchargement. Et tout comme cela, si l'application a été supprimée ou est passée en arrière-plan pendant le processus d'envoi, l' application(_:handleEventsForBackgroundURLSession:completionHandler:), arrivera application(_:handleEventsForBackgroundURLSession:completionHandler:), qui doit être traitée exactement selon les mêmes règles que lors du téléchargement des données.


Qu'est-ce qu'une demande complète?


Pour tester les téléchargements et les envois en arrière-plan, vous devez simuler l'achèvement de l'application (le travail en arrière-plan avec le réseau est spécialement conçu pour y survivre). Comment faire Initialement - aucun moyen. Autrement dit, il n'y a pas de méthode régulière (autorisée, publique) qui permettrait cela. Voyons où est le râteau.


  • Tout d'abord, la fermeture de l'application (en appuyant sur le bouton Accueil ou en faisant un geste approprié) ne fonctionnera pas. Cela ne tuera pas l'application, mais l'enverra uniquement en arrière-plan. Le fait de travailler avec une session en arrière-plan signifie que cela fonctionne même si l'application est "complètement, complètement" supprimée;
  • deuxièmement, il est impossible qu'un débogueur (AppCode, Xcode ou simplement LLDB) soit connecté, il ne laissera pas l'application mourir même un certain temps après sa "fermeture";
  • troisièmement, vous ne pouvez pas tuer l'application depuis la barre des tâches (gestionnaire de tâches, double accueil ou balayage lent "vers le haut"). Ainsi, une application supprimée est considérée comme étant supprimée «en permanence» et le système arrête, avec une telle action, les sessions d'arrière-plan associées à l'application;
  • quatrièmement, vous devez tester ce processus sur un appareil réel. Il n'y a aucun problème de journalisation (voir ci-dessous) et il est plus débogué. On fait valoir que le simulateur devrait également fonctionner comme il se doit. Mais j'ai remarqué des bizarreries inexplicables que je ne peux expliquer qu'avec autre chose que le simulateur. En général, testez sur l'appareil;
  • la seule façon raisonnable de faire ce que vous voulez est avec la fonction exit(int) . Comme tout le monde le sait, vous ne pouvez pas le télécharger sur le serveur ( cela contredit directement les exigences ), mais pour l'instant nous ne faisons que tester - ce n'est pas effrayant. Je connais deux options raisonnables pour utiliser cette fonction:
    • l'appeler automatiquement dans la AppDelegate.applicationDidEnterBackground(_:) afin que l'application soit AppDelegate.applicationDidEnterBackground(_:) immédiatement après sa sortie dans le Springboard;
    • créer un composant dans l'interface (par exemple un bouton, ou accrocher une action sur un geste), en cliquant sur lequel, la exit(...).
      Dans ce cas, l'application sera supprimée et le travail en arrière-plan avec le réseau devrait se poursuivre. Et, après un certain temps, nous devrions obtenir un appel à l' application(_:handleEventsForBackgroundURLSession:completionHandler:).

Comment enregistrer l'application si vous ne pouvez pas utiliser la console de débogage Xcode?


Eh bien, c'est impossible. Vous pouvez, si vous le voulez vraiment. Vous ne pouvez pas démarrer à partir de Xcode, et si l'application, par exemple, a déjà redémarré en raison d'un événement système, vous pouvez attacher (attacher au processus) à l'application et retirer la file d'attente. Mais cette solution est moyenne, vous devez en quelque sorte tester le processus de redémarrage lui-même.


Vous pouvez utiliser des protocoles (journaux, journaux) . Il existe plusieurs options pour leur mise en œuvre:


  • print. Il est souvent utilisé comme «sortons quelque chose rapidement». Dans notre cas, il est impossible à utiliser, puisque nous n'avons pas accès à la console sur l'appareil, l'application est tuée.
  • NSLog. Cela fonctionnera, car il utilise la troisième méthode.
  • os_log. La méthode la plus correcte qui vous permet de configurer correctement les journaux, de les fixer avec le type souhaité, de les désactiver après le débogage, sans couper le code lui-même, etc.

Attention! Avec os_log il y a des problèmes (par exemple, le manque de journaux de débogage) qui sont lus uniquement dans le simulateur, mais pas lus sur cet appareil. Utilisez l'appareil.

Comment utiliser os_log, lisez comment le configurer correctement dans la documentation Apple . En particulier, vous devez activer les journaux de debug et d' info , par défaut, ils sont masqués.


Suivi de la progression du téléchargement ou de l'envoi de données


Dans le processus de transfert de données, je veux comprendre combien a déjà été envoyé, combien il en reste. Il y a deux façons de procéder. La première consiste à utiliser des méthodes déléguées:


  • pour envoyer, vous devez utiliser urlSession(_:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:)
  • il existe une méthode urlSession(_:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:) similaire pour le téléchargement urlSession(_:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:)

Ces méthodes sont appelées chaque fois que la prochaine donnée est téléchargée ou envoyée. Ils ne sont pas nécessairement cohérents avec les méthodes d'achèvement du processus, ils peuvent également être appelés après que les données ont été complètement téléchargées ou envoyées, il est donc impossible de déterminer que «tout est terminé».


La deuxième méthode est plus intéressante. Le fait est que chaque tâche fournit un objet de type Progress (il se trouve dans le champ task.progress ), qui permet de surveiller un processus arbitraire, y compris le processus de transfert de données. En quoi est-il intéressant? Deux choses:


  • à partir des objets Progress , vous pouvez créer une arborescence d'exécution des tâches, dont chaque nœud montrera à quel point toutes les tâches qu'il contient sont avancées. Par exemple, si vous devez envoyer cinq fichiers, vous pouvez prendre la progression pour chacun, faire des progrès généraux, en ajouter cinq autres et surveiller la progression d'un parent, en liant ses mises à jour à un élément d'interface;
  • vous pouvez ajouter votre progression à cette arborescence et vous pouvez également suspendre et annuler les actions associées à la progression ajoutée.

Quel est le lien avec le téléchargement ou l'envoi de données en arrière-plan? Pas question. Les méthodes déléguées ne sont pas appelées et les objets progress meurent à la fin de l'application. Pour les sessions d'arrière-plan, cette méthode ne convient pas.


"Transférer" des tâches d'une session ordinaire à une session d'arrière-plan


Eh bien, il est plus difficile de travailler avec une session d'arrière-plan. Mais c'est pratique! Aucune tâche ne sera perdue, obtiendrons-nous jamais toutes les données demandées, pourquoi ne pas toujours utiliser la session d'arrière-plan?


Malheureusement, elle a des défauts et des défauts graves. Par exemple, une session d'arrière-plan est plus lente. Dans mes expériences, la vitesse a varié plusieurs fois. Deuxièmement, l'exécution en arrière-plan d'une tâche peut être retardée (surtout si le paramètre isDiscretionary est isDiscretionary , ce qui, comme je l'ai mentionné, est toujours true pour les tâches créées pendant que l'application s'exécute en arrière-plan.


Par conséquent, chaque fois que vous créez une tâche, vous devez comprendre exactement quels critères pour son travail, où l'ajouter, à une session régulière ou d'arrière-plan. Normal fonctionne plus vite, démarre immédiatement. Contexte - plus long, pas immédiatement, mais ne sera pas tué si l'utilisateur ferme l'application.


S'il n'y a pas de compréhension évidente que la tâche doit être effectuée dans la session d'arrière-plan (par exemple, le transfert non critique d'une très grande quantité de données, comme la synchronisation ou la sauvegarde), alors cela vaut la peine de procéder comme suit:


  • démarrer la tâche dans une session régulière. Dans ce cas, exécutez backgroundTask pour que le système comprenne que nous avons besoin de temps pour terminer la tâche. Cela donne un certain temps (jusqu'à plusieurs minutes, mais quelque chose a été cassé dans iOS 13 et on ne sait pas ce qui se passe avec) pour que la tâche puisse être terminée.
  • s'il n'a pas le temps, à la fin de backgroundTask, nous transférons la tâche d'une session ordinaire à une session en arrière-plan, où elle continue de fonctionner et se termine quand elle le peut.

Comment transférer? Pas question. Il suffit de tuer (annuler) la tâche habituelle et de créer un arrière-plan similaire (avec la même demande). Pourquoi cela s'appelle-t-il un «transfert»? Et pourquoi entre guillemets?


Il n'y a pas de transfert pour envoyer des données. Il y a exactement ce qui est décrit. Ils ont tué une tâche, lancé une autre, toutes les données envoyées pour la première fois ont été perdues.


Pour le téléchargement, la situation est différente. Le système sait dans quel fichier la demande est téléchargée. Si vous exécutez plusieurs tâches pour télécharger la même URL, par exemple, il n'exécutera pas la demande plusieurs fois. Les données sont téléchargées une fois, après quoi la dernière méthode déléguée (ou rappel) sera exécutée plusieurs fois. Une expérience est décrite ici qui confirme cela. Très probablement, la mise en cache HTTP standard est utilisée à l'intérieur, la même que dans les navigateurs.


Voici un exemple de code qui fait cela:


 let request = URLRequest(url: url) let task = foregroundSession.downloadTask(with: request) let backgroundId = UIApplication.shared.beginBackgroundTask { task.cancel() let task = backgroundSession.downloadTask(with: request) task.resume() } task.resume() 

Si la tâche se termine avant l' expirationHandler fonctionne, vous devez vous rappeler d'appeler UIApplication.shared.endBackgroundTask(backgroundId) . Ceci est décrit plus en détail dans la documentation .


Pour aider le système à continuer le téléchargement (par exemple, l'annulation peut entraîner la suppression du fichier temporaire avant la reprise du téléchargement en arrière-plan), il existe des méthodes spéciales:



 let request = URLRequest(url: url) let task = foregroundSession.downloadTask(with: request) let backgroundId = UIApplication.shared.beginBackgroundTask { task.cancel { data in let task: URLSessionDownloadTask if let data = data { task = backgroundSession.downloadTask(withResumeData: data) } else { task = backgroundSession.downloadTask(with: request) } task.resume() } } 

,


Journaux


— , . — , . background , .


, , background -, , , ( UI, ). , , — . , — , , os_log. ( NSLog)


-


- , . , - . , , , ( ) . , , -, , . — — , . — , - ( ), , .



. ( ), . , , , .


Limitations


:


  • , ;
  • — , ;
  • , (, …);


  • , (task.taskIdentifier) , (Dictionary). , 1, .
  • , URLSession.getAllTasks . , background . , . , . ¯\_(ツ)_/¯
  • , , , , .

, background , . , - . : https://developer.apple.com/library/archive/documentation/General/Conceptual/ExtensibilityPG/ExtensionScenarios.html#//apple_ref/doc/uid/TP40014214-CH21-SW1 . , :


If your app extension initiates a background NSURLSession task, you must also set up a shared container that both the extension and its containing app can access. Use the sharedContainerIdentifier property of the NSURLSessionConfiguration class to specify an identifier for the shared container so that you can access it later.

Source: https://habr.com/ru/post/fr478566/


All Articles