Warnung: Diese AsyncTask-Klasse sollte statisch sein, da sonst Lecks auftreten können

270

In meinem Code wird eine Warnung angezeigt, die besagt:

Diese AsyncTask-Klasse sollte statisch sein, da sonst Lecks auftreten können (anonym android.os.AsyncTask)

Die vollständige Warnung lautet:

Diese AsyncTask-Klasse sollte statisch sein, da sonst Lecks auftreten können (anonym android.os.AsyncTask). Ein statisches Feld leckt Kontexte. Nicht statische innere Klassen haben einen impliziten Verweis auf ihre äußere Klasse. Wenn diese äußere Klasse beispielsweise ein Fragment oder eine Aktivität ist, bedeutet diese Referenz, dass der lang laufende Handler / Loader / Task einen Verweis auf die Aktivität enthält, der verhindert, dass Müll gesammelt wird. In ähnlicher Weise können direkte Feldverweise auf Aktivitäten und Fragmente dieser länger laufenden Instanzen zu Lecks führen. ViewModel-Klassen sollten niemals auf Ansichten oder Nichtanwendungskontexte verweisen.

Das ist mein Code:

 new AsyncTask<Void,Void,Void>(){

        @Override
        protected Void doInBackground(Void... params) {
            runOnUiThread(new Runnable() {

                @Override
                public void run() {
                    mAdapter.notifyDataSetChanged();
                }
            });

            return null;
        }
    }.execute();

Wie korrigiere ich das?

Keyur Nimavat
quelle
2
Das Lesen dieses androiddesignpatterns.com/2013/01/… sollte Ihnen einen Hinweis geben, warum es statisch sein sollte
Raghunandan
Bisher konnte ich AsyncTask immer durch einen neuen Thread (...) ersetzen. Statr () in Kombination mit runOnUiThread (...), falls erforderlich, sodass ich mich nicht mehr mit dieser Warnung befassen muss.
Hong
1
Was ist die Lösung in Kotlin für dieses Problem?
TapanHP
Bitte überdenken Sie, welche Antwort die akzeptierte sein sollte. Siehe Antworten unten.
Ωmega
In meinem Fall erhalte ich diese Warnung von einem Singleton, der keine direkten Verweise auf Activity hat (er empfängt die Ausgabe von myActivity.getApplication()in den privaten Konstruktor für den Singleton, um RoomDB-Klassen und andere Klassen zu initialisieren). Meine ViewModels erhalten die Singleton-Instanz als private Referenz, um einige Operationen an der Datenbank auszuführen. Die ViewModels importieren also das Singleton-Paket sowie android.app.Applicationeines davon sogar android.app.Activity. Da "the Singleton" diese ViewModels nicht importieren muss, um zu funktionieren, können dennoch Speicherlecks auftreten?
SebasSBM

Antworten:

64

Nicht statische innere Klassen enthalten einen Verweis auf die enthaltende Klasse. Wenn Sie AsyncTaskals innere Klasse deklarieren , lebt sie möglicherweise länger als die enthaltende ActivityKlasse. Dies liegt an der impliziten Bezugnahme auf die enthaltende Klasse. Dies verhindert, dass die Aktivität durch Müll gesammelt wird, daher der Speicherverlust.

Verwenden Sie zur Lösung Ihres Problems entweder eine statisch verschachtelte Klasse anstelle einer anonymen, lokalen und inneren Klasse oder eine Klasse der obersten Ebene.

Anand
quelle
1
Die Lösung liegt in der Warnung selbst. Verwenden Sie entweder eine statisch verschachtelte Klasse oder eine Klasse der obersten Ebene.
Anand
3
@ KeyurNimavat Ich denke, Sie können einen schwachen Hinweis auf Ihre Aktivität übergeben
Peterchaula
42
Was bringt es also, AsyncTask zu verwenden? Wenn es einfacher ist, einen neuen Thread und handler.post oder view.post (zum Aktualisieren der Benutzeroberfläche) am Ende der Ausführungsmethode von Thread auszuführen. Wenn AsyncTask eine statische Klasse oder eine Klasse der obersten Ebene ist, ist es schwierig, von dort aus auf die erforderlichen Variablen / Methoden
zuzugreifen
8
Es wird kein Code zur Verfügung gestellt, wie man ihn richtig benutzt. Ich habe jemals versucht, dort statisch zu setzen, aber es wird mehr Warnung und Fehler geben
Kasnady
19
@Anand Bitte löschen Sie diese Antwort, damit die nützlichere Antwort unter stackoverflow.com/a/46166223/145119 ganz oben steht.
Mithaldu
555

Verwendung einer statischen inneren AsyncTask-Klasse

