皮皮网
皮皮网

【易起名源码】【tub小说源码】【强制分享 源码】viewrootimpl源码

来源:米波在线直播源码 发表时间:2024-12-22 21:22:53

1.Android ViewRootImpl
2.找到卡顿来源,源码BlockCanary源码精简分析
3.Android UI绘制之View绘制的工作原理
4.子线程更新UI全解
5.Android 14 HWUI 源码研究 View Canvas RenderThread ViewRootImpl skia
6.View 绘制流程源码分析

viewrootimpl源码

Android ViewRootImpl

       æœ¬æ–‡ä¸»è¦åˆ†æžä¸¤ä¸ªé—®é¢˜ï¼š

        1、为什么View 的绘制流程是从 ViewRootImpl 的performTraversals()方法开始的?

        2、View 的invalidate方法是怎么触发到ViewRootImpl 的performTraversals()方法的。

        在阅读本文前,最好先了解window的添加过程,Android消息处理机制 和 View 的绘制流程。推荐先阅读以下文章:

        Android Window和WindowManager

        Android-消息机制

        Android View 的绘制流程

       android 源码注释的意思是:ViewRootImpl是视图层次结构的顶部,实现 View 和 WindowManager 之间所需的协议。是 WindowManager Global 的内部实现中重要的组成部分。

        View 的绘制流程是从 ViewRootImpl 的performTraversals()方法开始的,那到底是哪里调用了performTraversals()方法呢,下面我们分析一下:

        1.私有属性的performTraversals()方法肯定是在内部调用起来的,经过搜索找到是doTraversal()方法调用了。

        2.接着找到了,调用了doTraversal() 的TraversalRunnable ç±»

        3.内部只有一个地方实例化了TraversalRunnable 的实例mTraversalRunnable ,查到到两个方法内都调用了mTraversalRunnable ,明显 scheduleTraversals 是主动触发这个 Runnable 。这就表明调用了scheduleTraversals ()函数的地方都主动触发了view的刷新。

        4.接着我们看一下 mChoreographer.postCallback 做了什么

        可以看到,最后后走进了scheduleVsyncLocked()方法内。

        5.mDisplayEventReceiver 的类 是 FrameDisplayEventReceiver,继承自

        DisplayEventReceiver 。

        最后走到这里就没了,那么这个方法是做了什么呢,这个方法的注释是这个意思:安排在下一个显示帧开始时传送单个垂直同步脉冲。意思就是,调用了这个方法可以收到系统传送过来的垂直同步脉冲信号。Android系统每隔ms就会发送一个VSYNC信号(VSYNC:vertical synchronization 垂直同步,帧同步),触发对UI进行渲染。这个垂直同步信对于应用来说了,只有了订阅了监听,才能收到。而且是订阅一次,收到一次。

        6.既然是在这个类里面订阅垂直同步信号的,那回调也应该在这里。于是找到了以下方法。

        native code 调用到 onVsync,这个方法的注释解释如下:当接收到垂直同步脉冲时调用。接收者应该渲染一个帧,然后调用 { @link scheduleVsync} 来安排下一个垂直同步脉冲。

        这个方法的具体实现在前面分析到的FrameDisplayEventReceiver 类里面。

        这里可以看到,其实mHandler就是当前主线程的handler,当接收到onVsync信号的时候,将自己封装到Message中,等到Looper处理,最后Looper处理消息的时候就会调用run方法,这里是Handler的机制,不做解释。

        7.最后,如下图调用所示,最终从mCallbackQueues取回之前添加的任务再执行run方法,也就是TraservalRunnable的run方法。

        总结上面的分析,调用流程如下图所示如下:

        上面我们分析到只要调用了ViewRootImpl 的scheduleTraversals ()方法,最终就能调用了ViewRootImpl 的performTraversals()来开始绘制。那肯定是我们常调用的view刷新的接口,经过一系列的方法调用,最终调用了ViewRootImpl 的scheduleTraversals ()方法。下面我们分析一下常用的View 的 invalidate()接口是怎么调用到了ViewRootImpl 的scheduleTraversals ()方法。

        可以看出,invalidate有多个重载方法,但最终都会调用invalidateInternal方法,在这个方法内部,进行了一系列的判断,判断View是否需要重绘,接着为该View设置标记位,然后把需要重绘的区域传递给父容器,即调用父容器的invalidateChild方法。

        接着我们看ViewGroup#invalidateChild:

        由于不断向上调用父容器的方法,到最后会调用到ViewRootImpl的invalidateChildInParent方法,我们来看看它的源码,ViewRootImpl#invalidateChildInParent:

        最后调用了scheduleTraversals方法,触发View的工作流程。至此,我们已经完整地分析了一次调用View 的 invalidate()方法到触发ViewRootImpl 的scheduleTraversals()方法。

