AsyncTask è concettualmente difettoso o mi sto perdendo qualcosa?


264

Ho studiato questo problema da mesi ormai, ho trovato diverse soluzioni ad esso, di cui non sono contento poiché sono tutti hack enormi. Non riesco ancora a credere che una classe imperfetta nel design sia entrata nel framework e nessuno ne parli, quindi immagino che mi debba mancare qualcosa.

Il problema è con AsyncTask. Secondo la documentazione esso

"consente di eseguire operazioni in background e pubblicare risultati sul thread dell'interfaccia utente senza dover manipolare thread e / o gestori."

L'esempio continua quindi a mostrare come showDialog()viene chiamato un metodo esemplare onPostExecute(). Questo, tuttavia, mi sembra del tutto inventato , perché mostrare una finestra di dialogo ha sempre bisogno di un riferimento a un valido Contexte un AsyncTask non deve mai contenere un forte riferimento a un oggetto di contesto .

Il motivo è ovvio: cosa succede se l'attività viene distrutta e ciò ha innescato l'attività? Questo può succedere sempre, ad esempio perché hai capovolto lo schermo. Se l'attività conterrà un riferimento al contesto che lo ha creato, non stai solo trattenendo un oggetto di contesto inutile (la finestra sarà stata distrutta e qualsiasi interazione dell'interfaccia utente non riuscirà con un'eccezione!), Rischi anche di creare un perdita di memoria.

A meno che la mia logica non sia difettosa qui, questo si traduce in: onPostExecute()è del tutto inutile, perché a che serve questo metodo eseguire sul thread dell'interfaccia utente se non si ha accesso a nessun contesto? Non puoi fare nulla di significativo qui.

Una soluzione alternativa sarebbe quella di non passare istanze di contesto a un AsyncTask, ma a Handlerun'istanza. Funziona così: dal momento che un gestore si lega vagamente al contesto e all'attività, è possibile scambiare messaggi tra di loro senza rischiare una perdita (giusto?). Ciò significherebbe che la premessa di AsyncTask, vale a dire che non è necessario preoccuparsi dei gestori, è sbagliata. Sembra anche abusare di Handler, poiché stai inviando e ricevendo messaggi sullo stesso thread (lo crei sul thread dell'interfaccia utente e lo invii in onPostExecute () che viene eseguito anche sul thread dell'interfaccia utente).

Per finire, anche con quella soluzione alternativa, hai ancora il problema che quando il contesto viene distrutto, non hai alcuna registrazione delle attività che ha generato. Ciò significa che è necessario riavviare qualsiasi attività quando si ricrea il contesto, ad esempio dopo una modifica dell'orientamento dello schermo. Questo è lento e dispendioso.

La mia soluzione a questo (come implementato nella libreria Droid-Fu ) è quella di mantenere una mappatura di WeakReferences dai nomi dei componenti alle loro istanze correnti sull'oggetto applicazione univoco. Ogni volta che viene avviato un AsyncTask, registra il contesto di chiamata in quella mappa e su ogni richiamata, recupera l'istanza di contesto corrente da quella mappatura. Ciò garantisce che non si farà mai riferimento a un'istanza di contesto obsoleta e che si abbia sempre accesso a un contesto valido nei callback in modo da poter svolgere un lavoro di interfaccia utente significativo lì. Inoltre non perde, perché i riferimenti sono deboli e vengono cancellati quando non esiste più alcuna istanza di un determinato componente.

Tuttavia, è una soluzione complessa e richiede di sottoclassare alcune delle classi di librerie Droid-Fu, rendendolo un approccio piuttosto invadente.

Ora voglio semplicemente sapere: mi sto perdendo qualcosa in modo massiccio o AsyncTask è davvero completamente imperfetto? In che modo le tue esperienze ci lavorano? Come hai risolto questi problemi?

Grazie per il tuo contributo.


1
Se sei curioso, abbiamo recentemente aggiunto una classe alla libreria del core di accensione denominata IgnitedAsyncTask, che aggiunge il supporto per l'accesso contestuale di tipo sicuro in tutti i callback utilizzando il modello di connessione / disconnessione descritto da Dianne di seguito. Consente inoltre di generare eccezioni e gestirle in un callback separato. Vedi github.com/kaeppler/ignition-core/blob/master/src/com/github/…
Matthias,

dai un'occhiata a questo: gist.github.com/1393552
Matthias,

1
Anche questa domanda è collegata.
Alex Lockwood,

Aggiungo le attività asincrone a un arraylist e mi assicuro di chiuderle tutte ad un certo punto.
NightSkyCode

Risposte:


