77 votes

L'AsyncTask ne s'arrête pas même si l'Activity a été détruite

J'ai un AsyncTask qui commence à s'exécuter lorsque l'objet Activity est créé et fait des choses en arrière-plan (télécharge jusqu'à 100 images). Tout fonctionne bien mais il y a ce comportement particulier que je n'arrive pas à comprendre.

Par exemple : lorsque l'orientation de l'écran d'Android change, la fonction Activity est détruit et recréé. Je surcharge donc la fonction onRetainNonConfigurationInstance() et enregistrer toutes les données téléchargées exécutées dans le fichier AsyncTask . Mon but est de ne pas avoir à AsyncTask courir à chaque fois Activity est détruit-créé lors des changements d'orientation, mais comme je peux le voir dans mes logs, l'orientation précédente a été détruite. AsyncTask est toujours en cours d'exécution. (Les données sont cependant sauvegardées correctement)

J'ai même essayé d'annuler le AsyncTask dans le onDestroy() de l'activité, mais les journaux indiquent toujours AsyncTask en cours d'exécution.

Ce comportement est vraiment étrange et je serais vraiment reconnaissant si quelqu'un pouvait m'indiquer la procédure correcte pour arrêter/annuler la AsyncTask .

146voto

Snicolas Points 19644

La réponse donnée par @Romain Guy est correcte. Néanmoins, j'aimerais ajouter un complément d'information et donner un pointeur vers une ou deux bibliothèques qui peuvent être utilisées pour les AsyncTask de longue durée et encore plus pour les asynctasks orientés réseau.

Les tâches asynchrones ont été conçues pour effectuer des tâches en arrière-plan. Et oui, vous pouvez les arrêter en utilisant la fonction cancel méthode. Lorsque vous téléchargez des documents sur l'internet, je vous conseille vivement de s'occuper de votre thread lorsqu'il est dans l'état de blocage de l'IO . Vous devez organiser votre téléchargement comme suit :

public void download() {
    //get the InputStream from HttpUrlConnection or any other
    //network related stuff
    while( inputStream.read(buffer) != -1 && !Thread.interrupted() ) {
      //copy data to your destination, a file for instance
    }
    //close the stream and other resources
}

L'utilisation de la Thread.interrupted aidera votre thread à quitter correctement un état io bloquant. Votre thread sera plus réactif à l'invocation de la fonction cancel méthode.

Défaut de conception d'AsyncTask

Mais si votre AsyncTask dure trop longtemps, vous serez confronté à deux problèmes différents :

  1. Les activités sont mal liées au cycle de vie de l'activité et vous n'obtiendrez pas le résultat de votre AsyncTask si votre activité meurt. En effet, si, vous pouvez le faire mais ce sera de manière approximative.
  2. Les AsyncTask ne sont pas très bien documentés. Une mise en œuvre et une utilisation naïves, bien qu'intuitives, d'une tâche asynchrone peuvent rapidement entraîner des fuites de mémoire.

RoboSpice la bibliothèque que j'aimerais présenter, utilise un service d'arrière-plan pour exécuter ce type de requêtes. Il a été conçu pour les requêtes réseau. Elle offre des fonctionnalités supplémentaires telles que la mise en cache automatique des résultats des requêtes.

Voici la raison pour laquelle les AsyncTasks ne sont pas adaptées aux tâches de longue durée. Le raisonnement suivant est une adaptation d'extraits de Motivations de RoboSpice : l'application qui explique pourquoi l'utilisation de RoboSpice répond à un besoin sur la plateforme Android.

Le cycle de vie de l'AsyncTask et de l'Activity

Les tâches asynchrones ne suivent pas le cycle de vie des instances d'activité. Si vous démarrez une AsyncTask à l'intérieur d'une Activity et que vous faites pivoter l'appareil, l'Activity sera détruite et une nouvelle instance sera créée. Mais l'AsyncTask ne mourra pas. Elle continuera à vivre jusqu'à ce qu'elle soit terminée.

Et lorsqu'elle se termine, l'AsyncTask ne met pas à jour l'interface utilisateur de la nouvelle activité. En effet, elle met à jour l'ancienne instance de l'activité qui n'est plus affichée. Cela peut conduire à une exception de type java.lang.IllegalArgumentException : View not attached to window manager si vous utilisez, par exemple, findViewById pour récupérer une vue à l'intérieur de l'Activity.

