AsyncTask & runtime config change: ¿qué enfoques, con ejemplos de código conciso, respalda el equipo de Android?

Desde AsyncTask se introdujo en Cupcake (API 3, Android 1.5) en 2009, ha sido constantemente promovido por el equipo de Android como simple:

  • " Roscado sin dolor "
  • " una manera fácil de ejecutar un trabajo en un hilo de fondo y publicar los resultados de nuevo en el hilo de interfaz de usuario ".
  • " una de las maneras más simples de despedir una nueva tarea del hilo de la interfaz de usuario "

Las muestras de código que proporcionan refuerzan este mensaje de simplicidad, especialmente para aquellos de nosotros que hemos tenido que trabajar con hilos de manera más dolorosa . AsyncTask es muy atractivo.

Sin embargo, en los muchos años transcurridos desde entonces, fallos, pérdidas de memoria y otros problemas han plagado a la mayoría de los desarrolladores que han elegido utilizar AsyncTask en sus aplicaciones de producción. Esto es a menudo debido a la destrucción de la Activity y la recreación en el cambio de configuración de tiempo de ejecución (especialmente la orientación / rotación) mientras que el AsyncTask está ejecutando doInBackground(Params...) ; cuando onPostExecute(Result) es llamado, la Activity ya ha sido destruida, dejando las referencias de UI en un estado inutilizable (o incluso null ).

Y la falta de guía obvia, clara y concisa y muestras de código del equipo de Android en este tema sólo ha empeorado las cosas, lo que lleva a la confusión, así como varias soluciones y hacks, algunos decente, algunos terribles:

  • ¿AsyncTask es realmente falto conceptualmente o simplemente estoy perdiendo algo?
  • Buenas prácticas: AsyncTask durante el cambio de orientación
  • Bloquear la orientación hasta que finalice Asynctask

Obviamente, ya que AsyncTask se puede utilizar en muchas situaciones, no hay One Way para acomodar este problema. Mi pregunta, sin embargo, es acerca de las opciones .

¿Cuáles son las mejores prácticas canónicas (respaldadas por el equipo de Android), con ejemplos de código concisos, para integrar AsyncTask con el ciclo de vida de Activity / Fragment y reinicios automáticos en el cambio de configuración de ejecución?

Recomendación proporcionada (para cualquier enfoque)

No contenga referencias a objetos específicos de UI

De Memoria y Roscado. (Patrones de rendimiento de Android Temporada 5, Ep. 3) :

Tienes algún objeto de subproceso que se declara como una clase interna de una Activity . El problema aquí es que el objeto AsyncTask ahora tiene una referencia implícita a la Activity AsyncTask , y mantendrá esa referencia hasta que el objeto de trabajo haya sido destruido … Hasta que este trabajo se complete, la Activity permanece en la memoria … Este tipo de patrón también conduce a tipos comunes de accidentes que se ven en las aplicaciones de Android …

Lo que aquí se puede sacar es que no debe contener referencias a ningún tipo de objetos específicos de UI en cualquiera de sus escenarios de subprocesos.

Abordajes proporcionados

Aunque la documentación es escasa y dispersa, el equipo de Android ha proporcionado al menos tres enfoques distintos para tratar con reinicios en el cambio de configuración con AsyncTask :

  1. Cancelar / guardar / reiniciar la tarea en ejecución en los métodos del ciclo de vida
  2. Uso de WeakReference s en objetos UI
  3. Administrar en la Activity o el Fragment nivel superior utilizando "registros de trabajo"

1. Cancelar / guardar / reiniciar la tarea en ejecución en los métodos de ciclo de vida

Desde el uso de AsyncTask | Procesos y Hilos | Desarrolladores de Android

Para ver cómo puede persistir su tarea durante uno de estos reinicios y cómo cancelar la tarea correctamente cuando se destruye la actividad, consulte el código fuente de la aplicación de ejemplo Shelves .

En la aplicación Estantes, las referencias a las tareas se mantienen como campos en una Activity , para que puedan ser administradas en los métodos de ciclo de vida de la Activity . Antes de echar un vistazo al código, sin embargo, hay un par de cosas importantes a tener en cuenta.

En primer lugar, esta aplicación se escribió antes de AsyncTask se agregó a la plataforma. Una clase que se asemeja mucho a lo que fue lanzado posteriormente como AsyncTask se incluye en el origen, llamada UserTask . Para nuestra discusión aquí, UserTask es funcionalmente equivalente a AsyncTask .

En segundo lugar, las subclases de UserTask se declaran como clases internas de una Activity . Este enfoque ahora se considera como un anti-patrón, como se señaló anteriormente (véase No mantener referencias a los objetos específicos de la interfaz de usuario anterior). Afortunadamente, este detalle de implementación no afecta el enfoque general de la gestión de tareas en ejecución en los métodos de ciclo de vida; sin embargo, si elige usar este código de ejemplo para su propia aplicación, declare sus subclases de AsyncTask otro lugar.

Cancelación de la tarea

  • onDestroy() , cancele las tareas y establezca las referencias a null . (No estoy seguro si establecer referencias a null tiene algún impacto aquí, si tiene más información, por favor comente y actualizaré la respuesta en consecuencia).

  • Anular AsyncTask#onCancelled(Object) si necesita limpiar o realizar cualquier otro trabajo necesario después de AsyncTask#doInBackground(Object[]) devuelve.

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

