深度性能测试能协助测试人员发现APP中存在的深层次性能问题,直接定位多项性能问题及瓶颈的根本原因,方便开发者快速提升APP性能表现,使得APP运行得更加稳定。MQC深度性能测试能够帮助开发者发现深层次的性能问题,更精准地定位问题。
功能决定现在,性能决定未来!
内存泄漏是指由于代码编写不当导致不再使用的对象无法得到及时释放。内存泄漏产生的内存垃圾不仅浪费资源,拖慢运行效率,甚至还可能造成内存溢出,直接导致应用崩溃。
对于Android应用,比较容易发生泄漏的是Activity、Fragment对象,此类对象的共性是其都有一定的生命周期。以Activity为例,一个Activity实例的生命起始于onCreate(),终结于onDestroy()。当一个Activity不再使用时,系统会调用回调方法Activity.onDestroy()方法做一些清理操作。但是对于Activity对象本身所占内存,则完全由虚拟机的垃圾回收器来完成回收。垃圾回收器会检查该实例是否被持有强引用,如果存在指向该对象的强引用,则不会回收其所占内存空间,这块内存空间也就成了内存垃圾。由此可见内存泄漏是由不当的强引用导致的。
MQC支持对Activity、Fragment对象的内存泄漏检测,检测结果可在性能报告-性能问题模块查看。
从GC ROOT到泄漏对象的引用链能精准地定位导致内存泄漏的原因。对象无法被垃圾回收器回收,一定是由于GC ROOT直接或间接持有了它的强引用。
常见的GCROOT有:声明为static的变量,未停止的线程,Application对象,甚至是栈内存中的局部变量。
编程过程中,我们常常会把一些对象加入到集合中。在我们不再需要该对象时,如果没有及时把它从集合中清理掉,就会导致这个集合占用的内存越来越大。同时如果这个集合是静态的话,那情况就更严重了。如下的代码段中在每次启动Activity的时候都往静态集合中添加了一个对象,如果Activity被频繁启动,set将不断变大,影响APP的正常运行。
所以,集合中不再使用的对象应及时释放掉。上述代码应该在Activity的onDestroy()方法中,及时清理set里的元素,避免无用对象继续存在强引用,例如:
这样可以保证set持有的强引用都被释放。
单例的静态特性使得其生命周期可能跟应用的生命周期一样长,如果使用不恰当的话,很容易造成内存泄漏。
如下代码是一个简单的单例模式实现:
在创建单例的时候,如果我们传入当前Activity的Context,例如:
单例testContextHelper里面一直保存着该Activity的引用,当这个Context 对应的 Activity 退出时,由于该 Context 的引用一直被单例对象持有,所以该Activity占用的内存并不会被回收,造成泄漏。在使用单例模式时,一定要避免持有短生命周期对象的引用,比如上述代码在引用Context时可以使用Application的Context代替Activity的Context,即:
因为Application在应用的运行过程中一直存在,不会退出。
在启动频繁的Activity中,为了避免反复创建某些资源,提高加载速度,我们可能会在Activity内部创建一个静态实例,每次启动Activity时都会使用该实例,如下代码:
此时Activity内部有一个静态单例,且为非静态内部类的实例。由于非静态内部类默认会持有外部类的引用,并且该类创建了一个静态实例,该实例的生命周期和应用的一样长,这就导致了该静态实例一直会持有该Activity的引用,导致Activity的内存资源不能正常回收。为了避免这一问题,在使用过程中,正确的做法是将内部类设为静态类或者变成单独的类。
在Android应用中,Handler通过发送Message与其他线程交互,发出的Message被存储在目标线程的MessageQueue中的,并且Message不一定马上就被处理,驻留时间可能比较久。比如我们用Handler发送一个延时比较久的Message:
而Message中持有Handler实例的强引用,如果Message在Queue中一直存在,就会导致Handler实例无法被回收,而Handler持有Activity的强引用,Activity对象也不会被回收,这就造成了实例泄露。所以,在创建Handler时,最好使用弱引用来引用目标Activity对象,比如:
这样可以避免由于Handler持有强引用导致Activity无法回收。
如果成员变量被声明为 static,其生命周期将与整个应用进程的生命周期一样。如果静态变量直接或间接强引用了某一短生命周期对象(比如Activity),这会导致即使app切到后台,这部分内存也不会被释放。下面的错误示范代码中,在Activity启动的时候,直接将其引用赋给了静态变量obj,会导致该Activity一直不能被回收,导致内存泄露。
因此,在使用静态变量时,应该避免其持有短生命周期对象的强引用,可以使用弱引用来代替强引用。
对于使用了BroadcastReceiver,ContentObserver,File,游标Cursor,Stream,Bitmap等资源的使用,应该在Activity销毁时及时关闭或者注销,否则这些资源将可能不会被回收,造成内存泄漏。虽然有些系统程序,它本身可以自动取消注册的(非即时),但是我们还是应该在我们的程序中明确的取消注册,程序结束时应该把所有的注册都取消掉。
MQC提供的深度性能测试能够帮助您发现并定位发生了内存泄露的地方。当发生内存泄漏时,测试报告中会给出发生泄露的内存大小,泄露类型,发生泄露的对象,以及该对象的引用链等信息,下图是MQC检测到的APP内存泄漏案例。
图中可以看到检测到了两条内存泄漏信息。并指出了泄露内存的大小和对象类型,均为Activity对象泄露。查看引用链得知,APP在ActivityManager里面持有了所有Activity的强引用,最终导致Activity退出后无法回收,属于前述介绍的集合对象使用不当造成的内存泄露。可以看到,MQC提供的内存泄漏分析能直接定位到相关代码,方便您快速修复BUG。
内存溢出(OOM, Out OfMemory)是指当已存在的对象的占用了绝大部分或者全部分配给该进程的内存空间时,如果进程再申请新的内存空间,由于没有空余内存可用于分配,或可分配的内存不够满足申请者的需求,此时系统就会抛出内存溢出异常。
很大一部分内存溢出都是由于内存泄露导致,由于已分配的内存被泄露对象占用并且无法释放,随着泄露的对象实例越来越多,导致可用内存越来越少,最终当内存耗尽时,系统就会抛出内存溢出异常。此时只要解决了内存泄露,也就解决了内存溢出。
另一个内存溢出的重要原因就是应用加载了多个占用内存较多的对象。比如应用在运行过程中加载并保存了多个较大的Bitmap,导致可用内存急剧减少。因此,在代码编写过程中,对于可能占据大量内存空间的对象,我们应该使用软引用或虚引用持有该对象,使得系统GC能在内存吃紧时回收该对象释放空间。并且在不使用Bitmap时,应及时recycle,主动释放内存空间。
在应用抛出内存溢出时,深度性能测试会主动捕获这一异常,给出抛出该异常的堆栈信息。并分析当前应用进程占用的总内存大小,已分配的内存大小和可用内存大小,方便开发者定位问题。
如下图,测试报告中首先给出发生内存溢出的机型,同时指出检测到内存溢出时应用自身和设备内存的使用情况,可以看到Native Heap和VM Heap的空余内存都已不多。打开StackTrace后,可以看到出现OOM错误的代码行,由此我们发现可能是在加载Bitmap的时候导致的内存溢出。图中红色箭头所指的地方是应用自身的代码,我们根据这些提示就能够快速找到源文件中出错的代码,立即修复。
内存抖动指的是短时间内大量对象被创建和回收。由于短时间内产生了大量的对象,需要分配大量内存,此时需要垃圾回收器(GC)频繁工作,回收不再使用的对象来腾出内存空间。GC的频繁启动占用了一定的系统资源,最终影响应用表现。
常见的内存抖动主要是由于在循环或其他场合中不停地创建新对象,并且短时间内这些对象又被释放。瞬间产生大量的对象会严重占用内存区域,当达到阀值,剩余空间不够的时候触发GC。即使每次分配的对象占用了很少的内存,但是他们叠加在一起会增加Heap的压力。GC启动时会占用CPU等资源,直接导致应用运行受到影响,可能出现界面操作不流畅等现象。
MQC能够监控系统的每一次GC,并给出GC发生时的内存使用情况,如下图所示。
图中给出了3种GC发生的时刻和内存变化的曲线图。3种GC分别为:
GC_EXPLICIT:应用主动调用System.gc()产生的GC事件
GC_FOR_ALLOC:内存分配时,发现可用内存不够时触发的GC事件
GC_CONCURRENT:已分配的内存大小达到某一阈值时会触发的GC事件
其中后两种是系统自己决定启动的GC,应用无法控制。但是我们可以优化代码,避免频繁生成和回收对象,比如不要在循环中频繁new新的对象。
界面卡顿指的是短时间内界面对用户操作没有响应。应用在出现卡顿的时候,就算知道是哪个页面出了问题,但是很难定位到具体的代码。应用卡顿检测就是帮助您快速定位卡顿的具体位置,方便您进行针对性的修复。
Android应用的UI绘制和用户操作消息分发都发生在应用主线程,如果主线程来不及处理UI更新和响应用户操作,用户就会感觉应用发生了卡顿。因此卡顿发生时尝尝伴随着主线程阻塞。如果在主线程中进行磁盘读写、网络操作或者大量计算时,尝尝会导致主线程被阻塞,发生界面卡顿。
如上图所示,在应用运行过程中出现卡顿时,MQC会记录当前卡顿的时长,例如图中为1935ms,用于给开发者评估本次卡顿的严重性,随后给出发生卡顿时系统CPU和内存的使用情况等信息辅助开发者分析问题。也会给出卡顿发生时的应用调用的完整堆栈,用于定位发生卡顿的代码,MQC同时归纳出具体的关键代码,免去开发者在大量堆栈中寻找关键行的麻烦。
过度绘制一般指的是屏幕上的某些区域在一帧中被多次绘制,一般是在界面的同一个地方叠加了多个控件。这样会加重GPU的工作负担,可能导致应用运行过程中频繁掉帧,影响用户体验。
当手机开启过度绘制时,屏幕上会标记发生过度绘制的区域,并根据不同的绘制次数使用不同的颜色,颜色标识从好到差依次是:蓝色-绿色-淡红色-红色,分别代表该区域被绘制1次、2次、3次和4次。一般情况下,最好把绘制控制在2次以下,3次绘制有时候是不能避免的,尽量避免,4次的绘制基本上是不允许的。
为了减少过度绘制,开发者应减少复杂的、层级较多的布局,去掉多余的背景色。简单的界面尽量使用线性布局;较为复杂的界面可以使用相对布局,避免嵌套过多的线性布局。可以使用ViewStub来动态加载界面。
MQC实时监测界面的过度绘制指数,当该指数大于1.5时,MQC认为该界面可能需要优化。最终,测试报告中会指出应用每个界面的过度绘制指数,并配合测试视频将过度绘制指数与Activity关联起来,并告诉开发者该界面对应的Activity。如下图所示。
启动分析通过分析应用启动过程产生的trace文件来得到应用的启动时间等信息。通常来说,Android应用的启动方式分为两种:冷启动和热启动。
冷启动:当启动应用时,后台没有该应用的进程,此时系统会创建一个新的进程分配给该应用。冷启动因为系统会创建一个新的进程分配给它,所以会先创建和初始化Application类,随后创建和初始化MainActivity类(包括一系列的测量、布局、绘制),最后显示在界面上。
热启动:当启动应用时,后台已有该应用的进程(例:按back键、home键,应用虽然会退出,但是该应用的进程是依然会保留在后台,可进入任务列表查看),这种启动会从已有的进程中来启动应用。热启动因为会从已有的进程中来启动,所以热启动就不会创建新的Application,而是直接创建和初始化MainActivity,而不必创建和初始化Application,因为一个应用从新进程的创建到进程的销毁,Application只会初始化一次。一般来讲,热启动时间都会在一定程度上小于冷启动时间。
MQC会分析应用的冷启动时间和热启动时间,给开发者作为参考。同时给出启动分阶段耗时分析、耗时方法定位、启动过程函数调用关系等更详细的信息,可以帮助开发者快速发现启动到底卡在哪了。
严苛模式(StrictMode)是一个开发辅助工具,可以帮助开发者发现那些由于编码过程中不注意而造成的问题。
StrictMode经常用于捕获那些在应用主线程中进行的磁盘读写操作和网络请求。由于应用主线程是接收UI操作消息和执行界面渲染的地方,为了使应用运行更加流畅和更快响应,请尽量不要在主线程执行磁盘操作和网络请求。当然,这也是避免系统弹出ANR对话框和提高应用稳定性的好方法。一旦检测到违反策略(policyviolation),系统将会输出一条相关的日志,其一般包含一个调用栈,来显示应用在何处发生违例。
注意:尽管Android设备的磁盘一般都是闪存盘,然而实际中很多设备只能以很有限的并发数来操作文件系统。虽然磁盘读写很快,但是具体过程中可能由于其他进程占用了I/O接口,等待的过程会导致整个磁盘操作流程比较慢。如果可以,请尽量假设磁盘读写是一个比较耗时的操作。
StricMode除了可以检测主线程的磁盘操作和网络请求以外,还可以发现主线程中执行时间较长的方法。当应用中有继承了Closeable接口的对象没有关闭的时候,例如文件流等,或者没有使用HTTPS进行网络请求,或者同一个Activity的实例太多,StrictMode都会给出提示。其能发现的错误主要包括:
a.应用在主线程中进行磁盘读写;
b.应用在主线程中进行网络请求;
c.应用在主线程中的某些自定义方法的执行时间比较长;
d.SQLCursor对象在使用之后没有关闭;
e.继承了Closeable接口的对象在使用之后没有关闭;
f. 某一Activity有较多的实例;
g.文件读取接口暴露给外部应用;
h.注册某些对象(广播接收器、观察者、Listener等)后没有取消注册;
i. 没有使用加密网络(HTTPS)进行网络数据传输。
在MQC深度性能测试检测到您的应用存在违反上述要求的时候,MQC首先会指出应用违反了哪些严苛模式的策略,随后分析问题发生时的应用堆栈信息,指出问题出现在哪儿,并统计该问题出现了多少次。针对在检测到的主线程操作(例如出现主线程磁盘操作,主线程网络操作等)时,还会给出该操作的持续时间等信息,辅助开发者评估问题的严重程度。
上图显示MQC检测到应用中存在主线程IO的情况,具体是在主线程中进行了文件读取操作,最长的一次持续了2356毫秒,测试过程中一共出现了62次磁盘读取操作。根据后面的堆栈信息,可以看到com.stephen.performance.MainActivity类里面的readFile方法是在主线程中执行的,因此这里我们就可以针对这一信息来进行修改。
功能决定现在,性能决定未来!欢迎大家来MQC免费体验深度性能测试!