¿Realmente es AsyncTask conceptualmente defectuoso o me falta algo?

Investigué este problema desde hace meses, presenté diferentes soluciones para él, y no estoy contento porque son piratas masivos. Todavía no puedo creer que una clase con fallas en el diseño haya entrado en el marco de trabajo y nadie lo hable, así que supongo que debo estar perdiendo algo.

El problema es con AsyncTask . De acuerdo con la documentación,

“permite realizar operaciones en segundo plano y publicar resultados en el subproceso de la interfaz de usuario sin tener que manipular subprocesos y / o controladores”.

El ejemplo continúa mostrando cómo se llama a onPostExecute() método showDialog() ejemplar onPostExecute() . Esto, sin embargo, me parece completamente ingenuo , porque mostrar un cuadro de diálogo siempre necesita una referencia a un Context válido, y una tarea asíncrona nunca debe contener una referencia fuerte a un objeto de contexto .

La razón es obvia: ¿y si se destruye la actividad que desencadenó la tarea? Esto puede suceder todo el tiempo, por ejemplo, porque volteó la pantalla. Si la tarea tuviera una referencia al contexto que la creó, no solo se estará aferrando a un objeto de contexto inútil (¡la ventana se habrá destruido y cualquier interacción de UI fallará con una excepción!), Incluso corre el riesgo de crear un pérdida de memoria.

A menos que mi lógica sea defectuosa aquí, esto se traduce en: onPostExecute() es completamente inútil, porque ¿de qué sirve que este método se ejecute en el hilo de UI si no tiene acceso a ningún contexto? No puedes hacer nada significativo aquí.

Una solución alternativa sería no pasar instancias de contexto a una AsyncTask, sino a una instancia de Handler . Eso funciona: dado que un Manejador enlaza vagamente el contexto y la tarea, puede intercambiar mensajes entre ellos sin correr el riesgo de una fuga (¿no?). Pero eso significaría que la premisa de AsyncTask, es decir, que no necesita molestarse con los controladores, es incorrecta. También parece abusar de Handler, ya que está enviando y recibiendo mensajes en el mismo subproceso (lo crea en el subproceso UI y lo envía en onPostExecute (), que también se ejecuta en el subproceso UI).

Para colmo, incluso con esa solución, todavía tiene el problema de que cuando se destruye el contexto, no tiene registro de las tareas que activó. Eso significa que debe reiniciar cualquier tarea al volver a crear el contexto, por ejemplo, después de cambiar la orientación de la pantalla. Esto es lento y derrochador

Mi solución a esto (tal como se implementa en la biblioteca Droid-Fu ) es mantener un mapeo de WeakReference desde nombres de componentes a sus instancias actuales en el objeto de aplicación único. Cada vez que se inicia una AsyncTask, registra el contexto de la llamada en ese mapa, y en cada callback, recuperará la instancia de contexto actual de esa asignación. Esto garantiza que nunca hará referencia a una instancia de contexto obsoleto y siempre tendrá acceso a un contexto válido en las devoluciones de llamada para que pueda realizar un trabajo de IU significativo allí. Tampoco se filtra, porque las referencias son débiles y se borran cuando ya no existe ninguna instancia de un componente dado.

Aún así, es una solución compleja y requiere subclasificar algunas de las clases de la biblioteca Droid-Fu, lo que hace que este sea un enfoque bastante intrusivo.

Ahora simplemente quiero saber: ¿Me estoy perdiendo algo de manera masiva o AsyncTask está totalmente defectuoso? ¿Cómo están tus experiencias trabajando con eso? ¿Cómo resolviste estos problemas?

Gracias por tu contribución.

Qué tal algo como esto:

 class MyActivity extends Activity { Worker mWorker; static class Worker extends AsyncTask { 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(...); } } 

La razón es obvia: ¿y si se destruye la actividad que desencadenó la tarea?

AsyncTask manualmente la actividad de AsyncTask en onDestroy() . AsyncTask a onCreate() manualmente la nueva actividad a AsyncTask en onCreate() . Esto requiere una clase interna estática o una clase Java estándar, más quizá 10 líneas de código.

Parece que AsyncTask es un poco más que conceptualmente defectuoso . También es inutilizable por problemas de compatibilidad. Los documentos de Android dicen:

Cuando se introdujo por primera vez, las AsyncTasks se ejecutaron en serie en una única cadena de fondo. Comenzando con DONUT, esto se cambió a un conjunto de subprocesos que permite que varias tareas funcionen en paralelo. Al iniciar HONEYCOMB, las tareas vuelven a ejecutarse en un solo subproceso para evitar los errores de aplicación comunes causados ​​por la ejecución paralela. Si realmente desea la ejecución en paralelo, puede usar la executeOnExecutor(Executor, Params...) de este método con THREAD_POOL_EXECUTOR ; sin embargo, consulte los comentarios sobre advertencias sobre su uso.

