文档开发者中心
我的

移动端性能测试之Android应用性能优化测试篇

前言:

用户在使用应用过程中可感知的:安装耗时、应用冷热启动耗时、登录耗时、页面加载耗时、控件点击耗时、页面帧率、 Crash、ANR等......
那么以上的都是能通过使用应用过程能够发现的,那么还有些隐性的性能问题,比如内存泄露、CPU占用率偏高,这些问题会引起时候在使用过程中卡顿、App崩溃、App使用页面不流畅等等。这些问题在短时间的使用中不一定会被发现,而对于大部分用户来说也并不了解,是什么原因引起的。大部分用户使用应用时只是简单的停留在大脑皮层,通过触觉、听觉、视觉去判断你的应用好坏。那么对于我们测试人员来说这些问题足以引起我们的重视和慎重的对待!不管是前台的UI、还是应用的内存、CPU占用,对于一个应用的性能来说都是至关重要的存在。那么我们在进行性能测试的时候,对于这样的问题必定加倍关注,既然知道测试重点所在。下面我们来讲讲这些关键点常见问题:

UI显示性能:
UI 可谓是一个应用的脸,所以每一款应用在开发阶段我们的交互、视觉、动画工程师都拼命的想让它变得自然大方美丽,可是现实总是不尽人意,动画和交互总会觉得 开发做出来的应用用上去感觉不自然,没有达到他们心目中的自然流畅细节;这种情况之下就更别提发布给终端用户使用了,用户要是能够感觉出来,少则影响心情,多则卸载应用;所以一个应用的UI显示性能问题就不得不被开发人员重视。
人类大脑与眼睛对一个画面的连贯性感知其实是有一个界限的,譬如我们看电影会觉得画面很自然连贯(帧率为24fps),用手机当然也需要感知屏幕操作的连贯性(尤其是动画过度),所以Android索性就把达到这种流畅的帧率规定为60fps。有了上面的背景,我们开发App的帧率性能目标就是保持在60fps,也就是说我们在进行App性能优化时心中要有如下准则:
1.换算关系:60帧/秒-----------16ms/帧;

2.准则:尽量保证每次在16ms内处理完所有的CPU与GPU计算、绘制、渲染等操作,否则会造成丢帧卡顿问题。

从上面可以看出来,所谓的卡顿其实是可以量化的,每次是否能够成功渲染是非常重要的问题,16ms能否完整的做完一次操作直接决定了卡顿性能问题。

当然了,针对Android系统的设计我们还需要知道另一个常识;虚拟机在执行GC垃圾回收操作时所有线程(包括UI线程)都需要暂停,当GC垃圾回收完成 之后所有线程才能够继续执行(这个细节下面小节会有详细介绍)。也就是说当在16ms内进行渲染等操作时如果刚好遇上大量GC操作则会导致渲染时间明显不 足,也就从而导致了丢帧卡顿问题。有了上面这两个简单的理论基础之后我们下面就会探讨一些UI卡顿的原因分析及解决方案。 所以,今天我们要讲的是UI卡顿的常见的原因。

UI卡顿的常见问题:

1.人为在UI线程中做轻微耗时操作,导致UI线程卡顿;

2.布局Layout过于复杂,无法在16ms内完成渲染;

3.同一时间动画执行的次数过多,导致CPU或GPU负载过重;

4.View过度绘制,导致某些像素在同一帧时间内被绘制多次,从而使CPU或GPU负载过重;

5.View频繁的触发measure、layout,导致measure、layout累计耗时过多及整个View频繁的重新渲染;

6.内存频繁触发GC过多(同一帧中频繁创建内存),导致暂时阻塞渲染操作;

7.冗余资源及逻辑等导致加载和执行缓慢;

8.臭名昭著的ANR;

以上的八个常见问题,是我们在测试过程中出现Bug时,需要先去考虑的。这也是大部分应用出现UI问题的常见原因(都是走在性能优化之路上的前辈总结出来的)。

