今天来简单的介绍下怎么分析android应用的内存泄漏问题。
首先我们要明白为什么会有内存泄漏,主要有2种情况:
a.全局进程(process-global)的static变量。这个无视应用的状态,持有Activity的强引用的怪物。
b.活在Activity生命周期之外的线程。没有清空对Activity的强引用。
1、了解下4种级别对象的引用:
从JDK 1.2版本开始,把对象的引用分为4种级别,从而使程序能更加灵活地控制对象的生命周期。这4种级别由高到低依次为:强引用、软引用、弱引用和虚引用。
强引用(Strong reference):
常见形式如:A a = new A();等
软引用(Soft Reference):
A a = new A();
SoftReference<A> srA = new SoftReference<A>(a);
软引用所指示的对象进行垃圾回收需要满足如下两个条件:
1.当其指示的对象没有任何强引用对象指向它;
2.当虚拟机内存不足时。
弱引用(Weak Reference):
A a = new A();
WeakReference<A> wrA = new WeakReference<A>(a);
WeakReference不改变原有强引用对象的垃圾回收时机,一旦其指示对象没有任何强引用对象时,此对象即进入正常的垃圾回收流程。
虚引用(Phantom Reference):
2、了解下内存的知识:
JAVA是在JVM所虚拟出的内存环境中运行的,JVM的内存可分为三个区:堆(heap)、栈(stack)和方法区(method)。
栈(stack):栈最显著的特征是:LIFO(Last In, First Out, 后进先出),栈中只存放基本类型和对象的引用(不是对象)。
堆(heap):堆内存用于存放由new创建的对象和数组。
在堆中分配的内存,由java虚拟机自动垃圾回收器来管理。JVM只有一个堆区(heap)被所有线程共享,堆中不存放基本类型和对象引用,只存放对象本身。
方法区(method):又叫静态区,跟堆一样,被所有的线程共享。方法区包含所有的class和static变量。
查看设备内存的方法:
a.查看meminfo文件
cat /proc/meminfo
cat /proc/meminfo |grep Mem
b.更详细的信息可以使用dumpsys命令
dumpsys meminfo 查看总的内存使用
dumpsys meminfo com.example.testleakmemory 查看单个应用的内存使用情况
dumpsys meminfo 27606 通过pid查看单个应用的内存使用情况
3、工具准备:
工欲善其事,必先利器。我们要使用的是DDMS+MAT(Memory Analysis Tool)来分析。
DDMS是ADT自带的调试工具。
MAT的下载网址:http://www.eclipse.org/mat/downloads.php
我自己在线安装一直都不成功,各种错误。后面直接在下载(Stand-alone Eclipse RCP Applications)版本。
开始的时候下载了64位的版本,发现运行exe文件的时候老是报 “failed to load the jni shared jvm.dll” 这个错误。
后面又下载32位了发现可以运行。查看本地的java版本,发现是32位的(我电脑是64位的),这就可以解释通。
D:\android-sdk-windows-4.3\platform-tools>java -version
java version “1.7.0_51”
Java(TM) SE Runtime Environment (build 1.7.0_51-b13)
Java HotSpot(TM) Client VM (build 24.51-b03, mixed mode, sharing)
4、构造一个有内存泄漏的代码:
MainActivity.java:
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void eventClick(View v) {//点击跳转到第二个页面
Intent intent = new Intent(this, Second.class);
startActivity(intent);
}
}
Second.java:
public class Second extends Activity {
private List<String> list = new ArrayList<String>();//全局的list
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_second);
// 模拟Activity一些其他的对象
for (int i = 0; i < 200000; i++) {
list.add(new String("hello world!"));
}
new MyThread().start();// 开启线程
}
public class MyThread extends Thread {
@Override
public void run() {
super.run();
try {// 模拟耗时操作,线程开启10分钟
Thread.sleep(1000*60*10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
5、获取hprof文件:
mat分析需要导出hprof文件。可以先导出泄漏之前的hprof文件,在操作app,导出泄漏之后的hprof。
操作之后的内存图:
dump hprof文件
6、转化hprof文件:
cmd到当前的目录下,把你需要转化的hprof文件也可以拷贝到当前的目录
hprof-conv 1.hprof before.hprof
7、使用mat打开文件开始分析-直方图:
这个时候我们获取了操作之前的before.hprof文件 和 操作之后的 after.hprof文件。
打开mat工具,导入(Open Heap Dump ...)hprof开始分析。
主界面
我们分析最常用到的就是Histogram(直方图)和 Dorminator Tree(支配树)
a.打开Histogram(直方图)他列举了每个对象的统计。它可以列出任意一个类的实例数。
比如你需要查看MainActivity,可以使用正则表达式 .*Main.* (注意大小写)
可以列出与MainActivity相关的类
b.选中com.example.testleakmemory.Second,右击,选择“Merge Shortest Paths to GC Roots”,
再选择选择“exclude all phantom/weak/soft etc.references”
(排查虚引用/弱引用/软引用等)因为被虚引用/弱引用/软引用的对象可以直接被GC给回收.
在JAVA中是通过可达性(Reachability Analysis)来判断对象是否存活,这个算法的基本思想是通过一系列的称谓"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走得路径称为引用链
c.如果存在GC Roots链,即存在内存泄露问题。
除了使用Merge Shortest Paths to GC Roots 我们还可以使用
List object - With outgoing References 显示选中对象持有那些对象
List object - With incoming References 显示选中对象被那些外部对象所持有
Show object by class - With outgoing References 显示选中对象持有哪些对象, 这些对象按类合并在一起排序
Show object by class - With incoming References 显示选中对象被哪些外部对象持有, 这些对象按类合并在一起排序
8、使用mat打开文件开始分析-Dorminator Tree支配树):
a.通过 Dorminator Tree(支配树),可以直观地反映一个对象的retained heap,根据retained heap进行排序.
shallow heap:指的是某一个对象所占内存大小。
retained heap:指的是一个对象的retained set所包含对象所占内存的总大小。
它主要可以用于诊断一个对象所占内存为什么会不断膨胀,一个对象膨胀,就说明它对应到支配树中的子树就越来越庞大。
b.通过dominator_tree查看单一对象所持有的对象
右击对象--Java Basics -- Open In Dominator Tree
很实用
注意:MAT工具上显示的size的大小单位是: Bytes 。例如 21414072 = 20.4M
在MAT中可以查看到有类似如下的显示
com.example.main.MainActivity$1
com.example.main.MainActivity$2
这个 $1 表示第一个匿名类的大小。
$2、$3这样排下去。
9、使用的小技巧1:
a.操作前后两个hprof的对比:
dump出前后2个hprof文件
使用mat工具打开,在Navigation History这个视图中,右击histogram--Add to Compare Basket
Window -- Compare Basket,里面会有2个histogram。点击对比就可以了。
上面这个对比结果不利于查找差异,还可以调整对比选项
Difference from base Table
10、使用的小技巧2:
我们使用对比发现了操作后的内存中多了很多 char[] (我们构造的20万个String未释放)。
在after.hprof的直方图视图中
a.可以右击 -- Immediate dominators(查看引用者) -- 可以发现Second这个activity持有20万个,基本可以判断Second这个页面有内存泄露。
我们再查看那个变量持有了这么多。
b.右击这个Second -- 选择 Dominated Objects(注意一定是这个不要选择Objects里面的) -- Merge Shortest Paths to GC Roots --
exclude all phantom/weak/soft etc.references
c.一步步看谁持有了这个Second不释放
备注:把这个问题解决了之后,再分析发现还有个输入法问题。
android.view.inputmethod.InputMethodManager$ControlledInputConnectionWrapper
.
.
list
可以确定是输入法持有了activity的引用,导致Second activity里面list这个变量未正常释放。
(输入法是单例的,只会持有一个Second的,所以不管你打开多少次这个页面,泄露的大小是固定的。网上查阅这个是系统的一个bug,
如果页面占用内存不是太多,主要是全局变量,那么泄露的就可以接受)
11、实际中遇到的内存泄漏问题:
在实际的应用中遇到了以下一些的内存泄露问题:
a.将一些View定义为static,例如窗口显示:我在其它的类里面直接调用静态的方法来设置静态的View上面的文本。
因为static的生命周期很长,导致这个View持有activity的引用不释放,导致activity销毁了,资源还得不到释放(如果有图片资源泄露的更多)。
解决的方法:
将View不要定义为static,Handler类定义为静态的,使用弱引用来持有activity的引用。
b.在工具类Information类中,定义了一个静态的WiFiManager变量wm
在使用的时候,直接将context传到这个工具类中,wm使用到了这个context获取服务,activity销毁了,wm不会销毁,导致activity资源不释放,内存泄露。
解决的方法:
在工具类中不用传递进来的context,直接获取全局的Context
或者context.getApplicationContext();来获取
c.将context传进了测试类中,类里面又将context定义为static,导致activity销毁内存不释放。(和上面的情况类似)
解决方式:
测试完成,将静态的context设置为null,断掉GC Root链路
d.使用第三方的jar包,显示gif图像导致的内存泄露:
我们启动了gif,但是jar提供的停止的方法中不能停止它里面的子线程。
使用mat分析到时jar包里面的子线程引用了activity,这样子线程不结束,activity就释放不了
解决方法:
我们没有jar的源码,通过查看class显示的私有属性有个isRunning,但是没有提供方法设置。我通过反射去设置了这个变量的属性。问题解决,可以正常释放了。
if(null != gv_toolbox_distribute_gif){
try {
Class<? extends GifView> clazz = gv_toolbox_distribute_gif.getClass();
Field f = clazz.getDeclaredField("isRun");
f.setAccessible(true);
f.set(gv_toolbox_distribute_gif, false);
} catch (Exception e) {
LogUtils.d(TAG, "失败:" + e.getMessage());
}
}
e.自定义一个WaveCircle动画水波纹,里面使用了postDelay方法。导致内存泄露(网上有比较多的这个分析案例)
通过mat工具分析发现,有个ThreadLocal老是持有activity,继续往下看,在getRunQueue里面有2个Runnable未执行。
后面解决方法:
1.在调用stop动画的时候睡500ms,测试有效。
2.我使用发消息的方法stop动画,测试也有效(暂未研究源码,也许是消息执行需要等待一段时间)
f.有个等待的状态Dialog没有正常关闭
通过mat分析到时有个WindowManger持有了activity的引用不释放。后面查代码发现dialog根本就没有关闭。
解决方法:
在dialog显示完成结束的时候,需要手动dismiss掉这个dialog,实际测试OK
g.使用了帧动画,没有正常结束
通过mat分析到有很大的Bitmap未释放,把原图导出来看下,发现是帧动画的原图,检查代码发现,原来开启了动画,当时没有结束动画。
解决方法:
在动画显示完成的时候stop掉就OK了。
**总结:
以上只是分析了一个简单的例子,基本的流程就是这样,以后遇到了复杂的问题,按照这个思路来分析也能逐步的找到问题的所在。
平时我们写代码的时候要多注意这些性能的问题。能避免的尽量避免,不然遇到了泄漏的问题后面来分析花费的代价会更大。
好的习惯很重要!!!**