最近碰到一件奇怪的事情,原来在android4.2下面跑完全没有问题的代码在4.4下面会出现如下异常:
01-17 13:06:25.087: E/AndroidRuntime(12673): android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
01-17 13:06:25.087: E/AndroidRuntime(12673): at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:6094)
01-17 13:06:25.087: E/AndroidRuntime(12673): at android.view.ViewRootImpl.doDie(ViewRootImpl.java:5333)
01-17 13:06:25.087: E/AndroidRuntime(12673): at android.view.ViewRootImpl.die(ViewRootImpl.java:5318)
01-17 13:06:25.087: E/AndroidRuntime(12673): at android.view.WindowManagerGlobal.removeViewLocked(WindowManagerGlobal.java:346)
01-17 13:06:25.087: E/AndroidRuntime(12673): at android.view.WindowManagerGlobal.removeView(WindowManagerGlobal.java:301)
01-17 13:06:25.087: E/AndroidRuntime(12673): at android.view.WindowManagerImpl.removeViewImmediate(WindowManagerImpl.java:84)
01-17 13:06:25.087: E/AndroidRuntime(12673): at android.app.Dialog.dismissDialog(Dialog.java:329)
01-17 13:06:25.087: E/AndroidRuntime(12673): at android.app.Dialog$1.run(Dialog.java:121)
01-17 13:06:25.087: E/AndroidRuntime(12673): at android.os.Handler.handleCallback(Handler.java:733)
01-17 13:06:25.087: E/AndroidRuntime(12673): at android.os.Handler.dispatchMessage(Handler.java:95)
01-17 13:06:25.087: E/AndroidRuntime(12673): at android.os.Looper.loop(Looper.java:136)
抛出异常为CalledFromWrongThreadException,很明显第一反应就是出现了非ui线程进行了ui操作造成了此异常。但是对于4.2下面不报错不是又说不通了么~
由此开始调查
1)非ui线程执行ui操作是否必然报错?
==》在ViewRootImpl代码中查看得知
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
上面的代码可以看到执行到checkThread方法是要是不报错必须
mThread==Thread.currentThread
。而mThread是该类的一个属性,声明如下
final Thread mThread;
很明显要么是在构造函数中要么是和声明一起完成初始化。
这也就表示viewRootImpl必然要是在和执行checkThread的线程里完成初始化。也就是只有创建该view的线程中才可以在执行checkThread方法不报错。当然对于大部分应用程序来说主要还是在ui线程里。因此才有了上诉说法非ui线程执行ui操作会报错。抛出CalledFromWrongThreadException
2.为何之前在4.2版本中非ui线程中执行ui操作不会报错?此处说明操作对象为ProgressDialog。而代码报错部分为ProgressDialog的dismiss部分。
查看源码=》Dialog对于ui操作有特别说明
/**
* Dismiss this dialog, removing it from the screen. This method can be
* invoked safely from any thread. Note that you should not override this
* method to do cleanup when the dialog is dismissed, instead implement
* that in {@link #onStop}.
*/
很明显,看起来dialog对于ui操作做了特别处理。详细看看代码
@Override
public void dismiss() {
if (Looper.myLooper() == mHandler.getLooper()) {
dismissDialog();
} else {
mHandler.post(mDismissAction);
}
}
Looper看来,当前执行dismiss操作的线程如果和mHandler所依附的线程不一致的话那么就会将dismiss操作丢到对应的mHandler的线程队列中等待执行。那么这个Handler又是哪里来的呢?和ViewRootImpl类似,又是一个final的Handler。当然又是可以分析得出,该Handler和new Dialog的线程应该是有直接关系的。分析后很明显会有如下结论。当该Dialog如果在UI线程中进行初始化,那么无论对该Dialog进行ui操作都不会抛出该异常(此结论是基于原先业界盛传的在ui线程操作ui)。很不幸的是该代码写成如下依旧会报错(4.4的机器上,4.2机器不会报错)
public class MyActivity extends Activity {
private ProgressDialog mProgressDialog = null;
private Handler mHandler = null;
/**
* Called when the activity is first created.
*/
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
mProgressDialog = new ProgressDialog(MyActivity.this);
HandlerThread handlerThread = new HandlerThread("atthread");
handlerThread.start();
mHandler = new Handler(handlerThread.getLooper());
mHandler.post(new AtThread());
}
public class AtThread implements Runnable {
@Override
public void run() {
mProgressDialog.setMessage(getResources().getString(R.string.app_name));
mProgressDialog.show();
}
}
看上去好像很奇怪,明明是会将ui操作丢到了主线程中啊。
那么继续分析如下。
首先4.2和4.4中同样的代码执行结果却不一样。那么第一件想到的事就是想必4.4中修改了部分源码导致报错了。那么就diff好了。得出如下结果。
4.2中Dialog的dismissDialog和4.4中Dialog的dismissDialog区别如下
try {
mWindowManager.removeView(mDecor);
}
===》
try {
mWindowManager.removeViewImmediate(mDecor);
}
莫非就是这个区别?继续跟踪下去最终在发现WindowManagerGlobal类中方法
removeViewLocked有如下一句
boolean deferred = root.die(immediate);
继续查看得之ViewRootImpl die方法如下
boolean die(boolean immediate) {
// Make sure we do execute immediately if we are in the middle of a traversal or the damage
// done by dispatchDetachedFromWindow will cause havoc on return.
if (immediate && !mIsInTraversal) {
doDie();
return false;
}
……
return true;
}
看到了熟悉的报错的地方了,progressDialog报错的堆栈不也是显示在做doDie的时候checkThread失败了么。换句话说ViewRootImpl本生的thread和handler不是在同一个线程里(1说明了viewRootImpl的mThread的由来)。而之前4.2的时候调用的api是removeView最终不会执行到doDie方法这也顺利的解释了为什么4.2的版本不会挂掉。而4.4的版本却会出现挂掉的情况。
3.到此处就顺利的分析完成了???NO,NO。在各种实验中发现了如下的奇特的情况。
将实验代码改成如下在4.4下面也不会报错
public class AtThread implements Runnable {
@Override
public void run() {
mProgressDialog.setMessage(getResources().getString(R.string.app_name));
runOnUiThread(new Runnable() {
@Override
public void run() {
mProgressDialog.show();
}
}); }
}
奇怪的事情总是不起而至啊,沙普莱斯啊(忍术名,看过爱4的都明白)~上面的分析已经很明白了就是dismiss执行checkThread会报错。而将show方法包在了runOnUiThread就不会报错这又是为毛?可以想到的是必然是将在checkThread中mThread==Thread.currentThread。Thread.currentThread就是为Dialog中mHandler所依附的Thread。那么mThread应该是发生了变化。那么在看一下源码好了。可以看removeViewImmediate(mDecor)不会报错,那么说明mDecor的viewRootImpl中的Thread和主线程应该是一致的。=》
public void show() {
……
mDecor = mWindow.getDecorView();
……
}
上面这一句mWindow.getDecorView()执行会做以下操作,如果存在对应的view就直接返回出来,否则就会new出对应的view。这就是关键。在那个线程new出view那么view里的viewroot就会保存这对应线程的引用。也就是说最终在checkThread的时候将会直接影响是否抛出异常。所以如果将show放到了主线程中去完成,那么最终4.4上就不会抛出异常。
方法1:
将dialog的show方法放在ui线程中执行
方法2:
将dialog的初始化放在子线程里执行
以上两种都不会出现异常。
至此已基本完成该bug的分析。