Join FlipAndroid.COM Telegram Group: https://t.me/joinchat/F_aqThGkhwcLzmI49vKAiw


Modificar vistas en AsyncTask doInBackground () no (siempre) arrojar excepción

Me encontré con un comportamiento inesperado cuando jugaba con algún código de ejemplo.

Como "todo el mundo sabe" no se puede modificar elementos de la interfaz de usuario de otro hilo, por ejemplo, el doInBackground() de un AsyncTask .

Por ejemplo:

 public class MainActivity extends Activity { private TextView tv; public class MyAsyncTask extends AsyncTask<TextView, Void, Void> { @Override protected Void doInBackground(TextView... params) { params[0].setText("Boom!"); return null; } } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); LinearLayout layout = new LinearLayout(this); tv = new TextView(this); tv.setText("Hello world!"); Button button = new Button(this); button.setText("Click!"); button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { new MyAsyncTask().execute(tv); } }); layout.addView(tv); layout.addView(button); setContentView(layout); } } 

Si ejecuta esto y hace clic en el botón, su aplicación se detendrá como se esperaba y encontrará el seguimiento de pila siguiente en logcat:

11: 21: 36.630: E / AndroidRuntime (23922): EXCEPCIÓN FATAL: AsyncTask # 1

11: 21: 36.630: E / AndroidRuntime (23922): java.lang.RuntimeException: Se produjo un error al ejecutar doInBackground ()

$ CalledFromWrongThreadException: Sólo el subproceso original que creó una jerarquía de vistas puede tocar sus vistas.
11: 21: 36.630: E / AndroidRuntime (23922): en android.view.ViewRootImpl.checkThread (ViewRootImpl.java:6357)

Hasta aquí todo bien.

Ahora cambié el onCreate() para ejecutar el AsyncTask inmediatamente, y no esperar el tecleo del botón.

 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // same as above... new MyAsyncTask().execute(tv); } 

La aplicación no se cierra, nada en los registros, TextView ahora muestra "Boom!" en la pantalla. Guau. No esperaba eso.

Tal vez demasiado pronto en el ciclo de vida de la Activity ? Vamos a mover el ejecutar a onResume() .

 @Override protected void onResume() { super.onResume(); new MyAsyncTask().execute(tv); } 

El mismo comportamiento que el anterior.

Ok, vamos a pegarlo en un Handler .

 @Override protected void onResume() { super.onResume(); Handler handler = new Handler(); handler.post(new Runnable() { @Override public void run() { new MyAsyncTask().execute(tv); } }); } 

El mismo comportamiento de nuevo. Me estoy quedando sin ideas y probar postDelayed() con una demora de 1 segundo:

 @Override protected void onResume() { super.onResume(); Handler handler = new Handler(); handler.postDelayed(new Runnable() { @Override public void run() { new MyAsyncTask().execute(tv); } }, 1000); } 

¡Finalmente! La excepción esperada:

$ CalledFromWrongThreadException: Sólo el subproceso original que creó una jerarquía de vistas puede tocar sus vistas.

Wow, este es el tiempo relacionado?

Intento diferentes retrasos y parece que para esta prueba en particular, en este dispositivo en particular (Nexus 4, ejecutando 5.1) el número mágico es 60ms, es decir, a veces se lanza la excepción, a veces se actualiza el TextView como si nada hubiera pasado.

Estoy asumiendo esto sucede cuando la jerarquía de la visión no ha sido creada totalmente en el punto donde es modificada por el AsyncTask . ¿Es esto correcto? ¿Hay una mejor explicación para ello? ¿Hay una devolución de llamada en la Activity que se puede utilizar para asegurarse de que la jerarquía de vista se ha creado completamente? Los temas relacionados con el tiempo son aterradores.

Encontré una pregunta similar aquí Alterando las vistas del hilo de interfaz de usuario en AsyncTask en doInBackground, CalledFromWrongThreadException no siempre se arroja pero no hay ninguna explicación.

