1、概述
Android开发中经常出现内存泄漏问题,本文将从发现问题,确定问题,分析问题,解决问题四个方面解决内存泄漏问题。
内存泄漏:
java是有垃圾回收机制的,JVM会派出一些回收线程不定时地回收那些不再需要的内存空间,回收的不是对象本身,而是对象占据的内存空间。
java凭引用来和对象进行关联,通过引用来操作对象。如果一个对象没有与任何引用关联,那么这个对象也就不太可能被使用到了,回收器便是把无任何引用的对象作为目标,回收了它们占据的内存空间。
判断对象有无引用:
上图中“GC Roots“为垃圾回收器对象,垃圾回收检查的起点。“箭头“代表对象之间的引用关系。“圆“代表内存中对象。
系统设置了一系列的“GC Roots“对象作为索引起点,如果一个对象与起点对象之间均为可达路径,那么这个不可达的对象就会成为回收对象。
内存泄漏产生原因:
虽然垃圾回收器会帮我们回收大部分无用的内存空间,但是对于还保存着引用,但逻辑上已经不再用到的对象,垃圾回收器就不会再回收他们。这些对象积累在内存中,直到程序结束,所以导致内存泄漏。
上图中E对F保持引用,但F已经不再用到了,垃圾回收时,F不能被回收,导致内存泄漏。
2、发现问题
(1)连续打开应用或界面后,界面卡顿,动画不流畅;
(2)操作过程中,Logcat频繁输出GC日志。
通过上述两个方面可初步发现内存泄漏问题。
3、确定问题
(1)反复操作观察内存变化
频繁打印GC日志,说明系统频繁触发GC来释放内存,内存泄漏常见变现为程序使用时间越长,内存占用越多。那我们通过反复操作应用,比如反复点开/关闭页面,观察内存变化状况是否一点点上涨,可以粗略地判断是否有内存泄漏。
通过DDMS的heap工具查看应用内存的使用情况
- 选择Tools -> Android -> Android Device Monitor;
- 选择需要测试进程;
- 点击update heap按钮,DDMS通知应用收集信息;
- 点击Cause GC,load内存信息;
- 连续打开应用,或反复打开关闭页面,观察各对象的Total Size变化,验证是否有内存泄漏;
通过Android Monitor查看
通过Android Monitor我们可以实时看到APP在运行时的Memory、CPU、GPU和Network的消耗情况。
4、分析问题
(1)DDMS Dump HPROF file
1、导出.prof文件
通过3中的DDMS操作步骤找到需要调试的APP进程,点击“update heap“ 按钮,可以先主动发出一次GC,待内存占用数据稍微稳定下来后,点击“DDMS Dump HPROF file“导出.prof文件。
2、分析.prof文件
Android studio能直接打开.prof文件。打开Analyzer Tasks面板,点击run分析。
分析完成后, 如果有内存泄漏在Analysis Results面板中会出现Leaked Activities目录,里面就是泄漏的Activity对象。找出这个Activity分析泄漏原因。
(2)分析内存泄漏原因工具:MAT(Memory Analyzer Tool)
打开MAT工具,如果没有,请下载。
MAT是用来分析Java程序的hprof文件,与Android导出的hprof有格式区别,因此需要先进行转换。
在Android sdk的platform-tools有hprof-conv工具进行格式转化:
hprof-conv 源文件 输出文件
hprof-conv old.hprof new.hprof
格式转化后,用MAT工具打开new.hprof文件,
正常打开后如下图所示:
Leak Suspects视图展示了app内存占用的比例,浅色是空闲的内存,其他是内存占用的空间。每块内存对应的问题也都列在下面。点开每个Problem Suspect下的details,可以看到有哪些类的实例占用了内存和占用大小等信息
我们就可以查看当前内存中存在的对象了,由于内存泄漏一般发生在Activity中,因此只需要查找Activity即可。
点击下图中标记的QQL图标 输入 select * from instanceof android.app.Activity 查找Activity的相关信息,点击红色叹号执行:
如上图所示, 其中内存中还存在 6个MainActivity实例,但是我们是想要全部退出的,这表明出现了内存泄漏。
其中有 Shallow size 和 Retained Size两个属性。
Shallow Size
对象自身占用的内存大小,不包括它引用的对象。针对非数组类型的对象,它的大小就是对象与它所有的成员变量大小的总和。
当然这里面还会包括一些java语言特性的数据存储单元。针对数组类型的对象,它的大小是数组元素对象的大小总和。
Retained Size
Retained Size=当前对象大小+当前对象可直接或间接引用到的对象的大小总和。(间接引用的含义:A->B->C, C就是间接引用)
不过,释放的时候还要排除被GC Roots直接或间接引用的对象。他们暂时不会被被当做Garbage。
右击一个MainActivity,选择 with all references :
打开下图:
this0引用这个Activity,this0表示内部类,也就是一个内部类引用了Activity 而 this$0又被 target引用 target是一个线程,原因找到了,内存泄漏的原因就是 Activity被内部类引用而内部类又被线程使用因此无法释放,查看此类代码。
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_second);
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
Thread.sleep(6*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
new Thread(runnable).start();
}
}
在MainActivity中存在Runnable 内部类对象,然后又被线程使用,而线程要执行6秒,因此 MainActivity对象被引用无法释放,导致了内存溢出。
要解决这种的内存溢出,要及时在Activity退出时结束线程,或者良好的控制线程执行的时间即可。
这样我们就找出了这个程序中的内存溢出。
(3)LeakCanary工具内存检测
LeakCanary是square公司推出的一款简单粗暴的检测内存泄漏的工具。
LeakCanary会检测应用的内存回收情况,如果发现有垃圾对象没有被回收,就会去分析当前的内存快照,也就是上边MAT用到的.hprof文件,找到对象的引用链,并显示在页面上。这款插件的好处就是,可以在手机端直接查看内存泄露的地方,可以辅助我们检测内存泄露。
详细LeakCanary,请查看这里,以后有时间我会再来单独分析这块。
5、内存泄漏的情景和解决办法
(1)使用关于application的context来替代和activity相关的context
这是一个很隐晦的内存泄漏的情况。有一种简单的方法来避免context相关的内存泄漏。最显著地一个是避免context逃出他自己的范围之外。使用Application context。这个context的生存周期和你的应用的生存周期一样长,而不是取决于activity的生存周期。如果你想保持一个长期生存的对象,并且这个对象需要一个context,记得使用application对象。你可以通过调用 Context.getApplicationContext() or Activity.getApplication()来获得。
(2)集合中对象没清理造成的内存泄漏
我们通常把一些对象的引用加入到了集合中,当我们不需要该对象时,并没有把它的引用从集合中清理掉,这样这个集合就会越来越大。如果这个集合是static的话,那情况就更严重了。
(3)构造Adapter时,没有使用缓存的convertView
以构造ListView的BaseAdapter为例,在BaseAdapter中提供了方法:
public View getView(int position, ViewconvertView, ViewGroup parent)
来向ListView提供每一个item所需要的view对象。初始时ListView会从BaseAdapter中根据当前的屏幕布局实例化一定数量的 view对象,同时ListView会将这些view对象缓存起来。当向上滚动ListView时,原先位于最上面的list item的view对象会被回收,然后被用来构造新出现的最下面的list item。这个构造过程就是由getView()方法完成的,getView()的第二个形参View convertView就是被缓存起来的list item的view对象(初始化时缓存中没有view对象则convertView是null)。由此可以看出,如果我们不去使用 convertView,而是每次都在getView()中重新实例化一个View对象的话,即浪费资源也浪费时间,也会使得内存占用越来越大。
错误代码:
public View getView(int position, ViewconvertView, ViewGroup parent) {
View view = new Xxx(...);
... ...
return view;
}
修正代码:
public View getView(int position, ViewconvertView, ViewGroup parent) {
View view = null;
if (convertView != null) {
view = convertView;
populate(view, getItem(position));
...
} else {
view = new Xxx(...);
...
}
return view;
}
(4)资源对象没关闭造成的内存泄漏
资源性对象比如(Cursor,File文件等)往往都用了一些缓冲,我们在不使用的时候,应该及时关闭它们,以便它们的缓冲及时回收内存。它们的缓冲不仅存在于 java虚拟机内,还存在于java虚拟机外。如果我们仅仅是把它的引用设置为null,而不关闭它们,往往会造成内存泄漏。因为有些资源性对象,比如 SQLiteCursor(在析构函数finalize(),如果我们没有关闭它,它自己会调close()关闭),如果我们没有关闭它,系统在回收它时也会关闭它,但是这样的效率太低了。因此对于资源性对象在不使用的时候,应该调用它的close()函数,将其关闭掉,然后才置为null.在我们的程序退出时一定要确保我们的资源性对象已经关闭。
程序中经常会进行查询数据库的操作,但是经常会有使用完毕Cursor后没有关闭的情况。如果我们的查询结果集比较小,对内存的消耗不容易被发现,只有在常时间大量操作的情况下才会复现内存问题,这样就会给以后的测试和问题排查带来困难和风险。
(5)注册没取消造成的内存泄漏
一些Android程序可能引用我们的Anroid程序的对象(比如注册机制)。即使我们的Android程序已经结束了,但是别的引用程序仍然还有对我们的Android程序的某个对象的引用,泄漏的内存依然不能被垃圾回收。调用registerReceiver后未调用unregisterReceiver。
(6)static变量引起的内存泄漏
因为static变量的生命周期是在类加载时开始类卸载时结束,也就是说static变量是在程序进程死亡时才释放,如果在static变量中引用了Activity 那么这个Activity由于被引用,便会随static变量的生命周期一样,一直无法被释放,造成内存泄漏。
解决办法:
在Activity被静态变量引用时,使用 getApplicationContext 因为Application生命周期从程序开始到结束,和static变量的一样。
(7)线程造成的内存泄漏
类似于上述例子中的情况,线程执行时间很长,及时Activity跳出还会执行,因为线程或者Runnable是Acticvity内部类,因此握有Activity的实例(因为创建内部类必须依靠外部类),因此造成Activity无法释放。
AsyncTask有线程池,问题更严重。
解决办法:
1.合理安排线程执行的时间,控制线程在Activity结束前结束。
2.将内部类改为静态内部类,并使用弱引用WeakReference来保存Activity实例因为弱引用只要GC发现了就会回收它 ,因此可尽快回收
(8)BitMap占用过多内存
bitmap的解析需要占用内存,但是内存只提供8M的空间给BitMap,如果图片过多,并且没有及时 recycle bitmap 那么就会造成内存溢出。
解决办法:
及时recycle 压缩图片之后加载图片
(9)Handler的使用造成的内存泄漏
由于在Handler的使用中,handler会发送message对象到 MessageQueue中 然后 Looper会轮询MessageQueue 然后取出Message执行,但是如果一个Message长时间没被取出执行,那么由于 Message中有 Handler的引用,而 Handler 一般来说也是内部类对象,Message引用 Handler ,Handler引用 Activity 这样 使得 Activity无法回收。
解决办法:
依旧使用 静态内部类+弱引用的方式可解决