¿AsyncTask tiene fallas conceptuales o me falta algo?

264

He investigado este problema durante meses, se me ocurrieron diferentes soluciones, que no estoy contento, ya que todos son hacks masivos. Todavía no puedo creer que una clase con un diseño defectuoso haya llegado al marco y nadie esté hablando de eso, así que supongo que me falta 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 showDialog()se llama a algún método ejemplar onPostExecute(). Esto, sin embargo, me parece totalmente artificial , porque mostrar un diálogo siempre necesita una referencia a una válida Context, y una AsyncTask nunca debe contener una referencia fuerte a un objeto de contexto .

La razón es obvia: ¿qué pasa si la actividad se destruye, lo 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 está aferrando a un objeto de contexto inútil (la ventana se habrá destruido y cualquier interacción de la IU fallará con una excepción), incluso se arriesga a 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 la interfaz de usuario 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 una Handlerinstancia. Eso funciona: dado que un controlador vincula libremente el contexto y la tarea, puede intercambiar mensajes entre ellos sin correr el riesgo de una fuga (¿verdad?). Pero eso significaría que la premisa de AsyncTask, es decir, que no necesita molestarse con los controladores, está mal. También parece abusar de Handler, ya que está enviando y recibiendo mensajes en el mismo hilo (lo crea en el hilo de la interfaz de usuario y lo envía a través de onPostExecute (), que también se ejecuta en el hilo de la interfaz de usuario).

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

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

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

Ahora simplemente quiero saber: ¿Me estoy perdiendo algo masivamente o AsyncTask es realmente completamente defectuoso? ¿Cómo están tus experiencias trabajando con él? ¿Cómo resolviste este problema?

Gracias por tu contribución.

Matías
fuente
1
Si tiene curiosidad, recientemente hemos agregado una clase a la biblioteca central de ignición llamada IgnitedAsyncTask, que agrega soporte para acceso de contexto seguro de tipo en todas las devoluciones de llamada utilizando el patrón de conexión / desconexión descrito por Dianne a continuación. También permite lanzar excepciones y manejarlas en una devolución de llamada separada. Ver github.com/kaeppler/ignition-core/blob/master/src/com/github/…
Matthias
eche un vistazo a esto: gist.github.com/1393552
Matthias
1
Esta pregunta también está relacionada.
Alex Lockwood
Agrego las tareas asíncronas a una lista de matrices y me aseguro de cerrarlas todas en un punto determinado.
NightSkyCode

Respuestas:

86

Qué tal algo como esto:

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(...);
    }
}
hackbod
fuente
55
Sí, mActivity será! = Nulo, pero si no hay referencias a su instancia de Worker, entonces cualquier referencia a esa instancia también estará sujeta a la eliminación de basura. Si su tarea se ejecuta para siempre, entonces tiene una pérdida de memoria de todos modos (su tarea), sin mencionar que está agotando la batería del teléfono. Además, como se mencionó en otra parte, puede configurar mActivity en nulo en onDestroy.
EboMike
13
El método onDestroy () establece mActivity en nulo. No importa quién tenga una referencia a la actividad antes de eso, porque todavía se está ejecutando. Y la ventana de la actividad siempre será válida hasta que se llame a onDestroy (). Al establecer nulo allí, la tarea asíncrona sabrá que la actividad ya no es válida. (Y cuando cambia una configuración, se llama a onDestroy () de la actividad anterior y se ejecuta onCreate () de la siguiente sin ningún mensaje en el bucle principal procesado entre ellos, de modo que AsyncTask nunca verá un estado inconsistente.)
hackbod
8
cierto, pero todavía no resuelve el último problema que mencioné: imagina que la tarea descarga algo de Internet. Con este enfoque, si voltea la pantalla 3 veces mientras la tarea se está ejecutando, se reiniciará con cada rotación de la pantalla, y cada tarea, excepto la última, arroja su resultado porque su referencia de actividad es nula.
Matthias
11
Para acceder en segundo plano, necesitará poner la sincronización adecuada alrededor de mActividad y lidiar con los momentos en que es nulo, o hacer que el hilo de fondo solo tome Context.getApplicationContext (), que es una instancia global única para la aplicación. El contexto de la aplicación está restringido en lo que puede hacer (sin interfaz de usuario como Dialog, por ejemplo) y requiere un poco de cuidado (los receptores registrados y los enlaces de servicio se dejarán para siempre si no los limpia), pero generalmente es apropiado para el código que no es No está vinculado al contexto de un componente en particular.
hackbod
44
Eso fue increíblemente útil, ¡gracias Dianne! Desearía que la documentación fuera tan buena en primer lugar.
Matthias
20

La razón es obvia: ¿qué pasa si la actividad se destruye, lo que provocó la tarea?

Desasociar manualmente la actividad desde AsyncTaskadentro onDestroy(). Manualmente volver a asociar la nueva actividad a la AsyncTaskde onCreate(). Esto requiere una clase interna estática o una clase estándar de Java, más quizás 10 líneas de código.

