深入探索Android布局优化(上)

前言

成为一名优秀的Android开发,需要一份完备的知识体系,在这里,让我们一起成长为自己所想的那样~。

Android的绘制优化其实可以分为两个部分,即布局(UI)优化和卡顿优化,而布局优化的核心问题就是要解决因布局渲染性能不佳而导致应用卡顿的问题,所以它可以认为是卡顿优化的一个子集。对于Android开发来说,写布局可以说是一个比较简单的工作,但是如果想将写的每一个布局的渲染性能提升到比较好的程度,要付出的努力是要远远超过写布局所付出的。由于布局优化这一主题包含的内容太多,因此,笔者将它分为了上、下两篇,本篇,即为深入探索Android布局优化的上篇。本篇包含的主要内容如下所示:

  • 1、绘制原理
  • 2、屏幕适配
  • 3、优化工具
  • 4、布局加载原理
  • 5、获取界面布局耗时

说到Android的布局绘制,那么我们就不得不先从布局的绘制原理开始说起。

一、绘制原理

Android的绘制实现主要是借助CPU与GPU结合刷新机制共同完成的。

1、CPU与GPU
  • CPU负责计算显示内容,包括Measure、Layout、Record、Execute等操作。在UI绘制上的缺陷在于容易显示重复的视图组件,这样不仅带来重复的计算操作,而且会占用额外的GPU资源。
  • GPU负责栅格化(用于将UI元素绘制到屏幕上,即将UI组件拆分到不同的像素上显示)。

这里举两个栗子来讲解一些CPU和GPU的作用:

  • 1、文字的显示首先经过CPU换算成纹理,然后再传给GPU进行渲染。
  • 2、而图片的显示首先是经过CPU的计算,然后加载到内存当中,最后再传给GPU进行渲染。

那么,软件绘制和硬件绘制有什么区别呢?我们先看看下图:

这里软件绘制使用的是Skia库(一款在低端设备如手机上呈现高质量的 2D 图形的 跨平台图形框架)进行绘制的,而硬件绘制本质上是使用的OpenGl ES接口去利用GPU进行绘制的。OpenGL是一种跨平台的图形API,它为2D/3D图形处理硬件指定了标准的软件接口。而OpenGL ES是用于嵌入式设备的,它是OpenGL规范的一种形式,也可称为其子集。

并且,由于OpenGl ES系统版本的限制,有很多 绘制API 都有相应的 Android API level 的限制,此外,在Android 7.0 把 OpenGL ES 升级到最新的 3.2 版本的时候,还添加了对Vulkan(一套适用于高性能 3D 图形的低开销、跨平台 API)的支持。Vulan作为下一代图形API以及OpenGL的继承者,它的优势在于大幅优化了CPU上图形驱动相关的性能。

2、Android 图形系统的整体架构

Android官方的架构图如下:

为了比较好的描述它们之间的作用,我们可以把应用程序图形渲染过程当作一次绘画过程,那么绘画过程中 Android 的各个图形组件的作用分别如下:

  • 画笔:Skia 或者 OpenGL。我们可以用 Skia去绘制 2D 图形,也可以用 OpenGL 去绘制 2D/3D 图形。
  • 画纸:Surface。所有的元素都在 Surface 这张画纸上进行绘制和渲染。在 Android 中,Window 是 View 的容器,每个窗口都会关联一个 Surface。而 WindowManager 则负责管理这些窗口,并且把它们的数据传递给 SurfaceFlinger。
  • 画板:Graphic Buffer。Graphic Buffer 缓冲用于应用程序图形的绘制,在 Android 4.1 之前使用的是双缓冲机制,而在 Android 4.1 之后使用的是三缓冲机制。
  • 显示:SurfaceFlinger。它将 WindowManager 提供的所有 Surface,通过硬件合成器 Hardware Composer 合成并输出到显示屏。

在了解完Android图形系统的整体架构之后,我们还需要了解下Android系统的显示原理,关于这块内容可以参考我之前写的Android性能优化之绘制优化的Android系统显示原理一节。

3、RenderThread

在Android系统的显示过程中,虽然我们利用了GPU的图形高性能计算的能力,但是从计算Display到通过GPU绘制到Frame Buffer都在UI线程中完成,此时如果能让GPU在不同的线程中进行绘制渲染图形,那么绘制将会更加地流畅。

于是,在Android 5.0之后,引入了RenderNode和RenderThread的概念,它们的作用如下:

  • RenderNode:进一步封装了Display和某些View的属性。
  • RenderThread:渲染线程,负责执行所有的OpenGl命令,其中的RenderNode保存有渲染帧的所有信息,能在主线程有耗时操作的前提下保证动画流畅。

