再谈Android View绘制流程

一,先思考何时开始绘制

笔者在这里提醒读者,Android的View是UI的高级抽象,我们平时使用的XML文件也好,本质是设计模式中的一种策略模式,其View可以理解为一种底层UI显示的Request。各种VIew的排布,来自于开发者编写的XML文件,或动态增添删除View,这一系列的集合即一种策略。而这种策略最终会被Android系统解析为一种Request请求。什么意思呢?就是应用进程在此充当绘制的客户端,而服务器是谁?SurfaceFinger。记住,即便屏幕前是我们所描绘的某种策略绘制的UI,那也是Surface系统服务器的Response表现。

希望读者能理解这一点。

接下来,笔者假设读者已经了解了Activity启动流程,并且当前Activity已经进入onCreate生命周期,我们就从这里讲起。

相信各位读者一定对Activity#onCreate中调用setContent方法一定熟悉,我们直接看下源码。

很简单,拿到Window,相信读者明白,这个Window就是PhoneWindow,在此不赘述。不过笔者仍啰嗦一下,这里Activity依然将视图委托给Window,其Activity-Window-View之间的关系越来越明显了吧。

然后,调用PhoneWindow#setContentView,传入layoutId,我们继续跟进。

这里的mContentParent就是我们setContent传入View的父View,在初始化时,这里肯定是null,因此调用到installDecor方法。这个方法是什么呢?简单来讲,这里面创建了一个DecorView,并且通过我们常用的findVIewById初始化一些View,如mContentParent。

我们回到正轨,当DecorView已经创建完毕,接下来就是使用LayoutInflater去膨胀我们传入的layoutID,其parentView参数时mContentParent。关于如何膨胀,读者可以自行解读XML解析实现,不过其核心是根据XML的一系列层次结构和属性,创建多个View,并按照层次与属性赋值而已。

然后呢?View从何时添加到所谓的WindowManager中呢?

此时我们需要快进到ActivityThread#handleResumeActivity方法。

关键核心在下面,

由于视图可见,DecorView添加至WindowManager中。在此之前,有必要解释下ViewRootImpl是啥。先看下ViewRootImpl的构造方法如下。

核心注意IWIndowSession,这是什么呢?可以理解为应用进程持有的Window#Takon,是Binder接口,窗口唯一,主要负责与WMS通信。笔者在这里通俗理解为View的抽象顶层,它既负责管理底层View(事件分发、View绘制等),又负责与系统交互,是应用层View的顶层通信抽象。

明白了这点,我们回到正轨,继续分析。

wm.addView,将DecorView添加到wm中,wm是WindowManagerImpl,我们跟进。

委托给mBlobal,这是进程唯一的WindowManagerGlobal,我们跟进。

上述部分是参数的一系列检查,我们继续跟进逻辑,

在这里,ViewRootImpl这种抽象,会发现是在每次添加WindowManagerGlobal时创建。我们继续跟进,核心逻辑是root.setView。由于VIewRootImpl#setView太长,就不在此截图,不过核心逻辑如下,

requestLayout,我们继续跟进,

这里可以理解为View视图逻辑Request创建的起点。检查线程、scheduleTraversals,里面便涉及三大流程。那么我们得到第一个答案,View从什么时候开始绘制的呢?是ActivityThread#handleResumeActivity时,通过wm.addView(decorView,l)开始进行绘制。但只有这一个起点吗?

相信读者知道,对指定View调用类似于setVisibile或TextView#setText时,也是绘制的起点。但最终会到哪呢?话不多说,一看了之。

我们跟进TextView#setText

查看核心逻辑,checkForRelayout,跟进,

不管怎样,都会调用到requestLayout方法。这是View内置方法,并且用final关键字修饰。我们看下该方法定义。

笔者在这里注意到,通过设置mPrivateFlags,标记此View强制layout,重绘,然后请求到mParent.requestLayout。mParent正如笔者所述,其顶层一定是VIewRootImpl。那么这里我们发现,通过更改子View的属性,仍通过委托机制到父View,此过程中如果需要自己View绘制流程中更改,需标记某些Flag。于是乎,我们又来到VIewRootImpl#requestLayout。

有兴趣的读者可自行分析setVisiblity方法,会发现仍是设置某种flag到自身mFlag类成员上,通过向父View委托,最终仍触发VIewRootImpl#requestLayout,那么笔者就假设VIewRootImpl#requestLayout是一切绘制流程的起点吧。

二,真正绘制前过程

