专栏首页老欧说安卓Android开发笔记(七十五)内存泄漏的处理

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

内存泄漏的原因

一直以来以为只有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();
	}

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

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

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开发笔记的完整目录

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 程序员带你学习安卓开发-XML文档的创建与解析

    这是程序员带你学习安卓开发系列教程。本文章致力于面向对象程序员可以快速学习开发安卓技术。

    做全栈攻城狮
  • 程序员带你学习安卓开发系列-Android文件存储

    输入帐号密码,并勾选记住帐号 ,点击登录时,保存帐号信息。下次登陆可以直接显示上次保存的QQ帐号。

    做全栈攻城狮
  • 安卓开发-使用异步网络请求框架、多线程文件下载

    相信对于前面的教程,大家已经很熟悉安卓网络编程了。这篇文章主要讲解一下异步网络编程和文件下载。学习编程重在写代码,只有自己的代码量上去了,自己才能完全理解。所以...

    做全栈攻城狮
  • 安卓开发基础教程-Android多界面应用程序开发

    本套教程主要讲解安卓开发的相关知识,从基础到精通。一方面可以巩固自己所得,另一方面可以帮助对安卓开发感兴趣的朋友。

    做全栈攻城狮
  • 安卓开发基础教程-Android多界面应用程序开发

    本套教程主要讲解安卓开发的相关知识,从基础到精通。一方面可以巩固自己所得,另一方面可以帮助对安卓开发感兴趣的朋友。

    做全栈攻城狮
  • React Native APP签名打包release版本APK

    首先React Native开发的APP是无法通过Android Studio进行打包的,因为AS打包的APK,也是和debug版本一样,需要进行依托local...

    做全栈攻城狮
  • React Native开发APP中的常用命令,如有遗漏请补充(不断更新...)

    做全栈攻城狮
  • 做全栈攻城狮-安卓开发教程目录

    1.程序员带你学习安卓开发,十天快速入门-安卓学习必要性:http://www.toutiao.com/i6319356348286894594/

    做全栈攻城狮
  • 用Android最火的快速开发框架XUtils,进行文件下载

    更多原创教程,关注微信公众平台:做全栈攻城狮。及做全栈攻城狮官网:www.8z5.net

    做全栈攻城狮
  • 安卓开发基础教程-使用隐式意图打开系统内置应用,干货

    本教程致力于讲解及快速进行安卓开发的学习。除安卓开发教程之外,还有Python、C#、网站建设、SEO等教程。对电脑技术感兴趣的朋友直接点击上方“关注”。

    做全栈攻城狮

扫码关注云+社区

领取腾讯云代金券