内存性能:
说完了应用开发中的UI性能问题后我们就该来关注应用开发中的另一个重要、严重、非常重要的性能问题了,那就是内存性能优化分析。Android其实就是嵌 入式设备,嵌入式设备核心关注点之一就是内存资源;有人说现在的设备都在堆硬件配置(譬如国产某米的某兔跑分手机、盒子等),所以内存不会再像以前那么紧 张了,其实这句话听着没错,但为啥再牛逼配置的Android设备上有些应用还是越用系统越卡呢?这里面的原因有很多,不过相信有了这一章下面的内容分 析,作为一个移动开发者的你就有能力打理好自己应用的那一亩三分地内存了,能做到这样就足以了。

Android内存管理原理:

Android 系统内核是基于Linux,所以说Android的内存管理其实也是Linux的升级版而已。Linux在进程停止后就结束该进程,而Android把这 些停止的进程都保留在内存中,直到系统需要更多内存时才选择性的释放一些,保留在内存中的进程默认(不包含后台service与Thread等单独UI线 程的进程)不会影响整体系统的性能(速度与电量等)且当再次启动这些保留在内存的进程时可以明显提高启动速度,不需要再去加载。再直白点就是说Android系统级内存管理机制其实类似于Java的垃圾回收机制,这下明白了吧;在Android系统中框架会定义如下几类进程、在系统内存达到规定的不同level阈值时触发清空不同level的进程类型。


可以看见,所谓的我们的Service在后台跑着跑着挂了,或者盒子上有些大型游戏启动起来就挂,有一个直接的原因就是这个阈值定义的太大,导致系统一直认为已经达到阈值,所以进行优先清除了符合类型的进程。所以说,该阈值的 设定是有一些讲究的,额,扯多了,我们主要是针 对应用层内存分析的,系统级内存回收了解这些就基本够解释我们应用在设备上的一些表现特征了。

应用级内存管理:

在说应用级别内存管理原理时大家先想一个问题,假设有一个内存为1G的Android设备,上面运行了一个非常非常吃内存的应用,如果没有任何机制的情况下是不是用着用着整个设备会因为我们这个应用把1G内存吃光然后整个系统运行瘫痪呢?其实Google的工程师才不会这么傻的把系统设计这么差劲。为了使系统不存在我们上面假想情况且能安全快速的运行,Android的框架使得每个应 用程序都运行在单独的进程中(这些应用进程都是由Zygote进程孵化出来的,每个应用进程都对应自己唯一的虚拟机实例);如果应用在运行时再存在上面假 想的情况,那么瘫痪的只会是自己的进程,不会直接影响系统运行及其他进程运行。 既然每个Android应用程序都执行在自己的虚拟机中,那了解Java的一定明白,每个虚拟机必定会有堆内存阈值限制(值得一提的是这个阈值一般都由厂商 依据硬件配置及设备特性自己设定,没有统一标准,可以为64M,也可以为128M等;它的配置是在Android的属性系统的/system/build.prop中配置dalvik.vm.heapsize=128m即可,若存在dalvik.vm.heapstartsize则表示初始申 请大小),也即一个应用进程同时存在的对象必须小于阈值规定的内存大小才可以正常运行。接着我们运行的App在自己的虚拟机中内存管理基本就是遵循Java的内存管理机制了,系统在特定的情况下主动进行垃圾回收。但是要注意的一点就是在 Android系统中执行垃圾回收(GC)操作时所有线程(包含UI线程)都必须暂停,等垃圾回收操作完成之后其他线程才能继续运行。这些GC垃圾回收一 般都会有明显的log打印出回收类型,常见的如下:
GC_MALLOC——内存分配失败时触发;

GC_CONCURRENT——当分配的对象大小超过一个限定值(不同系统)时触发;

GC_EXPLICIT——对垃圾收集的显式调用(System.gc()) ;

GC_EXTERNAL_ALLOC——外部内存分配失败时触发;

通过上面这几点的分析可以发现,应用的内存管理其实就是一个萝卜一个坑,坑都一般大,在开发应用时要保证的是内存使用同一时刻不能超过坑的大小,否则就装不下了。

Android应用内存泄露概念:

众所周知,在Java中有些对象的生命周期是有限的,当它们完成了特定的逻辑后将会被垃圾回收;但是,如果在对象的生命周期本来该被垃圾回收时这个对象还被 别的对象所持有引用,那就会导致内存泄漏;这样的后果就是随着我们的应用被长时间使用,他所占用的内存越来越大。
-java代码

