Изменение вида в 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): at 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): Caused by: android.view.ViewRootImpl $CalledFromWrongThreadException: только исходный поток, создавший иерархию представлений, может коснуться его представлений.
Ничего себе, это связано с хронологией?
Я пробую разные задержки и кажется, что для этого конкретного тестового прогона на этом конкретном устройстве (Nexus 4, работает 5.1) магическое число составляет 60 мс, то есть иногда выдает исключение, иногда оно обновляет TextView
, как будто ничего не случилось.
Я предполагаю, что это происходит, когда иерархия представлений не была полностью создана в точке, где она была изменена с помощью AsyncTask
. Это верно? Есть ли для этого лучшее объяснение? Есть ли обратный вызов на Activity
, который можно использовать, чтобы убедиться, что иерархия представления полностью создана? Сроки связаны с проблемами.
Я нашел аналогичный вопрос здесь Изменяет представления потоков пользовательского интерфейса в AsyncTask в doInBackground, CalledFromWrongThreadException не всегда выбрасывается, но объяснений нет.
Update:
Из-за запроса в комментариях и предлагаемого ответа я добавил несколько протоколов отладки, чтобы выяснить цепочку событий...
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()
завершено до того, как вызывается execute()
, который, в свою очередь, завершается до того, как setText()
вызывается из doInBackground()
. Так что не это.
Update:
Другой пример:
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
, что заставляет последнее генерировать исключение. У кого-нибудь есть объяснение или указатели на дальнейшую документацию о том, что это "что-то"?
Ответы
Ответ 1
Основываясь на ответе RocketRandom, я сделал еще несколько копаний и придумал более полный ответ, который мне кажется оправданным.
Ответственный за возможное исключение действительно ViewRootImpl.checkThread()
, который вызывается при вызове performLayout()
. performLayout()
перемещает иерархию представления до тех пор, пока она не закончится в ViewRootImpl
, но она возникнет в TextView.checkForRelayout()
, которая вызывается setText()
. Все идет нормально. Итак, почему исключение иногда не возникает, когда мы вызываем setText()
?
TextView.checkForRelayout()
вызывается только в том случае, если TextView
уже имеет Layout
(mLayout != null
). (Эта проверка запрещает исключение из этого исключения, а не mHandlingLayoutInLayoutRequest
в ViewRootImpl
.)
Итак, опять же, почему TextView
иногда не имеет Layout
? Или лучше, поскольку, очевидно, у него начинается не наличие одного, когда и откуда оно получается?
Когда TextView
изначально добавляется к LinearLayout
с помощью layout.addView(tv);
, снова вызывается цепочка из requestLayout()
, перемещаясь вверх по иерархии View
, заканчивая в 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
больше не имеет значения null, и любая попытка изменить TextView
, то есть вызов setText()
, как в нашем примере, приведет к известному CalledFromWrongThreadException
.
Итак, у нас здесь хорошее состояние гонки, если наш AsyncTask
может получить руки TextView
до завершения обходов Choreographer
, он может изменить его без штрафов. Конечно, это по-прежнему плохая практика, и ее не следует делать (есть много других сообщений SO, посвященных этому), но если это делается случайно или неосознанно, CalledFromWrongThreadException
не является идеальной защитой.
В этом надуманном примере используется TextView
, и детали могут отличаться для других представлений, но общий принцип остается тем же. Остается выяснить, может ли какая-либо другая реализация View
(возможно, пользовательская), которая не вызывает requestLayout()
, в каждом случае может быть изменена без штрафов, что может привести к большим (скрытым) проблемам.
Ответ 2
Метод checkThread() для ViewRootImpl.java отвечает за исключение этого исключения.
Эта проверка подавляется с использованием члена mHandlingLayoutInLayoutRequest до выполнения функцииLayout(), то есть все начальные обходы чертежа завершены.
поэтому он выдает исключение, только если мы используем задержку.
Не уверен, что это ошибка в андроиде или намеренно:)
Ответ 3
Вы можете писать в doInBackground в TextView, если он еще не является частью GUI.
Это только часть графического интерфейса после оператора setContentView(layout);
.
Просто моя мысль.