CommonsWare
fuente
Tenga cuidado con las referencias estáticas: he visto objetos recolectados como basura aunque haya referencias fuertes estáticas a ellos. Tal vez sea un efecto secundario del cargador de clases de Android, o incluso un error, pero las referencias estáticas no son una forma segura de intercambiar estados a lo largo de un ciclo de vida de la actividad. El objeto de la aplicación es, sin embargo, por eso lo uso.
Matthias
10
@Matthias: no dije que usara referencias estáticas. Dije que usara una clase interna estática. Hay una diferencia sustancial, a pesar de que ambos tienen "estática" en sus nombres.
CommonsWare
55
Ya veo: la clave aquí es getLastNonConfigurationInstance (), no la clase interna estática. Una clase interna estática no mantiene ninguna referencia implícita a su clase externa, por lo que es semánticamente equivalente a una clase pública simple. Solo una advertencia: no se garantiza que onRetainNonConfigurationInstance () no se llame cuando se interrumpe una actividad (una interrupción también puede ser una llamada telefónica), por lo que también debe parcelar su Tarea en onSaveInstanceState (), para obtener un estado realmente sólido solución. Pero aún así, buena idea.
Matthias
77
Um ... onRetainNonConfigurationInstance () siempre se llama cuando la actividad está en proceso de destrucción y recreación. No tiene sentido llamar en otros momentos. Si ocurre un cambio a otra actividad, la actividad actual se detiene / detiene, pero no se destruye, por lo que la tarea asíncrona puede continuar ejecutándose y utilizando la misma instancia de actividad. Si termina y dice que muestra un cuadro de diálogo, el cuadro de diálogo se mostrará correctamente como parte de esa actividad y, por lo tanto, no se mostrará al usuario hasta que regrese a la actividad. No puede poner AsyncTask en un paquete.
hackbod
15

Parece que AsyncTaskes un poco más que un defecto conceptual . 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 un solo hilo de fondo. Comenzando con DONUT, esto se cambió a un grupo de subprocesos que permite que varias tareas funcionen en paralelo. Al iniciar HONEYCOMB, las tareas vuelven a ejecutarse en un solo subproceso para evitar errores comunes de la aplicación causados ​​por la ejecución paralela. Si realmente desea una ejecución paralela, puede usar la executeOnExecutor(Executor, Params...) versión de este método con THREAD_POOL_EXECUTOR ; sin embargo, vea comentarios allí para advertencias sobre su uso.

Ambos executeOnExecutor()y THREAD_POOL_EXECUTORse agregan en API nivel 11 (Android 3.0.x, HONEYCOMB).

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

Y si desea apuntar a 2.xy 3.0+, las cosas se vuelven realmente complicadas.

Además, los documentos dicen:

Precaución: Otro problema que puede encontrar al usar un subproceso 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), lo que puede destruir su subproceso de trabajo . Para ver cómo puede persistir su tarea durante uno de estos reinicios y cómo cancelar adecuadamente la tarea cuando se destruye la actividad, consulte el código fuente de la aplicación de ejemplo Shelves.

18446744073709551615
fuente
12

Probablemente todos, incluido Google, estamos haciendo un mal uso AsyncTaskdesde 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 de la Actividad; recuerde que las Actividades se destruyen en la rotación. (En cuanto a la Vista , generalmente no se programan clases derivadas de, por ejemplo, android.widget.Button, pero se puede. Por lo general, lo único que se hace con respecto a la Vista es el xml).

En otras palabras, es incorrecto colocar derivados de 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.

18446744073709551615
fuente
5

No estoy seguro de que sea cierto que corras 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 alcance de uno de los métodos de la Actividad. Entonces, si la actividad se destruye, una vez que la AsyncTask se complete, ¿no será inalcanzable y elegible para la recolección de basura? Por lo tanto, la referencia a la actividad no importará porque AsyncTask en sí no se quedará.

oli
fuente
2
cierto, pero ¿y si la tarea se bloquea indefinidamente? Las tareas están destinadas a realizar operaciones de bloqueo, tal vez incluso aquellas que nunca terminan. Ahí tienes tu pérdida de memoria.
Matthias
1
Cualquier trabajador que realice algo en un bucle sin fin, o cualquier cosa que se bloquee, por ejemplo, en una operación de E / S.
Matthias
2

