前言
众所周知,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的。