内存泄漏的原因
一直以来以为只有C/C++才存在内存泄漏的问题,没想到拥有内存回收机制的Java也可能出现内存泄漏。C/C++存在指针的概念,程序中需要使用指针变量时,就从内存中开辟一块区域,并把该区域的首地址赋值给一个指针,这样程序才可操作该指针指向的内存区域。因为C/C++设计上的原因,手工分配的内存,也要手工来释放,如malloc/free是C中分配/释放内存的运算符,而new/delete则是C++中新增的分配/释放内存的运算符。
Java设计之初就是能够自动回收内存,可是有些时候因为某些因素,内存回收机制并不会都奏效。情况之一是调用了非java接口,比如调用了jni接口,jni中C/C++的内存就要手工回收;情况之二是调用了外部服务,使用完毕就得手工通知外部服务去回收;情况之三是异步处理,实时的内存回收显然顾不上异步处理的任务。
内存泄漏的场景
在Android开发中,内存泄漏可能发生在如下几个场景:
1、查询操作后,没有关闭游标Cursor;
2、刷新适配器Adapter时,没有重用convertView对象;
3、Bitmap对象使用完毕,没有调用recycle方法回收内存;
4、给系统服务注册了监听器,却没有及时注销;
5、Activity引用了耗时对象,造成页面关闭时无法释放被引用的对象;
内存泄漏的发现
检查app是否发生内存泄漏,有三个办法:
1、在代码中定期检查当前进程占用的内存大小。
2、使用ADT自带DDMS插件的heap工具,去发现是否有内存溢出。
如果在Heap的Tab中发现提示“DDMS Heap updates are NOT ENABLED for this client”,则在菜单“Preferences”——“Android”——“DDMS”中打开“Thread updates enabled by default”。如果还不行,则在DDMS的devices窗口中,选择调试的进程,点击上方的堆栈图标(Update Heap)。
3、通过内存分析工具MAT(Memory Analyzer Tool,一个Eclipse插件),找到内存泄露的对象。devices窗口上方堆栈图标右侧有个向下箭头的图标(DUMP HPROF file),这是heap工具生成的app内存统计文件,MAT读取该文件后会给出方便阅读的信息,配合它的查找、对比功能,就可以定位内存泄漏的原因。
注意MAT依赖于插件BIRT Chart Engine,得先安装这个BCE插件,然后才能安装MAT插件。
内存泄漏的预防
关闭游标
游标Cursor不光用于SQLite数据库,也可用于ContentProvider的ContentResolver对象,以及DownloadManager查询下载任务,相关介绍参见《 Android开发笔记(三十一)SQLite游标及其数据结构》。
预防游标产生的内存泄漏,可在每次查询操作完成后,都调用Cursor的close方法来关闭游标。
重用适配
APP往ListView或GridView中填充数据,都是通过适配器BaseAdapter的getView方法展示列表元素。列表元素较多的时候,Android只加载屏幕上可见的元素,其他元素只有在滑动屏幕使其位于可视区域内,才会即时加载并显示。当列表元素多次处于“展示->隐藏->展示->隐藏……”时,就有必要重用每个元素的视图,如果不重用,那么每次展示可视元素都得重新分配视图对象(从系统服务LAYOUT_INFLATER_SERVICE获取),这便产生了内存浪费。
不过即使不重用适配,也仅仅造成当前页面的内存浪费;一旦用户离开该页面,原列表页面的内存就统统回收。所以严格来说,这种情况不是真正意义上的内存泄漏,只是内存管理不善造成的内存浪费。适配器的相关介绍参见《 Android开发笔记(三十八)列表类视图》。
重用适配可先判断convertView,如果该对象为空,则分配视图对象,并调用setTag方法保存视图持有者;如果该对象非空,则调用getTag方法获取视图持有者。下面是重用的代码示例:
ViewHolder holder = null;
if (convertView == null) {
holder = new ViewHolder();
convertView = mInflater.inflate(R.layout.list_title, null);
holder.tv_seq = (TextView) convertView.findViewById(R.id.tv_seq);
holder.iv_title = (ImageView) convertView.findViewById(R.id.iv_title);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
回收图像
Android虽然定义了Bitmap类,但是读取图像数据并非java代码完成。查看sdk源码,在BitmapFactory类中一路跟踪到nativeDecodeStream函数,其实是个native方法,也就是说该方法来自jni接口。既然Bitmap的数据实际来自于C/C++代码,那么确实就得手工释放C/C++的内存资源了。查看Bitmap类的源码,回收方法recycle用到的nativeRecycle函数,其实也是个native方法,同样来自于jni接口。jni的介绍参见《 Android开发笔记(六十九)JNI实战》。
实测发现,即使recycle也存在内存泄漏,只是没recycle的话泄露有十倍。比如recycle之后,内存仍泄漏40K;但是如果没有recycle,那么内存泄漏有400K。另外,与图像有关的类实例,最好用完也要释放资源。例如Camera对象用完需release并置空,Canvas对象用完也要置空。
注销监听
Android中有许多监听器,不过注册到系统服务中的监听器并不多,TelephonyManager可算是其中一个(其对象来自于系统服务TELEPHONY_SERVICE)。TelephonyManager的listen方法,便是用来向系统的电话服务注册各种手机事件。手机相关事件的说明参见《 Android开发笔记(四十六)手机相关事件》,这里就不罗唆了。
预防监听器的内存泄漏,在Activity页面退出时,要及时注销TelephonyManager的监听器,具体做法是给TelephonyManager对象注册一个LISTEN_NONE的空监听器。代码示例如下:
@Override
protected void onStop() {
if (mType == 1) {
if (mCellInfoListener != null) {
mTelMgr.listen(mCellInfoListener, PhoneStateListener.LISTEN_NONE);
mCellInfoListener = null;
}
if (mSignalStrengthListener != null) {
mTelMgr.listen(mSignalStrengthListener, PhoneStateListener.LISTEN_NONE);
mSignalStrengthListener = null;
}
if (mCellLocationListener != null) {
mTelMgr.listen(mCellLocationListener, PhoneStateListener.LISTEN_NONE);
mCellLocationListener = null;
}
}
super.onStop();
}
释放引用
开发中编写Handler类时,ADT时常提示加上“@SuppressLint("HandlerLeak")”的标记,意味着这里可能发生内存泄漏。因为Handler类总是处理异步任务,每当它postDelayed一个任务时,依据postDelayed的间隔都得等待一段时间,倘若页面在这期间退出,就导致异步任务Runnable持有的引用无法回收,Runnable通常持有Activity的引用,造成Activity都无法回收了。
上面描述可能不好理解,确实也不容易解释清楚,那还是直接跳过繁琐的概念,讲讲如何解决HandlerLeak的问题。下面是预防此类内存泄漏的三个方法:
1、如果异步任务是由Handler对象的postDelayed方法发起,那么可用对应的removeCallbacks方法回收之,把消息对象从消息队列移除就行了。
但若线程是由start方法启动,则不适合使用该方法,但我们可尽量避免start方式启动。
2、按Android官方的推荐做法,可把Handler类改为静态类(static),同时Handler内部使用WeakReference关键字来持有目标的引用。
之所以使用静态类,是因为静态类不持有目标的引用,不会影响自动回收机制。但是不持有目标的引用,Handler内部也就无法操作Activity上面的控件(因为不持有Activity的引用)。为解决该问题,在构造Handler类时就得初始化目标的弱引用,弱引用不同于前面的引用(强引用),弱引用相当于一个指针,指针指向的地址随时可以回收,这又带来一个新问题,就是弱引用指向的对象可能是空的。幸好这个问题好解决,Handler内部使用目标前先判断以下弱引用是否为空就行了。
3、把Handler对象作为APP的全局变量,比如把Handler对象放入Application的声明中,这样只要app在运行,Handler对象一直都存在。
既然避免了为Handler分配内存,也就间接避免了内存泄漏。Application的介绍参见《 Android开发笔记(二十八)利用Application实现内存读写》。
下面是释放引用的代码示例:
import java.lang.ref.WeakReference;
import com.example.exmleak.util.ProcessUtil;
import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.widget.TextView;
public class HandlerActivity extends Activity {
private final static String TAG = "HandlerActivity";
private TextView tv_memory;
private int mType;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_handler);
tv_memory = (TextView) findViewById(R.id.tv_memory);
Bundle bundle = getIntent().getExtras();
mType = bundle.getInt("type");
}
@Override
protected void onStart() {
if (mType == 0) { //引用未释放
mHandler.postDelayed(mRefresh, 10000);
} else if (mType == 1) { //引用有释放
mHandler.postDelayed(mRefresh, 10000);
} else if (mType == 2) { //静态弱引用
mMyHandler.postDelayed(mRunnableRefresh, 10000);
} else if (mType == 3) { //线程弱引用
mMyHandler.postDelayed(mThreadRefresh, 10000);
}
super.onStart();
}
@Override
protected void onStop() {
if (mType == 1) {
mHandler.removeCallbacks(mRefresh);
}
super.onStop();
}
private Handler mHandler = new Handler();
private Runnable mRefresh = new Runnable() {
@Override
public void run() {
String desc = ProcessUtil.getRunningAppProcessInfo(HandlerActivity.this);
tv_memory.setText(desc);
mHandler.postDelayed(this, 3000);
}
};
private Handler mMyHandler = new MyHandler(this);
private static class MyHandler extends Handler {
public static WeakReference<HandlerActivity> mActivity;
public MyHandler(HandlerActivity activity) {
mActivity = new WeakReference<HandlerActivity>(activity);
}
@Override
public void handleMessage(Message msg) {
HandlerActivity act = mActivity.get();
if (act != null) {
String desc = ProcessUtil.getRunningAppProcessInfo(act);
act.tv_memory.setText(desc);
}
}
}
private static Runnable mRunnableRefresh = new Runnable() {
@Override
public void run() {
HandlerActivity act = MyHandler.mActivity.get();
if (act != null) {
act.mMyHandler.sendEmptyMessage(0);
}
}
};
private static Thread mThreadRefresh = new Thread() {
@Override
public void run() {
HandlerActivity act = MyHandler.mActivity.get();
if (act != null) {
act.mMyHandler.sendEmptyMessage(0);
}
}
};
}
点此查看Android开发笔记的完整目录