iOS Redes cuando la aplicación no se está ejecutando

imagen


Los usuarios esperan que la red funcione "mágicamente" y sin ser notada. Esta magia depende de los desarrolladores del sistema y las aplicaciones. Es difícil influir en el sistema, por lo tanto, nos limitaremos a la aplicación.


Este tema es complejo y hay innumerables problemas. Discutiremos los que hemos encontrado en los últimos meses. Pido disculpas por el volumen de inmediato. En resumen, de ninguna manera, demasiadas pequeñas cosas a las que vale la pena prestarles atención.


Para empezar, tratemos con la terminología.


La transferencia de datos ocurre en dos direcciones:


  • descargar (descargar, descargar datos del servidor),
  • cargar (enviar datos al servidor).

La aplicación puede estar activa, pero puede funcionar en segundo plano. Formalmente, tiene otros estados , pero solo nos interesan estos:


  • fondo (cuando la aplicación está minimizada),
  • activo (cuando la aplicación está activa, en la pantalla).

Patrones útiles: devolución de llamada , delegado ( Patrones de diseño de cacao , sobre devolución de llamada en Wikipedia ). También necesita saber URLSession (en el artículo, el enlace también menciona el trabajo de fondo con la red, pero de paso).


Todos los ejemplos están escritos en Swift 5 , funcionan en iOS 11 y versiones posteriores (probado en iOS 11 y 12) y asumen el uso de solicitudes HTTP regulares. En su mayor parte, todo esto funcionará, comenzando con iOS 9, pero hay "matices".


El esquema general de trabajo con la red. URLSession


Trabajar con la red no es particularmente difícil:


  • crear la configuración de URLSessionConfiguration ;
  • crear una instancia de configuración de URLSession ;
  • crear una tarea (usando session.dataTask(…) y métodos similares);
  • suscribirse a las actualizaciones de tareas. Las actualizaciones llegan de forma asíncrona, pueden llegar al delegado, que se registra cuando se crea la sesión, o pueden estar en la devolución de llamada, que se crea cuando se crea la tarea;
  • Cuando vimos que la tarea está completa, volvemos a la lógica de la aplicación.

Un ejemplo simple se ve así:


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

Este esquema es similar para varias tareas, solo cambian las pequeñas cosas. Y hasta que no necesitemos seguir trabajando con la red después de que el usuario haya cerrado la aplicación, todo es relativamente simple.


Noto de inmediato que incluso en este escenario hay muchas cosas interesantes. A veces necesita trabajar con redireccionamientos difíciles, a veces necesita autorización, fijación de SSL o todo a la vez. Puedes leer mucho sobre esto. Por alguna razón, trabajar con la red en segundo plano se describe mucho menos.

Crear una sesión para trabajar en segundo plano


