android开发笔记之MAT定位内存泄漏

内存泄漏的代码Demo

CommonUtils.java文件

public class CommonUtils {

    private static CommonUtils instance;
    private Context context;

    private CommonUtils(Context context) {
        this.context = context;
    }

    public static CommonUtils getInstance(Context context) {
        if (instance == null) {
            instance = new CommonUtils(context);
        }
        return instance;
    }

}

MainActivity类

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";
    private static Link linkInstance;

    class Link {
        public void dosomething() {
            Log.i(TAG,"dosomething");
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        if (linkInstance == null) {
            linkInstance = new Link();
            linkInstance.dosomething();
        }

        CommonUtils.getInstance(this);
        new MyThread().start();
    }

    class MyThread extends Thread {
        @Override
        public void run() {
            while (true) {
                try {
                    sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

}

生成hprof文件

在android profile中点击 gc, 然后执行横屏 ,竖屏切换,点击dump java heap,再点击Export capture to file,生成001.hprof.
android开发笔记之MAT定位内存泄漏_第1张图片
再执行命令,生成001_mat.hprof给MAT来使用:

hprof-conv D:\001.hprof  D:\001_mat.hprof

MAT的使用

下载一个 Mat工具
http://www.eclipse.org/mat/downloads.php

下载完之后就可以直接使用,双击MemoryAnalyzer.exe,然后直接把001_mat.hprof文件拖到 Mat 工具中。也可以File–Open Heap Dump,找到我们要使用的001_mat.hprof.
android开发笔记之MAT定位内存泄漏_第2张图片
然后点击 histogram:
android开发笔记之MAT定位内存泄漏_第3张图片
效果如下:
android开发笔记之MAT定位内存泄漏_第4张图片
我们重点关注自己写的代码,所以在最上面输入此应用的包名,然后回车:
android开发笔记之MAT定位内存泄漏_第5张图片
可以看到都是我们自己写的代码了,然后进行挨个分析:
android开发笔记之MAT定位内存泄漏_第6张图片
分析
选择第一个右键,List objects -> with incoming references ->回车
with incoming references : 表示该类被哪些内部对象 引用了
with outgoing references : 表示 该类持有了哪些外部对象的引用
回车 之后我们发现有三个 类如下图。
android开发笔记之MAT定位内存泄漏_第7张图片
我们选择去掉弱引用,软引用 所 引用的对象(右键—Path To GC Roots----exclude all phantom/ewak/soft etc.references):
android开发笔记之MAT定位内存泄漏_第8张图片
显示为:
android开发笔记之MAT定位内存泄漏_第9张图片
一下子就可以定位到问题的地方了.
MainActivity被三个对象引用了.
1.MyThread
2.CommonUtils
3.Link linkInstance
android开发笔记之MAT定位内存泄漏_第10张图片

内存泄漏的代码修改

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";
    
    //错误写法
    //private static Link linkInstance;
    // 解决方法 不定义为static类型
    private Link linkInstance;

    class Link {
        public void dosomething() {
            Log.i(TAG,"dosomething");
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        if (linkInstance == null) {
            linkInstance = new Link();
            linkInstance.dosomething();
        }

        // 解决方法 传入应用的context
        CommonUtils.getInstance(getApplicationContext());
        //错误写法
        //CommonUtils.getInstance(this);

        new MyThread().start();
    }
    //错误写法
    /*****
    class MyThread extends Thread {
        @Override
        public void run() {
            while (true) {
                try {
                    sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
     *****/
    // 解决方法   让MyThread为静态内部类,静态内部类就不会持有外部类的引用
    private static class MyThread extends Thread {
        @Override
        public void run() {
            while (true) {
                try {
                    sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
}

可能内存泄漏的代码修改后,我们再来上面操作后的情况:
android开发笔记之MAT定位内存泄漏_第11张图片
现在是不是没有上面的几个内存泄漏的问题了.

关于内存泄漏的一些知识点

为什么会有内存泄漏?

一个不会被使用的对象,因为另一个正在使用的对象持有该对象的引用,导致它不能正常被回收,而停留在堆内存中,内存泄漏就产生了

常见的内存泄漏:

持有Context造成的内存泄漏

在Android中有两种context对象:Activity和Application.当我们给一个类传递context的时候经常使用第一种,而这样就导致了改类持有对Activity的全部引用,当Activity关闭的时候因为被其他类持有,而导致无法正常被回收,而导致内存泄漏
解决方案:
在给其他给传递context的时候使用Application对象,这个对象的生命周期和共存亡,而不依赖activity的声明周期. 而对context的引用不要超过他本身的生命周期,谨慎对context使用static关键字.

Handler造成的内存泄漏

  public class SampleActivity extends Activity {
  private final Handler mLeakyHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
      // ... 
    }
  }
}

这样来使用Handler会造成严重的内存泄漏.
假设Hanlder中有延迟的任务或是等在执行的任务队列过长,由于消息队列持有对handler的引用,而handler又持有activity的隐式引用,这个引用会保持到消息得到处理,而导致activity无法被垃圾回收器进行回收,而导致内存泄漏

解决方案:
可以把Handler放到单独的类中,或者使用静态的内部类(静态内部类不会引用activity)避免泄漏
如果想要在handler内部去调用Activity中的资源,可以在Handler中使用弱引用的方式指向所在的Activity,使用static+WeakReference的方式断开handler与activity的关系

public static class MyHandler extends Handler {
    //声明一个弱引用对象
    WeakReference mReference;
 
    MyHandler(MainActivity activity) {
        //在构造器中传入Activity,创建弱引用对象
        mReference = new WeakReference(activity);
    }
 
    public void handleMessage(Message msg) {
        //在使用activity之前先判空处理
        if (mReference != null && mReference.get() != null) {
            mReference.get().text.setText("hello word");
        }
    }
}

使用单利模式造成的内存泄漏

在我们使用单利模式的时候如果使用不当也会造成内存泄漏.因为单利模式的静态特征使得单利模式的生命周期和应用一样的长,这说明了当一个对象不需要使用了,而单利对象还存在该对象的引用,那么这个对象就不能正常的被回收,就造成了内存泄漏
解决方案:

XXUtils.getInstance(this);

这句代码默认传入的是Activity的Context,而Activity是间接继承自Context的,当Activity退出之后,单利对象还持有他的引用,所以在为了避免传Activity的Context,在单利中通过传入的context获取到全局的上下文对象,而不使用Activity的Context就解决了这个问题.

public class XXUtils {
    private Context mContext;
    private XXUtils(Context context) {
        mContext = context.getApplicationContext();
    }
    private static XXUtils instance;
    public static XXUtils getInstance(Context context) {
        if (instance == null) {
            synchronized (XXUtils.class) {
                if (instance == null) {
                    instance = new XXUtils(context);
                }
            }
        }
        return instance;
    }
}

非静态内部类创建静态实例造成的内存泄漏

在项目中我们为了避免多次的初始化资源,常常会使用静态对象去保存这些对象,这种情况也很容易引发内存泄漏.
why?
非静态的内部类默认会持有外部类的引用
而我们又使用非静态内部类创建了一个静态的实例
该静态实例的声明周期和应用一样长,这就导致了该静态实例一直会持有该Activity的引用,导致Activity不能正常回收
解决方案:
将内部类修改成静态的,这样它对外部类就没有引用
将该对象抽取出来封装成一个单例.

private static TestResource mTestResource;

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
}
private void initData() {
    if (mTestResource == null) {
        mTestResource = new TestResource();
    }
}

//非静态内部类默认会持有外部类的引用
//修改成就太之后正常被回收,是因为静态的内部类不会对Activity有引用
private static class TestResource {
}

线程造成的内存泄漏

当我们在使用线程的时候,一般都使用匿名内部类,而匿名内部类会对外部类持有默认的引用,当Acticity关闭之后如果现成中的任务还没有执行完毕,就会导致Activity不能正常回收,造成内存泄漏

解决方案
创建一个静态的类,实现Runnable方法,在使用的时候实例化他.

最终代码:

private void loadData() {
    new Thread(new MyThread()).start();
}
private static class MyThread implements Runnable {
    public void run() {
        SystemClock.sleep(20000);
    }
}

资源未关闭造成的内存泄漏

对于使用了BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap等资源的代码,应该在Activity销毁时及时关闭或者注销,否则这些资源将不会被回收,造成内存泄漏。

监听器没有注销造成的内存泄漏

在Android程序里面存在很多需要register与unregister的监听器,我们需要确保及时unregister监听器。

集合中的内存泄漏

我们通常把一些对象的引用加入到了集合容器(比如ArrayList)中,当我们不需要该对象时,
并没有把它的引用从集合中清理掉,这样这个集合就会越来越大。如果这个集合是static的话,那情况就更严重了。 所以要在退出程序之前,将集合里的东西clear,然后置为null,再退出程序。

参考资料

1.Android 性能优化 - 彻底解决内存泄漏
https://blog.csdn.net/wanghao200906/article/details/79305126
2.Android 内存泄露和性能检测
https://blog.csdn.net/u012482178/article/details/78988176
3.Android Studio +MAT 分析内存泄漏实战
https://blog.csdn.net/u012760183/article/details/52068490

你可能感兴趣的:(android开发笔记)