checkThread,检查是否requestLayout在UI线程,这里的UI线程是主线程。读者在这里思考下,假设在Activity#onCreate中在子线程设置TextView#setText,会抛出异常吗?可能不会,为什么呢?因为只有在onResume时期,才会创建VIewRootImpl,而TextView通过setText委托了自己的请求向上传,但终点mParent是null,也就不会抛出此异常了。

我们继续跟进核心逻辑scheduleTraversals方法。

posySyncBarrier是在当前MessageQueue中插入消息屏障。什么是消息屏障呢?笔者在此啰嗦一下。消息屏障的本质在是MessageQueue中插入一条特别的Message,其target字段为null,代表没有处理者。这个时候,所有在消息屏障后面的同步消息都被阻塞,只有异步消息能通过屏障执行。如下,

那什么是异步呢?可以理解为高优先级Message,可以插队。我们跟进到Choregrapher看看,

通过Message#setAsynchronous为true,指定MSG_DO_SCHEDULE_CALLBACK为异步消息,action即使上文传入的runnable,即VIewRootImpl#TraversalRunnable。我们继续跟进。

笔者在这里不太想暂开讲了,上述代码在Choreographer中,是与帧同步相关的逻辑,感兴趣的读者可自行了解。当满足当前帧条件时,执行到Choreographer#doFrame方法。Choreographer#doFrame中会计算下是否有跳帧现象,如下

最终执行doCallbacks逻辑,

层层递进,终于执行VIewRootImpl#TraversalRunnable,

在此总结下,ViewRootImpl#scheduleTraversals会开启消息屏障,这时候在Choreographer中的消息又全部是异步消息,那样不管我们的MessageQueue中有多少同步消息,也不会延迟帧逻辑,就不会造成卡顿。当我们真正执行到scheduleTraversales时,就会移除消息屏障,调用performTraversals,这时候,可以说,进入了绘制的逻辑。

三,所谓的绘制过程

VIewRootImpl#performTraversals,该函数在Android中出了名的长,笔者在这里挑选核心逻辑解释下。

首先是performMeasure,dfs方式去解析每个View的大小,这很重要。我们跟进看一下,

由于meaure是被final修饰,内部会回调onMeasure方法,

测量参数widthMeasureSpec和heightMeasureSpec传入。

注意到,View的ionMeasure不复杂,重点在于VIewGroup提供了measureChildren方法,而各个继承ViewGroup的View需在OnMeasure中调用measureChildren,才能def测量过程,我们跟进,

进而继续调用View#measure,进而调用setMeasureDimensionRaw随后将测量大小保存到measureHeight,measureWidth中,这样一通操作下来,从跟DecorView->每个叶子View,其内部大小都被保存,接下来,我们回到ViewRootImpl#performTraversals中,开始performLayout过程,此过程众所周知,计算出每个View的位置。

跟进到layout,

笔者说下核心,isLayoutValid返回此次layout是否合理。注意到从子view委托到父view中,有设置一些Flag,来表示自己需要layout,如下

因此只有需要layout的view有此flag,就不会全局layout了。

此方法会回调View#Onlayout方法,View#Onlayout方法默认无实现,

ViewGroup重写此方法,标记为abstract,意图让ViewGroup的子类必须决定子View的摆放位置

具体的实现,感兴趣的读者可以自行阅读,比较简单。

那么,当ViewTree中所有的view大小和位置都确定后,该干什么呢?在ViewRootImpl#performTravels中,最后调用performDraw方法,如下。

注意,这很关键。正如笔者前面所说,所有的应用层绘制,本质是构造一种绘制Request,发给服务端SurfaceFinger,所以与其说此处是draw,不如说是createDrawRequest。不过,我们继续看下ViewRootImpl#draw实现。

注意到如下mSurface

这里的mSurface可以理解为DrawRequestClient这个概念,在ViewRootImpl中被创建,

其lockCavas方法可以同等理解为DrawRequestClient.createRequest,即我们向Cavas中添加的各种绘制操作,都可以理解为一种请求,通过unlockCanvasAndPost本质就是提交请求,具体的过程我们稍后再谈。

我们继续跟进逻辑drawSoftware,除了每个父View构造自己的请求,子View也需要在此添加些什么。

在drawSoftware中

这里的mView是DecorView,不再赘述,其直接调用View#draw方法,默认实现中会绘制前景图、背景图等,然后回调众所周知的onDraw方法,canvas作为参数传入。

并且,调用dispatchDraw方法dfs便利子View,ViewGroup实现了此方法,如下,

