iOS Rede quando o aplicativo não está sendo executado

imagem


Os usuários esperam que a rede funcione “magicamente” e despercebida. Essa mágica depende dos desenvolvedores do sistema e dos aplicativos. É difícil influenciar o sistema, portanto, nos restringiremos ao aplicativo.


Este tópico é complexo e há inúmeros problemas. Discutiremos aqueles que encontramos nos últimos meses. Peço desculpas pelo volume imediatamente. Em suma, de jeito nenhum, muitas pequenas coisas que valem a pena prestar atenção.


Para começar, vamos lidar com a terminologia.


A transferência de dados ocorre em duas direções:


  • download (download, download de dados do servidor),
  • upload (envio de dados para o servidor).

O aplicativo pode estar ativo, mas pode funcionar em segundo plano. Formalmente, ele tem outros estados , mas estamos interessados ​​apenas nestes:


  • plano de fundo (quando o aplicativo é minimizado),
  • ativo (quando o aplicativo está ativo, na tela).

Padrões úteis: retorno de chamada , delegado ( Cocoa Design Patterns , sobre retorno de chamada na Wikipedia ). Você também precisa saber URLSession (no artigo, o link também menciona o trabalho em segundo plano com a rede, mas de passagem).


Todos os exemplos estão escritos no Swift 5 , funcionam no iOS 11 e mais recentes (testados no iOS 11 e 12) e assumem o uso de solicitações HTTP regulares. Na maioria das vezes, tudo isso funcionará, começando com o iOS 9, mas existem "nuances".


O esquema geral de trabalhar com a rede. URLSession


Trabalhar com a rede não é particularmente difícil:


  • crie a configuração URLSessionConfiguration ;
  • crie uma instância de configuração do URLSession ;
  • crie uma tarefa (usando session.dataTask(…) e métodos similares);
  • assine atualizações de tarefas. As atualizações são assincronizadas, podem chegar ao delegado, registrado quando a sessão é criada, ou podem estar no retorno de chamada, criado quando a tarefa é criada;
  • quando vimos que a tarefa foi concluída, retornamos à lógica do aplicativo.

Um exemplo simples é assim:


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

Esse esquema é semelhante para várias tarefas, apenas as pequenas coisas mudam. E até não precisarmos continuar trabalhando com a rede após o usuário fechar o aplicativo, tudo é relativamente simples.


Percebo imediatamente que, mesmo nesse cenário, há muitas coisas interessantes. Às vezes, você precisa trabalhar com redirecionamentos complicados, às vezes, precisa de autorização, fixação de SSL ou tudo de uma vez. Você pode ler muito sobre isso. Por alguma razão, trabalhar com a rede no estado de segundo plano é descrito muito menos.

Criando uma sessão para trabalhar em segundo plano


Qual é a diferença entre URLSession em segundo plano e usual? Funciona fora do processo de aplicação, em algum lugar dentro do sistema. Portanto, ele não "morre" quando o processo de inscrição é concluído. É chamado de sessão em segundo plano (assim como o estado do aplicativo, que é um pouco confuso) e requer configurações específicas. Por exemplo, isto:


 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) 

