O AsyncTask é realmente conceitualmente defeituoso ou estou perdendo alguma coisa?

264

Eu tenho investigado esse problema há meses, encontrei soluções diferentes para ele, das quais não estou feliz, pois todos são hacks maciços. Ainda não consigo acreditar que uma classe que falhou no design entrou na estrutura e ninguém está falando sobre isso, então acho que devo estar perdendo alguma coisa.

O problema está com AsyncTask. De acordo com a documentação

"permite executar operações em segundo plano e publicar resultados no thread da interface do usuário sem ter que manipular threads e / ou manipuladores."

O exemplo continua a mostrar como showDialog()é chamado um método exemplar onPostExecute(). Isso, no entanto, parece inteiramente artificial para mim, porque mostrar uma caixa de diálogo sempre precisa de uma referência a um válido Context, e um AsyncTask nunca deve ter uma referência forte a um objeto de contexto .

O motivo é óbvio: e se a atividade for destruída, o que desencadeou a tarefa? Isso pode acontecer o tempo todo, por exemplo, porque você virou a tela. Se a tarefa contiver uma referência ao contexto que a criou, você não estará apenas segurando um objeto de contexto inútil (a janela será destruída e qualquer interação da interface do usuário falhará com uma exceção!), Você corre o risco de criar um vazamento de memória.

A menos que minha lógica seja falha aqui, isso se traduz em: onPostExecute()é totalmente inútil, porque de que serve esse método para executar no thread da interface do usuário se você não tiver acesso a nenhum contexto? Você não pode fazer nada significativo aqui.

Uma solução alternativa seria não passar instâncias de contexto para um AsyncTask, mas uma Handlerinstância. Isso funciona: como um manipulador vincula livremente o contexto e a tarefa, você pode trocar mensagens entre eles sem arriscar um vazamento (certo?). Mas isso significaria que a premissa do AsyncTask, a saber, que você não precisa se preocupar com os manipuladores, está errada. Também parece abusar do Handler, pois você está enviando e recebendo mensagens no mesmo thread (você o cria no thread da interface do usuário e o envia em onPostExecute (), que também é executado no thread da interface do usuário).

Além disso, mesmo com essa solução alternativa, você ainda tem o problema de que, quando o contexto é destruído, você não tem registro das tarefas que ele disparou. Isso significa que você deve reiniciar qualquer tarefa ao recriar o contexto, por exemplo, após uma alteração na orientação da tela. Isso é lento e desperdício.

Minha solução para isso (conforme implementada na biblioteca Droid-Fu ) é manter um mapeamento de WeakReferences dos nomes dos componentes para suas instâncias atuais no objeto de aplicativo exclusivo. Sempre que um AsyncTask é iniciado, ele registra o contexto de chamada nesse mapa e, em cada retorno de chamada, ele busca a instância de contexto atual desse mapeamento. Isso garante que você nunca faça referência a uma instância de contexto obsoleta e sempre tenha acesso a um contexto válido nos retornos de chamada, para que você possa fazer um trabalho significativo na interface do usuário. Também não vaza, porque as referências são fracas e são limpas quando não existe mais instância de um determinado componente.

Ainda assim, é uma solução alternativa complexa e requer a subclasse de algumas das classes da biblioteca Droid-Fu, tornando essa uma abordagem bastante intrusiva.

Agora, eu simplesmente quero saber: Estou apenas perdendo alguma coisa ou o AsyncTask é realmente totalmente defeituoso? Como suas experiências estão trabalhando com isso? Como você resolveu esse problema?

Obrigado pela sua contribuição.

Matthias
fonte
1
Se você estiver curioso, recentemente adicionamos uma classe à biblioteca principal de ignição chamada IgnitedAsyncTask, que adiciona suporte ao acesso ao contexto com segurança de tipo em todos os retornos de chamada usando o padrão de conexão / desconexão descrito por Dianne abaixo. Ele também permite lançar exceções e manipulá-las em um retorno de chamada separado. Veja github.com/kaeppler/ignition-core/blob/master/src/com/github/…
Matthias
dê uma olhada nisto: gist.github.com/1393552
Matthias
1
Esta questão também está relacionada.
Alex Lockwood
Eu adiciono as tarefas assíncronas a uma lista de matrizes e certifique-se de fechá-las em um determinado ponto.
NightSkyCode 15/10