通过drawchild,

随后dfs式向下传递canvas对象,当收集了此ViewRootImpl的所有绘制请求后。接下来干什么呢?笔者猜想,该去请求SurfaceFinger,真正绘制这些请求了。

四,真正的绘制过程

通过三过程,ViewRootImpl#draw中,最后执行surface.unlockCanvasAndPost方法将绘制请求提交,如下

那么,笔者对此非常感兴趣,因此继续跟踪。

此函数最终调用到nativeUnlockCanvasAndPost方法,

我们进入native层看看,

在native层,先校验surface是否有效,如果有效,进而调用unlockAndPost方法

由于笔者没有native层的源码,阅读起来实在不便。感兴趣的读者可以自行阅读下底层实现吧。不过大意可以说下,native层通过与SurfaceFinger进行通信,将绘制请求交给SufaceFinger,SurfaceFinger会在其后置缓存中绘制内存,等到下一给垂直帧信号来到时,如果绘制成功,则交换前置缓存和后置缓存,这样就实现了绘制内容的显示。

另外,简单总结下View的绘制流程。

当ActivityResume时或某个View变化时,通过委托机制请求到ViewRootImpl方法,调用其requestLayout方法,这时设置同步屏障。当垂直信号有效,移除同步屏障。开始对需要变化的View重新测量、布局,最后通过surface拿到canvas,将绘制过程添加到canvas中,然后与SurfaceFinger通信,实现内容的展示。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/246140.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C语言之指针的地址和指向的内容总结(八十四)

简介: CSDN博客专家,专注Android/Linux系统,分享多mic语音方案、音视频、编解码等技术,与大家一起成长! 优质专栏:Audio工程师进阶系列【原创干货持续更新中……】🚀 优质专栏:多媒…

【开源】基于JAVA语言的实验室耗材管理系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 耗材档案模块2.2 耗材入库模块2.3 耗材出库模块2.4 耗材申请模块2.5 耗材审核模块 三、系统展示四、核心代码4.1 查询耗材品类4.2 查询资产出库清单4.3 资产出库4.4 查询入库单4.5 资产入库 五、免责说明 一、摘要 1.1…

带【科技感】的Echarts 图表

Echarts脚本在线地址 https://cdn.jsdelivr.net/npm/echarts5.4.3/dist/echarts.min.js 引入Echarts 脚本后粘贴代码 vue2 代码&#xff1a; <template><div><div ref"col-2-row-2" class"col-2-row-2"></div></div> <…

机器学习笔记:地理加权回归(GWR)

1 传统的线性回归 机器学习笔记&#xff1a;线性回归_线性回归的读书笔记-CSDN博客 最优的β为&#xff1a; 2 地理加权回归&#xff08;GWR&#xff09; 2.1 模型概述 地理加权回归&#xff08;Geographically Weighted Regression&#xff0c;GWR&#xff09;是传统回归分…

【GPU】CUDA是什么?以及学习路线图!

什么是CUDA 作者&#xff1a;Keepin 1、cuda是英伟达开发的一套应用软件接口&#xff08;API&#xff09;。其主要应用于英伟达GPU显卡的调用。 2、云计算可以简单的理解为是通过网络组合成的计算机集群&#xff0c;用于各种加速&#xff0c;其中以CPU为主&#xff0c;GPU为辅…

Spring-AOP

1.概念 AOP(Aspect Oriented Programming)&#xff0c;意为“面向切片编程”&#xff0c;是Spring中一个重要的内容&#xff0c;其本质是动态代理&#xff0c;通过加入切片的方式&#xff0c;降低了各个业务逻辑之间的耦合度&#xff0c;让原生代码更加具有专一性 画个图方便理…

线性表--栈

1.什么是栈&#xff1f; 栈是一种特殊的线性表&#xff0c;其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除 操作的一端称为栈顶&#xff0c;另一端称为栈底。栈中的数据元素遵守后进先出的原则。 压栈&#xff1a;栈的插入操作叫做进栈/压栈/入栈&#xff…

智能AI系统开发,专业软件硬件物联网开发公司,探索未来科技新纪元

在信息时代&#xff0c;人工智能&#xff08;AI&#xff09;、物联网等前沿技术日益受到人们的关注。智能AI系统、专业软件硬件物联网开发公司应运而生。今天&#xff0c;我们将向大家介绍一家位于XX城的专业公司&#xff0c;致力于智能AI系统开发和软件硬件物联网领域的创新研…

华清远见作业第三十二天——C++(第一天)

