概述
内存泄露是Android开发中比较常见的问题,一旦发生会导致大量内存空间得不到释放,可用内存急剧减少,导致运行卡顿,部分功能不可用甚至引发应用crash。对于复杂度比较高、多人协同开发的项目来讲,如何快速排查并解决内存泄露问题,往往是一个很棘手的问题,也是作为一名高级Android工程的基本技能。本文旨在简单介绍内存泄漏产生的原因,总结Android中常见的内存泄漏,重点介绍如何使用工具快速排查并解决此类问题。
Android常见内存泄露分析
Java作为一种高级语言,内存管理的任务大部分由JVM自动完成,开发者只需要遵守一定的编程规范就可以避免绝大多数问题。由于对象创建时分配内存和对象销毁时回收内存都交给JVM来处理,正常情况下当我们需要销毁一个对象时只需要消除对它的引用就可以了,JVM会在GC时把它所占用的内存自动交还给系统。但如果我们在程序中错误的持有了多余的引用,超过了其正常的使用范围,就会导致该对象以及其引用的对象无法及时释放。最极端的情况是这个对象被声明为static或者被应用的Application引用,导致其存活的时间与整个应用的生命周期相同,这样便产生了内存泄漏。
网上对于内存泄露原理的介绍比较多,这里不做过多介绍,放一篇帖讲解的比较全面,供大家参考:内存泄漏全解析,从此拒绝ANR,让OOM远离你的身边,跟内存泄漏say byebye。结合作者观点,稍做总结,括号中是正确的做法:
- 单例模式中错误引用Activity作为Context(应该使用ApplicationContext)
- 使用非静态类Handler并且未及时移除message(使用静态Handler与WeakReference,退出Activity时及时remove messages)
- 使用匿名类/非静态内部类持有外部对象引用(尽量使用private static class作为内部类)
- 集合对象未及时清理
- WebView引发的内存泄露(退出Activity时及时销毁WebView)
- ListView的Adapter中创建ItemView时未使用缓存(使用convertView和静态Holder缓存)
- 对象的注册与反注册没有成对出现造成的内存泄露(注册与反注册一定要成对出现)
工具
虽然有上面这些原则,但是开发过程中我们更多需要的是能利用工具立刻定位出问题出现的,而不是去逐行审查代码。下面介绍一些用来快速排查内存泄露问题的工具,并演示如何结合AndroidStudio开发环境使用这工具。
LeakCanary
LeakCanary是一个专门用来检测内存泄露的组件,只需要简单的配置就可以集成到项目中,在程序运行时能够自动dump内存并且生成报告。
首先我们在build.gradle中添加如下依赖:
dependencies {
debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5'
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
}
然后在Application的onCreate()方法中对LeakCanary进行初始化:
public class LeakApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
if (LeakCanary.isInAnalyzerProcess(this)) {
return;
}
LeakCanary.install(this);
}
}
这样就完成了LeakCanary的配置。接下来我们模拟一个常见的错误,用来演示LeakCanary的用法。首先写一个错误实现的单例模式:
public class SingletonBad {
private static final String TAG = "Singleton";
private static SingletonBad instance;
private final Context mContext;
private SingletonBad(Context mContext) {
this.mContext = mContext;
}
public static SingletonBad getInstance(Context context) {
if (instance == null) {
synchronized (SingletonBad.class) {
if (instance == null) {
instance = new SingletonBad(context);
}
}
}
return instance;
}
public void doSomeThing() {
Log.d(TAG, "do something");
}
}
在MainActivity中有一个Button,点击后打开SecondActivity:
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="startSecond"
android:text="start"/>
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void startSecond(View view) {
Intent intent = new Intent(this, SecondActivity.class);
startActivity(intent);
}
}
SecondActivity中简单调用了SingletonBad的doSomething()方法。
public class SecondActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_second);
SingletonBad.getInstance(this).doSomeThing();
}
}
好了,我们启动应用进入MainActivity,点击start按钮进入到SecondActivity,再退回到MainActivity。此时LeakCanary开始工作了,弹出了一个Dump memory的提示如下图,程序会稍微卡顿。
Dump分析完成后,LeakCanary发现SecondActivity被泄露了,会在通知栏中给出提示如下图:
点击这条通知,跳转到分析报告:
怎么样,报告是不是很漂亮!我们可以清楚的看到,SingletonBad类中的静态变量instance持有了SecondActivity作为mContext,导致了内存泄露。有了LeakCanary帮我们做自动分析,内存泄露一目了然,省去了很多工作。然而LeakCanary并不是万能的,对于Activity的泄露基本都能及时发现,但是其他比较复杂的情况并不一定能分析出来,这个时候我们就需要用到MAT去做更具体的分析了。
MAT
MAT即Memory Analyzer,是一款专门用来分析内存对象的工具,可以集成到Eclipse中也能单独使用。由于现在Android开发基本都使用AndroidStudio,这里我们就使用了一个独立的版本。
首先我们先介绍一下AndroidStudio中的AndroidMonitor,一共有4项内容,分别是内存占用,cpu使用率,网络和GPU,我们今天关注的重点是第一项内存。红色框里的几个按钮我们需要用到,分别用来触发GC、Dump堆栈和跟踪内存分配。
我们继续使用上面的例子,为了让问题看起来更明显更接近真实情况,我们为SecondActivity添加一张背景图片。首先从MainActivity跳转到SecondActivity,然后按back键退回,这个时候的内存走势如下图,此时点击GC按钮清除掉没有引用的类,发现退回MainActivity后内存并没有下降。
然后点击Dump按钮,稍后便会生成一个dump文件,可以看到内存中保留的对象及其对应的count,size等,可以在此做简单的分析。
在内存中对象比较多的时候,AndroidStudio中做分析已经有些力不从心,这个时候就要我们的主角MAT登场了。
首先把前面生成的dump文件导出为一个标准的.hprof文件:在AndroidStudio的左侧边栏选择Captures选项,在Heap Snapshot下找到刚才生成的.hprof文件,右键选择“Export to standard .hprof”选项,选择保存位置即可。
然后我们打开MAT,File->Open->选择刚才保存的文件,看到如下界面:
上图中的饼图可以看出当前内存占用比例,其中Bitmap占用了28.6MB,明显是有问题的。我们点一下“Leak Suspects”按钮,生成下图:
再点击Details:
好了,现在我们能看到,是因为这个Bitmap被SecondActivity所引用,而SecondActivity又被SingletonBad引用导致,得出的结果跟我们的预期是一致的。
当然MAT中还有很多更强大的工具,比如我们可以点击“Dominator Tree”这个按钮,按照Retained Heap排序后,一个Bitmap对象排到了第一个位置。我们选中它,然后右键选中“Merge Shortest Paths to GC Roots” -> “exclude weak references”。
展开后便得到了下图:
对于前面这个操作稍作解释。我们知道JVM判断一个对象需要被GC的依据是这个对象没有路径可以通过强引用到达GC Root,通过上面这个操作我们去除了所有的weak reference,剩下的基本就是强引用了。注意最上面的SingletonBad对象左边的有一个黄色的点,表示这个对象是能够到达GC Root的,因此根据这个引用链我们可以看到,SecondActivity的背景图片最终被SingletonBad的instance引用,导致无法被回收,这便是内存泄露的根源所在。
如果对于前面的GC Root不理解的,可以去看郭霖大神这篇帖子,里面有比较详细的讲解。
本篇到这里就结束了,内存泄露是一种比较常见又不容易解决的问题,文中的例子都比较简单,实际遇到时情况可能会比这些复杂很多,但是有了这些工具的帮助,相信我们能更快的定位和处理这些问题。