web-dev-qa-db-fra.com

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

Cela fait des mois que j'étudie ce problème et que je propose différentes solutions, ce qui ne me satisfait pas car ce sont des piratages énormes. Je ne peux toujours pas croire qu'une classe dont la conception est défectueuse a été intégrée dans le cadre et que personne n'en parle, alors je suppose que je dois simplement 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 fil de l’interface utilisateur sans avoir à manipuler les fils et/ou les gestionnaires."

L'exemple continue ensuite à montrer comment une méthode showDialog() exemplaire est appelée dans onPostExecute(). Ceci, cependant, me semble entièrement artificiel , car l'affichage d'un dialogue nécessite toujours une référence à un Context valide et à un AsyncTask =. ne doit jamais contenir de forte référence à un objet de contexte.

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

À moins que ma logique ne soit erronée ici, cela se traduit par: onPostExecute() est totalement inutile, car à quoi sert-il que cette méthode s'exécute 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 transmettre d'instances de contexte à une AsyncTask, mais à une instance Handler. Cela fonctionne: dans la mesure où 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 embêter avec les gestionnaires, est fausse. Cela ressemble aussi à abuser de Handler, puisque vous envoyez et recevez des messages sur le même fil (vous le créez sur le fil de l'interface utilisateur et l'envoyez via onPostExecute () qui est également exécuté sur le fil de l'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 avez pas d'enregistrement des tâches qu'il a déclenchées. Cela signifie que vous devez redémarrer les tâches lorsque vous recréez le contexte, par exemple. après un changement d'orientation de l'écran. C'est lent et inutile.

Ma solution à cela (en tant que implémentée dans la bibliothèque Droid-F ) est de maintenir un mappage de WeakReferences à partir des noms de composants vers leurs instances actuelles sur l'objet d'application unique. Chaque fois qu'une AsyncTask est démarrée, elle enregistre le contexte d'appel dans cette mappe et, à chaque rappel, elle récupère l'instance de contexte en cours à partir de cette mappage. Cela garantit que vous ne ferez jamais référence à une instance de contexte obsolète et vous avez toujours accès à un contexte valide dans les rappels afin que vous puissiez y effectuer un travail d'interface 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.

Néanmoins, il s’agit d’une solution de contournement complexe nécessitant 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: Me manque-t-il massivement quelque chose ou AsyncTask est-il vraiment complètement défectueux? Comment tes expériences fonctionnent-elles? Comment avez-vous résolu ce problème?

Merci pour votre contribution.

258
Matthias

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

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

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

20
CommonsWare

Il semble que AsyncTask soit un peu plus que conceptuellement défectueux. Il est également inutilisable en raison de problèmes de compatibilité. Le Android docs lu:

Lors de la première introduction, les tâches asynchrones étaient exécutées en série sur un seul thread d'arrière-plan. À partir de DONUT, cette option a été remplacée par 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 en parallèle. Si vous voulez vraiment une exécution en parallèle, vous pouvez utiliser le fichierexecuteOnExecutor(Executor, Params...) version de cette méthode avecTHREAD_POOL_EXECUTOR; cependant, voir les commentaires sur ce sujet pour des avertissements sur son utilisation.

executeOnExecutor() et THREAD_POOL_EXECUTOR Sont tous deux ajoutés dans l'API de niveau 11 (Android 3.0.x, HONEYCOMB).

Cela signifie que si vous créez deux AsyncTasks pour télécharger deux fichiers, le deuxième téléchargement ne commencera pas avant la fin du premier. Si vous discutez via deux serveurs et que le premier serveur est en panne, vous ne pourrez pas vous connecter au second avant la fin de la connexion au premier. (Sauf si vous utilisez les nouvelles fonctionnalités API11, bien sûr, mais cela rendra votre code incompatible avec 2.x).

Et si vous voulez cibler à la fois les versions 2.x et 3.0+, le travail devient vraiment compliqué.

De plus, les docs disent:

Attention: un autre problème que vous pouvez rencontrer lors de l'utilisation d'un thread de travail est un redémarrage inattendu de votre activité en raison d'un changement de configuration d'exécution (tel que lorsque l'utilisateur modifie 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 l'annuler correctement lorsque l'activité est détruite, voir le code source de l'exemple d'application Shelves.

15

Probablement nous tous, y compris Google, utilisons mal AsyncTask du point de vue MVC .

Une activité est un contrôleur, et le contrôleur ne doit pas démarrer d'opérations pouvant survivre à View. C'est-à-dire que AsyncTasks doit être utilisé à partir de Modèle, à 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 par rotation. (Pour ce qui est de View, vous ne programmez généralement pas de classes dérivées de, par exemple, Android.widget.Button, mais vous pouvez le faire. La plupart des choses que vous faites à propos de View est le xml.)

En d'autres termes, il est erroné de placer les 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 de son attrait: cela était présenté comme une solution rapide et facile.

12

Je ne suis pas sûr que ce soit vrai que vous risquiez une fuite de mémoire en faisant référence à un contexte 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 Activity. Ainsi, si l'activité est détruite, une fois la tâche asynchrone terminée, ne sera-t-il pas inaccessible et ne pourra-t-il pas être récupéré? Ainsi, la référence à l'activité n'aura pas d'importance, car la tâche Async lui-même ne traîne pas.

5
oli

Il serait plus robuste de garder une référence de semaine 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]);
            }
        }
    }

}
2
Snicolas

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

1
Jeff Axelrod

Vous avez absolument raison - c’est la raison pour laquelle l’utilisation de tâches/chargeurs asynchrones dans les activités de récupération de données gagne du terrain. L'une des nouvelles méthodes consiste à utiliser un cadre Volley qui fournit essentiellement un rappel lorsque les données sont prêtes, ce qui est beaucoup plus cohérent avec le modèle MVC. Volley a été peuplé dans le Google I/O 2013. Je ne sais pas pourquoi plus de gens ne sont pas au courant.

1
C0D3LIC1OU5

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

0
androidworkz

Vous feriez mieux de penser à AsyncTask en tant que quelque chose qui est plus étroitement couplé avec une activité, un contexte, un contextuel, etc. C'est plus une commodité quand sa portée est parfaitement comprise.

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

Sans annuler votre AsyncTask lorsque vous vous éloignez de votre contexte, vous rencontrerez des fuites de mémoire et des exceptions NullPointerExceptions. Si vous avez simplement besoin de fournir des commentaires, comme un simple dialogue Toast, un singleton de votre contexte d'application aidera à éviter le problème NPE.

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

0
jtuchek

Je pensais annuler des travaux 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 qui l'exécute doit être interrompu pour tenter de l'arrêter. "

Cela ne signifie cependant pas que le thread est 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

0
nir