Android进阶——性能优化之Android Monitor、MAT、TraceView、Allocation Tracking、Lint的使用

前言

一个好的性能优化,可以让你的软件运行速度上比别人快,出现的卡顿现象少,而且一个好的性能软件,会在系统内存中生存的更久。性能优化最主要的就是对Java内存的管理,即堆内存中的管理,对于Java内存分配的讲解,详细可见我的博客文章

概念介绍

内存泄漏和内存溢出的区别

  • 内存泄漏:指程序分配出去的内存不再使用,无法进行回收
  • 内存溢出:指程序在申请内存时,没有足够的空间供其使用

成员变量和局部变量内存分配

  • 成员变量中的引用和引用对象存在堆中
  • 局部变量中的引用存在栈中,引用对象存在堆中

Android Monitor

我们通过一个单例模式的例子产生的内存泄漏来使用Android Monitor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* 单例模式
*/
public class CommUtil {
private static CommUtil instance;
private Context context;
private CommUtil(Context context) {
this.context=context;
}
public static CommUtil getInstance(Context context){
if(null==instance){
instance=new CommUtil(context);
}
return instance;
}
}
/**
* 使用单例
*/
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
CommUtil instance = CommUtil.getInstance(this);
}
}

接着我们旋转三次屏幕,让Activity不断调用onCreate方法,从而导致CommUtil不断被实例化,在Android Monitor的内存监测中也能看得出来内存在增加

这里写图片描述

1、打开Android Monitor

这里写图片描述

2、点击Dump java Heap让其产生一份快照

这里写图片描述

3、找到MainActivity类,查看其实例情况

这里写图片描述

我们可以看到MainActvity有三个实例对象被引用

  • 0号位属于内存泄漏的实例
  • 1号位属于垃圾内存,会被GC回收
  • 2号位属于当前界面引用的实例

这里需要注意,旋转屏幕3次以上都只会有2个MainActivity。当GC回收的时候会将第0个和最后一个留着,其他的都会被回收

4、点击对象实例,找到引用的对象

这里写图片描述

5、解决方法

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 使用单例
*/
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
CommUtil instance = CommUtil.getInstance(getApplicationContext());
}
}

这个时候,我们再按照前面的方法查看MainActivity的实例

这里写图片描述

Android Monitor之Memory Usage

正常的情况下,应用程序按返回键退出程序,并且经过多次手动GC,理论上所有的Activity都会被回收,我们可以通过Memory Usage的信息,查看退出程序后的Activity和View是否依然存在内存中,从此可以判断是否发生内存泄漏。我们主要是通过下面的入口,查看到最后程序退出并经过GC后的剩余情况,这里很明显可以看到内存泄漏了Activity和View,正常的值应该是0

这里写图片描述

MAT

我们还是按照上面单例模式的例子,产生内存泄漏来学习使用MAT工具

1、生成hprof文件导出

这里写图片描述

2、打开MAT 导入我们的2个hprof文件,其中1个是旋转多次屏幕之后的文件(属于内存泄漏部分),另1个是没有内存泄漏的文件,通过以下操作打开:Open File->选择文件->Leak Suspects Report->Finish

这里写图片描述

3、在OverView视图找到Histogram

这里写图片描述

4、进入Histogram,查看内存使用情况

这里写图片描述

5、分别在2个hprof文件中做如下动作,将Histogram视图添加到对比栏

这里写图片描述

6、点击对比

这里写图片描述

7、通过比较后就会生成一个比较结果表ComPared Tables,我们通过输入我们自己的包名,找到对应的比较结果

这里写图片描述

8、通过比较发现我们的程序存在内存泄漏,那么下面就要在内存泄漏的文件中找到泄漏的根源

这里写图片描述

9、回到泄漏的文件中,找到Histogram入口,输入我们发现泄漏的类名

这里写图片描述

10、通过右键,查看MainActivity实例被哪些对象使用

这里写图片描述

这里写图片描述

这里写图片描述

11、由于使用的对象大多数为系统级别的引用,很难让我们去分辨具体的内存泄漏,所以我们通过遍历GC Root树去将那些有可能被GC回收的实例将他们去除,右键取出可能会被GC的虚/弱/软引用

这里写图片描述

12、最后只剩下我们泄漏的内存

这里写图片描述

TraceView

我们通过计算斐波拉契列数的例子,学习TraceView的使用

