深度性能测试能协助测试人员发现 APP 中存在的深层次性能问题,直接定位多项性能问题及瓶颈的根本原因,方便开发者快速提升 APP 性能表现,使得 APP 运行得更加稳定。MQC 深度性能测试能够帮助开发者发现深层次的性能问题,更精准地定位问题。
  功能决定现在,性能决定未来!

一、 内存泄漏

  内存泄漏是指由于代码编写不当导致不再使用的对象无法得到及时释放。内存泄漏产生的内存垃圾不仅浪费资源,拖慢运行效率,甚至还可能造成内存溢出,直接导致应用崩溃。
  对于 Android 应用,比较容易发生泄漏的是 Activity、Fragment 对象,此类对象的共性是其都有一定的生命周期。以 Activity 为例,一个 Activity 实例的生命起始于 onCreate(),终结于 onDestroy()。当一个 Activity 不再使用时,系统会调用回调方法 Activity.onDestroy() 方法做一些清理操作。但是对于 Activity 对象本身所占内存,则完全由虚拟机的垃圾回收器来完成回收。垃圾回收器会检查该实例是否被持有强引用,如果存在指向该对象的强引用,则不会回收其所占内存空间,这块内存空间也就成了内存垃圾。由此可见内存泄漏是由不当的强引用导致的。
  MQC 支持对 Activity、Fragment 对象的内存泄漏检测,检测结果可在性能报告-性能问题模块查看。

1.1 对象的引用链

  从 GC ROOT 到泄漏对象的引用链能精准地定位导致内存泄漏的原因。对象无法被垃圾回收器回收,一定是由于 GC ROOT 直接或间接持有了它的强引用。

  常见的 GCROOT 有:声明为 static 的变量,未停止的线程,Application 对象,甚至是栈内存中的局部变量。

1.2 Android 中常见的内存泄露

a.集合中对象没清理造成的内存泄露

  编程过程中,我们常常会把一些对象加入到集合中。在我们不再需要该对象时,如果没有及时把它从集合中清理掉,就会导致这个集合占用的内存越来越大。同时如果这个集合是静态的话,那情况就更严重了。如下的代码段中在每次启动 Activity 的时候都往静态集合中添加了一个对象,如果 Activity 被频繁启动,set 将不断变大,影响 APP 的正常运行。
file
  所以,集合中不再使用的对象应及时释放掉。上述代码应该在 Activity 的 onDestroy() 方法中,及时清理 set 里的元素,避免无用对象继续存在强引用,例如:

file
  这样可以保证 set 持有的强引用都被释放。

b. 单例模式造成的内存泄漏

  单例的静态特性使得其生命周期可能跟应用的生命周期一样长,如果使用不恰当的话,很容易造成内存泄漏。

  如下代码是一个简单的单例模式实现:

file
  在创建单例的时候,如果我们传入当前 Activity 的 Context,例如:

file
  单例 testContextHelper 里面一直保存着该 Activity 的引用,当这个 Context 对应的 Activity 退出时,由于该 Context 的引用一直被单例对象持有,所以该 Activity 占用的内存并不会被回收,造成泄漏。在使用单例模式时,一定要避免持有短生命周期对象的引用,比如上述代码在引用 Context 时可以使用 Application 的 Context 代替 Activity 的 Context,即:

  因为 Application 在应用的运行过程中一直存在,不会退出。

c. 非静态内部类创建静态实例造成的内存泄漏

  在启动频繁的 Activity 中,为了避免反复创建某些资源,提高加载速度,我们可能会在 Activity 内部创建一个静态实例,每次启动 Activity 时都会使用该实例,如下代码:

file
  此时 Activity 内部有一个静态单例,且为非静态内部类的实例。由于非静态内部类默认会持有外部类的引用,并且该类创建了一个静态实例,该实例的生命周期和应用的一样长,这就导致了该静态实例一直会持有该 Activity 的引用,导致 Activity 的内存资源不能正常回收。为了避免这一问题,在使用过程中,正确的做法是将内部类设为静态类或者变成单独的类。

