AsyncTask действительно концептуально ошибочен или я просто что-то упустил?

264

Я уже несколько месяцев исследую эту проблему, придумаю разные решения, которые меня не устраивают, так как все они массивные хаки. Я до сих пор не могу поверить, что класс с ошибками в дизайне превратил его в фреймворк, и никто об этом не говорит, так что, наверное, я что-то упустил.

Проблема с AsyncTask. Согласно документации это

«позволяет выполнять фоновые операции и публиковать результаты в потоке пользовательского интерфейса без необходимости манипулировать потоками и / или обработчиками».

Затем пример продолжает показывать, как showDialog()вызывается какой-то примерный метод onPostExecute(). Это, однако, кажется мне полностью надуманным , потому что для отображения диалога всегда нужна ссылка на допустимое Context, а AsyncTask никогда не должен содержать сильную ссылку на объект контекста .

Причина очевидна: что, если действие будет уничтожено, что вызвало задачу? Это может происходить постоянно, например, потому что вы перевернули экран. Если задача будет содержать ссылку на контекст, который ее создал, вы не только держитесь за бесполезный объект контекста (окно будет разрушено и любое взаимодействие с пользовательским интерфейсом завершится ошибкой!), Вы даже рискуете создать утечка памяти.

Если моя логика здесь не лишена недостатков, это будет означать: onPostExecute()совершенно бесполезно, потому что какая польза для этого метода в потоке пользовательского интерфейса, если у вас нет доступа к какому-либо контексту? Вы не можете сделать ничего значимого здесь.

Одним из обходных путей может быть не передача экземпляров контекста в AsyncTask, а Handlerэкземпляр. Это работает: поскольку обработчик свободно связывает контекст и задачу, вы можете обмениваться сообщениями между ними, не рискуя утечкой (верно?). Но это будет означать, что предпосылка AsyncTask, а именно то, что вам не нужно беспокоиться о обработчиках, неверна. Это также похоже на злоупотребление Handler, поскольку вы отправляете и получаете сообщения в одном потоке (вы создаете его в потоке пользовательского интерфейса и отправляете через него в onPostExecute (), который также выполняется в потоке пользовательского интерфейса).

Чтобы завершить все это, даже с этим обходным приемом, у вас все еще есть проблема, что, когда контекст разрушается, у вас нет записи о задачах, которые он выполнял. Это означает, что вам нужно перезапускать любые задачи при воссоздании контекста, например, после изменения ориентации экрана. Это медленно и расточительно.

Мое решение этого (как реализовано в библиотеке Droid-Fu ) состоит в том, чтобы поддерживать отображение WeakReferences из имен компонентов в их текущие экземпляры в уникальном объекте приложения. Всякий раз, когда AsyncTask запускается, он записывает вызывающий контекст в этой карте и при каждом обратном вызове извлекает текущий экземпляр контекста из этого сопоставления. Это гарантирует, что вы никогда не будете ссылаться на экземпляр устаревшего контекста, и у вас всегда будет доступ к действительному контексту в обратных вызовах, чтобы вы могли выполнять там значимую работу пользовательского интерфейса. Он также не пропускает, потому что ссылки слабые и очищаются, когда больше нет экземпляров данного компонента.

Тем не менее, это сложный обходной путь и требует подкласса некоторых классов библиотеки Droid-Fu, что делает этот подход довольно навязчивым.

Теперь я просто хочу знать: я просто что-то упускаю или AsyncTask действительно полностью испорчен? Как ваш опыт работы с ним? Как вы решили эту проблему?

Спасибо за ваш вклад.

Матиас
источник
1
Если вам интересно, мы недавно добавили в базовую библиотеку зажигания класс IgnitedAsyncTask, который добавляет поддержку безопасного доступа к типу контекста во всех обратных вызовах, используя шаблон подключения / отключения, описанный Дайанной ниже. Это также позволяет генерировать исключения и обрабатывать их в отдельном обратном вызове. См. Github.com/kaeppler/ignition-core/blob/master/src/com/github/…
Матиас
взгляните на это: gist.github.com/1393552
Матиас
1
Этот вопрос также связан.
Алекс Локвуд
Я добавляю асинхронные задачи к массиву и обязательно закрываю их все в определенный момент.
NightSkyCode

