android 沉淀 - 渲染原理
ps:android 渲染啊我以为很简单呢,半天就能搞定,谁想居然有一大堆七八糟的名字,让我绕了很久…… 可是着实费了一番功夫,时间可没少花,这些东西其实基本和 PC 渲染原理差不多,做过 PC 客户端的同学学起 Android 来会轻松很多,所以我以前很好奇公司新来的搞 PC 客户端的同学为啥学起 android 来进度这么块呢,原来真是一法通万法通啊,原理都是通用的,只要理解了再学新的真是能省很大力气啊
本文我基本上都是讲讲原理,不怎么涉及代码层面,因为代码层面基本上都是 android 底层 C 的代码,我也讲不好,大家想看代码的话就去看我发的学习链接
大家看我文章看不懂的话,请看这篇入门文章,会好很多:
这些日子觉得自己的学习能力见涨,应该是俗称的开窍
了,多一决定再次挑战这部分。期间我找了一堆资料,android 渲染涉及很多部分,包括:
-
WindowManageService、SurfaceFlinger
这2个系统进程,尤其是SurfaceFlinger
进程可是渲染的核心 - 还有
window、suarface、双缓存、3缓存、VSYNC 同步信号,同步屏障,渲染,格栅化,纹理,硬件加速
等概念 - 更有
Skia,OpenGL/ES
绘图引擎
说实话,啃下这些来真是不容易啊,我是看一点理解一点,然后又迷糊一点,再查资料搞懂区别,关联,这是费劲,就其原因是没有适当全面的入门资料,网上资料很多,但是很不好理解,知识点都是一块块割裂开的,把他们串起来太费劲了,但是这是所有显示设备显示的通用逻辑,我们在 android 这里学明白了,再学其他平台也是一样的
非常感谢几位同学的资料,非诚系统, 刚兴趣的推荐去看看:
夕月风 | ZHTo0 | Stan_Z | 老罗
纷乱的系统 API
系统的这些 API 才是系统运行机制的核心,熟悉他们的具体运行才能了解一个系统的大概。另外万变不离其宗,换个系统,这些很多都是换汤不换药,有助于我们熟悉新系统:
- WindowManagerService - 单独的系统进程,专门管理所有 UI window 的排版,位置,显示问题,像输入法弹出来就是 WindowManagerService 控制的
- ActivityManagerService - 单独的系统进程,专门负责 android 4大组件的创建和运行,像 Activity,service 生命周期的调度都是由 ActivityManagerService 管理的
- PackageManagerService - 单独的系统进程,apk 安装时用,保存所有的 app 信息,launch 左面启动是就是从 PackageManagerService 里获取多有的 app 的信息
- ServiceManage - 单独的系统进程,虽然名字最后没有Service,但这也是个系统进程,是 android 系统第一个启动的系统进程,负责 IPC 跨进程通信,保存有所有进程的 binder 通信地址,ServiceManage 进程的 binder 通信地址是固定的,是为了能与所有的进程通信,binder 通信地址统一都是 0
- Surface Flinger - 单独的系统进程,负责 2D 渲染,生成最终的帧,直接操作硬件驱动在屏幕上显示
- Surface - 可以看成一块显存,任由 canvas 在上面作画
- WindowManager - app 进程内是唯一的,用来和 WindowManagerService 通信,记录管理 app 内所有的 Window
- Window - 系统层面抽象的界面单元,每一个 Activiyt 对应一个 Window
进一步理解系统 API
ServiceManage
比较特殊,一般不涉及到他,但是其在binder
中办严重最重要的角色,学习ServiceManage
最好是从binder
入手,具体的请大家把上面binder
那一节学习透彻
接下来我们从功能分层
这个角度好好盘点一下这些系统 API,google 为什么要搞这么多角色出来。大家会发现 Google 在这里使用了大量的模板模式
,模板
那是一层套一层,期中我们需要学明白2点:
这2点都是核心的知识,是可以作为我们的核心积累一直对我们提供帮助的,这些都是属于变化较慢但是非常基础重要的部分,大家学好在今后的学习中自然事半功倍,比如去学习 Flutter 就能 体会到了
我们来讲个故事
ps: 非常感谢重学安卓:Activity 的快乐你不懂这篇文章提供的思路
android 一开始开发时就是块搬砖,就有个屏幕, 开发人员直接用 C++ 调HAL 硬件抽象层
画界面
但是吧 C++ 也不是人人都会写的,广大开发者不会啊,怎么办,只能给开发这个提供工具啦,于是Surface Flinger
这个系统服务诞生啦,surface Flinger
封装了那些直接操作硬件驱动的C++代码,这才让广大开发者开发 app 成为了可能
Surface Flinger
的职责就是专门负责 UI 的渲染,人们想要在屏幕上渲染出什么内容,都可以通过Surface Flinger
来间接地与屏幕打交道。这就好比你在电脑上排版好的文档,只需通过打印机驱动程序这个中介,就能帮助你将文档内容输出到纸上,至于内容本身究竟有些什么,这我不管,我只负责统一地、有序地将内容安排成输出设备能理解的方式,来实现输出
Surface Flinger
的出现是什么思路,就是剥离具体功能
,把一类功能专门抽象封装放在一起,方便调用,简化思路,
得意于Surface Flinger
,我们画界面可以不用管 C++ 代码了,但是一个 app 界面好多啊,不光有开发者自己 app 的界面,还有系统的界面,比如状态栏
,底部按键导航栏
都是系统的 UI,他们直接的显示,切换,动画好复杂的,而且每个界面里我都要写这些代码,不说写几遍的问题,但是写出来就好难啊,这就把广大开发者距之门外,除了个别技术大N 之外谁能开发的了 app 啊,这可不成啊,软件环境起不来是怎么占领市场,是要被别人干死的啊,所以啊 window 这个概念出现了,连带着产生了WindowManagerService
,WindowManager
,Window
这几个 API
其核心就是Window
这个概念,我们可以把Window
看作是一个空白的界面,界面具体什么样子有具体的页面实现,Window
负责和Surface Flinger
交互,我们在Window
这里看到是模板
,所以Window
就是界面的抽象模板
,这样就好理解多了
WindowManager
也好理解,一个 app 范围内WindowManager
的实现类是单例的,保存有app内所有的界面的根view,一个页面对应一个根view
WindowManagerService
也不难理解,他是个系统进程,负责管理所有的Window
,Window
也是份系统自己的还是用户app的,他们之间如何重叠显示,如何动画,都是WindowManagerService
关心的,最典型的就是状态栏
了,状态栏
可是一个单独的系统Window
,他和所有app页面的重叠显示都是靠WindowManagerService
完成的,另外 android 的点击事件的传递也是依靠WindowManagerService
实现的
上文书我们有了Window
了,终于可以轻松很多了,不用再一遍遍的写那些会绘制的底层代码了,但是随着 app 变得越来越复杂,页面数量也是大幅度增加啊, 这Window
一多,相互之间的切换显示,生命周期同步,事件接受,view 视图树的绘制要一遍一遍的写真是麻烦啊,虽然我把相关功能抽象封装成功能累了,但是每次这次功能类代码还是要在写一遍,很麻烦的呀,更麻烦的是这部分逻辑代码真是频繁修改啊,每次改代码真是要死的心都有啦,要改的地方越来越多啊,天天不干别的事了就是改他们玩了。这样不行啊,我们像Window
一样搞个模板出来啦,处理具体的 view 长啥样,其他的生命周期交互,和WindowManagerService
通信,事件分发都封装到Activity
这个模板里
- 总之
相由心生 境随心转
,在系统 API 中,每个类必然有其扮演的角色,必然有需要其完成的任务,这些 API 角色很多,对于大家最不友好的一点就是命名问题
,大家看名字看不懂啊,查字典看翻译也不知具体是干啥的,这就造成了学习上的困难,提高了系统复杂度。但是反过来大家再想想,在我们封装功能模块时,命名是不是要好好想想,符合中国人的语言环境
,类要不要精简以下,做到以上那么我们的代码至少能好看不少,好理解不好,局座说过:好看的必然现金
,这话放在编程领域也是通用的,好看的代码,能见名知意的,功能分离清晰的,模板抽象准确的
其实就是大 N 级别的代码,要是我们再能把代码效率性能搞上去就是大神级别的代码,要是我们能有所创新,那我们自己就是业务的 NO1 了。不要妄自菲薄,随着积累加深和我们有意为之,其实达到大N级别是通过积累和努力能达到的,要是有点天赋在网上也是很可能的
这一切的基础都是积累,各种基础知识、套路、思路的积累,积累到了自然NB,所以大家多多写博客吧,写下来的才是自己的,才能告诉未来的自己我会什么
关于 window 和显示的问题再解释一波
做 android 的朋友都知道window
就是干显示的活,至于如何显示的其实很门道的,很多人不清楚,但是本身也不复杂,了解清楚对于我们很有好处
这部分内容在学习 app 启动流程和 activity 启动流程时有详细涉及:
window
是具体页面的模板抽象,WindowManagerService
负责处理整个 android 系统中所有window
的排版,Surface Flinger
干的是滴啊勇硬件驱动显示的活
但是这里面其实还有一个surface
的概念,surface
可以看成一块显存,一个window
窗口对应一块surface
,这个surface
的管理是Surface Flinger
干的。每创建一个window
窗口时,WindowManagerService
都会给对应的window
窗口找Surface Flinger
申请一块surface
显存出来,然后window
和WindowManagerService
共同持有和Surface Flinger
IPC通信的ISurface
binder对象,window
里面的 view 通过 canvas 直接操作ISurface
在这块显存中绘制图像,WindowManagerService
则通过ISurface
操作window
之间位置变换和动画,surface
之间也存在z轴
这个概念,Surface Flinger
在16ms
刷新通知来了之后把自己管理的所有surface
按照z轴
顺序生成最终的一帧纹理,然后交给 GPU 显示。surface
的本质是矩阵,参考 bitmap 想想下。基本上 android 的显示核心就是这个流程,大家熟悉下
window
的本质是什么呢?其实就是一块显示区域,在 Android 中就是绘制的画布:Surface
当一块Surface
显示在屏幕上时,就是用户所看到的窗口了。WindowManagerService 添加一个窗口的过程,其实就是WindowManagerService
为其分配一块Surface
的过程,一块块的Surface
在WindowManagerService
的管理下有序的排列在屏幕上,Android 才得以呈现出多姿多彩的界面,根据对 Surface 的操作类型可以将 Android 的显示系统分为三个层次,如下图:
介绍几个概念
先对总体有个概念,那么接下来学习就会顺利很多,会减少你一些困扰,这是学习一个大系统的不二法门
-
格栅化 - 从
向量图(矢量图)
到位图
的过程 - 向量图的本质是:
多边形和纹理
- 多边形 - 字面理解就行,用 canvas 画个图形出来,这个图形就是多边形
- 纹理 - 每个对变形对应的颜色个图片,比如我们 canvas.drawRect,然后画笔有个颜色,那么这个颜色就是这个多边形的纹理了,或者画笔有个图样,我们画近这个多边形里,那么这张图也就变成了纹理了
- android 系统的渲染,我们可以理解成:从代码到位图的过程没,比
格栅化
多出来的是计算多边形和纹理
的过程
概括下 android 渲染过程
- 解析 xml,生成相应的 DisplayList 绘制任务队列,viewgroup 是 RenderNode 渲染节点,具体的 view 是 DisplayList 绘制任务,此时 DisplayList 绘制队列还是空的
- 对 viewTree layout、draw,把每个 view 的具体绘制任务填充到 DisplayList 里面
- 然后把 RenderNode 和 DisplayList 组成的绘制任务队列交给 CPU 调用 Skia 2D引擎渲染成 bitmap 位图信息保存在共享内存中(surface管理的),并通知 SuafceFlinger 进程
- SuafceFlinger 进程接到通知,合并所有前台可以显示的 surface,生成最终显示的那一帧(也是bitmap)位图,保存到硬件帧缓存区
- 等下一个 VSYNC 同步信号过来后,SuafceFlinger 硬件帧缓存区数据,LCD 读取数据映射到 LCD 上
DisplayList 大家有的没听说说,对于应用层来说,没有能力把所属 window 计算成用于显示的 bitmap 位图的,这是需要调用底层 2D 渲染引擎的(随着 android 版本变迁和硬件加速的加入,这个 2D 渲染引擎在不断变化),应用层的任务是把 2D 渲染任务根据 view 层级计算成任务队列交给渲染引擎。RenderNode 和 DisplayList 就组成了这个渲染任务队列:
- rootRenderNode - 渲染跟根阶段,对应 viewTree 的根视图
- RenderNode - viewTree 中的每个 viewGroup
- DisplayList - 具体 view 对应的绘制任务,其实就是 canvas 如何画,canvas 画几次,DisplayList 就有几条绘制任务
Skia,OpenGL/ES 引擎
本节参考:Android 中的 skia 和 OpenGL ES
这里我们先看看 Skia,OpenGL/ES 这2个引擎,挺好理解的
Skia 和 OpenGL/ES 都是绘图引擎,在格栅化过程中执行任务,SurfaceFlinger 进程每个 window 对应的 surface 渲染完成后(实际是 layer 图层),操作 GPU 调用 OpenGL/ES API 压和 layer 图层生成最终的帧(bitmap)
- Skia - 2D 渲染
- OpenGL/ES - 3D 操作,另外 RenderScript,OpenCV,到最近的Vulkan 也都是 3D 绘图引擎,SurfaceFlinger 执行的图层合并也是 3D 渲染任务
- 一般来说 Skia 是跑在 CPU 上的,OpenGL/ES 是跑在 GPU 上的,但是在启动硬件加速后,OpenGL/ES 也可以执行 2D 渲染任务,但是还是要跑在 GPU 上
1. Skia
Skia 是 Google 系的库,android,chrome 均使用的是 Skia 这个库来渲染 2D 图形,甚至 Flutter 都是用 Skia 在 IOS 平台上渲染图形的
Skia 是 C++ 层的代码我们平时接触不到,接触的都是 Skia 的 java 封装类,比如:canvas,bitmap,animation,shapes 这些都会追溯到 Skia 的 API。Skia 核心类是 canvas,bitmap 这2个类,那么很明显了 Skia 核心是提供位图操作和各种绘制 API 的功能库
Skia 在 android 中有2种使用方式:
- canvas,bitmap 这个就不用说了
- images,shapes,colors,animation 这些都是可都是 android 内置好的绘制简单图形的 API,底层实现就是用 Skia 的绘图 API
2. OpenGL/ES
OpenGL/ES 是移动平台专用的 3D 渲染引擎,在开启硬件加速后也可以执行 2D 渲染任务。3D 渲染对性能要求较高,尤其是需要更多的内存,除了游戏之外一般很少在 android 平台上使用,在启动赢家加速后,有时会出现渲染出错,图片加载不出来的问题,其实就是因为 3D 渲染需要的内存较多。
android 平台专门提供了一个 view 进程 3D 绘制:GLSurfaceView
,详细的大家自己找找,这里不涉及了
硬件加速
这个加速是指用 GPU 代替 CPU 执行渲染任务,加速渲染任务。那么 GPU 是代替 CPU 执行的哪块任务呢,上面我们说过了应用层会把 viewTree 计算成相应的 DisplayList 渲染任务队列,这个渲染队列是 CPU 在 UI 线程,使用 Skai 2D 渲染引擎执行的。硬件加速呢,是新开一个线程 RanderThread,在这里线程里使用 GPU 使用 OpenGL/ES 执行渲染队列任务
硬件加速的这个渲染引擎啊,随着版本的变化一直在进化:
- 3.0 之前,不支持硬件加速,CPU 使用 skia 执行 2D 渲染任务
- 4.X 开始,android 默认支持硬件加速,GPU 使用 OpenGL/ES 执行 2D 渲染任务
- 8.0 渲染引擎可以选择切换为 SkiaGL
- 9.0 渲染引擎默认为 SkiaGL,但是 9.0 可以支持 vulkan 这个渲染引擎了
- SkiaGL 相比 OpenGL/ES 没有什么性能上的提升,但是 vulkan 这个渲染引擎就不同了,相比 OpenGL/ES,vulkan 的性能是大幅度提升的
硬件加速兼容性
4.X 开始 Android 默认开启硬件加速,但是硬件加速有部分操作是不支持的,尤其是自定义 view 这里有时我们会碰到一些:
渲染错误,渲染不出来,色块错误,图片加载不出来
白屏、花屏、闪屏
低 RAM 内存配置手机上闪退
页面中使用大量图片或者过于复杂的CSS样式时同样容易出现白屏、花屏、闪屏现象
还有些很摸不着头脑的问题,比如:
- 加载大图,硬件加速中 OpenGL 对于内存是有限制的,如果遇到了这个限制,Log 会提示这个问题:
- EidtText 重叠显示
- Canvas
- clipPath()
- clipRegion()
- drawPicture()
- drawTextOnPath()
- drawVertices()
- Paint
- setLinearText()
- setMaskFilter()
- setRasterizer()
- Xfermodes
- AvoidXfermode
- PixelXorXfermode
- Canvas
- clipRect(): XOR, Difference 和 ReverseDifference 这三种裁剪模式被忽略。3D转换不适用于修剪矩形
- drawBitmapMesh(): 颜色矩阵被忽略
- Paint
- setDither(): 被忽略
- setFilterBitmap(): 过滤一直处于开启状态
- setShadowLayer(): 只能和文字一起使用
- PorterDuffXfermode
- PorterDuff.Mode.DARKEN 等价于 SRC_OVER 如果帧缓冲区不支持混合
- PorterDuff.Mode.LIGHTEN 等价于 SRC_OVER 如果帧缓冲区不支持混合
- PorterDuff.Mode.OVERLAY 等价于 SRC_OVER 如果帧缓冲区不支持混合
- ComposeShader
- ComposeShader 仅能包含不同类型的着色器(比如包含一个 BitmapShader 的实例和一个 LinearGradient的实例是允许的,包含两个 BitmapShader 的实例却是不行的)
- ComposeShader 不能包含一个 ComposeShader
所以使用硬件加速很苦恼,硬件加速带来的性能优势非常明显,系统都默认开始了,但是硬件加速在 android 平台上又有些兼容问题,所以我们必须对这块非常清楚才行
系统自带的 view 大家放心,是没有硬件加速问题的,但是自定义 view 时我们就要格外小心了,碰到问题我们可以对某个 vierw,或是某个 activity 关闭硬件加速,只要在适当的地方关闭硬件加速即可
硬件加速性能优势
恩,现在大家都知道了硬件加速有性能优势了,但是大家知道具体的性能优势在哪里吗?万一有面试官问呢?
硬件加速的性能优势主要来源自2点:
- GPU 渲染效率比 CPU 高的多
- 开启硬件加速后,可以减少很多无关 view 的重绘
1. GPU 渲染效率比 CPU 高
这点是 GPU、CPU 自身架构决定的,CPU 计算单元少,逻辑控制单元多,为的是执行维度、类型复杂的各种任务,毕竟 CPU 是中枢嘛~。GPU 计算单元多,逻辑控制单元少,专注于图形绘制、渲染
大家看图就明白了:ALU 是计算单元,剩下的不用说大家也能猜到,所以但是把绘图、渲染任务从 CPU 切到 GPU 都是性能上的巨大提升
2. 可以减少无关 view 的重绘
这要从 DisplayList 渲染任务队列说起了,我们看个例子:
比如:button 在一个 view 上面,此时我们调用 button.Invalidate 重绘
- 软件绘制的话 - 理论上是与内容变化的 view 有关系的 view都会重绘,这里就是 button 和 view 都会重新绘制
- 硬件加速的话 - 理论上只有内容变化的 view 会重绘,这里就是 button 自己重新绘制了,button 下面的 view 不会重新绘制,这不就节约性能了嘛
硬件加速对于 DisplayList 的优化不止减少重绘的 view 数量,甚至我们修改 view 的属性(非内容,比如文字就是内容)都不会重新绘制
还是举个例子:一个 LinearLayout 包含了一个 ListView 和 Button,listview 在 button的上面,此时我们修改 ListView 透明度,Listview.setAlpha(0.5f)
原始的 DisplayList 渲染任务队列是:
- DrawDisplayList(ListView)
- DrawDisplayList(Button)
修改透明度之后,DisplayList 渲染任务队列是:
- SaveLayerAlpha(0.5)
- DrawDisplayList(ListView)
- Restore
- DrawDisplayList(Button)
现在大家体会到了硬件加速带来的性能优势了吧,属性动画涉及的属性硬件加速都支持免重绘的,所以硬件加速对属性动画的优化非常大, 这也是 Goolge 推荐大家使用属性动画的原因
这些属性我还是列一下吧:
- alpha
- x, y, translationX, translationY
- scaleX, scaleY
- rotation, rotationX, rotationY
- pivotX, pivotY
执行动画时可以强制开启硬件加速
view.setLayerType(View.LAYER_TYPE_HARDWARE, null);
ObjectAnimator animator = ObjectAnimator.ofFloat(view, "rotationY", 180);
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
view.setLayerType(View.LAYER_TYPE_NONE, null);
}
});
animator.start();
硬件加速的控制
硬件加速在 Target API >= 14 时是默认开启的
可以在 4 个级别上控制硬件加速
- application
<application
...
android:hardwareAccelerated="true"
...
>
- Activity
<activity
android:name=".MainActivity"
android:hardwareAccelerated="true">
- window
getWindow().setFlags(
WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);
- view
view.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
硬件加速设置的几个 Type:
- LAYER_TYPE_NONE - 默认行为,正常渲染,不会在off-screen buffer上备份
- LAYER_TYPE_HARDWARE - 如果应用开启了硬件加速view渲染在硬件上,否则与LAYER_TYPE_SOFTWARE行为一样
- LAYER_TYPE_SOFTWARE - 用软件渲染在一个bitmap上
检测硬件加速是否启动 API:
view.isHardwareAccelerated();
canvas.isHardwareAccelerated();
推荐使用Canvas.isHardwareAccelerated()来检测,因为View关联到一个硬件加速的window上,仍然可以使用非硬件加速的canvas绘制
android 双缓存,3缓存技术
android 可以用到双缓存(多缓存)的地方有4个,这里发散一下,但是主要的还是介绍 android 渲染机制
硬件显示设备在内核内存中有一个映射,一般叫硬件帧缓存区
,我们主要把渲染好的位图放在帧缓存区
里就能在屏幕显示出来,这个帧缓存区每隔 16ms 刷新一次
双缓存技术把硬件帧缓存区
分成 2份:Frame Buffer、Back Buffer
- Frame Buffer - 存储当前帧
- Back Buffer - 存储下一帧
实际上 LCD 对应的真正的硬件缓存去只有一个,就是 Frame Buffer,Back Buffer 时系统自身添加的没,为的是优化显示
2块硬件缓存去的交互逻辑很简单,android 系统先计算下一帧,把位图保存在 Back Buffer 里,然后在接到 VSYNC 同步信号后,把 Back Buffer 里的数据复制到 Frame Buffer 里去显示,然后再渲染下一帧
剩下用到双缓存的地方:
- surfaceView 拥有自己的 surface,自己的线程渲染数据,不用等系统同步信号,UI 数据量大一点都必须上 surfaceView 了,最明显的即使游戏了
- 在自定义 view 中,若是渲染量大了,也就是需要绘制的元素多了,那么一样会卡,会掉帧,这时候要不选择 surfaceView,要不就自己实现类似 surfaceView 的显示逻辑,使用2套 canvas、bitmap,在空闲时把内容绘制到用于缓存的 bitmap 中,在 onDraw 函数内只要 drawBitmap 就行了,例子大家可以看这篇:android双缓冲绘图技术分析
- 图片加载库,用了多个地方的缓存来暂时保存数据,内存的,硬盘的
3缓存技术其实就是多了一个硬件帧缓存区:Triple Buffer
在 4.0 的时候,android 默认启动 3缓存技术。如果说 Frame Buffer
储存当前帧,Back Buffer
储存下一阵,那么Triple Buffer
就是存储下下帧的,那么为啥要有Triple Buffer
呢,听我说上一说
在计算下一帧时,有时候计算任务较大,在 16ms 内执行不完,那么在没有 Triple Buffer
的时候,下一个 16ms 在继续计算Back Buffer
帧之后会空闲。Triple Buffer
的意思在于节约这些空闲时机,开始计算掉帧之后的下一帧,这样为了的是尽量保证下一帧不再掉帧
我们下面会学习 Choreographer 编舞者,Choreographer 会同步 LCD 硬件设备的 VSYNC 信号给 app 进程,也就是说 app 进程什么时候触发绘制任务是 Choreographer 说的算的,每 16ms Choreographer 才会触发绘制任务,那么 Triple Buffer
为啥就能不管 Choreographer 的调度而自省开始绘制呢,这不就乱了吗,要是这样的话 UI 线程会时时都在计算下下帧,那还有时间执行其他任务。所以 Triple Buffer
3缓存技术是有前置触发条件的,就是上一帧掉帧了,才会触发 Triple Buffer
去计算下下帧,以便之后不再掉帧了。所以说在 Systrace 工具中,有时候我们看到的黄色F 的那一帧,在有 Triple Buffer
3缓存技术时不一定就表示这一帧就掉帧了,需要具体看执行的时间点
图解说明以下:
- 正常不掉帧绘制
- 掉帧之后下一个 16ms 大部分时间浪费掉了
- 有了 Triple Buffer 之后,前一帧要是掉帧了,那么会有优化,充分利用空闲时间绘制下下帧
我先说一下 surfaceflinger
surfaceflinger 简称 SF,是 android 图形渲染的核心,surfaceflinger 是个系统服务,运行在独立进程里。surfaceflinger 的作用是接受多个来源的图形显示数据,将他们合成,然后发送到显示设备。比如打开应用,常见的有三层显示,顶部的 statusbar 底部或者侧面的导航栏以及应用的界面,每个层是单独更新和渲染,这些界面都是有 surfaceflinger 合成一个刷新到硬件显示。
statusbar 状态栏,系统提供的底部导航栏(比如魅族底部的那一条),他们都不再 docverView 里,不是写在基类里的,而是由专门的系统进程维护的,可以看成别的 app 里面的 window,由 surfaceflinger 进程把这些合并在一起次啊可以看见,看下图:
看到没,壁纸也是由独立进程维护的,有自己的 window
渲染中各个角色
上面说了很多边边角角的东西,现在终于到了 android 渲染的核心啦,先来看看这段经典的叙述,基本上 android 渲染中的主要角色都有提及:
SurfaceFlinger在系统启动阶段作为系统服务被加载。应用程序中的每个窗口,对应本地代码中的Surface,而Surface又对应于SurfaceFlinger中的各个Layer,SurfaceFlinger的主要作用是为这些Layer申请内存,根据应用程序的请求管理这些Layer显示、隐藏、重画等操作,最终由SurfaceFlinger把所有的Layer组合到一起,显示到显示器上。当一个应用程序需要在一个Surface上进行画图操作时,首先要拿到这个Surface在内存中的起始地址,而这块内存是在SurfaceFlinger中分配的,因为SurfaceFlinger和应用程序并不是运行在同一个进程中,应用客户端(Surface)和服务端(SurfaceFlinger - Layer)之间使用匿名共享内存来传递和同步显示缓冲区
android 渲染其实就2个端:应用进程
和 Surfaceflinger 进程
,应用进程负责渲染 viewTree,把数据交给 Surfaceflinger,Surfaceflinger 负责图层(layer)压和。因为 UI 渲染数据量大,应用进程和Surfaceflinger 进程之间不适合使用 Binder 传递数据,而是采取共享内存的方式
这块匿名共享内存的使用是交给底层 Native 代码的,应用层是感受不到的,需要把共享内存和 Canvas 在 Native 底层的实现关联起来才能操作:
JAVA层要想在Surface上进行画图操作,必须要先把其中的一个缓冲区绑定到Canvas中,然后所有对该Canvas的画图操作最后都会画到该缓冲区内
在这2端负责管理这块共享内存的就是:surface
和 layer
了,下面会重点看看surface
和 layer
,看看他们的类结构和关联,那么渲染的基本逻辑很明确的表现出来了
Surfaceflinger 端显示缓存的管理
Surfaceflinger 服务是在独立的进程中,每当一个 app 进程与 Surfaceflinger 服务建立联系后,都会给这个 app 在 Surfaceflinger 端生成一个管理显示缓存的类:SharedClient
app 端每个 window 对应一个 surface(Surfaceflinger 显示缓存在 app 端的映射),每个 surface 在 Surfaceflinger 端的 SharedClient 中都会对应一个生成一个 SharedBufferStack 用于管理显示缓存。因为 app 端是可以有多个 window 的,所以 SharedClient 中的 SharedBufferStack 是队列结构,是复数的,一般 SharedClient 中最多可以有 31个 SharedBufferStack,看图: SharedBufferStack 代表每一个 surface(渲染平面)对应的显示缓存,但是 SharedBufferStack 中的缓存可以不是一块,也是一个队列,看图:SharedBufferStack 采用缓存队列的方式存储显示数据,GraphicBuffer 才是具体一块显示缓存, SharedBufferStack 这里应该是为了性能考虑才采用的对来结构吧
app 端每次渲染后会先从 Surfaceflinger 端申请显示缓存,Surfaceflinger 一次会给 app 端2个 GraphicBuffer,为啥是2个呢,这里我没有找到确切的说法,我认为是为了处理 window 切换动画,因为在启动页面切换动画时,下一个 window 也需要参与显示,所有需要2块显示缓存 GraphicBuffer 去存储2个 window
但是话又说回来,新的页面的 window 自然也会去渲染,也会自己申请 surface 的,window 的变换动画由 WMS 控制,同步给 SF 的 layer 层,这么一想的话又说不通了,大家还是自己去找找吧,我是没搞清楚这块
Surfaceflinger 端 layer
一块显示缓存在 Surfaceflinger 端的表现就是 layer,layer 大家直接当图层看也行,反正在 Surfaceflinger 端 layer 就是管理显示缓存的顶层类了
Layer 对象的层级结构比较深,可以追溯出一层层父类来,这些父类提供了 Layer 的核心功能,来看看 Layer 的类图:
1346180533_7152.jpg
- SharedBufferServer - layer 类有一个 SharedBufferServer 类型的成员变量用来管理显示缓存,SharedBufferServer 对象会和该 layer 在 SharedClient 中对应的缓存 SharedBufferStack 挂钩,自此 Layer 通过 SharedBufferServer 类型的成员变量和 SharedClient 共享内存区建立了关联,并且每个Layer 对应于 SharedBufferStack 数组中的一项。应用层在每次绘制时申请显存地址都是 Surfaceflinger 端 layer 对象的成员变量 SharedBufferServer 代劳的
- SurfaceLayer - layer 对象还有一个 SurfaceLayer 类型的 Binder 对象,它是 SurfaceFlinger 服务用来与 Android 应用程序建立通信的,以便可以共同维护一个绘图表面,layer 会把这个 Binder 提供给 app 端 surface,app 端 surface 靠着这个 Binder 可以直接操控这块显示缓存
Surfaceflinger 端 layer 结构不难看,分配了2个职责,一个是联通共享缓存栈,一个是跨进程通讯,以便和 app 端直接通信
app 端 surface
surface 是 app 进程端的,也有叫:渲染平面
的。surface 和 window 是一一对应的,另外有些特殊 view 也拥有自己的 surface,比如 surfaceView、textrueView
具体的解释大家看看下面的,非常贴切:
每个应用程序可能对应着一个或者多个图形界面,而每个界面我们就称之为一个surface ,或者说是window
每个surface 在屏幕上有它的位置,以及大小,然后每个surface 里面还有要显示的内容。这就要求我们需要一个结构来记录应用程序界面的位置,大小,以及一个buffer 来记录需要显示的内容,所以这就是我们 surface 的概念,surface 实际我们可以把它理解成一个容器,这个容器记录着应用程序界面的控制信息,比如说大小啊,位置啊,而它还有 buffer 来专门存储需要显示的内容。
各个surface 之间可能有重叠,我们可以想象在屏幕平面的垂直方向还有一个Z 轴,所有的surface 根据在Z 轴上的坐标来确定前后,这样就可以描述各个surface 之间的上下覆盖关系了,而这个在Z 轴上的顺序,图形上有个专业术语叫Z-order。
当图形(surface )重合的时候怎么处理,有些surface 还带有透明信息,这就用到 SurfaceFlinger 了。SurfaceFlinger 会把各个surface 组合(compose/merge) 成一个main Surface ,最后将Main Surface 的内容发送给FB/V4l2 Output ,这样屏幕上就能看到我们想要的效果。
有2个节点大家记一下:
- app 创建和 Surfaceflinger 通讯
viewRootImpl 在 setView 时会会走 mWindowSession.addToDisplay -> WMS.addWindow,创建 windowState 对象,windowState 会建立和 SurfaceFlinger 服务的连接,并同步给 app 端 - 创建 window 所属 surface
在 viewRootImpl 的绘制方法中会调用 relayoutWindow,WMS 端会去 SurfaceFlinger 端申请创建对应的 layer,layer 对象会把内部 Binder(ISurface 接口实现类) 对象返回给 WMS,WMS 根据获取的 ISurface 会创建出 SurfaceContorl 对象管理 surface,WMS 再会 ISurface 同步给 app 端,app 端会创建对应的 Surafce 对象出来
注意: WMS 改变 Surface 的位置、大小时会影响 Surfaceflinger 合成 layer 的,layer 所处的位置大小变化,自然最终的图像也会变化的,这里 WMS 对 Surface 的修改是要通知 Surfaceflinger 的,Surfaceflinger 会根据变化重新计算每个 layer 可显示范围
- SurfaceControl - 内部很简单,就2个成员变量,一个是 SurfaceComposerClient 类型的成员变量 mClient,是和 Surfaceflinger 进程通信的 Binder。另一个是 ISurface 类型的成员变量 surface,是用于来和 Surfaceflinger 中所属 layer 通信的 Binder,这2个 Binder 就是其道通信的坐着用,可以看出来 WMS 是会对 surface 有重要操作的
- Surface - Surface 是 app 端对显示缓存的管理对象,其核心就是去 Surfaceflinger 申请显示缓存,然后绑定到 Canvas 上,Surface 2个成员变量基本都是给这个思路服务的。mClient 变量指向 Surfaceflinger 进程中 app 对应的 SurfaceClient,具体资料没找到,不过可以猜到应该也是个用来通信的 Binder 对象。SharedBufferClient 类型的成员变量用来保存从 Surfaceflinger 申请到的显示缓存。ISurface 类型的成员变量自然是和 Surfaceflinger 对应的 layer 通信的 Binder 对象
可以看到对应显示缓存管理都放到 Surfaceflinger 进程了,WMS、app 端都是对应不同级别的 Binder 通信对象
关于 surface 就说这么多,看看类结构就知道大概的流程了,要分析代码的话都得深入系统层的 C 代码里,大家去看上面我列出的参考资料吧,那里全,我这里只是做个科普。有多大量,吃多少饭,我觉得我写到这个程度就差不多了,仔细分析 C 代码超出我的能力了
Surfaceflinger 捕捉硬件 VSYNC 信号
大家知道 Android 屏幕没 16ms 刷洗一帧,一直以来我都是认为是 android 系统控制的这个刷频率,知道近期学习后才知道真是大错特错。这个刷新频率不是软件实现的,而是由硬件控制的。大家都知道一加 pro7
吧,他可是用了 90HZ 的屏幕,要不是硬件 LCD 来发送同步刷新信号的话,那么一加 pro7 换屏也不能从 60帧 提升到 90帧了
android LCD 硬件设备专门有一块控制屏幕刷新的电路,这块电路每 16ms 通过硬件驱动向系统发送一个刷新信号,一般大家都遵循 PC 上的传统管这个刷新信号叫:VSYNC 信号
android 系统把管理硬件缓存区和和控制图像渲染的工作交给了 Surfaceflinger 进程,那么可以肯定这个 VSYNC 信号肯定是 Surfaceflinger 进程来捕捉了,所以我们来看看 Surfaceflinger 进程
Surfaceflinger.init 函数初始化了硬件信号发生器对象 HWComposer,代码要分层嘛,Surfaceflinger 进程把硬件信号管理放到了 HWComposer 里
HWComposer 里最关键的就是往 LCD 硬件驱动上注册了同步信号监听 registerProcs
HWComposer::HWComposer(const sp<SurfaceFlinger>& flinger, EventHandler& handler)
: mFlinger(flinger),
mFbDev(0), mHwc(0), mNumDisplays(1), //初始情况下mHwc是一个空指针
mCBContext(new cb_context), //这里新创建了一个cb_context对象
mEventHandler(handler),
mDebugForceFakeVSync(false)
{
bool needVSyncThread = true;
.....
// Note: some devices may insist that the FB HAL be opened before HWC. 尝试打开硬件HWC
int fberr = loadFbHalModule();
loadHwcModule(); //加载HWC模块,若打开成功,则mHwc将不为空
.......
if (mHwc) {
ALOGI("Using %s version %u.%u", HWC_HARDWARE_COMPOSER,
(hwcApiVersion(mHwc) >> 24) & 0xff,
(hwcApiVersion(mHwc) >> 16) & 0xff);
if (mHwc->registerProcs) {
mCBContext->hwc = this;
mCBContext->procs.invalidate = &hook_invalidate;
mCBContext->procs.vsync = &hook_vsync; //硬件产生中断,通过hook_vsync函数进行同步信号的通知
if (hwcHasApiVersion(mHwc, HWC_DEVICE_API_VERSION_1_1))
mCBContext->procs.hotplug = &hook_hotplug;
else
mCBContext->procs.hotplug = NULL;
memset(mCBContext->procs.zero, 0, sizeof(mCBContext->procs.zero));
mHwc->registerProcs(mHwc, &mCBContext->procs);
}
// don't need a vsync thread if we have a hardware composer 存在硬件同步信号发生器,就不需要软件模拟线程
needVSyncThread = false;
}
}
HWComposer 接受到硬件同步信号后会回调自己的 hook_vsync方法,hook_vsync方法里又会回调 EventHandler.onVSyncReceived 方法,HWComposer 对象里的 EventHandler 是在构造函数里传进来的,看代码其实就是外层的 Surfaceflinger 自己
到这,Surfaceflinger 就实现监听硬件同步信号的任务,HWComposer 的具体代码大家自己去找,我这里不放了
Surfaceflinger 分发硬件 VSYNC 信号
上文说到 Surfaceflinger 实现了硬件同步信号监听,在硬件同步信号来了后会回调 EventHandler.onVSyncReceived 方法,这里我们来看看 Surfaceflinger 收到信号后怎么处理
我们先看 onVSyncReceived 这个函数,往 DispSync 里添加了一个同步信号
void SurfaceFlinger::onVSyncReceived(int type, nsecs_t timestamp) {
bool needsHwVsync = false;
{ // Scope for the lock
Mutex::Autolock _l(mHWVsyncLock);
if (type == 0 && mPrimaryHWVsyncEnabled) {
needsHwVsync = mPrimaryDispSync.addResyncSample(timestamp);
}
}
}
DispSync 是 Surfaceflinger 的成员变量,在 Surfaceflinger 初始化时就创建了,DispSync 核心就是创建并启动了一个 DispSyncThread 类型的线程
DispSync::DispSync() : mRefreshSkipCount(0), mThread(new DispSyncThread()) {
mThread->run("DispSync", PRIORITY_URGENT_DISPLAY + PRIORITY_MORE_FAVORABLE);
reset();
beginResync(); .....
}
DispSyncThread 线程里有集合,保存监听 VSYNC 信号的具体任务,有队列来不停循环遍历执行任务,没任务时线程挂起,上面的 mPrimaryDispSync.addResyncSample 方法其实是唤醒线程,添加VSYNC 信号
// 这是线程循环任务
virtual bool threadLoop() {
while (true) {
Vector<CallbackInvocation> callbackInvocations;
nsecs_t targetTime = 0;
{ // Scope for lock
Mutex::Autolock lock(mMutex);
bool isWakeup = false;
if (now < targetTime) {
err = mCond.waitRelative(mMutex, targetTime - now);
}
now = systemTime(SYSTEM_TIME_MONOTONIC);
callbackInvocations = gatherCallbackInvocationsLocked(now);
}
if (callbackInvocations.size() > 0) {
fireCallbackInvocations(callbackInvocations);
}
}
returnfalse;
}
// 遍历所有 callback 回调
void fireCallbackInvocations(const Vector<CallbackInvocation>& callbacks) {
for (size_t i = 0; i < callbacks.size(); i++) {
callbacks[i].mCallback->onDispSyncEvent(callbacks[i].mEventTime);
}
}
Surfaceflinger 在接受硬件的 VSYNC 信号后,唤醒专门分发 VSYNC 信号的线程,在线程里面遍历所有关心 VSYNC 信号任务的监听器的 onDispSyncEvent 函数。这里 listener 监听器其实是别人进程通过 Binder 注册到 SurfaceFlinger.mPrimaryDispSync.mEventListeners 里的
app 端接受硬件同步信号
上面说到了 Surfaceflinger 里面专门有个线程 DispSyncThread 来分发 VSYNC 信号,那么只要 app 在合适的时候往 Surfaceflinger 里注册下监听 callback 就能准确的相应屏幕刷新任务了
在绘制 view 时,不管我们走的是 requestLayout,还是 view.invalidate,最终都是走到 ViewRootImpl.scheduleTraversals 方法
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();//检查是否在主线程
mLayoutRequested = true;//mLayoutRequested 是否measure和layout布局。
scheduleTraversals();
}
}
void scheduleTraversals() {
if (!mTraversalScheduled) {//同一帧内不会多次调用遍历
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();//拦截同步Message
//Choreographer回调,执行绘制操作
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
}
}
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
performTraversals();
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
scheduleTraversals 方法干了2件事
- 先执行 postSyncBarrier 方法,给 UI 线程 loop 加了一个同步屏障,目的是阻止 handle 的同步消息执行,同步消息就是我们平常添加入 handle 的任务,这样就可以把 UI 线程腾出来,保证下一个 VSYNC 信号来时第一时间可以执行渲染页面的任务,记住这里
- mChoreographer.postCallback,给编舞者 Choreographer 添加一个任务,这个任务就是 doTraversal 渲染页面,包裹在一个 Runnable 里面,为什么要包一层 Runnable 呢,因为下一个 VSYNC 信号来后,Choreographer 会发送一个 handle 异步任务,message.post(runnable) 里的 runnable 就是我们刚刚添加的 runnable 任务。因为给 handle 设置同步屏障后,之后异步任务才能执行,这样才能保证页面渲染的及时性,后面会具体分析这里的,大家先记住结论
- doTraversal 方法中,及时接触了 handle 的同步屏障,要不 handle 的同步消息永远没有执行的机会,添加同步屏障后 handle 的运行机制是: 遍历执行异步任务,异步任务没有了线程会进入休眠状态,并不会执行同步任务
注意:Choreographer 是页面接收 Surfaceflinger 进程发送 VSYNC 信号的核心
我们每 mChoreographer.postCallback 一次,就会接收一次下一个 VSYNC 信号,记住只能接收一下,要是以后还要接收 VSYNC 信号,就得继续 mChoreographer.postCallback,所以重点就转移到 Choreographer 身上了
Choreographer 我们称为:编舞者
,用来同步 LCD 硬件刷新频率,应用层可以及时第一时间相应屏幕刷新信号
Choreographer 运行机制
- Choreographer 和 ViewRootImpl 一一对应,有一个 ViewRootImpl 就有一个 Choreographer
public ViewRootImpl(Context context, Display display) {
...
//获取Choreographer实例
mChoreographer = Choreographer.getInstance();
...
}
public static Choreographer getInstance() {
return sThreadInstance.get();
}
private static final ThreadLocal<Choreographer> sThreadInstance =
new ThreadLocal<Choreographer>() {
@Override
protected Choreographer initialValue() {
Looper looper = Looper.myLooper();
if (looper == null) {
throw new IllegalStateException("The current thread must have a looper!");
}
return new Choreographer(looper);
}
}
- Choreographer 的核心是 mCallbackQueues 存储绘制任务的容器和 mDisplayEventReceiver 接收 VSYNC 信号的接收器,mDisplayEventReceiver
private Choreographer(Looper looper) {
mLooper = looper;
mHandler = new FrameHandler(looper);
// 根据是否使用了VSYNC来创建一个FrameDisplayEventReceiver对象
mDisplayEventReceiver = USE_VSYNC ? new FrameDisplayEventReceiver(looper) : null;
mLastFrameTimeNanos = Long.MIN_VALUE;//是指上一次帧绘制时间点
mFrameIntervalNanos = (long)(1000000000 / getRefreshRate());//帧间时长,一般等于16.7ms
// CALLBACK_LAST + 1 = 4,创建一个容量为4的CallbackQueue数组,用来存放4种不同的Callback
mCallbackQueues = new CallbackQueue[CALLBACK_LAST + 1];
for (int i = 0; i <= CALLBACK_LAST; i++) {
mCallbackQueues[i] = new CallbackQueue();
}
}
- postCallback 最终会调用 postCallbackDelayedInternal,别看这个方法热闹,其实就是把传进来的 runnable 任务保存到 mCallbackQueues 队列里,然后执行 scheduleFrameLocked 去 Surfaceflinger 进程上注册监听任务
private void postCallbackDelayedInternal(int callbackType,
Object action, Object token, long delayMillis) {
synchronized (mLock) {
// 当前时间
final long now = SystemClock.uptimeMillis();
// 回调执行时间,为当前时间加上延迟的时间
final long dueTime = now + delayMillis;
// obtainCallbackLocked(long dueTime, Object action, Object token)会将传入的3个参数转换为CallbackRecord(具体请看源码,非主要部分,此处略过),然后CallbackQueue根据回调类型将CallbackRecord添加到链表上。
mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
if (dueTime <= now) {
// 如果delayMillis=0的话,dueTime=now,则会马上执行
scheduleFrameLocked(now);
} else {
// 如果dueTime>now,则发送一个what为MSG_DO_SCHEDULE_CALLBACK类型的定时消息,等时间到了再处理,其最终处理也是执行scheduleFrameLocked(long now)方法
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
msg.arg1 = callbackType;
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, dueTime);
}
}
}
- scheduleFrameLocked 会执行 scheduleVsyncLocked,再执行 scheduleVsync。在 scheduleVsync 内部会通过 nativeScheduleVsync 在 Native 代码层上去 Surfaceflinger 进程上注册监听任务,这种监听任务只能监听一次
private void scheduleVsyncLocked() {
mDisplayEventReceiver.scheduleVsync();
}
public void scheduleVsync() {
if (mReceiverPtr == 0) {
Log.w(TAG, "Attempted to schedule a vertical sync pulse but the display event "
+ "receiver has already been disposed.");
} else {
nativeScheduleVsync(mReceiverPtr);
}
}
中断一下,这是别人写的,大家先看看,略顺了好往下看
mDisplayEventReceiver 对应的是FrameDisplayEventReceiver,它继承自 DisplayEventReceiver , 主要是用来接收同步脉冲信号 VSYNC。scheduleVsync()方法通过底层nativeScheduleVsync()向SurfaceFlinger 服务注册,即在下一次脉冲接收后会调用 DisplayEventReceiver的dispatchVsync()方法。这里类似于订阅者模式,但是每次调用nativeScheduleVsync()方法都有且只有一次dispatchVsync()方法回调
至于 nativeScheduleVsync 如何与 SurfaceFlinger 建立联系,我看老罗的说法:SurfaceFlinger 进程有一个 EventThread 线程,该线程接受到 SurfaceFlinger 第一次发射的 VSYNC 信号再进行2次发射。每个 app 进程与 SurfaceFlinger 进程保持一个 socket 通信链接,类型为 conntation 然后保存到 EventThread 线程的指定容器内。这里我只是看到老罗这么说,其然的解释我没找到,先姑且这么认为
- FrameDisplayEventReceiver onVsync 直接接受到 SurfaceFlinger 发送过来的 VSYNC 信号,然后向主线程 handle 发送一个异步任务,异步任务里执行的是下面的 run 方法
private final class FrameDisplayEventReceiver extends DisplayEventReceiver implements Runnable {
private boolean mHavePendingVsync;
private long mTimestampNanos;
private int mFrame;
@Override
public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
//忽略来自第二显示屏的Vsync
if (builtInDisplayId != SurfaceControl.BUILT_IN_DISPLAY_ID_MAIN) {
scheduleVsync();
return;
}
...
mTimestampNanos = timestampNanos;
mFrame = frame;
//该消息的callback为当前对象FrameDisplayEventReceiver
Message msg = Message.obtain(mHandler, this);
msg.setAsynchronous(true);
//此处mHandler为FrameHandler
mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
}
@Override
public void run() {
mHavePendingVsync = false;
doFrame(mTimestampNanos, mFrame);
}
}
我描述不清的话,大家看别人的解释
可见onVsync()过程是通过FrameHandler向主线程Looper发送了一个自带callback的消息 callback为FrameDisplayEventReceiver。 当主线程Looper执行到该消息时,则调用FrameDisplayEventReceiver.run()方法,紧接着便是调用doFrame。
- doFrame 就是把之前存进来的 callback 都跑一遍
void doFrame(long frameTimeNanos, int frame) {
final long startNanos;
synchronized (mLock) {
if (!mFrameScheduled) {
return; // mFrameScheduled=false,则直接返回。
}
long intendedFrameTimeNanos = frameTimeNanos; //原本计划的绘帧时间点
startNanos = System.nanoTime();//保存起始时间
//由于Vsync事件处理采用的是异步方式,因此这里计算消息发送与函数调用开始之间所花费的时间
final long jitterNanos = startNanos - frameTimeNanos;
//如果线程处理该消息的时间超过了屏幕刷新周期
if (jitterNanos >= mFrameIntervalNanos) {
//计算函数调用期间所错过的帧数
final long skippedFrames = jitterNanos / mFrameIntervalNanos;
//当掉帧个数超过30,则输出相应log
if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
Log.i(TAG, "Skipped " + skippedFrames + " frames! "
+ "The application may be doing too much work on its main thread.");
}
final long lastFrameOffset = jitterNanos % mFrameIntervalNanos;
frameTimeNanos = startNanos - lastFrameOffset; //对齐帧的时间间隔
}
//如果frameTimeNanos小于一个屏幕刷新周期,则重新请求VSync信号
if (frameTimeNanos < mLastFrameTimeNanos) {
scheduleVsyncLocked();
return;
}
mFrameInfo.setVsync(intendedFrameTimeNanos, frameTimeNanos);
mFrameScheduled = false;
mLastFrameTimeNanos = frameTimeNanos;
}
try {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#doFrame");
//分别回调CALLBACK_INPUT、CALLBACK_ANIMATION、CALLBACK_TRAVERSAL事件
mFrameInfo.markInputHandlingStart();
doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
mFrameInfo.markAnimationsStart();
doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
mFrameInfo.markPerformTraversalsStart();
doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
- 这样经 Choreographer 调度之后,最终又回到了 ViewRootImpl.doTraversal,在这里先解除了 handle 的同步屏障,然后进行具体的绘制任务
好了到这里 Choreographer 同步 VSYNC 信号的流程就过了一遍,大家几个大概就行,记代码也没有意义
Surfaceflinger 线程模型
前面可是说了 Surfaceflinger 不少东西了,但是 Surfaceflinger 还是要再撸撸的,谁让 Surfaceflinger
干的事多呢,多就复杂了,越复杂越不好搞,越容易理解错误
Surfaceflinger 里面一共有3个线程和一个线程池:
- Binder 线程池 - 这个是 Binder 线程池,这个好理解,在 AIDL 的 C/S 模型中,服务端的 Binder 对象运行在线程池的线程中, Surfaceflinger 要与不同 app 进程,WMS 进程建立通讯,Binder 自然少不了了,具体的就不解释了
- DispSyncThread 线程 - 这个前面说了,Surfaceflinger 启动时会一块启动 DispSyncThread 线程,用来接收硬件驱动层发送的 VSYNC 信号再转发出去
- app EventThread 线程 - 上文说 app 端的 viewTree 在准备绘制的时候会先向 Surfaceflinger 进程注册一下以便及时拿到 VSYNC 信号,这个任务是由 Choreographer 编舞者通过 Native 底层代码实现的,app 端和 Surfaceflinger 进程建立一个 socket 通讯。Surfaceflinger 进程主线程很忙的,自热不可能去管这事的,那么就交给名叫 app 类型为 EventThread 的线程了,这个类型的线程很好理解,和 UI 线程一样有 handle、looper,区别是 handle、looper 类型是特殊的。具体的下面细说
- sf EventThread - 这个线程就是 Surfaceflinger 进程的主线程了,专门干合成 layer 图层,交换帧缓存去数据的任务,app 端完成 surface 的渲染后会用 Binder 通知 sf 线程,sf 接到通知后会开始渲染任务,WMS 修改 surface 大小,位置后也会通过 Binder 通知 sf,因为 surface 是堆叠在一起的,surface 的任何修改都会造成最终合成图形的变化。具体的下面细说
代码分析大家去看: Android垂直同步信号VSync的产生及传播结构详解
app EventThread
上文书这个名为 app 的 EventThread 线程负责接收 Surfaceflinger 进程中 DispSyncThread 线程赚发的来自硬件层的 VSYNC 信号,进行二次转发。为啥需要二次转发,我认为应该是考虑必须要第一时间相应硬件层 VSYNC 信号,这个线程不能干别的,只干一件事:就是信号转发给二道贩子。二道贩子拿到信号之后怎么处理都不干扰接收信号。
app 层的 Choreographer 编舞者是通过 DisplayEventReceiver.nativeScheduleVsync 方法通过底层到 Surfaceflinger 进程上注册 callback 回调,这个 callback 啥也不干也是就接收 VSYNC 信号,保存 callback 的活自然落到这个 app EventThread 身上了
具体的过程大家看:EventThread 分析 这篇分析吧
总之每个 app 在 DisplayEventReceiver.nativeScheduleVsync 后会与app EventThread 建立一个 Connection,这个 Connection 是 socket 链接,但后再把这个 Connection 包装成 callbacj 注册到
DispSyncThread VSYNC 监听线程上
SF EventThread
SF EventThread 可以看成是 Surfaceflinger 的 Ui线程了,进行 layer 合成工作,具体的内容是很复杂的,这里简单介绍下流程
SF EventThread 线程内部一样是 handle 队列, 等待 invalidate 重绘消息,这个通知来自其进程,可以是 app 进程,也可以是 WMS 进程。app 进程渲染完自己的 surface 后会通知 Surfaceflinger 进程,这个通知就是一个 invalidate 消息。WMS 修改 window 的大小,位置后也会通知 Surfaceflinger 进程,同样这也是一个 invalidate 消息
SF EventThread 线程接到消息后会进行以下操作:
- handleTransaction - 根据 window 大小位置的变化,遍历 layer ,同步所有 layer 相关的变化,计算主 layer 的相关转台,比如屏幕方向(手机横过来,屏幕要同步横过来)
- handlePageFlip - 根据 layer Z 轴顺序,遍历 layer,计算 layer 的可见区域。这中间的计算是通过定义在 skia 中的一种与或非的图形逻辑运算实现的,类似我们数学中的与或非逻辑图。
- handleRepaint - 计算完 layer 的可见区域,把这些可见区域合成到主 layer 上
- postFrameBuffer - 刷新硬件帧缓存区
这里我就写这么多了,详细的大家应该去看看 C 层的代码
Surfaceflinger layer 合成方式
前面常常说图层合成,其实 layer 图层合成有2种方式,目前 android 设备用的都是同一种方式:Client 离屏合成,但是未来不排除出现另一种图层合成方式,这里算是增加大家认识
目前 SurfaceFlinger 支持两种合成方式,SurfaceFlinger 在收集可见层的所有缓冲区之后,便会询问 Hardware Composer 应如何进行合成:
- Client合成 - 也叫离屏合成,思路就是上面说的那样,layer 存储在设备内存中,然后合成成一个 layer 保存到 LCD 设备的帧缓存区。重点是 layer 的合成是交给设备本身处理的,而不是 LCD 设备
- Device合成 - 也叫硬件合成器合成,是指 layer 的合成交给 LCD 设备完成,比如渲染完每个 surface 之后,把 layer 集合交给 LCD 进程后续渲染,LCD 设备本身没有处理这些的能力,需要专用设备绑定在 LCD 电路中
这2种 layer 合成方式本质上是有没有专用的硬件设备,大家想啊,要合成 layer 需要有 GPU 类似的设备去调用 openGL API,这得给 LCD 增加多少成本啊,所以目前 android 设备中没有采用这种合成方式的,但是也不能排除没有,我觉得那些简单的户外广告机说不准就是采用这种合成方式
handler 同步屏障
在学习 Choreographer 编舞者的时候,我们看到了每次绘制 viewTree 都会先给 handler 添加同步屏障,以阻塞同步任务,保证能第一时间接收 SF 进程发送过来的 VSYNC 信号
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();//检查是否在主线程
mLayoutRequested = true;//mLayoutRequested 是否measure和layout布局。
scheduleTraversals();
}
}
void scheduleTraversals() {
if (!mTraversalScheduled) {//同一帧内不会多次调用遍历
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();//拦截同步Message
//Choreographer回调,执行绘制操作
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
}
}
那么大家有没有想过为什么?万事问个为什么其实是我们成长的标志,标致我们不在满足于表面的 API 了,而是去耐心思考期中的运行规律,原理,这是非常重要的,原理出处通用,差别是平台不同,实现不同
先看看 postSyncBarrier 方法:
private int postSyncBarrier(long when) {
// Enqueue a new sync barrier token.
// We don't need to wake the queue because the purpose of a barrier is to stall it.
synchronized (this) {
final int token = mNextBarrierToken++;
final Message msg = Message.obtain();
msg.markInUse();
msg.when = when;
msg.arg1 = token;
Message prev = null;
Message p = mMessages;
if (when != 0) {
while (p != null && p.when <= when) {
prev = p;
p = p.next;
}
}
if (prev != null) { // invariant: p == prev.next
msg.next = p;
prev.next = msg;
} else {
msg.next = p;
mMessages = msg;
}
return token;
}
}
postSyncBarrier 大家看懂了没,handler 模型中存储 message 消息的是 loop 里面 MessageQueue,MessageQueue 里面有个成员变量 mMessages,他是最终的数据容器,采用链表结构,以前我还看不懂,但是在这次研究同步屏障时终于搞懂了
大家知道了 postSyncBarrier 中出现的 mMessages 是个链表,也知道链表的结构是 next 一级级的指向下一个数据,那么这个方法其实就好懂了
- 在 mMessages != null 时,把新建的 tager == null 的 message 置于 mMessages 链表的最后
- 在 mMessages == null 时,把新建的 tager == null 的 message 的 next 下一个数据置为 mMessages 链表
其实就是把 tager == null 的消息放到链表最后,tager == null 的 message 其实是表示开始忽略同步消息,专门获取异步消息,这样做的目的是给 handler 加个优先级,放到队尾是为了不影响前面同步消息的执行
添加了 tager == null 的标记,那么下一步的重点就是获取消息的方法了 next(),我们正常 sendMessage 发送消息时,API 都会给 message 添加 tager 的,tager 就是 handle,一遍后面执行
Message next() {
//...
int pendingIdleHandlerCount = -1; // -1 only during first iteration
int nextPollTimeoutMillis = 0;
for (;;) {
//...
synchronized (this) {
// Try to retrieve the next message. Return if found.
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;
if (msg != null && msg.target == null) {//碰到同步屏障
// Stalled by a barrier. Find the next asynchronous message in the queue.
// do while循环遍历消息链表
// 跳出循环时,msg指向离表头最近的一个异步消息
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
if (msg != null) {
if (now < msg.when) {
//...
} else {
// Got a message.
mBlocked = false;
if (prevMsg != null) {
//将msg从消息链表中移除
prevMsg.next = msg.next;
} else {
mMessages = msg.next;
}
msg.next = null;
if (DEBUG) Log.v(TAG, "Returning message: " + msg);
msg.markInUse();
//返回异步消息
return msg;
}
} else {
// No more messages.
nextPollTimeoutMillis = -1;
}
//...
}
//...
}
}
next 方法中同步屏障的重点就在这里
if (msg != null && msg.target == null) {//碰到同步屏障
// Stalled by a barrier. Find the next asynchronous message in the queue.
// do while循环遍历消息链表
// 跳出循环时,msg指向离表头最近的一个异步消息
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
msg.target == null 标致启动同步屏障,do while 循环大家仔细看是把 链表队列中的 msg.isAsynchronous == true 的 message 消息拿出来,不是的就不拿,有就执行,没有就会阻塞当前线程,msg.isAsynchronous == true 的 message 在哪里发出来的,这个 Asynchronous 标志位就是表示该消息是异步的
可能有朋友会问,为什么我在启动同步屏障之后添加同步消息都不会执行啊,大家继续看上面的代码,链表永远不会消费碰到的第一个 tager == null 的消息,在首次 tager == null 时会开始 do while 去继续向后寻找 tager == null 的 message,有就拿出来消费,没有的话阻塞线程,并不会返回第一个 tager == null,有这个 tager == null 的 message 卡着,同步消息永远不会执行。取消同步屏障就是把这个永远占位的 tager == null 的 message 拿走
这个消息 msg.isAsynchronous == true 是 Choreographer 的 VSYNC 接收器在接收到信号时发射的,这下大家知道了吧,这个消息里会解除同步屏障
private final class FrameDisplayEventReceiver extends DisplayEventReceiver implements Runnable {
private boolean mHavePendingVsync;
private long mTimestampNanos;
private int mFrame;
@Override
public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
//忽略来自第二显示屏的Vsync
if (builtInDisplayId != SurfaceControl.BUILT_IN_DISPLAY_ID_MAIN) {
scheduleVsync();
return;
}
...
mTimestampNanos = timestampNanos;
mFrame = frame;
//该消息的callback为当前对象FrameDisplayEventReceiver
Message msg = Message.obtain(mHandler, this);
msg.setAsynchronous(true);
//此处mHandler为FrameHandler
mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
}
@Override
public void run() {
mHavePendingVsync = false;
doFrame(mTimestampNanos, mFrame);
}
}
OK,同步屏障就是个样子,所谓的同步与异步在这里其实名不副实,只是表示一个优先级罢了, handler 把他们放在一起,逻辑上非常复杂,非常不容易理解,大家多看看,这也是为什么大公司强调算法的原因,学习源码,这样的算法随处可见
其他一些概念
材质和纹理都只是颜色
,但是侧重不同
- Material 材质 - 指的是颜色,准确的说,指的是物体表面对射到表面上的色光的RGB分量的反射率。通常材质都包括环境光、漫射光、镜面光和自发光等成分,指的就是对不同的光线,不同颜色分量的反射程度
-
textures 纹理 - 纹理指的是位图,把一张图贴到一个表面上去,实际是摹拟了自然事物的漫射材质。因为材质一般只对顶点指定,你不可能对这个平面上的每个像素都指定一种材质。纹理其实就是起这个作用,相当于对这个平面上的每个像素都指定了不同的漫射材质,简单的我们按照
贴图
去理解就好了 - 栅格化 - 就是将向量图转化为机器可以识别的位图的一个过程,这个过程非常耗时,所以要提前进行,具体到 Android 身上来说,就是在显示当前帧的同时去绘制下一阵,16ms后再刷新屏幕。我们使用 canvas 绘制 view,canvas 画布本身有形状尺寸,每个 view 也是有形状有尺寸的,我们给 view/canvas 设置颜色、图片,这些都是纹理,都是贴图。而 GPU 使用 OpenGL把多边形和贴图关联起来,或者叫把纹理映射到图形上,我们给多边形的每个顶点指定对应纹理的哪个部分,这样每个顶点就会关联着一个纹理坐标,用来标明该从纹理图像的哪个部分采样,就形成了最终的带有颜色值的二位数组的位图,屏幕硬件按照这个颜色位图就能显示出最终的图像了