¿Cuál es la diferencia entre URLSession de fondo y habitual? Funciona fuera del proceso de solicitud, en algún lugar dentro del sistema. Por lo tanto, no "muere" cuando se completa el proceso de solicitud. Se llama una sesión en segundo plano (así como el estado de la aplicación, que es un poco confuso) y requiere configuraciones específicas. Por ejemplo, esto:


 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 configuración tiene muchos otros parámetros, pero estos se relacionan directamente con las sesiones en segundo plano:


  • El identificador (pasado en el inicializador) es una cadena que se utiliza para hacer coincidir las sesiones en segundo plano cuando la aplicación se reinicia. Si la aplicación se reinicia y crea una sesión en segundo plano con un identificador que ya se utilizó en otra sesión en segundo plano, la nueva tendrá acceso a las tareas de la anterior. La conclusión de esto es simple. Para un funcionamiento correcto, necesita que este identificador sea único para su aplicación y permanente (puede usar, por ejemplo, un derivado de aplicaciones bundleId );
  • sessionSendsLaunchEvents indica si la sesión en segundo plano debe iniciar la aplicación cuando se completa la transferencia de datos. Si este parámetro se establece en false, el disparador no sucederá y la aplicación recibirá todos los eventos la próxima vez que se inicie. Si el parámetro es true , luego de que se complete la transferencia de datos, el sistema iniciará la aplicación y llamará al método AppDelegate: application(_:handleEventsForBackgroundURLSession:completionHandler:) correspondiente AppDelegate: application(_:handleEventsForBackgroundURLSession:completionHandler:) ;
  • isDiscretionary permite que el sistema programe tareas más raramente. Esto, por un lado, mejora la duración de la batería y, por otro, puede ralentizar la tarea. O tal vez acelerarlo. Por ejemplo, si se descarga un gran volumen, el sistema podrá pausar la tarea hasta que se conecte a WiFi y luego descargar todo rápidamente sin gastar Internet móvil lento (si está permitido, ¿qué sigue?). Si la tarea se crea cuando la aplicación ya está en segundo plano, este parámetro se establece automáticamente en true ;
  • allowCellularAccess : un parámetro que muestra que puede usar la comunicación celular para trabajar con la red. No jugué con él con cuidado, pero según las revisiones, allí (junto con un cambio de sistema similar) se presentan una gran cantidad de rastrillos;
  • shouldUseExtendedBackgroundIdleMode. Un parámetro útil que muestra que el sistema debe mantener una conexión con el servidor durante más tiempo cuando la aplicación pasa a segundo plano. De lo contrario, la conexión se interrumpirá.
  • waitsForConnectivity En un dispositivo móvil, las comunicaciones pueden desaparecer por cortos períodos de tiempo. Las tareas creadas en este momento pueden suspenderse hasta que aparezca una conexión, o inmediatamente devolver un error de "no hay conexión". El parámetro le permite controlar este comportamiento. Si es false, en ausencia de comunicación, la tarea se interrumpirá inmediatamente con un error. Si es true , espere hasta que aparezca un enlace.
  • la última línea (inicializador de sesión) contiene un parámetro importante, delegado. Sobre él, un poco más.

Delegado vs Callbacks


Como dije anteriormente, hay dos formas de obtener eventos de una tarea / de una sesión. El primero es la devolución de llamada:


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

En este caso, el evento de finalización de la tarea se enviará al cierre, donde debe verificar si hay un error, qué hay en la respuesta y qué datos han llegado.


La segunda opción para trabajar con una sesión es a través de delegado. En este caso, debemos crear una clase que implemente los protocolos URLSessionDataDelegate y (u) otros cercanos (para los diferentes tipos de tareas, los protocolos son ligeramente diferentes). Una referencia a una instancia de esta clase vive en una sesión, y sus métodos se llaman cuando los eventos se pasan al delegado. El enlace puede ser registrado en la sesión por el inicializador. En el ejemplo, self.


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

Para sesiones regulares, ambos métodos están disponibles. Las sesiones en segundo plano solo pueden ser utilizadas por un delegado.


Entonces, configuramos la sesión, la creamos, veamos cómo descargar algo.


Esquema general para descargar datos en segundo plano


Para descargar datos, generalmente necesita formar una (URLRequest) , registrar los parámetros / encabezados / datos necesarios en ella, crear una URLSessionDownloadTask y ejecutarla para su ejecución. Algo como esto:


 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() 

En este punto, nada es muy diferente de la tarea de descarga habitual. Es cierto que aparecieron dos parámetros countOfBytesClientExpectsToSend / countOfBytesClientExpectsToReceive , muestran la cantidad de datos que planeamos enviar en la solicitud y volver a la respuesta. Esto es necesario para que el sistema pueda planificar más correctamente el trabajo con la tarea, descargar más rápido, sin sobrecargarse. Estos valores no tienen que ser precisos.


Después de resume() tarea se ejecutará. Durante la transferencia de datos, se transmitirá el progreso (al respecto; lea a continuación, también hay opciones allí) y, una vez completado, se ejecutarán varios métodos de delegado. Entre ellos, hay uno muy importante:


 urlSession(_:downloadTask:didFinishDownloadingTo:) 

