Android 如何让子线程更新UI (仅分析,不建议使用)


前言

我们从事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,会有报错信息:
在这里插入图片描述
我们整理一下堆栈信息

  1. Button: setText()
  2. TextView : setText() -> checkForRelayout() -> requestLayout()
  3. 循环调用父类的requestLayout()
if (mParent != null && !mParent.isLayoutRequested()) {
    mParent.requestLayout();
}
  1. ViewRootImpl: requestLayout()
@Override
public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        checkThread();
        mLayoutRequested = true;
        scheduleTraversals();
    }
}
  1. 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的绘制流程。

以上内容,如有问题,请指正

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ByeMoon丶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值