Actualizar:

Debido a una solicitud en los comentarios y una respuesta propuesta, he añadido algún registro de depuración para determinar la cadena de eventos …

 public class MainActivity extends Activity { private TextView tv; public class MyAsyncTask extends AsyncTask<TextView, Void, Void> { @Override protected Void doInBackground(TextView... params) { Log.d("MyAsyncTask", "before setText"); params[0].setText("Boom!"); Log.d("MyAsyncTask", "after setText"); return null; } } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); LinearLayout layout = new LinearLayout(this); tv = new TextView(this); tv.setText("Hello world!"); layout.addView(tv); Log.d("MainActivity", "before setContentView"); setContentView(layout); Log.d("MainActivity", "after setContentView, before execute"); new MyAsyncTask().execute(tv); Log.d("MainActivity", "after execute"); } } 

Salida:

10: 01: 33.126: D / MainActivity (18386): antes de setContentView
10: 01: 33.137: D / MainActivity (18386): después de setContentView, antes de ejecutar
10: 01: 33.148: D / MainActivity (18386): después de ejecutar
10: 01: 33.153: D / MyAsyncTask (18386): antes de setText
10: 01: 33.153: D / MyAsyncTask (18386): después de setText

Todo lo que se espera, nada inusual aquí, setContentView() completado antes de execute() se llama, que a su vez se completa antes de setText() se llama desde doInBackground() . Así que no es eso.

Actualizar:

Otro ejemplo:

 public class MainActivity extends Activity { private LinearLayout layout; private TextView tv; public class MyAsyncTask extends AsyncTask<Void, Void, Void> { @Override protected Void doInBackground(Void... params) { tv.setText("Boom!"); return null; } } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); layout = new LinearLayout(this); Button button = new Button(this); button.setText("Click!"); button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { tv = new TextView(MainActivity5.this); tv.setText("Hello world!"); layout.addView(tv); new MyAsyncTask().execute(); } }); layout.addView(button); setContentView(layout); } } 

Esta vez, estoy agregando el TextView en el onClick() del Button inmediatamente antes de llamar a execute() en el AsyncTask . En esta etapa, el Layout inicial (sin el TextView ) se ha mostrado correctamente (es decir, puedo ver el botón y hacer clic en él). Una vez más, ninguna excepción lanzada.

Y el ejemplo del contador, si agrego Thread.sleep(100); En el execute() antes de setText() en doInBackground() la excepción habitual es lanzada.

Otra cosa que acabo de notar es que justo antes de que se lance la excepción, el texto del TextView se actualiza y se muestra correctamente, por sólo una fracción de segundo, hasta que la aplicación se cierre automáticamente.

