Android内存泄漏分析与常见案例
1、什么是内存泄漏
内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
以上是官方针对内存泄漏的说法。说的通俗一点,应该释放的内存没有被正常释放,就是内存泄漏。当我们程序出现内存泄漏后,轻则影响运行速度,重则内存溢出,造成程序崩溃。
内存溢出(Out Of Memory,简称OOM)是指应用系统中存在无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于能提供的最大内存
所以,当我们程序出现疑似内存泄漏的时候,千万要引起重视。
2、如何识别内存泄漏
找出程序中有内存泄漏风险的代码,我们可以通过人工去review代码,找出内存泄漏并解决掉;也可以借助adb命令去观察相关状态;也可以借助AndroidStudio提供的Profiler工具来检测。总体来说,办法总比困难多,以下简单列举几种
使用adb命令
在窗口中输入命令:adb shell dumpsys meminfo 包名,即可查看当前进程的使用情况。
还可以使用其他命令查看
显示全部系统进程:adb shell ps
筛选包含特定关键字进程:adb shell ps | grep [keyword]
查看系统内存使用情况:adb shell cat /proc/meminfo
使用Profiler工具
Android Studio 3.0 及更高版本中的 Android Profiler 取代了 Android Monitor工具。Android Profiler 工具可提供实时数据,帮助您了解应用的 CPU、内存、网络和电池资源使用情况。
在AndroidStudio中提供Profiler工具,供开发者进行性能分析。内存的分析仅仅是其中的一部分。当我们使用固定手顺操作程序时,在Profiler工具中监测内存,内存有序增长且无法被释放,大概率就是存在内存泄漏了。接下来介绍下Profiler在内存泄漏分析中的简单使用。
需要注意的是,如果调试的设备是 Android 7.1 或更低版本时,在使用的时候会有一定的限制,详细可以查看官网,这里不再过多描述。Android Profiler
设想一个简单的场景,程序中有两个Activity,A和B,我们页面跳转顺序是A—B—A,此手顺下,借助Profiler工具观察页面B是否存在内存泄漏风险以及正常回收。
按照图片中的步骤进行操作:
- 添加我们要监测的进程
- 选中内存模块
- 操作我们要测试的手顺,也就是A—B—A
- 点击启动GC回收
- 选中该选项
- 在启动GC回收后停滞几秒,就可以点击生成报告。
以上为生成的报告结果,按照标注的顺序进行观察
1:将设置项调整为当前包名下的类
2:观察黄色叹号的区域,如果有数值,说明存在内存泄漏风险的数量
3:选中存在风险的类,观察风险类型。
此截图中提示的风险为:MainActivity1中有一个listener被一个单例持有。我们找到对应的类进行修改即可。
在实际开发的情况下,还存在这一种可能,我们发现了内存泄漏,但是Profiler工具并没有提示出现内存泄漏的风险。这种情况下,我们可以观察对象实例数量来判断是否有内存泄漏的存在,还是按照上图中举例。
我们的页面跳转顺序是:A(MainActivity)—B(MainActivity1)—A,按照操作手顺,我们的程序应该不会持有B的对象,正常情况应该被释放掉,但是在上面的截图中,B对象的实例依然持有,Allocations字段显示的是1,所以持有的B对象的数量为1。理论上持有数量应该是0,实际为1,也可判定出此类中存在内存泄漏的可能,导致此对象未被回收。
3、常见内存泄漏分析
在查找内存泄漏时,也并非无迹可寻,以下总结几点容易造成内存泄漏的原因
- 需要被释放的资源被更长生命周期的对象持有
- 非静态内部类持有外部引用
- 资源使用后未释放
以上三点是个人总结出容易引起内存泄漏的几种情况,接下来举例说明
需要被释放的资源被更长生命周期的对象持有
需要被释放的对象,被拥有更长生命周期的对象持有时,会造成该对象无法被释放,造成内存泄漏。常见引起内存泄漏的情况有:Context、单例、注册接口,static变量等。
观察看以下代码,看看能找出几处内存泄漏
public class MainActivity1 extends AppCompatActivity {
public static Context mContext = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main1);
Button btn_back = findViewById(R.id.btn_back);
btn_back.setOnClickListener(view -> finish());
mContext = this;
test1();
test2();
}
private void test1() {
Singleton.getInstance().registerListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
}
});
}
private void test2() {
Singleton.getInstance().setContext(this);
}
}
以上代码中,存在三处内存泄漏
- mContext为静态变量,在页面销毁后无法被正常释放,所以MainActivity1的对象依旧被持有,造成内存泄漏
- test1方法中向单例中注册了一个接口,页面销毁时没有解绑,造成内存泄漏
- test2方法中向单例中传递了Context,对象被更长周期的单例持有,造成内存泄漏。
解决方案就是,在页面销毁的时候释放它们
@Override
protected void onDestroy() {
super.onDestroy();
mContext = null;
Singleton.getInstance().unRegisterListener();
Singleton.getInstance().setContext(null);
}
这里拓展个知识点,为什么static修饰的变量生命周期会更长?
首先要了解static关键字的作用。static关键字可以用在变量、方法、代码块和嵌套类上。static关键字修饰的部分属于类,而不是对象,这一点很重要。正因为它属于类而不是对象,所以我们可以直接访问数据而不需要new。当我们启动APP时,系统会自动创建一个进程,进程启动的时候,类被加载,静态变量被分配内存。在进程结束的时候,静态变量在类被销毁。也就是说静态变量的生命周期,伴随着程序的开始与结束,所以static修饰的变量生命周期会更长。
非静态内部类持有外部引用
先来看一段代码
public class MainActivity2 extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main2);
Button btn_back = findViewById(R.id.btn_back);
btn_back.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
mHandler.sendMessageDelayed(Message.obtain(), 60 * 1000);
finish();
}
});
}
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
//处理UI显示
test1();
}
};
private void test1() {
}
}
这是一段典型的Handler内存泄漏的代码。页面销毁后,在存在这一个延迟1min执行的handler,而此Handler又默认持有外部对象的引用。所以即使页面已经销毁了,但是对象并未被释放,造成内存泄漏。
相似的情景容易造成内存泄漏的还有AsyncTask,非静态内部类等。原因与此Handler泄漏大同小异,都是因非静态内部类默认持有外部引用导致。可将上述代码修改一下
public class MainActivity2 extends AppCompatActivity {
private MyHandler myHandler = new MyHandler(this);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main2);
Button btn_back = findViewById(R.id.btn_back);
btn_back.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
myHandler.sendMessageDelayed(Message.obtain(), 60 * 1000);
finish();
}
});
}
private void test1() {
}
private static class MyHandler extends Handler {
WeakReference<Context> weakReference;
public MyHandler(Context context) {
weakReference = new WeakReference<>(context);
}
@Override
public void handleMessage(Message msg) {
//处理UI显示
MainActivity2 activity = (MainActivity2) weakReference.get();
if (activity != null) {
activity.test1();
}
}
}
@Override
protected void onDestroy() {
super.onDestroy();
myHandler.removeCallbacksAndMessages(null);
}
解决此问题,总共分三部,将Handler改成静态,使用弱引用持有外部引用,页面销毁时取消相关操作。AsyncTask的内存泄漏处理也是同理。
上一小节我们了解了static的部分知识,我们再思考一个问题,为什么非静态内部类会持有外部引用?
通过将apk反编译看一看代码编译后是什么样子,就了然了。
以上截取了部分反编译的效果,我们可以看到,在mHandler实例中调用test1方法,编译过程中会出现外部对象(MainActivity.this)。这下应该可以更容易理解此类内存泄漏的形成原因了。
资源使用未释放造成的内存泄漏
此类就比较好理解,类被销毁时,使用的资源未及时释放就容易造成内存泄漏,常见的有:动画、BraodcastReceiver、ContentObserver、File,Cursor,Stream,Bitmap、VelocityTracker等。还有一些第三方库的使用,不及时释放也会存在风险。相关使用注意事项这里不再过多描述。
4、如何规避内存泄漏风险
良好的编码规范与习惯,可以让我们省去很多后顾之忧。同时我们也可以借助工具来帮助我们进行相关监测。我们可以添加AndroidStudio插件来实时监测我们的编码规范;项目中可以集成LeakCanary来监测内存泄漏等。
总体来说,办法总比困难多。内存泄漏并不是什么高不可攀的东西。很多人在面对内存泄漏时无从下手,希望本文会对此有所帮助。