找到卡顿来源,源码BlockCanary源码精简分析

       通过屏幕渲染机制我们了解到,源码Android的源码屏幕渲染是通过vsync实现的。软件层将数据计算好后,源码放入缓冲区,源码易起名源码硬件层从缓冲区读取数据绘制到屏幕上,源码渲染周期是源码ms,这让我们看到不断变化的源码画面。如果计算时间超过ms,源码就会出现卡顿现象,源码这通常发生在软件层,源码而不是源码硬件层。卡顿发生的源码原因在于软件层的计算时间需要小于ms,而计算的源码执行地点则在Handler中,具体来说是在UI的Handler中。Android进程间的交互通过Binder实现,线程间通信通过Handler。

       软件层在收到硬件层的vsync信号后,会在Java层向UI的Handler中投递一个消息,进行view数据的计算。这涉及到测量、布局和绘制,通常在`ViewRootImpl`的`performTraversals()`函数中实现。因此,view数据计算在UI的tub小说源码Handler中执行,如果有其他操作在此执行且耗时过长,则可能导致卡顿,我们需要找到并优化这些操作。

       要找到卡顿的原因,可以通过在消息处理前后记录时间,计算时间差,将这个差值与预设的卡顿阈值比较。如果大于阈值,表示发生了卡顿,此时可以dump主线程堆栈并显示给开发者。实现这一功能的关键在于在Looper中设置日志打印类。通过`Looper.loop()`函数中的日志打印,我们可以插入自定义的Printer,并在消息执行前后计算时间差。另一种方法是在日志中添加前缀和后缀,根据这些标志判断时间点。

       BlockCanary是一个用于检测Android应用卡顿的工具,通过源码分析,我们可以了解到它的实现逻辑。要使用BlockCanary,首先需要定义一个继承`BlockCanaryContext`的类,并重写其中的关键方法。在应用的`onCreate()`方法中调用BlockCanary的安装方法即可。当卡顿发生时,BlockCanary会通知开发者,强制分享 源码并在日志中显示卡顿信息。

       BlockCanary的核心逻辑包括安装、事件监控、堆栈和CPU信息的采集等。在事件发生时,会创建LooperMonitor,同时启动堆栈采样和CPU采样。当消息将要执行时,开始记录开始时间,执行完毕后停止记录,并计算执行时间。如果时间差超过预设阈值,表示发生了卡顿,并通过回调传递卡顿信息给开发者。

       堆栈和CPU信息的获取通过`AbstractSampler`类实现,它通过`post`一个`Runnable`来触发采样过程,循环调用`doSample()`函数。StackSampler和CpuSampler分别负责堆栈和CPU信息的采集,核心逻辑包括获取当前线程的堆栈信息和CPU速率,并将其保存。获取堆栈信息时,通过在`StackSampler`类中查找指定时间范围内的堆栈信息;获取CPU信息时,从`CpuSampler`类中解析`/proc/stat`和`/proc/mpid/stat`文件的CPU数据,并保存。

       总结而言,bp算法源码BlockCanary通过在消息处理前后记录时间差,检测卡顿情况,并通过堆栈和CPU信息提供详细的卡顿分析,帮助开发者定位和优化性能问题。