Guardar y reiniciar la tarea

  • onSaveInstanceState(Bundle, PersistableBundle) , cancela las tareas y guarda el estado de las tareas para que se puedan reiniciar cuando se restaure el estado de la instancia.

  • onRestoreInstanceState(Bundle, PersistableBundle) , recupera el estado sobre tareas canceladas e inicia nuevas tareas con los datos del estado de tarea cancelado.

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

Este es un enfoque sencillo, y debe tener sentido incluso para los principiantes que se están familiarizando con el ciclo de vida de la Activity . También tiene la ventaja de no requerir código mimimal fuera de la propia clase de tarea, tocando uno a tres métodos de ciclo de vida, dependiendo de las necesidades. Un sencillo, 7-línea onDestroy() fragmento en la sección de uso de la AsyncTask javadoc podría haber salvado a todos nosotros un montón de pena. Quizás alguna generación futura puede ser ahorrada.

2. Use WeakReferences para objetos UI

  • Pase los objetos UI como parámetros en el AsyncTask del AsyncTask . Almacene referencias débiles a estos objetos como campos WeakReference en AsyncTask .

  • En onPostExecute() , compruebe que el objeto UI WeakReference s no sea null luego actualícelos directamente.

De utilizar un AsyncTask | Procesamiento de mapas de bits fuera del subproceso de interfaz de usuario | Desarrolladores de 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); } } } } 

El WeakReference para el ImageView asegura que el AsyncTask no impide que ImageView y cualquier cosa que las referencias de ser basura recogida. No hay garantía de que el ImageView siga en pie cuando finalice la tarea, por lo que también debe comprobar la referencia en onPostExecute() . Es posible que el ImageView ya no exista si, por ejemplo, el usuario se aleja de la actividad o si ocurre un cambio de configuración antes de que finalice la tarea.

Este enfoque es más simple y ordenado que el primero, agregando sólo un cambio de tipo y una comprobación nula a la clase de tarea y ningún código adicional en ningún otro lugar.

Sin embargo, esto tiene un costo: la tarea se ejecutará hasta su fin sin ser cancelada en el cambio de configuración. Si su tarea es costosa (CPU, memoria, batería), tiene efectos secundarios o necesita reiniciarse automáticamente al reiniciar la Activity , entonces el primer enfoque es probablemente una mejor opción.

3. Administrar en la Actividad o Fragmento de nivel superior usando "registros de trabajo"

De Memoria y Roscado. (Patrones de rendimiento de Android Temporada 5, Ep. 3)

… obligan a la Activity o Fragment nivel superior a ser el único sistema responsable de actualizar los objetos de la IU.

Por ejemplo, si desea iniciar algún trabajo, cree un "registro de trabajo" que empareje una View con alguna función de actualización. Cuando ese bloque de trabajo ha terminado, envía los resultados a la Activity usando una llamada de Intent o runOnUiThread(Runnable) .

La Activity puede entonces llamar a la función de actualización con la nueva información, o si la View no está allí, simplemente dejar el trabajo por completo. Y, si la Activity que emitió la obra fue destruida, entonces la nueva Activity no tendrá una referencia a nada de esto, y simplemente dejará el trabajo, también.

Aquí hay una captura de pantalla del diagrama adjunto que describe este enfoque:

Diagrama de enfoque de gestión de subprocesos de trabajo

Las muestras del código no fueron proporcionadas en el vídeo, así que aquí está mi toma en una puesta en práctica básica:

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

(la clase en la que inicia la tarea)

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

Obviamente, este enfoque es un esfuerzo enorme en comparación con los otros dos, y el exceso para muchas implementaciones. Para las aplicaciones más grandes que hacen un montón de trabajo de roscado, sin embargo, esto puede servir como una arquitectura de base adecuada.

Implementando este enfoque exactamente como se describe en el video, la tarea se ejecutará hasta su fin sin ser cancelado en el cambio de configuración, como el segundo enfoque anterior. Si su tarea es costosa (CPU, memoria, batería), tiene efectos secundarios o necesita reiniciarse automáticamente en Activity reinicio, entonces deberá modificar este enfoque para acomodar la cancelación, opcionalmente, guardar y reiniciar la tarea. O simplemente seguir con el primer enfoque; Romain tenía una visión clara de esto y la implementó bien.

Correcciones

Esta es una gran respuesta, y es probable que haya cometido errores y omisiones. Si encuentra alguna, por favor comente y actualizaré la respuesta. ¡Gracias!

  • Android cómo implementar eficientemente el caché de datos y los eventos de cambio de configuración en fragmentos?
  • Cuando una actividad se destruye debido a un cambio de configuración, ¿también se destruyen sus cargadores?
  • Obtener "Fragmento no creó una vista" después de la adición de otro Fragmento sin UI
  • Fragmento no UI vs Singleton
  • FindFragmentByTag null para Fragmento A, si setRetain (true) en Fragmento B
  • Después de que el fragmento de cambio de configuración de backstack ahora está compartiendo el FrameLayout?
  • Cuadros de permisos múltiples en Android usando Iniciar sesión con Google después del cambio de orientación
  • FlipAndroid es un fan de Google para Android, Todo sobre Android Phones, Android Wear, Android Dev y Aplicaciones para Android Aplicaciones.