Ostrzeżenie: ta klasa AsyncTask powinna być statyczna, w przeciwnym razie mogą wystąpić wycieki

270

W moim kodzie pojawia się ostrzeżenie:

Ta klasa AsyncTask powinna być statyczna, w przeciwnym razie mogą wystąpić wycieki (anonimowy android.os.AsyncTask)

Pełne ostrzeżenie to:

Ta klasa AsyncTask powinna być statyczna, w przeciwnym razie mogą wystąpić wycieki (anonimowy android.os.AsyncTask) Pole statyczne spowoduje wyciek kontekstów. Niestatyczne klasy wewnętrzne mają niejawne odniesienie do swojej klasy zewnętrznej. Jeśli ta klasa zewnętrzna jest na przykład Fragmentem lub Działaniem, to odwołanie to oznacza, że ​​długo działający moduł obsługi / moduł ładujący / zadanie będzie zawierał odwołanie do działania, które uniemożliwi gromadzenie śmieci. Podobnie bezpośrednie odniesienia pól do działań i fragmentów z tych dłużej działających instancji mogą powodować wycieki. Klasy ViewModel nigdy nie powinny wskazywać na widoki lub konteksty nieaplikacyjne.

To jest mój kod:

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

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

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

            return null;
        }
    }.execute();

Jak to naprawić?

Keyur Nimavat
źródło
2
przeczytanie tego androiddesignpatterns.com/2013/01/… powinno dać ci wskazówkę, dlaczego powinno być statyczne
Raghunandan
Do tej pory zawsze byłem w stanie zastąpić AsyncTask nowym wątkiem (...). Statr () w połączeniu z runOnUiThread (...), jeśli to konieczne, więc nie muszę już radzić sobie z tym ostrzeżeniem.
Hong
1
Jakie jest rozwiązanie problemu w Kotlin?
TapanHP
Zastanów się, która odpowiedź powinna być zaakceptowana. Zobacz odpowiedzi poniżej.
Ωmega
W moim przypadku otrzymuję to ostrzeżenie od Singletona, który nie ma bezpośrednich odniesień do działania (otrzymuje dane wyjściowe myActivity.getApplication()do prywatnego konstruktora dla Singletona, w celu zainicjowania klas RoomDB i innych klas). Moje ViewModels pobierają instancję Singleton jako prywatne odwołanie do wykonywania niektórych operacji na DB. Tak więc ViewModels importuje pakiet Singleton, a android.app.Applicationnawet jeden z nich android.app.Activity. Ponieważ „Singleton” nie musi importować tych modeli ViewModels do działania, może jednak wystąpić przeciek pamięci?
SebasSBM,

Odpowiedzi:

64

Niestatyczne klasy wewnętrzne zawierają odniesienie do klasy zawierającej. Gdy deklarujesz AsyncTaskjako klasę wewnętrzną, może ona żyć dłużej niż Activityklasa zawierająca . Wynika to z niejawnego odwołania do zawierającej klasy. Zapobiegnie to gromadzeniu śmieci przez działanie, stąd wyciek pamięci.

Aby rozwiązać problem, użyj albo statycznej zagnieżdżonej klasy zamiast anonimowej, lokalnej i wewnętrznej lub użyj klasy najwyższego poziomu.

Anand
źródło
1
Rozwiązanie leży w samym ostrzeżeniu. Użyj albo statycznej klasy zagnieżdżonej, albo klasy najwyższego poziomu.
Anand
3
@KeyurNimavat Myślę, że możesz przekazać słabe odniesienie do swojej działalności
peterchaula,
42
więc po co używać AsyncTask? jeśli łatwiej jest uruchomić nowy wątek i handler.post lub view.post (w celu aktualizacji interfejsu użytkownika) na końcu w metodzie uruchamiania wątku. Jeśli AsyncTask jest statyczną lub najwyższą klasą, wówczas trudno jest uzyskać dostęp do wymaganych zmiennych / metod
user924
8
nie podano kodu wskazującego, w jaki sposób można z niego korzystać. Zawsze próbowałem umieścić tam statyczne, ale pojawi się więcej ostrzeżeń i błędów
Kasnady
19
@Anand Proszę usunąć tę odpowiedź, aby bardziej użyteczna odpowiedź na stackoverflow.com/a/46166223/145119 mogła znajdować się na górze.
Mithaldu,
555

Jak używać statycznej wewnętrznej klasy AsyncTask

Aby zapobiec wyciekom, możesz ustawić statyczną klasę wewnętrzną. Problem polega na tym, że nie masz już dostępu do widoków interfejsu użytkownika działania lub zmiennych składowych. Możesz przekazać odniesienie do, Contextale wtedy ryzykujesz przeciek pamięci. (Android nie może wyrzucić śmieci po zakończeniu działania, jeśli klasa AsyncTask ma do niego silne odniesienie). Rozwiązaniem jest słabe odwołanie do działania (lub cokolwiek, Contextczego potrzebujesz).

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