Um Undichtigkeiten zu vermeiden, können Sie die innere Klasse statisch machen. Das Problem dabei ist jedoch, dass Sie keinen Zugriff mehr auf die Benutzeroberflächenansichten oder Mitgliedsvariablen der Aktivität haben. Sie können einen Verweis auf das übergeben, Contextaber dann besteht das gleiche Risiko eines Speicherverlusts. (Android kann die Aktivität nach dem Schließen nicht mit Müll sammeln, wenn die AsyncTask-Klasse einen starken Verweis darauf hat.) Die Lösung besteht darin, einen schwachen Verweis auf die Aktivität (oder was auch immer ContextSie benötigen) zu erstellen .

public class MyActivity extends AppCompatActivity {

    int mSomeMemberVariable = 123;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // start the AsyncTask, passing the Activity context
        // in to a custom constructor 
        new MyTask(this).execute();
    }

    private static class MyTask extends AsyncTask<Void, Void, String> {

        private WeakReference<MyActivity> activityReference;

        // only retain a weak reference to the activity 
        MyTask(MyActivity context) {
            activityReference = new WeakReference<>(context);
        }

        @Override
        protected String doInBackground(Void... params) {

            // do some long running task...

            return "task finished";
        }

        @Override
        protected void onPostExecute(String result) {

            // get a reference to the activity if it is still there
            MyActivity activity = activityReference.get();
            if (activity == null || activity.isFinishing()) return;

            // modify the activity's UI
            TextView textView = activity.findViewById(R.id.textview);
            textView.setText(result);

            // access Activity member variables
            activity.mSomeMemberVariable = 321;
        }
    }
}

Anmerkungen

  • Soweit ich weiß, war diese Art von Speicherverlustgefahr immer wahr, aber ich habe die Warnung erst in Android Studio 3.0 gesehen. Viele der wichtigsten AsyncTaskTutorials da draußen beschäftigen sich immer noch nicht damit (siehe hier , hier , hier und hier ).
  • Sie würden auch ein ähnliches Verfahren anwenden, wenn Sie AsyncTaskeine erstklassige Klasse wären . Eine statische innere Klasse ist im Grunde dieselbe wie eine Klasse der obersten Ebene in Java.
  • Wenn Sie die Aktivität selbst nicht benötigen, aber dennoch den Kontext möchten (z. B. um a anzuzeigen Toast), können Sie einen Verweis auf den App-Kontext übergeben. In diesem Fall würde der AsyncTaskKonstruktor folgendermaßen aussehen:

    private WeakReference<Application> appReference;
    
    MyTask(Application context) {
        appReference = new WeakReference<>(context);
    }
  • Es gibt einige Argumente dafür, diese Warnung zu ignorieren und nur die nicht statische Klasse zu verwenden. Immerhin soll die AsyncTask sehr kurzlebig sein (höchstens ein paar Sekunden), und sie wird ihren Verweis auf die Aktivität freigeben, wenn sie trotzdem beendet ist. Sehen Sie dies und das .
  • Ausgezeichneter Artikel: Wie man einen Kontext verliert: Handler & innere Klassen

Kotlin

Geben Sie in Kotlin einfach nicht das innerSchlüsselwort für die innere Klasse an. Dies macht es standardmäßig statisch.

class MyActivity : AppCompatActivity() {

    internal var mSomeMemberVariable = 123

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // start the AsyncTask, passing the Activity context
        // in to a custom constructor
        MyTask(this).execute()
    }

    private class MyTask
    internal constructor(context: MyActivity) : AsyncTask<Void, Void, String>() {

        private val activityReference: WeakReference<MyActivity> = WeakReference(context)

        override fun doInBackground(vararg params: Void): String {

            // do some long running task...

            return "task finished"
        }

        override fun onPostExecute(result: String) {

            // get a reference to the activity if it is still there
            val activity = activityReference.get()
            if (activity == null || activity.isFinishing) return

            // modify the activity's UI
            val textView = activity.findViewById(R.id.textview)
            textView.setText(result)

            // access Activity member variables
            activity.mSomeMemberVariable = 321
        }
    }
}
Suragch
quelle
1
@ManojFrekzz, Nein, eigentlich Sie können die Benutzeroberfläche aktualisieren , indem die Verwendung der schwachen Bezug auf die Aktivität zu machen , die in übergeben wurde. Schauen Sie sich meine onPostExecuteMethode wieder in dem obigen Code. Sie können sehen, dass ich dort die Benutzeroberfläche aktualisiert habe TextView. Verwenden Sie einfach activity.findViewById, um einen Verweis auf das zu aktualisierende UI-Element zu erhalten.
Suragch
7
+1. Dies ist die beste und sauberste Lösung, die ich je gesehen habe! Nur für den Fall, dass Sie die Benutzeroberfläche in der onPostExecute-Methode ändern möchten, sollten Sie auch überprüfen, ob die Aktivität zerstört wird: activity.isFinishing ()
zapotec
1
Hinweis! Bei Verwendung dieser Antwort stieß ich immer wieder auf Null-Zeiger-Ausnahmen, da die doInBackground-Operation speicherintensiv war, die Speicherbereinigung auslöste, die schwache Referenz sammelte und die Asynctask abbrach. Möglicherweise möchten Sie eine SoftReference anstelle einer schwachen verwenden, wenn Sie wissen, dass Ihre Hintergrundoperationen speicherintensiver sind.
PGMacDesign
2
@Sunny, geben Sie einen Verweis auf das Fragment anstelle der Aktivität ein. Sie würden den activity.isFinishing()Scheck herausnehmen und möglicherweise durch einen fragment.isRemoving()Scheck ersetzen . Ich habe in letzter Zeit jedoch nicht viel mit Fragmenten gearbeitet.
Suragch
1
@bashan, (1) Wenn die äußere Klasse keine Aktivität ist, übergeben Sie in Ihrem AsyncTaskKonstruktor einen Verweis auf Ihre äußere Klasse. Und in doInBackground()können Sie einen Verweis auf die äußere Klasse mit bekommen MyOuterClass ref = classReference.get(). Überprüfen Sie auf null. (2) In onPostExecute()aktualisieren Sie die Benutzeroberfläche nur mit den Ergebnissen der Hintergrundaufgabe. Es ist genau wie bei jedem anderen Mal, wenn Sie die Benutzeroberfläche aktualisieren. Die Überprüfung activity.isFinishing()besteht nur darin, sicherzustellen, dass die Aktivität noch nicht abgeschlossen ist. In diesem Fall wäre es sinnlos, die Benutzeroberfläche zu aktualisieren.
Suragch
23