CPU将数据同步给GPU之后,通常不会阻塞等待RenderThread去利用GPU去渲染完视图,而是通知结束之后就返回。加入ReaderThread之后的整个显示调用流程图如下图所示:

在Android 6.0之后,其在adb shell dumpsys gxinfo命令中添加了更加详细的信息,在优化工具一节中我将详细分析下它的使用。

在Android 7.0之后,对HWUI进行了重构,它是用于2D硬件绘图并负责硬件加速的主要模块,其使用了OpenGl ES来进行GPU硬件绘图。此外,Android 7.0还支持了Vulkan,并且,Vulkan 1.1在Android 被引入。

硬件加速存在哪些问题?

我们都知道,硬件加速的原理就是将CPU不擅长的图形计算转换成GPU专用指令。

  • 1、其中的OpenGl API调用和Graphic Buffer缓冲区至少会占用几MB以上的内存,内存消耗较大
  • 2、有些OpenGl的绘制API还没有支持,特别是比较低的Android系统版本,并且由于Android每一个版本都会对渲染模块进行一些重构,导致了在硬件加速绘制过程中会出现一些不可预知的Bug。如在Android 5.0~7.0机型上出现的libhwui.so崩溃问题,需要使用inline Hook、GOT Hook等native调试手段去进行分析定位,可能的原因是ReaderThread与UI线程的sync同步过程出现了差错,而这种情况一般都是有多个相同的视图绘制而导致的,比如View的复用、多个动画同时播放
4、刷新机制

16ms发出VSync信号触发UI渲染,大多数的Android设备屏幕刷新频率为60HZ,如果16ms内不能完成渲染过程,则会产生掉帧现象。

二、屏幕适配

我们都知道,Android手机屏幕的差异化导致了严重的碎片化问题,并且屏幕材质也是用户比较关注的一个重要因素。

首先,我们来了解下主流Android屏幕材质,目前主要有两类:

  • LCD(Liquid Crystal Display):液晶显示器。
  • OLED(Organic Light-Emitting Diode ):有机发光二极管。

早在20世纪60年代,随着半导体集成电路的发展,美国人成功研发出了第一块液晶显示屏LCD,而现在大部分最新的高端机使用的都是OLED材质,这是因为相比于LCD屏幕,OLED屏幕在色彩、可弯曲程度、厚度和耗电等方面都有一定的优势。正因为如此,现在主流的全面屏、曲面屏与未来的柔性折叠屏,使用的几乎都是 OLED 材质。当前,好的材质,它的成本也必然会比较昂贵。

1、OLED 屏幕和 LCD 屏幕的区别

如果要明白OLED 屏幕和LCD屏幕的区别,需要了解它们的运行原理,下面,我将分别进行讲解。

屏幕的成像原理

屏幕由无数个点组成,并且,每个点由红绿蓝三个子像素组成,每个像素点通过调节红绿蓝子像素的颜色配比来显示不同的颜色,最终所有的像素点就会形成具体的画面。

LCD背光源与OLED自发光

下面,我们来看下LCD和OLED的总体结构图,如下所示:

LCD的发光原理主要在于背光层Back-light,它通常都会由大量的LED背光灯组成以用于显示白光,之后,为了显示出彩色,在其上面加了一层有颜色的薄膜,白色的背光穿透了有颜色的薄膜后就可以显示出彩色了。但是,为了实现调整红绿蓝光的比例,需要在背光层和颜色薄膜之间加入一个控制阀门,即液晶层liquid crystal,它可以通过改变电压的大小来控制开合的程度,开合大则光多,开合小则光少

对于OLED来说,它不需要LCD屏幕的背光层和用于控制出光量的液晶层,它就像一个有着无数个小的彩色灯泡组成的屏幕,只需要给它通电就能发光。

LCD的致命缺陷

它的液晶层不能完全关合,如果LCD显示黑色,会有部分光穿过颜色层,所以LCD的黑色实际上是白色和黑色混合而成的灰色。而OLED不一样,OLED显示黑色的时候可以直接关闭区域的像素点。

此外,由于背光层的存在,所以LCD显示器的背光非常容易从屏幕与边框之间的缝隙泄漏出去,即会产生显示器漏光现象。

OLED屏幕的优势
  • 1、由于没有有背光层和液晶层的存在,所以它的厚度更薄,其弯曲程度可以达到180%
  • 2、对比度(白色比黑色的比值)更高,使其画面颜色越浓;相较于LCD来说,OLED是油画,色彩纯而细腻,而LCD是水彩笔画,色彩朦胧且淡
  • 3、OLED每个像素点都是独立的,所以OLED可以单独点亮某些像素点,即能实现单独点亮。而LCD只能控制整个背光层的开关。并且,由于OLED单独点亮的功能,使其耗电程度大大降低
  • 4、OLED的屏幕响应时间很快,不会造成画面残留以致造成视觉上的拖影现象。而LCD则会有严重的拖影现象。