d. 使用 handler 时的内存问题

  在 Android 应用中,Handler 通过发送 Message 与其他线程交互,发出的 Message 被存储在目标线程的 MessageQueue 中的,并且 Message 不一定马上就被处理,驻留时间可能比较久。比如我们用 Handler 发送一个延时比较久的 Message:
file
  而 Message 中持有 Handler 实例的强引用,如果 Message 在 Queue 中一直存在,就会导致 Handler 实例无法被回收,而 Handler 持有 Activity 的强引用,Activity 对象也不会被回收,这就造成了实例泄露。所以,在创建 Handler 时,最好使用弱引用来引用目标 Activity 对象,比如:

file
  这样可以避免由于 Handler 持有强引用导致 Activity 无法回收。

e. 静态成员变量造成的内存泄露

  如果成员变量被声明为 static,其生命周期将与整个应用进程的生命周期一样。如果静态变量直接或间接强引用了某一短生命周期对象 (比如 Activity),这会导致即使 app 切到后台,这部分内存也不会被释放。下面的错误示范代码中,在 Activity 启动的时候,直接将其引用赋给了静态变量 obj,会导致该 Activity 一直不能被回收,导致内存泄露。

file
  因此,在使用静态变量时,应该避免其持有短生命周期对象的强引用,可以使用弱引用来代替强引用。

f. 资源未关闭造成的内存泄漏

  对于使用了 BroadcastReceiver,ContentObserver,File,游标 Cursor,Stream,Bitmap 等资源的使用,应该在 Activity 销毁时及时关闭或者注销,否则这些资源将可能不会被回收,造成内存泄漏。虽然有些系统程序,它本身可以自动取消注册的 (非即时),但是我们还是应该在我们的程序中明确的取消注册,程序结束时应该把所有的注册都取消掉。

1.3 MQC 提供的内存泄露分析

  MQC 提供的深度性能测试能够帮助您发现并定位发生了内存泄露的地方。当发生内存泄漏时,测试报告中会给出发生泄露的内存大小,泄露类型,发生泄露的对象,以及该对象的引用链等信息,下图是 MQC 检测到的 APP 内存泄漏案例。

file
  图中可以看到检测到了两条内存泄漏信息。并指出了泄露内存的大小和对象类型,均为 Activity 对象泄露。查看引用链得知,APP 在 ActivityManager 里面持有了所有 Activity 的强引用,最终导致 Activity 退出后无法回收,属于前述介绍的集合对象使用不当造成的内存泄露。可以看到,MQC 提供的内存泄漏分析能直接定位到相关代码,方便您快速修复 BUG。

二、 内存溢出

  内存溢出 (OOM, Out OfMemory) 是指当已存在的对象的占用了绝大部分或者全部分配给该进程的内存空间时,如果进程再申请新的内存空间,由于没有空余内存可用于分配,或可分配的内存不够满足申请者的需求,此时系统就会抛出内存溢出异常。

2.1 常见的内存溢出原因

  很大一部分内存溢出都是由于内存泄露导致,由于已分配的内存被泄露对象占用并且无法释放,随着泄露的对象实例越来越多,导致可用内存越来越少,最终当内存耗尽时,系统就会抛出内存溢出异常。此时只要解决了内存泄露,也就解决了内存溢出。
  
  另一个内存溢出的重要原因就是应用加载了多个占用内存较多的对象。比如应用在运行过程中加载并保存了多个较大的 Bitmap,导致可用内存急剧减少。因此,在代码编写过程中,对于可能占据大量内存空间的对象,我们应该使用软引用或虚引用持有该对象,使得系统 GC 能在内存吃紧时回收该对象释放空间。并且在不使用 Bitmap 时,应及时 recycle,主动释放内存空间。

2.2 MQC 提供的内存溢出分析

  在应用抛出内存溢出时,深度性能测试会主动捕获这一异常,给出抛出该异常的堆栈信息。并分析当前应用进程占用的总内存大小,已分配的内存大小和可用内存大小,方便开发者定位问题。
  
  如下图,测试报告中首先给出发生内存溢出的机型,同时指出检测到内存溢出时应用自身和设备内存的使用情况,可以看到 Native Heap 和 VM Heap 的空余内存都已不多。打开 StackTrace 后,可以看到出现 OOM 错误的代码行,由此我们发现可能是在加载 Bitmap 的时候导致的内存溢出。图中红色箭头所指的地方是应用自身的代码,我们根据这些提示就能够快速找到源文件中出错的代码,立即修复。

