子线程中更新UI?

前言

 Android系统被设计为单线程模型,当应用启动Zygotefork出进程,并在进程中创建一个主线程(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
@Override
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() {

@Override
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还没有被初始化,猜测ViewRootImplonResume()方法时被初始化,进入Activity源码中找到对应方法
handleResumeActivity

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
//...
@Override
public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
String reason) {
//省略代码..
if (r.window == null && !a.mFinished && willBeVisible) {
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
l.softInputMode |= forwardBit;
if (r.mPreserveWindow) {
a.mWindowAdded = true;
r.mPreserveWindow = false;
// Normally the ViewRoot sets up callbacks with the Activity
// in addView->ViewRootImpl#setView. If we are instead reusing
// the decor view we have to notify the view root that the
// callbacks may have changed.
ViewRootImpl impl = decor.getViewRootImpl();
if (impl != null) {
impl.notifyChildRebuilt();
}
}
if (a.mVisibleFromClient) {
if (!a.mWindowAdded) {
a.mWindowAdded = true;
//此wm其实是接口WindowManager的实现类 WindowManagerImpl
wm.addView(decor, l);
} else {
// The activity will get a callback for this {@link LayoutParams} change
// earlier. However, at that time the decor will not be set (this is set
// in this method), so no action will be taken. This call ensures the
// callback occurs with the decor set.
a.onWindowAttributesChanged(l);
}
}
}
}

3.WindowManager的实现类WindowManagerImplActivity创建后会将DecorView添加到window中,而ViewRootImpl是连接WindowManagerDecorView的纽带,View的测量、绘制均是通过ViewRootImpl来完成。进入到WindowManagerImpl找到addView()方法:

1
2
3
4
5
6
7
//WindowManagerImpl
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
//mGlobal 即为 WindowManagerGlobal
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}

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() {
@Override 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
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
//ViewRootImpl
/**
* We have one child
*/
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
//代码省略
mAdded = true;
int res; /* = WindowManagerImpl.ADD_OKAY; */

// Schedule the first layout -before- adding to the window
// manager, to make sure we do the relayout before receiving
// any other events from the system.
requestLayout();
//代码省略
}
}

@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}

//熟悉且亲切 - _ -
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}

总结

 最终回到ViewRootImpl中对view的测量、布局、绘制。上述表面上在子线程中“更新了UI”,与其说是更新,不如理解为是对view初始属性的设置。因为此时ViewRootImpl还未被初始化,可以推论在onCreate-> onStop-> onResume这个过程中,在onResume之前上述操作有可能都是成立的。类似的问题还有:在onCreate方法中获取View的宽高是零,而调用view.post()就能获取宽高的值。

这个功能是摆设,看看就好~~~