Изменение конфигурации AsyncTask и среды выполнения: какие подходы с краткими примерами кода одобряет команда Android?

Поскольку AsyncTask был представлен в Cupcake (API 3, Android 1.5) в В 2009 году команда Android постоянно продвигала его как простую вещь:

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

Однако за прошедшие с тех пор годы сбои, утечки памяти и другие проблемы преследовали большинство разработчиков, которые решили использовать AsyncTask в своих производственных приложениях. Часто это происходит из-за Activity разрушения и восстановления при изменении конфигурации среды выполнения (особенно ориентация / поворот) во время работы AsyncTask doInBackground(Params...); когда вызывается onPostExecute(Result), Activity уже был уничтожен, в результате чего ссылки пользовательского интерфейса остались в непригодном для использования состоянии (или даже null).

И отсутствие очевидных, четких и кратких рекомендаций и примеров кода от команды Android по этой проблеме только усугубило ситуацию, вызвав путаницу, а также различные обходные пути и хаки, некоторые приличные, некоторые ужасные:

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

Каковы канонические (одобренные командой Android) передовые методы с краткими примерами кода для интеграции AsyncTask с _12 _ / _ 13_ жизненным циклом и автоматического перезапуска при изменении конфигурации среды выполнения?




Ответы (1)


Предоставляется рекомендация (для любого подхода)

Не храните ссылки на объекты, специфичные для пользовательского интерфейса.

Из Память и потоки. (Шаблоны производительности Android, сезон 5, серия 3):

У вас есть объект потоковой передачи, который объявлен как внутренний класс _1 _. Проблема здесь в том, что объект AsyncTask теперь имеет неявную ссылку на включающий Activity, и будет хранить эту ссылку до тех пор, пока рабочий объект не будет уничтожен ... Пока эта работа не будет завершена, Activity остается в памяти ... Этот тип шаблона также приводит к обычным типам сбоев, наблюдаемых в приложениях Android ...

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

Предусмотренные подходы

Несмотря на то, что документация скудная и разрозненная, команда Android предложила как минимум три различных подхода к решению проблемы перезапуска при изменении конфигурации с помощью AsyncTask:

  1. Отменить / сохранить / перезапустить запущенную задачу в методах жизненного цикла
  2. Используйте WeakReferences для объектов пользовательского интерфейса
  3. Управляйте на верхнем уровне Activity или Fragment, используя "рабочие записи"

1. Отменить / сохранить / перезапустить запущенную задачу в методах жизненного цикла.

Из Использование AsyncTask | Процессы и потоки | Разработчики Android

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

В приложении Shelves ссылки на задачи поддерживаются в виде полей в _9 _, чтобы ими можно было управлять с помощью методов жизненного цикла Activity. Однако перед тем, как взглянуть на код, следует отметить несколько важных моментов.

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

Во-вторых, подклассы UserTask объявлены как внутренние классы Activity. Этот подход теперь рассматривается как анти-шаблон, как отмечалось ранее (см. Не хранить ссылки на объекты, специфичные для пользовательского интерфейса выше). К счастью, эта деталь реализации не влияет на общий подход к управлению запущенными задачами в методах жизненного цикла; однако, если вы решите использовать этот образец кода для своего собственного приложения, объявите свои подклассы AsyncTask в другом месте.

Отмена задачи

  • Переопределить onDestroy(), отменить задачи и установить ссылки на задачи до null. (Я не уверен, что установка ссылок на null имеет здесь какое-либо влияние; если у вас есть дополнительная информация, прокомментируйте, и я соответствующим образом обновлю ответ).

  • Переопределите AsyncTask#onCancelled(Object), если вам нужно очистить или выполнить любую другую необходимую работу после AsyncTask#doInBackground(Object[]) возвращается.