file

三、内存抖动

  内存抖动指的是短时间内大量对象被创建和回收。由于短时间内产生了大量的对象,需要分配大量内存,此时需要垃圾回收器 (GC) 频繁工作,回收不再使用的对象来腾出内存空间。GC 的频繁启动占用了一定的系统资源,最终影响应用表现。

3.1 常见的内存抖动

  常见的内存抖动主要是由于在循环或其他场合中不停地创建新对象,并且短时间内这些对象又被释放。瞬间产生大量的对象会严重占用内存区域,当达到阀值,剩余空间不够的时候触发 GC。即使每次分配的对象占用了很少的内存,但是他们叠加在一起会增加 Heap 的压力。GC 启动时会占用 CPU 等资源,直接导致应用运行受到影响,可能出现界面操作不流畅等现象。

3.2 MQC 提供的内存抖动分析

  MQC 能够监控系统的每一次 GC,并给出 GC 发生时的内存使用情况,如下图所示。

file
  图中给出了 3 种 GC 发生的时刻和内存变化的曲线图。3 种 GC 分别为:

  GC_EXPLICIT:应用主动调用 System.gc() 产生的 GC 事件

  GC_FOR_ALLOC:内存分配时,发现可用内存不够时触发的 GC 事件

  GC_CONCURRENT:已分配的内存大小达到某一阈值时会触发的 GC 事件
  

  其中后两种是系统自己决定启动的 GC,应用无法控制。但是我们可以优化代码,避免频繁生成和回收对象,比如不要在循环中频繁 new 新的对象。

四、界面卡顿

  界面卡顿指的是短时间内界面对用户操作没有响应。应用在出现卡顿的时候,就算知道是哪个页面出了问题,但是很难定位到具体的代码。应用卡顿检测就是帮助您快速定位卡顿的具体位置,方便您进行针对性的修复。

4.1 常见的界面卡顿原因

  Android 应用的 UI 绘制和用户操作消息分发都发生在应用主线程,如果主线程来不及处理 UI 更新和响应用户操作,用户就会感觉应用发生了卡顿。因此卡顿发生时尝尝伴随着主线程阻塞。如果在主线程中进行磁盘读写、网络操作或者大量计算时,尝尝会导致主线程被阻塞,发生界面卡顿。

4.2 MQC 提供的界面卡顿分析

file
  如上图所示,在应用运行过程中出现卡顿时,MQC 会记录当前卡顿的时长,例如图中为 1935ms,用于给开发者评估本次卡顿的严重性,随后给出发生卡顿时系统 CPU 和内存的使用情况等信息辅助开发者分析问题。也会给出卡顿发生时的应用调用的完整堆栈,用于定位发生卡顿的代码,MQC 同时归纳出具体的关键代码,免去开发者在大量堆栈中寻找关键行的麻烦。

五、过度绘制

  过度绘制一般指的是屏幕上的某些区域在一帧中被多次绘制,一般是在界面的同一个地方叠加了多个控件。这样会加重 GPU 的工作负担,可能导致应用运行过程中频繁掉帧,影响用户体验。

5.1 过度绘制详细介绍

  当手机开启过度绘制时,屏幕上会标记发生过度绘制的区域,并根据不同的绘制次数使用不同的颜色,颜色标识从好到差依次是:蓝色 - 绿色 - 淡红色 - 红色,分别代表该区域被绘制 1 次、2 次、3 次和 4 次。一般情况下,最好把绘制控制在 2 次以下,3 次绘制有时候是不能避免的,尽量避免,4 次的绘制基本上是不允许的。
为了减少过度绘制,开发者应减少复杂的、层级较多的布局,去掉多余的背景色。简单的界面尽量使用线性布局;较为复杂的界面可以使用相对布局,避免嵌套过多的线性布局。可以使用 ViewStub 来动态加载界面。

