Android 性能优化 - 彻底解决内存泄漏

起源

有趣的灵魂千奇百怪,内存泄漏的也是各式各样
我在15年写过一遍 文章 《 android中常见的内存泄漏和解决办法》http://blog.csdn.net/wanghao200906/article/details/50426881,时隔三年居然还有人我问 该如何解决 内存泄漏的问题,因为 有趣的灵魂 千奇百怪,所以 内存泄漏的也是各式各样,所以想避免 内存泄漏 ,不能只记住 常见 问题的代码,而是要学会如果发现内存泄漏的方法。

学习内容

  • 内存泄漏的一些 基础支持(估计有你不会的)
  • 学会使用android studio 3.0 自带的 android profile 检查内存泄漏
  • 使用 mat 工具 来检查 内存泄漏

内存泄漏的基础

内存泄漏:内存不在GC掌控之内了。当一个对象已经不需要再使用了,本该被回收时,而有另外一个正在使用的对象持有它的引用从而就导致,对象不能被回收。这种导致了本该被回收的对象不能被回收而停留在堆内存中,就产生了内存泄漏

四中引用

  • StrongReference强引用:
    回收时机:从不回收 使用:对象的一般保存 生命周期:JVM停止的时候才会终止
  • SoftReference软引用
    回收时机:当内存不足的时候;使用:SoftReference结合- ReferenceQueue构造有效期短;生命周期:内存不足时终止
  • WeakReference,弱引用
    回收时机:在垃圾回收的时候;使用:同软引用; 生命周期:GC后终止
  • PhatomReference 虚引用
    回收时机:在垃圾回收的时候;使用:合ReferenceQueue来跟踪对象呗垃圾回收期回收的活动; 生命周期:GC后终止

开发时,为了防止内存溢出,处理一些比较占用内存大并且生命周期长的对象的时候,可以尽量使用软引用和弱引用。
软引用比LRU算法更加任性,回收量是比较大的,你无法控制回收哪些对象。

因为我们主要是 查找 内存泄漏 ,这里不对 基础知识 做过多的扩充

内存分配

  • 成员变量全部存储在堆中(包括基本数据类型,引用及引用的对象实体)—因为他们属于类,类对象最终还是要被new出来的。
  • 局部变量的基本数据类型和引用存储于栈当中,引用的对象实体存储在堆中。—–因为他们属于方法当中的变量,生命周期会随着方法一起结束。
    看下面代码
public class Main{
    int a = 1; // a 和1 都在堆里
    Student s = new Student();// s 和new d的Student()都在 堆里
    public void XXX(){
        int b = 1;//b 和 1 栈里面
        Student s2 = new Student();// s2 在栈里, new的 Student() 在堆里
    }

}

ok 如果你不同意,在你查询完资料之后, 欢迎来讨论

基础知识就到这吧。下面开始主题

Android Profiler 查找内存泄漏

该方法可以解决一部分问题,但不是很好用,之所以得学习他是因为为后面做个铺垫

下面来介绍一下,因为不是特别好用 所以用一个简单的例子介绍一下,
看下面的代码

package sven.com.practise32_performance_optimization;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        CommonUtils.getInstance(this);
    }
}



package sven.com.practise32_performance_optimization;

import android.content.Context;

/**
 * Created by wanghao on 2018/2/8.
 */

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;
    }


}

我们看一下 android profiler
这里写图片描述

这样的。然后我们点击中间的MEMORY.

这里写图片描述

然后我们执行如下操作。
我们 横屏 , 然后在竖屏

  • 横屏一下 就会将该MainActivity onDestory掉。然后在onCreate();

横屏竖屏,操作结束后我们点击 箭头指向的按钮,如下图
这里写图片描述

我们看到最下面

这里写图片描述

这就是 堆的一些信息。