A configuração possui muitos outros parâmetros, mas eles estão diretamente relacionados às sessões em segundo plano:


  • identificador (passado no inicializador) é uma sequência usada para corresponder às sessões em segundo plano quando o aplicativo é reiniciado. Se o aplicativo reiniciar e você criar uma sessão em segundo plano com um identificador já usado em outra sessão em segundo plano, o novo terá acesso às tarefas da anterior. A conclusão disso é simples. Para uma operação correta, você precisa que esse identificador seja exclusivo para seu aplicativo e permanente (você pode usar, por exemplo, uma derivada de aplicativos bundleId );
  • sessionSendsLaunchEvents indica se a sessão em segundo plano deve iniciar o aplicativo quando a transferência de dados for concluída. Se esse parâmetro for definido como false, o gatilho não ocorrerá e o aplicativo receberá todos os eventos na próxima vez em que for iniciado. Se o parâmetro for true , depois que a transferência de dados for concluída, o sistema iniciará o aplicativo e chamará o método AppDelegate: application(_:handleEventsForBackgroundURLSession:completionHandler:) correspondente AppDelegate: application(_:handleEventsForBackgroundURLSession:completionHandler:) ;
  • isDiscretionary permite que o sistema agende tarefas mais raramente. Isso, por um lado, melhora a vida útil da bateria e, por outro, pode retardar a tarefa. Ou talvez acelere. Por exemplo, se um grande volume for baixado, o sistema poderá pausar a tarefa até se conectar ao Wi-Fi e depois baixar tudo rapidamente sem gastar a Internet móvel lenta (se for permitido, o que vem a seguir). Se a tarefa for criada quando o aplicativo já estiver em segundo plano, esse parâmetro será automaticamente definido como true ;
  • allowCellularAccess - um parâmetro que mostra que você pode usar a comunicação celular para trabalhar com a rede. Eu não brinquei com ele com cuidado, mas, de acordo com os comentários, há (junto com uma troca de sistema semelhante) um grande número de rakes;
  • shouldUseExtendedBackgroundIdleMode. Um parâmetro útil que mostra que o sistema deve manter uma conexão com o servidor por mais tempo quando o aplicativo entrar em segundo plano. Caso contrário, a conexão será interrompida.
  • waitsForConnectivity Em um dispositivo móvel, as comunicações podem desaparecer por curtos períodos de tempo. As tarefas criadas neste momento podem ser suspensas até que uma conexão apareça ou retornar imediatamente um erro "sem conexão". O parâmetro permite controlar esse comportamento. Se for false, na ausência de comunicação, a tarefa será interrompida imediatamente com um erro. Se true , aguarde até que um link apareça.
  • a última linha (inicializador de sessão) contém um parâmetro importante, delegar. Sobre ele - um pouco mais.

Delegado x retornos de chamada


Como eu disse acima, existem duas maneiras de obter eventos de uma tarefa / de uma sessão. O primeiro é o retorno de chamada:


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

Nesse caso, o evento de conclusão da tarefa será enviado ao fechamento, onde você precisará verificar se há um erro, o que há na resposta e quais dados chegaram.


A segunda opção para trabalhar com uma sessão é através do delegado. Nesse caso, devemos criar uma classe que implemente os protocolos URLSessionDataDelegate e (ou) outros próximos (para diferentes tipos de tarefas, os protocolos são ligeiramente diferentes). Uma referência a uma instância dessa classe reside em uma sessão e seus métodos são chamados quando os eventos são passados ​​para o delegado. O link pode ser registrado na sessão pelo inicializador. No exemplo, o self.


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

Para sessões regulares, ambos os métodos estão disponíveis. As sessões em segundo plano só podem ser usadas por um delegado.


Então, montamos a sessão, criamos, vamos ver como fazer o download de algo.


Esquema geral para baixar dados em segundo plano


Para baixar dados, geralmente é necessário formar uma (URLRequest) , registrando os parâmetros / cabeçalhos / dados necessários, criar uma URLSessionDownloadTask e executá-la para execução. Algo assim:


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

Neste ponto, nada é muito diferente da tarefa de download usual. É verdade que dois parâmetros apareceram countOfBytesClientExpectsToSend / countOfBytesClientExpectsToReceive , eles mostram a quantidade de dados que planejamos enviar na solicitação e voltamos à resposta. Isso é necessário para que o sistema possa planejar mais corretamente o trabalho com a tarefa, faça o download mais rápido, sem sobrecarregar. Esses valores não precisam ser precisos.


Após resume() tarefa será executada. Durante a transferência de dados, o progresso será transmitido (sobre isso - leia abaixo, também existem opções) e, após a conclusão, vários métodos de delegação serão executados. Entre eles, há um muito importante:


 urlSession(_:downloadTask:didFinishDownloadingTo:) 

