Я просто столкнулся с каким-то неожиданным поведением при игре с некоторым примером кода.
Как "все знают", вы не можете изменять элементы пользовательского интерфейса из другого потока, например. 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
, что заставляет последнее генерировать исключение. У кого-нибудь есть объяснение или указатели на дальнейшую документацию о том, что это "что-то"?