子曰:温故而知新,可以为师矣。 《论语》-- 孔子
作为性能优化专栏的第五篇,阅读本文章前可以先阅读 (四)内存优化前奏篇(Java虚拟机、垃圾回收机制、内存泄漏/溢出/抖动,再阅读本篇文章效果更佳。
本篇文章就来讲一下在项目中如何使用工具来分析内存泄漏
。
一、 模拟内存泄漏代码
我这边先写一个引发内存泄漏的代码:
public class MainActivity extends AppCompatActivity {
private Handler mHandler = new Handler(){
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
mHandler.sendEmptyMessageDelayed(1,1000);
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView viewById = (TextView)findViewById(R.id.tv_main);
viewById.setText("主界面");
mHandler.sendEmptyMessageDelayed(1,1000);
}
}
我在主页面不断延迟发消息,当点击物理返回键时,由于 handler 持有 MainActivity 的引用
,会引发内存泄漏,这个大家应该都知道,那么这是我们自己写的,那么如何通过工具去分析定位到这个内存泄漏呢??
二、使用 Profile app
在我们的 Android Studio 上 点击 Profile App
按钮运行程序,中间蓝色的就是我们程序运行时内存消耗的情况,如图:
我们点击 Memory
区域,具体显示如图所示:
此时我们的项目在主界面,点击物理返回键后
,点击强制垃圾回收按钮
,手动触发GC
(下图),如图所示:
在这张图上,我已经标记的很清楚了,我们强制垃圾回收按钮,手动触发 GC 后,选取垃圾回收之后的一段内存消耗做研究。
将下方的查找过程改成以 package
方式查找,找到 MainActivity
,我们已经在 MainActivity 页面点击了返回键,理论上此时 MainActivity 应该销毁了,但是此时 MainActivity 对象此时还存在一个,说明 MainActivity 并未被回收,所以发生了内存泄漏,那么具体是哪块代码触发的内存泄漏,我们点击 dump
下载文件,会出现一段黑色区域,右键选择 Export
导出文件, 如下图所示:
三、使用 Mat
工具
那么我们下载的文件该如何查看呢,这边我自己用的是 Mat
工具,我这边给出地址Mat 下载。我们之前下载的文件要转化一下,来到 Android Studio 的 SDK 目录下的 platform-tools 文件夹,在这个文件夹中有个 hprof-conf 的 文件,我们在终端中 cd 到 platform-tools 目录下, 输入命令:
hprof-conf -z infile outfile
例如 hprof-conf -z 1.hprof 1-mat.hprof
infile 是之前下载的文件名,outfile 是要转换的文件名
接下来启动 Mat 工具,如果你是使用 Mac 电脑,同时启动报错的话,那么右键 Mat,点击 查看包内容,在终端中进入 MacOs 文件夹,输入以下命令就可启动成功:
./MemoryAnalyzer -data ./dump
现在把我们转化后的文件在 Mat 中打开,点击 File --> open file
,如图所示:
我们点击 Biggest Objects by Retained size
按钮,再点击 Histogram
按钮,在搜索区(Regex 区域)搜索 MainActivity,在显示的 MainActivity 那一行右键,做以下如图操作:
显示结果如下:
从这张图中,我们可以很明显的看出,这是一连串的 引用链
,this$0 这个表示内部类,那么这个内部类一层层持有引用,最顶层的引用是 mMainLooper,那么就说明就是内部的 Handler 持有 MainActivity 引用,根本原因就是 mMainLooper 的生命周期长于 MainActivity 的生命周期
,当 MainActivity 页面销毁了,但是由于 mMainLooper 持有 MainActivity 引用且生命周期长于它,所以无法销毁,所以就有了内存泄漏。
说到这儿,可能就有人会说了,你这是已知答案,反推原因,那么如果要对整个项目去分析内存泄漏,该如何操作,我能告诉你的就是,你在项目中 各个模块点击,多点击,然后点击 GC,去分析 Activity 以及 Fragment 的个数
,主要你要知道当 Activity 或者 Fragment 销毁时,在分析工具中不会有该对象存在的,如果你多次操作某一个 Activity ,结果它的数量在不断增加,说明这个 Activity 中一定存在内存泄漏
,那么就可以用 Mat 工具去分析一波了,查看具体是哪块代码导致的内存泄漏。
有可能有人问了,如果在 Mat 工具中发现的是系统存在的内存泄漏,我该如何修改,这边我就一个例子,在低版本的项目中,系统的输入法是会产生内存泄漏的,但是在高版本就不会产生,这边提供一个方法:暴力置空
,采用反射技术:
/**
* 反射置空输入法中的属性
*/
public void method(String attr){
InputMethodManager im=(InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
try{
Field mCurRootViewField = InputMethodManager.class.getDeclaredField(attr);
mCurRootViewField.setAccessible(true);
//取对象
Object mCurRootView=mCurRootViewField.get(im);
if (null!=mCurRootView) {
Context context=((View)mCurRootView).getContext();
if(context==this){
//破坏GC链
mCurRootViewField.set(im,null);
}
}
}catch(Exception e){
e.printStackTrace();
}
}
当然这是针对系统代码产生的内存泄漏可以这么做,这也是不得已而为之。
分析内存泄漏,并且解决内存泄漏,不是短期就能全部处理完的,可能你刚解决一个内存泄漏,又无形中产生了另外一个内存泄漏,我们能做的就是保持一个良好的写代码的姿势,也就是写优质代码,写模块功能时,尽量多考虑几种情况。这也是自己要写性能优化专栏的目的,为的也是希望能够进一步提高自己写代码的姿势。
写在文末
纸上得来终觉浅,绝知此事要躬行。 《冬夜读书示子聿》-- 陆游
好了,关于 内存泄漏使用 Mat 工具分析定位问题并提供解决方案
就介绍到这,各位看官食用愉快。