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

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

Проблема заключается в AsyncTask . Согласно документации

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

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

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

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

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

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

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

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

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

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

Solutions Collecting From Web of "Is AsyncTask действительно концептуально испорчен или я просто что-то пропустил?"

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

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

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

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

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

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

И executeOnExecutor() и THREAD_POOL_EXECUTOR добавлены в уровень API 11 (Android 3.0.x, HONEYCOMB).

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

И если вы хотите настроить таргетинг как на 2.x, так и на 3.0+, материал становится очень сложным.

Кроме того, в документах говорится:

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

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

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

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

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

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

Было бы более надежно держать 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]); } } } } 

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

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

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

Здесь они RTFMing об этом:

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

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

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

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

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

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

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

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

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