5.2 MQC 提供的过度绘制分析

  MQC 实时监测界面的过度绘制指数,当该指数大于 1.5 时,MQC 认为该界面可能需要优化。最终,测试报告中会指出应用每个界面的过度绘制指数,并配合测试视频将过度绘制指数与 Activity 关联起来,并告诉开发者该界面对应的 Activity。如下图所示。

  如上图所示,在应用运行过程中出现卡顿时,MQC 会记录当前卡顿的时长,例如图中为 1935ms,用于给开发者评估本次卡顿的严重性,随后给出发生卡顿时系统 CPU 和内存的使用情况等信息辅助开发者分析问题。也会给出卡顿发生时的应用调用的完整堆栈,用于定位发生卡顿的代码,MQC 同时归纳出具体的关键代码,免去开发者在大量堆栈中寻找关键行的麻烦。

file

六、启动分析

  启动分析通过分析应用启动过程产生的 trace 文件来得到应用的启动时间等信息。通常来说,Android 应用的启动方式分为两种:冷启动和热启动。

  冷启动:当启动应用时,后台没有该应用的进程,此时系统会创建一个新的进程分配给该应用。冷启动因为系统会创建一个新的进程分配给它,所以会先创建和初始化 Application 类,随后创建和初始化 MainActivity 类(包括一系列的测量、布局、绘制),最后显示在界面上。

  热启动:当启动应用时,后台已有该应用的进程(例:按 back 键、home 键,应用虽然会退出,但是该应用的进程是依然会保留在后台,可进入任务列表查看),这种启动会从已有的进程中来启动应用。热启动因为会从已有的进程中来启动,所以热启动就不会创建新的 Application,而是直接创建和初始化 MainActivity,而不必创建和初始化 Application,因为一个应用从新进程的创建到进程的销毁,Application 只会初始化一次。一般来讲,热启动时间都会在一定程度上小于冷启动时间。

  MQC 会分析应用的冷启动时间和热启动时间,给开发者作为参考。同时给出启动分阶段耗时分析、耗时方法定位、启动过程函数调用关系等更详细的信息,可以帮助开发者快速发现启动到底卡在哪了。

file

七、严苛模式 (StrictMode)

  严苛模式 (StrictMode) 是一个开发辅助工具,可以帮助开发者发现那些由于编码过程中不注意而造成的问题。

7.1 严苛模式的详细介绍

  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) 进行网络数据传输。

7.2 MQC 提供的严苛模式检测

  在 MQC 深度性能测试检测到您的应用存在违反上述要求的时候,MQC 首先会指出应用违反了哪些严苛模式的策略,随后分析问题发生时的应用堆栈信息,指出问题出现在哪儿,并统计该问题出现了多少次。针对在检测到的主线程操作 (例如出现主线程磁盘操作,主线程网络操作等) 时,还会给出该操作的持续时间等信息,辅助开发者评估问题的严重程度。

file
  上图显示 MQC 检测到应用中存在主线程 IO 的情况,具体是在主线程中进行了文件读取操作,最长的一次持续了 2356 毫秒,测试过程中一共出现了 62 次磁盘读取操作。根据后面的堆栈信息,可以看到 com.stephen.performance.MainActivity 类里面的 readFile 方法是在主线程中执行的,因此这里我们就可以针对这一信息来进行修改。

  MQC 测试平台是为广大企业客户和移动开发者提供真机测试服务的云平台,拥有大量热门机型,提供 7x24 全天候服务。

  我们致力于提供专业、稳定、全面、高价值的自动化测试能力,以及简单易用的使用流程、贴心的技术服务,并且帮助客户以最低的成本、最高的效率发现 APP 中的各类隐患(APP 崩溃、各类兼容性问题、功能性问题、性能问题等),减少用户流失,提高 APP 质量和市场竞争力。

联系我们:

网站地址:https://mqc.aliyun.com

开发者交流旺旺群:335334143

开发者交流 QQ 群:492028798

客服邮箱:mqc_group@service.alibaba.com;

更多精彩技术分享 欢迎关注 MQC 公众号

file


↙↙↙阅读原文可查看相关链接,并与作者交流