一些信息的含义

  • Heap Count:堆中的实例数。
  • Shallow Size:此堆中所有实例的总大小(以字节为单位)。
  • Retained Size:为此类的所有实例而保留的内存总大小(以字节为单位)。
    在类列表顶部,您可以使用左侧下拉列表在以下堆转储之间进行切换:

  • Default heap:系统未指定堆时。

  • App heap:您的应用在其中分配内存的主堆。
  • Image heap:系统启动映像,包含启动期间预加载的类。 此处的分配保证绝不会移动或消失。
  • Zygote heap:写时复制堆,其中的应用进程是从 Android 系统中派生的。
    默认情况下,此堆中的对象列表按类名称排列。 您可以使用其他下拉列表在以下排列方式之间进行切换:

  • Arrange by class:基于类名称对所有分配进行分组。

  • Arrange by package:基于软件包名称对所有分配进行分组。
  • Arrange by callstack:将所有分配分组到其对应的调用堆栈。 此选项仅在记录分配期间捕获堆转储时才有效。 即使如此,堆中的对象也很可能是在您开始记录之前分配的,因此这些分配会首先显示,且只按类名称列出。
    默认情况下,此列表按 Retained Size 列排序。 您可以点击任意列标题以更改列表的排序方式。

在 Instance View 中,每个实例都包含以下信息:

  • Depth:从任意 GC 根到所选实例的最短 hop 数。
  • Shallow Size:此实例的大小。
  • Retained Size:此实例支配的内存大小(根据 dominator 树)。

接下来我们查看app Heap 中的内容
经过漫长的查找。我们找到了MainActivity (这就是我觉得 android profiler 不要用的地方,必须得挨个查找,效率太低了)。
这里写图片描述

点击MainActivity. 然后右边出来了 Instance View ,里面出现了 三个 MainActivity 。
这里写图片描述

分析1

  • 我们看到Depth 从上到下 是 2 ,0,3 ,上文说了 Depth:从任意 GC 根到所选实例的最短 hop 数。
  • 在回忆刚才,我们 打开app,横屏,竖屏,创建了3个MainAvtivity ,这点儿可以理解。
  • 2,0,3 怎么解释呢。
    0 就代表该activity,可以被 gc 回收
    2 ,3 就代表 最短的hop数不为零。肯定不会被gc回收。

    • 我们点击第一个MainAvtivity看看

这里写图片描述

我们看到 instance 这个代码, 是我们自己定义的,它持有了MainActivity 。

  • 在看第三个MainAvtivity

这里写图片描述

我们看到这里就没有刚才 被CommonUtils中 instance 持有的activity。说明 当前的activity。是我们看到的MainActivity。他当然不可能被销毁。同时因为 第一次创建的MainActivity的时候 生成了 CommonUtils中 instance 它不为null。所以 在创建MainActivity的时候 它的上下文不会再次被持有。

到这里我们执行以下gc操作,然后在点击dump操作
这里写图片描述

在根据刚才的方法 找到MainAvtivity。点击。 结果如下
这里写图片描述

我们看到 这个 MainAvtivity被持有了。所以MainActivity 在横屏的时候,虽然执行了onDestory(),但依然不会被销毁,他无法被回收,所以就会内存泄露。

到此 android profiler 中 查找内存泄漏的方法就介绍完了。

我觉得的缺点就是,如果代码过于复杂,我们又不知道查找那些代码。只能挨个点击,挨个看,太耗时耗力了。

所以 还是MAT 工具是最好用的。

MAT 完美查找内存泄漏

为了 说明MAT 的强大 我们写一个稍微复杂的内存泄漏的代码


public class MainActivity extends AppCompatActivity {
    private static final String TAG = "MainActivity";
    private static Link linkInstance;