Notatki

  • O ile mi wiadomo, ten rodzaj wycieku pamięci zawsze był prawdziwy, ale zacząłem widzieć ostrzeżenie tylko w Android Studio 3.0. Wiele głównych AsyncTasksamouczków wciąż nie radzi sobie z tym (patrz tutaj , tutaj , tutaj i tutaj ).
  • Podobną procedurę zastosowałbyś również, gdybyś AsyncTaskbył na najwyższym poziomie. Statyczna klasa wewnętrzna jest zasadniczo taka sama jak klasa najwyższego poziomu w Javie.
  • Jeśli nie potrzebujesz samego działania, ale nadal chcesz kontekst (na przykład, aby wyświetlić a Toast), możesz przekazać odwołanie do kontekstu aplikacji. W tym przypadku AsyncTaskkonstruktor wyglądałby tak:

    private WeakReference<Application> appReference;
    
    MyTask(Application context) {
        appReference = new WeakReference<>(context);
    }
  • Istnieją pewne argumenty przemawiające za zignorowaniem tego ostrzeżenia i po prostu użyciem klasy niestatycznej. W końcu AsyncTask ma trwać bardzo krótko (najdłużej kilka sekund) i po zakończeniu opublikuje swoje odwołanie do działania. Zobacz to i to .
  • Doskonały artykuł: Jak wyciec kontekst: Handlery i klasy wewnętrzne

Kotlin

W Kotlin po prostu nie dołączaj innersłowa kluczowego dla klasy wewnętrznej. Domyślnie jest to statyczne.

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
źródło
1
@ManojFrekzz, No, rzeczywiście jesteś może zaktualizować UI poprzez wykorzystanie słabego odniesieniu do działalności, która została przyjęta w. Zobacz mój onPostExecutesposób ponownie w powyższym kodzie. Widać, że zaktualizowałem TextViewtam interfejs użytkownika . Wystarczy użyć, activity.findViewByIdaby uzyskać odniesienie do dowolnego elementu interfejsu użytkownika, który należy zaktualizować.
Suragch,
7
+1. To najlepsze i najczystsze rozwiązanie, jakie kiedykolwiek widziałem! Tylko, jeśli chcesz zmodyfikować interfejs użytkownika w metodzie onPostExecute, powinieneś również sprawdzić, czy działanie jest niszczone: activity.isFinishing ()
zapotec
1
Uwaga! Korzystając z tej odpowiedzi, ciągle napotykałem wyjątki zerowego wskaźnika, ponieważ operacja doInBackground wymagała dużej ilości pamięci, wyzwoliła odśmiecanie, zebrała słaby element referencyjny i zabiła asynchronię. Możesz użyć SoftReference zamiast słabego, jeśli wiesz, że operacje w tle wymagają więcej pamięci.
PGMacDesign
2
@Sunny, przekaż odwołanie do Fragmentu zamiast Aktywności. Wyjmiesz activity.isFinishing()czek i ewentualnie zastąpisz go fragment.isRemoving()czekiem. Jednak ostatnio nie pracowałem dużo z fragmentami.
Suragch,
1
@bashan, (1) Jeśli klasa zewnętrzna nie jest działaniem, to w AsyncTaskkonstruktorze przekazujesz odwołanie do swojej klasy zewnętrznej. I doInBackground()możesz uzyskać odniesienie do klasy zewnętrznej za pomocą MyOuterClass ref = classReference.get(). Sprawdź, czy nie null. (2) W onPostExecute()aktualizujesz tylko interfejs użytkownika o wyniki z zadania w tle. Podobnie jak w przypadku każdej innej aktualizacji interfejsu użytkownika. Sprawdzenie activity.isFinishing()polega na upewnieniu się, że działanie jeszcze się nie zakończyło, w takim przypadku aktualizacja interfejsu użytkownika byłaby bezcelowa.
Suragch
23

Ta AsyncTaskklasa powinna być statyczna, ponieważ mogą wystąpić przecieki

  • Kiedy Activityzostanie zniszczony AsyncTask(oba staticlubnon-static ) nadal działają
  • Jeśli klasa wewnętrzna jest klasą non-static( AsyncTask), będzie miała odwołanie do klasy zewnętrznej ( Activity).
  • Jeśli obiekt nie ma odniesień do niego, Garbage Collectedzwolni go. Jeśli obiekt nie jest używany i Garbage Collected nie można go zwolnić => pamięć wycieku

=> Jeśli AsyncTasktak non-static, Activitynie zwolni zdarzenia, które zostanie zniszczone => wyciek

Rozwiązanie do aktualizacji interfejsu użytkownika po ustawieniu AsyncTask jako klasy statycznej bez wycieku

1) Użyj WeakReferencejak @ Suragch odpowiedź
2) Wyślij i usuń Activityodniesienie do (z)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
źródło
5
@Suragch twój link stwierdza, że ​​chociaż nie można zagwarantować, że onDestroy zostanie wywołany, jedyną sytuacją, w której tak się nie stanie, jest zakończenie procesu przez system, więc wszystkie zasoby zostaną zwolnione. Więc nie rób tutaj zapisów, ale możesz tutaj wydać zasób.
Angelo Fuchs
2
W przypadku niestatycznego przypadku użycia AsyncTask, dlaczego nie możemy po prostu ustawić zmiennej instancji AsyncTask na NULL, podobnie do tej. czy to nie powie GC, by zwolniło Aktywność, mimo że AsyncTask jest uruchomiony?
Hanif
@Hanif ustawienie zmiennej instancji AsyncTask na NUL nie pomoże, ponieważ zadanie wciąż ma ref za pośrednictwem nasłuchiwania.
Susanta