Respostas:

86

Que tal algo como isso:

class MyActivity extends Activity {
    Worker mWorker;

    static class Worker extends AsyncTask<URL, Integer, Long> {
        MyActivity mActivity;

        Worker(MyActivity activity) {
            mActivity = activity;
        }

        @Override
        protected Long doInBackground(URL... urls) {
            int count = urls.length;
            long totalSize = 0;
            for (int i = 0; i < count; i++) {
                totalSize += Downloader.downloadFile(urls[i]);
                publishProgress((int) ((i / (float) count) * 100));
            }
            return totalSize;
        }

        @Override
        protected void onProgressUpdate(Integer... progress) {
            if (mActivity != null) {
                mActivity.setProgressPercent(progress[0]);
            }
        }

        @Override
        protected void onPostExecute(Long result) {
            if (mActivity != null) {
                mActivity.showDialog("Downloaded " + result + " bytes");
            }
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mWorker = (Worker)getLastNonConfigurationInstance();
        if (mWorker != null) {
            mWorker.mActivity = this;
        }

        ...
    }

    @Override
    public Object onRetainNonConfigurationInstance() {
        return mWorker;
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (mWorker != null) {
            mWorker.mActivity = null;
        }
    }

    void startWork() {
        mWorker = new Worker(this);
        mWorker.execute(...);
    }
}
hackbod
fonte
5
Sim, mActivity será! = Null, mas se não houver referências à sua instância Worker, as referências a essa instância também estarão sujeitas à remoção de lixo. Se sua tarefa for executada para sempre, haverá uma perda de memória (sua tarefa) - sem mencionar que você está gastando a bateria do telefone. Além disso, como mencionado em outro lugar, você pode definir mActivity como null no onDestroy.
EboMike
13
O método onDestroy () define mActivity como nulo. Não importa quem possui uma referência à atividade antes disso, porque ela ainda está em execução. E a janela da atividade sempre será válida até que onDestroy () seja chamado. Ao definir como nulo lá, a tarefa assíncrona saberá que a atividade não é mais válida. (E quando uma configuração é alterada, o onDestroy () da atividade anterior é chamado e o onCreate () do próximo é executado sem nenhuma mensagem no loop principal processada entre eles para que o AsyncTask nunca veja um estado inconsistente.)
hackbod
8
verdade, mas ainda não resolve o último problema que mencionei: imagine que a tarefa baixe algo da Internet. Usando essa abordagem, se você virar a tela três vezes enquanto a tarefa estiver em execução, ela será reiniciada a cada rotação da tela e todas as tarefas, exceto a última, jogam fora o resultado porque a referência de atividade é nula.
Matthias
11
Para acessar em segundo plano, você precisa colocar a sincronização apropriada em torno do mActivity e lidar com a execução em momentos em que é nulo, ou fazer com que o thread de segundo plano use o Context.getApplicationContext (), que é uma instância global única para o aplicativo. O contexto do aplicativo é restrito no que você pode fazer (nenhuma interface do usuário, como o Dialog, por exemplo) e requer algum cuidado (os receptores registrados e as ligações de serviço serão deixados para sempre se você não os limpar), mas geralmente é apropriado para o código que não é está vinculado ao contexto de um componente específico.
hackbod
4
Isso foi incrivelmente útil, obrigado Dianne! Eu gostaria que a documentação fosse tão boa em primeiro lugar.
Matthias
20

O motivo é óbvio: e se a atividade for destruída, o que desencadeou a tarefa?

Desassociar manualmente a atividade do AsyncTaskin onDestroy(). Associe manualmente novamente a nova atividade ao AsyncTaskin onCreate(). Isso requer uma classe interna estática ou uma classe Java padrão, além de talvez 10 linhas de código.

CommonsWare
fonte
Cuidado com as referências estáticas - eu vi objetos sendo coletados como lixo, mesmo que houvesse referências estáticas fortes para eles. Talvez seja um efeito colateral do carregador de classes do Android, ou até mesmo um bug, mas as referências estáticas não são uma maneira segura de trocar estado ao longo do ciclo de vida de uma atividade. O objeto do aplicativo é, no entanto, é por isso que eu uso isso.
Matthias
10
@ Matthias: Eu não disse para usar referências estáticas. Eu disse para usar uma classe interna estática. Há uma diferença substancial, apesar de ambos terem "estática" em seus nomes.
CommonsWare
5
Entendo - a chave aqui é getLastNonConfigurationInstance (), mas não a classe interna estática. Uma classe interna estática não mantém referência implícita à sua classe externa, portanto é semanticamente equivalente a uma classe pública simples. Apenas um aviso: não é garantido que onRetainNonConfigurationInstance () seja chamado quando uma atividade for interrompida (uma interrupção também pode ser uma ligação telefônica), para que você também precise parcelar sua tarefa em onSaveInstanceState (), para obter uma experiência verdadeiramente sólida. solução. Mas ainda assim, boa ideia.
Matthias
7
Um ... onRetainNonConfigurationInstance () é sempre chamado quando a atividade está sendo destruída e recriada. Não faz sentido ligar em outros momentos. Se uma mudança para outra atividade ocorrer, a atividade atual será pausada / parada, mas não será destruída, portanto a tarefa assíncrona poderá continuar em execução e usando a mesma instância de atividade. Se terminar e digitar exibir uma caixa de diálogo, a caixa de diálogo será exibida corretamente como parte dessa atividade e, portanto, não será exibida ao usuário até que ele retorne à atividade. Você não pode colocar o AsyncTask em um pacote.
hackbod
15

Parece que AsyncTaské um pouco mais do que apenas conceitualmente falho . Também é inutilizável por problemas de compatibilidade. Os documentos do Android liam:

Quando introduzidas pela primeira vez, as AsyncTasks foram executadas em série em um único encadeamento em segundo plano. Começando com DONUT, isso foi alterado para um pool de threads, permitindo que várias tarefas operassem em paralelo. Iniciando o HONEYCOMB, as tarefas voltam a ser executadas em um único encadeamento para evitar erros comuns de aplicativo causados ​​pela execução paralela. Se você realmente deseja execução paralela, pode usar a executeOnExecutor(Executor, Params...) versão deste método com THREAD_POOL_EXECUTOR ; no entanto, consulte os comentários para avisos sobre seu uso.

Ambos executeOnExecutor()e THREAD_POOL_EXECUTORsão adicionados no nível de API 11 (3.0.x Android, Honeycomb).

Isso significa que, se você criar dois AsyncTasks para baixar dois arquivos, o segundo download não será iniciado até que o primeiro termine. Se você conversar através de dois servidores, e o primeiro servidor estiver inativo, você não se conectará ao segundo antes que a conexão com o primeiro expire. (A menos que você use os novos recursos da API11, é claro, mas isso tornará seu código incompatível com 2.x).

E se você deseja segmentar tanto 2.x como 3.0+, as coisas se tornam realmente complicadas.

Além disso, os documentos dizem:

Cuidado: Outro problema que você pode encontrar ao usar um encadeamento de trabalho é reiniciar inesperadamente em sua atividade devido a uma alteração na configuração do tempo de execução (como quando o usuário altera a orientação da tela), que pode destruir o encadeamento de trabalho . Para ver como você pode manter sua tarefa durante uma dessas reinicializações e como cancelar corretamente a tarefa quando a atividade é destruída, consulte o código-fonte do aplicativo de exemplo Shelves.

18446744073709551615
fonte
12

Provavelmente todos nós, incluindo o Google, estamos AsyncTaskusando mal do ponto de vista do MVC .

Uma Atividade é um Controlador , e o controlador não deve iniciar operações que possam sobreviver à Visualização . Ou seja, AsyncTasks deve ser usado no Model , de uma classe que não está vinculada ao ciclo de vida da atividade - lembre-se de que as atividades são destruídas na rotação. (No que diz respeito ao View , você normalmente não programa classes derivadas de, por exemplo, android.widget.Button, mas pode. Geralmente, a única coisa que você faz sobre o View é o xml.)

Em outras palavras, é errado colocar derivados do AsyncTask nos métodos de Atividades. OTOH, se não devemos usar AsyncTasks em Atividades, o AsyncTask perde sua atratividade: costumava ser anunciado como uma solução rápida e fácil.

18446744073709551615
fonte
5

Não sei se é verdade que você corre o risco de um vazamento de memória com uma referência a um contexto de uma AsyncTask.

A maneira usual de implementá-las é criar uma nova instância do AsyncTask no escopo de um dos métodos da Activity. Portanto, se a atividade for destruída, depois que o AsyncTask for concluído, ele não poderá ser alcançado e qualificado para a coleta de lixo? Portanto, a referência à atividade não importa, porque o AsyncTask em si não fica por aqui.

oli
fonte
2
true - mas e se a tarefa bloquear indefinidamente? As tarefas destinam-se a executar operações de bloqueio, talvez até as que nunca terminam. Lá você tem seu vazamento de memória.
Matthias
1
Qualquer trabalhador que executa algo em um loop sem fim ou qualquer coisa que trava, por exemplo, em uma operação de E / S.
Matthias
2

Seria mais robusto manter um WeekReference em sua atividade:

public class WeakReferenceAsyncTaskTestActivity extends Activity {
    private static final int MAX_COUNT = 100;