86

Che ne dici di qualcosa del genere:

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

5
Sì, mActivity sarà! = Null, ma se non ci sono riferimenti all'istanza di Worker, anche i riferimenti a quell'istanza saranno soggetti anche alla rimozione dell'immondizia. Se l'attività viene eseguita per sempre, allora hai comunque una perdita di memoria (attività), per non parlare del fatto che stai scaricando la batteria del telefono. Inoltre, come menzionato altrove, è possibile impostare mActivity su null in onDestroy.
EboMike,

13
Il metodo onDestroy () imposta mActivity su null. Non importa chi detiene un riferimento all'attività prima, perché è ancora in esecuzione. E la finestra dell'attività sarà sempre valida fino alla chiamata di onDestroy (). Impostando su null lì, l'attività asincrona saprà che l'attività non è più valida. (E quando cambia una configurazione, viene chiamata onDestroy () dell'attività precedente e la successiva onCreate () viene eseguita senza alcun messaggio sul ciclo principale elaborato tra loro in modo che AsyncTask non vedrà mai uno stato incoerente.)
hackbod

8
vero, ma non risolve ancora l'ultimo problema che ho citato: immagina che l'attività scarichi qualcosa da Internet. Utilizzando questo approccio, se capovolgi lo schermo 3 volte mentre l'attività è in esecuzione, verrà riavviato ad ogni rotazione dello schermo e ogni attività, tranne l'ultima, getta via il risultato perché il riferimento all'attività è nullo.
Matthias,

11
Per accedere in background, dovresti o mettere la sincronizzazione appropriata intorno a mActivity e gestire l'esecuzione nei momenti in cui è null, oppure avere il thread in background semplicemente prendere il Context.getApplicationContext () che è una singola istanza globale per l'app. Il contesto dell'applicazione è limitato in ciò che puoi fare (nessuna interfaccia utente come Dialog per esempio) e richiede un po 'di attenzione (i destinatari registrati e i collegamenti di servizio rimarranno per sempre se non li ripulisci), ma è generalmente appropriato per il codice che non è è legato al contesto di un particolare componente.
hackbod,

4
È stato incredibilmente utile, grazie Dianne! Vorrei che la documentazione fosse altrettanto buona in primo luogo.
Matthias,

20

Il motivo è ovvio: cosa succede se l'attività viene distrutta e ciò ha innescato l'attività?

Dissociare manualmente l'attività dal AsyncTaskin onDestroy(). Riassociare manualmente la nuova attività a AsyncTaskin onCreate(). Ciò richiede una classe interna statica o una classe Java standard, più forse 10 righe di codice.


Fai attenzione ai riferimenti statici: ho visto gli oggetti che venivano raccolti, anche se c'erano forti riferimenti statici. Forse un effetto collaterale del caricatore di classi di Android, o addirittura un bug, ma i riferimenti statici non sono un modo sicuro di scambiare stato attraverso un ciclo di vita delle attività. L'oggetto app è, tuttavia, è per questo che lo uso.
Matthias,

10
@Matthias: non ho detto di usare riferimenti statici. Ho detto di usare una classe interna statica. C'è una differenza sostanziale, nonostante entrambi abbiano "statico" nei loro nomi.
Commons War il


5
Vedo che qui la chiave è getLastNonConfigurationInstance (), non la classe interna statica. Una classe interna statica non mantiene alcun riferimento implicito alla sua classe esterna, quindi è semanticamente equivalente a una semplice classe pubblica. Solo un avvertimento: onRetainNonConfigurationInstance () NON è garantito per essere chiamato quando un'attività viene interrotta (un'interruzione può anche essere una telefonata), quindi dovresti parcellizzare la tua attività in onSaveInstanceState (), anche per un risultato veramente solido soluzione. Ma comunque, bella idea.
Matthias,

7
Um ... onRetainNonConfigurationInstance () viene sempre chiamato quando l'attività sta per essere distrutta e ricreata. Non ha senso chiamare altre volte. Se si verifica il passaggio a un'altra attività, l'attività corrente viene messa in pausa / interrotta, ma non viene distrutta, pertanto l'attività asincrona può continuare a essere eseguita e utilizzando la stessa istanza di attività. Se termina e dice che visualizza una finestra di dialogo, la finestra di dialogo verrà visualizzata correttamente come parte di tale attività e quindi non mostrerà all'utente fino a quando non torneranno all'attività. Non puoi mettere AsyncTask in un pacchetto.
hackbod,

15

Sembra che AsyncTasksia un po ' più di un semplice difetto concettuale . È inoltre inutilizzabile per problemi di compatibilità. I documenti Android leggono:

