㈠ android view的onDraw从第二次触发开始,界面变样了。。。
这个view的大小被限制了,导致draw出来会缺了边,将view设大一些,或者边变小一点
㈡ “Android渲染”图像是怎样显示到屏幕上的
我们每天花很多时间盯着手机屏幕,不知道你有没有好奇过:
这时候来了一位Android程序员(当然也可以是iOS或者是前端程序员)说: 这里显示的其实是一个View树,我们看到的都是大大小小的View。
。。。听起来很有道理,我们也经常指着屏幕说这个View怎么怎么样,可问题又来了:
程序员老兄又来了: 屏幕当然不能识别View,它作为一个硬件,只能根据收到的数据改变每个像素单元的数据,这样整体来看,用户就发现屏幕上的内容变化了。至于View的内容是如何一步一步转化成屏幕可是识别的数据的,简单讲可以分成三步:
。。。听起来很有道理,可问题又来了:
那可就说来话长了。。。
对于 measure layout 和 draw ,Android工程师(大都)非常熟悉,我们常常在执行了 onDraw() 方法后,一个让人自豪的自定义View就显示出来了。在实际的Android绘制流程中,第一步就是通过 measure layout 和 draw 这些步骤准备了下面的材料:
在Android的绘制中,我们使用Canvas API进行来告诉表示画的内容,如 drawCircle() drawColor() drawText() drawBitmap() 等,也是这些内容最终呈现在屏幕上。
在当前应用中,View树中所有元素的材料最终会封装到 DisplayList 对象中(后期版本有用 RenderNode 对 DisplayList 又做了一层封装,实现了更好的性能),然后发送出去,这样第一阶段就完成了。
当然就有一个重要的问题:
会将Bitmap复制到下一个阶段(准确地讲就是复制到GPU的内存中)。
现在大多数设备使用了GPU硬件加速,而GPU在渲染来自Bitmap的数据时只能读取GPU内存中的数据, 所以需要赋值Bitmap到GPU内存,这个阶段对应的名称叫 Sync&upload 。另外,硬件加速并不支持所有Canvas API,如果自定义View使用了不支持硬件加速的Canvas API(参考 Android硬件加速文档 ),为了避免出错就需要对View进行软件绘制,其处理方式就是生成一个Bitmap,然后复制到GPU进行处理。
这时可能会有问题:如果Bitmap很多或者单个Bitmap尺寸很大,这个过程可能会时间比较久,那有什么办法吗?
当然有(做作。。。)
关于Bitmap这里再多说一句:
Bitmap的内存管理一直是Android程序员很关心的问题,毕竟它是个很占内存的大胖子,在Android3.0~Android7.0,Bitmap内存放在java堆中,而android系统中每个进程的Java堆是有严格限制的,处理不好这些Bitmap内存,容易导致频繁GC,甚至触发Java堆的 OutOfMemoryError 。从Android8.0开始,bitmap的像素数据放入了native内存,于是Java Heap的内存问题暂时缓解了。
Tip:
现在材料已经备好,我们要真正地画东西了。
接下来就要把东西画出来了,画出来的过程就是把前面的材料转化成一个堆像素数据的过程,也叫 栅格化 ,那这个活儿谁来干呢?
候选人只有两个:
大部分情况下,都是GPU来干这个活儿,因为GPU真的特别快!!!
所谓的“画”,对于计算机来讲就是处理图像,其实就是根据需要(就是DisplayList中的命令)对数据做一些特定类型的数学运算,最后输出结果的过程。我们看到的每一帧精美界面,(几乎)都是GPU吭哧吭哧"算"出来的,这个就有疑问了:
我们简单地聊聊CPU与GPU的区别:
CPU的核心数通常是几个,单个核心的主频高,功能强大,擅长串行处理复杂的流程;
GPU ( Graphics Processing Unit ) 有成百上千个核心,单个核心主频低,功能有限,擅长(利用超多核心)大量并行简单运算;正如它的名字一样,GPU就是为图像绘制这个场景量身定做的硬件(所以使用GPU也叫硬件加速),后来也被用到挖矿和神经网络中。
图片肯定没有视频直观,我们从感性的角度感受一下GPU到底有多快,我想下面的视频看过就不会忘掉,你会被GPU折服:
Mythbusters Demo GPU versus CPU
看这个视频,我们对于“加速”应该有了更深刻的印象,这里不再进一步分析CPU和GPU更微观的差别(因为不懂),我想已经讲明白为什们GPU更快了。
另外,在GPU开始绘制之前,系统也做了一些优化(对DisplayList中的命令进行预处理),让整个绘制流程更加高效:
第二步的具体过程还是很复杂的,比如涉及到Alpha绘制,相关的优化会失效,详情查看文章 为什么alpha渲染性能低 .
至于画在哪里,我们现在理解为一个缓冲(Buffer)中就可以了,具体的机制放在第三步讲。
到此,我们已经画(绘制)完了图像内容,把这个内容发送出去,第二步的任务就完成了。
Tip:
我们知道,除了我们的应用界面,手机屏幕上同时显示着其他内容,比如SystemUI(状态栏、导航栏)或者另外的悬浮窗等,这些内容都需要显示到屏幕上。所以要先 把这些界面的内容合成,然后再显示到屏幕 。
在讲合成图像之前,我们有必要知道这些界面图像(Buffer)是怎么传递的:
Android图形架构中,使用生产者消费者模型来处理图像数据,其中的图像缓冲队列叫 BufferQueue , 队列中的元素叫 Graphic Buffer ,队列有生产者也有消费者;每个应用通常会对应一个 Surface ,一个 Surface 对应着一个缓冲队列,每个队列中 Graphic Buffer 的数量不超过3个, 上面两步后绘制的图像数据最终会放入一个 Graphic Buffer ,应用自身就是队列的生产者( BufferQueue 在Android图形处理中有广泛的应用,当前只讨论界面绘制的场景)。
每个 Graphic Buffer 本身体积很大,在从生产者到消费者的传递过程中不会进行复制的操作,都是用匿名共享内存的方式,通过句柄来跨进程传递。
我们可以通过以下命令来查看手机当前用到的 Graphic Buffer 情况:
关于上面的命令,你可能会好奇这个 SurfaceFlinger 是什么东西啊?
上文提到过每个应用(一般)对应一个 Surface ,从字面意思看, SurfaceFlinger 就是把应用的 Surface 投射到目的地。
实际上, SurfaceFlinger 就是界面(Buffer)合成的负责人,在应用界面绘制的场景, SurfaceFlinger 充当了 BufferQueue 的消费者。绘制好的 Graphic Buffer 会进入(queue)队列, SurfaceFlinger 会在合适的时机(这个时机下文讨论),从队列中取出(acquire)Buffer数据进行处理。
我们知道,除了我们的应用界面,手机屏幕上同时显示着其他内容,比如SystemUI(状态栏、导航栏)或者另外的悬浮窗等,这些部分的都有各自的Surface,当然也会往对应的 BufferQueue 中生产 Graphic Buffer 。
如下图所示, SurfaceFlinger 获取到所有Surface的最新Buffer之后,会配合HWComposer进行处理合成,最终把这些Buffer的数据合成到一个 FrameBuffer 中,而FrameBuffer的数据会在另一个合适的时机(同样下文讨论)迅速地显示到屏幕上,这时用户才观察到屏幕上的变化。
关于上图中的 HWComposer ,它是Android HAL接口中的一部分,它定义了上层需要的能力,让由硬件提供商来实现,因为不同的屏幕硬件差别很大,让硬件提供商驱动自己的屏幕,上层软件无需关心屏幕硬件的兼容问题。
事实上,如果你观察足够仔细的话,可能对上图还有疑问:
同学你观察很仔细(...),事实上,这是 SurfaceFlinger 合成过程中重要的细节,对于不同 Surface 的Buffer, 合成的方法有两种:
显然第一种方法是最高效的,但为了保证正确性,Android系统结合了两种方法。具体实现上, SurfaceFlinger 会询问( prepare ) HWComposer 是否支持直接合成,之后按照结果做对应处理。
有的朋友憋不住了:
Good question! (太做作了。。。)
为了保证最好的渲染性能,上面各个步骤之间并不是串行阻塞运行的关系,所以有一个机制来调度每一步的触发时机,不过在此之前,我们先讲介绍一个更基础的概念:
屏幕刷新率
刷新率是屏幕的硬件指标,单位是Hz(赫兹),意思是屏幕每秒可以刷新的次数。
回到问题,既然屏幕这个硬件每隔一段时间(如60Hz屏幕是16ms)就刷新一次,最佳的方案就是屏幕刷新时开始新一轮的绘制流程,让一次绘制的流程尽可能占满整个刷新周期,这样掉帧的可能性最小。基于这样的思考,在Android4.1(JellyBean)引入 VSYNC(Vertical Synchronization - 垂直同步信号)
收到系统发出的VSYNC信号后, 有三件事会同时执行(并行) :
下图描述了没有掉帧时的VSYNC执行流程,现在我们可以直接回答问题了: 合适的时机就是VSYNC信号 。
从上图可以看出,在一次VSYNC信号发出后,屏幕立即显示2个VSYNC周期(60Hz屏幕上就是32ms)之前开始绘制的图像,这当然是延迟,不过这个延迟非常稳定, 只要前面的绘制不掉链子 ,界面也是如丝般顺滑。当然,Android还是推出一种机制让延迟可以缩小到1个VSYNC周期,详情可参考 VSYNC-offset 。
实际上,系统只会在需要的时候才发出VSYNC信号,这个开关由SurfaceFlinger来管理。应用也只是在需要的时候才接收VSYNC信号,什么时候需要呢?也就是应用界面有变化,需要更新了,具体的流程可以参考 View.requestLayout() 或 View.invalidate() 到 Choreographer (编舞者)的调用过程。这个过程会注册一次VSYNC信号,下一次VSYNC信号发出后应用就能收到了,然后开始新的绘制工作;想要再次接收VSYNC信号就需要重新注册,可见,应用界面没有改变的时候是不会进行刷新的。
我们可以看到,无论是VSYNC开关,还是应用对VSYNC信号的单次注册逻辑,都是秉承着按需分配的原则,这样的设计能够带来Android操作系统更好的性能和更低的功耗。
Tip:
终于。。。说完了
我们简单回顾一下,
更形象一点就是:
之所以有这一节,是因为随着Android版本的更替,渲染方案也发生了很多变化。为了简化表达,我们前文都以当前最新的方案来讲解,事实上,部分流程的实现方式在不同版本可能会有较大的变化,甚至在之前版本没有实现方案,这里我尽可能详细地列出Android版本更迭过程中与渲染相关的更新(包括监控工具)。
如果你居然能读到这里,那我猜你对下面的参考文章也会感兴趣:
https://source.android.com/devices/graphics
https://hencoder.com/tag/hui-/
https://www.youtube.com/watch?v=wIy8g8yNhNk&feature=emb_logo
https://www.youtube.com/watch?v=v9S5EO7CLjo
https://www.youtube.com/watch?v=zdQRIYOST64&t=177s
https://www.youtube.com/watch?v=we6poP0kw6E&index=64&list=
https://developer.android.com/topic/performance/rendering
https://developer.android.com/guide/topics/graphics/hardware-accel
https://developer.android.com/topic/performance/rendering/profile-gpu#su
https://mp.weixin.qq.com/s/0OOSmrzSkjG3cSOFxWYWuQ
Android Developer Backstage - Android Rendering
Android Developer Backstage - Graphics Performance
https://elinux.org/images/2/2b/Android_graphics_path--chis_simmonds.pdf
㈢ Android 自定义View之Draw过程(上)
Draw 过程系列文章
Android 展示之三部曲:
前边我们已经分析了:
这俩最主要的任务是: 确定View/ViewGroup可绘制的矩形区域。
接下来将会分析,如何在这给定的区域内绘制想要的图形。
通过本篇文章,你将了解到:
Android 提供了关于View最基础的两个类:
然而ViewGroup 并没有约定其内部的子View是如何布局的,是叠加在一起呢?还是横向摆放、纵向摆放等。同样的View 也没有约定其展示的内容是啥样,是矩形、圆形、三角形、一张图片、一段文字抑或是不规则的形状?这些都要我们自己去实现吗?
不尽然,值得高兴的是Android已经考虑到上述需求了,为了开发方便已经预制了一些常用的ViewGroup、View。
如:
继承自ViewGroup的子类
继承自View的子类
虽然以上衍生的View/ViewGroup子类已经大大为我们提供了便利,但也仅仅是通用场景下的通用控件,我们想实现一些较为复杂的效果,比如波浪形状进度条、会发光的球体等,这些系统控件就无能为力了,也没必要去预制千奇百怪的控件。想要达到此效果,我们需要自定义View/ViewGroup。
通常来说自定义View/ViewGroup有以下几种:
3 一般不怎么用,除非布局比较特殊。1、2、4 是我们常用的手段,对于我们常说的"自定义View" 一般指的是 4。
接下来我们来看看 4是怎么实现的。
在xml里引用MyView
效果如下:
黑色部分为其父布局背景。
红色矩形+黄色圆形即是MyView绘制的内容。
以上是最简单的自定义View的实现,我们提取重点归纳如下:
由上述Demo可知,我们只需要在重写的onDraw(xx)方法里绘制想要的图形即可。
来看看View 默认的onDraw(xx)方法:
发现是个空实现,因此继承自View的类必须重写onDraw(xx)方法才能实现绘制。该方法传入参数为:Canvas类型。
Canvas翻译过来一般叫做画布,在重写的onDraw(xx)里拿到Canvas对象后,有了画布我们还需要一支笔,这只笔即为Paint,翻译过来一般称作画笔。两者结合,就可以愉快的作画(绘制)了。
你可能发现了,在Demo里调用
并没有传入Paint啊,是不是Paint不是必须的?实际上调用该方法后,底层会自动生成Paint对象。
可以看到,底层初始化了Paint,并且给其设置的颜色为在Java层设置的颜色。
onDraw(xx)比较简单,开局一个Canvas,效果全靠画。
试想,这个Canvas怎么来的呢,换句话说是谁调用了onDraw(xx)。发挥一下联想功能,在Measure、Layout 过程有提到过两者套路很像:
那么Draw过程是否也是如此套路呢?看见了onDraw(xx),那么draw(xx)还远吗?
没错,还真有draw(xx)方法:
可以看出,draw(xx)主要分为两个部分:
不管是A分支还是B分支,都进行了好几步的绘制。
通常来说,单一一个View的层次分为:
后面绘制的可能会遮挡前边绘制的。
对于一个ViewGroup来说,层次分为:
来看看A分支标注的4个点:
(1)
onDraw(canvas)
前面分析过,对于单一的View,onDraw(xx)是空实现,需要由我们自定义绘制。
而对于ViewGroup,也并没有具体实现,如果在自定义ViewGroup里重写onDraw(xx),它会执行吗?默认是不会执行的,相关分析请移步:
Android ViewGroup onDraw为什么没调用
(2)
dispatchDraw(canvas),来看看在View.java里的实现:
发现是个空实现,再看看ViewGroup.java里的实现:
也即是说,对于单一View,因为没有子布局,因此没必要再分发Draw,而对于ViewGroup来说,需要触发其子布局发起Draw过程(此过程后续分析),可以类比事件分发过程View、ViewGroup的处理。感兴趣的请移步:
Android 输入事件一撸到底之View接盘侠(3)
(3)
OverLay,顾名思义就是"盖在某个东西上面",此处是在绘制内容之后,绘制前景之前。怎么用呢?
以上是给一个ViewGroup设置overLay,效果如下:
你可能发现了,这和设置overLay差不多的嘛,实际还是有差别的。在onDrawForeground(xx)里会重新调整Drawable的尺寸,该尺寸与View大小一致,之前给Drawable设置的尺寸会失效。运行效果如下:
可以看出,ViewGroup都被前景盖住了。
再来看看B分支的重点:边缘渐变效果
先来看看TextView 边缘渐变效果:
加上这俩参数。
实际上系统自带的一些控件也使用了该效果,如NumberPicker、YearPickerView
以上是NumberPicker 的效果,可以看出是垂直方向渐变的。
对于View.java 里的onDraw(xx)、draw(xx),ViewGroup.java里并没有重写。
而对于dispatchDraw(xx),在View.java里是空实现。在ViewGroup.java里发起对子布局的绘制。
来看看标记的2点:
(1)
设置padding的目的是为了让子布局留出一定的空隙出来,因此当设置了padding后,子布局的canvas需要根据padding进行裁减。判断标记为:
FLAG_CLIP_TO_PADDING 默认设置为true
FLAG_PADDING_NOT_NULL 只要有padding不为0,该标记就会打上。
也就是说:只要设置了padding 不为0,子布局显示区域需要裁减。
能不能不让子布局裁减显示区域呢?
答案是可以的。
考虑到一种场景:使用RecyclerView的时候,我们需要设置paddingTop = 20px,效果是:RecyclerView Item展示时离顶部有20px,但是滚动的时候永远滚不到顶部,看起来不是那么友好。这就是上述的裁减起作用了,需要将此动作禁止。通过设置:
当然也可以在xml里设置:
(2)
drawChild(xx)
从方法名上看是调用子布局进行绘制。
child.draw(x1,x2,x3)里分两种情况:
这两者具体作用与区别会在下篇文章分析,不管是硬件加速绘制还是软件加速绘制,最终都会调用View.draw(xx)方法,该方法上面已经分析过。
注意,draw(x1,x2,x3)与draw(xx)并不一样,不要搞混了。
用图表示:
View/ViewGroup Draw过程的联系:
一般来说,我们通常会自定义View,并且重写其onDraw(xx)方法,有没有绘制内容的ViewGroup需求呢?
是有的,举个例子,大家可以去看看RecyclerView ItemDecoration 的绘制,其中运用到了ViewGroup draw(xx)、ViewGroup onDraw(xx) 、View onDraw(xx)绘制的先后顺序来实现分割线,分组头部悬停等功能的。
本篇文章基于 Android 10.0