    class Link {
        public void dosomething() {

        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        if (linkInstance == null) {
            linkInstance = new Link();
        }

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

    }


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


}

然后执行才做: 运行 ,横屏 ,竖屏,在android profile中 点击 gc,然后在点击dump java heap。效果如下

这里写图片描述

点击Export capture to file 保存到本地文件夹。格式为 xxx.hprof 比如 叫 test1.hprof

然后执行命令行

hprof-conv /Users/mypro/Desktop/mat/test1.hprof  /Users/mypro/Desktop/mat/test2.hprof
  • /Users/mypro/Desktop/mat/test1.hprof 是我们在as中保存的hprof文件
  • /Users/mypro/Desktop/mat/test2.hprof 是我们用命令行要生成的 hprof文件,为了mat可以识别

然后 使用MAT 工具了。可惜的是 as没有Mat 工具。只能下载一个 Mat工具
http://www.eclipse.org/mat/downloads.php

下载完之后就安装,打开。然后直接把test2.hprof 文件 拖到 Mat 工具中。

为了 以后的方面快捷,真的很有必要下载一个Mat工具,你是程序员,为了完美的找到内存泄漏的地方,难道还怕麻烦么,难道你看到测试组 报告后 ,你的代码有好多内存泄漏的地方,你说你面子往哪搁。

在选择框中选择第一个,生成报告。
这里写图片描述

然后点击 histogram,效果如下

这里写图片描述

我们看到了 有好多的类。跟在android profile中看到的一样。但是这么多。我们不用挨个检查,只要检查我们自己写的代码即可,如果在最上面可以输入包名,然后回车。

可以看到 出现的都是我们自己写的代码了。进行挨个分析
这里写图片描述

分析

选择第一个右键,List objects -> with incoming references ->回车

  • with incoming references : 表示该类被 哪些外部对应 引用了
  • with outgoing references : 表示 该类 持有了 哪些外部对象的引用

回车 之后我们发现有三个 类如下图。
这里写图片描述

我们发现 第一个MainActivity 被 好多 对象引用着了。该如挨个查找呢。莫慌。如下图操作

这里写图片描述

我们选择 去掉 弱引用,软引用 所 引用的对象。
这里写图片描述

可以看到 我们的MainAvtivity类,被 一个 MyThread的线程 所引用了。找到以一个 内存泄漏的地方。
因为我们 内部类会持有 外部类的引用。所以 MyThread 类持有了MainActiviy的 上下文。又因为 thread 是一个 while(true)的死循环,所以 不会释放。

我们现在 依据上面 的分析流程,继续查看第二个类。

这里写图片描述

还是右键->Path to Gc Root -> exclude all phantom/weak/soft etc. references- >回车

这里写图片描述

ok,我们发现MainActivity 的上下文被好几个类引用了。但是除了MyThread 类是我们自己定义的,其他的类都是 安卓源码,所以我们可以推断,当前的MainActivity类可能是 正在展示在手机页面上的 MainActivity 类。所以该类并不存在内存泄漏的问题。

我们看完第二个 再看看三个 。
这里写图片描述

还是右键->Path to Gc Root -> exclude all phantom/weak/soft etc. references- >回车

这里写图片描述

ok,我们看到 三个地方引用 了MainActivity 的 上下文。这三个地方度出现了 内存溢出。

  • MainActivity 创建了三次,一共创建三次 MyThread 类,MyThread 类属于内部类,他持有外部类的额引用
  • CommonUtils 属于 单利,当CommonUtils 的instance 不为null的时候 就不会再继续持有 MainActivity的上下文引用。所以只持有了一次
  • Link 属于 内部类 持有MainActivity 的引用,但是在onCreate里面有判断只有当 Link的instance为null的时候才会创建一次。所以也只持有了 一次MainActivity的上下文引用。

到这里 内存泄漏的方法就介绍完了。学会该方法,不管是什么内存泄漏都可以轻松的找到了。
能找到内存泄漏的地方,基本上就可以解决了。

下面在稍微总结一下 常见内存泄漏的就该方法。。
- 静态变量引起的内存泄露

当调用getInstance时,如果传入的context是Activity的context。只要这个单利没有被释放,那么这个
    Activity也不会被释放一直到进程退出才会释放。
    public class CommUtil {
        private static CommUtil instance;
        private Context context;
        private CommUtil(Context context){
        this.context = context;
        }