El hecho es que la descarga se realiza en un archivo temporal, después de lo cual la aplicación tiene la oportunidad de mover este archivo a algún lugar o hacer algo más con él. Este archivo temporal está disponible solo dentro de este método, después de salir de él, el archivo se elimina y no se puede hacer nada con él.


Después de este importante método, se llamará a otro método donde caerá el error si ocurre. Si no hay error , el error será nil.


 urlSession(_:task:didCompleteWithError:) 

¿Y qué sucede al final, si la solicitud pasó a un segundo plano o se completó? ¿Cómo llamar a los métodos delegados? No es fácil aquí.


Si la descarga de algo iniciado por la aplicación ha finalizado y el indicador sessionSendsLaunchEvents en la configuración de la sesión, el sistema iniciará la aplicación (en segundo plano) y llamará a la aplicación (_: handleEventsForBackgroundURLSession: completeHandler :) método en AppDelegate, .


En este método, la aplicación debería:


  • save completionHandler (deberá llamarse después de un tiempo, de forma asincrónica y en el hilo principal);
  • recrear una sesión en segundo plano con el mismo identificador que antes (y que se pasa a este método en caso de que haya varias sesiones en segundo plano);
  • en una sesión recién creada, los eventos llegarán al delegado (en particular, la muy importante urlSession(_:downloadTask:didFinishDownloadingTo:) ), debe procesarlos, copiar los archivos donde desee;
  • después de llamar a todos los métodos, se llama a otro método delegado, que se llama urlSessionDidFinishEvents(forBackgroundURLSession:) y en el que tendrá que llamar al completionHandler. almacenado anteriormente completionHandler.

Es importante Es necesario llamar a completionHandler en el hilo principal usando DispatchQueue.main.async(...) .

Al mismo tiempo, debe recordar que todo esto sucede en una aplicación que funciona en segundo plano. Y esto significa que los recursos (tiempo de ejecución) son limitados. Guarde rápidamente los archivos donde lo necesite, cambie los estados necesarios en la aplicación y cierre, eso es todo lo que se puede hacer. Si desea hacer más, puede usar UIApplication.beginBackgroundTask() o las nuevas BackgroundTasks .


Esquema general de envío de datos de fondo


Subir archivos al servidor también funciona con restricciones. Sin embargo, todo comienza de manera similar: formamos una solicitud, creamos una tarea (ahora será URLSessionUploadTask) , ejecutamos la tarea. Cual es el problema


El problema es cómo creamos la solicitud. Por lo general, formamos los datos enviados como Data . Background URLSession, no sabe cómo trabajar con esto. Y con una solicitud de transmisión ( uploadTask(withStreamedRequest:) ) tampoco sabe cómo. Es necesario escribir todo lo que debe enviarse a un archivo y crear una tarea de envío desde el archivo. Resulta de alguna manera así:


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

Pero no es necesario registrar el tamaño, URLSession puede verlo por sí mismo. Después de enviar, urlSession(_:task:didCompleteWithError:) al mismo método delegado urlSession(_:task:didCompleteWithError:) como cuando se descarga. Y así, si la aplicación fue eliminada o pasó a un segundo plano durante el proceso de envío, la application(_:handleEventsForBackgroundURLSession:completionHandler:), llegará application(_:handleEventsForBackgroundURLSession:completionHandler:), que debe procesarse exactamente de acuerdo con las mismas reglas que cuando se descargan datos.


¿Qué es una solicitud completa?


