文章目录
前言
我们从事Android开发以来,一直都有一个规定就是不能在子线程更新UI,否则会报一个: “Only the original thread that created a view hierarchy can touch its views.” 的错误,但是真的不能在子线程更新UI吗,答案是可以的,只要我们能绕开那个报错的检测方法即可。
一、探寻为什么会报错
首先我们写一个简单布局,带一个Button
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<Button
android:id="@+id/btn_top_tab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="TopTab"
/>
</FrameLayout>
然后写一个子线程更新UI的操作:
val topBtn = mLayoutView.findViewById<Button>(R.id.btn_top_tab)
topBtn.setOnClickListener {
thread {
topBtn.text = "改一下"
}
}
运行后,点击Button,会有报错信息:
我们整理一下堆栈信息
- Button: setText()
- TextView : setText() -> checkForRelayout() -> requestLayout()
- 循环调用父类的requestLayout()
if (mParent != null && !mParent.isLayoutRequested()) {
mParent.requestLayout();
}
- ViewRootImpl: requestLayout()
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
- ViewRootImpl:checkThread()
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
发现最后报错是在ViewRootImpl类的checkThread()。
二、子线程更新UI思路
根据对报错堆栈信息的分析,我们可以总结一下2个思路去实现子线程更新UI
1. 让checkThread()不报错
现在报错的原因是ViewRootImpl是在主线程即UI线程创建的,而我们要在子线程更新UI,导致了报错。那么如果我们在子线程创建了ViewRootImpl,那么我们在这个子线程中更新UI,checkThread方法不就不会报错了吗。
2. 绕开checkThread()方法
如果能规避checkThread()的调用,就不会报错了。
(1) 直接绕开ViewRootImpl,让View的getParent为null
首先我们要先了解View的绘制流程,不是很清楚的同学可以看一下我的另一篇文章,我们在onCreate()中调用setContentView()方法后,会生成DecorView,这是Window的最顶层布局,而DecorView也有一个Parent,就是ViewRootImpl了。那么DecorView是如何与ViewRootImpl关联起来的呢,是在ViewRootImpl的setView()中有一行
view.assignParent(this);
此处的view就是DecorView,而this就是ViewRootImpl本身。而触发ViewRootImpl的setView()方法是从ActivityThread的handleResumeActivity(),代码如下:
ActivityThread.class
@Override
public void handleResumeActivity(ActivityClientRecord r, boolean finalStateRequest,
boolean isForward, String reason) {
......
//触发Activity的onResume
if (!performResumeActivity(r, finalStateRequest, reason)) {
return;
}
......
if (r.window == null && !a.mFinished && willBeVisible) {
......
if (a.mVisibleFromClient) {
if (!a.mWindowAdded) {
a.mWindowAdded = true;
//触发WindowManagerImpl的addView,进而触发WindowManagerGlobal的addView()
//进而创建了ViewRootImpl并调用了它的setView()
wm.addView(decor, l);
} else {
a.onWindowAttributesChanged(l);
}
}
}
......
}
所以我们能看到,真正让DecorView和ViewRootImpl关联起来是在Activity的onResume()被调用之后,所以在此之前更新UI,就可以让DecorView的getParent返回null,就可以避开checkThread()。
(2) 让mParent.isLayoutRequested()为true
先看一下isLayoutRequested()源码
//判断是否有PFLAG_FORCE_LAYOUT标志
public boolean isLayoutRequested() {
return (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
}
我们在View的requestLayout()方法里有这样一行代码
public void requestLayout() {
......
//即调用requestLayout时,就会在mPrivateFlags中添加PFLAG_FORCE_LAYOUT标识
mPrivateFlags |= PFLAG_FORCE_LAYOUT;
......
}
那么,如果我们在主线程中直接手动调用requestLayout,接着在子线程更新UI,那么就可以
让mParent.isLayoutRequested()为true
(3) 直接绕开View的requestLayout()
TextView源码中有一个checkForRelayout()方法,去判断是否调用View的requestLayout(),
private void checkForRelayout() {
//如果宽度不是WRAP_CONTENT
if ((mLayoutParams.width != LayoutParams.WRAP_CONTENT
|| (mMaxWidthMode == mMinWidthMode && mMaxWidth == mMinWidth))
&& (mHint == null || mHintLayout != null)
&& (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight() > 0)) {
......
if (mEllipsize != TextUtils.TruncateAt.MARQUEE) {
//如果高度不是WRAP_CONTENT和MATCH_PARENT
if (mLayoutParams.height != LayoutParams.WRAP_CONTENT
&& mLayoutParams.height != LayoutParams.MATCH_PARENT) {
autoSizeText();
//直接刷新空间,不走requestLayout()
invalidate();
return;
}
//高度没有变化
if (mLayout.getHeight() == oldht
&& (mHintLayout == null || mHintLayout.getHeight() == oldht)) {
autoSizeText();
//直接刷新空间,不走requestLayout()
invalidate();
return;
}
}
//需要重新layout一下
requestLayout();
invalidate();
} else {
nullLayouts();
//需要重新layout一下
requestLayout();
invalidate();
}
}
然后我们进入invalidate(),层层调用走到ViewGroup的invalidateChild()
ViewGroup.class
public final void invalidateChild(View child, final Rect dirty) {
final AttachInfo attachInfo = mAttachInfo;
//如果开启硬件加速
if (attachInfo != null && attachInfo.mHardwareAccelerated) {
//最后会递归到ViewRootImpl的onDescendantInvalidated(),
//然后调用ViewRootImpl的invalidate()
onDescendantInvalidated(child, child);
return;
}
......
}
ViewRootImpl.class
void invalidate() {
mDirty.set(0, 0, mWidth, mHeight);
if (!mWillDrawSoon) {
//此处直接开启了View绘制的三大流程,并没有调用checkThread()
scheduleTraversals();
}
}
三、代码实践
1. 子线程创建ViewRootImpl
thread {
Looper.prepare() //需要Looper是因为ViewRootImpl中会创建Handler
val btn = Button(context)
btn.text = "点我"
btn.setOnClickListener {
btn.text = "改一下"
}
windowManager.addView(btn,WindowManager.LayoutParams().apply {
this.width = WindowManager.LayoutParams.WRAP_CONTENT
this.height = WindowManager.LayoutParams.WRAP_CONTENT
})
Looper.loop()
}
2. 在onCreate()中更新UI
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val topBtn = findViewById<Button>(R.id.btn_top_tab)
thread {
topBtn.text = "改一下"
}
}
3. 更新UI之前调用一次requestLayout
val topBtn = mLayoutView.findViewById<Button>(R.id.btn_top_tab)
topBtn.setOnClickListener {
it.requestLayout()
thread {
topBtn.text = "改一下"
}
}
4. 开启硬件加速并设置控件的宽高为具体值
当TargetSDK > 14时,默认是开启硬件加速的,即 android:hardwareAccelerated=“true”
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<Button
android:id="@+id/btn_top_tab"
android:layout_width="200dp"
android:layout_height="100dp"
android:text="TopTab"
/>
</FrameLayout>
5. SurfaceView
SurfaceView是Android中一种特殊的控件,拥有独立的绘图表面,不与其宿主窗口共享同一个绘图表面,所以可以在独立的线程中进行绘制。具体使用方法我就不介绍了。
总结
这篇文章主要还是分析为何子线程更新UI会导致报错,并不是让大家使用这种方式,主要还是分析View的绘制流程。
以上内容,如有问题,请指正