OLED屏幕的劣势
  • 1、由于OLED是有机材料,导致其寿命是不如LCD的

有机材料的。并且,由于OLED单独点亮的功能,会使每个像素点工作的时间不一样,这样,在屏幕老化时就会导致色彩显示不均匀,即产生烧屏现象。

  • 2、由于OLED就不能采取控制电压的方式去调整亮度,所以目前只能通过不断的开关开关开关去进行调光。
  • 3、OLED的屏幕像素点排列方式不如LCD的紧凑,所以在分辨率相同的情况下,OLED的屏幕是不如LCD清楚的。即OLED的像素密度较低
2、屏幕适配方案

我们都知道,Android 的 系统碎片化、机型以及屏幕尺寸碎片化、屏幕分辨率碎片化非常地严重。所以,一个好的屏幕适配方案是很重要的。接下来,我将介绍目前主流的屏幕适配方案。

1、最原始的Android适配方案:dp + 自适应布局或weight比例布局

首先,我们来回顾一下px、dp、dpi、ppi、density等概念:

  • px:像素点,px = density * dp。
  • ppi:像素密度,每英寸所包含的像素数目,屏幕物理参数,不可调整,dpi没有人为调整时 = ppi。
  • dpi:像素密度,在系统软件上指定的单位尺寸的像素数量,可人为调整,dpi没有人为调整时 = ppi。
  • dp:density-independent pixels,即密度无关像素,基于屏幕物理分辨率的一个抽象的单位,以dp为尺寸单位的控件,在不同分辨率和尺寸的手机上代表了不同的真实像素,比如在分辨率较低的手机中,可能1dp = 1px,而在分辨率较高的手机中,可能1dp=2px,这样的话,一个6464dp的控件,在不同的手机中就能表现出差不多的大小了,px = dp (dpi / 160)。
  • denstiy:密度,屏幕上每平方英寸所包含的像素点个数,density = dpi / 160。

通常情况下,我们只需要使用dp + 自适应布局(如鸿神的AutoLayout、ConstraintLayout等等)或weight比例布局即可基本解决碎片化问题,当然,这种方式也存在一些问题,比如dpi和ppi的差异所导致在同一分辨率手机上控件大小的不同

2、宽高限定符适配方案

它就是穷举市面上所有的Android手机的宽高像素值,通过设立一个基准的分辨率,其他分辨率都根据这个基准分辨率来计算,在不同的尺寸文件夹内部,根据该尺寸编写对应的dimens文件,如下图所示:

比如以480×320为基准分辨率:

  • 宽度为320,将任何分辨率的宽度整分为320份,取值为x1-x320。
  • 高度为480,将任何分辨率的高度整分为480份,取值为y1-y480。

那么对于800*480的分辨率的dimens文件来说:

  • x1=(480/320)*1=1.5px
  • x2=(480/320)*2=3px

此时,如果UI设计界面使用的就是基准分辨率,那么我们就可以按照设计稿上的尺寸填写相对应的dimens去引用,而当APP运行在不同分辨率的手机中时,系统会根据这些dimens去引用该分辨率对应的文件夹下面去寻找对应的值。但是这个方案由一个缺点,就是无法做到向下兼容去使用更小的dimens,比如说800×480的手机就一定要找到800×480的限定符,否则就只能用统一默认的dimens文件了。

3、UI适配框架AndroidAutoLayout的适配方案

因宽高限定符方案的启发,鸿神出品了一款能使用UI适配更加开发高效和适配精准的项目。

项目地址

基本使用步骤如下:

第一步:在你的项目的AndroidManifest中注明你的设计稿的尺寸:

<meta-data android:name="design_width" android:value="768">
</meta-data>
<meta-data android:name="design_height" android:value="1280">
</meta-data>


第二步:让你的Activity继承自AutoLayoutActivity。如果你不希望继承AutoLayoutActivity,可以在编写布局文件时,直接使用AutoLinearLayout、Auto*等适配布局即可。

接下来,直接在布局文件里面使用具体的像素值就可以了,因为在APP运行时,AndroidAutoLayout会帮助我们根据不同手机的具体尺寸按比例伸缩。

AndroidAutoLayout在宽高限定符适配的基础上,解决了其dimens不能向下兼容的问题,但是它在运行时会在onMeasure里面对dimens去做变换,所以对于自定义控件或者某些特定的控件需要进行单独适配;并且,整个UI的适配过程都是由框架完成的,以后想替换成别的UI适配方案成本会比较高,而且,不幸的是,项目已经停止维护了。

