对于每个Android 开发团队来说产品上线,是让人喜忧参半的一件事。**喜指的是:付出了大量的时间,产品终于上线了;而忧指的是:担心中间会不会出现一些性能相关的问题,比如卡顿、内存泄漏、崩溃……等,**想必不少同行都有个这种经历。
性能优化可以说是在开发中最核心的一个环节,不管你是做的Android 软件开发、Android 系统开发、音视频开发、车载开发等等,都需要了解这块。因为今时不同往日,用户对性能的要求越来越高了,当你的软件出现启动时间过长、卡顿、闪退……,大部分用户就会选择卸载。所以我们需要对性能的优化需要进一步了解,以免这种事情发生。
可能有些开发只会茫目的做业务层的开发,没有考虑的这些因素,所以对这块了解的不清楚,导致面试时就被面试官暴虐了。下面就跟大家简单的分享下性能优化。
什么是性能?
在用户眼里对APP性能有求就是:快、稳、省、小。
而在开发眼里指的则是APP是否能正常的运行,在用户心里是不是一款良好的APP,如果有一项有问题,就说明了产品有待优化。
一般在开发时我们可能注重了功能的实现,忽略了一些小问题,到最后发现这样的应用拿不出手,问题很多,性能很差,想优化但又不知道从哪开始下手。这时我们需要理清好思路,一步步进行分析。
1.布局优化
Android 布局优化是指优化Android应用程序的布局,以提高性能和用户体验。它可以通过减少布局层次结构,减少布局嵌套,使用视图复用,使用缓存等方式来实现。
每个人对于界面的布局思路不一样,自然写的也会不一样,特别是复杂页面,需要采用布局嵌套的方法进行布局,可能会造成嵌套的层级过多。
屏幕上的某个像素在同一帧的时间内被绘制了多次。在多层次的UI结构里面,如果不可见的UI也在做绘制的操作,这就会导致某些像素区域被绘制了多次。这就浪费大量的CPU以及GPU资源。
我们的Android虚拟机也会这么抱怨,咱们家本来就不富裕,什么都要省着用,你这么搞,肯定运转有问题啊,那么多嵌套的小隔断间需要处理,都会占用cpu计算的时间和GPU渲染的时间。显示GPU过度绘制,分层如下如所示:
通过颜色我们可以知道我们应用是否有多余层次的绘制,如果一路飘红,那么我们就要相应的处理了。
2.绘制优化
绘制任务是由应用发起的,最终通过系统层绘制到硬件屏幕上,应用进程绘制好后,通过跨进程通信机制把需要显示的数据传到系统层,由系统层中的SurfaceFlinger服务绘制到屏幕上。
在Android的每个View绘制中有三个核心步骤:通过Measure和Layout来确定当前需要绘制的View所在的大小和位置,通过绘制(Draw)到surface。
在Android系统中整体的绘图源码是在ViewRootImp类(源码位置:frameworks/base/core/java/android/view/ViewRootImpl.java)的**performTraversals()**方法,通过这个方法可以看出Measure和Layout都是递归来获取View的大小和位置,并且以深度作为优先级。可以看出,层级越深,元素越多,耗时也就越长。
(1)应用层:
Measure:用深度优先原则递归得到所有视图(View)的宽、高;获取当前View的正确宽度childWidthMeasureSpec和高度childHeightMeasureSpec之后,可以调用它的成员函数Measure来设置它的大小。如果当前正在测量的子视图child是一个视图容器,那么它又会重复执行操作,直到它的所有子孙视图的大小都测量完成为止。
Layout:用深度优先原则递归得到所有视图(View)的位置;当一个子View在应用程序窗口左上角的位置确定之后,再结合它在前面测量过程中确定的宽度和高度,就可以完全确定它在应用程序窗口中的布局。
Draw:目前Android支持了两种绘制方式:软件绘制(CPU)和硬件加速(GPU),其中硬件加速在Android 3.0开始已经全面支持,,硬件加速在UI的显示和绘制的效率远远高于CPU绘制,但硬件加速也存在明显的缺点:
- 耗电:GPU的功耗比CPU高
- 兼容问题:某些接口和函数不支持硬件加速。
- 内存大:使用OpenGL的接口至少需要8MB内存。
(2)系统层
真正把需要显示的数据渲染到屏幕上,是通过系统级进程中的SurfaceFlinger服务来实现的。
SurfaceFlinger主要负责的任务:
- 响应客户端事件,创建Layer与客户端的Surface建立连接。
- 接收客户端数据及属性,修改Layer属性,如尺寸、颜色、透明度等。
- 将创建的Layer内容刷新到屏幕上。
- 维持Layer的序列,并对Layer最终输出做出裁剪计算。。
既然两个不同进程,那么肯定需要一个跨进程的通信机制来实现数据传输,在Android的显示系统,使用了Android的匿名共享内存:SharedClient,每一个应用和SurfaceFlinger之间都会创建一个SharedClient,在每个SharedClient中,最多可以创建31个SharedBufferStack,每个Surface都对应一个SharedBufferStack,也就是一个window。
一个SharedClient对应一个Android应用程序,而一个Android应用程序可能包含多个窗口,即Surface。也就是说SharedClient包含的是SharedBufferStack的集合。因为最多可以创建31个SharedBufferStack,也就是说一个Android应用程序最多可以包含31个窗口,同时每个SharedBufferStack中又包含了两个(低于4.1版本)或者三个(4.1及以上版本)缓冲区,即显示刷新机制中会提到的双缓冲和三重缓冲技术。
总结起来显示整体流程分为三个模块:1.应用层绘制到缓存区,2.SurfaceFlinger把缓存区数据渲染到屏幕,3.由于是两个不同的进程,所以使用Android的匿名共享内存SharedClient缓存需要显示的数据来达到目的。
cpu和gpu是如何系统工作的呢?
绘制过程首先是CPU准备数据,通过Driver层把数据交给CPU渲染,其中CPU主要负责Measure、Layout、Record、Execute的数据计算工作,GPU负责Rasterization(栅格化)、渲染。由于图形API不允许CPU直接与GPU通信,而是通过中间的一个图形驱动层(GraphicsDriver)来连接这两部分。图形驱动维护了一个队列,CPU把display list添加到队列中,GPU从这个队列取出数据进行绘制,最终才在显示屏上显示出来
导致过度绘制的主要原因一般有如下两点:
- XML布局:控件有重叠且都有设置背景。
- View自绘:View.OnDraw里面同一个区域被绘制多次。
3.内存优化
内存问题是一个普遍问题,但是却普遍缺少关注度,具体有以下几个原因:
1.内存问题相对比较隐蔽,表现并不明显
2.同时android使用Jvm语言开发,垃圾回收是自动的,所以一般没有特别关注
3.内存问题难以定位,出现问题的地方往往只是表现的地方,真正的原因难以收集
大家可以查看下图思路进行去优化:
对于优化的大方向,我们应该优先去做见效快的地方,主要有以下几个部分:
1.内存泄漏
2.内存抖动
3.Bitmap大图监控
4.OOM线上监控
内存优化是个复杂的过程,我们在做内存优化的过程中,需要结合多种工具,线上线下结合,系统化地配合来定位与解决问题
4.启动速度
启动速度可以说是APP的门面,产品上线后能否留住第一波用户继续使用该应用它是关键。
app启动分为冷启动(Cold start)、热启动(Hot start)和温启动(Warm start)三种。
冷启动(Cold start)
冷启动是指应用程序从头开始:系统的进程在此开始之前没有创建应用程序。冷启动发生在诸如自设备启动以来首次启动应用程序或自系统终止应用程序以来。
在冷启动开始时,系统有三个任务。这些任务是:
1、加载并启动应用程序
2、启动后立即显示应用程序的空白启动窗口
3、创建应用程序进程
当系统为我们创建了应用进程之后,开始创建应用程序对象。
1、启动主线程
2、创建主Activity
3、加载布局
4、屏幕布局
5、执行初始绘制
应用程序进程完成第一次绘制后,系统进程会交换当前显示的背景窗口,将其替换为主活动。此时,用户可以开始使用该应用程序。至此启动完成。
热启动(Hot start)
应用程序的热启动比冷启动要简单得多,开销也更低。在一个热启动中,系统都会把你的Activity带到前台。如果应用程序的Activity仍然驻留在内存中,那么应用程序可以避免重复对象初始化、布局加载和渲染。
热启动显示与冷启动方案相同的屏幕行为:系统进程显示空白屏幕,直到应用程序完成呈现活动。
温启动(Warm start)
温启动包含了冷启动时发生的一些操作,与此同时,它表示的开销比热启动少,有许多潜在的状态可以被认为是温暖的开始。
场景:
- 用户退出您的应用,但随后重新启动它。该过程可能已继续运行,但应用程序必须通过调用从头开始重新创建Activity 的 onCreate() 。
- 系统将您的应用程序从内存中逐出,然后用户重新启动它。需要重新启动进程和活动,但是在调用onCreate()的时候可以从Bundle(savedInstanceState)获取数据。
了解完启动过程,我们就知道哪里会影响我们启动的速度了。在创建应用程序和创建Activity期间都可能会出现性能问题。
这里是慢的定义:
- 冷启动需要5秒或更长时间。
- 温启动需要2秒或更长时间。
- 热启动需要1.5秒或更长时间。
无论何种启动,我们的优化点都是: Application、Activity创建以及回调等过程
谷歌官方给的建议是:
1、利用提前展示出来的Window,快速展示出来一个界面,给用户快速反馈的体验;
2、避免在启动时做密集沉重的初始化(Heavy app initialization);
3、避免I/O操作、反序列化、网络操作、布局嵌套等。
5.体积包优化
可能项目会优先做启动优化、内存优化、卡顿优化,包体积优化在实际的项目中优先级是比较低的,因为做了之后它的好处不是那么明显,特别是对还未处于稳定期的项目。
一个商业产品最主要的目的就是能为公司带来收益盈利,包体积优化让用户在更快的速度下载使用应用,聚拢收集用户群,推荐给用户需要的产品才能产生盈利。这是具体运营业务的指标,在项目精细化运营的阶段非常重要。在应用商店有很多大型的 app,它们也会推出一款 Lite 极速版本也是同样的原因。
还有一个项目做大之后,可能会和一些渠道合作商合作预装,一般合作商也是会对 app 的包体积有具体和详细的要求。
所以,包体积优化是一个项目在发展壮大后必然会经历的一个优化过程。
apk 的结构、打包流程分析我们可以优化的方向主要有三点:代码优化、资源优化和 so 优化,并且从三个优化方向总结出一些常规的优化手段。
代码优化手段主要是 ProGuard 代码混淆,还有根据项目场景选出适合的三方库和版本,也可以使用 Lint 扫描出项目中冗余的代码。其中主要关注 ProGuard 代码混淆优化手段,选择合适的混淆范围能一定程度对包体积优化带来不错的收益。
资源优化手段主要是从图片体积、Lint 扫描冗余资源、shrinkResources、资源最少化配置和提到 AndResGuard 资源混淆。其中 shrinkResources、资源最少化配置、AndResGuard 是资源优化可以优先使用的优化手段。在项目有多套资源或国际化时使用资源最少化配置,根据场景只配置满足需求的资源,能很好的降低包体积;AndResGuard 虽然也能一定程度生效,但要注意有动态换肤等场景考虑选用。一般情况考虑时间和成本评估,图片体积优化这种手段的使用优先级不高,如果有极致的包体积优化需求就可以考虑,但要注意每种图片格式的适用场景。
so 优化手段主要关注指定 CPU 架构生成 so 的方案,优先考虑适配市面上覆盖范围广的 CPU 架构,例如 armeabi-v7a 和 arm64-v8a;我们也简单了解了 so 动态加载的方案和思路,但如若没有一套成熟完善经过大量测试的 so 动态加载框架,我的建议是非必须不使用,因为成本太高要考虑安全性等问题,使用不当也容易线上出现稳定性隐患。
三种优化方向一般可以先从 so 优化开始,因为 so 一般体积大优化效果明显,往后是资源优化和代码优化。
需要注意的是,了解了每种优化方向的这些常规化手段,更重要的是明白它们背后的原理,并且结合业务及成本评估是否适合应用在自己的业务场景,避免弄巧成拙。
6、耗电优化
我们可能对耗电优化不怎么感冒,没事,谷歌这方面做得也不咋地,5.0之后才有像样的方案,讲实话这个优化的优先级没有前面几个那么高,但是我们也要了解一些避免耗电的坑,至于更细的耗电分析可以使用这个Battery Historian]。
Battery Historian 是由Google提供的Android系统电量分析工具,从手机中导出bugreport文件上传至页面,在网页中生成详细的图表数据来展示手机上各模块电量消耗过程,最后通过App数据的分析制定出相关的电量优化的方法。
我们来谈一下怎么规避电老虎吧。
谷歌推荐使用JobScheduler,来调整任务优先级等策略来达到降低损耗的目的。JobScheduler可以避免频繁的唤醒硬件模块,造成不必要的电量消耗。避免在不合适的时间(例如低电量情况下、弱网络或者移动网络情况下的)执行过多的任务消耗电量。
具体功能:
1、可以推迟的非面向用户的任务(如定期数据库数据更新);
2、当充电时才希望执行的工作(如备份数据);
3、需要访问网络或 Wi-Fi 连接的任务(如向服务器拉取配置数据);
4、零散任务合并到一个批次去定期运行;
5、当设备空闲时启动某些任务;
6、只有当条件得到满足, 系统才会启动计划中的任务(充电、WIFI…);
同时谷歌针对耗电优化也提出了一个懒惰第一的法则:
- 减少 你的应用程序可以删除冗余操作吗?例如,它是否可以缓存下载的数据而不是重复唤醒无线电以重新下载数据?
- 推迟 应用是否需要立即执行操作?例如,它可以等到设备充电才能将数据备份到云端吗?
- 合并 可以批处理工作,而不是多次将设备置于活动状态吗?例如,几十个应用程序是否真的有必要在不同时间打开收音机发送邮件?在一次唤醒收音机期间,是否可以传输消息?
谷歌在耗电优化这方面确实显得有些无力,希望以后可以退出更好的工具和解决方案,不然这方面的优化优先级还是很低。付出和回报所差太大。
7、ListView和 Bitmap优化
针对ListView优化,主要是合理使用ViewHolder。创建一个内部类ViewHolder,里面的成员变量和view中所包含的组件个数、类型相同,在convertview为null的时候,把findviewbyId找到的控件赋给ViewHolder中对应的变量,就相当于先把它们装进一个容器,下次要用的时候,直接从容器中获取。
现在我们现在一般使用RecyclerView,自带这个优化,不过还是要理解一下原理的好。 然后可以对接受来的数据进行分段或者分页加载,也可以优化性能。
对于Bitmap,这个我们使用的就比较多了,很容易出现OOM的问题。
Bitmap的优化套路很简单,粗暴,就是让压缩。 三种压缩方式:
1.对图片质量进行压缩
2.对图片尺寸进行压缩
3.使用libjpeg.so库进行压缩
8、响应速度优化
影响响应速度的主要因素是主线程有耗时操作,影响了响应速度。所以响应速度优化的核心思想是避免在主线程中做耗时操作,把耗时操作异步处理。
9、线程优化
线程优化的思想是采用线程池,避免在程序中存在大量的Thread。线程池可以重用内部的线程,从而避免了现场的创建和销毁所带来的性能开销,同时线程池还能有效地控制线程池的最大并发数,避免大量的线程因互相抢占系统资源从而导致阻塞现象发生。
总结
上述中的是对Android APP性能调优优化各方向的简单介绍,其实面试能问道的性能优化相关知识点也就这么多,只是每个人回答的方式不同而已,最终的结果怎么样,还得看面试官怎么理解吧。如果想彻底搞懂这块知识点的话,可以去参考学习《Android 性能调优学习手册》:https://qr18.cn/FVlo89
,可以说里面记录比较详细比较全了。
《Android 性能调优手册》
1.内存优化
2.UI优化及渲染优化
3.电量优化
4.网络优化
5.Bitmap优化
6.图片压缩优化
7.多线程并发优化
8.数据传输效率优化
9.启动优化
10.卡顿优化
11.安装包优化