Android UI绘制之View绘制的工作原理

        这是AndroidUI绘制流程分析的第二篇文章,主要分析界面中View是如何绘制到界面上的具体过程。

        ViewRoot 对应于 ViewRootImpl 类,它是连接 WindowManager 和 DecorView 的纽带,View的三大流程均是通过 ViewRoot 来完成的。在 ActivityThread 中,当 Activity 对象被创建完毕后,会将 DecorView 添加到 Window 中,同时会创建 ViewRootImpl 对象,并将 ViewRootImpl 对象和 DecorView 建立关联。

        measure 过程决定了 View 的宽/高, Measure 完成以后,可以通过 getMeasuredWidth 和 getMeasuredHeight 方法来获取 View 测量后的宽/高,在几乎所有的情况下,它等同于View的最终的宽/高,但是特殊情况除外。 Layout 过程决定了 View 的四个顶点的坐标和实际的宽/高,完成以后,可以通过 getTop、getBottom、getLeft 和 getRight 来拿到View的四个顶点的位置,可以通过 getWidth 和 getHeight 方法拿到View的最终宽/高。 Draw 过程决定了 View 的显示,只有 draw 方法完成后 View 的内容才能呈现在屏幕上。

        DecorView 作为顶级 View ,一般情况下,它内部会包含一个竖直方向的 LinearLayout ,在这个 LinearLayout 里面有上下两个部分,上面是标题栏,下面是内容栏。在Activity中,我们通过 setContentView 所设置的布局文件其实就是被加到内容栏中的,而内容栏id为 content 。可以通过下面方法得到 content:ViewGroup content = findViewById(R.android.id.content) 。通过 content.getChildAt(0) 可以得到设置的 view 。 DecorView 其实是一个 FrameLayout , View 层的事件都先经过 DecorView ,然后才传递给我们的 View 。

        MeasureSpec 代表一个位的int值,高2位代表 SpecMode ,低位代表 SpecSize , SpecMode 是指测量模式,而 SpecSize 是指在某种测量模式下的规格大小。

        SpecMode 有三类,如下所示:

        UNSPECIFIED

        EXACTLY

        AT_MOST

        LayoutParams需要和父容器一起才能决定View的MeasureSpec,从而进一步决定View的宽/高。

        对于顶级View,即DecorView和普通View来说,MeasureSpec的转换过程略有不同。对于DecorView,其MeasureSpec由窗口的尺寸和其自身的LayoutParams共同确定;

        对于普通View,其MeasureSpec由父容器的MeasureSpec和自身的Layoutparams共同决定;

        MeasureSpec一旦确定,onMeasure就可以确定View的测量宽/高。

        小结一下

        当子 View 的宽高采用 wrap_content 时,不管父容器的模式是精确模式还是最大模式,子 View 的模式总是最大模式+父容器的剩余空间。

        View 的工作流程主要是指 measure 、 layout 、 draw 三大流程,即测量、布局、绘制。其中 measure 确定 View 的测量宽/高, layout 确定 view 的最终宽/高和四个顶点的位置,而 draw 则将 View 绘制在屏幕上。

        measure 过程要分情况,如果只是一个原始的 view ,则通过 measure 方法就完成了其测量过程,如果是一个 ViewGroup ,除了完成自己的测量过程外,还会遍历调用所有子元素的 measure 方法,各个子元素再递归去执行这个流程。

        如果是一个原始的 View,那么通过 measure 方法就完成了测量过程,在 measure 方法中会去调用 View 的 onMeasure 方法,View 类里面定义了 onMeasure 方法的默认实现:

        先看一下 getSuggestedMinimumWidth 和 getSuggestedMinimumHeight 方法的源码:

        可以看到, getMinimumWidth 方法获取的是 Drawable 的原始宽度。如果存在原始宽度(即满足 intrinsicWidth > 0),那么直接返回原始宽度即可;如果不存在原始宽度(即不满足 intrinsicWidth > 0),那么就返回 0。

        接着看最重要的 getDefaultSize 方法:

        如果 specMode 为 MeasureSpec.UNSPECIFIED 即未指定模式,那么返回由方法参数传递过来的尺寸作为 View 的测量宽度和高度;

        如果 specMode 不是 MeasureSpec.UNSPECIFIED 即是最大模式或者精确模式,那么返回从 measureSpec 中取出的 specSize 作为 View 测量后的宽度和高度。

        看一下刚才的表格:

        当 specMode 为 EXACTLY 或者 AT_MOST 时,View 的布局参数为 wrap_content 或者 match_parent 时,给 View 的 specSize 都是 parentSize 。这会比建议的最小宽高要大。这是不符合我们的预期的。因为我们给 View 设置 wrap_content 是希望View的大小刚好可以包裹它的内容。

        因此:

        如果是一个 ViewGroup,除了完成自己的 measure 过程以外,还会遍历去调用所有子元素的 measure 方法,各个子元素再递归去执行 measure 过程。

        ViewGroup 并没有重写 View 的 onMeasure 方法,但是它提供了 measureChildren、measureChild、measureChildWithMargins 这几个方法专门用于测量子元素。

        如果是 View 的话,那么在它的 layout 方法中就确定了自身的位置(具体来说是通过 setFrame 方法来设定 View 的四个顶点的位置,即初始化 mLeft , mRight , mTop , mBottom 这四个值), layout 过程就结束了。

        如果是 ViewGroup 的话,那么在它的 layout 方法中只是确定了 ViewGroup 自身的位置,要确定子元素的位置,就需要重写 onLayout 方法;在 onLayout 方法中,会调用子元素的 layout 方法,子元素在它的 layout 方法中确定自己的位置,这样一层一层地传递下去完成整个 View 树的 layout 过程。

        layout 方法的作用是确定 View 本身的位置,即设定 View 的四个顶点的位置,这样就确定了 View 在父容器中的位置;

        onLayout 方法的作用是父容器确定子元素的位置,这个方法在 View 中是空实现,因为 View 没有子元素了,在 ViewGroup 中则进行抽象化,它的子类必须实现这个方法。

        1.绘制背景( background.draw(canvas); );

        2.绘制自己( onDraw );

        3.绘制 children( dispatchDraw(canvas) );

        4.绘制装饰( onDrawScrollBars )。

        dispatchDraw 方法的调用是在 onDraw 方法之后,也就是说,总是先绘制自己再绘制子 View 。

        对于 View 类来说, dispatchDraw 方法是空实现的,对于 ViewGroup 类来说, dispatchDraw 方法是有具体实现的。

        通过 dispatchDraw 来传递的。 dispatchDraw 会遍历调用子元素的 draw 方法,如此 draw 事件就一层一层传递了下去。dispatchDraw 在 View 类中是空实现的,在 ViewGroup 类中是真正实现的。

        如果一个 View 不需要绘制任何内容,那么就设置这个标记为 true,系统会进行进一步的优化。

        当创建的自定义控件继承于 ViewGroup 并且不具备绘制功能时,就可以开启这个标记,便于系统进行后续的优化;当明确知道一个 ViewGroup 需要通过 onDraw 绘制内容时,需要关闭这个标记。

        参考:《Android开发艺术探索》

