Изменение вида в AsyncTask doInBackground () не исключает (всегда) исключение

Я просто столкнулся с каким-то неожиданным поведением при игре с некоторым примером кода.

Поскольку «все знают», вы не можете изменять элементы пользовательского интерфейса из другого потока, например doInBackground() для AsyncTask .

Например:

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

Если вы запустите это и нажмите кнопку, приложение будет остановлено, как ожидалось, и вы увидите следующую трассировку стека в logcat:

11: 21: 36.630: E / AndroidRuntime (23922): FATAL EXCEPTION: AsyncTask # 1

11: 21: 36.630: E / AndroidRuntime (23922): java.lang.RuntimeException: Произошла ошибка при выполнении doInBackground ()

11: 21: 36.630: E / AndroidRuntime (23922): вызвано: android.view.ViewRootImpl $ CalledFromWrongThreadException: только исходный поток, создавший иерархию представлений, может коснуться его представлений.
11: 21: 36.630: E / AndroidRuntime (23922): на android.view.ViewRootImpl.checkThread (ViewRootImpl.java:6357)

Все идет нормально.

Теперь я изменил onCreate() чтобы немедленно запустить AsyncTask , и не дожидаться нажатия кнопки.

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

Приложение не закрывается, ничего в журналах, TextView теперь показывает «Boom!». на экране. Вау. Не ожидал этого.

Может быть, слишком рано в жизненном цикле Activity ? Переместите выполнение в onResume() .

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

Такое же поведение, как указано выше.

Хорошо, давайте придерживаться его на Handler .

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

Такое же поведение снова. У меня закончились идеи и попробуйте postDelayed() с задержкой в ​​1 секунду:

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

В заключение! Ожидаемое исключение:

11: 21: 36.630: E / AndroidRuntime (23922): вызвано: android.view.ViewRootImpl $ CalledFromWrongThreadException: только исходный поток, создавший иерархию представлений, может коснуться его представлений.

Вау, это связано с хронометром?

Я пробую разные задержки и кажется, что для этого конкретного тестового прогона на этом конкретном устройстве (Nexus 4, работающем 5.1) магическое число составляет 60 мс, то есть иногда выдает исключение, иногда оно обновляет TextView как будто ничего не произошло.

Я предполагаю, что это происходит, когда иерархия представлений не была полностью создана в точке, где она была изменена AsyncTask . Это верно? Есть ли для этого лучшее объяснение? Есть ли обратный вызов для Activity который может быть использован для обеспечения полного создания иерархии представлений? Сроки связаны с проблемами.

Я нашел аналогичный вопрос здесь. Развертывание представлений потока пользовательского интерфейса в AsyncTask в doInBackground, CalledFromWrongThreadException не всегда выбрасывается, но объяснений нет.

Обновить:

Из-за запроса в комментариях и предлагаемого ответа я добавил несколько протоколов отладки, чтобы выяснить цепочку событий …

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

Вывод:

10: 01: 33.126: D / MainActivity (18386): перед setContentView
10: 01: 33.137: D / MainActivity (18386): после setContentView перед выполнением
10: 01: 33.148: D / MainActivity (18386): после выполнения
10: 01: 33.153: D / MyAsyncTask (18386): перед setText
10: 01: 33.153: D / MyAsyncTask (18386): после setText

Все, как ожидалось, ничего необычного здесь, setContentView() завершено до setContentView() execute() , которое, в свою очередь, завершается до setText() из doInBackground() . Вот и все.

Обновить:

Другой пример:

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

На этот раз я добавляю TextView в onClick() Button непосредственно перед вызовом execute() в AsyncTask . На этом этапе исходный Layout (без TextView ) отображается правильно (т. Е. Я могу увидеть кнопку и щелкнуть ее). Опять же, никакого исключения не было.

И пример счетчика, если я добавлю Thread.sleep(100); В execute() перед setText() в doInBackground() обычное исключение.

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

Я предполагаю, что что-то должно происходить (асинхронно, т.е. отделяться от любых методов / обратных вызовов жизненного цикла) до моего TextView который каким-то образом «прикрепляет» его к ViewRootImpl , что заставляет последнее генерировать исключение. У кого-нибудь есть объяснение или указатели на дальнейшую документацию о том, что это такое?

Метод checkThread () для ViewRootImpl.java отвечает за исключение этого исключения. Эта проверка подавляется с использованием члена mHandlingLayoutInLayoutRequest до выполнения функцииLayout (), т.е. все начальные обходные чертежи завершены.

Поэтому он выдает исключение, только если мы используем задержку.

Не уверен, что это ошибка в андроиде или намеренно 🙂

Основываясь на ответе RocketRandom, я сделал еще несколько копаний и придумал более полный ответ, который, как мне кажется, здесь оправдан.

Ответственный за возможное исключение – это действительно ViewRootImpl.checkThread() который вызывается при performLayout() . performLayout() перемещает иерархию представления до тех пор, пока она не ViewRootImpl в ViewRootImpl , но она возникла в TextView.checkForRelayout() , которая вызывается setText() . Все идет нормально. Итак, почему исключение иногда не получается, когда мы вызываем setText() ?

TextView.checkForRelayout() вызывается только в том случае, если TextView уже имеет Layout ( mLayout != null ). (Эта проверка запрещает исключение из этого исключения, а не mHandlingLayoutInLayoutRequest в ViewRootImpl .)

Итак, опять же, почему TextView иногда не имеет Layout ? Или лучше, поскольку, очевидно, у него начинается не наличие одного, когда и откуда оно получается?

Когда TextView первоначально добавляется в LinearLayout используя layout.addView(tv); , Снова requestLayout() цепочка requestLayout() , перемещаясь вверх по иерархии View , заканчивая в ViewRootImpl , где на этот раз не ViewRootImpl исключение, потому что мы все еще находимся в потоке пользовательского интерфейса. Здесь ViewRootImpl затем вызывает scheduleTraversals() .

Важная часть здесь заключается в том, что это сообщение обратного вызова / Runnable на очереди сообщений Choreographer , которое обрабатывается «асинхронно» с основным потоком выполнения:

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

Choreographer конечном итоге обработает это с помощью Handler и запустит все Runnable ViewRootImpl , которое в конечном итоге вызовет функции performTraversals() , measureHierarchy() и performMeasure() (на ViewRootImpl ), которые будут выполнять еще одну серию View.measure() , onMeasure() (и несколько других), перемещаясь по иерархии View до тех пор, пока она наконец не достигнет нашего TextView.onMeasure() , который вызывает makeNewLayout() , который вызывает makeSingleLayout() , который, наконец, устанавливает переменную-член mLayout :

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

После этого mLayout больше не имеет значения, и любая попытка изменить TextView , то есть вызов setText() как в нашем примере, приведет к известному CalledFromWrongThreadException .

Итак, у нас здесь хорошее состояние гонки, если наша AsyncTask может получить AsyncTask к TextView до того, как TextView Choreographer , он может изменить его без штрафов. Конечно, это по-прежнему плохая практика, и ее не следует делать (есть много других сообщений SO, посвященных этому), но если это происходит случайно или неосознанно, CalledFromWrongThreadException не является идеальной защитой.

Этот надуманный пример использует TextView и детали могут отличаться для других представлений, но общий принцип остается тем же. Остается увидеть, может ли какая-либо другая реализация представления (возможно, requestLayout() ), которая не вызывает requestLayout() в каждом случае может быть изменена без штрафов, что может привести к большим (скрытым) проблемам.

Вы можете писать в doInBackground в TextView, если он еще не является частью GUI.

Это только часть GUI после оператора setContentView(layout); ,

Просто моя мысль.