Предупреждение: этот класс AsyncTask должен быть статическим, иначе могут возникнуть утечки

270

Я получаю предупреждение в своем коде, в котором говорится:

Этот класс AsyncTask должен быть статическим, иначе могут возникнуть утечки (анонимный android.os.AsyncTask)

Полное предупреждение:

Этот класс AsyncTask должен быть статическим, иначе могут возникнуть утечки (анонимный android.os.AsyncTask). Статическое поле будет пропускать контексты. Нестатические внутренние классы имеют неявную ссылку на свой внешний класс. Если этот внешний класс является, например, Fragment или Activity, то эта ссылка означает, что долго выполняющийся обработчик / загрузчик / задача будет содержать ссылку на операцию, которая не позволяет собирать мусор. Точно так же прямые полевые ссылки на действия и фрагменты из этих более длительных экземпляров могут вызвать утечки. Классы ViewModel никогда не должны указывать на представления или контексты вне приложения.

Это мой код:

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

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

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

            return null;
        }
    }.execute();

Как мне это исправить?

Кейур Нимават
источник
2
Чтение этого androiddesignpatterns.com/2013/01/… должно дать вам подсказку, почему оно должно быть статичным
Рагхунандан,
До сих пор мне всегда удавалось заменить AsyncTask новым Thread (...). Statr () в сочетании с runOnUiThread (...) при необходимости, поэтому мне больше не нужно иметь дело с этим предупреждением.
Гонконг
1
Какое решение в kotlin для этой проблемы?
TapanHP
Пожалуйста, пересмотрите, какой ответ должен быть принятым. Смотрите ответы ниже.
Ωmega
В моем случае я получаю это предупреждение от Singleton, который не имеет прямых ссылок на Activity (он получает выходные данные myActivity.getApplication()в закрытый конструктор для Singleton, чтобы инициализировать классы RoomDB и другие классы). Мои ViewModels получают экземпляр Singleton как частную ссылку для выполнения некоторых операций с БД. Итак, ViewModels импортирует пакет Singleton, а также android.app.Applicationодин из них android.app.Activity. Поскольку «Синглтону» не нужно импортировать эти ViewModel для работы, даже в этом случае могут возникнуть утечки памяти?
SebasSBM

Ответы:

65

Нестатические внутренние классы содержат ссылку на содержащий класс. Когда вы объявляетеAsyncTask как внутренний класс, он может жить дольше, чем содержащий Activityкласс. Это из-за неявной ссылки на содержащий класс. Это предотвратит сбор мусора, что приведет к утечке памяти.

Чтобы решить вашу проблему, используйте статический вложенный класс вместо анонимного, локального и внутреннего класса или класс верхнего уровня.

Ананд
источник
1
Решение заключается в самом предупреждении. Либо используйте статический вложенный класс, либо класс верхнего уровня.
Ананд
3
@KeyurNimavat Я думаю, вы можете сослаться на слабую ссылку на вашу деятельность
peterchaula
42
так какой смысл использовать AsyncTask? если проще запустить новый Thread и handler.post или view.post (для обновления пользовательского интерфейса) в конце в методе выполнения Thread. Если AsyncTask является статическим классом или классом верхнего уровня, тогда трудно получить доступ к необходимым переменным / методам из него
user924
8
не предоставлено никакого кода того, как правильно его использовать. Я когда-либо пытался поставить статический там, но будет больше предупреждений и ошибок появится
Kasnady
19
@Anand Пожалуйста, удалите этот ответ, чтобы более полезный ответ на stackoverflow.com/a/46166223/145119 можно было найти сверху.
Миталду
557

Как использовать статический внутренний класс AsyncTask

Чтобы предотвратить утечки, вы можете сделать внутренний класс статическим. Однако проблема в том, что у вас больше нет доступа к представлениям пользовательского интерфейса Activity или переменным-членам. Вы можете передать ссылку на, Contextно тогда вы рискуете утечкой памяти. (Android не может собирать мусор после закрытия, если класс AsyncTask имеет сильную ссылку на него.) Решение состоит в том, чтобы сделать слабую ссылку на действие (или все, что Contextвам нужно).

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