Supongo que algo debe estar sucediendo (asincrónicamente, es decir, desvinculado de cualquier método de ciclo de vida / devoluciones de llamada) a mi TextView que de alguna manera "lo adjunta" a ViewRootImpl , lo que hace que el último lance la excepción. ¿Alguien tiene una explicación o indicaciones para más documentación sobre lo que ese "algo" es?

  • Android llama a AsyncTask justo después de que otro termine
  • Error de Android Studio: "El método getText () debe ser llamado desde el subproceso de interfaz de usuario, el subproceso inferido actualmente es el trabajador
  • Android: pasa la referencia de función a AsyncTask
  • Cómo detener asynctask hilo en Android?
  • Android cargar datos asincrónicamente en vista pager
  • Android: problemas con una cadena de actividad multinivel
  • Cuando la aplicación va a fondo durante una ejecución AsyncTask ¿qué debería hacer?
  • Cómo obtener el resultado de OnPostExecute () en la actividad principal porque AsyncTask es una clase aparte?
  • 3 Solutions collect form web for “Modificar vistas en AsyncTask doInBackground () no (siempre) arrojar excepción”

    El método checkThread () de ViewRootImpl.java es responsable de lanzar esta excepción. Esta comprobación se suprime utilizando el miembro mHandlingLayoutInLayoutRequest hasta que executeLayout () es decir, todos los recorridos de dibujo iniciales estén completos.

    Por lo que lanza excepción sólo si utilizamos delay.

    No estoy seguro si esto es un error en android o intencional 🙂

    Basado en la respuesta de RocketRandom he hecho un poco más de excavación y se me ocurrió una respuesta más completa, que creo que se justifica aquí.

    Responsable de la eventual excepción es de hecho ViewRootImpl.checkThread() que se llama cuando se llama performLayout() . performLayout() viaja hasta la jerarquía de vista hasta que finalmente termina en ViewRootImpl , pero se origina en TextView.checkForRelayout() , que es llamado por setText() . Hasta aquí todo bien. Entonces, ¿por qué la excepción a veces no se lanza cuando llamamos a setText() ?

    TextView.checkForRelayout() sólo se llama si TextView ya tiene un Layout ( mLayout != null ). (Esta comprobación es lo que inhibe la excepción de ser lanzada en este caso, no mHandlingLayoutInLayoutRequest en ViewRootImpl .)

    Así que, de nuevo, ¿por qué el TextView veces no tiene un Layout ? O mejor, ya que obviamente empieza a no tener uno, ¿cuándo y dónde lo consigue?

    Cuando TextView se agrega inicialmente a LinearLayout usando layout.addView(tv); , De nuevo, se llama una cadena de requestLayout() , viajando por la jerarquía View , terminando en ViewRootImpl , donde esta vez, no se produce ninguna excepción, porque todavía estamos en el subproceso de la interfaz de usuario. Aquí, ViewRootImpl llama a scheduleTraversals() .

    La parte importante aquí es que esto coloca una llamada de retorno / Runnable en las colas de mensajes de Choreographer , que se procesa "asincrónicamente" al flujo principal de ejecución:

     mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); 

    El Choreographer finalmente procesará esto usando un controlador y ejecutará lo que Runnable ViewRootImpl haya publicado aquí, lo que finalmente llamará a performTraversals() , measureHierarchy() y performMeasure() (en ViewRootImpl ), que realizará una serie adicional de View.measure() , onMeasure() (y algunos otros), viajando por la jerarquía View hasta que finalmente llega a nuestro TextView.onMeasure() , que llama a makeNewLayout() , que llama a makeSingleLayout() , que finalmente establece nuestra variable de miembro mLayout :

     mLayout = makeSingleLayout(wantWidth, boring, ellipsisWidth, alignment, shouldEllipsize, effectiveEllipsize, effectiveEllipsize == mEllipsize); 

    Después de que esto suceda, mLayout ya no es nulo, y cualquier intento de modificar el TextView , es decir, llamar a setText() como en nuestro ejemplo, dará lugar a la bien conocida CalledFromWrongThreadException .

    Así que lo que tenemos aquí es una condición de carrera poco agradable, si nuestro AsyncTask puede obtener sus manos en el TextView antes de que los recorridos de Choreographer están completos, se puede modificar sin sanciones. Por supuesto esto todavía es una mala práctica, y no debería hacerse (hay muchos otros puestos de SO que tratan con esto), pero si esto se hace accidentalmente o sin saberlo, la CalledFromWrongThreadException no es una protección perfecta.

    Este ejemplo artificial utiliza un TextView y los detalles pueden variar para otras vistas, pero el principio general sigue siendo el mismo. Queda por ver si alguna otra implementación de View (tal vez una personalizada) que no llame requestLayout() en todos los casos puede modificarse sin penalizaciones, lo que podría llevar a problemas más grandes (ocultos).

    Usted puede escribir en doInBackground a un TextView si no es parte de la GUI todavía.

    Es sólo parte de la GUI después de la sentencia setContentView(layout); .

    Sólo mi pensamiento.

    FlipAndroid es un fan de Google para Android, Todo sobre Android Phones, Android Wear, Android Dev y Aplicaciones para Android Aplicaciones.