Sería más robusto mantener una WeekReference sobre 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<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]);
            }
        }
    }

}
Snicolas
fuente
Esto es similar a lo que hicimos por primera vez con Droid-Fu. Mantendríamos un mapa de referencias débiles a objetos de contexto y haríamos una búsqueda en las devoluciones de llamada de tareas para obtener la referencia más reciente (si está disponible) para ejecutar la devolución de llamada. Sin embargo, nuestro enfoque significaba que había una sola entidad que mantuvo este mapeo, mientras que su enfoque no lo hace, por lo que esto es realmente mejor.
Matthias
1
¿Has echado un vistazo a RoboSpice? github.com/octo-online/robospice . Creo que este sistema es aún mejor.
Snicolas
El código de muestra en la portada parece estar filtrando una referencia de contexto (una clase interna mantiene una referencia implícita a la clase externa). ¡No estoy convencido!
Matthias
@Matthias, tienes razón, es por eso que propongo una clase interna estática que tendrá una WeakReference en la Actividad.
Snicolas
1
@Matthias, creo que esto comienza a estar fuera de tema. Pero los cargadores no proporcionan almacenamiento en caché de la caja como lo hacemos nosotros, más aún, los cargadores tienden a ser más detallados que nuestra biblioteca. En realidad, manejan los cursores bastante bien, pero para la creación de redes, un enfoque diferente, basado en el almacenamiento en caché y un servicio, es más adecuado. Ver neilgoodman.net/2011/12/26/… parte 1 y 2
Snicolas
1

¿Por qué no simplemente anular el onPause()método en la Actividad propietaria y cancelarlo AsyncTaskdesde allí?

Jeff Axelrod
fuente
depende de lo que esté haciendo esa tarea. si solo carga / lee algunos datos, entonces estaría bien. pero si cambia el estado de algunos datos en un servidor remoto, preferimos darle a la tarea la capacidad de ejecutarse hasta el final.
Vit Khudenko
@Arhimed y lo tomo si sostienes el hilo de la interfaz de usuario onPauseporque es tan malo como sostenerlo en otro lugar. Es decir, ¿podrías obtener un ANR?
Jeff Axelrod
exactamente. no podemos bloquear el subproceso de la interfaz de usuario (ya sea una onPauseo otra cosa) porque corremos el riesgo de obtener un ANR.
Vit Khudenko
1

Tiene toda la razón: es por eso que un movimiento para evitar el uso de tareas / cargadores asíncronos en las actividades para obtener datos está ganando impulso. Una de las nuevas formas es utilizar un marco de Volley que esencialmente proporciona una devolución de llamada una vez que los datos están listos, mucho más consistente con el modelo MVC. Volley apareció en Google I / O 2013. No estoy seguro de por qué más personas no lo saben.

C0D3LIC1OU5
fuente
gracias por eso ... voy a investigarlo ... mi razón por la que no me gusta AsyncTask es porque me atora con un conjunto de instrucciones en PostExecute ... a menos que lo piratee como usar interfaces o anularlo cada vez Lo necesito.
carinlynchin 01 de
0

Personalmente, solo extiendo Thread y uso una interfaz de devolución de llamada para actualizar la interfaz de usuario. Nunca podría hacer que AsyncTask funcione correctamente sin problemas de FC. También uso una cola sin bloqueo para administrar el grupo de ejecución.

androidworkz
fuente
1
Bueno, su fuerza de cierre probablemente se debió al problema que mencioné: trató de hacer referencia a un contexto que se había salido del alcance (es decir, se había destruido su ventana), lo que dará como resultado una excepción de marco.
Matthias
No ... en realidad fue porque la cola apesta que está integrada en AsyncTask. Siempre uso getApplicationContext (). No tengo problemas con AsyncTask si solo son unas pocas operaciones ... pero estoy escribiendo un reproductor multimedia que actualiza la carátula del álbum en segundo plano ... en mi prueba tengo 120 álbumes sin carátula ... así que, mientras mi aplicación no se cerró por completo, asynctask arrojaba errores ... así que en su lugar construí una clase singleton con una cola que gestiona los procesos y hasta ahora funciona muy bien.
androidworkz
0

Pensé que cancelar funciona pero no lo hace.

aquí ellos RTFMing al respecto:

"" Si la tarea ya se ha iniciado, el parámetro mayInterruptIfRunning determina si el subproceso que ejecuta esta tarea debe interrumpirse en un intento de detener la tarea ".

Sin embargo, eso no implica que el hilo sea interrumpible. Eso es una cosa de Java, no una cosa de AsyncTask ".

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

nir
fuente
0

Sería mejor que pienses en AsyncTask como algo que está más estrechamente vinculado con una Actividad, Contexto, ContextWrapper, etc. Es más conveniente cuando su alcance se entiende completamente.

Asegúrese de tener una política de cancelación en su ciclo de vida para que eventualmente se recolecte basura y ya no guarde una referencia a su actividad y también se pueda recolectar basura.

Sin cancelar su AsyncTask mientras se aleja de su Contexto, se encontrará con pérdidas de memoria y NullPointerExceptions, si simplemente necesita proporcionar comentarios como un brindis por un diálogo simple, un solo tono 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 conducir a algunos escollos imprevistos.

jtuchek
fuente
-1

En cuanto a "experiencias de trabajo con él": es posible para matar el proceso , junto con todos los AsyncTasks, Android volverá a crear la pila de actividad para que el usuario no se menciona nada.

18446744073709551615
fuente