Ответы:

86

Как насчет чего-то вроде этого:

class MyActivity extends Activity {
    Worker mWorker;

    static class Worker extends AsyncTask<URL, Integer, Long> {
        MyActivity mActivity;

        Worker(MyActivity activity) {
            mActivity = activity;
        }

        @Override
        protected Long doInBackground(URL... urls) {
            int count = urls.length;
            long totalSize = 0;
            for (int i = 0; i < count; i++) {
                totalSize += Downloader.downloadFile(urls[i]);
                publishProgress((int) ((i / (float) count) * 100));
            }
            return totalSize;
        }

        @Override
        protected void onProgressUpdate(Integer... progress) {
            if (mActivity != null) {
                mActivity.setProgressPercent(progress[0]);
            }
        }

        @Override
        protected void onPostExecute(Long result) {
            if (mActivity != null) {
                mActivity.showDialog("Downloaded " + result + " bytes");
            }
        }
    }

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

        mWorker = (Worker)getLastNonConfigurationInstance();
        if (mWorker != null) {
            mWorker.mActivity = this;
        }

        ...
    }

    @Override
    public Object onRetainNonConfigurationInstance() {
        return mWorker;
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (mWorker != null) {
            mWorker.mActivity = null;
        }
    }

    void startWork() {
        mWorker = new Worker(this);
        mWorker.execute(...);
    }
}
hackbod
источник
5
Да, mActivity будет! = Null, но если нет ссылок на ваш экземпляр Worker, то любые ссылки на этот экземпляр также будут подвергаться удалению мусора. Если ваша задача выполняется вечно, то у вас все равно есть утечка памяти (ваша задача) - не говоря уже о том, что вы разряжаете батарею телефона. Кроме того, как упоминалось в другом месте, вы можете установить mActivity в nuD в onDestroy.
EboMike
13
Метод onDestroy () устанавливает для mActivity значение null. Неважно, кто имеет ссылку на действие до этого, потому что оно все еще выполняется. И окно активности всегда будет действительным, пока не будет вызвана onDestroy (). Установив значение null, асинхронная задача узнает, что действие больше не является допустимым. (И когда конфиг изменяется, вызывается onDestroy () предыдущего действия, а onCreate () следующего запускается без обработки сообщений в главном цикле между ними, поэтому AsyncTask никогда не увидит противоречивое состояние.)
hackbod
8
правда, но это все еще не решает последнюю проблему, которую я упомянул: представьте, что задача загружает что-то из Интернета. Используя этот подход, если вы перевернете экран 3 раза во время выполнения задачи, он будет перезапущен при каждом повороте экрана, и каждая задача, кроме последней, выбрасывает свой результат, потому что ее ссылка на активность равна нулю.
Матиас
11
Для доступа в фоновом режиме, вы либо должны поставить соответствующую синхронизацию вокруг mActivity и сделки с запуском в то время , когда она равна нулю, или фоновый поток просто взять Context.getApplicationContext () , который представляет собой единый глобальный экземпляр для приложения. Контекст приложения ограничен в том, что вы можете сделать (например, никакой пользовательский интерфейс, такой как Dialog), и требует некоторой осторожности (зарегистрированные получатели и привязки служб останутся навсегда, если вы их не очистите), но в целом подходит для кода, который не привязаны к контексту конкретного компонента.
hackbod
4
Это было невероятно полезно, спасибо Дайан! Я хотел бы, чтобы документация была так же хороша в первую очередь.
Матиас
20

Причина очевидна: что, если действие будет уничтожено, что вызвало задачу?

Вручную отсоединить деятельность от AsyncTaskв onDestroy(). Вручную повторно связать новую активность на AsyncTaskв onCreate(). Для этого требуется либо статический внутренний класс, либо стандартный класс Java, а также, возможно, 10 строк кода.

