子线程不可以更新UI吗

子线程不可以更新UI吗?(2019-11-05)

这里文章基于Android 系统8.0及以上版本分析子线程更新UI并没有报错的原因,其它版本并没有出现这种情况可以参考这篇文章子线程能更新UI吗。经常看到文章说不要在子线程更新UI,不要在UI线程进行耗时操作。偶然间的尝试发现了一些奇怪的问题,先分析第一个问题子线程中可以更新UI吗?下面是我的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
textView = findViewById(R.id.textView3);
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
textView.setText("子线程可以更新UI吗?");
}
}).start();
}

点击运行之后,并没有出现我们预料中的崩溃和异常信息问题,即便我们在子线程中做了一些耗时操作。首先我们看一下我们希望预料之中出现的错误,然后根据堆栈分析错误信息为什么没有出现。

1
2
3
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:7753)
at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1225)

根据堆栈信息分析,可以看出错误信息是在 ViewRootImpl.checkThread() 方法中抛出,我们先来看一下 checkThread() 方法:

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

方法很简单判断 mThread(即 View 初始化所在线程)和当先线程比较,不是同一个线程就抛出以上异常信息。很明显我们之前的子线程和主线程的 TextView 并处在同一线程,但是为什么日志中没有看到对应的异常信息了,首先我们看一下 ViewRootImpl 中哪些方法调用了 checkThread() 方法,如下所示:

截屏2019-11-05下午3.04.26

上述代码我们使用的是TextView,这里我们只分析父类View抛出异常信息的触发条件。我们挑选常见的 ViewRootImpl#requstLayout() 方法来分析具体的原因。我们来看一下 View#requestLayout() 方法:

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
//View.java
public void requestLayout() {
if (mMeasureCache != null) mMeasureCache.clear();

if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
// Only trigger request-during-layout logic if this is the view requesting it,
// not the views in its parent hierarchy
ViewRootImpl viewRoot = getViewRootImpl();
if (viewRoot != null && viewRoot.isInLayout()) {
if (!viewRoot.requestLayoutDuringLayout(this)) {
return;
}
}
mAttachInfo.mViewRequestingLayout = this;
}

mPrivateFlags |= PFLAG_FORCE_LAYOUT;
mPrivateFlags |= PFLAG_INVALIDATED;

if (mParent != null && !mParent.isLayoutRequested()) {
mParent.requestLayout();
}
if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
mAttachInfo.mViewRequestingLayout = null;
}
}

看一下第20行 mParent.requestLayout() 方法,其中 mParent 属于ViewParent 接口对象。从这篇文章关于View中mParent的来龙去脉中我们可以知道 mParent 为 ViewRootImpl(实现ViewParent接口)的具体实现,所以当我们调用 view#requestLayout 方法会出现预料之中的错误。其它的触发方法可以由此分析。我的总结是只有触发某些特定方法类似 View#requestLayout 等以及子类View比如TextView#setCompoundDrawables() 方法,才会出现某些异常退出,所以不推荐在子线程更新UI操作。(仅适用于Android 8以上设备。Android 7没有测试,Android 6.0 设备会出现异常信息并崩溃)。而我们上述的TextView#setText方法最终并没有调用checkThread()方法(不是某些文章提出的 ViewRootImpl 在 onResume 方法之前的原因,已验证 ),具体的原因可以对比一下Android6.0和Android8.0版本setText的源码和调试执行。待续…

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×