Android 内存优化-命令行定位内存泄露,Monitors及Profiler追寻内存问题

一、如何定位内存泄漏

1、Android Studio

  1. 通过命令行

     打开AndroidStudio,选中Monitors选项卡,这个时候选中Memory,会有一个可视化的视图。

打开AndroidStudio,选中Terminal选项卡,运行项目到真机上,进入主界面操作各个界面,然后重新进入主界面,这个时候项目中应该只有一个MainActivity的实例,此时在Terminal中输入 

adb shell dumpsys meminfo  进程名 -d 

举例

adb shell dumpsys meminfo com.panda.app -d 

就有目前栈中所有的Activity的实例,如果数量大于1,说明有内存泄漏的界面。

报错参考  

    2.通过Monitors

    打开AndroidStudio,我用的是Android Studio4.0,Android Studio3.0以下才有选中Monitors选项卡,这个时候选中Memory,会有一个可视化的视图。

操作App一遍,然后回退到MainActivity,先点击购物车(手动触发GC),再点击一下打点(生成horof文件),稍等片刻,会在AndroidStudio生成一个窗口。

点击分析内存的Task,会出现一个分析视图,然后点击运行,查看是否内存泄漏的Activity。

通过结果分析我们发现,WebViewActivity有内存泄漏,泄漏点是其中的WebView持有界面的Context。

Android Studio4.0自带的模拟器会跟着进入一个Android Device Monitor

Android studio3.0以上,我用的最新的Android Studio4.0,使用的是Android Profiler

点击跳过之后重新GC回收再Dmup Java heap

2、MAT

MAT早期是Eclipse中的插件,但可独立运行:

下载地址:https://www.eclipse.org/mat/

第一步:生成horof对比文件

MAT内存分析基本是靠手动来分析,所以要生成对比文件,horof文件的生成参考Monitor一节,第一个文件生成:项目刚运行时。第二个文件的生成:项目操作运行一遍退回到第一个文件生成的界面,这时候点击AndroidStudio的Capture选项卡,会找到刚才我们生成的两个文件。

选中文件,导出到桌面,生成MAT可以读取的文件类型。

第二步对比horof文件

打开MAT导入刚才生成的两个文件,选中Overview选项卡,再选择下面的Histogram按钮,

再点击Nacigation History选项卡,然后右键点击histogram文件,把两个文件都加入对比。

这个时候会产生一个对比选项卡,然后点击右边的红色感叹号,进行对比,会生成一个Compared Tables选项卡,通常我们分析的内容是byte【】。

第三步分析疑似内存泄漏点

选中byte[],右键按下图依次操作,这个操作主要看那些地方持有这些数组的引用。

点击运行之后会生成一个新的选项卡,这个选项卡主要表示哪些类持有这些引用,通常我们主要分析BitMap。

选中BitMap 右键一路Next,找到该类的引用。

这时会生成一个更加详细的疑似内存泄漏点,这个列表会定位到我们项目中的类名,通过查看项目的源码来定位是否有内存泄漏。

可以看到定位的几个点,其中有CrashHandler、BroadCastMVP,和腾讯WebView。

腾讯WebView是已知问题,那么BradCastMvp是如何泄漏了呢?查看源码可以看到是这个类中静态持有了Context。

如果Context引用的是Activity或service或广播等的对象,只要是静态的,它的生命周期就很长

出现内存泄露主要去注意静态怎么来用?常见两种:

  1. 静态的变量
  2. 静态的类

3、三方工具:如LeakCanary

方式

优点

缺点

备注

AS

快速

可能有遗漏点

 

MAT

全面

需要手动排查

 

LeakCanary

傻瓜式

可能有遗漏点

 

二、哪些操作可能造成长期的内存泄漏

  1. 静态变量-非静态内部类

LeakMemoryTest是LeakMemoryActivity的内部类,全局静态变量leakMemoryTest引用的是LeakMemoryTest这个内部类,在leakMemoryFunction()方法(包含在onCreate()中)里面创建了LeakMemoryTest的对象,这个对象是静态变量存在当前的Activity里,而LeakMemoryTest就是所谓的非静态内部类

我们启动之后再退出,然后检测一下内存,这个时候已经发现该界面内存泄漏了。

原理:静态变量一旦初始化之后,不会随着该界面的销毁会销毁,而是等该应用进程销毁掉才会销毁,那么静态变量的内部类为什么会持有该Activity?因为非静态的内部会持有外部对象的引用(比如:非静态内部类LeakMemoryTest持有静态变量leakMemoryTest的引用)

那该如何操作?

有两种方式,去除静态变量或者是把LeakMemoryTest 变成静态内部类即可(静态内部类不会持有外部对象的引用),

如加上static :

static class LeakMemoryTest{
    public LeakMemoryTest() {
    }
}

  1. 线程操作