子线程更新UI全解

       子线程更新 UI 的问题在 Android 开发中至关重要。通常,尝试在子线程更新 UI 会导致应用崩溃,这是因为 Android 设计了一条铁律:禁止子线程直接操作 UI。原因在于,屏幕刷新率至少是每 ms 一次,要求 UI 更新快速响应以避免卡顿。如果在子线程中操作 UI,可能导致线程不安全,进而产生不可预知的 UI 结果。

       深入理解这一限制,我们从源码层面分析。Android 版本的框架中,错误通常从 View#setBackgroundColor() 开始,层层传递至 ViewRootImpl#checkThread(),这里会检查线程是否与预期一致,如果不一致则抛出异常。这个检查机制确保 UI 更新在主线程中进行,避免了多线程对 UI 的并发修改。

       源码追踪显示,从 imageView.setBackgroundColor() 开始,完美网站源码调用链会到 View#requestLayout(),接着递归到 Activity 的顶层 View,通过 setContentView() 和 PhoneWindow 的设置,最终到达 DecorView,这是整个 View 树的根节点。DecorView 作为 ViewParent 的概念在此时显得重要,因为它虽然没有直接父 View,但仍有 ViewParent,即 ViewRootImpl。

       ViewRootImpl 的 requestLayout() 方法再次执行线程检查,确保它是在主线程初始化的。如果试图在子线程更新 View,除非满足特定条件,如在 Activity 的 onResume() 之前更新,或者在非硬件加速且更新的是特定 View 的某些方法时,否则会触发异常。

       总结来说,尽管存在绕过检查的可能,但遵循 Android 设计原则,最好避免在子线程更新 UI。因为这种做法可能带来不可预测的行为,尤其是在定制化系统中。了解了这些底层机制后,开发者应确保 UI 更新始终在主线程进行,以保证应用的稳定性和用户体验。