Problème de fuite de mémoire

Il est très pratique de créer des AsyncTasks en tant que classes internes de vos Activities. Comme l'AsyncTask devra manipuler les vues de l'activité lorsque la tâche est terminée ou en cours, l'utilisation d'une classe interne de l'activité semble pratique : les classes internes peuvent accéder directement à n'importe quel champ de la classe externe. Les classes internes peuvent accéder directement à n'importe quel champ de la classe externe.

Néanmoins, cela signifie que la classe interne détiendra une référence invisible sur l'instance de la classe externe : l'activité.

A long terme, cela produit une fuite de mémoire : si l'AsyncTask dure longtemps, elle maintient l'activité "en vie" alors qu'Android voudrait s'en débarrasser car elle ne peut plus être affichée. L'activité ne peut pas être ramassée et c'est un mécanisme central de la gestion de l'activité. mécanisme central d'Android pour préserver les ressources sur l'appareil.

La progression de votre tâche sera perdue

Vous pouvez utiliser des solutions de contournement pour créer une tâche asynchrone de longue durée et gérer son cycle de vie en fonction du cycle de vie de l'activité. Vous pouvez soit annuler l'AsyncTask dans la méthode onStop de votre activité ou vous pouvez laisser votre tâche asynchrone se terminer, et ne pas perdre sa progression et son le relier à l'instance suivante de votre activité .

C'est possible et nous montrons comment dans les motivations de RobopSpice, mais cela devient compliqué et le code n'est pas vraiment générique. De plus, vous perdrez toujours la progression de votre tâche si l'utilisateur quitte l'activité et revient. Le même problème se pose avec les chargeurs, bien qu'il s'agisse d'un équivalent plus simple de la solution AsyncTask avec relinking mentionnée ci-dessus.

Utilisation d'un service Android

La meilleure solution consiste à utiliser un service pour exécuter vos tâches de fond de longue durée. Et c'est exactement la solution proposée par RoboSpice. Encore une fois, cette bibliothèque est conçue pour les réseaux, mais elle pourrait être étendue à des choses qui ne sont pas liées aux réseaux. Cette bibliothèque dispose d'un un grand nombre de fonctionnalités .

Vous pouvez même vous en faire une idée en moins de 30 secondes grâce à une infographies .


C'est vraiment une très très mauvaise idée d'utiliser les AsyncTasks pour des opérations de longue durée. Néanmoins, elles conviennent parfaitement pour les opérations de courte durée, comme la mise à jour d'une vue après 1 ou 2 secondes.

Je vous encourage à télécharger le RoboSpice Motivations app Il explique vraiment tout cela en profondeur et fournit des exemples et des démonstrations des différentes façons de faire des choses liées au réseau.


Si vous cherchez une alternative à RoboSpice pour des tâches non liées au réseau (par exemple sans cache), vous pouvez également consulter les sites suivants Ruban .

1 votes

@Snicolas, pouvez-vous ajouter droidQuery comme alternative à RoboSpice sur le Github page ? Je l'ai développée l'année dernière et elle offre, entre autres, la possibilité d'effectuer des tâches en réseau à l'aide de jQuery -style Ajax (écrit en Java Android pur).

0 votes

@Phil, c'est fait. Merci pour le lien. BTW, votre syntaxe est très proche de celle de Ion (qui est venu après).

0 votes

@Snicolas, merci ! On dirait que les deux droidQuery y Ion a repris une partie de la syntaxe de Picasso . github.com/koush/ion/search?q=picasso&ref=cmdform ;)

16voto

Cagatay Kalan Points 1489

Romain Guy a raison. En fait, une tâche asynchrone est responsable de terminer son propre travail dans tous les cas. L'interruption n'est pas le meilleur moyen, il faut donc vérifier en permanence si quelqu'un veut que l'on annule ou que l'on arrête sa tâche.

Supposons que votre AsyncTask fait quelque chose en boucle plusieurs fois. Vous devez alors vérifier isCancelled() dans chaque boucle.

