Explicação detalhada da relação entre o thread principal do Android e o thread filho

Explicação detalhada da relação entre o thread principal do Android e o thread filho

Tópico principal e renderização de tela

Quando o usuário inicia um aplicativo, o Android cria um novo processo Linux e thread de execução. Este thread principal também é chamado de thread de interface (UI thread) e é responsável por todas as atividades que ocorrem na tela.

No Android, o design do thread principal é muito simples: sua única tarefa é obter tarefas (blocos de trabalho) da fila de trabalho segura para thread e executá-los até que o aplicativo seja encerrado.

Essas tarefas executadas pelo encadeamento principal vêm das seguintes fontes: retornos de chamada relacionados às informações do ciclo de vida, eventos do usuário (como entrada) ou eventos de outros aplicativos e processos. Claro, também podemos implementar filas de tarefas por nós mesmos, sem usar estruturas no desenvolvimento de aplicativos.

Quase qualquer bloco de código executado pelo aplicativo está associado a callbacks de eventos (como entrada, expansão de layout ou desenho). Quando uma operação dispara um evento, o encadeamento filho que possui o evento enviará o evento do encadeamento filho para a fila de mensagens do encadeamento principal. Então, o thread principal pode fornecer serviços para o evento.

Quando uma animação ou atualização de tela estiver sendo realizada, o sistema tentará realizar uma tarefa (responsável por desenhar a tela) a cada 16 ms ou mais, de modo a renderizar a uma velocidade uniforme de 60 quadros por segundo. Para que o sistema atinja este objetivo, a hierarquia de interface / visualização deve ser atualizada no thread principal. No entanto, se houver tarefas demais ou longas na fila de mensagens do thread principal, o thread principal não poderá concluir a atualização rápido o suficiente. Se o thread principal não puder concluir a tarefa em 16 ms, o usuário pode perceber um congelamento, atraso ou a interface não responde à entrada. Se o thread principal for bloqueado por cerca de 5 segundos, o sistema exibirá uma caixa de diálogo "O aplicativo não está respondendo" (ANR), permitindo que o usuário feche diretamente o aplicativo.

A fim de evitar que o encadeamento principal execute um grande número de tarefas demoradas e causem problemas como travamentos, devemos mover um grande número de tarefas demoradas ou demoradas do encadeamento principal para os sub-encadeamentos para processamento, de modo que não afeta a renderização suave e responde rapidamente à entrada do usuário.

Relação de referência entre threads filhos e objetos de IU

De acordo com o design do Android, os objetos de IU no Android não são seguros para thread. Independentemente de estar criando, usando ou destruindo objetos de UI, os aplicativos devem ser executados no thread principal. Se você tentar modificar ou mesmo fazer referência a objetos de IU em threads diferentes do thread principal, isso pode causar exceções, falhas silenciosas, travamentos e outros comportamentos anormais indefinidos.

Portanto, devemos operar objetos de UI no thread principal. Conforme mencionado acima, precisamos realizar tarefas demoradas em threads filho, mas e se as tarefas demoradas que realizamos estiverem relacionadas a objetos de IU? Por exemplo, nossa operação de solicitação de dados comum é solicitar dados primeiro e, em seguida, atualizar o objeto de IU. Solicitar dados geralmente é uma operação demorada. Se for operado no thread principal, inevitavelmente fará com que o thread principal bloqueie e causar atolamentos. Então, como usamos threads filhos para resolver o problema?

Antes de discutir, vamos discutir os dois tipos de referências de objeto de UI de threads filho: referências explícitas e referências implícitas.

Mostrar referências

O objetivo final de muitas tarefas em threads não principais é atualizar objetos de IU. No entanto, se um dos threads acessa um objeto na hierarquia de visualização, o aplicativo pode se tornar instável: se o thread de trabalho alterar as propriedades do objeto enquanto qualquer outro thread faz referência ao objeto ao mesmo tempo, o resultado não pode ser determinado.

Por exemplo, suponha que um aplicativo faça referência direta a objetos de IU no thread de trabalho. O objeto no thread de trabalho pode conter uma referência à visão, mas antes que o trabalho seja concluído, a visão é removida da hierarquia de visão. Quando essas duas operações ocorrem ao mesmo tempo, a referência manterá o objeto View na memória e definirá propriedades nele. No entanto, o usuário nunca verá o objeto neste momento e o aplicativo excluirá o objeto depois que a referência do objeto desaparecer.

Para dar outro exemplo, suponha que o objeto View contenha uma referência à Activity à qual pertence. Se a Atividade for destruída, mas ainda houver tarefas de processamento de encadeamento que direta ou indiretamente se referem a ela, o coletor de lixo aguardará até que a tarefa de processamento seja concluída antes de coletar a Atividade.

Se um evento de ciclo de vida de atividade (como rotação de tela) ocorrer durante a execução do trabalho de processamento de encadeamento, essa situação pode causar problemas. O sistema não poderá realizar a coleta de lixo até que o trabalho em andamento seja concluído. Portanto, quando a coleta de lixo está disponível, pode haver dois objetos Activity na memória.