Para probar las descargas y envíos en segundo plano, debe simular la finalización de la aplicación (el trabajo en segundo plano con la red está especialmente diseñado para sobrevivir). Como hacerlo Inicialmente, de ninguna manera. Es decir, no existe un método regular (autorizado, público) que permita hacer esto. Veamos dónde está el rastrillo.


  • En primer lugar, simplemente cerrar la aplicación (presionando el botón de inicio o haciendo un gesto apropiado) no funcionará. Esto no matará la aplicación, sino que solo la enviará a un segundo plano. El significado de trabajar con una sesión en segundo plano es que funciona incluso si la aplicación se elimina "por completo";
  • en segundo lugar, es imposible que un depurador (AppCode, Xcode o simplemente LLDB) esté conectado, no permitirá que la aplicación muera ni siquiera un tiempo después de que se "cierre";
  • en tercer lugar, no puede eliminar la aplicación desde la barra de tareas (administrador de tareas, inicio doble o deslizar lentamente "hacia arriba"). Por lo tanto, una aplicación cancelada se considera "permanentemente" y el sistema detiene, junto con dicha acción, las sesiones de fondo asociadas con la aplicación;
  • cuarto, debe probar este proceso en un dispositivo real. No hay problemas con el registro (ver más abajo) y está más depurado. Se argumenta que el simulador también debería funcionar como debería. Pero noté rarezas inexplicables que no puedo explicar con nada más que fallas en el simulador. En general, prueba en el dispositivo;
  • La única forma razonable de hacer lo que quiere es con la función exit(int) . Como todos saben, no puede cargarlo en el servidor ( esto contradice directamente los requisitos ), pero por ahora solo estamos probando, no da miedo. Conozco dos opciones razonables para usar esta función:
    • AppDelegate.applicationDidEnterBackground(_:) automáticamente en el método AppDelegate.applicationDidEnterBackground(_:) para que la aplicación se cierre inmediatamente después de salir al Springboard;
    • haga un componente en la interfaz (por ejemplo, un botón o cuelgue una acción en un gesto), haciendo clic en el cual exit(...).
      En este caso, la aplicación se cerrará y el trabajo en segundo plano con la red debería continuar. Y, después de algún tiempo, deberíamos recibir una llamada a la application(_:handleEventsForBackgroundURLSession:completionHandler:).

¿Cómo registrar la aplicación si no puede usar la consola de depuración Xcode?


Bueno, es imposible Puedes, si realmente quieres. No puede comenzar desde Xcode, y si la aplicación, por ejemplo, ya se ha reiniciado debido a un evento del sistema, puede adjuntarla (adjuntarla al proceso) a la aplicación y retirarla. Pero esta solución es regular, debe probar de alguna manera el proceso de reinicio.


Puede usar protocolos (registros, registros) . Hay varias opciones para su implementación:


  • print. A menudo se usa como "vamos a sacar algo rápidamente". En nuestro caso, es imposible de usar, ya que no tenemos acceso a la consola en el dispositivo, la aplicación se cierra.
  • NSLog. Funcionará, ya que utiliza el tercer método.
  • os_log. El método más correcto que le permite configurar correctamente los registros, pegarlos con el tipo deseado, deshabilitar después de la depuración, sin cortar el código en sí, y así sucesivamente.

Atencion Con os_log hay problemas (por ejemplo, la falta de registros de depuración) que se reproducen solo en el simulador, pero no se reproducen en este dispositivo. Usa el dispositivo.

Cómo usar os_log, lea cómo configurarlo correctamente en la documentación de Apple . En particular, debe habilitar los registros de debug e info , de forma predeterminada están ocultos.


Seguimiento del progreso de descarga o envío de datos


En el proceso de transferencia de datos, quiero entender cuánto se ha enviado, cuánto queda. Hay dos formas de hacer esto. El primero es usar métodos delegados:


  • para enviar, debe usar urlSession(_:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:)
  • existe un método similar para descargar urlSession(_:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:)

Estos métodos se llaman cada vez que se descarga o envía la siguiente pieza de datos. No son necesariamente consistentes con los métodos para completar el proceso; también se pueden invocar después de que los datos se hayan descargado o enviado por completo, por lo tanto, es imposible determinar que "todo ha terminado".


El segundo método es más interesante. El hecho es que cada tarea proporciona un objeto de tipo Progress (se encuentra en el campo task.progress ), que proporciona la capacidad de monitorear un proceso arbitrario, incluido el proceso de transferencia de datos. ¿Cómo es él interesante? Dos cosas:


  • desde los objetos de Progress puede crear un árbol de ejecución de tareas, cada uno de los cuales mostrará cuán avanzadas son todas las tareas que contiene. Por ejemplo, si necesita enviar cinco archivos, puede tomar el progreso de cada uno, hacer un progreso general, agregarle otros cinco y monitorear el progreso de un padre, vinculando sus actualizaciones a algún elemento de la interfaz;
  • puede agregar su progreso a este árbol, y también puede pausar y cancelar acciones asociadas con el progreso agregado.

