最近碰到一件奇怪的事情,原来在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的分析。