O fato é que o download ocorre em um arquivo temporário, após o qual o aplicativo tem a oportunidade de mover esse arquivo para algum lugar ou fazer outra coisa com ele. Este arquivo temporário está disponível apenas dentro deste método, depois de sair dele, o arquivo é excluído e nada pode ser feito com ele.


Após esse método importante, outro método será chamado onde o erro cairá se ocorrer. Se não houver error , o error será nil.


 urlSession(_:task:didCompleteWithError:) 

E o que acontece no final, se o aplicativo entrou em segundo plano ou foi concluído? Como chamar métodos de delegação? Não é fácil aqui.


Se o download de algo iniciado pelo aplicativo tiver terminado e o sinalizador sessionSendsLaunchEvents na configuração da sessão, o sistema iniciará o aplicativo (em segundo plano) e chamará o método application (_: handleEventsForBackgroundURLSession: conclusãoHandler :) no AppDelegate, .


Nesse método, o aplicativo deve:


  • salve o completionHandler (ele precisará ser chamado depois de algum tempo, de forma assíncrona e no encadeamento principal);
  • recrie uma sessão em segundo plano com o mesmo identificador de antes (e que é passado para esse método, caso haja várias sessões em segundo plano);
  • em uma sessão recém-criada, os eventos chegarão ao delegado (em particular, a muito importante urlSession(_:downloadTask:didFinishDownloadingTo:) ), você precisa processá-los, copiar os arquivos onde quiser;
  • depois que todos os métodos são chamados, outro método delegado é chamado, chamado urlSessionDidFinishEvents(forBackgroundURLSession:) e no qual você precisará chamar o completionHandler. armazenado anteriormente completionHandler.

É importante. É necessário chamar a completionHandler no thread principal usando DispatchQueue.main.async(...) .

Ao mesmo tempo, é preciso lembrar que tudo isso acontece em um aplicativo que funciona em segundo plano. E isso significa que os recursos (tempo de execução) são limitados. Salve rapidamente os arquivos onde precisar, altere os estados necessários no aplicativo e desligue - é tudo o que pode ser feito. Se você quiser fazer mais, pode usar UIApplication.beginBackgroundTask() ou as novas BackgroundTasks .


Esquema geral de envio de dados gerais


O upload de arquivos para o servidor também funciona com restrições. No entanto, tudo começa de maneira semelhante: formamos uma solicitação, criamos uma tarefa (agora será URLSessionUploadTask) , executamos a tarefa. Qual é o problema?


O problema é como criamos a solicitação. Normalmente, formamos os dados enviados como Data . URLSession, segundo URLSession, não sabe como trabalhar com isso. E com uma solicitação de streaming ( uploadTask(withStreamedRequest:) ) também não sabe como. É necessário escrever tudo o que precisa ser enviado para um arquivo e criar uma tarefa de envio a partir do arquivo. Acontece de alguma forma assim:


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

Mas não há necessidade de registrar o tamanho, o URLSession pode ver por si próprio. Após o envio, o mesmo método delegado urlSession(_:task:didCompleteWithError:) como durante o download. E assim, se o aplicativo foi morto ou entrou em segundo plano durante o processo de envio, chegará o application(_:handleEventsForBackgroundURLSession:completionHandler:), que deverá ser processado exatamente de acordo com as mesmas regras do download de dados.


O que é um aplicativo completo?