Tanto executeOnExecutor() como THREAD_POOL_EXECUTOR se agregan en el nivel API 11 (Android 3.0.x, HONEYCOMB).

Esto significa que si crea dos AsyncTask para descargar dos archivos, la segunda descarga no comenzará hasta que termine el primero. Si chatea a través de dos servidores, y el primer servidor está inactivo, no se conectará al segundo antes de que se desconecte la primera conexión. (A menos que use las nuevas características API11, por supuesto, pero esto hará que su código sea incompatible con 2.x).

Y si quiere apuntar tanto a 2.x como a 3.0+, las cosas se vuelven realmente complicadas.

Además, los documentos dicen:

Precaución: Otro problema que puede encontrar al usar un hilo de trabajo es el reinicio inesperado de su actividad debido a un cambio en la configuración del tiempo de ejecución (como cuando el usuario cambia la orientación de la pantalla), que puede destruir su hilo de trabajo . Para ver cómo puede continuar su tarea durante uno de estos reinicios y cómo cancelar correctamente la tarea cuando se destruye la actividad, consulte el código fuente de la aplicación de muestra Shelves.

Probablemente todos, incluido Google, estamos haciendo AsyncTask uso AsyncTask de AsyncTask desde el punto de vista de MVC .

Una actividad es un controlador y el controlador no debe iniciar operaciones que puedan sobrevivir a la vista . Es decir, AsyncTasks debe usarse desde Model , desde una clase que no está vinculada al ciclo de vida Activity. Recuerde que las Actividades se destruyen en la rotación. (En cuanto a la Vista , generalmente no se progtwign las clases derivadas de, por ejemplo, android.widget.Button, pero se puede. Por lo general, lo único que se hace con la Vista es el xml).

En otras palabras, es incorrecto colocar derivadas AsyncTask en los métodos de Actividades. OTOH, si no debemos usar AsyncTasks en Actividades, AsyncTask pierde su atractivo: solía anunciarse como una solución rápida y fácil.

No estoy seguro de que sea cierto que corre el riesgo de una pérdida de memoria con una referencia a un contexto de una AsyncTask.

La forma habitual de implementarlos es crear una nueva instancia de AsyncTask dentro del scope de uno de los métodos de la actividad. Entonces, si la actividad se destruye, una vez que la AsyncTask finalice, ¿no será inalcanzable y luego elegible para la recolección de basura? Entonces, la referencia a la actividad no importará porque la AsyncTask en sí misma no se mantendrá.

Sería más sólido mantener una WeekReference en su actividad:

 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(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 { WeakReference mActivity; AsyncTaskCounter(WeakReferenceAsyncTaskTestActivity activity) { mActivity = new WeakReference(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]); } } } } 

¿Por qué no sobrescribir el método onPause() en la Actividad propietaria y cancelar la AsyncTask desde allí?

Tiene toda la razón, es por eso que un movimiento que se aleja del uso de tareas / cargadores asíncronos en las actividades para obtener datos está ganando impulso. Una de las nuevas formas es usar un marco Volley que esencialmente proporciona una callback una vez que los datos estén listos, mucho más consistentes con el modelo MVC. Volley se popularizó en Google I / O 2013. No estoy seguro de por qué más personas no están al tanto de esto.

Personalmente, simplemente extiendo Thread y uso una interfaz de callback para actualizar la interfaz de usuario. Nunca podría hacer que AsyncTask funcione bien sin problemas de FC. También uso una cola que no bloquea para administrar el grupo de ejecución.

Pensé que cancelar funciona pero no funciona.

aquí ellos RTFMing al respecto:

“” Si la tarea ya ha comenzado, el parámetro mayInterruptIfRunning determina si el hilo que ejecuta esta tarea debe interrumpirse para intentar detener la tarea “.

Eso no implica, sin embargo, que el hilo sea interrumpible. Eso es algo de Java, no una cosa de AsyncTask “.

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

Sería mejor pensar en AsyncTask como algo que está más estrechamente relacionado con Activity, Context, ContextWrapper, etc. Es más conveniente cuando se entiende completamente su scope.

Asegúrese de tener una política de cancelación en su ciclo de vida para que eventualmente sea recolectada como basura y ya no tenga una referencia a su actividad y también puede ser basura.

Sin cancelar su AsyncTask mientras se aleja de su Contexto, se encontrará con memory leaks y NullPointerExceptions, si solo necesita proporcionar comentarios como un Toast en un cuadro de diálogo simple, entonces una sola parte de su Contexto de Aplicación ayudaría a evitar el problema de NPE.

AsyncTask no es del todo malo, pero definitivamente hay mucha magia que puede llevar a algunas trampas imprevistas.

En cuanto a “experiencias que trabajan con él”: es posible matar el proceso junto con todas las AsyncTasks, Android volverá a crear la stack de actividades para que el usuario no mencione nada.