Android4.4DialogUI线程CalledFromWrongThreadExcection

最近碰到一件奇怪的事情,原来在android4.2下面跑完全没有问题的代码在4.4下面会出现如下异常:

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
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代码中查看得知

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.");
        }
}

上面的代码可以看到执行到checkThread方法是要是不报错必须

1
mThread==Thread.currentThread

。而mThread是该类的一个属性,声明如下

1
final Thread mThread;

很明显要么是在构造函数中要么是和声明一起完成初始化。

这也就表示viewRootImpl必然要是在和执行checkThread的线程里完成初始化。也就是只有创建该view的线程中才可以在执行checkThread方法不报错。当然对于大部分应用程序来说主要还是在ui线程里。因此才有了上诉说法非ui线程执行ui操作会报错。抛出CalledFromWrongThreadException

2.为何之前在4.2版本中非ui线程中执行ui操作不会报错?此处说明操作对象为ProgressDialog。而代码报错部分为ProgressDialog的dismiss部分。

查看源码=》Dialog对于ui操作有特别说明

1
2
3
4
5
6
/**
     * 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操作做了特别处理。详细看看代码

1
2
3
4
5
6
7
8
 @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机器不会报错)

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
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区别如下

1
2
3
4
try {
            mWindowManager.removeView(mDecor);
        }

===》

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
 try {
            mWindowManager.removeViewImmediate(mDecor);
        }
``` plain
 
莫非就是这个区别?继续跟踪下去最终在发现WindowManagerGlobal类中方法
removeViewLocked有如下一句
```java 
boolean deferred = root.die(immediate);
``` plain
 
继续查看得之ViewRootImpl  die方法如下
 
```java
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;
}
``` plain
 
看到了熟悉的报错的地方了,progressDialog报错的堆栈不也是显示在做doDie的时候checkThread失败了么。换句话说ViewRootImpl本生的thread和handler不是在同一个线程里(1说明了viewRootImpl的mThread的由来)。而之前4.2的时候调用的api是removeView最终不会执行到doDie方法这也顺利的解释了为什么4.2的版本不会挂掉。而4.4的版本却会出现挂掉的情况。
3.到此处就顺利的分析完成了???NO,NO。在各种实验中发现了如下的奇特的情况。
将实验代码改成如下在4.4下面也不会报错
 
```java
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和主线程应该是一致的。=》

1
2
3
4
5
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的分析。