    private ProgressBar progressBar;

    private AsyncTaskCounter mWorker;

    @SuppressWarnings("deprecation")
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_async_task_test);

        mWorker = (AsyncTaskCounter) getLastNonConfigurationInstance();
        if (mWorker != null) {
            mWorker.mActivity = new WeakReference<WeakReferenceAsyncTaskTestActivity>(this);
        }

        progressBar = (ProgressBar) findViewById(R.id.progressBar1);
        progressBar.setMax(MAX_COUNT);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.activity_async_task_test, menu);
        return true;
    }

    public void onStartButtonClick(View v) {
        startWork();
    }

    @Override
    public Object onRetainNonConfigurationInstance() {
        return mWorker;
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (mWorker != null) {
            mWorker.mActivity = null;
        }
    }

    void startWork() {
        mWorker = new AsyncTaskCounter(this);
        mWorker.execute();
    }

    static class AsyncTaskCounter extends AsyncTask<Void, Integer, Void> {
        WeakReference<WeakReferenceAsyncTaskTestActivity> mActivity;

        AsyncTaskCounter(WeakReferenceAsyncTaskTestActivity activity) {
            mActivity = new WeakReference<WeakReferenceAsyncTaskTestActivity>(activity);
        }

        private static final int SLEEP_TIME = 200;

        @Override
        protected Void doInBackground(Void... params) {
            for (int i = 0; i < MAX_COUNT; i++) {
                try {
                    Thread.sleep(SLEEP_TIME);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                Log.d(getClass().getSimpleName(), "Progress value is " + i);
                Log.d(getClass().getSimpleName(), "getActivity is " + mActivity);
                Log.d(getClass().getSimpleName(), "this is " + this);

                publishProgress(i);
            }
            return null;
        }

        @Override
        protected void onProgressUpdate(Integer... values) {
            super.onProgressUpdate(values);
            if (mActivity != null) {
                mActivity.get().progressBar.setProgress(values[0]);
            }
        }
    }

}
Snicolas
fonte
Isso é semelhante ao que fizemos primeiro com o Droid-Fu. Manteríamos um mapa de referências fracas a objetos de contexto e faríamos uma pesquisa nos retornos de chamada da tarefa para obter a referência mais recente (se disponível) para a qual executar o retorno de chamada. Nossa abordagem, no entanto, significava que havia uma única entidade que mantinha esse mapeamento, enquanto sua abordagem não, portanto, isso é realmente melhor.
Matthias
1
Você deu uma olhada no RoboSpice? github.com/octo-online/robospice . Eu acredito que este sistema é ainda melhor.
Snicolas 21/09/12
O código de exemplo na primeira página parece estar vazando uma referência de contexto (uma classe interna mantém uma referência implícita à classe externa.) Não está convencido!
Matthias
@ Matthias, você está certo, é por isso que proponho uma classe interna estática que realizará uma WeakReference na atividade.
Snicolas 24/09/12
1
@ Matthias, acredito que isso começa a estar fora de tópico. Mas os carregadores não fornecem armazenamento em cache imediato, pois, além disso, os carregadores tendem a ser mais detalhados que a nossa lib. Na verdade, eles lidam com cursores muito bem, mas para redes, uma abordagem diferente, baseada em cache e em um serviço, é mais adequada. Veja neilgoodman.net/2011/12/26/… parte 1 e 2
Snicolas
1