1
2
3
4
5
6
7
8
9
10
11
//调用斐波拉契列数
computeFibonacci(40);
public int computeFibonacci(int positionInFibSequence) {
if (positionInFibSequence <= 2) {
return 1;
} else {
return computeFibonacci(positionInFibSequence - 1)
+ computeFibonacci(positionInFibSequence - 2);
}
}

1、打开Android Device Monitor (DDMS),按步骤启动TraceView

这里写图片描述

2、开始start Method Profiling之后,在主程序中执行斐波拉契列数,等待大概5秒就stop Method Profiling,系统会生成一份分析文件

这里写图片描述

3、鼠标移动到某一处黑色的地方

这里写图片描述

4、滑动鼠轮进行放大

这里写图片描述

5、产看详细信息面板

这里写图片描述

列名 作用
Name 该进程中所调用的函数名称
Incl Cpu Time 函数占用的CPU时间,包含内部调用其它函数的CPU时间
Excl Cpu Time 函数占用的CPU时间,但不包含内部调用其它函数所占用的CPU时间
Incl Real Time 函数运行的真实时间(以毫秒为单位),内含调用其它函数所占用的真实时间
Excl Real Time 函数运行的真实时间(以毫秒为单位),不包含调用其它函数所占用的真实时间
Calls+Recur Calls/Total 函数被调用次数以及递归调用占总调用次数的百分比
Cpu Time/Call 函数调用CPU时间与调用次数的比(该函数平均执行时间)
Real Time/Call 同CPU Time/Call类似,只不过统计单位换成了真实时间

6、解决方法

1
2
3
4
5
6
7
8
9
10
11
12
//将递归换为for循环 同时使用缓存 先将结果缓存起来
public int computeFibonacci(int positionInFibSequence) {
int prev=0;
int current=1;
int values;
for(int i=0;i<positionInFibSequence;i++){
values=prev+current;
prev=current;
current=values;
}
return current;
}

Allocation Tracking

1、打开Android Monitor,找到下面按钮,点击start Allocation Tracking,然后执行我们的程序进行分配内存,最后stop Allocation Tracking,可以看到图片上有一段矩形,就是我们追踪的内存分配部分,等一会会生成一份分析报告

这里写图片描述

2、在详细报告中打开图形图,可以看到每一层的内存分布情况,后面的内存分配都可以在图形中结合右边介绍找到

这里写图片描述

Lint

1、采用Lint工具系统会检测出一些简单的内存泄漏,或者是书写规范等等问题,使用Analyze里面的Inspect Code

这里写图片描述

2、选择整个目录结构即可得到分析报告

这里写图片描述

常见的内存泄漏例子

1、静态变量引起的内存泄露情况

当调用getInstance时,如果传入的context是Activity的context,只要这个单例没有被释放,那么这个Activity也不会被释放,一直到进程退出才会释放。解决方法就是将传入的context设置为getApplicationContext()即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class CommUtil {
private static CommUtil instance;
private Context context;
private CommUtil(Context context) {
this.context = context;
}
public static CommUtil getInstance(Context mcontext) {
if (instance == null) {
instance = new CommUtil(mcontext);
}
return instance;
}
}

2、非静态内部类引起的内存泄露情况

由于非静态内部类对外部类持有应用,且非静态内部类的生命周期和外部类生命周期不一样,就会导致内存泄漏。解决方法就是将非静态内部类设置为静态内部类即可

3、注册的监听未移除引起的内存泄露情况

最常见的是 registerReceiver()、订阅-发布模式

4、资源未关闭引起的内存泄露情况

BroadCastReceiver、Cursor、Bitmap、IO流、自定义属性attribute、attr.recycle()的回收

5、无限循环动画引起的内存泄露情况

没有在onDestroy中停止动画,否则Activity就会变成泄露对象

处理过的内存泄露

1、Handler和Callback内存泄露

在Activity内部创建Handler,由于Activity的生命周期和Handler不一样,所以Handler会内存泄露,解决方法就是将Handler设置为静态类,有时Handler里面会存储Callback等变量,请务必将这些变量设置为弱引用进行存储

2、DCL(双检查锁机制)

DCL的单例需要对单例变量进行volatile修饰,否则JVM会存在指令的重排序优化,导致内存泄露

坚持原创技术分享,您的支持将鼓励我继续创作!