问题 在使用Dialog时,因为线程问题,在调用dismiss方法时出现了CalledFromWrongThreadException的Crash,如下:
1 android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
抛出异常为CalledFromWrongThreadException
,很明显第一反应就是出现了非ui线程进行了ui操作造成了此异常。通过分析工程代码,发现本质上是因为在非ui线程中创建了Dialog,而在主线程(即ui线程)中调用了show()
以及dismiss()
方法,我把问题模型写成测试代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 public class MainActivity extends BaseActivity { private static final String TAG = "MainActivity test" ; private ProgressDialog dialog; @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); EventBus.getDefault().register(this ); new Thread(new Runnable() { @Override public void run () { Looper.prepare(); dialog = new ProgressDialog(MainActivity.this ); dialog.setCanceledOnTouchOutside(true ); dialog.setOnCancelListener(new DialogInterface.OnCancelListener() { @Override public void onCancel (DialogInterface dialog) { Log.d(TAG, "Dialog onCancel thread: " + getThreadInfo()); } }); dialog.setOnDismissListener(new DialogInterface.OnDismissListener() { @Override public void onDismiss (DialogInterface dialog) { Log.d(TAG, "Dialog onDismiss thread: " + getThreadInfo()); } }); dialog.setMessage("正在加载..." ); Log.d(TAG, "Dialog create thread: " + getThreadInfo()); Looper.loop(); } }).start(); Button btn = (Button) findViewById(R.id.btn_helloworld); btn.setOnClickListener(new View.OnClickListener() { @Override public void onClick (View v) { dialog.show(); Log.d(TAG, "Dialog show thread: " + getThreadInfo()); } }); } private String getThreadInfo () { return "[" + Thread.currentThread().getId() + "]" + ((Looper.myLooper() == Looper.getMainLooper())? " is UI-Thread" : "" ); } }
就是Activity打开的时候,使用work子线程创建了一个Dialog,然后手动点击按钮的时候,显示Dialog。再点击空白处,dialog本应该dismiss的,但是直接crash了。抛出了CalledFromWrongThreadException
的异常。
在上面的代码中,我顺便输出了Dialog每个操作的线程ID,同时会判定是不是ui主线程。我们来看看log:
10-26 16:11:07.836 7405-7652/com.cuc.myandroidtest D/MainActivity test: Dialog create thread: [3953] 10-26 16:11:27.763 7405-7405/com.cuc.myandroidtest D/MainActivity test: Dialog show thread: [1] is UI-Thread 10-26 16:11:35.642 7405-7652/com.cuc.myandroidtest D/MainActivity test: Dialog onCancel thread: [3953]
——– beginning of crash
可以看到,以上出现的问题中执行Dialog操作的线程信息如下:
创建Dialog:work子线程
show():ui主线程
cancel():work子线程
dismiss():因为crash没有执行到,未知
如果说只有创建这个控件的线程才能去更新该控件的内容 。那么在调用show方法的时候为什么不会crash,然后dismiss的时候才会崩溃?
另外,到底是不是所有的操作都必须放到ui线程中执行才对?带着疑问我们深入Dialog源码一看究竟。
源码分析 我们先看Dialog的dismiss方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Override public void dismiss () { if (Looper.myLooper() == mHandler.getLooper()) { dismissDialog(); } else { mHandler.post(mDismissAction); } } private final Runnable mDismissAction = new Runnable() { public void run () { dismissDialog(); } };
我们先看注释,意思是dismiss()这个函数可以在任意线程中调用,不用担心线程安全问题 。
很明显,dialog对于ui操作做了特别处理。如果当前执行dismiss操作的线程和mHandler所依附的线程不一致的话那么就会将dismiss操作丢到对应的mHandler的线程队列中等待执行。那么这个Handler又是哪里来的呢?
我们开始调查,可以看到mHandler
对象是Dialog类中私有的,会在new Dialog的时候自动初始化:
1 2 3 4 5 6 7 public class Dialog implements DialogInterface, Window.Callback, KeyEvent.Callback, OnCreateContextMenuListener, Window.OnWindowDismissedCallback { private final Handler mHandler = new Handler(); //...省略其余代码... }
可以分析得出,该mHandler直接关联的就是new Dialog的线程。也就能得出以下结论:
结论一 :最终真正执行dismissDialog()
方法销毁Dialog的线程就是new Dialog的线程。
然后我们跟进去dismissDialog()
看看到底如何销毁Dialog的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 void dismissDialog () { if (mDecor == null || !mShowing) { return ; } if (mWindow.isDestroyed()) { Log.e(TAG, "Tried to dismissDialog() but the Dialog's window was already destroyed!" ); return ; } try { mWindowManager.removeViewImmediate(mDecor); } finally { if (mActionMode != null ) { mActionMode.finish(); } mDecor = null ; mWindow.closeAllPanels(); onStop(); mShowing = false ; sendDismissMessage(); } }
可以看出最终调用了mWindowManager.removeViewImmediate(mDecor);
来销毁Dialog,继续跟进removeViewImmediate()
这个方法。发现mWindowManager
的类WindowManager是个abstract的类,我们来找找本尊。
Dialog中mWindowManager对象的来历 发现mWindowManager
这个对象的初始化是在Dialog的构造函数中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 Dialog(Context context, int theme, boolean createContextThemeWrapper) { if (createContextThemeWrapper) { if (theme == 0 ) { TypedValue outValue = new TypedValue(); context.getTheme().resolveAttribute(com.android.internal.R.attr.dialogTheme, outValue, true ); theme = outValue.resourceId; } mContext = new ContextThemeWrapper(context, theme); } else { mContext = context; } mWindowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE); Window w = PolicyManager.makeNewWindow(mContext); mWindow = w; w.setCallback(this ); w.setOnWindowDismissedCallback(this ); w.setWindowManager(mWindowManager, null , null ); w.setGravity(Gravity.CENTER); mListenersHandler = new ListenersHandler(this ); }
它是通过context.getSystemService(Context.WINDOW_SERVICE);
得到的,这里的context肯定就是Activity了,我们去Activity中找getSystemService()
函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 @Override public Object getSystemService (@ServiceName @NonNull String name) { if (getBaseContext() == null ) { throw new IllegalStateException( "System services not available to Activities before onCreate()" ); } if (WINDOW_SERVICE.equals(name)) { return mWindowManager; } else if (SEARCH_SERVICE.equals(name)) { ensureSearchManager(); return mSearchManager; } return super .getSystemService(name); } final void attach (Context context, ActivityThread aThread, Instrumentation instr, IBinder token, int ident, Application application, Intent intent, ActivityInfo info, CharSequence title, Activity parent, String id, NonConfigurationInstances lastNonConfigurationInstances, Configuration config, IVoiceInteractor voiceInteractor) { mWindow.setWindowManager((WindowManager)context.getSystemService(Context.WINDOW_SERVICE), mToken, mComponent.flattenToString(), (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0 ); mWindowManager = mWindow.getWindowManager(); }
我们看到mWindowManager
这个对象是在Activity被创建之后调用attach函数的时候通过mWindow.setWindowManager()
初始化的,而这个函数里干了什么呢?
1 2 3 4 5 6 7 8 9 10 11 public void setWindowManager (WindowManager wm, IBinder appToken, String appName, boolean hardwareAccelerated) { mAppToken = appToken; mAppName = appName; mHardwareAccelerated = hardwareAccelerated || SystemProperties.getBoolean(PROPERTY_HARDWARE_UI, false ); if (wm == null ) { wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE); } mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this ); }
可以看到mWindowManager
这个对象最终来源于WindowManagerImpl
类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public final class WindowManagerImpl implements WindowManager { private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance(); private final Display mDisplay; private final Window mParentWindow; public WindowManagerImpl createLocalWindowManager (Window parentWindow) { return new WindowManagerImpl(mDisplay, parentWindow); } @Override public void addView (View view, ViewGroup.LayoutParams params) { mGlobal.addView(view, params, mDisplay, mParentWindow); } @Override public void removeView (View view) { mGlobal.removeView(view, false ); } @Override public void removeViewImmediate (View view) { mGlobal.removeView(view, true ); } }
在其中我们终于看到了removeViewImmediate()
函数的身影,也就是说,在执行Dialog销毁的函数dismissDialog()
中,最终调用了mWindowManager.removeViewImmediate(mDecor);
来销毁Dialog。实际上调用的就是WindowManagerImpl
实例中的removeViewImmediate()
方法。
而它又调用的是WindowManagerGlobal
的removeView()
函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 public void removeView (View view, boolean immediate) { if (view == null ) { throw new IllegalArgumentException("view must not be null" ); } synchronized (mLock) { int index = findViewLocked(view, true ); View curView = mRoots.get(index).getView(); removeViewLocked(index, immediate); if (curView == view) { return ; } throw new IllegalStateException("Calling with view " + view + " but the ViewAncestor is attached to " + curView); } } private void removeViewLocked (int index, boolean immediate) { ViewRootImpl root = mRoots.get(index); View view = root.getView(); if (view != null ) { InputMethodManager imm = InputMethodManager.getInstance(); if (imm != null ) { imm.windowDismissed(mViews.get(index).getWindowToken()); } } boolean deferred = root.die(immediate); if (view != null ) { view.assignParent(null ); if (deferred) { mDyingViews.add(view); } } }
注意这句boolean deferred = root.die(immediate);
,其中root对象是个ViewRootImpl
的实例,我们看看它的die()
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 boolean die (boolean immediate) { if (immediate && !mIsInTraversal) { doDie(); return false ; } } void doDie () { checkThread(); }
最终,执行到了ViewRootImpl
类的doDie()
方法,这个方法的第一句就是checkThread()
,根据Android4.4DialogUI线程CalledFromWrongThreadExcection 这篇文章,我们知道最终抛出异常的位置就是是在ViewRootImpl
代码中的checkThread
函数:
1 2 3 4 5 6 void checkThread () { if (mThread != Thread.currentThread()) { throw new CalledFromWrongThreadException( "Only the original thread that created a view hierarchy can touch its views." ); } }
也就是说,当调用Dialog的dismiss()
方法时,Dialog会自动抛到new Dialog的线程中执行,而这个线程就是当前的Thread.currentThread()
。换句话说ViewRootImpl本身的mThread和这个new Dialog的线程不是同一个线程。然后我们看看这个ViewRootImpl本身的mThread的来源在何处。
ViewRootImpl中mThread的来历 在ViewRootImpl的构造函数中发现了mThread赋值的地方:
1 2 3 4 5 public ViewRootImpl (Context context, Display display) { mThread = Thread.currentThread(); }
那这个ViewRootImpl什么时候调用这个构造函数创建实例的呢?我们刚才在WindowManagerGlobal
的removeView()
函数中,看到了root
对象是从mRoots
对象中取出来的,而mRoots
是一个ArrayList<ViewRootImpl>
。
所以我们来WindowManagerGlobal
中找找mRoots.add()
的地方,发现是在它的addView()
函数中创建了一个ViewRootImpl
对象并添加到了mRoots
这个list中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public void addView (View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) { ViewRootImpl root; synchronized (mLock) { root = new ViewRootImpl(view.getContext(), display); mRoots.add(root); } try { root.setView(view, wparams, panelParentView); } catch (RuntimeException e) { synchronized (mLock) { final int index = findViewLocked(view, false ); if (index >= 0 ) { removeViewLocked(index, true ); } } throw e; } }
而这个addView
方法什么时候会调用呢?就是WindowManagerImpl
。
就是刚才分析Dialog中mWindowManager
对象的来历时,知道了它其实是WindowManagerImpl
类的一个实例,WindowManagerImpl会通过WindowManagerGlobal
的removeView()
方法去实现removeView。同理,此处WindowManagerGlobal
的addView()
方法也是被WindowManagerImpl调用的。
我们在Dialog的源码中找一下mWindowManager
对象调用addView()
方法的地方,很让人惊喜,它竟然在Dialog的show()
方法中出现了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public void show () { onStart(); mDecor = mWindow.getDecorView(); try { mWindowManager.addView(mDecor, l); mShowing = true ; sendShowMessage(); } finally { } }
也就是说,Dialog的show()
方法,会通过mWindowManager.addView(mDecor, l);
创建一个ViewRootImpl
的对象,这个对象会在创建的时候保存一个当前线程的Thread对象。也就是调用Dialog的show()
方法的线程。
而在调用Dialog的dismiss()
方法时,会首先把它抛到new Dialog的线程中执行,最后通过调用mWindowManager.removeViewImmediate()
来销毁View,此时也就自然调用到了ViewRootImpl
对象的doDie()
方法,这个方法中会checkThread();
,此时会检查当前线程(也就是调用new Dialog的线程)是不是创建ViewRootImpl
的对象的线程(也就是Dialog的show()
方法的线程)。
到这里,本文的bug根源也就找到了说通了。我们再来熟悉一下这个异常的场景。
创建Dialog:work子线程
show():ui主线程
cancel():work子线程
dismiss():因为crash没有执行到,未知(其实是抛到了work子线程)
现在就明确了,执行show()
方法的时候ViewRootImpl
没有checkThread()
,所以不会出现crash。而在执行dismiss()
的时候,它首先被抛到创建Dialog的线程中执行,而后真正销毁View时ViewRootImpl
会checkThread()
,保证addView的线程才能removeView。而在文章开头出错的例子中,Dialog的show()
是在主线程执行,new Dialog()
是在work子线程中执行的,所以抛出了CalledFromWrongThreadException
的异常。
结论
Dialog的dismiss()
会首先被抛到new Dialog的线程中执行。
只要保证创建Dialog和show()
方法在同一个线程中执行,无论是在放到ui线程还是work子线程都可以。
比如,把文章开头的例子中的show()
方法同样放到work线程中,可以正常执行,输出log如下:
10-26 19:23:02.603 27689-27760/com.cuc.myandroidtest D/MainActivity test: Dialog create thread: [4213] 10-26 19:23:02.686 27689-27760/com.cuc.myandroidtest D/MainActivity test: Dialog show thread: [4213] 10-26 19:23:07.243 27689-27760/com.cuc.myandroidtest D/MainActivity test: Dialog onCancel thread: [4213] 10-26 19:23:07.243 27689-27760/com.cuc.myandroidtest D/MainActivity test: Dialog onDismiss thread: [4213]
版本差异 注意,本文的这个CalledFromWrongThreadException
异常,是在4.4版本及以上才会出现的。具体区别可以参考这篇文章:Android4.4DialogUI线程CalledFromWrongThreadExcection
4.2中Dialog的dismissDialog和4.4中Dialog的dismissDialog区别如下:
1 2 3 4 try { mWindowManager.removeView(mDecor); }
1 2 3 4 try { mWindowManager.removeViewImmediate(mDecor); }
参考资料