Nesses casos, não devemos incluir referências explícitas a objetos de interface nas tarefas de processamento de thread do aplicativo. Evitar essas referências ajuda a evitar esses tipos de vazamentos de memória, evitando a contenção de processamento de thread.

Em qualquer caso, o aplicativo deve atualizar apenas os objetos de interface no thread principal. Como o thread filho e o thread principal devem trabalhar juntos?

Podemos mudar do thread principal para o sub-thread e usar o sub-thread para lidar com tarefas demoradas (como a operação de solicitação de dados neste exemplo). Quando o processamento da tarefa for concluído, retorne ao thread principal para realizar atualizações da IU. Dessa forma, a tarefa de trabalho é processada no thread filho e a atualização final da visualização da UI é transferida de volta para o thread principal para processamento unificado, evitando vários problemas causados ​​pelo thread filho exibindo o objeto de UI de referência.

Referência implícita

Em classes internas Java (incluindo classes internas anônimas e classes internas nomeadas), seus objetos conterão implicitamente referências a objetos de classe externa, o que causará o problema de que classes externas não podem ser coletadas como lixo.

Dê um exemplo comum no Android:

    public class MainActivity extends Activity {
      // ...
      public class MyAsyncTask extends AsyncTask<Void, Void, String>   {
        @Override protected String doInBackground(Void... params) {...}
        @Override protected void onPostExecute(String result) {...}
      }
    }

No código de amostra, o objeto de processamento de thread MyAsyncTask é declarado como uma classe interna não estática de uma Activity (ou uma classe interna em Kotlin). Essa declaração faz com que a classe interna MyAsyncTask mantenha uma referência implícita à instância Activity. Portanto, antes que o trabalho de processamento de thread seja concluído, o objeto sempre contém uma referência à Activity correspondente, o que causa um atraso na destruição da Activity referenciada e causa um problema de vazamento de memória.

Como resolver isso?

Na verdade, é simples, basta definir a classe interna MyAsyncTask como uma classe estática para remover a referência implícita.

Declare o objeto AsyncTask como uma classe aninhada estática (ou remova o qualificador interno em Kotlin). Fazer isso pode eliminar o problema de referência implícita, porque as classes aninhadas estáticas são diferentes das classes internas: as instâncias das classes internas requerem a instanciação das instâncias das classes externas e podem acessar diretamente métodos e campos da instância encapsulada. Em contraste, uma classe aninhada estática não precisa se referir a uma instância da classe de encapsulamento, portanto, não contém referências a membros da classe externa.

    public class MainActivity extends Activity {
      // ...
      static public class MyAsyncTask extends AsyncTask<Void, Void, String>   {
        @Override protected String doInBackground(Void... params) {...}
        @Override protected void onPostExecute(String result) {...}
      }
    }

Ciclo de vida de atividade de thread e aplicativo

O ciclo de vida do aplicativo afeta como os threads filhos funcionam no APP. Podemos precisar determinar se o thread filho deve ser retido depois que a Activity é destruída, e também devemos prestar atenção à relação entre a prioridade do thread e se a Activity está sendo executada em primeiro ou segundo plano.

Tópico reservado

Quando a interface da IU é destruída, precisamos confirmar se devemos continuar a reter o thread filho para continuar a executar a tarefa original.

A seguir está um exemplo.

Supondo que uma atividade gere um conjunto de threads para processar tarefas de trabalho e, em seguida, seja destruída antes que os threads de trabalho possam executar as tarefas de trabalho, como o aplicativo deve lidar com as tarefas de trabalho que estão sendo executadas?

Se a tarefa de trabalho atualizar a interface que foi destruída, o trabalho não precisa continuar. Por exemplo, se a tarefa for carregar informações do usuário do banco de dados e, em seguida, atualizar a visualização, o encadeamento não será mais necessário.

Em contraste, as tarefas de trabalho lidam com operações de dados que não estão completamente relacionadas à atualização da interface e os dados podem precisar ser usados ​​posteriormente. Nesse caso, devemos manter o segmento. Por exemplo, um pacote de dados pode estar esperando para baixar uma imagem, armazená-la em cache no disco e atualizar o objeto View associado. Embora o objeto não exista mais, ainda pode ser útil fazer o download e armazenar em cache a imagem, caso o usuário retorne à Activity destruída.

Linha prioritária

Ao criar e gerenciar encadeamentos em um aplicativo, certifique-se de definir a prioridade do encadeamento para que ele receba a prioridade correta. Se a prioridade do sub-thread for definida muito alta, isso pode interferir no thread principal e no thread de renderização, fazendo com que o aplicativo perca quadros e cause atolamentos. Se a prioridade do thread filho for definida muito baixa, isso pode fazer com que tarefas assíncronas (como carregamento de imagem) não atinjam a velocidade necessária.

