前言

说起Android中的Thread与UI,许多Android开发者都看到过这个传言“Android中子线程中不能刷新UI”。但是其实异常是这么描述的:

android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

只有创建这个view hierarchy的原始线程才能操作这些视图。

原因

要探究异常的原因要从Window的添加说起。Window可以理解为视图管理器,它有三种类型,应用Window、子Window和系统Window。应用Window对应一个Activity;子Window不能单独存在,需要附属在特定的父Window中(例如Dialog);Toast和状态栏都是所谓的系统Window,创建需要声明相应的权限。

Activity的创建过程这里不详细叙述,只要知道Activity的创建是在ActivityThread(主线程)中完成的。这其中会创建PhoneWindow(Window实现类),并在setContentView()时将contentView添加到DecorView(FrameLayout)中。至此,PhoneWindow和DecorView还没有发生任何联系。接着ActivityThread会调用Activity的onResume()方法和makeVisible()方法,正是在makeVisible()中完成了PhoneWindow和DecorView的绑定:

1
2
3
4
5
6
7
8
void makeVisible() {
if (!mWindowAdded) {
ViewManager wm = getWindowManager();
wm.addView(mDecor, getWindow().getAttributes());
mWindowAdded = true;
}
mDecor.setVisibility(View.VISIBLE);
}

上述方法中addView()最终会调用WindowManagerGlobal中的addView()方法。它会先创建ViewRootImpl,并将DecorView添加进去。ViewRootImpl构造方法有这么一句:

1
mThread = Thread.currentThread();

最开始提到的view hierarchy指的是ViewRootImpl,而此处的原始线程指的就是ActivityThread。再看最初的异常栈:

1
2
3
4
5
6
7
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:7275)
at android.view.ViewRootImpl.invalidateChildInParent(ViewRootImpl.java:1120)
at android.view.ViewRootImpl.invalidateChild(ViewRootImpl.java:1115)
at android.view.View.invalidateInternal(View.java:12081)
at android.view.View.invalidate(View.java:12041)
at android.view.View.invalidate(View.java:12025)

由于Android中View的设计是非线程安全的,所以要确保只有创建View的线程才能操控它。所以每个View的invalidate()都会最终由它所属的ViewRootImpl去检查是否运行在原始线程。即checkThread():

1
2
3
4
5
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}

理解

那么又为什么说“Android中子线程中不能刷新UI”是不对的呢?如果我们在子线程中构造新的ViewRootImpl呢,试着理解以下代码:

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
public class MainActivity extends AppCompatActivity {
private static final int DELAY_TIME = 2000;
private static final int QUIT_SIGNAL = 4;
private Handler subThreadHandler, mainThreadHandler;
private TextView subThreadTextView;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//init main thread handler
mainThreadHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
subThreadHandler.sendEmptyMessageDelayed(msg.what + 1, DELAY_TIME);
showShortToast(msg.what + "");
//in main thread manipulate sub thread view will cause crush
//subThreadTextView.setText("this will cause crash");
}
};
//add view in sub thread
new Thread() {
@Override
public void run() {
Looper.prepare();
//init sub thread TextView
subThreadTextView = new TextView(MainActivity.this);
subThreadTextView.setTextColor(0xFF000000);
subThreadTextView.setText("subThreadTextView");
//init sub thread handler
subThreadHandler = new Handler(Looper.myLooper()) {
@Override
public void handleMessage(Message msg) {
showShortToast(msg.what + "");
subThreadTextView.setText(subThreadTextView.getText().toString() + ":what is " + msg.what);
if (msg.what > QUIT_SIGNAL) {
Looper.myLooper().quit();
return;
}
mainThreadHandler.sendEmptyMessageDelayed(msg.what + 1, DELAY_TIME);
}
};
//add view
WindowManager wManager = (WindowManager) getApplicationContext().getSystemService(Context.WINDOW_SERVICE);
WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();
mParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;
mParams.format = PixelFormat.TRANSLUCENT;
mParams.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
mParams.width = 600;
mParams.height = 600;
mParams.alpha = 0.5f;
wManager.addView(subThreadTextView, mParams);
Looper.loop();
}
}.start();
//send message to sub thread looper
mainThreadHandler.postDelayed(new Runnable() {
@Override
public void run() {
subThreadHandler.sendEmptyMessageDelayed(0, DELAY_TIME);
}
}, DELAY_TIME);
}

/**
* show a short time toast
*
* @param str
*/

private void showShortToast(String str) {
Toast.makeText(this, str, Toast.LENGTH_SHORT).show();
}

}