AsyncTask wird auch dann nicht gestoppt, wenn die Aktivität zerstört wurde

76

Ich habe ein AsyncTask-Objekt, das ausgeführt wird, wenn die Aktivität erstellt wird, und Dinge im Hintergrund ausführt (lädt bis zu 100 Bilder herunter). Alles funktioniert gut, aber es gibt dieses eigenartige Verhalten, das ich nicht verstehen kann.

Zum Beispiel: Wenn sich die Ausrichtung des Android-Bildschirms ändert, wird die Aktivität zerstört und erneut erstellt. Daher überschreibe ich die onRetainNonConfigurationInstance () -Methode und speichere alle heruntergeladenen Daten, die in der AsyncTask ausgeführt werden. Mein Zweck dabei ist es, AsyncTask nicht jedes Mal ausführen zu lassen, wenn eine Aktivität zerstört wird, die während Orientierungsänderungen erstellt wird. Wie ich jedoch in meinen Protokollen sehen kann, wird die vorherige AsyncTask noch ausgeführt. (Die Daten werden jedoch korrekt gespeichert)

Ich habe sogar versucht, die AsyncTask in der onDestroy () -Methode der Aktivität abzubrechen, aber in den Protokollen wird AsyncTask weiterhin als ausgeführt angezeigt.

Dies ist ein wirklich seltsames Verhalten und wäre wirklich dankbar, wenn mir jemand das richtige Verfahren zum Stoppen / Abbrechen der AsyncTask mitteilen könnte.

Vielen Dank

Raja
quelle

Antworten:

144

Die Antwort von @Romain Guy ist richtig. Trotzdem möchte ich eine Ergänzung von Informationen hinzufügen und einen Zeiger auf eine Bibliothek oder 2 geben, die für AsyncTask mit langer Laufzeit und noch mehr für netzwerkorientierte Asynctasks verwendet werden können.

AsyncTasks wurden für Aufgaben im Hintergrund entwickelt. Und ja, Sie können es mit der cancelMethode stoppen . Wenn Sie Inhalte aus dem Internet herunterladen, empfehle ich Ihnen dringend, sich um Ihren Thread zu kümmern, wenn es sich um den E / A-Blockierungsstatus handelt . Sie sollten Ihren Download wie folgt organisieren:

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
}

Die Verwendung des Thread.interruptedFlags hilft Ihrem Thread, einen blockierenden io-Status ordnungsgemäß zu beenden. Ihr Thread reagiert schneller auf einen Aufruf der cancelMethode.

AsyncTask Designfehler

Wenn Ihre AsyncTask jedoch zu lange dauert, treten zwei verschiedene Probleme auf:

  1. Aktivitäten sind schlecht an den Aktivitätslebenszyklus gebunden und Sie erhalten das Ergebnis Ihrer AsyncTask nicht, wenn Ihre Aktivität stirbt. In der Tat, ja, Sie können, aber es wird der grobe Weg sein.
  2. AsyncTask sind nicht sehr gut dokumentiert. Eine naive, wenn auch intuitive Implementierung und Verwendung einer Asynctask kann schnell zu Speicherverlusten führen.

RoboSpice , die Bibliothek, die ich vorstellen möchte, verwendet einen Hintergrunddienst, um diese Art von Anforderungen auszuführen. Es wurde für Netzwerkanforderungen entwickelt. Es bietet zusätzliche Funktionen wie das automatische Zwischenspeichern der Ergebnisse von Anforderungen.

Hier ist der Grund, warum AsyncTasks für lange laufende Aufgaben schlecht sind. Das folgende Argument ist eine Anpassung aus Auszügen von RoboSpice-Motivationen : Die App, die erklärt, warum die Verwendung von RoboSpice einen Bedarf auf der Android-Plattform erfüllt.

Der Lebenszyklus von AsyncTask und Aktivität

AsyncTasks folgen nicht dem Lebenszyklus von Aktivitätsinstanzen. Wenn Sie eine AsyncTask innerhalb einer Aktivität starten und das Gerät drehen, wird die Aktivität zerstört und eine neue Instanz erstellt. Aber die AsyncTask wird nicht sterben. Es wird weiterleben, bis es fertig ist.

Wenn dies abgeschlossen ist, aktualisiert die AsyncTask die Benutzeroberfläche der neuen Aktivität nicht. In der Tat wird die frühere Instanz der Aktivität aktualisiert, die nicht mehr angezeigt wird. Dies kann zu einer Ausnahme vom Typ java.lang.IllegalArgumentException führen: Ansicht, die nicht an den Fenstermanager angehängt ist, wenn Sie beispielsweise findViewById verwenden, um eine Ansicht innerhalb der Aktivität abzurufen.