AddBookActivity.java
public class AddBookActivity extends Activity implements View.OnClickListener,
        AdapterView.OnItemClickListener {

    // ...

    private SearchTask mSearchTask;
    private AddTask mAddTask;

    // Tasks are initialized and executed when needed
    // ...

    @Override
    protected void onDestroy() {
        super.onDestroy();

        onCancelAdd();
        onCancelSearch();
    }

    // ...

    private void onCancelSearch() {
        if (mSearchTask != null && mSearchTask.getStatus() == UserTask.Status.RUNNING) {
            mSearchTask.cancel(true);
            mSearchTask = null;
        }
    }

    private void onCancelAdd() {
        if (mAddTask != null && mAddTask.getStatus() == UserTask.Status.RUNNING) {
            mAddTask.cancel(true);
            mAddTask = null;
        }
    }

    // ...

    // DO NOT DECLARE YOUR TASK AS AN INNER CLASS OF AN ACTIVITY
    // Instances of this class will hold an implicit reference to the enclosing
    // Activity as long as the task is running, even if the Activity has been
    // otherwise destroyed by the system.  Declare your task where you can be
    // sure it holds no implicit references to UI-specific objects (Views,
    // etc.), and do not hold explicit references to them in your own
    // implementation.
    private class AddTask extends UserTask<String, Void, BooksStore.Book> {
        // ...

        @Override
        public void onCancelled() {
            enableSearchPanel();
            hidePanel(mAddPanel, false);
        }

        // ...
    }

    // DO NOT DECLARE YOUR TASK AS AN INNER CLASS OF AN ACTIVITY
    // Instances of this class will hold an implicit reference to the enclosing
    // Activity as long as the task is running, even if the Activity has been
    // otherwise destroyed by the system.  Declare your task where you can be
    // sure it holds no implicit references to UI-specific objects (Views,
    // etc.), and do not hold explicit references to them in your own
    // implementation.
    private class SearchTask extends UserTask<String, ResultBook, Void>
            implements BooksStore.BookSearchListener {

        // ...

        @Override
        public void onCancelled() {
            enableSearchPanel();

            hidePanel(mSearchPanel, true);
        }

        // ...
    }

Сохранение и перезапуск задачи

AddBookActivity.java
public class AddBookActivity extends Activity implements View.OnClickListener,
        AdapterView.OnItemClickListener {

    // ...

    private static final String STATE_ADD_IN_PROGRESS = "shelves.add.inprogress";
    private static final String STATE_ADD_BOOK = "shelves.add.book";

    private static final String STATE_SEARCH_IN_PROGRESS = "shelves.search.inprogress";
    private static final String STATE_SEARCH_QUERY = "shelves.search.book";

    // ...

    @Override
    protected void onRestoreInstanceState(Bundle savedInstanceState) {
        super.onRestoreInstanceState(savedInstanceState);
        // ...
        restoreAddTask(savedInstanceState);
        restoreSearchTask(savedInstanceState);
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        if (isFinishing()) {
            // ...
            saveAddTask(outState);
            saveSearchTask(outState);
        }
    }

    // ...

    private void saveAddTask(Bundle outState) {
        final AddTask task = mAddTask;
        if (task != null && task.getStatus() != UserTask.Status.FINISHED) {
            final String bookId = task.getBookId();
            task.cancel(true);

            if (bookId != null) {
                outState.putBoolean(STATE_ADD_IN_PROGRESS, true);
                outState.putString(STATE_ADD_BOOK, bookId);
            }

            mAddTask = null;
        }
    }

    private void restoreAddTask(Bundle savedInstanceState) {
        if (savedInstanceState.getBoolean(STATE_ADD_IN_PROGRESS)) {
            final String id = savedInstanceState.getString(STATE_ADD_BOOK);
            if (!BooksManager.bookExists(getContentResolver(), id)) {
                mAddTask = (AddTask) new AddTask().execute(id);
            }
        }
    }

    private void saveSearchTask(Bundle outState) {
        final SearchTask task = mSearchTask;
        if (task != null && task.getStatus() != UserTask.Status.FINISHED) {
            final String bookId = task.getQuery();
            task.cancel(true);

            if (bookId != null) {
                outState.putBoolean(STATE_SEARCH_IN_PROGRESS, true);
                outState.putString(STATE_SEARCH_QUERY, bookId);
            }

            mSearchTask = null;
        }
    }

    private void restoreSearchTask(Bundle savedInstanceState) {
        if (savedInstanceState.getBoolean(STATE_SEARCH_IN_PROGRESS)) {
            final String query = savedInstanceState.getString(STATE_SEARCH_QUERY);
            if (!TextUtils.isEmpty(query)) {
                mSearchTask = (SearchTask) new SearchTask().execute(query);
            }
        }
    }

Это простой подход, который должен иметь смысл даже для новичков, которые только знакомятся с Activity жизненным циклом. Он также имеет то преимущество, что не требует минимального кода вне самого класса задачи, затрагивая от одного до трех методов жизненного цикла, в зависимости от потребностей. Простой 7-строчный onDestroy() фрагмент в разделе Использование AsyncTask javadoc Могли бы всех нас уберечь от горя. Возможно, какое-то будущее поколение будет пощадить.

2. Используйте WeakReferences для объектов пользовательского интерфейса.

  • Передайте объекты пользовательского интерфейса в качестве параметров в конструкторе AsyncTask. Храните слабые ссылки на эти объекты как поля WeakReference в AsyncTask.

  • В onPostExecute() убедитесь, что объекты пользовательского интерфейса WeakReferences не null, а затем обновите их напрямую.

Из Используйте AsyncTask | Обработка растровых изображений вне потока пользовательского интерфейса | Разработчики Android

class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
    private final WeakReference<ImageView> imageViewReference;
    private int data = 0;

    public BitmapWorkerTask(ImageView imageView) {
        // Use a WeakReference to ensure the ImageView can be garbage collected
        imageViewReference = new WeakReference<ImageView>(imageView);
    }

    // Decode image in background.
    @Override
    protected Bitmap doInBackground(Integer... params) {
        data = params[0];
        return decodeSampledBitmapFromResource(getResources(), data, 100, 100));
    }

    // Once complete, see if ImageView is still around and set bitmap.
    @Override
    protected void onPostExecute(Bitmap bitmap) {
        if (imageViewReference != null && bitmap != null) {
            final ImageView imageView = imageViewReference.get();
            if (imageView != null) {
                imageView.setImageBitmap(bitmap);
            }
        }
    }
}

