由于“众所周知”,您无法从另一个线程修改UI元素,例如一个AsyncTask的doInBackground().
例如:
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: An error occured while executing doInBackground()
…
11:21:36.630: E/AndroidRuntime(23922): Caused by: android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
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!”屏幕上.哇.没想到这个.
也许活动生命周期过早?让我们把执行移到onResume().
@Override protected void onResume() { super.onResume(); new MyAsyncTask().execute(tv); }
与上述相同的行为.
好的,我们把它放在处理程序上.
@Override protected void onResume() { super.onResume(); Handler handler = new Handler(); handler.post(new Runnable() { @Override public void run() { new MyAsyncTask().execute(tv); } }); }
同样的行为.我用完了想法,并尝试延迟1秒的postDelayed():
@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: Only the original thread that created a view hierarchy can touch its views.
哇,这是与时间有关吗?
我尝试不同的延迟,似乎对于这个特定的测试运行,在这个特定的设备(Nexus 4,运行5.1)上,魔术数字是60ms,即有时会抛出异常,有时它更新TextView,就好像没有发生任何事情.
我认为当在AsyncTask被修改的时候,没有完全创建视图层次结构时,会发生这种情况.它是否正确?有更好的解释吗? Activity上有一个回调,可以用来确保视图hierachy已经被完全创建了吗?与时间有关的问题是可怕的.
我在这里发现了一个类似的问题Altering UI thread’s Views in AsyncTask in doInBackground,CalledFromWrongThreadException not always thrown,但没有解释.
更新:
由于在评论和建议的答案中的请求,我添加了一些调试日志来确定事件链…
public class MainActivity extends Activity { private TextView tv; public class MyAsyncTask extends AsyncTask<TextView,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): before setContentView
10:01:33.137: D/MainActivity(18386): after setContentView,before execute
10:01:33.148: D/MainActivity(18386): after execute
10:01:33.153: D/MyAsyncTask(18386): before setText
10:01:33.153: D/MyAsyncTask(18386): after setText
一切如预期的,这里没有什么不寻常的,setContentView()在执行execute()之前完成,这又在doInBackground()中调用setText()之前完成.所以不是这样.
更新:
另一个例子:
public class MainActivity extends Activity { private LinearLayout layout; private TextView tv; public class MyAsyncTask extends AsyncTask<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); } }
这一次,我在AsyncTask上调用execute()之前,立即在Button的onClick()中添加TextView.在这个阶段,初始布局(没有TextView)已经正确显示(即我可以看到按钮并点击它).再次,没有抛出异常.
和柜台的例子,如果我添加Thread.sleep(100);在doInBackground()中的setText()之前的execute()中抛出通常的异常.
我刚刚注意到的另外一件事就是,在异常抛出之前,TextView的文本实际上已经被更新了,只要一秒钟就可以正常显示,直到应用程序自动关闭.
我想有事情必须发生(异步地,即从任何生命周期方法/回调分离)到我的TextView,以某种方式“附加”到ViewRootImpl,这使得后者抛出异常.有人有任何解释或指针,进一步的文件说明什么是“某事”?
解决方法
这个检查使用成员mHandlingLayoutInLayoutRequest进行抑制,直到performLayout(),即所有的初始绘图遍历都完成.
因此只有当我们使用延迟时才会抛出异常.
不知道这是否是android或故意的bug?