Speicherverlust Problem

Es ist sehr praktisch, AsyncTasks als innere Klassen Ihrer Aktivitäten zu erstellen. Da die AsyncTask die Ansichten der Aktivität bearbeiten muss, wenn die Aufgabe abgeschlossen ist oder ausgeführt wird, erscheint die Verwendung einer inneren Klasse der Aktivität praktisch: Innere Klassen können direkt auf jedes Feld der äußeren Klasse zugreifen.

Dies bedeutet jedoch, dass die innere Klasse einen unsichtbaren Verweis auf ihre äußere Klasseninstanz enthält: die Aktivität.

Auf lange Sicht führt dies zu einem Speicherverlust: Wenn die AsyncTask lange anhält, bleibt die Aktivität "am Leben", während Android sie gerne entfernen möchte, da sie nicht mehr angezeigt werden kann. Die Aktivität kann nicht durch Müll gesammelt werden. Dies ist ein zentraler Mechanismus für Android, um Ressourcen auf dem Gerät zu schonen.

Der Fortschritt Ihrer Aufgabe geht verloren

Sie können einige Problemumgehungen verwenden, um eine Asynctask mit langer Laufzeit zu erstellen und ihren Lebenszyklus entsprechend dem Lebenszyklus der Aktivität zu verwalten. Sie können die AsyncTask entweder in der onStop-Methode Ihrer Aktivität abbrechen oder Ihre asynchrone Aufgabe beenden lassen, ohne ihren Fortschritt zu verlieren und sie erneut mit der nächsten Instanz Ihrer Aktivität zu verknüpfen .

Dies ist möglich und wir zeigen, wie in RobopSpice Motivationen, aber es wird kompliziert und der Code ist nicht wirklich generisch. Darüber hinaus verlieren Sie immer noch den Fortschritt Ihrer Aufgabe, wenn der Benutzer die Aktivität verlässt und zurückkommt. Das gleiche Problem tritt bei Loadern auf, obwohl es ein einfacheres Äquivalent zu AsyncTask mit der oben erwähnten Problemumgehung für das erneute Verknüpfen wäre.

Verwenden eines Android-Dienstes

Am besten verwenden Sie einen Dienst, um Ihre lang laufenden Hintergrundaufgaben auszuführen. Und genau diese Lösung schlägt RoboSpice vor. Auch hier ist es für das Networking konzipiert, kann aber auch auf nicht netzwerkbezogene Inhalte erweitert werden. Diese Bibliothek verfügt über eine Vielzahl von Funktionen .

Dank einer Infografik können Sie sich sogar in weniger als 30 Sekunden ein Bild davon machen .


Es ist wirklich eine sehr, sehr schlechte Idee, AsyncTasks für lange laufende Vorgänge zu verwenden. Trotzdem eignen sie sich gut für kurzlebige Personen wie das Aktualisieren einer Ansicht nach 1 oder 2 Sekunden.

Ich empfehle Ihnen, die RoboSpice Motivations-App herunterzuladen. Sie erklärt dies ausführlich und bietet Beispiele und Demonstrationen der verschiedenen Möglichkeiten, netzwerkbezogene Aufgaben zu erledigen.


Wenn Sie nach einer Alternative zu RoboSpice für nicht netzwerkbezogene Aufgaben suchen (z. B. ohne Caching), können Sie sich auch Tape ansehen .

Snicolas
quelle
1
@Snicolas, können Sie droidQuery als Alternative zu RoboSpice auf der Github- Seite hinzufügen ? Ich habe es letztes Jahr entwickelt und bietet unter anderem die Möglichkeit, Netzwerkaufgaben mit Ajax im jQuery- Stil (geschrieben in reinem Android Java) auszuführen .
Phil
@Phil, fertig. Danke für den Link. Übrigens, Ihre Syntax kommt Ion sehr nahe (das kam später).
Snicolas
@Snicolas, danke! Es sieht so aus, als hätten sowohl droidQuery als auch Ion ein Syntaxdesign von Picasso übernommen . github.com/koush/ion/search?q=picasso&ref=cmdform ;)
Phil
"Es ist wirklich eine sehr, sehr schlechte Idee, AsyncTasks für lange laufende Vorgänge zu verwenden. Trotzdem eignen sie sich gut für kurzlebige Vorgänge wie das Aktualisieren einer Ansicht nach 1 oder 2 Sekunden." . Ich denke nicht, dass es unbedingt wahr ist. Der Grund, warum Android dies vorschlägt, ist die Tatsache, dass AsyncTask den globalen Thread-Pool-Executor verwendet. Sie können Ihren benutzerdefinierten Thread-Pool-Executor einfach über die Execute-Methode übergeben und mögliche Blockierungsprobleme beseitigen. Obwohl AsyncTask keine Silberkugel ist, wie Sie "undicht" erwähnt haben.
Stdout
Das Problem ist nicht der Pool, es ist wirklich das Leck.
Snicolas
16

