前言
Android系统被设计为单线程模型,当应用启动Zygote
fork出进程,并在进程中创建一个主线程(UI线程),都知道UI线程是不能做耗时操作的。例如网络请求,操作数据库等,否则就会出现ANR错误。UI组件以及UI操作是线程不安全的,规定除了主线程外的线程不能够对UI进行操作。如果设计成线程安全的,势必需要涉及到加锁等一些机制,大大降低反馈效率,对于移动设备,用户体验显然是不好,另外多线程操作UI变得更加不可控。历史原因 Why are most UI frameworks single threaded?
为什么在Activity的onCreate()方法中对TextView操作没有报出异常?
相信很多人被问到过这样的问题1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
setContentView(binding.getRoot());
uiTest();
}
private void uiTest() {
Thread thread = new Thread(new Runnable() {
public void run() {
//SystemClock.sleep(1000);
binding.tvHandler.setText("子线程更新UI");
}
});
thread.start();
}
1.真实测试时,这段代码是不会报出异常的Only the original thread that created a view hierarchy can touch its views.
,但是如果延时操作SystemClock.sleep(1000);
还是会出现那个熟悉的异常信息。WTF???
ViewRootImpl何时被创建?
1.Only the original thread that created a view hierarchy can touch its views.
异常信息出现在ViewRootImpl
中,并且在方法中:1
2
3
4
5
6
7
8
9/**
* 省略...
*/
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
1.可以看到正是这个checkThread()
方法,当对UI组件进行操作时,判断了是否在主线程,不是则抛出异常。由此可见上述虽然在子线程中“更新了UI”,但是延迟操作还是会抛出异常,说明checkThread()
方法的调用时机,或者说ViewRootImpl
实例化时机跟Activity
的生命周期存在一定的关系。
2.显然在onCreate()
方法调用时ViewRootImpl
还没有被初始化,猜测ViewRootImpl
在onResume()
方法时被初始化,进入Activity
源码中找到对应方法handleResumeActivity
1 | //... |
3.WindowManager
的实现类WindowManagerImpl
在Activity
创建后会将DecorView
添加到window
中,而ViewRootImpl
是连接WindowManager
与DecorView
的纽带,View
的测量、绘制均是通过ViewRootImpl
来完成。进入到WindowManagerImpl
找到addView()
方法:
1 | //WindowManagerImpl |
4.WindowManagerGlobal
中的addView()
具体实现:方法主要对ViewRootImpl
做了初始化,并且调用了setView()
方法,而requestLayout()
在setView()
内部被调用,对线程做检查。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//WindowManagerGlobal 中addView()
public void addView(View view, ViewGroup.LayoutParams params,Display display, Window parentWindow) {
//省略代码。。。
ViewRootImpl root;
View panelParentView = null;
synchronized (mLock) {
// Start watching for system property changes.
if (mSystemPropertyUpdater == null) {
mSystemPropertyUpdater = new Runnable() {
public void run() {
synchronized (mLock) {
for (int i = mRoots.size() - 1; i >= 0; --i) {
mRoots.get(i).loadSystemProperties();
}
}
}
};
SystemProperties.addChangeCallback(mSystemPropertyUpdater);
}
int index = findViewLocked(view, false);
if (index >= 0) {
if (mDyingViews.contains(view)) {
// Don't wait for MSG_DIE to make it's way through root's queue.
mRoots.get(index).doDie();
} else {
throw new IllegalStateException("View " + view
+ " has already been added to the window manager.");
}
// The previous removeView() had not completed executing. Now it has.
}
// If this is a panel window, then find the window it is being
// attached to for future reference.
if (wparams.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
wparams.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
final int count = mViews.size();
for (int i = 0; i < count; i++) {
if (mRoots.get(i).mWindow.asBinder() == wparams.token) {
panelParentView = mViews.get(i);
}
}
}
//此时对viewrootompl做了初始化
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
// do this last because it fires off messages to start doing things
try {
//当调用setview时,内部会调用到requestLayout
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
// BadTokenException or InvalidDisplayException, clean up.
if (index >= 0) {
removeViewLocked(index, true);
}
throw e;
}
}
}
5.ViewRootImpl
中的相关方法实现
1 | //ViewRootImpl |
总结
最终回到ViewRootImpl
中对view的测量、布局、绘制。上述表面上在子线程中“更新了UI”,与其说是更新,不如理解为是对view初始属性的设置。因为此时ViewRootImpl
还未被初始化,可以推论在onCreate-> onStop-> onResume
这个过程中,在onResume
之前上述操作有可能都是成立的。类似的问题还有:在onCreate
方法中获取View
的宽高是零,而调用view.post()
就能获取宽高的值。