Por que não substituir o onPause()método na Atividade proprietária e cancelar a AsyncTaskpartir daí?

Jeff Axelrod
fonte
depende do que essa tarefa está fazendo. se apenas carregar / ler alguns dados, tudo bem. mas se alterar o estado de alguns dados em um servidor remoto, preferimos dar à tarefa a capacidade de executar até o fim.
Vit Khudenko 9/08/2012
@Arhimed e aceito se você segurar o thread da interface do usuário, onPausepois é tão ruim quanto segurá-lo em outro lugar? Ou seja, você pode obter um ANR?
precisa
exatamente. não podemos bloquear o thread da interface do usuário (seja ele onPauseou outra coisa) porque corremos o risco de obter um ANR.
Vit Khudenko 9/08/2012
1

Você está absolutamente certo - é por isso que um movimento para não usar tarefas / carregadores assíncronos nas atividades para buscar dados está ganhando força. Uma das novas maneiras é usar uma estrutura Volley que essencialmente fornece um retorno de chamada assim que os dados estiverem prontos - muito mais consistente com o modelo MVC. O Volley foi preenchido no Google I / O 2013. Não sei por que mais pessoas não estão cientes disso.

C0D3LIC1OU5
fonte
obrigado por isso ... vou dar uma olhada nele ... minha razão de não gostar do AsyncTask é porque ele me deixa preso a um conjunto de instruções no PostExecute ... a menos que eu o corte como o uso de interfaces ou substituindo-o sempre Eu preciso disso.
Carinlynchin
0

