对于所有的Android开发者来说,"View的更新必须在UI线程中进行"是一项最基本常识。
如果不在UI线程中更新View,系统会抛出CalledFromWrongThreadException异常。那么有没有什么办法可以不在UI线程中更新View?答案当然是有的!
一.ViewRootImpl渲染体系
在Android系统中,ViewRootImpl负责View的绘制调度、事件分发、窗口管理等功能。
各层级View遵循单一父View对应多个子View的关系,通过嵌套形成树形结构。
由于ViewRootImpl不是真正的View,因此ViewRootImpl只是View调度的根节点,并不是View树的根节点。View树真正的根节点是DecorView。DecorView继承自FrameLayout,是真正的View容器。ViewRootImpl通过管理DecorView,间接统筹管理所有层级的View。
1.DecorView的创建
当启动Activity时,系统会调用ActivityThread的handleLaunchActivity方法处理Activity的启动流程。
在ActivityThread的handleLaunchActivity方法中,会分别调用performLaunchActivity方法、handleStartActivity方法、handleResumeActivity方法,反射创建Activity,并回调Activity的生命周期,如下图所示:
在实际的开发过程中,通常会在Activity的onCreate方法中,调用setContentView方法,为Activity设置对应的View。在setContentView方法中,会调用installDecor方法,创建DecorView,如下图所示:
2.ViewRootImpl的创建
在ActivityThread的handleResumeActivity方法中,主要做了两件事:
1)回调Activity的onResume方法,切换生命周期。
2)调用Activity的makeVisible方法,创建ViewRootImpl与DecorView进行绑定。
在Activity的makeVisible方法中,会通过WindowManager创建ViewRootImpl对象,并与DecorView进行绑定,如下图所示:
在ViewRootImpl的setView方法中,ViewRootImpl会与DecorView进行双向绑定,如下图所示:
3.渲染体系与生命周期
在Activity的首次启动过程中:
- 回调onCreate方法时:调用setContentView方法,触发DecorView的创建。
- 回调onStart方法时:DecorView完成创建,ViewRootImpl未创建。
- 回调onResume方法时:DecorView完成创建,ViewRootImpl未创建。回调后立刻创建ViewRootImpl,并与DecorView完成绑定。
二.线程检测机制
1.异常产生
CalledFromWrongThreadException异常的抛出发生在ViewRootImpl类的checkThread方法中。当对View进行更新时,最终都会调用ViewRootImpl类的checkThread方法进行线程检测,代码如下:
java
void checkThread() {
Thread current = Thread.currentThread();
if (mThread != current) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views."
+ " Expected: " + mThread.getName()
+ " Calling: " + current.getName());
}
}
当判断调用checkThread方法的线程和mThread不一致时,会抛出CalledFromWrongThreadException异常。
2.检测路径
在ViewRootImpl中,共有13个方法在执行时会进行线程检测。如下所示:
- requestFitSystemWindows:请求调整View的布局以适应系统窗口。
- requestLayout:请求重新对View布局。
- invalidateChildInParent:通知父View某个子View需要重绘。
- setWindowStopped:设置Window的停止状态。
- requestTransparentRegion:请求计算View的透明区域。
- requestChildFocus:请求将焦点设置到某个子View上。
- clearChildFocus:清除子View焦点。
- focusableViewAvailable:通知父View某个子View可以获取焦点。
- recomputeViewAttributes:重新计算View的属性。
- playSoundEffect:播放与View交互相关的音效。
- focusSearch:在View树中搜索下一个可以获取焦点的View。
- keyboardNavigationClusterSearch:在键盘导航集群中搜索下一个可以获取焦点的View。
- doDie:销毁当前的ViewRootImpl。
但与View更新最为密切的是requestLayout方法和invalidateChildInParent方法。
在Android系统中,任何对View的更新操作,最终都要直接或间接调用View的invalidate方法或requestLayout方法。这两个方法会触发ViewRootImpl中的相应逻辑,在绘制调度前进行线程检测。
View的invalidate方法和requestLayout方法都会触发ViewRootImpl对View重新进行绘制调度(measure、layout、draw),但二者的区别在于:
- invalidate方法:标记当前区域为dirty,表示需要重新绘制,并在下一次绘制调度中触发draw流程,不会触发measure流程和layout流程。
- requestLayout方法:清除已经测量的数据,并在下一次绘制调度中触发measure流程和layout流程,如果在layout过程中发现View的大小发生变化,则会通过调用setFrame方法,间接触发调用一次invalidate方法,并在下一次绘制调度中触发draw流程。
1)invalidate方法触发线程检测
当调用View的invalidate方法时,invalidate方法内部会调用父View的invalidateChild方法,通过循环的方式,一层一层的获取父View,通知重新绘制,最终通知到ViewRootImpl,如下图所示:
在ViewRootImpl的invalidateChildInParent方法中,会进行线程检测,代码如下:
java
@Override
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
// 线程检测
checkThread();
...
return null;
}
2)requestLayout方法触发线程检测
当调用View的requestLayout方法时,会调用父View的requestLayout方法。通过一层一层的递归调用向上通知,最终通知到ViewRootImpl,如下图所示:
在ViewRootImpl的requestLayout方法中,会进行线程检测,代码如下:
java
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
// 线程检测
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
三.子线程更新View
子线程更新View的方式分为两种:基于独立渲染体系和基于ViewRootImpl渲染体系。需要注意的是,尽管ViewRootImpl渲染体系支持在子线程更新View,但为了保证View状态的一致性,还是建议在UI线程更新View。
1.基于独立渲染体系
1)使用SurfaceView绘制
SurfaceView依靠自身维护BLASTBufferQueue获取Surface,在SurfaceFlinger中拥有独立的Layer。在绘制时不经过ViewRootImpl,详情参考:SurfaceView与TextureView的绘制渲染,代码如下:
kotlin
class TestActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.layout_activity_test)
// 获取SurfaceView
val view = findViewById<SurfaceView>(R.id.surface_view)
// 创建调度器为IO线程的协程作用域
val scope = CoroutineScope(Dispatchers.IO)
// 监听Surface变化
view.holder.addCallback(object : SurfaceHolder.Callback {
override fun surfaceCreated(holder: SurfaceHolder) {
// Surface创建时启动运行在IO线程的协程
scope.launch {
while (true) {
// 每隔100ms绘制一次背景
delay(100)
val canvas = holder.lockCanvas()
canvas.drawColor(Color.RED)
holder.unlockCanvasAndPost(canvas)
}
}
}
override fun surfaceChanged(
holder: SurfaceHolder,
format: Int,
width: Int,
height: Int
) {
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
// Surface销毁时取消作用域内的协程
scope.cancel()
}
})
}
}
2)使用TextureView绘制
TextureView依靠自身维护的SurfaceTexture获取Surface,在绘制时不经过ViewRootImpl。
但与SurfaceView不同的是,通过TextureView的Surface绘制后的内容,不会直接提交到SurfaceFlinger,而是通过回调的方式触发调用一次invalidate方法,并在下一次绘制时通过硬件加速层的方式挂在View树下一起绘制,详情参考:SurfaceView与TextureView的绘制渲染,代码如下:
kotlin
class TestActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.layout_activity_test)
// 获取TextureView
val view = findViewById<TextureView>(R.id.texture_view)
// 创建调度器为IO线程的协程作用域
val scope = CoroutineScope(Dispatchers.IO)
// 监听SurfaceTexture变化
view.surfaceTextureListener = object : TextureView.SurfaceTextureListener {
override fun onSurfaceTextureAvailable(
surface: SurfaceTexture,
width: Int,
height: Int
) {
// SurfaceTexture创建时启动运行在IO线程的协程
scope.launch {
while (true) {
// 每隔100ms绘制一次背景
delay(100)
val canvas = view.lockCanvas() ?: continue
canvas.drawColor(Color.RED)
view.unlockCanvasAndPost(canvas)
}
}
}
override fun onSurfaceTextureSizeChanged(
surface: SurfaceTexture,
width: Int,
height: Int
) {
}
override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean {
// SurfaceTexture销毁时取消作用域内的协程
scope.cancel()
return true
}
override fun onSurfaceTextureUpdated(surface: SurfaceTexture) {
}
}
}
}
3)接管ViewRootImpl的Surface
当在Activity中调用Window的takeSurface方法,会接管ViewRootImpl的Surface,Activity的渲染会脱离ViewRootImpl渲染体系,相当于整个Activity都变成了SurfaceView,代码如下:
kotlin
class TestActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 创建调度器为IO线程的协程作用域
val scope = CoroutineScope(Dispatchers.IO)
// 接管ViewRootImpl的Surface
window.takeSurface(object : SurfaceHolder.Callback2 {
override fun surfaceCreated(holder: SurfaceHolder) {
// Surface创建时启动运行在IO线程的协程
scope.launch {
while (true) {
// 每隔100ms绘制一次背景
delay(100)
val canvas = holder.lockCanvas()
canvas.drawColor(Color.RED)
holder.unlockCanvasAndPost(canvas)
}
}
}
override fun surfaceChanged(
holder: SurfaceHolder,
format: Int,
width: Int,
height: Int
) {
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
// Surface销毁时取消作用域内的协程
scope.cancel()
}
override fun surfaceRedrawNeeded(holder: SurfaceHolder) {
}
})
}
}
2.基于ViewRootImpl渲染体系
1)ViewRootImpl渲染体系形成前
当Activity首次启动并在onCreate方法内调用setContentView方法后,在onCreate方法、onStart方法、onResume方法中,使用非UI线程更新View,不会触发线程检测,代码如下:
kotlin
class TestActivity : AppCompatActivity() {
// 创建调度器为IO线程的协程作用域
private val scope = CoroutineScope(Dispatchers.IO)
// 标记在onResume方法中执行一次
private var firstResume = true
// 标记在onStart方法中执行一次
private var firstStart = true
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.layout_activity_test)
// onCreate方法中,启动运行在IO线程的协程
scope.launch {
// 更新TextView的文字内容
findViewById<TextView>(R.id.text_view)?.text = "hello world"
}
}
override fun onStart() {
super.onStart()
// 使用标志位,确保只在首次调用onStart时执行
if(!firstStart) return
firstStart = false
// onStart方法中,启动运行在IO线程的协程
scope.launch {
// 更新TextView的文字内容
findViewById<TextView>(R.id.text_view)?.text = "hello world !"
}
}
override fun onResume() {
super.onResume()
// 使用标志位,确保只在首次调用onResume时执行
if (!firstResume) return
firstResume = false
// onResume方法中,启动运行在IO线程的协程
scope.launch {
// 更新TextView的文字内容
findViewById<TextView>(R.id.text_view)?.text = "hello world !!"
}
}
}
2)绑定ViewRootImpl渲染体系前
当动态创建完View后,在没有添加到与ViewRootImpl有关联的ViewGroup前,在非UI线程更新View,不会触发线程检测,代码如下:
kotlin
class TestActivity : AppCompatActivity() {
// 创建调度器为IO线程的协程作用域
private val scope = CoroutineScope(Dispatchers.IO)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.layout_activity_test)
// 获取TextView,监听点击事件
findViewById<TextView>(R.id.text_view)?.setOnClickListener {
// 当点击TextView时,启动运行在IO线程的协程
scope.launch {
// 创建一个TextView
val view = TextView(this@TestActivity)
// 设置文本内容
view.text = "hello world"
// 切换到UI线程
withContext(Dispatchers.Main) {
// 添加到DecorView中
[email protected](
view, ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
)
}
}
}
}
}
4)硬件渲染模式下的invalidate方法
在软件渲染模式下,当调用View的invalidate方法时,会调用父类的invalidateChild方法。但在硬件渲染模式下,为了防止循环遍历耗时,会直接调用onDescendantInvalidated方法,代码如下:
java
@Override
public final void invalidateChild(View child, final Rect dirty) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null && attachInfo.mHardwareAccelerated) {
// HW accelerated fast path
onDescendantInvalidated(child, child);
return;
}
...
}
在ViewGroup的onDescendantInvalidated方法中,会通过递归调用的方式,最终调用ViewRootImpl的onDescendantInvalidated方法,如下图所示:
在ViewRootImpl的onDescendantInvalidated方法中,会直接调用invalidate方法,跳过线程检查,代码如下:
java
private static boolean sToolkitEnableInvalidateCheckThreadFlagValue =
Flags.enableInvalidateCheckThread();
@Override
public void onDescendantInvalidated(@NonNull View child, @NonNull View descendant) {
// Android Tool Kit为debug留的开关,默认为false
if (sToolkitEnableInvalidateCheckThreadFlagValue) {
checkThread();
}
if ((descendant.mPrivateFlags & PFLAG_DRAW_ANIMATION) != 0) {
mIsAnimating = true;
}
invalidate();
}
@UnsupportedAppUsage
void invalidate() {
mDirty.set(0, 0, mWidth, mHeight);
if (!mWillDrawSoon) {
// 启动绘制流程
scheduleTraversals();
}
}
Android系统默认的渲染模式为硬件渲染,这里在AndroidManifest中再手动声明一下,代码如下:
xml
<manifest xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
package="com.test.ui">
...
<!-- 启动应用级别的硬件渲染模式 -->
<application android:hardwareAccelerated="true">
...
</application>
</manifest>
在代码使用上,硬件渲染与软件渲染基本没有差别。当开启硬件渲染模式后,在子线程直接或间接调用View的invalidate方法不会产生崩溃,代码如下:
kotlin
class TestActivity : AppCompatActivity() {
// 创建调度器为IO线程的协程作用域
private val scope = CoroutineScope(Dispatchers.IO)
// 文字大小
private var size = 30f
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.layout_activity_test)
}
override fun onResume() {
super.onResume()
// 每次onResume时,启动一个运行在IO线程的协程
scope.launch {
// 更新文字大小
size += 10f
// 获取TextView,设置文字大小
findViewById<TextView>(R.id.text_view)?.textSize = size
}
}
}
5)子线程中创建ViewRootImpl
实际上,Android系统并未要求View的更新必须在UI线程中进行。
通过分析CalledFromWrongThreadException异常抛出时的提示可以知道:View的更新必须在original thread中。而original thread就是ViewRootImpl中mThread字段保存的线程。
java
void checkThread() {
Thread current = Thread.currentThread();
if (mThread != current) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views."
+ " Expected: " + mThread.getName()
+ " Calling: " + current.getName());
}
}
在ViewRootImpl的构造方法中,会对mThread进行初始化,代码如下:
java
public ViewRootImpl(
@UiContext Context context,
Display display,
IWindowSession session,
WindowLayout windowLayout) {
...
// 获取当前的线程并保存
mThread = Thread.currentThread();
...
}
因此,Android系统要求View更新必须在UI线程执行,本质上是因为ViewRootImpl在UI线程被创建,并在构造方法中保存当前线程引用(mThread),并在每次操作时通过checkThread方法验证调用线程是否与mThread一致。
由于Activity的启动需要系统调度,系统会将Activity的启动安排在UI线程中进行,这也就导致无法在子线程中启动Activity,进而无法在子线程中创建ViewRootImpl。
但是在Android系统中,不仅Activity拥有ViewRootImpl,Dialog和PopupWindow等组件也各自拥有独立的ViewRootImpl。
如果在子线程中创建了Dialog或PopupWindow,那么后续对Dialog或PopupWindow中View的更新也必须在该子线程中进行,代码如下:
kotlin
class TestActivity : AppCompatActivity() {
// 创建HandlerThread,并启动子线程update
private val handleThread = HandlerThread("update").apply { start() }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.layout_activity_test)
// 创建子线程Handler,并向子线程update中提交一个任务
Handler(handleThread.looper).post {
// 获取容器View
val parent = findViewById<ViewGroup>(R.id.container)
// 通过加载XML的方式,创建一个子View
val view = LayoutInflater.from(this)
.inflate(R.layout.layout_test_popup_window, parent, false)
// 创建PopupWindow,并将子View添加进去
val popupWindow = PopupWindow(
view,
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
// 从子View中获取TextView
val textView = popupWindow.contentView.findViewById<TextView>(R.id.pop_text)
// 监听TextView的点击事件
textView.setOnClickListener {
// 更新TextView的文字内容,
// 这里注意点击事件的回调线程变成了子线程update
textView.text = "${Thread.currentThread()}"
// 这里会产生CalledFromWrongThreadException异常
// 因为没有在子线程update中更新
window.decorView.post { textView.text = "${Thread.currentThread()}" }
}
// 这里先将任务提交到UI线程执行
// 因为在onCreate方法中,容器View对应的Window还未创建好
// 获取不到Window的Token,会产生异常
window.decorView.post {
// 切换到子线程,创建子线程Handler,并向子线程update中提交一个任务
Handler(handleThread.looper).post {
// 子线程中展示popupWindow,会触发ViewRootImpl在子线程update中创建
popupWindow.showAtLocation(parent, Gravity.CENTER, 0, 0)
}
}
}
}
}
三.总结
1.View的更新必须在UI线程进行的原因
ViewRootImpl在UI线程中被创建,并在构造方法中保存了当前线程的引用(mThread)。在每次更新View时,通过调用View的invalidate方法或requestLayout方法触发ViewRootImpl的checkThread方法,验证调用线程是否与mThread一致。
2.Activity启动流程中渲染体系的创建
- 回调onCreate方法时:调用setContentView方法,触发DecorView的创建。
- 回调onStart方法时:DecorView完成创建,ViewRootImpl未创建。
- 回调onResume方法时:DecorView完成创建,ViewRootImpl未创建。回调后立刻创建ViewRootImpl,并与DecorView完成绑定。
3.invalidate方法与requestLayout方法的区别
View的invalidate方法和requestLayout方法都会触发ViewRootImpl对View重新进行绘制调度(measure、layout、draw),但二者的区别在于:
- invalidate方法:标记当前区域为dirty,表示需要重新绘制,并在下一次绘制调度中触发draw流程,不会触发measure流程和layout流程。
- requestLayout方法:清除已经测量的数据,并在下一次绘制调度中触发measure流程和layout流程,如果在layout过程中发现View的大小发生变化,则会通过调用setFrame方法,间接触发调用一次invalidate方法,并在下一次绘制调度中触发draw流程。
4.子线程更新View的方法
- 基于独立渲染体系
- 使用SurfaceView,直接对Surface进行绘制。
- 使用TextureView,直接对Surface进行绘制。
- 接管ViewRootImpl的Surface,直接对Surface进行绘制。
- 基于ViewRootImpl渲染体系
- 在ViewRootImpl渲染体系形成前,使用子线程更新View。
- 在绑定ViewRootImpl渲染体系前,使用子线程更新View。
- 硬件渲染模式下,子线程直接或间接调用View的invalidate方法。
- 对于独立拥有ViewRootImpl的组件,在子线程中触发组件创建ViewRootImpl,并在对应的子线程中更新View。