Para testar os downloads e envios em segundo plano, você precisa simular a conclusão do aplicativo (o trabalho em segundo plano com a rede foi especialmente projetado para sobreviver a isso). Como fazer isso? Inicialmente - de jeito nenhum. Ou seja, não existe um método regular (autorizado, público) que permita que isso seja feito. Vamos ver onde está o ancinho.


  • Primeiro, apenas fechar o aplicativo (pressionando o botão Início ou fazendo um gesto apropriado) não funcionará. Isso não mata o aplicativo, mas apenas o envia para segundo plano. O significado de trabalhar com uma sessão em segundo plano é que ele funciona mesmo que o aplicativo seja "completamente, completamente" eliminado;
  • segundo, é impossível que um depurador (AppCode, Xcode ou apenas LLDB) esteja conectado; ele não permitirá que o aplicativo morra nem um pouco depois de ser "fechado";
  • em terceiro lugar, você não pode matar o aplicativo na barra de tarefas (gerenciador de tarefas, Home duplo ou furto lento "up"). Assim, um aplicativo morto é considerado morto "permanentemente" e o sistema interrompe, juntamente com essa ação, as sessões em segundo plano associadas ao aplicativo;
  • quarto, você precisa testar esse processo em um dispositivo real. Não há problemas com o log (veja abaixo) e é mais depurado. Argumenta-se que o simulador também deve funcionar como deveria. Mas notei esquisitices inexplicáveis ​​que não consigo explicar com nada além das falhas do simulador. Em geral, teste no dispositivo;
  • a única maneira razoável de fazer o que você quer é com a função exit(int) . Como todos sabem, você não pode enviá-lo para o servidor ( isso contradiz diretamente os requisitos ), mas por enquanto estamos apenas testando - não é assustador. Conheço duas opções razoáveis ​​para usar esta função:
    • chame-o automaticamente no método AppDelegate.applicationDidEnterBackground(_:) para que o aplicativo seja AppDelegate.applicationDidEnterBackground(_:) imediatamente após sair para o Springboard;
    • crie um componente na interface (por exemplo, um botão ou pendure uma ação em um gesto), clicando no qual a exit(...).
      Nesse caso, o aplicativo será interrompido e o trabalho em segundo plano com a rede deve continuar. E, após algum tempo, devemos receber uma chamada para o application(_:handleEventsForBackgroundURLSession:completionHandler:).

Como registrar o aplicativo se você não pode usar o console de depuração do Xcode?


Bem, é impossível. Você pode, se você realmente quiser. Você não pode iniciar a partir do Xcode, e se o aplicativo, por exemplo, já tiver sido reiniciado devido a um evento do sistema, você poderá anexar (anexar ao processo) ao aplicativo e desenfileirar. Mas essa solução é tão positiva que você precisa testar de alguma forma o próprio processo de reinicialização.


Você pode usar protocolos (logs, logs) . Existem várias opções para sua implementação:


  • print. É frequentemente usado como "vamos lançar algo rapidamente". No nosso caso, é impossível usar, já que não temos acesso ao console no dispositivo, o aplicativo é encerrado.
  • NSLog. Ele funcionará, pois usa o terceiro método.
  • os_log. O método mais correto que permite configurar corretamente os logs, afixá-los com o tipo desejado, desabilitar após a depuração, sem cortar o próprio código e assim por diante.

Atenção! Com o os_log existem problemas (por exemplo, a falta de logs de depuração) que são executados apenas no simulador, mas não executados neste dispositivo. Use o dispositivo

Como usar o os_log, leia como configurá-lo corretamente na documentação da Apple . Em particular, você deve habilitar os logs de debug e info , por padrão eles estão ocultos.


Acompanhar o andamento do download ou envio de dados


No processo de transferência de dados, quero entender quanto já foi enviado, quanto resta. Existem duas maneiras de fazer isso. O primeiro é usar métodos de delegação:


  • para enviar, você precisa usar urlSession(_:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:)
  • existe um método urlSession(_:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:) semelhante para fazer o download urlSession(_:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:)

Esses métodos são chamados sempre que o próximo dado é baixado ou enviado. Eles não são necessariamente consistentes com os métodos para concluir o processo, mas também podem ser chamados após os dados terem sido completamente baixados ou enviados, portanto, é impossível determinar que "tudo terminou".