4、smallestWidth适配方案(sw限定符适配)

smallestWidth即最小宽度,系统会根据当前设备屏幕的 最小宽度 来匹配 values-sw<N>dp。

我们都知道,移动设备都是允许屏幕可以旋转的,当屏幕旋转时,屏幕的高宽就会互换,加上 最小 这两个字,是因为这个方案是不区分屏幕方向的,它只会把屏幕的高度和宽度中值最小的一方认为是 最小宽度。

并且它跟宽高限定符适配原理上是一样,都是系统通过特定的规则来选择对应的文件。它与AndroidAutoLayout一样,同样解决了其dimens不能向下兼容的问题,如果该屏幕的最小宽度是360dp,但是项目中没有values-sw360dp文件夹的话,它就可能找到values-sw320dp这个文件夹,其尺寸规则命名如下图所示:

假如加入我们的设计稿的像素宽度是375,那么其对应的values-sw360dp和values-sw400dp宽度如下所示:

smallestWidth的适配机制由系统保证,我们只需要针对这套规则生成对应的资源文件即可,即使对应的smallestWidth值没有找到完全对应的资源文件,它也能向下兼容,寻找最接近的资源文件。虽然多个dimens文件可能导致apk变大,但是其增加大小范围也只是在300kb-800kb这个区间,这还是可以接受的。这套方案唯一的变数就是选择需要适配哪些最小宽度限定符的文件,如果您生成的 values-sw<N>dp 与设备实际的 最小宽度 差别不大,那误差也就在能接受的范围内,如果差别很大,那效果就会很差。最后,总结一下这套方案的优缺点:

优点:

  • 1、稳定且无性能损耗。
  • 2、可通过选择需要哪些最小宽度限定符文件去控制适配范围。
  • 3、在自动生成values-sw<N>的插件基础下,学习成本较低。

插件地址为自动生成values-sw<N>的项目代码。生成需要的values-sw<N>dp文件夹的步骤如下:

  • 1、clone该项目到本地,以Android项目打开。
  • 2、DimenTypes文件中写入你希望适配的sw尺寸,默认的这些尺寸能够覆盖几乎所有手机适配需求。
  • 3、DimenGenerator文件中填写设计稿的尺寸(DESIGN_WIDTH是设计稿宽度,DESIGN_HEIGHT是设计稿高度)。
  • 4、执行lib module中的DimenGenerator.main()方法,当前地址下会生成相应的适配文件,把相应的文件连带文件夹拷贝到正在开发的项目中。

缺点:

  • 1、侵入性高,后续切换其他屏幕适配方案需修改大量 dimens 引用。
  • 2、覆盖更多不同屏幕的机型需要生成更多的资源文件,使APK体积变大。
  • 3、不能自动支持横竖屏切换时的适配,如要支持需使用 values-w<N>dp 或 屏幕方向限定符 再生成一套资源文件,又使APK体积变大。

如果想让屏幕宽度随着屏幕的旋转而做出改变该怎么办呢?

此时根据 values-w<N>dp (去掉 sw 中的 s) 去生成一套资源文件即可。

如果想区分屏幕的方向来做适配该怎么办呢?

去根据 屏幕方向限定符 生成一套资源文件,后缀加上 -land 或 -port 即可,如:values-sw360dp-land (最小宽度 360 dp 横向),values-sw400dp-port (最小宽度 720 dp 纵向)。

注意:

如果UI设计上明显更适合使用wrap_content,match_parent,layout_weight等,我们就要毫不犹豫的使用,毕竟,上述都是仅仅针对不得不使用固定宽高的情况,我相信基础的UI适配知识大部分开发者还是具备的。如果不具备的话,请看下方:

<div align=”center”>
<img src=”https://user-gold-cdn.xitu.io/2020/1/14/16fa19bf2363f9e1?w=240&h=240&f=gif&s=29989″ width=30%>
</div>

5、今日头条适配方案

它的原理是根据屏幕的宽度或高度动态调整每个设备的 density (每 dp 占当前设备屏幕多少像素),通过修改density值的方式,强行把所有不同尺寸分辨率的手机的宽度dp值改成一个统一的值,这样就可以解决所有的适配问题。其对应的重要公式如下:

当前设备屏幕总宽度(单位为像素)/  设计图总宽度(单位为 dp) = density

今日头条适配方案默认项目中只能以高或宽中的一个作为基准来进行适配,并不像 AndroidAutoLayout 一样,高以高为基准,宽以宽为基准,来同时进行适配,为什么?

因为&

  • 20
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

闻啼鸟小馆

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值