¿Cómo se relaciona esto con la descarga o el envío de datos en segundo plano? De ninguna manera No se llama a los métodos delegados, y los objetos de progreso mueren cuando finaliza la aplicación. Para sesiones en segundo plano, este método no es adecuado.


"Transferir" tareas de una sesión normal a una sesión en segundo plano


Bueno, es más difícil trabajar con una sesión de fondo. ¡Pero esto es conveniente! No se perderá una sola tarea, alguna vez obtendremos todos los datos que solicitamos, ¿por qué no usar siempre la sesión en segundo plano?


Desafortunadamente, ella tiene fallas y serias. Por ejemplo, una sesión en segundo plano es más lenta. En mis experimentos, la velocidad varió varias veces. En segundo lugar, la ejecución en segundo plano de una tarea puede retrasarse (especialmente si se establece el parámetro isDiscretionary , que, como mencioné, siempre es true para las tareas creadas mientras la aplicación se ejecuta en segundo plano.


Por lo tanto, cada vez que crea una tarea, debe comprender exactamente qué criterios para su trabajo, dónde agregarla, a una sesión regular o en segundo plano. Normal corre más rápido, comienza de inmediato. Fondo: más largo, no inmediatamente, pero no se eliminará si el usuario cierra la aplicación.


Si no hay una comprensión obvia de que la tarea debe realizarse en la sesión en segundo plano (por ejemplo, la transferencia no crítica de una gran cantidad de datos, como la sincronización o la copia de seguridad), entonces vale la pena hacer lo siguiente:


  • Comience la tarea en una sesión regular. En este caso, ejecute backgroundTask para que el sistema comprenda que necesitamos tiempo para completar la tarea. Esto da algo de tiempo (hasta varios minutos, pero algo se rompió en iOS 13 y no está claro qué está sucediendo con él) para que la tarea se pueda completar.
  • si no tiene tiempo, al final de backgroundTask transferimos la tarea de una sesión normal a una de fondo, donde continúa funcionando y termina cuando puede.

¿Cómo transferir? De ninguna manera Simplemente elimine (cancele) la tarea habitual y cree un fondo similar (con la misma solicitud). ¿Por qué se llama esto una "transferencia"? ¿Y por qué entre comillas?


No hay transferencia para enviar datos. Hay exactamente lo que se describe. Mataron una tarea, lanzaron otra, se perdieron todos los datos que se enviaron por primera vez.


Para descargar, la situación es diferente. El sistema sabe a qué archivo se descarga la solicitud. Si ejecuta varias tareas para descargar la misma URL, por ejemplo, no ejecutará la solicitud varias veces. Los datos se descargan una vez, después de lo cual el método delegado final (o devolución de llamada) se ejecutará varias veces. Aquí se describe un experimento que confirma esto. Lo más probable es que el almacenamiento en caché HTTP estándar se use en el interior, al igual que en los navegadores.


Aquí hay un código de muestra que hace esto:


 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 tarea finaliza antes de que expirationHandler UIApplication.shared.endBackgroundTask(backgroundId) , debe recordar llamar a UIApplication.shared.endBackgroundTask(backgroundId) . Esto se describe con más detalle en la documentación .


Para ayudar al sistema a continuar la descarga (por ejemplo, la cancelación puede hacer que el archivo temporal se elimine antes de que se reanude la descarga en segundo plano), existen métodos especiales:



 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() } } 

El rastrillo que pisé


Registros


La parte más difícil de todo esto es entender exactamente lo que está sucediendo. Un registro excelente es la primera tarea que debe abordarse de inmediato. El comportamiento de las sesiones en segundo plano no se puede probar de ninguna manera, excepto mediante registros normales.


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


-


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



. ( ), . , , , .


Limitaciones


:


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


  • , (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/478566/


All Articles