Android内存监控与分析(四):内存分析实例演示

Android内存监控与分析(四):内存分析实例演示

APP测试中难免会有各种显式或者隐式的内存泄漏(Memory Leak)问题,如果不及时发现处理,可能会因为内存泄漏导致各种奇怪的问题(如,卡顿和闪退),甚至可能出现因内存不足(Out of Memory,简称OOM)而导致APP崩溃。
本文将通过实战分析内存泄漏和内存溢出问题,并在必要时说明原理或机制。结构分为四个模块,如图1:

这里写图片描述
图1 内存监控与分析

四、内存分析实例演示

(一)内存测试流程中的要点
1. 代码
通常用来进行内存测试的版本是纯净版本,不应该附加多余的Log和调试用组件。例如有些情况下,为了测试界面延迟/函数执行时间等性能,会加入一些桩点代码。在内存测试中这些代码是不必要的,它们可能会分配临时内存,引起更多的GC,导致应用出现运行缓慢、卡顿等现象。
2. 测试场景
测试场景通常有两类。一类是当前有新开发或改动的某项功能,需要对该功能进行性能测试。因此测试场景主要针对该功能组织,包括功能的开启前、运行、结束后等测试点。另一类是整体性能,考察应用的常见场景,在综合使用情况下的性能指标。测试场景应当包括启动后待机,切换到后台,执行主要功能,以及反复执行各功能后。
在各类场景中,经常作为测试重点的有:
·包含了图片显示的界面。
·网络传输大量数据。
·需要缓存数据的场景。
3. 场景转换成用例
选取了测试场景后,用例设计也要考虑内存测试的特点。一些常见的方法是:
·结合场景比较操作前后或不同版本的内存变化。
·显示多张图片的前台进程。
·多个场景来回切换。
·长时间运行进程的内存增长。
4. 执行
由于GC和广播机制的存在,应用内存通常都在不停地波动,幅度可能会达到几百KB,因此执行时需要考虑这种情况。在采集数据时,需要多次采集并计算平均值。
执行完成,我们就可以根据数据进行比较初步的分析以确定方向。一方面是我们熟悉的Dalvik Heap部分,即由Java代码直接分配的内存,可以通过IDE直接观察到使用情况,也可以使用MAT进行细致的分析。
图19是FSM(有限状态机模型)框架执行的结果。由于有Activity强引用,内存一直增长直到OOM。

Android内存监控与分析(四):内存分析实例演示_第1张图片
图19 FSM结果

另一方面,假如我们发现Dalvik Heap没怎么增长,而其他部分增长了许多,这种情况下的分析就要复杂一些。
排除方法通过logcat命令输出的log信息,搜关键词“GC”(如图20)。如果有下面四个中的一个,就可能存在内存泄露。
GC_FOR_ALLOC:因为在分配内存时内存不够引发的;
GC_EXPLICIT:表明GC被显式请求触发的,如System.gc()调用;
GC_CONCURRENT:表明GC在内存使用率达到一定的警戒值时,自动触发;
GC_BEFORE_OOM:表明在抛出内存溢出(OOM)异常之前,尝试执行最后一次内存回收。

Android内存监控与分析(四):内存分析实例演示_第2张图片
图20 GC_FOR_ALLOC情况

(二)实战演示(TestMemory.apk实例)
Java堆用于存储对象实例,我们只有不断地创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,就会在对象数量到达最大堆的容量限制后产生内存溢出异常。
图五、六操作导致的内存泄漏部分代码如下:

public void addList(){
    if(list == null){
        list = new ArrayList();
    }
    StringBuilder sb = new StringBuilder();
    for(int i=0;i<2048000;i++){
        sb.append("a");
    }
    list.add(sb.toString());

}
static Activity staticActivity;
void setStaticActivity() {
    staticActivity = this;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    //内存泄漏开始---线程
    Runnable runnable = new Runnable() {
        @Override
        public void run() {
            try {
                Thread.sleep(1000*1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    };
    new Thread(runnable).start();
    //内存泄漏结束---线程
    addMemory = (Button)findViewById(R.id.addMemory);
    clickToSecondPage = (Button)findViewById(R.id.clickToSecondPage);
    addMemory.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            addList();
        }
    });
    clickToSecondPage.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            setStaticActivity();
            Intent intent = new Intent(MainActivity.this,SecondActivity.class);
            startActivity(intent);
        }
    });
}

在一个MainActivity,实现了listeners。 这个MainActivity中,addMemory.setOnClickListener创建一个大的字符串数组,不断点击此按钮导致内存泄漏;持续点击直到内存溢出。在跳转页面clickToSecondPage.setOnClickListener后,staticActivity 引用MainActivity导致内存泄漏。

分析TestMemory内存泄漏任务包括:
1) 检测泄露的Activity实例
2) 查找重复的String实例

HPROF Viewer界面,分析内存泄漏。如图21:

Android内存监控与分析(四):内存分析实例演示_第3张图片
图21 内存泄漏分析

第一步
点击“Analyzer Tasks”视图中的启动按钮,启动分析。
第二步
查看“Analysis Result”中的分析结果,点击“Leaked Activities”中的具体实例,该实例的引用关系将会展示在“Reference Tree”视图中。
第三步
根据“Reference Tree”视图中的引用关系找到是谁让这个leak的activity活着的,也就是谁支配(Dominate)这个activity对象。
此例中, 比较简单,可以很清晰看到是this的实例最终支配了MainActivity。“this”实例连接到GC Roots,故而导致MainActivity GC Roots可达,无法被回收。

上述步骤, 可以让我们快速定位可能的内存泄露。 当然,内存问题,除了内存泄露,还有内存消耗过大。我们可以在Heap Viewer中查看分析内存的消耗点。

使用Heap Viewer查看内存消耗。如图22:

Android内存监控与分析(四):内存分析实例演示_第4张图片
图22 Heap Viewer查看内存消耗

第一步
点击视图中“Package Tree View”的按钮, 使用包视图,可以让我们关注到自动APP相关对象的实例。
第二步
点击“Retained Size”排序, 快速找到内存消耗点
第三步
找到最大内存消耗点。

总结
分析内存问题, 主要是观察和比较内存增长情况。然后,分析对象的内存占用(Retained Size)情况,找出Retained Size较大的对象,找到其直接支配者(Immediate Dominator),跟踪其GC可达路径(Path to GC Roots),从而找到是谁让这个大对象活着。

参考资料:
1.《深入理解Java虚拟机:JVM高级特性与最佳实践》,周志明 著,机械工业出版社
2.《移动App性能评测与优化》,TMQ专项测试团队 编著,机械工业出版社
3.文档,Android内存分析指南,追逐 编

PS:感谢光荣之路的追逐和悟空老师悉心的指导和热情的帮助,让我获益匪浅!

你可能感兴趣的:(性能测试,Android)