O escalonador de threads do sistema dará prioridade a threads de prioridade mais alta e fará uma troca entre essas prioridades e a necessidade de, eventualmente, concluir todo o trabalho. De modo geral, o grupo de primeiro plano é responsável por cerca de 95% do tempo total de execução do dispositivo, enquanto o grupo de segundo plano é responsável por cerca de 5%.

Cada vez que um thread é criado, setThreadPriority () deve ser chamado para definir a prioridade do thread.

Para definir a prioridade do thread, consulte "A maneira correta de definir a prioridade do thread no Android (2 métodos)" .

Além disso, geralmente precisamos definir a prioridade de threads no pool de threads, como fazer isso? Consulte: "Implementação de código Android de prioridade de thread no pool de threads" .

O sistema também usa a classe Process para atribuir a cada thread seu próprio valor de prioridade.

Por padrão, o sistema define a prioridade de um thread para ter a mesma prioridade e associação de grupo do thread que o gerou, ou seja, por padrão, a prioridade do thread filho herda a prioridade do thread pai que o criou. No entanto, devemos usar setThreadPriority () para ajustar explicitamente a prioridade do thread.

A classe Process fornece um conjunto de constantes para a prioridade do thread para ajudar a simplificar a atribuição de valores de prioridade. Por exemplo, THREAD_PRIORITY_DEFAULT representa o valor padrão do encadeamento. Se o trabalho executado pelo thread não for muito urgente, o aplicativo deve definir a prioridade do thread para THREAD_PRIORITY_BACKGROUND.

Também podemos usar as constantes THREAD_PRIORITY_LESS_FAVORABLE e THREAD_PRIORITY_MORE_FAVORABLE como incrementadores para definir a prioridade relativa.

Como criar e usar threads assíncronos

O Android fornece as mesmas classes e primitivas Java para facilitar o processamento de threads, como as classes Thread, Runnable e Executors. E, para ajudar a facilitar e desenvolver o processamento de thread para Android, o Android fornece um conjunto de programas auxiliares que podem auxiliar no desenvolvimento, como AsyncTaskLoader e AsyncTask. Cada classe auxiliar tem um conjunto específico de nuances de desempenho, dedicado a resolver um pequeno número de problemas específicos de processamento de thread. Usar a classe errada na situação errada pode causar problemas de desempenho.

Para o método de uso de threads no Android, você pode consultar o artigo anterior: "Explicação detalhada dos seis métodos de implementação de tarefas assíncronas do Android" .

Quantos tópicos devemos criar?

Conforme mencionado acima, o uso de sub-threads pode evitar que o thread principal pare devido a tarefas demoradas. Então, é melhor criar mais threads?

Embora em um nível técnico, possamos criar centenas de threads no código, mas fazer isso pode causar problemas de desempenho. Nosso aplicativo compartilha recursos limitados de CPU com serviços de segundo plano, programas de renderização, motores de áudio, redes, etc. A CPU pode realmente processar apenas um pequeno número de threads em paralelo (isso está relacionado ao número de núcleos do dispositivo); uma vez que o limite seja excedido, ela encontrará problemas de prioridade e agendamento. Portanto, é importante criar o número certo de threads com base nos requisitos de carga de trabalho. Além disso, a prioridade dos encadeamentos filhos deve ser controlada ao mesmo tempo.

O thread filho e o thread principal competem pelos recursos da CPU:

[Falha na transferência da imagem do link externo. O site de origem pode ter um mecanismo de link anti-leech. Recomenda-se salvar a imagem e carregá-la diretamente (img-Tl4v5SpC-1608193450595) (evernotecid: // 6FE75482-54A0-433A-9625- A01F7FEE92EC / appyinxiangcom / 9896050 / ENResource / p2888)]

Na verdade, quantos threads criar depende de muitas variáveis, mas você pode escolher um valor (por exemplo, escolha 4 primeiro) e usar o Systrace para teste.Esta estratégia é tão confiável quanto qualquer outra estratégia. Podemos usar tentativa e erro para encontrar pelo menos quantos threads devem ser reduzidos para evitar problemas.

Ao decidir quantos threads criar, você também precisa considerar que os threads não estão livres, eles ocuparão memória. Os threads no Android são finalmente criados através da camada nativa. FixStackSize é usado para definir o tamanho da pilha de threads. Por padrão, o tamanho total da memória exigido pela pilha de threads = 1M + 8k + 8k, que é 1040k. Os inúmeros aplicativos instalados no dispositivo irão somar esse número rapidamente, especialmente quando a pilha de chamadas é significativamente expandida.

Na verdade, em circunstâncias normais, devemos reutilizar threads para otimização de recursos Podemos reutilizar o pool de threads existente, o que pode reduzir a memória e a contenção de recursos de processamento, ajudando assim a melhorar o desempenho.


** PS: Para conteúdo mais interessante, verifique -> "Desenvolvimento Android"
** PS: Para conteúdo mais interessante, verifique -> "Desenvolvimento Android"
** PS: Para conteúdo mais emocionante, verifique -> "Desenvolvimento Android"

Acho que você gosta

Origin blog.csdn.net/u011578734/article/details/111318450
Recomendado
Clasificación