Android HWUI 源码研究 View Canvas RenderThread ViewRootImpl skia

       HUWUI是Android系统中负责应用可视化元素绘制的核心组件,其架构主要在C++层实现,从Java层接收View绘制信息,通过唯一的渲染线程使用skia技术完成渲染任务。整体上,从应用程序到UI线程,再到渲染线程,形成了清晰的层级关系。

       HUWUI的构建主要包括三个核心类,它们分别是:RecordingCanvas、Canvas、RenderNode、RenderProxy、RenderThread、CanvasContext、IRenderPipeline。在Java层,主要涉及两类Canvas,RecordingCanvas用于记录绘制指令,Canvas则是直接用于渲染。RecordingCanvas在构造时创建,而Canvas在调用时创建。这两个类在C++层分别对应SkiaRecordingCanvas和SkiaCanvas,后者直接引用SkCanvas。

       在全局循环中,UI线程与渲染线程之间的协同操作至关重要。具体流程包括:新创建Activity后,附着到对应的PhoneWindow,然后调用PhoneWindow的setContentView方法,将View添加到DecorView作为子节点。接着,DecorView与ViewRootImpl对接,完成View的更新与渲染。整个过程包含了measure、layout和draw等复杂子流程。

       渲染线程创建与核心对象紧密关联,主要包括RenderProxy、RenderThread和DrawFrameTask。RenderProxy负责Java层信息的衔接,RenderThread作为进程唯一的渲染线程,持有DrawFrameTask和CanvasContext,完成一帧的绘制任务。指令记录流程的核心在于使用C++层的RecordingCanvas将View属性和绘制信息记录到DisplayList中,进而完成指令的渲染。

       Surface、ANativeWindow、EGLSurface的创建流程在ViewRootImpl的performTraversals函数中初始化。ReliableSurface的封装和EGL与Skia环境的创建主要在RenderThread的requireGlContext函数中实现。从源码分析,这一过程通常在三个地方调用。

       View树与RenderNode树之间的协作关系明确,一个Application进程对应多个Activity,每个Activity与一个PhoneWindow绑定,PhoneWindow持有DecorView,DecorView对应一个ViewRootImpl,而ViewRootImpl与ThreadedRender模块对接。ThreadedRender与C++层的RenderProxy一一对应,RenderProxy持有关键对象,如RenderThread、CanvasContext、DrawFrameTask等。RenderThread是单例模式,进程唯一,负责一帧绘制的逻辑。

       在RenderPipeline模块中,关键操作包括makeCurrent、draw和swapBuffers。Native Canvas在这一过程中扮演了桥梁角色,接收Java API调用,而RecordingCanvas完成Op记录,最终DisplayListData存储这些Op。

       skia的核心资源主要在三个使用场景中发挥作用,具体细节需深入分析,这些资源对于实现高效、稳定的渲染效果至关重要。

View 绘制流程源码分析

       在View的绘制流程中,ViewRootImpl的setView主流程涉及的关键步骤包括设置PFLAG_FORCE_LAYOUT和PFLAG_INVALIDATED。这一步骤在执行时,触发了View的重绘逻辑。

       接下来,当View收到需要重绘的信号后,会执行invalidate方法。这个方法首先计算出需要重绘的dirty区域,然后从下向上,最终调用到ViewRootImpl的scheduleTraversals方法。这个过程中,脏区域的范围逐步扩大,直至整个View需要进行重绘。

       在View的绘制流程中,PFLAG_FORCE_LAYOUT和PFLAG_INVALIDATED的使用至关重要。它们的设置触发了视图的重绘和布局过程,保证了UI在用户操作或其他事件触发时能够及时响应和更新。通过这种方式,系统确保了用户界面的实时性和交互性。

       具体来说,当View收到布局或尺寸变化的信号时,会调用requestLayout方法,同时设置PFLAG_FORCE_LAYOUT标志。这个标志告诉系统,当前布局需要强制执行,即使布局尚未完成,也应立即进行更新。同时,invalidate方法的调用,会触发PFLAG_INVALIDATED标志的设置,表明视图需要重绘。

       在ViewRootImpl中,scheduleTraversals方法是负责组织和执行视图层级中所有视图的重绘和布局的。它会根据脏区域和布局标志的设置,合理安排视图的更新顺序,确保系统的性能和用户体验。

       总结整个流程,View的绘制和布局机制通过一系列的标志(如PFLAG_FORCE_LAYOUT和PFLAG_INVALIDATED)和方法(如requestLayout和invalidate)来协调和控制。这些机制使得系统能够高效地响应用户操作,实现流畅的UI交互。通过深入理解这些源码细节,开发者能够更好地优化UI性能,提高用户体验。

相关栏目:知识