Android子线程更新View的方法原理

对于所有的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。
相关推荐
志旭36 分钟前
android15 vsync源码分析
android
志旭44 分钟前
Android 14 HWUI 源码研究 View Canvas RenderThread ViewRootImpl skia
android
whysqwhw1 小时前
Egloo 高级用法
android
whysqwhw1 小时前
Egloo 架构设计
android
whysqwhw1 小时前
Egloo 快速入门指南
android
雨白2 小时前
精通 Android 通知
android
用户2018792831673 小时前
搜索算法故事:从图书馆找书到迷宫探险
android
用户2018792831674 小时前
计数排序故事:运动会的奖牌统计
android
vocal4 小时前
我的安卓第一课:四大组件之一Broadcast
android