AsyncTask действительно концептуально несовершенен или я просто что-то упустил?
Я уже несколько месяцев исследую эту проблему, придумаю разные решения, которые меня не устраивают, так как все они массивные хаки. Я до сих пор не могу поверить, что класс с ошибками в дизайне превратил его в фреймворк, и никто об этом не говорит, так что, наверное, я что-то упустил.
Проблема с AsyncTask
, Согласно документации это
"позволяет выполнять фоновые операции и публиковать результаты в потоке пользовательского интерфейса без необходимости манипулировать потоками и / или обработчиками".
Затем пример продолжает показывать, как некоторые showDialog()
метод вызывается в onPostExecute()
, Это, однако, кажется мне полностью надуманным, потому что для отображения диалога всегда нужна ссылка на действительный Context
и AsyncTask никогда не должен содержать сильную ссылку на объект контекста.
Причина очевидна: что, если действие будет уничтожено, что вызвало задачу? Это может происходить постоянно, например, потому что вы перевернули экран. Если задача будет содержать ссылку на контекст, который ее создал, вы не только держитесь за бесполезный объект контекста (окно будет разрушено, а любое взаимодействие с пользовательским интерфейсом завершится ошибкой!), Вы даже рискуете создать утечка памяти.
Если моя логика здесь не ошибочна, это означает: onPostExecute()
совершенно бесполезно, потому что какой смысл запускать этот метод в потоке пользовательского интерфейса, если у вас нет доступа к какому-либо контексту? Вы не можете сделать ничего значимого здесь.
Одним из обходных путей было бы не передавать экземпляры контекста в AsyncTask, а Handler
пример. Это работает: так как обработчик свободно связывает контекст и задачу, вы можете обмениваться сообщениями между ними, не рискуя утечкой (верно?). Но это будет означать, что предпосылка AsyncTask, а именно то, что вам не нужно беспокоиться о обработчиках, неверна. Это также похоже на злоупотребление обработчиком, поскольку вы отправляете и получаете сообщения в одном потоке (вы создаете его в потоке пользовательского интерфейса и отправляете через него в onPostExecute(), который также выполняется в потоке пользовательского интерфейса).
Чтобы завершить все это, даже с этим обходным приемом, у вас все еще есть проблема, что, когда контекст разрушен, у вас нет записи о задачах, которые он выполнил. Это означает, что вам нужно перезапускать любые задачи при воссоздании контекста, например, после изменения ориентации экрана. Это медленно и расточительно.
Мое решение этого (как это реализовано в библиотеке Droid-Fu) состоит в том, чтобы сохранить отображение WeakReference
от имен компонентов до их текущих экземпляров в уникальном объекте приложения. Всякий раз, когда AsyncTask запускается, он записывает вызывающий контекст в этой карте и при каждом обратном вызове извлекает текущий экземпляр контекста из этого сопоставления. Это гарантирует, что вы никогда не будете ссылаться на экземпляр устаревшего контекста, и у вас всегда будет доступ к действительному контексту в обратных вызовах, чтобы вы могли выполнять там значимую работу пользовательского интерфейса. Он также не пропускает, потому что ссылки слабые и очищаются, когда больше не существует экземпляров данного компонента.
Тем не менее, это сложный обходной путь и требует подкласса некоторых классов библиотеки Droid-Fu, что делает этот подход довольно навязчивым.
Теперь я просто хочу знать: я просто что-то упускаю или AsyncTask действительно полностью испорчен? Как ваш опыт работы с ним? Как вы решили эту проблему?
Спасибо за ваш вклад.
12 ответов
Как насчет чего-то вроде этого:
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
в 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
Чтобы загрузить два файла, вторая загрузка не начнется, пока не закончится первый. Если вы общаетесь через два сервера, а первый сервер не работает, вы не сможете подключиться ко второму, пока не истечет время подключения к первому. (Если, конечно, вы не используете новые функции API11, но это сделает ваш код несовместимым с 2.x).
И если вы хотите нацелиться на 2.x и 3.0+, материал становится действительно сложным.
Кроме того, документы говорят:
Предостережение. Другая проблема, с которой вы можете столкнуться при использовании рабочего потока, - непредвиденные перезапуски в вашей деятельности из-за изменения конфигурации среды выполнения (например, когда пользователь меняет ориентацию экрана), что может привести к разрушению вашего рабочего потока. Чтобы увидеть, как вы можете сохранить свою задачу во время одного из этих перезапусков и как правильно отменить задачу, когда действие уничтожено, см. Исходный код примера приложения Shelves.
Вероятно, мы все, включая Google, злоупотребляем AsyncTask
с точки зрения MVC.
Activity - это Controller, и контроллер не должен запускать операции, которые могут пережить View. То есть AsyncTasks следует использовать из Model, из класса, который не связан с жизненным циклом Activity - помните, что Activity уничтожаются при ротации. (Что касается View, вы обычно не программируете классы, производные, например, от android.widget.Button, но можете. Обычно единственное, что вы делаете в View - это 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. Залп был популяризирован в Google I/O 2013. Не уверен, почему больше людей не знают об этом.
Лучше было бы думать об AsyncTask как о чем-то, что более тесно связано с Activity, Context, ContextWrapper и т. Д. Удобнее, когда его область действия полностью понятна.
Убедитесь, что у вас есть политика отмены в вашем жизненном цикле, так что в конечном итоге она будет собирать мусор и больше не будет хранить ссылку на вашу деятельность, и это тоже может быть сборщиком мусора.
Не отменяя вашу AsyncTask во время обхода вашего контекста, вы столкнетесь с утечками памяти и NullPointerExceptions, если вам просто нужно предоставить обратную связь, например, простое диалоговое окно Toast, тогда единственный контекст вашего Application Context поможет избежать проблемы NPE.
AsyncTask не так уж и плох, но определенно происходит много магии, которая может привести к непредвиденным ошибкам.
Я думал, что отмена работает, но это не так.
вот они RTFMing об этом:
"Если задача уже запущена, параметр mayInterruptIfRunning определяет, следует ли прерывать поток, выполняющий эту задачу, в попытке остановить задачу".
Это не означает, однако, что поток прерывается. Это Java, а не AsyncTask ".
Лично я просто расширяю Thread и использую интерфейс обратного вызова для обновления пользовательского интерфейса. Я никогда не мог заставить AsyncTask работать правильно без проблем с FC. Я также использую неблокирующую очередь для управления пулом выполнения.
Что касается "опыта работы с ним": возможно убить процесс вместе со всеми AsyncTasks, Android заново создаст стек активности, чтобы пользователь ничего не упоминал.