对于性能优化系列,一直想写但一直纠结于现在从事的电商行业应用开发,自己实践的性能优化技术方案其实并不多,本着先用后写的技术人原则,在没有深入考究的情况下,翻阅资料去写一篇没有实操过的文章,无外乎就是资料的各种知识点堆砌。因此,在花费一段时间的阅读书籍并将优化技术应用起来之后,总结系列优化性能方案文章。
前言
这一系列文章的输出,需要感谢从一开始就购入的张绍文大佬的极客课程《Anddroid开发高手课》,这篇文章堪称移动端Android性能优化宝典,里面涉及到的知识无论是深度还是广度,都绝对够格。而后购买的一些书籍主要也是Tencent内部大佬早年编写的有关性能优化的书籍,这其中包括《移动APP性能评测与优化》【16年9月】、《Android应用性能优化最佳实践》【17年2月出版】、《Android移动性能实战》【17年4月出版】。不得不赞叹,腾讯在移动端质量把关上的技术输出是多么的强大,清一色的腾讯系。
他们在近几年的移动互联网浪潮中,早已习成了一套体系,从当初的PC/Web的测试领域征战到围绕iOS/Android开发疆域,在这片未知的疆土上,他们汇总出一套从流畅度/卡顿、耗电/CPU、强弱网络,到内存泄露(OOM)、稳定性(ANR/Crash)、再到数据库(SQlite)、IO读写,和适配兼容等多个维度上的优化技术方案。涉及到的技术知识面之广,很多人都会觉得这不应该是测试工程师该做该去了解的东西吗?不可否认这确实是,但是作为一名高工,这些技术点也是必然需要了解和掌握的。这里不得不提我们Android之神Jake Wharton,大佬的开源框架一般都是对外直接提供sdk使用,开发者在引入使用甚至都很少出现性能问题或者毁灭性bug,诚然大神的功底很深厚,但千里之堤毁于蚁穴,这些开源神作真的是一气呵成的么?前段时间clone大神唯一一项对外提供的Android开发demo——u2020,这个demo很简单,Jake将他的很多开源库应用在里面,但发现最多的引入库就是各种测试库,有兴趣的朋友可以阅读【致敬那个男人系列】之全面剖析U2020项目,其中用到检测OOM的leakCanary、静态代码分析框架error-prone、分布式仪表 (Instrumentation)测试框架spoon、真相单元测试框架truth 等等。这更加让我坚信,移动互联网的后半场,我们不管商业形态如何改变,单单就对技术开发领域,越来越多的开发公司会注重应用的整体质量。
因此,这个系列文也会跟着大厂的专项战略地图(内存、电量、网络、流畅度、稳定性、包体积)从一而终的完成,希望可以在读者朋友的心中建立起一套性能专项的知识体系。
目标锁定公司开发项目,让我们试试一套规范流程检测下来我们应用的问题都在哪?我们依照发现问题(经典场景复现)—— 提出问题(追根溯源解析底层原理)—— 解决问题(依靠各类常用工具定位问题解决)
而对于流畅度这一块的评测,无非就是两点:APP启动秒开、界面渲染一步到位(UI不卡顿),那么开始吧!
毫无头绪的开始——让工具告诉我们应用的病灶在哪里
分析问题和确认问题是否解决,最直接的感官感受无非就是通过图形化来呈现,因此借助相应的调试工具,对性能优化都起到很大程度的作用。接下来介绍几款开发调试利器和常用工具,可以说大部分的性能分析都离不开这几个工具。
Hierarchy View —— 常用布局优化工具,查看页面的布局层次
- 布局优化(Hierarchy View)
- 布局层级检查(Lint)
布局优化
布局的好坏影响到绘制的时间,通过减少Layout层级、减少测量、绘制时间,提高复用性三个方面来优化布局,优化的目的就是减少Layout层级,让布局扁平化,以提高绘制的时间,提高布局的复用性。从而节省开发成本和维护成本。
减少层级
层级越少,测试和绘制时间就越短,通常减少层级有以下两个常用方案
- 合理使用RelativeLayout和LinearLayout,目前更多的是合理的使用约束布局,以达到界面扁平化
- 合理使用Merge
提高显示速度
在View的可见性上,如果功能不是动态的控制显隐藏,优先考虑使用ViewStub,ViewStub是一个轻量级的View,它是一个看不见的,并且不占布局位置,占用资源非常小的视图对象。
布局复用
相同的布局在很多页面都在用的情况下,优先考虑include标签来提取公共布局。
避免过度绘制
我们一般在XML文件中和自定义控件中绘制,因此可以看出导致过度绘制的主要原因,如下:
- XML布局 —> 控件有重叠且都有设置背景
- View自绘 -> View.OnDraw里面同一个区域被绘制多次
过度绘制检测工具 - Android系统自带
要知道是否有过度绘制的情况,可以打开手机设置开发者选项,进行查看。
如何避免过度绘制
- 布局上优化
- 移除XML中非必须的背景
- 移除window默认的背景
- 按需显示占位背景图
- 自定义View优化
- 避免绘制越界
- 快速判断Canvas是否需要绘制
启动优化
应用启动流程
启动分为两种类型,冷热启动,介绍如下
- 冷启动
因为系统会重新创建一个新的进程分配给应用,所以会先创建和初始化Application类,再创建和初始化首页(包括一系列的测量、布局、绘制),最后显示在界面上。
- 热启动
因为会从已有的进程中启动,所以热启动不会再创建和初始化Application,而是直接创建和初始化首页(包括一系列测量、绘制、布局),即Applicatio只会初始化一次,质保函 Activity中的生命周期流程。
启动耗时检测
通过adb shell来获取启动耗时,命令如下:
adb shell am start -W [packageName]/[packageName.AppstartActivity] |
Profile GPU Rendering —— Android系统自带,分析绘制耗时
Lint —— Android IDE自带,静态代码检查工具
Systrace UI —— 性能数据采样和分析工具
帮助收集Android关键子系统(如SurfaceFlinger、WindowManagerService等Framework部分关键模块、服务、View系统等)的运行信息。从而更直观的分析系统瓶颈,改善性能。
Systrace的功能包括跟踪系统的I/O操作、内核工作队列、CPU负载等等,在UI显示性能分析上提供很好的数据,特别是在动画动画播放不流畅、渲染卡顿等问题上。Systrace工具可以跟踪、收集、检查定时信息,可以很直观的查看CPU周期消耗的具体时间,显示每个线程和进程的跟踪信息,使用不同颜色来突出问题的严重性,并提供如何解决这些问题的建议。
由于Systrace是以系统的角度返回一些信息,并不能定位搭配具体耗时的方法,要进一步获取CPU满负荷运行的原因,还需要使用接下来介绍的工具TraceView.
TraceView —— AndroidSDK自带,分析函数调用过程耗时
刨根究底——Android系统的显示原理是怎样
说到显示原理,说真的学完很多视频教学,即使当时搞明白java层与native层的各种技术原理,什么vsync生成机制,vsync分发原理等等,但是隔一晚上依旧记忆模糊,话到嘴边也讲不出个所以然,为什么会这样呢?是因为整个显示系统很复杂么?没错,确实很复杂,但是这次我想通过笔记来好好梳理一下整体流程,相信只要抓住关键知识,从应用的角度出发,构建一整个相对完整的知识体系还是不难的。下面首先介绍再应用开发上涉及到的知识点和整体流程。
Android系统的显示过程可以简单概括为:Android应用程序把经过测量(measure)、布局(layout)、绘制(draw)后的数据放入从系统服务申请得到的buffer块中,并将其提交给系统服务。然后系统服务将这个buffer写到屏幕的一块缓冲区里,接下来屏幕会以一定的帧率去刷新,每次刷新的时候,就会从缓冲区里面把这个图像数据读取出来然后显示出来。
简单来说,就是应用层负责绘制,系统层负责渲染,通过进程间通信(GraphicBufferProducer)把应用层需要绘制的数据传递到系统层服务,系统层服务通过刷新机制把数据渲染到屏幕上。通过阅读源码可知,Android的图形显示系统采用的是C/S架构。SurfaceFlinger(Server)由C++代码编写,Client端代码分为两个部分,其中之一是由Java提供给应用使用的API,另一部分则是由C++写成的native层的实现。下面具体梳理一下整个过程
绘制原理
绘制任务是由应用发起的,最终通过系统层会知道硬件屏幕上,也就是说,应用进程绘制好以后,通过跨进程通信方式将需要显示的数据传到系统层,由系统层中的SurfaceFlinger服务绘制到屏幕上。
应用层
应用的界面无外乎是由一个个View和ViewGroup组成的一整棵View Tree,它们有着不同的嵌套存在着父子关系,子View在父View中,这些View都经过一个相同的流程(Measure -> Layout -> Draw到Surface)最终都会显示到屏幕上,这也意味着要想完整的显示所有的数据,就要对其中的每一个View都进行一次绘制,并且这将是一个递归过程。
在源码中,整体的绘图代码是在ViewRootImpl类的performTraversals()方法里。通过这个方法可以看出Measure和Layout都是递归来获取View的大小和位置,并且以深度作为优先级。可以看出,层级越深,元素越多,耗时越长。
Measure
用深度优先原则递归得到所有的视图View的宽、高,获取当前View的正确宽度childWidthMeasureSpec 和高度childHeightMeasureSpec之后,可以调用它的成员函数Measure来设置它的大小。如果当前正在测量的子视图child是一个视图容器,则会继续重复递归调用,直到它的子孙视图的代销都测量完成为止。
Layout
用深度优先原则递归得到所有的视图View的位置,当一个子View在应用程序窗口左上角的位置确定之后,再结合它在前面测量过程中的确定的宽高,就可以完全确定它在应用程序窗口中的布局了。
Draw
目前Android支持两种绘制方式:软件绘制(CPU)和硬件加速(GPU),当然硬件加速在UI显示和绘制的效率远远高于CPU绘制,但硬件加速并非没有缺点,如下:
- 耗电:GPU的功耗比CPU高
- 兼容问题:某些接口和函数不支持硬件加速
- 内存占用大:使用OpenGL的接口类比较占据内存
系统层
真正把需要显示的数据渲染到屏幕上,是通过系统进程中SurfaceFlinger服务来实现的,SurfaceFlinger的具体实现和工作原理会有专门一篇来介绍,这里先略过。先了解SurfaceFlinger它的主要工作即可。
- 响应客户端事件,创建Layer与客户端的Surface建立连接;
- 接收客户端数据集属性,修改Layer属性,如尺寸、颜色、透明度等;
- 将创建的Layer内容刷新到屏幕上
- 维持Layer的序列,并对Layer最终输出做裁剪计算。
这里涉及到跨进程通信,查阅资料得知,使用了Android匿名共享内存的方式SharedClient。这里先不细说,具体有关Android系统的图形渲染机制,请阅读这篇文章,后面也会总结系列文章。
刷新机制
在理解刷新机制之前,先介绍一些技术名次以及VSync和Choreographer:
- 双缓冲
显示内容的数据内存,为什么要使用双缓冲?我们知道在Linux上通常使用Framebuffer来做显示输出,当用户进程更新Framebuffer中的数据后,显示驱动会把Fb中每个像素点的值更新到屏幕上,但这样会带来一个问题,如果上一帧的数据还没显示完,Fb中的数据又更新了,就会带来重影的问题,带给用户的直观感受就是画面有闪烁感。为了解决这个问题,就采用了双缓冲技术。双缓冲意味着要使用两个缓冲区,其中一个是Front Buffer,另一个是Back Buffer。UI总是先在Back Buffer中绘制,然后再和Front Buffer交换,渲染到显示设备上。即只有当另一个buffer的数据准备好后,就可通过io_ctrl来通知显示设备切换Buffer。
- Vsync
通过双缓冲技术可以知道,如果只有当另一个buffer准备好后,才能通知刷新的话,这种机制效率是很低下的,基于此,所以引入了Vsync。这是一种垂直同步信号,可以简单地把它认为是一种定时中断,一旦收到vsync中断信号,CPU就可以开始处理各个帧的数据了。
- Choreographer
起舞者,通过这个类可以协调应用端的响应与vsync中断周期的关系,收到VSync信号时,调用用户设置的回调函数。
卡顿情况分析
那究竟是什么原因导致的应用界面卡顿呢?从Android系统的显示原理可以看出,影响绘制的根本原因有以下两个方面:
- 绘制任务太重,绘制一帧内容耗时过长
- 主线程过于繁忙,导致VSync信号到来之时还没有准备好数据导致丢帧
但其实上面两点总结起来就是垂直同步信号到来没有绘制下一帧依旧显示上一帧画面造成的页面卡顿错觉。我们知道我们所有的绘制工作都是由主线程,也就是UI线程来负责(这一句话在开发过程不会错,但是不严谨),主线程的关键职责是处理用户交互,在屏幕上绘制像素,并进行加载显示相关的数据。在实际开发过程中,需要尽量避免任何阻碍主线程执行的耗时操作,这样APP才会做到即时响应,体验优良。
同时在开发过程中,我们需要知道主线程做哪些方面的工作,总结如下:
- UI生命周期控制
- 系统事件处理
- 消息处理
- 界面布局
- 界面绘制
- 界面刷新
除了这些,要尽量避免将其他处理放到主线程中,特别是复杂的数据计算和网路请求。