Diese AsyncTaskKlasse sollte statisch sein, da sonst Lecks auftreten können

  • Wenn Activityzerstört wird, laufen AsyncTask(beide staticoder non-static) noch
  • Wenn die innere Klasse die Klasse non-static( AsyncTask) ist, verweist sie auf die äußere Klasse ( Activity).
  • Wenn auf ein Objekt kein Verweis verweist, Garbage Collectedwird es freigegeben. Wenn ein Objekt nicht verwendet wird und Garbage Collected es nicht freigeben kann => Speicherverlust

=> Wenn AsyncTaskja non-static, Activitywird das Ereignis nicht freigegeben, es wird zerstört => Leck

Lösung für die Aktualisierung der Benutzeroberfläche, nachdem AsyncTask als statische Klasse ohne Leck erstellt wurde

1) Verwenden Sie WeakReferencewie @ Suragch Antwort
2) Senden und entfernen Sie ActivityVerweise auf (von)AsyncTask

public class NoLeakAsyncTaskActivity extends AppCompatActivity {
    private ExampleAsyncTask asyncTask;

    @Override 
    protected void onCreate(Bundle savedInstanceState) {
        ...

        // START AsyncTask
        asyncTask = new ExampleAsyncTask();
        asyncTask.setListener(new ExampleAsyncTask.ExampleAsyncTaskListener() {
            @Override
            public void onExampleAsyncTaskFinished(Integer value) {
                // update UI in Activity here
            }
        });
        asyncTask.execute();
    }

    @Override
    protected void onDestroy() {
        asyncTask.setListener(null); // PREVENT LEAK AFTER ACTIVITY DESTROYED
        super.onDestroy();
    }

    static class ExampleAsyncTask extends AsyncTask<Void, Void, Integer> {
        private ExampleAsyncTaskListener listener;

        @Override
        protected Integer doInBackground(Void... voids) {
            ...
            return null;
        }

        @Override
        protected void onPostExecute(Integer value) {
            super.onPostExecute(value);
            if (listener != null) {
                listener.onExampleAsyncTaskFinished(value);
            }
        }

        public void setListener(ExampleAsyncTaskListener listener) {
            this.listener = listener;
        }

        public interface ExampleAsyncTaskListener {
            void onExampleAsyncTaskFinished(Integer value);
        }
    }
}
Phan Van Linh
quelle
5
@Suragch Ihr Link besagt, dass onDestroy zwar nicht garantiert aufgerufen werden kann, dies jedoch nicht der Fall ist, wenn das System den Prozess abbricht, sodass alle Ressourcen trotzdem freigegeben werden. Speichern Sie hier also nicht, aber Sie können hier eine Ressourcenfreigabe durchführen.
Angelo Fuchs
2
Im Fall eines nicht statischen AsyncTask-Anwendungsfalls können wir die AsyncTask-Instanzvariable nicht einfach auf NULL setzen, ähnlich wie in diesem Fall. Wird dies GC nicht anweisen, die Aktivität freizugeben, obwohl AsyncTask ausgeführt wird?
Hanif
@ Hanif Das Setzen der AsyncTask-Instanzvariablen auf NUL hilft nicht, da die Aufgabe weiterhin den Ref über den Listener hat.
Susanta