Pessoalmente, apenas estendo o Thread e uso uma interface de retorno de chamada para atualizar a interface do usuário. Eu nunca poderia fazer o AsyncTask funcionar corretamente sem problemas de FC. Eu também uso uma fila sem bloqueio para gerenciar o pool de execução.

androidworkz
fonte
1
Bem, sua força de fechamento provavelmente foi por causa do problema que mencionei: você tentou fazer referência a um contexto que estava fora do escopo (ou seja, sua janela havia sido destruída), o que resultará em uma exceção de estrutura.
Matthias
Não ... na verdade, foi porque a fila é uma merda incorporada ao AsyncTask. Eu sempre uso getApplicationContext (). Não tenho problemas com o AsyncTask se forem apenas algumas operações ... mas estou escrevendo um media player que atualiza a arte do álbum em segundo plano ... no meu teste, tenho 120 álbuns sem arte ... então, enquanto meu aplicativo não foi fechado completamente, o asynctask estava lançando erros ... então, em vez disso, criei uma classe singleton com uma fila que gerencia os processos e, até agora, funciona muito bem.
androidworkz
0

Eu pensei que cancelar funciona, mas não funciona.

aqui eles RTFMing sobre isso:

"" Se a tarefa já foi iniciada, o parâmetro mayInterruptIfRunning determina se o encadeamento que está executando esta tarefa deve ser interrompido na tentativa de interromper a tarefa. "

Isso não implica, no entanto, que o encadeamento seja interrompível. Isso é coisa do Java, não do AsyncTask ".

http://groups.google.com/group/android-developers/browse_thread/thread/dcadb1bc7705f1bb/add136eb4949359d?show_docid=add136eb4949359d

nir
fonte
0

É melhor pensar em AsyncTask como algo que está mais fortemente associado a uma Atividade, Contexto, ContextWrapper, etc. É mais conveniente quando seu escopo é totalmente compreendido.

Verifique se você possui uma política de cancelamento em seu ciclo de vida, para que ela seja coletada como lixo e não mantenha mais uma referência à sua atividade, e também pode ser coletada.

Sem cancelar o seu AsyncTask enquanto você se afasta do seu Contexto, ocorrerá vazamentos de memória e NullPointerExceptions, se você simplesmente fornecer feedback como um Toast em uma caixa de diálogo simples, um singleton do seu Contexto do Aplicativo ajudará a evitar o problema do NPE.

O AsyncTask não é tão ruim, mas definitivamente há muita mágica acontecendo que pode levar a armadilhas imprevistas.

jtuchek
fonte
-1

Quanto à "experiências de trabalho com ele": é possível para matar o processo , juntamente com todos os AsyncTasks, Android irá recriar a pilha de atividade de forma que o usuário não vai falar nada.

18446744073709551615
fonte