Alla prima introduzione, gli AsyncTask erano eseguiti in serie su un singolo thread in background. A partire da DONUT, questo è stato modificato in un pool di thread che consente a più attività di operare in parallelo. A partire da HONEYCOMB, le attività tornano ad essere eseguite su un singolo thread per evitare errori di applicazione comuni causati dall'esecuzione parallela. Se si desidera veramente l'esecuzione parallela, è possibile utilizzare la executeOnExecutor(Executor, Params...) versione di questo metodo con THREAD_POOL_EXECUTOR ; tuttavia, vedere i commenti lì per avvertenze sul suo utilizzo.

Entrambi executeOnExecutor()e THREAD_POOL_EXECUTORsono aggiunti nel livello API 11 (Android 3.0.x, HONEYCOMB).

Ciò significa che se si creano due AsyncTasksecondi per scaricare due file, il secondo download non inizierà fino al termine del primo. Se chatti tramite due server e il primo server è inattivo, non ti collegherai al secondo prima del timeout della connessione al primo. (A meno che tu non usi le nuove funzionalità API11, ovviamente, ma questo renderà il tuo codice incompatibile con 2.x).

E se vuoi scegliere come target sia 2.x sia 3.0+, le cose diventano davvero difficili.

Inoltre, i documenti dicono:

Attenzione: un altro problema che potresti riscontrare quando si utilizza un thread di lavoro è il riavvio imprevisto dell'attività a causa di una modifica della configurazione di runtime (ad esempio quando l'utente modifica l'orientamento dello schermo), che potrebbe distruggere il thread di lavoro . Per vedere come è possibile persistere l'attività durante uno di questi riavvii e come annullare correttamente l'attività quando l'attività viene distrutta, vedere il codice sorgente per l'applicazione di esempio Shelves.


12

Probabilmente tutti, incluso Google, utilizziamo in modo improprio AsyncTaskdal punto di vista MVC .

Un'attività è un controller e il controller non deve avviare operazioni che potrebbero sopravvivere alla vista . Cioè, AsyncTasks dovrebbe essere usato dal Modello , da una classe che non è legata al ciclo di vita dell'Attività - ricorda che le Attività vengono distrutte durante la rotazione. (Per quanto riguarda la vista , di solito non programmate classi derivate da es. Android.widget.Button, ma potete farlo. Di solito, l'unica cosa che fate sulla vista è l'xml.)

In altre parole, è errato collocare i derivati ​​AsyncTask nei metodi delle attività. OTOH, se non dobbiamo usare AsyncTasks nelle attività, AsyncTask perde la sua attrattiva: era pubblicizzato come una soluzione rapida e semplice.


5

Non sono sicuro che sia vero che rischi una perdita di memoria con un riferimento a un contesto da un AsyncTask.

Il modo normale di implementarli è creare una nuova istanza di AsyncTask nell'ambito di uno dei metodi dell'attività. Quindi, se l'attività viene distrutta, una volta completato AsyncTask non sarà irraggiungibile e quindi idoneo alla raccolta dei rifiuti? Quindi il riferimento all'attività non avrà importanza perché l'AsyncTask stesso non rimarrà in sospeso.


2
vero - ma cosa succede se l'attività si blocca indefinitamente? Le attività hanno lo scopo di eseguire operazioni di blocco, forse anche quelle che non si interrompono mai. Ecco la tua perdita di memoria.
Matthias,

1
Qualsiasi lavoratore che esegue qualcosa in un ciclo infinito o qualsiasi cosa si blocchi ad esempio in un'operazione di I / O.
Matthias,

2

Sarebbe più efficace mantenere un riferimento settimanale sulla tua attività:

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]);
            }
        }
    }

}

Questo è simile a quello che abbiamo fatto per la prima volta con Droid-Fu. Manterremmo una mappa di riferimenti deboli agli oggetti di contesto e faremmo una ricerca nei callback dell'attività per ottenere il riferimento più recente (se disponibile) per eseguire il callback. Il nostro approccio, tuttavia, significava che esisteva un'unica entità che manteneva questa mappatura, mentre il tuo approccio no, quindi è davvero più bello.
Matthias,

1
Hai dato un'occhiata a RoboSpice? github.com/octo-online/robospice . Credo che questo sistema sia ancora migliore.
Snicolas,

Il codice di esempio in prima pagina sembra perdere un riferimento al contesto (una classe interna mantiene un riferimento implicito alla classe esterna). Non convinto !!
Matthias,

@Matthias, hai ragione, ecco perché propongo una classe interna statica che conterrà un riferimento debole sull'attività.
Snicolas,

