AsyncTask est-il vraiment défectueux sur le plan conceptuel ou manque-t-il simplement quelque chose?

264

J'ai étudié ce problème depuis des mois maintenant, j'ai trouvé différentes solutions, ce dont je ne suis pas satisfait car ce sont tous des hacks massifs. Je ne peux toujours pas croire qu'une classe qui a des défauts de conception soit entrée dans le cadre et que personne n'en parle, alors je suppose que je dois manquer quelque chose.

Le problème est avec AsyncTask. Selon la documentation, il

"permet d'effectuer des opérations en arrière-plan et de publier les résultats sur le thread d'interface utilisateur sans avoir à manipuler les threads et / ou les gestionnaires."

L'exemple continue ensuite de montrer comment une showDialog()méthode exemplaire est appelée onPostExecute(). Cela, cependant, me semble entièrement artificiel , car afficher une boîte de dialogue a toujours besoin d'une référence à un valide Context, et une AsyncTask ne doit jamais contenir une référence forte à un objet de contexte .

La raison est évidente: que se passe-t-il si l'activité est détruite, ce qui a déclenché la tâche? Cela peut arriver tout le temps, par exemple parce que vous avez renversé l'écran. Si la tâche contient une référence au contexte qui l'a créée, vous ne vous accrochez pas seulement à un objet de contexte inutile (la fenêtre aura été détruite et toute interaction d'interface utilisateur échouera à une exception!), Vous risquez même de créer un fuite de mémoire.

À moins que ma logique ne soit défectueuse ici, cela se traduit par: onPostExecute()est entièrement inutile, car à quoi sert cette méthode de s'exécuter sur le thread d'interface utilisateur si vous n'avez accès à aucun contexte? Vous ne pouvez rien faire de significatif ici.