        public static CommUtil getInstance(Context mcontext){
        if(instance == null){
            instance = new CommUtil(mcontext);
        }
    //        else{
    //            instance.setContext(mcontext);
    //        }
        return instance;
        }

  • 非静态内部类引起内存泄露(包括匿名内部类)

public class MainActivity extends AppCompatActivity {
    private static final String TAG = "MainActivity";
    //    private static Link linkInstance;
    private Link linkInstance;
    private boolean flag = true;

    class Link {
        public void dosomething() {

        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        if (linkInstance == null) {
            linkInstance = new Link();
        }

//        CommonUtils.getInstance(this);
        CommonUtils.getInstance(getApplicationContext());
        new MyThread().start();

    }

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

//    解决方法 方法1
//    class MyThread extends Thread {
//        @Override
//        public void run() {
//            while (flag) { //MainActivity 销毁的时候让thread 停止
//                try {
//                    sleep(1000);
//                } catch (InterruptedException e) {
//                    e.printStackTrace();
//                }
//            }
//        }
//
// 解决方法2   让MyThread 为 静态内部类,静态内部类就不会持有 外部类的引用
//    
    static class MyThread extends Thread {
        @Override
        public void run() {
            while (true) {
                try {
                    sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }


    @Override
    protected void onDestroy() {
        super.onDestroy();
        flag = false;
    }
}

再来一个 handler的内部类 错误示范

 //错误的示范:
    //mHandler是匿名内部类的实例,会引用外部对象MainActivity.this。如果Handler在Activity退出的时候,它可能还活着,这时候就会一直持有Activity。
//    private Handler mHandler = new Handler(){
//        @Override
//        public void handleMessage(Message msg) {
//            super.handleMessage(msg);
//            switch (msg.what){
//                case 0:
//                    //加载数据
//                    break;
//
//            }
//        }
//    };

    //解决方案:
    private static class MyHandler extends Handler{
//        private MainActivity mainActivity;//直接持有了一个外部类的强引用,会内存泄露
        private WeakReference<MainActivity> mainActivity;//设置软引用保存,当内存一发生GC的时候就会回收。

        public MyHandler(MainActivity mainActivity) {
            this.mainActivity = new WeakReference<MainActivity>(mainActivity);
        }

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            MainActivity main =  mainActivity.get();
            if(main==null||main.isFinishing()){
                return;
            }
            switch (msg.what){
                case 0:
                    //加载数据
//                    MainActivity.this.a;//有时候确实会有这样的需求:需要引用外部类的资源。怎么办?
                    int b = main.a;
                    break;

            }
        }
    };
  • 不需要用的监听未移除会发生内存泄露
例子1//        tv.setOnClickListener();//监听执行完回收对象
        //add监听,放到集合里面
        tv.getViewTreeObserver().addOnWindowFocusChangeListener(new ViewTreeObserver.OnWindowFocusChangeListener() {
            @Override
            public void onWindowFocusChanged(boolean b) {
                //监听view的加载,view加载出来的时候,计算他的宽高等。

                //计算完后,一定要移除这个监听
                tv.getViewTreeObserver().removeOnWindowFocusChangeListener(this);
            }
        });

    例子2:
            SensorManager sensorManager = getSystemService(SENSOR_SERVICE);
        Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ALL);
        sensorManager.registerListener(this,sensor,SensorManager.SENSOR_DELAY_FASTEST);
        //不需要用的时候记得移除监听
        sensorManager.unregisterListener(listener);


  • 资源未关闭引起的内存泄露情况
    比如:BroadCastReceiver、Cursor、Bitmap、IO流、自定义属性attribute
attr.recycle()回收。
当不需要使用的时候,要记得及时释放资源。否则就会内存泄露。
  • 无限循环动画
    没有在onDestroy中停止动画,否则Activity就会变成泄露对象。
    比如:轮播图效果。
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值