while ( true ) {
    if ( isCancelled())
        break;
    doTheTask();
}

doTheTask() est votre véritable travail et avant de le faire dans chaque boucle, vous vérifiez si votre tâche doit être annulée.

En règle générale, vous devez définir un indicateur dans votre AsyncTask ou renvoyer un résultat approprié à partir de votre doInBackground() afin que, dans votre onPostExecute() Vous pouvez ainsi vérifier si vous avez pu terminer ce que vous vouliez ou si votre travail a été annulé en cours de route.

1voto

Frane Poljak Points 94

Ce qui suit ne résout pas votre problème, mais le prévient : Dans le manifeste de l'application, faites ceci :

    <activity
        android:name=".(your activity name)"
        android:label="@string/app_name"
        android:configChanges="orientation|keyboardHidden|screenSize" > //this line here
    </activity>

Lorsque vous ajoutez ceci, votre activité ne se recharge pas lors d'un changement de configuration, et si vous voulez faire des changements lors d'un changement d'orientation, il vous suffit de surcharger la méthode suivante de l'activité :

@Override
public void onConfigurationChanged(Configuration newConfig) {
    super.onConfigurationChanged(newConfig);

        //your code here
    }

0 votes

O,... cette méthode sera appelée une fois que l'orientation de l'appareil aura changé ? @FranePoljak... i c, i c,...

-1voto

A partir de la MVC point de vue, l'activité est le Contrôleur ; c'est une erreur de la part de la Contrôleur pour effectuer des opérations qui dépassent la durée de vie de l Voir (dérivé de Android.view.View, il suffit généralement de réutiliser les classes existantes). Par conséquent, il devrait s'agir de la classe Modèle La responsabilité de démarrer les AsyncTasks incombe à l'utilisateur.

-3voto

Vous pouvez utiliser class MagicAppRestart de ce poste à tuer le processus ainsi que toutes les AsyncTasks ; Android rétablira la pile d'activités (l'utilisateur ne mentionnera rien). Il est important de noter que la seule notification avant le redémarrage d'un processus est l'appel à onPause() ; en fonction de la Logique du cycle de vie des applications Android Votre application doit être prête à faire face à une telle résiliation de toute façon.

J'ai essayé et cela semble fonctionner. Néanmoins, pour le moment, je prévois d'utiliser des méthodes "plus civilisées" comme les références faibles de la classe Application (mes AsyncTasks ont une durée de vie plutôt courte et, je l'espère, ne consomment pas trop de mémoire).

Voici un code avec lequel vous pouvez jouer :

MagicAppRestart.java

package com.xyz;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;

/** This activity shows nothing; instead, it restarts the android process */
public class MagicAppRestart extends Activity {
    // Do not forget to add it to AndroidManifest.xml
    // <activity android:name="your.package.name.MagicAppRestart"/>
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        System.exit(0);
    }
    public static void doRestart(Activity anyActivity) {
        anyActivity.startActivity(new Intent(anyActivity.getApplicationContext(), MagicAppRestart.class));
    }
}

Le reste est ce qu'Eclipse a créé pour un nouveau projet Android pour com.xyz.AsyncTaskTestActivity :

AsyncTaskTestActivity.java

package com.xyz;

import android.app.Activity;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;
import android.view.View;

public class AsyncTaskTestActivity extends Activity {
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        Log.d("~~~~","~~~onCreate ~~~ "+this);
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
    }
    public void onStartButton(View view) {
        Log.d("~~~~","~~~onStartButton {");
        class MyTask extends AsyncTask<Void, Void, Void> {

            @Override
            protected Void doInBackground(Void... params) {
                // TODO Auto-generated method stub
                Log.d("~~~~","~~~doInBackground started");
                try {
                    for (int i=0; i<10; i++) {
                        Log.d("~~~~","~~~sleep#"+i);
                        Thread.sleep(200);
                    }
                    Log.d("~~~~","~~~sleeping over");
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                Log.d("~~~~","~~~doInBackground ended");
                return null;
            }
            @Override
            protected void onPostExecute(Void result) {
                super.onPostExecute(result);
                taskDone();
            }
        }
        MyTask task = new MyTask();
        task.execute(null);
        Log.d("~~~~","~~~onStartButton }");
    }
    private void taskDone() {
        Log.d("~~~~","\n\n~~~taskDone ~~~ "+this+"\n\n");
    }
    public void onStopButton(View view) {
        Log.d("~~~~","~~~onStopButton {");
        MagicAppRestart.doRestart(this);
        Log.d("~~~~","~~~onStopButton }");
    }
    public void onPause() {   Log.d("~~~~","~~~onPause ~~~ "+this);   super.onPause(); }
    public void onStop() {    Log.d("~~~~","~~~onStop ~~~ "+this);    super.onPause(); }
    public void onDestroy() { Log.d("~~~~","~~~onDestroy ~~~ "+this); super.onDestroy(); }
}