Une solution de contournement consisterait à ne pas passer d'instances de contexte à une AsyncTask, mais à une Handlerinstance. Cela fonctionne: puisqu'un gestionnaire lie vaguement le contexte et la tâche, vous pouvez échanger des messages entre eux sans risquer une fuite (non?). Mais cela signifierait que la prémisse d'AsyncTask, à savoir que vous n'avez pas besoin de vous soucier des gestionnaires, est erronée. Cela semble également abuser de Handler, car vous envoyez et recevez des messages sur le même thread (vous le créez sur le thread d'interface utilisateur et l'envoyez via onPostExecute () qui est également exécuté sur le thread d'interface utilisateur).

Pour couronner le tout, même avec cette solution de contournement, vous avez toujours le problème que lorsque le contexte est détruit, vous n'avez aucun enregistrement des tâches qu'il a déclenchées. Cela signifie que vous devez redémarrer toutes les tâches lors de la recréation du contexte, par exemple après un changement d'orientation de l'écran. C'est lent et inutile.

Ma solution à cela (telle qu'implémentée dans la bibliothèque Droid-Fu ) est de maintenir un mappage des WeakReferences des noms de composants à leurs instances actuelles sur l'objet d'application unique. Chaque fois qu'un AsyncTask est démarré, il enregistre le contexte appelant dans cette carte, et à chaque rappel, il récupère l'instance de contexte actuelle à partir de cette correspondance. Cela garantit que vous ne référencerez jamais une instance de contexte périmé et que vous aurez toujours accès à un contexte valide dans les rappels afin que vous puissiez y effectuer un travail d'interface utilisateur significatif. Il ne fuit pas non plus, car les références sont faibles et sont effacées lorsqu'aucune instance d'un composant donné n'existe plus.

Pourtant, c'est une solution de contournement complexe et nécessite de sous-classer certaines des classes de la bibliothèque Droid-Fu, ce qui en fait une approche assez intrusive.

Maintenant, je veux simplement savoir: suis-je simplement en train de manquer quelque chose ou AsyncTask est-il vraiment entièrement défectueux? Comment vos expériences fonctionnent-elles avec lui? Comment avez-vous résolu ce problème?

Merci pour votre contribution.

Matthias
la source
1
Si vous êtes curieux, nous avons récemment ajouté une classe à la bibliothèque de base d'allumage appelée IgnitedAsyncTask, qui ajoute la prise en charge de l'accès au contexte de type sécurisé dans tous les rappels en utilisant le modèle de connexion / déconnexion décrit par Dianne ci-dessous. Il permet également de lever des exceptions et de les gérer dans un rappel séparé. Voir github.com/kaeppler/ignition-core/blob/master/src/com/github/…
Matthias
jetez un oeil à ceci: gist.github.com/1393552
Matthias
1
Cette question est également liée.
Alex Lockwood
J'ajoute les tâches asynchrones à une liste d'array et je m'assure de les fermer toutes à un certain point.
NightSkyCode

Réponses:

86

Que diriez-vous quelque chose comme ça:

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
la source
5
Oui, mActivity sera! = Null, mais s'il n'y a aucune référence à votre instance Worker, toutes les références à cette instance seront également sujettes à l'élimination des ordures. Si votre tâche s'exécute indéfiniment, vous avez quand même une fuite de mémoire (votre tâche) - sans oublier que vous videz la batterie du téléphone. En outre, comme mentionné ailleurs, vous pouvez définir mActivity sur null dans onDestroy.
EboMike
13
La méthode onDestroy () définit mActivity sur null. Peu importe qui détient une référence à l'activité avant cela, car elle est toujours en cours d'exécution. Et la fenêtre de l'activité sera toujours valide jusqu'à ce que onDestroy () soit appelé. En définissant cette valeur sur null, la tâche asynchrone saura que l'activité n'est plus valide. (Et quand une configuration change, onDestroy () de l'activité précédente est appelée et onCreate () de la suivante s'exécute sans aucun message sur la boucle principale traitée entre eux, de sorte que la tâche AsyncTask ne verra jamais un état incohérent.)
hackbod
8
vrai, mais cela ne résout toujours pas le dernier problème que j'ai mentionné: imaginez que la tâche télécharge quelque chose sur Internet. En utilisant cette approche, si vous retournez l'écran 3 fois pendant que la tâche est en cours d'exécution, elle sera redémarrée à chaque rotation d'écran et chaque tâche, à l'exception de la dernière, jette son résultat car sa référence d'activité est nulle.
Matthias
11
Pour accéder en arrière-plan, vous devez soit mettre une synchronisation appropriée autour de mActivity et gérer le passage dans les moments où il est nul, soit demander au thread d'arrière-plan de prendre simplement Context.getApplicationContext () qui est une seule instance globale pour l'application. Le contexte de l'application est limité dans ce que vous pouvez faire (pas d'interface utilisateur comme Dialog par exemple) et nécessite une certaine attention (les récepteurs enregistrés et les liaisons de service seront laissés pour toujours si vous ne les nettoyez pas), mais il est généralement approprié pour le code qui n'est pas 'pas lié au contexte d'un composant particulier.
hackbod
4
C'était incroyablement utile, merci Dianne! Je souhaite que la documentation soit aussi bonne en premier lieu.
Matthias
20

La raison est évidente: que se passe-t-il si l'activité est détruite et a déclenché la tâche?

Dissociez manuellement l'activité de l' AsyncTaskintérieur onDestroy(). Réassociez manuellement la nouvelle activité à l' AsyncTaskentrée onCreate(). Cela nécessite une classe interne statique ou une classe Java standard, plus peut-être 10 lignes de code.

CommonsWare
la source
Soyez prudent avec les références statiques - j'ai vu des objets être récupérés, même s'il y avait des références statiques fortes à eux. Peut-être un effet secondaire du chargeur de classe d'Android, ou même un bogue, mais les références statiques ne sont pas un moyen sûr d'échanger l'état à travers le cycle de vie d'une activité. L'objet de l'application est, cependant, c'est pourquoi j'utilise cela.
Matthias
10
@Matthias: Je n'ai pas dit d'utiliser des références statiques. J'ai dit d'utiliser une classe interne statique. Il y a une différence substantielle, bien que les deux aient «statique» dans leurs noms.
CommonsWare
5
Je vois - la clé ici est getLastNonConfigurationInstance (), mais pas la classe interne statique. Une classe interne statique ne conserve aucune référence implicite à sa classe externe, elle est donc sémantiquement équivalente à une classe publique ordinaire. Juste un avertissement: onRetainNonConfigurationInstance () n'est PAS garanti d'être appelé lorsqu'une activité est interrompue (une interruption peut également être un appel téléphonique), vous devrez donc répartir votre tâche dans onSaveInstanceState (), aussi, pour une véritable solidité Solution. Mais quand même, bonne idée.
Matthias
7
Um ... onRetainNonConfigurationInstance () est toujours appelée lorsque l'activité est en train d'être détruite et recréée. Cela n'a aucun sens d'appeler à d'autres moments. Si un basculement vers une autre activité se produit, l'activité en cours est suspendue / arrêtée, mais elle n'est pas détruite, de sorte que la tâche asynchrone peut continuer à s'exécuter et à utiliser la même instance d'activité. S'il se termine et affiche une boîte de dialogue, la boîte de dialogue s'affichera correctement dans le cadre de cette activité et ne s'affichera donc pas à l'utilisateur jusqu'à ce qu'il revienne à l'activité. Vous ne pouvez pas mettre AsyncTask dans un bundle.
hackbod
15

Il semble que ce AsyncTasksoit un peu plus que des défauts conceptuels . Il est également inutilisable en raison de problèmes de compatibilité. Les documents Android lisent:

Lors de leur introduction, les AsyncTasks ont été exécutés en série sur un seul thread d'arrière-plan. À partir de DONUT, cela a été changé en un pool de threads permettant à plusieurs tâches de fonctionner en parallèle. À partir de HONEYCOMB, les tâches sont de nouveau exécutées sur un seul thread pour éviter les erreurs d'application courantes causées par l'exécution parallèle. Si vous voulez vraiment une exécution parallèle, vous pouvez utiliser la executeOnExecutor(Executor, Params...) version de cette méthode avec THREAD_POOL_EXECUTOR ; cependant, voir les commentaires pour les avertissements sur son utilisation.

Les deux executeOnExecutor()et THREAD_POOL_EXECUTORsont ajoutés dans l'API niveau 11 (Android 3.0.x, HONEYCOMB).

Cela signifie que si vous créez deux AsyncTasks pour télécharger deux fichiers, le 2e téléchargement ne démarrera pas avant la fin du premier. Si vous discutez via deux serveurs et que le premier serveur est en panne, vous ne vous connecterez pas au second avant que la connexion au premier n'expire. (À moins que vous n'utilisiez les nouvelles fonctionnalités d'API11, bien sûr, mais cela rendra votre code incompatible avec 2.x).

Et si vous souhaitez cibler à la fois 2.x et 3.0+, les choses deviennent vraiment délicates.

De plus, les documents disent:

Attention: Un autre problème que vous pourriez rencontrer lors de l'utilisation d'un thread de travail est le redémarrage inattendu de votre activité en raison d'un changement de configuration d'exécution (tel que lorsque l'utilisateur change l'orientation de l'écran), ce qui peut détruire votre thread de travail . Pour voir comment vous pouvez conserver votre tâche pendant l'un de ces redémarrages et comment annuler correctement la tâche lorsque l'activité est détruite, consultez le code source de l'exemple d'application Shelves.

18446744073709551615
la source
12

Probablement nous tous, y compris Google, abusons AsyncTaskdu point de vue MVC .

Une activité est un contrôleur , et le contrôleur ne doit pas démarrer d'opérations susceptibles de survivre à la vue . Autrement dit, les AsyncTasks doivent être utilisés à partir de Model , à partir d'une classe qui n'est pas liée au cycle de vie de l'activité - rappelez-vous que les activités sont détruites lors de la rotation. (En ce qui concerne la vue , vous ne programmez généralement pas de classes dérivées, par exemple, de android.widget.Button, mais vous le pouvez. Habituellement, la seule chose que vous faites à propos de la vue est le xml.)

En d'autres termes, il est faux de placer des dérivés AsyncTask dans les méthodes d'Activités. OTOH, si nous ne devons pas utiliser AsyncTasks dans les activités, AsyncTask perd son attrait: il était auparavant annoncé comme une solution rapide et facile.

18446744073709551615
la source
5

Je ne suis pas sûr qu'il soit vrai que vous risquez une fuite de mémoire avec une référence à un contexte à partir d'une AsyncTask.

La manière habituelle de les implémenter est de créer une nouvelle instance AsyncTask dans le cadre de l'une des méthodes de l'activité. Donc, si l'activité est détruite, une fois la tâche AsyncTask terminée, ne sera-t-elle pas accessible et pourra-t-elle être récupérée? La référence à l'activité n'aura donc aucune importance car la tâche AsyncTask elle-même ne restera pas en place.

oli
la source
2
vrai - mais que faire si la tâche se bloque indéfiniment? Les tâches sont destinées à effectuer des opérations de blocage, peut-être même celles qui ne se terminent jamais. Voilà votre fuite de mémoire.
Matthias
1
Tout travailleur qui effectue quelque chose dans une boucle sans fin, ou tout ce qui se bloque par exemple sur une opération d'E / S.
Matthias
2

Il serait plus robuste de conserver une semaine de référence sur votre activité:

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
la source
C'est similaire à ce que nous avons fait pour la première fois avec Droid-Fu. Nous conserverions une carte des références faibles aux objets de contexte et ferions une recherche dans les rappels de tâches pour obtenir la référence la plus récente (si disponible) pour exécuter le rappel. Notre approche signifiait cependant qu'il y avait une seule entité qui maintenait ce mappage, contrairement à votre approche, c'est donc bien plus agréable.
Matthias
1
Avez-vous regardé RoboSpice? github.com/octo-online/robospice . Je crois que ce système est encore meilleur.
Snicolas
L'exemple de code sur la page d'accueil ressemble à une fuite d'une référence de contexte (une classe interne conserve une référence implicite à la classe externe.) Pas convaincu !!
Matthias
@Matthias, vous avez raison, c'est pourquoi je propose une classe interne statique qui contiendra une WeakReference sur l'activité.
Snicolas
1
@Matthias, je crois que cela commence à être hors sujet. Mais les chargeurs ne fournissent pas de mise en cache prête à l'emploi comme nous le faisons, plus encore, les chargeurs ont tendance à être plus verbeux que notre bibliothèque. En fait, ils gèrent assez bien les curseurs mais pour le réseautage, une approche différente, basée sur la mise en cache et un service est mieux adaptée. Voir neilgoodman.net/2011/12/26/… partie 1 & 2
Snicolas
1

Pourquoi ne pas simplement remplacer la onPause()méthode dans l'activité propriétaire et annuler la à AsyncTaskpartir de là?

Jeff Axelrod
la source
cela dépend de ce que fait cette tâche. s'il ne fait que charger / lire des données, ce serait OK. mais s'il modifie l'état de certaines données sur un serveur distant, nous préférerions donner à la tâche la possibilité de s'exécuter jusqu'à la fin.
Vit Khudenko
@Arhimed et je suppose que si vous maintenez le thread d'interface utilisateur en onPausece qu'il est tout aussi mauvais que de le maintenir ailleurs? C'est à dire, vous pourriez obtenir un ANR?
Jeff Axelrod
exactement. nous ne pouvons pas bloquer le thread d'interface utilisateur (que ce soit un onPauseou autre) car nous risquons d'obtenir un ANR.
Vit Khudenko
1

Vous avez tout à fait raison - c'est pourquoi l'abandon de l'utilisation des tâches / chargeurs asynchrones dans les activités pour récupérer des données prend de l'ampleur. L'une des nouvelles façons consiste à utiliser un framework Volley qui fournit essentiellement un rappel une fois que les données sont prêtes - beaucoup plus cohérent avec le modèle MVC. Volley a été peuplé dans Google I / O 2013. Je ne sais pas pourquoi plus de gens ne le savent pas.

C0D3LIC1OU5
la source
merci pour cela ... je vais y jeter un coup d'œil ... ma raison de ne pas aimer AsyncTask est parce que cela me fait coincer avec un ensemble d'instructions sur PostExecute ... à moins que je le pirate comme en utilisant des interfaces ou en le remplaçant à chaque fois J'en ai besoin.
carinlynchin
0

Personnellement, je viens d'étendre Thread et d'utiliser une interface de rappel pour mettre à jour l'interface utilisateur. Je n'ai jamais pu faire fonctionner AsyncTask sans problèmes FC. J'utilise également une file d'attente non bloquante pour gérer le pool d'exécution.

androidworkz
la source
1
Eh bien, votre fermeture de force était probablement due au problème que j'ai mentionné: vous avez essayé de référencer un contexte qui était hors de portée (c'est-à-dire que sa fenêtre avait été détruite), ce qui entraînerait une exception de cadre.
Matthias
Non ... en fait, c'est parce que la file d'attente est nulle qui est intégrée à AsyncTask. J'utilise toujours getApplicationContext (). Je n'ai pas de problèmes avec AsyncTask si ce n'est que quelques opérations ... mais j'écris un lecteur multimédia qui met à jour la pochette d'album en arrière-plan ... dans mon test, j'ai 120 albums sans art ... donc, alors que mon application n'a pas fermé tout le chemin, asynctask lançait des erreurs ... j'ai donc plutôt construit une classe singleton avec une file d'attente qui gère les processus et jusqu'à présent, cela fonctionne très bien.
androidworkz
0

Je pensais que l'annulation fonctionne, mais ce n'est pas le cas.

ici, ils RTFMing à ce sujet:

"" Si la tâche a déjà démarré, le paramètre mayInterruptIfRunning détermine si le thread exécutant cette tâche doit être interrompu pour tenter d'arrêter la tâche. "

Cela n'implique cependant pas que le thread soit interruptible. C'est une chose Java, pas une chose AsyncTask. "

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

nir
la source
0

Vous feriez mieux de penser à AsyncTask comme quelque chose qui est plus étroitement associé à une activité, un contexte, un ContextWrapper, etc. C'est plus pratique lorsque sa portée est pleinement comprise.

Assurez-vous que vous avez une politique d'annulation dans votre cycle de vie afin qu'elle finisse par être récupérée et ne conserve plus de référence à votre activité et qu'elle puisse également être récupérée.

Sans annuler votre AsyncTask pendant que vous vous éloignez de votre contexte, vous rencontrerez des fuites de mémoire et des NullPointerExceptions, si vous avez simplement besoin de fournir des commentaires comme un Toast une simple boîte de dialogue, puis un singleton de votre contexte d'application aiderait à éviter le problème NPE.

AsyncTask n'est pas si mal, mais il y a certainement beaucoup de magie qui peut conduire à des pièges imprévus.

jtuchek
la source
-1

Quant aux «expériences de travail avec lui»: il est possible de tuer le processus avec tous les AsyncTasks, Android va recréer la pile d'activités afin que l'utilisateur ne mentionne rien.

18446744073709551615
la source