5种子线程更新UI的方式

前言

众所周知,Android中在UI线程(主线程)才能更新UI(比如操作控件等等),在子线程中更新UI是不被允许的,会直接抛出异常:

在这里插入图片描述

根据调用链能很快定位到报错所在:

在这里插入图片描述

这里会把mThread和当前调用它的线程做比较,不一致则直接抛出异常!但是这个判断是可以通过某些“小手段”绕过去的,接下来就说说4种子线程更新UI的方法

1、onCreate中调用修改UI

	override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        thread {
            tv_discount.text = "setText"
        }
    }

Android的布局流程发生在onResume回调之后,具体的调用链为(以目前最新版SDK31为准):ActivityThread.handleResumeActivity→wm(WindowManager).addView→WindowManagerImpl.addView→WindowManagerGlobal.addView→ViewRootImpl.setView然后完成调用requestLayout测量(performMeasure)、布局(performLayout)和绘制(performDraw)的过程。在requestLayout中会通过checkThread来判断线程的一致性。所以在onCreate中设置text时发生在以上过程之前,不会抛出线程一致性的异常。

2、先在主线程中调用requestLayout,然后在子线程中修改

	override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        tv_discount.setOnClickListener {
            tv_discount.requestLayout()
            thread {
                tv_discount.text = "setText"
            }
        }
    }

首先在主线程调用requestLayout:

在这里插入图片描述

可以发现有个参数mPrivateFlags或上了PFLAG_FORCE_LAYOUT的信息,表示是否强制绘制
。然后在通过下面的调用mParent的requestLayout将整个View树的所有View的mPrivateFlags都或上了PFLAG_FORCE_LAYOUT的信息,最终走到ViewRootImpl的requestLayout方法完成绘制。最终代码会走到performLayout→getValidLayoutRequesters,然后重新将View树的mPrivateFlags去除PFLAG_FORCE_LAYOUT信息:

在这里插入图片描述

整个过程相当于一个开关打开关闭的过程。如果在这期间子线程调用了操纵UI的步骤,比如调用了setText,代码会通过setText→checkForRelayout走到requestLayout和invalidate,在如下判断中:

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

	
    public boolean isLayoutRequested() {
        return (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
    }

mParent的isLayoutRequested方法会返回true,因为mPrivateFlags此时是有PFLAG_FORCE_LAYOUT的信息的,因此条件不满足,不会往上走到ViewRootImpl的requestLayout方法,因此也不会调用checkThread检查线程,自然不会出异常。

3、修改xml布局中控件的大小,将wrap_content或match_parent改成固定大小

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        tv_discount.setOnClickListener {
            thread {
                tv_discount.text = "setText"
            }
        }
    }

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    android:id="@+id/parentLayout"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"
    android:gravity="center">

    <TextView
        android:id="@+id/tv_discount"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:textSize="14dp"
        android:text="3.4折"
        android:gravity="center"/>
</LinearLayout>

setText方法会调用checkRelayout方法,在这个方法中:
在这里插入图片描述

可以看到如果高度不为WRAP_CONTENT和MATCH_PARENT就会调用invalidate方法重绘,但是这个方法并不会检查线程也不会报错。并且绘制后调用return直接退出方法,不会走到下面的requestLayout方法。但是如果把TexrView的属性ellipsize改成marquee,那么就会跳过if判断走到下面的requestLayout,一样会报错。

但是调用invalidate并不是一定不会检查线程一致性!invalidate调用链为:invalidate→invalidateInternal→p(ViewParent).invalidateChild(),最终走到ViewGroup的invalidateChild方法中:
在这里插入图片描述

如果开启了硬件加速,即mHardwareAccelerated为true,然后会走onDescendantInvalidated方法,这个方法中不会去检查线程一致性。因为现在大部分手机都已经默认开启了硬件加速,所以这样做没问题,但是在很老的手机(几乎已经没有了)上,硬件加速是不开启,或者在AndroidManifest.xml中手动将硬件加速关闭:

<application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:hardwareAccelerated="false"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:supportsRtl="true"
    android:theme="@style/Theme.MyApplication">

那么代码会继续下走走到invalidateChildInParent,然后往上循环调用到ViewRootImpl的invalidateChildInParent方法,第一行就是checkThread:
在这里插入图片描述
一样会检查线程的一致性。

4、在子线程中通过WindowManager添加控件

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        thread {
            Looper.prepare()
            val button = Button(this)
            windowManager.addView(button, WindowManager.LayoutParams())
            button.text = "在子线程更新UI"
            button.setOnClickListener {
                button.text = "点击事件触发"
            }
            Looper.loop()
        }
    }

我们知道ViewRootImpl中检查线程一致性的方法checkThread:

void checkThread() {
    if (mThread != Thread.currentThread()) {
        throw new CalledFromWrongThreadException(
                "Only the original thread that created a view hierarchy can touch its views.");
    }
}

mThread一定是主线程(UI线程)嘛?这可不一定。mThread的创建是在ViewRootImpl的构造方法中:

在这里插入图片描述
在ActivityThread调用handleResumeActivity方法中,通过调用链:ActivityThread.handleResumeActivity→wm(WindowManager).addView→WindowManagerImpl.addView→WindowManagerGlobal.addView中:

在这里插入图片描述
ViewRootImpl被创建,因为创建的是当前调用线程(UI线程),所有ViewRootImpl中的mThread即为主线程。那么只要让这个过程发生在子线程,然后mThread即为这个线程,在这同一个线程中调绘制方法,checkThread自然能检查通过。

5、通过SurfaceView来更新UI

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    surfaceView.holder.addCallback(object : SurfaceHolder.Callback {
        override fun surfaceCreated(holder: SurfaceHolder) {
            thread {
                while (!destroy) {
                    val canvans = holder.lockCanvas()
                    val random = Random()
                    val r = random.nextInt(255)
                    val g = random.nextInt(255)
                    val b = random.nextInt(255)
                    canvans.drawColor(Color.rgb(r, g, b))
                    holder.unlockCanvasAndPost(canvans)
                    Thread.sleep(2000)
                }
            }
        }

        override fun surfaceChanged(
            holder: SurfaceHolder,
            format: Int,
            width: Int,
            height: Int
        ) {
            Log.d("MyTag", "surfaceChanged: ")
        }

        override fun surfaceDestroyed(holder: SurfaceHolder) {
            destroy = true
        }
    })
}

直接在更底层的通过canvas在surface上绘制,然后和硬件打交道,完全绕过了View绘制的一套方法。

以上就是几种子线程中更新ui的方式,当然在实际项目中肯定还是要在主线程中更新UI的。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

哒哒呵

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

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

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

打赏作者

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

抵扣说明:

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

余额充值