内存泄露排查实战手记

Time:2013.09.02Author:sodino问题现象:这里内存泄露是指已实例化的对象长期被hold住且无法释放或不能按照对象正常的生命周期进行释放。问题期望:进行多次重复操作后,能够正常回收该对象(JobAppInterface)。期望在切换帐号后,之前的JobAppInterface能够及时回收(允许等待一段时间后再回收)。问题排查:经过排查,总结为三种情况导致JobAppInterface内存泄露:1.静态实例长期占用JobAppInterface。2.线程没有被stop导致JobAppInterface无法释放。3.Observer/Listener没有被反注册导致Activity或JobAppInterface无法被释放。一号坑:静态实例长期占用严重,无法释放SettingManager的静态实例长期占据第一次初始化的JobAppInterface表现:启动应用时所创建的JobAppInterface会一直被引用,无法释放。原因:经全文查找,发现其静态实例只有初始化的入口,条件是当其实例为null的时候就初始化。sInstance执行new操作后,再没有任何回收操作。这样,除非手Q完全退出了,不然sInstance会一直存在并且占其初始化时使用的JobAppInterface。

public static SettingManager getInstance(AppInterface app) {
		if (sInstance == null) {
			sInstance = new SettingManager(app);
		}
		return sInstance;
	}



解决方案:

在获取实例时,如果sInstance已经存在,则对比app,当app不一致时则进行回收和新的实例的生成。

public static SettingManager getInstance(AppInterface app) {
        if (sInstance == null) {
            sInstance = new SettingManager(app);
        } else if (sInstance.mApp != app) {
        	//切换了账号
			sInstance = null;
			sInstance = new SettingManager(app);
		}
        return sInstance;
    }

另,将该实例的回收与JobAppInterface的生命周期保持一致。在JobAppInterface.onDestory()时及时清理掉。

JobAppInterface.java
protected void onDestroy() {
	... ...
	SettingManager.clearInstance(this);
	... ...
}

SettingManager.java
public static void clearInstance(AppInterface app){
	if(sInstance != null && sInstance.mApp == app){
		sInstance = null;
	}
}


本文为Sodino所有,转载请注明出处:


二号坑:线程长期running导致对象无法被释放

严重,无法释放

MessageThread线程一直在运行,导致无法释放JobAppInterface

表现:

这个在疯狂乱点切换帐号时出现。MAT工具发现无法释放的JobAppInterface都有被多个MessageThread引用着。嫌疑很大。但当时对这块逻辑不熟,为了进一步确认,做了如下操作:

1.在生成MessageThread的地方将新new出现的Thread命名为"index_System.time",并在其start()之后输出相应的start日志,日志信息包括Thread的name及其所关联的JobAppInterface.hashcode。

2.在Thread.run()方法结尾处输出"exit"日志,日志信息同样包括Thread的name及其所关联的JobAppInterface.hashcode。

经比较,发现快速切换帐号后,每切换一次会新生成5个Thread,但最后都只有5个Thread会执行"exit"操作,则仍然为(n -1)*5个Thread仍在运行着,导致JobAppInterface无法释放。

原因:

确定了MessageThread有问题后,可以下定决心分析下原因了。经查,发现在原逻辑中,是有关闭这些Thread的地方,如下:

JobInitHandler.java中:
   private void onStateRunning() {
		... ...
		... ...
		// 消息拉取完成后,做一些事情
        if(curStep == (STEP_GET_MSG + 1)){
        	doSomethingAfterSyncMsg();//  ---->这里去关闭MessageThread..
        }
		
		switch (curStep) {
        case STEP_INIT:
            // action...
            break;
        case STEP_START:
        	// action...
            break;
			... ...
			... ...
		};
	}




在JobInitHandler.onStateRunning(),"当消息拉取完成后,做一些事情"这里会去把MessageThread 停止掉。但问题就出在这里,关闭的时机出现了问题,在快速切换帐号的操作中,由于没有足够的时间让消息拉取操作完成,也就造成了curStep无法走到值为"(STEP_GET_MSG + 1)"的情形,导致MessageThread一直在空跑无法,其引用的JobAppInterface无法被释放。

解决方案:

在JobInitHandler.destory()时将仍在运行的Thread停止掉。

public void destroy() {
		... ...
		... ...
		// 停止代理处理线程
		app.getHandler().stopProxyThread();
		... ...
		... ...
	}



三号坑:Observer/Listener没有被反注册

严重,无法释放

注册BroadcastReceiver后没有反注册

表现:

直接看图吧,new的一个BroadcastReceiver在构造函数中直接register,但通篇没有被ungister导致内存泄露。见图1。


android 查找jni内存泄露 android排查内存泄露_android 查找jni内存泄露


解决方案:

if(tmpHandler != null && tmpHandler instanceof DataLineHandler){
		((DataLineHandler)tmpHandler).close();// 执行反注册操作
	}




问题总结:

就目前来说,经过以上三种类型问题的排查,目前已经达成目的。

这里小总结一下,泄露的原因分别为静态实例占用、线程没被及时停止、注册的Observer/BroadcastReceiver没有及时被反注册。但解决的方法都是同样的:在JobAppInterface的onDesotry()方法(或相似的如JobInitHandler.destory())中及时执行关停回收操作即可避免。