public final class MainActivity extends Activity {

private DbManager mDbManager;

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

//DbManager是一个单例模式类,这样就持有了MainActivity引用,导致泄露

mDbManager = DbManager.getInstance(this);

}

}
可以看见,上面例子中我们让一个单例模式的对象持有了当前Activity的强引用,那在当前Acvitivy执行完onDestroy()后,这个Activity就无法得到垃圾回收,也就造成了内存泄露。

上面这些,是我们做性能测试需要知道的,虽说我们是做测试,但是懂得Android内存管理原理、应用级内存管理、Android应用内存泄露概念等知识,
那么在你提出应用内存泄漏问题时,可以说的有理有据,还可以帮助开发点位,这样别人对你也是刮目相看(即使是装*都相当到位)。
(如果想详细了解关于Android内存方面的知识,可以查看我之前发的《Android 君:“谁动了我的内存?”》,这篇文字写的非常详细。
那么既然知道了应用会有内存泄漏,我们肯定要去查找应用是否存在内存泄漏?而内存泄漏又是出在哪里?如果要去查找内存泄漏,就得借助工具和一些方法。
比如常用的“adb shell dumpsys meminfo”这个命令,但是它是不跟参数直接展示系统所有内存状态。还有工具可以通过leakcanary工具定位分析。当然了,查看应用内存泄漏的方式有很多种,这里我就不一个个细说。

那么上面我们讲了这么多内存泄漏的知识,当然不是在空谈。现在问题来了,为什么我们要做这个内存泄漏的测试?首先我们知道应用内存泄漏是个隐性的Bug,而且内存泄露可以引发很多的问题,常见的内存泄露导致问题如下:

1.应用卡顿,响应速度慢(内存占用高时JVM虚拟机会频繁触发GC);

2.应用被从后台进程干为空进程(上文系统内存原理有介绍,也就是超过了阈值);

3.应用莫名的崩溃(上文应用内存原理有介绍,也就是超过了阈值OOM);

造成内存泄露泄露的最核心原理就是一个对象持有了超过自己生命周期以外的对象强引用导致该对象无法被正常垃圾回收;可以发现,应用内存泄露是个相当棘手重要的问题,如果不做处理当这个隐性而又不稳地的问题爆发了,那么应用的小宇宙是抵挡不了这个攻势的。所以我们要在这个毒瘤现世之前就把它扼杀在摇篮里。这点我们必须重视!不管是开发还是测试。

Android应用内存溢出OOM概念:

上面我们探讨了Android内存管理和应用开发中的内存泄露问题,可以知道内存泄露一般影响就是导致应用卡顿,但是极端的影响是使应用挂掉。前面也提到过 应用的内存分配是有一个阈值的,超过阈值就会出问题,这里我们就来看看这个问题—–内存溢出(OOM–OutOfMemoryError)。

内存溢出的主要导致原因有如下几类:

  • 应用代码存在内存泄露,长时间积累无法释放导致OOM;

  • 应用的某些逻辑操作疯狂的消耗掉大量内存(譬如加载一张不经过处理的超大超高清图片等)导致超过阈值OOM;

可以发现,无论哪种类型,导致内存溢出(OutOfMemoryError)的核心原因就是应用的内存超过阈值了。


内存泄露导致的OOM分析:

这种OOM一旦发生后会在logcat中打印相关OutOfMemoryError的异常栈信息,不过你别高兴太早,这种情况下导致的OOM打印异常信息是没有太大作用,因为这种OOM的导致一般都如下图情况(图示为了说明问题数据和场景有夸张,请忽略):

从图片可以看见,这种OOM我们有时也遇到,第一反应是去分析OOM异常打印栈,可是后来发现打印栈打印的地方没有啥问题,没有可优化的余地了,于是就郁闷了。其实这时候你留心观察几个现象即可,如下:

  • 留意你执行触发OOM操作前的界面是否有卡顿或者比较密集的GC打印;

  • 使用命令查看下当前应用占用内存情况;

确认了以上这些现象你基本可以断定该OOM的log真的没用,真正导致问题的原因是内存泄露,所以我们应该按照上节介绍的方式去着手排查内存泄露问题,解决掉内存泄露后红色空间都能得到释放,再去显示一张0.8M的优化图片就不会再报OOM异常了。


不珍惜内存导致的OOM分析:

上面说了内存泄露导致的OOM异常,下面我们再来看一幅图(数据和场景描述有夸张,请忽略),如下:

可见,这种类型的OOM就很好定位原因了,一般都可以从OOM后的log中得出分析定位。

如下例子,我们在Activity中的ImageView放置一张未优化的特大的(30多M)高清图片,运行直接崩溃如下:


通过上面的log可以很方便的看出来问题原因所在地,那接下来的做法就是优化呗,降低图片的相关规格即可(譬如使用BitmapFactory的Option类操作等)。

PS:提醒一句的是记得应用所属的内存是区分Java堆和native堆的!

Android应用String/StringBuilder/StringBuffer优化建议:

字符串操作在Android应用开发中是十分常见的操作,也就是这个最简单的字符串操作却也暗藏很多潜在的性能问题,下面我们实例来说说。

先看下面这个关于String和StringBuffer的对比例子:


通过这个例子可以看出来,String对象(记得是对象,不是常量)和StringBuffer对象的主要性能区别在于String对象是不可变的,所以每 次对String对象做改变操作(譬如“+”操作)时其实都生成了新的String对象实例,所以会导致内存消耗性能问题;而StringBuffer对 象做改变操作每次都会对自己进行操作,所以不需要消耗额外的内存空间。

我们再看一个关于String和StringBuffer的对比例子:


在这种情况下你会发现StringBuffer的性能反而没有String的好,原因是在JVM解释时认为
String Str = 'Name:' + 'GJRS';就是String Str = 'Name:GJRS';,所以自然比StringBuffer快了。

可以发现,如果我们拼接的是字符串常量则String效率比StringBuffer高,如果拼接的是字符串对象,则StringBuffer比 String效率高,我们在开发中要酌情选择。当然,除过注意StringBuffer和String的效率问题,我们还应该注意另一个问题,那就是 StringBuffer和StringBuilder的区别,其实StringBuffer和StringBuilder都继承自同一个父类,只是 StringBuffer是线程安全的,也就是说在不考虑多线程情况下StringBuilder的性能又比StringBuffer高。

Android应用HashMap与ArrayMap及SparseArray优化建议:

在 Android开发中涉及到数据逻辑部分大部分用的都是Java的API(譬如HashMap),但是对于Android设备来说有些Java的API并 不适合,可能会导致系统性能下降,好在Google团队已经意识到这些问题,所以他们针对Android设备对Java的一些API进行了优化,优化最多 就是使用了ArrayMap及SparseArray替代HashMap来获得性能提升。

HashMap:

HashMap 内部使用一个默认容量为16的数组来存储数据,数组中每一个元素存放一个链表的头结点,其实整个HashMap内部结构就是一个哈希表的拉链结构。 HashMap默认实现的扩容是以2倍增加,且获取一个节点采用了遍历法,所以相对来说无论从内存消耗还是节点查找上都是十分昂贵的。

SparseArray:

SparseArray 比HashMap省内存是因为它避免了对Key进行自动装箱(int转Integer),它内部是用两个数组来进行数据存储的(一个存Key,一个存 Value),它内部对数据采用了压缩方式来表示稀疏数组数据,从而节约内存空间,而且其查找节点的实现采用了二分法,很明显可以看见性能的提升。

ArrayMap:

ArrayMap内部使用两个数组进行数据存储,一个记录Key的Hash值,一个记录Value值,它和SparseArray类似,也会在查找时对Key采用二分法。

有了上面的基本了解我们可以得出结论供开发时参考,当数据量不大(千位级内)且Key为int类型时使用SparseArray替换HashMap效率高; 当数据量不大(千位级内)且数据类型为Map类型时使用ArrayMap替换HashMap效率高;其他情况下HashMap效率相对高于二者。

有了UI性能优化、内存性能优化、代码编写优化之后我们在来说说应用开发中很重要的一个优化模块—–电量优化。

Android应用耗电量概念:

在盒子等开发时可能电量优化不是特别重视(视盒子待机真假待机模式而定),但是在移动设备开发中耗电量是一个非常重要的指标,如果用户一旦发现我们的应用非常耗电,不好意思,他们大多会选择卸载来解决此类问题,所以耗电量是一个十分重要的问题。

关于我们应用的耗电量情况我们可以进行定长时间测试,至于具体的耗电量统计等请参考此文,同时我们还可以直接通过Battery Historian Tool来查看详细的应用电量消耗情况。最简单常用办法是通过命令直接查看,如下:

adb shell dumpsys batterystats


其实我们一款应用耗电量最大的部分不是UI绘制显示等,常见耗电量最大原因基本都是因为网络数据交互、GPS定位、大量内存性能问题、冗余的后台线程和Service等造成。

Android应用耗电量优化建议:

优化电量使用情况我们不仅可以使用系统提供的一些API去处理,还可以在平时编写代码时就养成好的习惯。具体的一些建议如下:

  • 在需要网络的应用中,执行某些操作前尽量先进行网络状态判断。

  • 在网络应用传输中使用高效率的数据格式和解析方法,譬如JSON等。

  • 在传输用户反馈或者下载OTA升级包等不是十分紧急的操作时尽量采用压缩数据进行传输且延迟到设备充电和WIFI状态时进行。

  • 在有必要的情况下尽量通过PowerManager.WakeLock和JobScheduler来控制一些逻辑操作达到省电优化。

  • 对定位要求不太高的场景尽量使用网络定位,而不是GPS定位。

  • 对于定时任务尽量使用AlarmManager,而不是sleep或者Timer进行管理。

  • 尽可能的减少网络请求次数和减小网络请求时间间隔。

  • 后台任务要尽可能少的唤醒CPU,譬如IM通信的长连接心跳时间间隔、一些应用的后台定时唤醒时间间隔等要设计合理。

  • 特殊耗电业务情况可以进行弹窗等友好的交互设计提醒用户该操作会耗用过多电量。

可以看见,上面只是一些常见的电量消耗优化建议。总之,作为应用开发者的我们要意识到电量损耗对于用户来说是非常敏感的,只有我们做到合理的电量优化才能赢得用户的芳心。



以上这些我们只做相关了解即可,便于我们在测试过程中发现问题的时候知道是问题发生的原因、性质和概念。这样才能有助于我们在和开发沟通问题的时候,能够提供一些建议。或者,说可以清晰的描述引起问题发生的原因及引起问题类型。当然,这些都是需要自己在项目测试中不断累积得来的经验。



Android应用移动端性能优化测试总结:

性能优化是一个很大的话题,上面我们谈到的只是应用常见的性能问题,也是应用性能问题的冰山一角,所以说性能优化是一个涉及面非常广的话题,而我们性能测试工程师要多累积测试过程中出现的问题来丰富自己的测试经验。如果你想对你一个应用进行性能测试你必须对这个应用的整个框架有一个非常清晰的认识。

当然了,我们也不能只是一味的追求各种极致的性能,让开发不断的优化。因为优化本来就是存在风险的,甚至有些过度的优化会直接导致项目的臃肿,所以不要因为极致的性能优化而破坏掉了项目的合理架构。总之一句话,性能优化的路还长着,需要我们自己去不断的积累经验。一个好的性能测试工程师不仅在于能发现问题,还有要能给予解决问题的建议和方案。


点赞
1
收藏
2
回复
2
分享
举报
浏览7066 发布于2019-07-09 01:27未知归属地
全部评论
最多点赞
最新发布
最早发布
点个赞,评个5星
回复于2019-07-18 07:23 未知归属地

太专业了,来点简单的。

1楼回复于2019-07-18 07:22 未知归属地
写回答
新增插入模板功能
一键使用模板,快速填写内容,轻松发帖~
知道了
  • 为了保障您的信息安全,请勿上传您的敏感个人信息(如您的密码等信息)和您的敏感资产信息(如关键源代码、签名私钥、调试安装包、业务日志等信息),且您需自行承担由此产生的信息泄露等安全风险。
  • 如您发布的内容为转载内容,请注明内容来源。

我要发帖子

目录 收起
Android应用内存溢出OOM概念:
Android应用String/StringBuilder/StringBuffer优化建议:
Android应用HashMap与ArrayMap及SparseArray优化建议:
Android应用耗电量概念:
Android应用耗电量优化建议:
了解社区公约,与您携手共创和谐专业的开发者社区。