CommonsWare
источник
Будьте осторожны со статическими ссылками - я видел объекты, собираемые мусором, даже если на них были статические ссылки. Может быть, побочный эффект загрузчика классов Android или даже ошибка, но статические ссылки не являются безопасным способом обмена состояниями в течение жизненного цикла активности. Объект приложения, однако, поэтому я использую это.
Матиас
10
@Matthias: я не говорил использовать статические ссылки. Я сказал использовать статический внутренний класс. Существует существенная разница, несмотря на то, что оба имеют «статические» в своих именах.
CommonsWare
5
Я вижу - они ключевой здесь getLastNonConfigurationInstance (), а не статический внутренний класс, хотя. Статический внутренний класс не содержит неявной ссылки на свой внешний класс, поэтому он семантически эквивалентен простому общедоступному классу. Просто предупреждение: onRetainNonConfigurationInstance () НЕ гарантированно вызывается, когда действие прерывается (прерывание также может быть телефонным звонком), поэтому вам придется также посылать свою задачу в onSaveInstanceState () для действительно надежного решение. Но все равно хорошая идея.
Матиас
7
Гм ... onRetainNonConfigurationInstance () всегда вызывается, когда действие находится в процессе уничтожения и повторного создания. Нет смысла звонить в другое время. Если происходит переключение на другое действие, текущее действие приостанавливается / останавливается, но не уничтожается, поэтому асинхронная задача может продолжать выполняться и использовать тот же экземпляр действия. Если он заканчивается и, скажем, отображает диалоговое окно, диалоговое окно будет правильно отображаться как часть этого действия и, следовательно, не будет отображаться пользователю, пока они не вернутся к действию. Вы не можете поместить AsyncTask в Bundle.
hackbod
15

Похоже, AsyncTaskэто нечто большее, чем просто концептуальные недостатки . Это также невозможно использовать из-за проблем совместимости. Документы для Android гласят:

При первом представлении AsyncTasks выполнялись последовательно в одном фоновом потоке. Начиная с DONUT, это было изменено на пул потоков, позволяющий нескольким задачам работать параллельно. При запуске HONEYCOMB задачи возвращаются к выполнению в одном потоке, чтобы избежать распространенных ошибок приложения, вызванных параллельным выполнением. Если вы действительно хотите параллельное выполнение, вы можете использовать executeOnExecutor(Executor, Params...) версию этого метода с THREAD_POOL_EXECUTOR ; однако, см. комментарий там для предупреждений относительно его использования.

Оба executeOnExecutor()и THREAD_POOL_EXECUTORявляются Добавлено в уровне API 11 (Android 3.0.x, Honeycomb).

Это означает, что если вы создадите два параметра AsyncTasks для загрузки двух файлов, вторая загрузка не начнется, пока не закончится первый. Если вы общаетесь через два сервера, а первый сервер не работает, вы не сможете подключиться ко второму, пока не истечет время подключения к первому. (Конечно, если вы не используете новые функции API11, но это сделает ваш код несовместимым с 2.x).

И если вы хотите нацелиться на 2.x и 3.0+, материал становится действительно сложным.

Кроме того, документы говорят:

Предостережение. Другая проблема, с которой вы можете столкнуться при использовании рабочего потока, - непредвиденные перезапуски в вашей деятельности из-за изменения конфигурации среды выполнения (например, когда пользователь меняет ориентацию экрана), что может разрушить ваш рабочий поток . Чтобы увидеть, как вы можете сохранить свою задачу во время одного из этих перезапусков и как правильно отменить задачу, когда действие уничтожено, см. Исходный код примера приложения Shelves.

18446744073709551615
источник
12

Вероятно, мы все, включая Google, злоупотребляем AsyncTaskс точки зрения MVC .

Activity - это Controller , и контроллер не должен запускать операции, которые могут пережить View . То есть AsyncTasks следует использовать из Model , из класса, который не связан с жизненным циклом Activity - помните, что Activity уничтожаются при ротации. (Что касается View , вы обычно не программируете классы, производные от, например, android.widget.Button, но можете. Обычно единственное, что вы делаете в View - это xml.)

Другими словами, неправильно размещать производные AsyncTask в методах Деятельности. OTOH, если мы не должны использовать AsyncTasks в Деятельности, AsyncTask теряет свою привлекательность: раньше его рекламировали как быстрое и простое решение.

18446744073709551615
источник
5

Я не уверен, что это правда, что вы рискуете утечкой памяти со ссылкой на контекст из AsyncTask.

