我们每天花很多时间盯着手机屏幕,不知道你有没有好奇过:
手机屏幕上的这些东西是怎么显示出来的?
这时候来了一位Android程序员(当然也可以是iOS或者是前端程序员)说: 这里显示的其实是一个View树,我们看到的都是大大小小的View。
。。。听起来很有道理,我们也经常指着屏幕说这个View怎么怎么样,可问题又来了:
屏幕认识View吗?
我们把一个View发给屏幕,它就显示出来了?
程序员老兄又来了: 屏幕当然不能识别View,它作为一个硬件,只能根据收到的数据改变每个像素单元的数据,这样整体来看,用户就发现屏幕上的内容变化了。至于View的内容是如何一步一步转化成屏幕可是识别的数据的,简单讲可以分成三步:
- 准备材料
- 画出来
- 显示到屏幕
。。。听起来很有道理,可问题又来了:
这也太简单了吧,能详细一点吗?
那可就说来话长了。。。
1. 准备材料
对于measure
layout
和draw
,Android工程师(大都)非常熟悉,我们常常在执行了onDraw()
方法后,一个让人自豪的自定义View就显示出来了。在实际的Android绘制流程中,第一步就是通过measure
layout
和draw
这些步骤准备了下面的材料:
- 画什么
- 画的参数
画什么
在Android的绘制中,我们使用Canvas API进行来告诉表示画的内容,如drawCircle()
drawColor()
drawText()
drawBitmap()
等,也是这些内容最终呈现在屏幕上。
画的参数
-
画的坐标
坐标系: Android图像坐标系以左上角为0点,x轴左负右正,y轴上负下正,z轴内负外正;
View
的layout
基准点是父容器的左上角,View的draw
内容基准点是View
的左上角。根节点父容器是当前
Window
的DecorView
,它的布局信息由WindowManger
来管理。到此,当前应用所有View放在哪个位置就确定了。
-
画的层级(重叠时的覆盖关系)
View之间并不是井水不犯河水,经常出现重叠的情况,重叠时该怎样覆盖和显示正确的View大体遵循以下规则:
- 指定
z-order
情况下,数值最大的显示在最上层,剩下的降序显示。 - 在没有指定
z-order
的情况下,子View覆盖父容器,相同父容器View后添加的显示在最上层。
- 指定
-
特定参数
不同的方法需要的参数不同,比如
drawCircle()
会有圆心和半径,drawText()
需要对应的text资源,drawBitmap()
需要对应的Bitmap资源等等。
在当前应用中,View树中所有元素的材料最终会封装到DisplayList
对象中(后期版本有用RenderNode
对DisplayList
又做了一层封装,实现了更好的性能),然后发送出去,这样第一阶段就完成了。
当然就有一个重要的问题:
这个阶段怎么处理Bitmap呢?
会将Bitmap复制到下一个阶段(准确地讲就是复制到GPU的内存中)。
现在大多数设备使用了GPU硬件加速,而GPU在渲染来自Bitmap的数据时只能读取GPU内存中的数据, 所以需要赋值Bitmap到GPU内存,这个阶段对应的名称叫Sync&upload
。另外,硬件加速并不支持所有Canvas API,如果自定义View使用了不支持硬件加速的Canvas API(参考Android硬件加速文档),为了避免出错就需要对View进行软件绘制,其处理方式就是生成一个Bitmap,然后复制到GPU进行处理。
这时可能会有问题:如果Bitmap很多或者单个Bitmap尺寸很大,这个过程可能会时间比较久,那有什么办法吗?
当然有(做作。。。)
-
使用
Hardware-Only Bitmap
(from Android 8.0 - Oreo)从Android 8.0 开始,支持了
Hardware-Only Bitmap
类型,这种类型的Bitmap的数据只存放在GPU内存中,这样在Sync&upload
阶段就不需要upload这个Bitmap了。使用很简单,只需要将Options.inPreferredConfig
赋值为Bitmap.Config.HARDWARE
即可。这种方式能实现特定场景的极致性能,提供便利的同时,这种Bitmap的某些操作是受限的(毕竟