WeakReference к _ 41_ гарантирует, что _ 42_ не предотвращает ImageView и все, на что он ссылается, из сборщика мусора. Нет гарантии, что ImageView все еще будет рядом, когда задача завершится, поэтому вы также необходимо проверить ссылку в onPostExecute(). ImageView может больше не существовать, если, например, пользователь уходит из действия или если изменение конфигурации произошло до завершения задачи.

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

Однако за эту простоту приходится платить: задача будет выполняться до конца без отмены при изменении конфигурации. Если ваша задача дорогая (процессор, память, батарея), имеет побочные эффекты или требует автоматического перезапуска на _ 47_ перезапустите, тогда первый подход, вероятно, будет лучшим вариантом.

3. Управляйте действием или фрагментом верхнего уровня, используя «рабочие записи».

Из Память и потоки. (Шаблоны производительности Android, сезон 5, серия 3)

... принудительно установить Activity верхнего уровня или _ 49_, чтобы быть единственной системой, ответственной за обновление объектов пользовательского интерфейса.

Например, если вы хотите начать какую-то работу, создайте «рабочую запись», которая объединяет _ 50_ с некоторой функцией обновления. Когда этот блок работы завершен, он отправляет результаты обратно в Activity с помощью Intent или _53 _ вызов.

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

Вот снимок экрана с прилагаемой схемой, описывающей этот подход:

Схема подхода к управлению потоками рабочих записей

Примеры кода не были представлены в видео, поэтому вот мой взгляд на базовую реализацию:

WorkRecord.java

public class WorkRecord {
  public static final String ACTION_UPDATE_VIEW = "WorkRecord.ACTION_UPDATE_VIEW";
  public static final String EXTRA_WORK_RECORD_KEY = "WorkRecord.EXTRA_WORK_RECORD_KEY";
  public static final String EXTRA_RESULT = "WorkRecord.EXTRA_RESULT";
  public final int viewId;
  public final Callback callback;

  public WorkRecord(@IdRes int viewId, Callback callback) {
    this.viewId = viewId;
    this.callback = callback;
  }

  public interface Callback {
    boolean update(View view, Object result);
  }

  public interface Store {
    long addWorkRecord(WorkRecord workRecord);
  }
}

MainActivity.java

public class MainActivity extends AppCompatActivity implements WorkRecord.Store {
  // ...

  private final Map<Long, WorkRecord> workRecords = new HashMap<>();
  private BroadcastReceiver workResultReceiver;

  // ...

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

