前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Android开发笔记(七十五)内存泄漏的处理

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

作者头像
aqi00
发布2019-01-18 13:01:26
1.1K0
发布2019-01-18 13:01:26
举报
文章被收录于专栏:老欧说安卓

内存泄漏的原因

一直以来以为只有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方法获取视图持有者。下面是重用的代码示例:

代码语言:javascript
复制
		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的空监听器。代码示例如下:

代码语言:javascript
复制
	@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();
	}

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

代码语言:javascript
复制
	@Override
	public 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实现内存读写》。 下面是释放引用的代码示例:

代码语言:javascript
复制
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开发笔记的完整目录

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2016年03月04日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 内存泄漏的原因
  • 内存泄漏的场景
  • 内存泄漏的发现
  • 内存泄漏的预防
    • 关闭游标
      • 重用适配
        • 回收图像
          • 注销监听
            • 释放引用
            相关产品与服务
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档