Ноты

  • Насколько я знаю, этот тип утечки памяти всегда был правдой, но я только начал видеть предупреждение в Android Studio 3.0. Многие основные AsyncTaskучебники до сих пор с этим не справляются (см. Здесь , здесь , здесь и здесь ).
  • Вы также следовали бы аналогичной процедуре, если бы вы AsyncTaskбыли классом высшего уровня. Статический внутренний класс в основном такой же, как класс верхнего уровня в Java.
  • Если вам не нужна сама активность, но все же требуется контекст (например, для отображения a Toast), вы можете передать ссылку на контекст приложения. В этом случае AsyncTaskконструктор будет выглядеть так:

    private WeakReference<Application> appReference;
    
    MyTask(Application context) {
        appReference = new WeakReference<>(context);
    }
  • Есть некоторые аргументы для игнорирования этого предупреждения и просто использования нестатического класса. В конце концов, AsyncTask предназначен для очень короткого срока действия (самое большее пару секунд), и он все равно выпустит ссылку на Activity, когда все равно завершится. Смотрите это и это .
  • Отличная статья: Как вывести контекст: обработчики и внутренние классы

Котлин

В Kotlin просто не включайте innerключевое слово для внутреннего класса. Это делает его статическим по умолчанию.

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
источник
1
@ManojFrekzz, нет, на самом деле вы можете обновить пользовательский интерфейс, используя слабую ссылку на активность, которая была передана. Проверьте мой onPostExecuteметод еще раз в коде выше. Вы можете видеть, что я обновил пользовательский интерфейс TextViewтам. Просто используйте, activity.findViewByIdчтобы получить ссылку на любой элемент пользовательского интерфейса, который нужно обновить.
Сурагч
7
+1. Это лучшее и самое чистое решение, которое я когда-либо видел! Только в случае, если вы хотите изменить пользовательский интерфейс в методе onPostExecute, вы также должны проверить, уничтожается ли действие: activity.isFinishing ()
zapotec
1
Заметка! При использовании этого ответа я продолжал сталкиваться с исключениями пустых указателей, потому что операция doInBackground требовала большого объема памяти, запускала сборку мусора, собирала weakReference и уничтожала асинхронную задачу. Возможно, вы захотите использовать SoftReference вместо слабого, если вы знаете, что ваши фоновые операции требуют больше памяти.
PGMacDesign
2
@Sunny, передайте ссылку на фрагмент вместо действия. Вы бы вынули activity.isFinishing()чек и, возможно, заменили его fragment.isRemoving()чеком. Я не много работал с фрагментами в последнее время, хотя.
Сурагч
1
@bashan, (1) Если внешний класс не является Activity, то в вашем AsyncTaskконструкторе вы передаете ссылку на ваш внешний класс. И doInBackground()вы можете получить ссылку на внешний класс с MyOuterClass ref = classReference.get(). Проверьте для null. (2) У onPostExecute()вас обновляется только пользовательский интерфейс с результатами фоновой задачи. Как и в любой другой раз, вы обновляете пользовательский интерфейс. Проверка activity.isFinishing()состоит только в том, чтобы убедиться, что действие еще не начало завершаться, и в этом случае было бы бессмысленно обновлять пользовательский интерфейс.
Сураг
23

Этот AsyncTaskкласс должен быть статическим, иначе могут возникнуть утечки

  • Когда Activityуничтожен, AsyncTask(оба staticили non-static) все еще работает
  • Если внутренним классом является класс non-static( AsyncTask), он будет иметь ссылку на внешний класс ( Activity).
  • Если объект не имеет ссылок на него, он Garbage Collectedбудет освобожден. Если объект не используется и Garbage Collected не может его освободить => утечка памяти

=> Если AsyncTaskесть non-static, Activityне выпустит событие, оно уничтожено => утечка

Решение для обновления пользовательского интерфейса после создания AsyncTask как статический класс без утечки

1) Используйте WeakReferenceкак @Suragch ответ
2) Отправить и удалить Activityссылку на (из)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);
        }
    }
}
Фан Ван Линь
источник
5
@ Сурач по вашей ссылке гласит, что хотя onDestroy не гарантированно вызывается, единственная ситуация, в которой он не вызывается, - это когда система убивает процесс, поэтому все ресурсы в любом случае освобождаются. Так что не сохраняйте здесь, но вы можете сделать релиз ресурса здесь.
Анджело Фукс
2
В случае нестатического варианта использования AsyncTask, почему мы не можем просто установить переменную экземпляра AsyncTask в NULL, аналогично этому. разве это не скажет GC освободить Activity, хотя AsyncTask работает?
Ханиф
@Hanif Установка переменной экземпляра AsyncTask в NUL не поможет, потому что задача все еще имеет ссылку через слушателя.
Сюзанта