Romain Guy hat recht. Tatsächlich ist eine asynchrone Aufgabe in jedem Fall dafür verantwortlich, ihren eigenen Job zu beenden. Unterbrechen ist nicht der beste Weg, daher sollten Sie kontinuierlich überprüfen, ob jemand möchte, dass Sie Ihre Aufgabe abbrechen oder stoppen.

Nehmen wir an, Sie machen AsyncTaskviele Male etwas in einer Schleife. Dann sollten Sie isCancelled()jede Schleife einchecken .

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

doTheTask() ist Ihre eigentliche Aufgabe und bevor Sie dies in jeder Schleife tun, prüfen Sie, ob Ihre Aufgabe abgebrochen werden soll.

Im Allgemeinen sollten Sie in Ihrer AsyncTaskKlasse eine Flagge setzen oder ein entsprechendes Ergebnis von Ihrer zurückgeben, doInBackground()damit Sie in Ihrer Klasse onPostExecute()überprüfen können, ob Sie das beenden können, was Sie möchten, oder ob Ihre Arbeit in der Mitte abgebrochen wurde.

Cagatay Kalan
quelle
1

Folgendes löst Ihr Problem nicht, verhindert es jedoch: Führen Sie im App-Manifest Folgendes aus:

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

Wenn Sie dies hinzufügen, wird Ihre Aktivität bei Konfigurationsänderungen nicht neu geladen. Wenn Sie bei Änderungen der Ausrichtung einige Änderungen vornehmen möchten, überschreiben Sie einfach die folgende Methode der Aktivität:

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

        //your code here
    }
Frane Poljak
quelle
o, ... wird diese Methode aufgerufen, sobald sich die Ausrichtung des Geräts geändert hat? @FranePoljak ... ic, ic, ...
gumuruh
1

Die Aktivität wird bei einem Orientierungswechsel neu erstellt, ja, das stimmt. Sie können Ihre Asynctask jedoch fortsetzen, wenn dieses Ereignis eintritt.

Sie überprüfen es auf

@Override
protected void onCreate(Bundle savedInstanceState) { 
     if ( savedInstanceState == null ) {
           startAsyncTask()
     } else {
           // ** Do Nothing async task will just continue.
     }
}

-Prost

ralphgabb
quelle
-1

Aus MVC- Sicht ist Aktivität der Controller . Es ist falsch, dass der Controller Vorgänge ausführt, die die Ansicht überleben (abgeleitet von android.view.View, normalerweise verwenden Sie nur die vorhandenen Klassen wieder). Daher sollte es in der Verantwortung des Modells liegen , AsyncTasks zu starten.

18446744073709551615
quelle
-4

Sie können verwenden class MagicAppRestartvon diesem Beitrag zu dem Prozess zu töten zusammen mit allen AsyncTasks; Android stellt den Aktivitätsstapel wieder her (der Benutzer erwähnt nichts). Es ist wichtig zu beachten, dass die einzige Benachrichtigung vor einem Prozessneustart der Aufruf ist onPause(). Gemäß der Android-App-Lebenszykluslogik muss Ihre Anwendung ohnehin für eine solche Beendigung bereit sein.

Ich habe es versucht und es scheint zu funktionieren. Dennoch plane ich im Moment, "zivilisiertere" Methoden wie schwache Referenzen aus der Application-Klasse zu verwenden (meine AsyncTasks sind eher kurzlebig und hoffentlich nicht so speicherintensiv).

Hier ist ein Code, mit dem Sie spielen können:

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

Den Rest hat Eclipse für ein neues Android-Projekt für com.xyz.AsyncTaskTestActivity erstellt :

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>

und ein relevanter Teil der Protokolle (beachten Sie, dass nur onPauseaufgerufen wird ):

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
18446744073709551615
quelle
Beantwortet im Jahr 2013 (die Zeiten von Android 2.x), im Jahr 2017 herabgestuft (die Zeiten von Android 6 und 7).
Seitdem