Обычный способ их реализации - создание нового экземпляра AsyncTask в рамках одного из методов Activity. Таким образом, если действие уничтожено, то, как только AsyncTask завершит свою работу, не будет ли он недоступен и пригоден для сбора мусора? Таким образом, ссылка на действие не имеет значения, потому что сама AsyncTask не будет зависать.

оли
источник
2
верно - но что, если задача блокируется на неопределенный срок? Задачи предназначены для выполнения операций блокировки, возможно, даже тех, которые никогда не завершаются. Там у вас утечка памяти.
Матиас
1
Любой работник, который выполняет что-то в бесконечном цикле, или что-то, что просто блокируется, например, при операции ввода-вывода.
Матиас
2

Было бы надежнее сохранить WeekReference в вашей деятельности:

public class WeakReferenceAsyncTaskTestActivity extends Activity {
    private static final int MAX_COUNT = 100;

    private ProgressBar progressBar;

    private AsyncTaskCounter mWorker;

    @SuppressWarnings("deprecation")
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_async_task_test);

        mWorker = (AsyncTaskCounter) getLastNonConfigurationInstance();
        if (mWorker != null) {
            mWorker.mActivity = new WeakReference<WeakReferenceAsyncTaskTestActivity>(this);
        }

        progressBar = (ProgressBar) findViewById(R.id.progressBar1);
        progressBar.setMax(MAX_COUNT);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.activity_async_task_test, menu);
        return true;
    }

    public void onStartButtonClick(View v) {
        startWork();
    }

    @Override
    public Object onRetainNonConfigurationInstance() {
        return mWorker;
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (mWorker != null) {
            mWorker.mActivity = null;
        }
    }

    void startWork() {
        mWorker = new AsyncTaskCounter(this);
        mWorker.execute();
    }

    static class AsyncTaskCounter extends AsyncTask<Void, Integer, Void> {
        WeakReference<WeakReferenceAsyncTaskTestActivity> mActivity;

        AsyncTaskCounter(WeakReferenceAsyncTaskTestActivity activity) {
            mActivity = new WeakReference<WeakReferenceAsyncTaskTestActivity>(activity);
        }

        private static final int SLEEP_TIME = 200;

        @Override
        protected Void doInBackground(Void... params) {
            for (int i = 0; i < MAX_COUNT; i++) {
                try {
                    Thread.sleep(SLEEP_TIME);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                Log.d(getClass().getSimpleName(), "Progress value is " + i);
                Log.d(getClass().getSimpleName(), "getActivity is " + mActivity);
                Log.d(getClass().getSimpleName(), "this is " + this);

                publishProgress(i);
            }
            return null;
        }

        @Override
        protected void onProgressUpdate(Integer... values) {
            super.onProgressUpdate(values);
            if (mActivity != null) {
                mActivity.get().progressBar.setProgress(values[0]);
            }
        }
    }

}
Snicolas
источник
Это похоже на то, что мы впервые сделали с Droid-Fu. Мы сохраняли бы карту слабых ссылок на объекты контекста и выполняли бы поиск в обратных вызовах задачи, чтобы получить самую последнюю ссылку (если она доступна) для выполнения обратного вызова. Наш подход, однако, означал, что была одна сущность, которая поддерживала это отображение, а ваш подход - нет, так что это действительно лучше.
Матиас
1
Вы смотрели на RoboSpice? github.com/octo-online/robospice . Я верю, что эта система еще лучше.
Сниколас
Пример кода на первой странице выглядит как утечка контекстной ссылки (внутренний класс сохраняет неявную ссылку на внешний класс.) Не уверен !!
Матиас
@Matthias, вы правы, поэтому я предлагаю статический внутренний класс, который будет содержать WeakReference для Activity.
Сниколас
1
@Matthias, я считаю, что это начинает быть не по теме. Но загрузчики не обеспечивают кэширование "из коробки", как мы, более того, загрузчики, как правило, более многословны, чем наша библиотека. На самом деле они хорошо справляются с курсорами, но для работы в сети лучше подходит другой подход, основанный на кэшировании и сервисе. См. Neilgoodman.net/2011/12/26/… часть 1 и 2
Сниколас
1

