内存泄漏的原因

一直以来以为只有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的空监听器。代码示例如下:

 @Overrideprotected 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();}

另一个注销监听的例子,是页面退出时注销LocationManager的定位监听器,代码示例如下:

 @Overridepublic void onStop() {if (mLocationManager!=null && mLocationListener!=null) {mLocationManager.removeUpdates(mLocationListener);}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;@Overrideprotected 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");}@Overrideprotected 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();}@Overrideprotected void onStop() {if (mType == 1) {mHandler.removeCallbacks(mRefresh);}super.onStop();}private Handler mHandler = new Handler();private Runnable mRefresh = new Runnable() {@Overridepublic 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);}@Overridepublic 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() {@Overridepublic void run() {HandlerActivity act = MyHandler.mActivity.get();if (act != null) {act.mMyHandler.sendEmptyMessage(0);}}};private static Thread mThreadRefresh = new Thread() {@Overridepublic void run() {HandlerActivity act = MyHandler.mActivity.get();if (act != null) {act.mMyHandler.sendEmptyMessage(0);}}};}

点击下载本文用到的处理内存泄漏的代码例子

点此查看Android开发笔记的完整目录

Android开发笔记(七十五)内存泄漏的处理相关推荐

  1. Android开发笔记(十五)淡入淡出动画TransitionDrawable

    说到淡入淡出动画,可能大家会想到补间动画里面的AlphaAnimation,不过这个深浅动画只能对透明度做渐变效果,也就是只能对一个图形做深浅的颜色变换.如果我们想要从A图片逐渐变为B图片,也就是要实 ...

  2. Android开发笔记(九十五)自定义Drawable

    Drawable Bitmap是Android对图像的定义描述,而Drawable则是对图像的展现描述,在View视图中显示图像都是通过Drawable来实现的.其中有关Bitmap的介绍参见< ...

  3. 5 个 Android 开发中比较常见的内存泄漏问题及解决办法

    Android开发中,内存泄漏是比较常见的问题,有过一些Android编程经历的童鞋应该都遇到过,但为什么会出现内存泄漏呢?内存泄漏又有什么影响呢? 在Android程序开发中,当一个对象已经不需要再 ...

  4. Android开发笔记(一百五十七)使用OpenGL实现翻书动画

    上一篇文章介绍了如何通过纹理渲染绘制地球仪,当然OpenGL的三维图形处理能力是很强大的,只要善于利用OpenGL,就能很方便地虚拟各种现实生活中的动画效果.本文再来谈谈使用OpenGL实现浏览电子书 ...

  5. Android开发笔记(一百五十四)OpenGL的画笔工具GL10

    上一篇文章介绍了OpenGL绘制三维图形的流程,其实没有传说中的那么玄乎,只要放平常心把它当作一个普通控件就好了,接下来继续介绍OpenGL具体的绘图操作,这项工作得靠三维图形的画笔GL10来完成了. ...

  6. Android开发笔记(一百五十九)Android7.0的分屏模式

    现在的手机屏幕越来越大,使得在屏幕上同时开多个窗口不再奢侈,因此Android从7.0开始顺势推出了分屏功能,也被称作多窗口模式.比如把竖长的手机屏幕分成上下两个窗口,一边在上面的窗口中观看电影,一边 ...

  7. Android开发笔记(一百五十八)运行时动态授权管理

    App开发过程中,涉及到硬件设备的操作,比如拍照.录音.定位等等,都要在AndroidManifest.xml中声明相关的权限.可是Android系统为了防止某些App滥用权限,从而允许用户在系统设置 ...

  8. Android开发笔记(一百五十六)通过渲染纹理展示地球仪

    上一篇文章介绍了如何使用GL10描绘三维物体的线段框架,后面给出的立方体和球体效果图,虽然看起来具备立体的轮廓,可离真实的物体还差得远.因为现实生活中的物体不仅仅有个骨架,还有花纹有光泽(比如衣服), ...

  9. Android开发笔记(一百五十五)利用GL10描绘点、线、面

    上一篇文章介绍了GL10的常用方法,包括如何设置颜色.如何指定坐标系.如何调整镜头参数.如何挪动观测方位等等,不过这些方法只是绘图前的准备工作,真正描绘点.线.面的制图工作并未涉及,那么本文就来谈谈如 ...

  10. Android开发笔记(一百五十二)H5通过WebView上传图片

    上一篇文章介绍了WebView与JS之间的数据交互,其实就是把字符串传来传去,这对文本格式的信息传输来说倒还凑合,倘若要传输图片信息就不管用了.所以,要想让h5网页支持从手机上传图片,还得另外想办法, ...

最新文章

  1. 学习UI设计的一些小技巧你会了吗
  2. python字符串操作符结果没显示_Python字符串格式化 (%操作符)
  3. nginx请求频率限制模块ngx_http_limit_req_module
  4. 【nyist】6 喷水装置(一) (简单的贪心)
  5. Bootstrap 分页导航
  6. android switch控件的大小,关于Android Action Bar 上的 Switch控件
  7. 招加盟的外卖店,是怎么做到每月10000+单量的?一个餐饮小店,靠外卖年收入过百万是真的吗?
  8. scrcpy设置快捷键_推荐电脑高清晰同步Anroid屏幕软件Scrcpy
  9. Mybatis的生命周期及作用域
  10. 便签如何把短音频文件mp3的转换成文字
  11. Opencv实现身份证OCR识别
  12. l麒麟安装oracle,中标麒麟linux安装Oracle客户端
  13. 跨境电商一件代发和专线小包是什么意思?有什么区别?
  14. 智能跳过节假日算法java_java计算两个日期之间的天数,排除节假日和周末
  15. wps怎么添加附录_如何将附录生成目录 - 卡饭网
  16. mysql安装问题:由于找不到MSVCR120.dIl,无法继续执行代码。
  17. 2022最新淘宝天猫商品详情接口采集方法
  18. 手机刷linux插鼠标U盘,用OTG线连接键盘鼠标U盘 手机瞬间变电脑 自制otg线
  19. Yara引擎编译和发布
  20. MFC CPropertySheet 多页面切换 事例

热门文章

  1. Typora使用指南以及各种小技巧
  2. Java并发(七)——并发容器
  3. PHP如何实现百万级数据导出
  4. c语言处理机调度实验报告,操作系统实验处理机调度C语言实现.docx
  5. [过年菜谱之]红烧鸡
  6. Extjs TreeNode的图标元素。 (节点图标, expand的加减号)
  7. getAttribute和getParameter的区别
  8. java oracle 换行,oracle中Clob字段中的回车换行在jsp中展示的问题
  9. python中breakpoint什么意思_it/breakpoint是什么意思
  10. mysql升级代码_phpstudy 升级mysql 及MySQL服务等问题(示例代码)