main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >

    <Button android:text="Start" android:onClick="onStartButton" android:layout_width="fill_parent" android:layout_height="wrap_content"/>
    <Button android:text="Stop" android:onClick="onStopButton" android:layout_width="fill_parent" android:layout_height="wrap_content"/>
    <TextView
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="@string/hello" />

</LinearLayout>

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.xyz"
    android:versionCode="1"
    android:versionName="1.0" >
    <uses-sdk android:minSdkVersion="7" />
    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name" >
        <activity
            android:name=".AsyncTaskTestActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:name=".MagicAppRestart"/>
    </application>
</manifest>

et une partie pertinente des journaux (notez que seulement onPause est appelé ) :

D/~~~~    (13667): ~~~onStartButton {
D/~~~~    (13667): ~~~onStartButton }
D/~~~~    (13667): ~~~doInBackground started
D/~~~~    (13667): ~~~sleep#0
D/~~~~    (13667): ~~~sleep#1
D/~~~~    (13667): ~~~sleep#2
D/~~~~    (13667): ~~~sleep#3
D/~~~~    (13667): ~~~sleep#4
D/~~~~    (13667): ~~~sleep#5
D/~~~~    (13667): ~~~sleep#6
D/~~~~    (13667): ~~~sleep#7
D/~~~~    (13667): ~~~sleep#8
D/~~~~    (13667): ~~~sleep#9
D/~~~~    (13667): ~~~sleeping over
D/~~~~    (13667): ~~~doInBackground ended
D/~~~~    (13667): 
D/~~~~    (13667): 
D/~~~~    (13667): ~~~taskDone ~~~ com.xyz.AsyncTaskTestActivity@40516988
D/~~~~    (13667): 

D/~~~~    (13667): ~~~onStartButton {
D/~~~~    (13667): ~~~onStartButton }
D/~~~~    (13667): ~~~doInBackground started
D/~~~~    (13667): ~~~sleep#0
D/~~~~    (13667): ~~~sleep#1
D/~~~~    (13667): ~~~sleep#2
D/~~~~    (13667): ~~~sleep#3
D/~~~~    (13667): ~~~sleep#4
D/~~~~    (13667): ~~~sleep#5
D/~~~~    (13667): ~~~onStopButton {
I/ActivityManager(   81): Starting: Intent { cmp=com.xyz/.MagicAppRestart } from pid 13667
D/~~~~    (13667): ~~~onStopButton }
D/~~~~    (13667): ~~~onPause ~~~ com.xyz.AsyncTaskTestActivity@40516988
I/ActivityManager(   81): Process com.xyz (pid 13667) has died.
I/WindowManager(   81): WIN DEATH: Window{4073ceb8 com.xyz/com.xyz.AsyncTaskTestActivity paused=false}
I/ActivityManager(   81): Start proc com.xyz for activity com.xyz/.AsyncTaskTestActivity: pid=13698 uid=10101 gids={}
I/ActivityManager(   81): Displayed com.xyz/.AsyncTaskTestActivity: +44ms (total +65ms)
D/~~~~    (13698): ~~~onCreate ~~~ com.xyz.AsyncTaskTestActivity@40517238

1 votes

Réponse en 2013 (à l'époque d'Android 2.x), downvoted en 2017 (à l'époque d'Android 6 et 7). Beaucoup de choses ont dû changer depuis, je sais que le hack du redémarrage de l'appli ne fonctionnait plus depuis Android 4.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X