Почему бы просто не переопределить onPause()метод в собственном Activity и отменить его AsyncTaskоттуда?

Джефф Аксельрод
источник
это зависит от того, что делает эта задача. если он просто загружает / читает некоторые данные, то все будет в порядке. но если это изменит состояние некоторых данных на удаленном сервере, мы бы предпочли дать задаче возможность работать до конца.
Вит Худенко
@ Архимед, и я так понимаю, если ты поддерживаешь поток пользовательского интерфейса в onPauseтом смысле, что он так же плох, как и в другом месте? Т.е. вы могли бы получить ANR?
Джефф Аксельрод
именно. мы не можем заблокировать поток пользовательского интерфейса (будь то onPauseили другое), потому что мы рискуем получить ANR.
Вит Худенко
1

Вы абсолютно правы - поэтому движение от использования асинхронных задач / загрузчиков в действиях для извлечения данных набирает обороты. Одним из новых способов является использование инфраструктуры Volley, которая, по сути, обеспечивает обратный вызов после того, как данные будут готовы - гораздо более совместимый с моделью MVC. Залп был популяризирован в Google I / O 2013. Не уверен, почему больше людей не знают об этом.

C0D3LIC1OU5
источник
спасибо за это ... я собираюсь разобраться в этом ... моя причина неприязни к AsyncTask заключается в том, что он заставляет меня застрять с одним набором инструкций на PostExecute ... если я не взламываю его как использование интерфейсов или переопределение каждый раз Мне это надо.
carinlynchin
0

Лично я просто расширяю Thread и использую интерфейс обратного вызова для обновления пользовательского интерфейса. Я никогда не мог заставить AsyncTask работать правильно без проблем с FC. Я также использую неблокирующую очередь для управления пулом выполнения.

androidworkz
источник
1
Что ж, ваше принудительное закрытие было, вероятно, из-за проблемы, о которой я упоминал: вы пытались ссылаться на контекст, который вышел из области видимости (т. Е. Его окно было уничтожено), что приведет к исключению фреймворка.
Матиас
Нет ... на самом деле это произошло потому, что очередь отстой встроена в AsyncTask. Я всегда использую getApplicationContext (). У меня нет проблем с AsyncTask, если это всего лишь несколько операций ... но я пишу медиаплеер, который обновляет обложки альбомов в фоновом режиме ... в моем тесте у меня 120 альбомов без графики ... так что пока мое приложение не закрывалось полностью, asynctask выдавала ошибки ... поэтому вместо этого я создал одноэлементный класс с очередью, которая управляет процессами, и пока он отлично работает.
androidworkz
0

Я думал, что отмена работает, но это не так.

вот они RTFMing об этом:

«Если задача уже запущена, параметр mayInterruptIfRunning определяет, следует ли прерывать поток, выполняющий эту задачу, в попытке остановить задачу».

Однако это не означает, что поток прерывается. Это Java, а не AsyncTask ".

http://groups.google.com/group/android-developers/browse_thread/thread/dcadb1bc7705f1bb/add136eb4949359d?show_docid=add136eb4949359d

NIR
источник
0

Лучше было бы думать об AsyncTask как о чем-то, что более тесно связано с Activity, Context, ContextWrapper и т. Д. Удобнее, когда его область действия полностью понятна.

Убедитесь, что у вас есть политика отмены в вашем жизненном цикле, так что в конечном итоге она будет собирать мусор и больше не будет хранить ссылку на вашу деятельность, и это тоже может быть сборщиком мусора.

Не отменяя вашу AsyncTask при обходе вашего контекста, вы столкнетесь с утечками памяти и NullPointerExceptions, если вам просто нужно предоставить отзыв, такой как простое диалоговое окно Toast, то единственный контекст вашего Application Context поможет избежать проблемы NPE.

AsyncTask не так уж и плох, но определенно происходит много магии, которая может привести к непредвиденным ошибкам.

jtuchek
источник
-1

Что касается «опыта работы с ним»: это возможно , чтобы убить процесс вместе со всеми AsyncTasks, Android будет заново создать стек активности , так что пользователь не будет ничего упоминать.

18446744073709551615
источник