【Android】Dialog异常CalledFromWrongThreadException深入分析

问题

在使用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
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) {
//UI主线程中show,然后点击空白区域dismiss
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
/**
* 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}.
*/
@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()方法。

而它又调用的是WindowManagerGlobalremoveView()函数:

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) {
// 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;
}

//...省略其余代码...
}



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什么时候调用这个构造函数创建实例的呢?我们刚才在WindowManagerGlobalremoveView()函数中,看到了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);
}

// do this last because it fires off messages to start doing things
try {
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
// BadTokenException or InvalidDisplayException, clean up.
synchronized (mLock) {
final int index = findViewLocked(view, false);
if (index >= 0) {
removeViewLocked(index, true);
}
}
throw e;
}
}

而这个addView方法什么时候会调用呢?就是WindowManagerImpl

就是刚才分析Dialog中mWindowManager对象的来历时,知道了它其实是WindowManagerImpl类的一个实例,WindowManagerImpl会通过WindowManagerGlobalremoveView()方法去实现removeView。同理,此处WindowManagerGlobaladdView()方法也是被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时ViewRootImplcheckThread(),保证addView的线程才能removeView。而在文章开头出错的例子中,Dialog的show()是在主线程执行,new Dialog()是在work子线程中执行的,所以抛出了CalledFromWrongThreadException的异常。

结论

  1. Dialog的dismiss()会首先被抛到new Dialog的线程中执行。

  2. 只要保证创建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
//4.2中Dialog的dismissDialog
try {
mWindowManager.removeView(mDecor);
}
1
2
3
4
//4.4中Dialog的dismissDialog
try {
mWindowManager.removeViewImmediate(mDecor);
}

参考资料

Powered by Hexo and Hexo-theme-hiker

Copyright © 2013 - 2019 iTimeTraveler All Rights Reserved.

访客数 : | 访问量 :