思维导图&#xff1a; 提示并输入一个字符串&#xff0c;统计字符中大写、小写个数、空格个数以及其他字符个数要求使用C风格完成。 代码&#xff1a; #include <iostream> #include<array> using namespace std;int main() {string str;cout << "请输…

用大模型训练实体机器人,谷歌推出机器人代理模型

谷歌DeepMind的研究人员推出了一款&#xff0c;通过视觉语言模型进行场景理解&#xff0c;并使用大语言模型来发出指令控制实体机器人的模型——AutoRT AutoRT可有效地推理自主权和安全性&#xff0c;并扩大实体机器人学习的数据收集规模。在实验中&#xff0c;AutoRT指导超过…

K8s 安装部署-Master和Minion(Node)文档

K8s 安装部署-Master和Minion(Node)文档 操作系统版本&#xff1a;CentOS 7.4 Master &#xff1a;172.20.26.167 Minion-1&#xff1a;172.20.26.198 Minion-2&#xff1a;172.20.26.210&#xff08;后增加节点&#xff09; ETCD&#xff1a;172.20.27.218 先安装部署ETC…

pytorch 实现中文文本分类

&#x1f368; 本文为[&#x1f517;365天深度学习训练营学习记录博客&#x1f366; 参考文章&#xff1a;365天深度学习训练营&#x1f356; 原作者&#xff1a;[K同学啊 | 接辅导、项目定制]\n&#x1f680; 文章来源&#xff1a;[K同学的学习圈子](https://www.yuque.com/mi…

WPF自定义圆形百分比进度条

先看效果图 1.界面代码 <UserControl x:Class"LensAgingTest.CycleProcessBar1"xmlns"http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x"http://schemas.microsoft.com/winfx/2006/xaml"xmlns:mc"http://schemas.op…

Java研学-代理模式

一 概述 1 分类 静态代理&#xff1a;在程序运行前就已经存在代理类的字节码文件&#xff0c;代理对象和真实对象的关系在运行前就确定了。&#xff08;代理类及对象要自行创建&#xff09;   动态代理&#xff1a;代理类是在程序运行期间由 JVM 通过反射等机制动态的生成的…

朴素贝叶斯分类算法

1.分类算法 分类算法是有监督学习的一个核心问题&#xff0c;他从数据中学习一个分类决策函数或分类模型&#xff0c;对新的输入进行预测&#xff0c;输出变量取有限个离散值。 &#x1f30d;分类算法的内容是要求给定特征&#xff0c;让我们得出类别。 那么如何由指定特征&…

Asp.Net Core 获取应用程序相关目录

在ASP.NET Core中&#xff0c;可以通过以下三种方式获取应用程序所在目录&#xff1a; 1、使用AppContext.BaseDirectory属性&#xff1a; string appDirectory AppContext.BaseDirectory; 例如&#xff1a;D:\后端项目\testCore\test.WebApi\bin\Debug\net6.0\ 2、使用…

Leetcode刷题笔记题解(C++):LCR 153. 二叉树中和为目标值的路径

思路&#xff1a;利用回溯的思想&#xff0c;回溯的退出条件为当前节点为空&#xff0c;是符合路径的判断条件为路径和为目标值且叶子节点包含了&#xff0c;代码如下&#xff1a; /*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *…

【C++】入门基础

前言&#xff1a;C是在C的基础之上&#xff0c;容纳进去了面向对象编程思想&#xff0c;并增加了许多有用的库&#xff0c;以及编程范式等。熟悉C语言之后&#xff0c;对C学习有一定的帮助&#xff0c;因此从今天开始们将进入&#xff23;的学习。 &#x1f496; 博主CSDN主页:…

《动手学深度学习(PyTorch版)》笔记4.5

注&#xff1a;书中对代码的讲解并不详细&#xff0c;本文对很多细节做了详细注释。另外&#xff0c;书上的源代码是在Jupyter Notebook上运行的&#xff0c;较为分散&#xff0c;本文将代码集中起来&#xff0c;并加以完善&#xff0c;全部用vscode在python 3.9.18下测试通过。…

ES文档索引、查询、分片、文档评分和分析器技术原理

技术原理 索引文档 索引文档分为单个文档和多个文档。 单个文档 新建单个文档所需要的步骤顺序&#xff1a; 客户端向 Node 1 发送新建、索引或者删除请求。节点使用文档的 _id 确定文档属于分片 0 。请求会被转发到 Node 3&#xff0c;因为分片 0 的主分片目前被分配在 …