内存泄漏,可以说是android app性能优化中的常见的拦路虎。它出现时一般满足两个条件,一是在有向图中,内存泄漏的对象是可达的;二是泄漏的对象对app来说是不会再使用的。这就导致GC不能回收相应内存。
这次内存泄漏分析过程中,遇到了Android中常出现的内存泄漏原因。一是生命周期长的对象持有声明周期短的对象引起。比如静态的单例对象持有Activity的引用。二是普通内部类和匿名内部类隐式的持有外部类的应用,建议多用静态内部类。三是对对象(View等)添加监听器,广播注册等,在界面销毁时需要进行反注册。后续分析过程会提到。
内存泄漏检测
实战中采用以下两种方法检测
- 一、通过adb shell dumpsys meminfo 包名,检测activity泄漏
安卓app中一般约定back键退出activity时,回收相应的内存空间。可通过dumpsys meminfo命令来检测内存中的Activity数量。
理论上,用户通过back退出应用时,Activity的数量应该为0。否则要考虑内存泄漏的可能。
- 二、通过Android Monitor工具检测
当前我使用的Android Studio版本为2.3.3,可以通过Android Monitor监控应用当前的内存。在进行操作时可以直观的观测内存增长和回收的情况。
内存泄漏分析
点击Android Monitor中的Dump Java Heap按钮,生成hprof文件来分析当前的内存使用情况,可帮助我们找到内存泄漏的根源。
万事俱备,接下来就开始进行分析了。第一步想到的就是测试back退出应用,结果竟然真的发现了内存泄漏,真是瞌睡时被送上了枕头。
Local Broadcast泄漏
这次内存泄漏的现象很明显,每次进入app主界面内存上升,但通过back退出应用后,手动点击Initiate GC,内存仍未下降。配合dumpsys meminfo命令,内存中的Activity数量每次进行app都会增长。
通过Dump Java Heap生成的hprof文件如上图所示。点击侧边栏的Analyzer Tasks按钮打开相应面板,通过绿色的Perform Analysis即可生成检测结果,并显示在Analysis Results面板中。
正如图中所示,主页面MainActivity在内存中有多个实例,存在内存泄漏。选中一个MainActivity实例,即可在Reference Tree中查看泄漏对象的引用关系。这次根据图中信息,可看出是LocalBroadcastManager未返注册引起。检查MainActivity中的广播注册,果然找到了问题。这里还有个小插曲,代码乍一看,明明在Activity的onDestroy方法调用了unregisterReceiver呀。后续脑袋清醒了才发现,原来是向LocalBroadcastManager注册的mNetworkReconnectedBroadcastReceiver,代码中竟然用了普通Broadcast的unregisterReceiver。具体见下代码:
//MainActivity.java
@Override
protected void onDestroy() {
super.onDestroy();
try {
unregisterReceiver(updateSucess);
//错误代码,
//unregisterReceiver(mNetworkReconnectedBroadcastReceiver);
//正确代码,通过LocalBroadcastManager进行反注册。
LocalBroadcastManager.getInstance(this).unregisterReceiver(mNetworkReconnectedBroadcastReceiver);
} catch (Exception e) {
}
......
}
单例模式对象泄漏
继续进行测试,多次进入退出MainActivity。发现内存增长很少,GC后内存可正常回收。但是通过dumpsys meminfo命令可看到,应用退出后仍有一个MainActivity的泄漏。
根据hprof文件,可看到是SocketService的一个匿名内部类间接持有MainActivity作为其Context对象导致。选中SocketService$1所在行,右键选择Go to instance可查看内存中的对应对象实例。内存中竟然有7个SocketService$1的实例,初步看是用来处理网络请求的异步返回结果。
检查代码发现app在退出时,会进行单例对象NetWorkService的释放,而NetworkService单例对象中持有SocketService单例对象。根据下面代码中可知,NetworkService对象在release函数中,将其单例对象置为null。但直接查看NetWorkService对象,内存中存在一个实例。根据Refrence Tree中显示,可知被ThinkiveInitializer对象持有。
//NetWorkService.java
public void release() {
if (mSocketService != null) {
mSocketService.release();
}
if (mHttpService != null) {
mHttpService.release();
}
if (mMemoryCache != null) {
mMemoryCache.clear();
}
if (sInstance != null) {
sInstance = null;
}
}
查看代码知,ThinkiveInitializer是一个单例模式,其中定义了成员变量mNetWorkService持有了NetWorkService对象的实例。ThinkiveInitializer在进程退出时仍然存在,清理操作中未将mNetWorkService变量置空。因成员变量mNetWorkService仅在Application的onCreate过程中赋值,app退出时未置空,导致进程第一次启动时创建的mNetWorkService对象泄漏。
可通过删除mNetWorkService变量,直接使用其Network.getInstance()方法解决该问题。
ThinkiveInitializer.java
/**
* 退出应用
*/
/*package*/ void exit() {
isExit = true;
onTerminate();
// android.os.Process.killProcess(android.os.Process.myPid());
}
public void onTerminate() {
cancelPendingRequests(TAG);
ThinkiveDatabase.getInstance(mContext).close();
//正确代码。删除mNetWorkService变量,直接使用单例避免内存泄漏。因NetWorkService的release操作会将NetWorkService实例置为空
NetWorkService.getInstance().release();
//错误代码
//mNetWorkService.release();
}
webview及匿名内部类的泄漏
在测试中还发现了关于Webview相关的内存泄漏。如下图所示,多次进入退出主界面后,内存中总是存在2个MainActivity的实例。
由于历史原因,该项目中使用单例模式的WebViewManager管理内存中的WebView对象。其中成员变量mWebViewBank存储待复用的Webview。由于刚接手项目,不能贸然的进行大的改动,只能先尽量减少内存泄漏。
下图中的第二个MainActivity对象(MainActivity@853280208)引用链较简单。主要是由于app第一次启动创建WebView时,传入了MainActivity作为其Context。后续这个WebView被静态对象WebViewManager一直持有,留作下次进入app复用。app这一特有的设计,也导致第一次启动的MainActivity总是被泄漏。后续会尝试用Application的Context来初始化WebView,但短期不能贸然变化。
下图中的第一个MainActivity对象(MainActivity@853280944)可看出,主要是由于WebView设置匿名内部类作为WebChromeClient引起的。MainActivity创建时,初始化WebView时调用了setWebChromeClient(new WebChromeClient{...})。
对比两个MainActivity引用链中的MyWebView对象,地址都是MyWebView@855378944.这就是上面提到的WebView的复用。但在第二次及后续启动MainActivity时,setWebChromeClient操作导致产生了引用链:MyWebView-》匿名WebChromeClient-》WebView所在Fragement-》MainActivity。进而导致用户通过back退出时,MainActivity不能被回收。
WebViewClient也可能存在类似问题。同时在检查中还发现了对WebView设置setOnTouchListener/setOnKeyListener等后续未置null导致的内存泄漏。
解决方法:
webview.setWebChromeClient(null);
webview.setWebViewClient(null)
上面Webview相关的内存泄漏主要是项目特殊的复用问题引入,可能仅仅是个案。但是在分析中让我更熟练的使用Monitor工具,并深刻认识到匿名内部类隐式持有外部类对象导致的内存泄漏可能性,收获很大。
当然后续还有工作要做。那就是搞清楚WebView复用时如何避免内存泄漏?使用Application的Context创建WebView是否会引入问题?复用的WebView持有已不可见的MainActivity对象是否会出问题?有答案后会继续更新。
参考:
Android内存泄漏的简单检查与分析方法
Android 性能优化&内存篇
android内存优化之webview
Android 如何有效的解决内存泄漏的问题
Android 内存泄漏总结(超级实用)