(五)内存泄漏分析工具的使用以及解决方案

子曰:温故而知新,可以为师矣。 《论语》-- 孔子


作为性能优化专栏的第五篇,阅读本文章前可以先阅读 (四)内存优化前奏篇(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 工具分析定位问题并提供解决方案 就介绍到这,各位看官食用愉快。


码字不易,如果本篇文章对您哪怕有一点点帮助,请不要吝啬您的点赞,我将持续带来更多优质文章。

发布了14 篇原创文章 · 获赞 15 · 访问量 9049

猜你喜欢

转载自blog.csdn.net/wild_onlyking/article/details/104737804