先上代码,请看下图。

这里面是个线程操作,退出该界面再来检查一下Activity,发现已经泄露了。

原理:Java中的Thread有一个特点就是她们都是直接被GC Root所引用,也就是说Dalvik虚拟机所有被激活状态的线程都是持有强引用,导致GC永远都无法回收掉这些线程对象,除非线程被手动停止并置为null或者用户直接kill进程操作。

内存泄露匿名内部对象会持有外部类的引用,如:new Thread(new Runnable())

参考的正确写法:   自定义的静态内部类(通过继承Thread的类去重写

private void leakMemoryFunction() {
    MyStread myStread = new MyStread();
    myStread.start();
}

public static class MyStread extends Thread{
    @Override
    public void run() {
        super.run();
        while (true) {
            Log.d("haha", "hehe");
        }
    }
}

备注:匿名内部的Handler 也是同样的道理

重点

3.Activity Fragment 静态持有

参见 第一小节的 MAT  BraodCastMVP

4.单例模式传入的是Activity的Context。

下面给出参考写法

//有参构造方法
public HttpServerPlayer(Context context) {
    mContext = context.getApplicationContext(); // 防止内存泄漏
}

//单例设计模式
@Deprecated
public static HttpServerPlayer getInstance(Context context) {
    if (instance == null) {
        synchronized (HttpServerPlayer.class) {
            if (instance == null) {
                instance = new HttpServerPlayer(context);
            }
        }
    }
    return instance;
}

单例长期的内存泄露

/**
 * 内存测试
 * 内存泄露的本质是:长生命周期对象持有短生命周期对象的引用
 * 所以像MemoryTest的生命周期很长,而SplashActivity生命周期短,点击跳转会执行finish()就没有了
 * 这时候长生命周期MemoryTest持有SplashActivity的对象的引用,这时候就会造成内存泄露
 */
public class MemoryTest {

    /**
     * 静态的变量跟app应用的生命周期是一样的,只要应用不销毁掉,这个实例就会存在内存当中
     * 它会持有Context
     */
    private static MemoryTest mInstance;

    /**
     * Context是被强引用状态,不会被GC回收掉的
     */
    private final Context context;

    //getInstance()方法下的MemoryTest构造方法
    public MemoryTest(Context context) {
        //this.context = context; //不要直接将传入的context直接赋给创建的这个类里面
        //防止内存泄露,不要用普通的context(带来的是组件的对象),而生命周期:组件<App
        //可以这样想象:组件(如:Activity)只是一些线程,而APP是一个进程,线程是小于进程的
        this.context = context.getApplicationContext();//拿到的是Application级别的,跟应用的生命周期一致
    }

    /**
     * 单例设计模式
     * @param context 传入当前Activity => Context
     * @return
     */
    public static MemoryTest getInstance(Context context) {
        if (mInstance == null) {
            mInstance = new MemoryTest(context);
        }
        return mInstance;
    }
}

5.Android系统的内存泄漏

当SDK版本在 19 到 23 之间时,LeakCanary 会检测出系统软键盘有内存泄漏。

可参见下面这篇文章。

http://www.imooc.com/article/13913

6.Android 原生 WebView (混合编程-JS代码)

目前还没有找到有效的解决办法,需要JavaScript写的好,除非多进程运行,将WebView方法另一个线程中。

7.使用系统服务没有反注册

比如使用Android系统的传感器,要进行注册,但没有反注册有时也会造成内存泄漏,下图代码:

private void leakMemoryFunction() {
    SensorManager sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
    Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ALL);
    sensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_FASTEST);
}

长期的内存泄露导致OOM(OutOfMemory)

三、哪些操作可能造成内存泄漏

刚才基本是Activity层级的内存泄漏,这种泄漏是长期的,要优先解决,但Java层级的短期内存泄漏往往被Android开发者所忽略,OOM(OutOfMemory)的产生就是长期的内存泄漏 + 短期内存泄漏 + 压力的内存使用造成的。

  1. 多余的成员变量(全局变量)和临时变量(局部变量)

在Java中 对象的引用是有作用域的,类中的成员变量会占用堆(heap)中一定的内存空间,如果该类持续被引用,那么该类中无效的成员变量也会因为强引用而无法释放。

如果说这个方法当中,没有被其他的类或方法所使用,最好去创建它的局部变量

我们先来建个纯净的空页面:看下运行时候占用的内存空间

系统稳定在19.37MB。

下面我们在其中一个方法中创建成员变量(ArrayList<String> arr)

下面我们看下内存的使用,已经到达了24.93M:

下面我们主动触发一下GC,系统回收不必要的对象,系统到达了22.29M.

下面我们做下优化,由于该集合没有在其他方法中使用,那么我把成员变量转变成局部变量,看下使用的内存,跟使用成员变量差不多。