1
@Matthias, credo che questo inizi ad essere fuori tema. Ma i caricatori non forniscono la cache fuori dalla scatola come noi, più oltre, i caricatori tendono ad essere più dettagliati della nostra lib. In realtà gestiscono abbastanza bene i cursori ma per il networking, un approccio diverso, basato sulla memorizzazione nella cache e un servizio è più adatto. Vedi neilgoodman.net/2011/12/26/… parte 1 e 2
Snicolas,

1

Perché non sovrascrivere il onPause()metodo nell'Attività proprietaria e annullare il AsyncTaskda lì?


dipende da cosa sta facendo quel compito. se carica / legge solo alcuni dati, allora sarebbe OK. ma se cambia lo stato di alcuni dati su un server remoto, preferiremmo dare all'attività la possibilità di eseguire fino alla fine.
Vit Khudenko,

@Arhimed e lo prendo se tieni il thread dell'interfaccia utente in onPausequanto è male come tenerlo in qualsiasi altro posto? Cioè, potresti ottenere un ANR?
Jeff Axelrod,

Esattamente. non possiamo bloccare il thread dell'interfaccia utente (sia esso un onPauseo altro) perché rischiamo di ottenere un ANR.
Vit Khudenko,

1

Hai assolutamente ragione - ecco perché un movimento lontano dall'uso di compiti / caricatori asincroni nelle attività per recuperare i dati sta guadagnando slancio. Uno dei nuovi modi è utilizzare un framework Volley che essenzialmente fornisce un callback una volta che i dati sono pronti, molto più coerenti con il modello MVC. Volley è stata popolata in Google I / O 2013. Non so perché più persone non ne siano a conoscenza.


grazie per quello ... ho intenzione di esaminarlo ... il motivo per cui non mi piace AsyncTask è perché mi rende bloccato con un set di istruzioni su PostExecute ... a meno che non lo hackeri come usare interfacce o ignorarlo ogni volta Ne ho bisogno.
carinlynchin,

0

Personalmente, estendo Thread e utilizzo un'interfaccia di callback per aggiornare l'interfaccia utente. Non potrei mai far funzionare AsyncTask senza problemi FC. Uso anche una coda non bloccante per gestire il pool di esecuzione.


1
Bene, la tua forza vicina è stata probabilmente a causa del problema che ho citato: hai provato a fare riferimento a un contesto che era andato fuori dal campo di applicazione (cioè che la sua finestra era stata distrutta), il che si tradurrebbe in un'eccezione del framework.
Matthias,

No ... in realtà è stato perché la coda fa schifo integrata in AsyncTask. Uso sempre getApplicationContext (). Non ho problemi con AsyncTask se sono solo alcune operazioni ... ma sto scrivendo un lettore multimediale che aggiorna le copertine degli album in background ... nel mio test ho 120 album senza arte ... quindi, mentre la mia app non si è chiusa completamente, asynctask ha lanciato errori ... quindi ho creato una classe singleton con una coda che gestisce i processi e finora funziona alla grande.
androidworkz,

0

Ho pensato che annullare funzioni ma non lo fa.

qui hanno RTFMing a riguardo:

"" Se l'attività è già stata avviata, il parametro mayInterruptIfRunning determina se il thread che esegue questa attività deve essere interrotto nel tentativo di interrompere l'attività. "

Ciò non implica, tuttavia, che il thread sia interrompibile. Questa è una cosa Java, non una cosa AsyncTask. "

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


0

Faresti meglio a pensare ad AsyncTask come qualcosa che è più strettamente associato a un'attività, un contesto, un contesto, ecc. È più conveniente quando il suo ambito è pienamente compreso.

Assicurati di avere una politica di cancellazione nel tuo ciclo di vita in modo che alla fine venga raccolta spazzatura e non conservi più un riferimento alla tua attività e anche possa essere raccolta spazzatura.

Senza cancellare il tuo AsyncTask mentre ti allontani dal tuo Contesto, ti imbatterai in perdite di memoria e NullPointerExceptions, se hai semplicemente bisogno di fornire un feedback come un Toast in una semplice finestra di dialogo, allora un singleton del tuo Contesto dell'applicazione contribuirebbe ad evitare il problema NPE.

AsyncTask non è affatto male, ma c'è sicuramente molta magia in corso che può portare a insidie ​​impreviste.


-1

Per quanto riguarda "esperienze di lavoro con essa": è possibile per uccidere il processo insieme a tutti AsyncTasks, Android sarà ricreare lo stack di attività in modo che l'utente non parlare di nulla.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.