    initWorkResultReceiver();
    registerWorkResultReceiver();
  }

  @Override protected void onDestroy() {
    super.onDestroy();
    // ...

    unregisterWorkResultReceiver();
  }

  // Initializations

  private void initWorkResultReceiver() {
      workResultReceiver = new BroadcastReceiver() {
        @Override public void onReceive(Context context, Intent intent) {
          doWorkWithResult(intent);
        }
      };
    }

  // Result receiver

  private void registerWorkResultReceiver() {
    final IntentFilter workResultFilter = new IntentFilter(WorkRecord.ACTION_UPDATE_VIEW);
    LocalBroadcastManager.getInstance(this).registerReceiver(workResultReceiver, workResultFilter);
  }

  private void unregisterWorkResultReceiver() {
    if (workResultReceiver != null) {
      LocalBroadcastManager.getInstance(this).unregisterReceiver(workResultReceiver);
    }
  }

  private void doWorkWithResult(Intent resultIntent) {
    final long key = resultIntent.getLongExtra(WorkRecord.EXTRA_WORK_RECORD_KEY, -1);
    if (key <= 0) {
      Log.w(TAG, "doWorkWithResult: WorkRecord key not found, exiting:"
          + " intent=" + resultIntent);
      return;
    }

    final Object result = resultIntent.getExtras().get(WorkRecord.EXTRA_RESULT);
    if (result == null) {
      Log.w(TAG, "doWorkWithResult: Result not found, exiting:"
          + " key=" + key
          + ", intent=" + resultIntent);
      return;
    }

    final WorkRecord workRecord = workRecords.get(key);
    if (workRecord == null) {
      Log.w(TAG, "doWorkWithResult: matching WorkRecord not found, exiting:"
          + " key=" + key
          + ", workRecords=" + workRecords
          + ", result=" + result);
      return;
    }

    final View viewToUpdate = findViewById(workRecord.viewId);
    if (viewToUpdate == null) {
      Log.w(TAG, "doWorkWithResult: viewToUpdate not found, exiting:"
          + " key=" + key
          + ", workRecord.viewId=" + workRecord.viewId
          + ", result=" + result);
      return;
    }

    final boolean updated = workRecord.callback.update(viewToUpdate, result);
    if (updated) workRecords.remove(key);
  }

  // WorkRecord.Store implementation

  @Override public long addWorkRecord(WorkRecord workRecord) {
    final long key = new Date().getTime();
    workRecords.put(key, workRecord);
    return key;
  }
}

MyTask.java

public class MyTask extends AsyncTask<Void, Void, Object> {
  // ...
  private final Context appContext;
  private final long workRecordKey;
  private final Object otherNeededValues;

  public MyTask(Context appContext, long workRecordKey, Object otherNeededValues) {
    this.appContext = appContext;
    this.workRecordKey = workRecordKey;
    this.otherNeededValues = otherNeededValues;
  }

  // ...

  @Override protected void onPostExecute(Object result) {
    final Intent resultIntent = new Intent(WorkRecord.ACTION_UPDATE_VIEW);
    resultIntent.putExtra(WorkRecord.EXTRA_WORK_RECORD_KEY, workRecordKey);
    resultIntent.putExtra(WorkRecord.EXTRA_RESULT, result);
    LocalBroadcastManager.getInstance(appContext).sendBroadcast(resultIntent);
  }
}

(класс, в котором вы инициируете задачу)

  // ...
  private WorkRecord.Store workRecordStore;
  private MyTask myTask;

  // ...

  private void initWorkRecordStore() {
    // TODO: get a reference to MainActivity and check instanceof WorkRecord.Store
    workRecordStore = (WorkRecord.Store) activity;
  }

  private void startMyTask() {
    final long key = workRecordStore.addWorkRecord(key, createWorkRecord());
    myTask = new MyTask(getApplicationContext(), key, otherNeededValues).execute()
  }

  private WorkRecord createWorkRecord() {
    return new WorkRecord(R.id.view_to_update, new WorkRecord.Callback() {
      @Override public void update(View view, Object result) {
        // TODO: update view using result
      }
    });
  }

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

Реализуя этот подход точно так, как описано в видео, задача будет выполняться до конца без отмены при изменении конфигурации, как и во втором подходе, описанном выше. Если ваша задача дорогостоящая (ЦП, память, батарея), имеет побочные эффекты или требует автоматического перезапуска при Activity перезапуске, вам необходимо изменить этот подход, чтобы отменить, при необходимости сохранить и перезапустить задачу. Или просто придерживайтесь первого подхода; У Ромена было четкое видение этого и он хорошо его реализовал.

Исправления

Это серьезный ответ, и вполне вероятно, что я допустил ошибки и упущения. Если найдете, прокомментируйте, и я обновлю ответ. Спасибо!

person unrulygnu    schedule 13.03.2016