下面手动触发GC,可以发现竟然到达了空Activity的内存使用,18.45MB。

这个成员变量是直接被Activity所引用,Activity不被销毁,这个ArrayList就不会被销毁,当把这个全局变量转换为这个方法里面的局部变量,那这个方法就是作用域,并且这个作用域是放在onCreate()里面的,当这个方法执行完,这个对象就可以被回收了

这样在低端机型上,当系统触发GC时可大大降低OOM的可能。

2.没有在合适的时候释放成员变量

相关场景容器Bitmap资源文件、IO等。

先展示下相关的耗时方法代码及大量产生的局部变量。

点击运行从19M上升到41,手动触发GC降到37M.

下面我们把代码改造一下,在合适的时候把无用的对象置为null(GC会回收掉),可以发现手动触发GC,应用使用的内存又回到了初始值

3.比较大的容器即使关闭界面也会造成短期内存问题,虽然不会造成长期的内存泄漏。

代码如下,这个界面有个较大的集合:

操作步骤如下:打开页面,再关闭页面,按理来说这个集合会从内存清除掉,但是页面关闭了,内存也没有降低下来。

我们来分析下此时的内存dump文件,看下为什么界面关闭了,内存还依然这么高(对象依然还在内存里)。

上图可看到这个对象还依然在内存里。

下面我们来加句代码。

@Override
protected void onDestroy() {
    super.onDestroy();
    arr.clear(); //数据清空
    arr = null;
    System.gc();
}

 按理说这样总可以了吧,但是。。。。。 第一次打开有效,多次打开也会出现同样的问题。

那这样我们把这个集合改成软引用试试。

private SoftReference<ArrayList<LeakMenoryBean>> soft;

经过测试发现很容易空指针在Activity里面做弱和软引用也不太现实

目前来说这种如果想尽早释放,还是建议在 onDestroy里面做数据清空(加快GC回收)及GC操作(不太建议在Android里做过多GC,容易卡顿)。

JVM回收原理--分代回收

JVM中的分代回收算法:

现代的JVM虚拟机都采用了分代回收,即分为新生代,新生代采用复制回收算法,老年代采用标记整理算法

新生代:

new出来的对象一般都放在新生代

hotspot虚拟机新生代分 为三块,Eden区、From Sunvivor、To Survivor区(其中两者相等),大小比例8:1:1

之所以新生代采用复制算法,并且这样设计大小,是有依据的,IBM研究过新生代的对象98%的对象都熬不到下一次GC,即“朝生夕死”,所以这样设计效率很高。

新生代发生GC(minor GC)的时候,会将Eden区存活的对象和Survivor中的对象,一起复制到另一个Survivor中,然后清空Eden和Survivor区。

新生代的对象可能在多次Minor GC后移到老年代。

老年代:

老年代的对象,一般存活时间较长,因此回收效率不高,性价比不高。

老年代的对象一般来自下面几种情况:

1.经历多次minor GC未被回收(一般是15次,在虚拟机中对象有个age属性,通常每经历一次minor GC,对象的age+1,当age大于15即放到老年代中)

2.new 的对象过大(比如ArrayList数组或LinkedList链表或Map集合),而新生代经历了minor GC之后,依然放不下这个对象,那么这个对象将直接被放入到老年代。

3.可通过启动参数设置-XX-PretenureSizeThreshold=1024(单位为字节,默认为0)来代表超过多大时就不在新生代分配,而是直接在老年代分配。

4.如1中所述,新生代的某个age的对象,达到新生代Survivor空间一半以上时,大于或等于这个年代里的这些对象将会被移到老年代中去。

四、哪些操作可以减少内存使用压力

  1. 较大的内存缓存类可使用弱引用和引用,BitMap。
  2. 字符串拼接尽量使用StringBuffer和StringBudler。
  3. 功能需要去使用静态变量。
  4. 大量的字符串存储建议使用文件存储,尽量不用sp。
  5. 耗时操作要放入异步任务
  6. 成员变量转化成局部变量,适当的时机释放成员变量。
  7. 尽量使用效率高的代码,比如增强for循环,减少使用枚举。
  8. 三种常见的布局优化。
  9. 方法中尽量减少多余的局部变量,比如 return 1 +2;而不是 int a = 1 +2:return a
  10. 尽量使用Android优化过的容器,如SparseArray,SparseBooleanArray, 与 LongSparseArray这种集合的使用场景并不一定适用多数场景。
  11. 广播和service 注册时机,并不一定是一进入App就去注册
  12. 三方库调研和对比,有些时候是某些库拖垮了我们的整个应用。
  13. 多进程控制整个App的内存使用。
  14. 三方图片加载框架在加载图片质量要求不高时,尽量不使用8888.
  15. 适当使用ONtrimMemory 处理资源的释放。

 

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Alex-panda

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值