O segundo método é mais interessante. O fato é que cada tarefa fornece um objeto do tipo Progress (está no campo task.progress ), que fornece a capacidade de monitorar um processo arbitrário, incluindo o processo de transferência de dados. Como ele é interessante? Duas coisas:


  • dos objetos Progress , você pode criar uma árvore de execução de tarefas, cada nó mostrando o quão avançadas são todas as tarefas que ela contém. Por exemplo, se você precisar enviar cinco arquivos, poderá acompanhar o progresso de cada um, progredir em geral, adicionar outros cinco e monitorar um progresso dos pais, vinculando suas atualizações a algum elemento da interface;
  • você pode adicionar seu progresso a essa árvore e também pode pausar e cancelar ações associadas ao progresso adicionado.

Como isso está relacionado ao download ou envio de dados em segundo plano? De jeito nenhum. Os métodos delegados não são chamados e os objetos de progresso morrem quando o aplicativo termina. Para sessões em segundo plano, esse método não é adequado.


"Transferir" tarefas de uma sessão regular para uma sessão em segundo plano


Bem, é mais difícil trabalhar com uma sessão em segundo plano. Mas isso é conveniente! Nem uma única tarefa será perdida; sempre obteremos todos os dados solicitados; por que nem sempre usamos a sessão em segundo plano?


Infelizmente, ela tem defeitos e sérios. Por exemplo, uma sessão em segundo plano é mais lenta. Nas minhas experiências, a velocidade variou várias vezes. Em segundo lugar, a execução em segundo plano de uma tarefa pode ser atrasada (especialmente se o parâmetro isDiscretionary estiver isDiscretionary , o que, como mencionei, é sempre true para tarefas criadas enquanto o aplicativo está sendo executado em segundo plano.


Portanto, toda vez que você cria uma tarefa, precisa entender exatamente quais critérios para seu trabalho, onde adicioná-lo, a uma sessão regular ou em segundo plano. Normal corre mais rápido, inicia imediatamente. Segundo plano - mais longo, não imediatamente, mas não será eliminado se o usuário fechar o aplicativo.


Se não houver um entendimento óbvio de que a tarefa deve ser executada na sessão em segundo plano (por exemplo, transferência não crítica de uma quantidade muito grande de dados, como sincronização ou backup), vale a pena fazer o seguinte:


  • inicie a tarefa em uma sessão regular. Nesse caso, execute backgroundTask para que o sistema entenda que precisamos de tempo para concluir a tarefa. Isso leva algum tempo (até vários minutos, mas algo foi quebrado no iOS 13 e não está claro o que está acontecendo com ele) para que a tarefa possa ser concluída.
  • se não houver tempo, no final da tarefa em segundo plano, transferiremos a tarefa de uma sessão regular para uma em segundo plano, onde ela continua a funcionar e termina quando pode.

Como transferir? De jeito nenhum. Basta matar (cancelar) a tarefa usual e criar um plano de fundo semelhante (com a mesma solicitação). Por que isso é chamado de "transferência"? E por que entre aspas?


Não há transferência para enviar dados. Existe exatamente o que é descrito. Eles mataram uma tarefa, lançaram outra, todos os dados enviados pela primeira vez foram perdidos.


Para baixar, a situação é diferente. O sistema sabe para qual arquivo a solicitação foi baixada. Se você executar várias tarefas para baixar o mesmo URL, por exemplo, ele não executará a solicitação várias vezes. Os dados são baixados uma vez, após o qual o método delegado final (ou retorno de chamada) será executado várias vezes. Um experimento é descrito aqui que confirma isso. Provavelmente, o cache HTTP padrão é usado dentro, o mesmo que nos navegadores.


Aqui está o código de exemplo que faz isso:


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

Se a tarefa terminar antes que o expirationHandler funcione, lembre-se de chamar UIApplication.shared.endBackgroundTask(backgroundId) . Isso é descrito em mais detalhes na documentação .


Para ajudar o sistema a continuar o download (por exemplo, o cancelamento pode excluir o arquivo temporário antes que o download em segundo plano seja retomado), existem métodos especiais:



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

,


Logs


— , . — , . background , .


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


-


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



. ( ), . , , , .


Limitações


:


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


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


All Articles