Android性能优化(二)-内存优化

1,248 阅读8分钟

一、常见内存问题

常见的内存问题主要分为三种:

1、内存抖动

指在短时间内有大量的对象被创建或者被回收的现象

危害:gc频繁,gc时会发生stw,导致卡顿

2、内存泄漏

指程序在申请内存后,无法释放已申请的内存空间

危害:一次内存泄漏似乎不会有很大影响,但内存泄漏堆积后的后果就是内存溢出,导致崩溃。即使没发生内存溢出,占据较多内存后,会引发gc,导致卡顿

3、内存溢出

指程序运行要用到的内存大于能提供的最大内存,程序运行不了的现象

危害:程序无法运行

二、常用的内存分析工具

1、Memory Profiler

AndroidStudio提供的内存分析工具,常用于直观判断是否有内存抖动和内存泄漏

如图所示,可以直观的看出来内存的波动,从而判断是否有内存问题

2、Mat

Mat可以用于深度分析内存抖动和内存泄漏

使用方法如下:

1、先用Memory Profiler的堆转储功能,记录下一段时间内的内存情况,存储为hprof文件。

2、使用hprof-conv工具,转换从Memory Profiler保存下来的hprof文件。因为Mat无法解析直接从Memory Profiler保存下来的文件,但是Android官方提供了转换工具hprof-conv。hprof-conv在安卓SDK中的platform-tools目录中。使用命令如下

hprof-conv 旧hprof文件 转换后的hprof文件

3、使用Mat解析转换后的hprof文件

点击下图该按钮打开hprof文件

4、Mat的基本功能

查看某个类引用相关

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

查看GC Root

在dominator_tree中,查找Activity->Path To GC Roots->with all references

在本案例中,结果如下:

结果出了系统类引用外,Activity被单例PluginManager给引用了,造成了泄漏,Activity无法释放

3、LeakCanary

LeakCanary是一个自动化的内存泄漏分析工具

使用方法

1、增加依赖

compile 'com.squareup.leakcanary:leakcanary-android:1.6.1'

2、Application中开启检测功能

public class MyApplication extends Application {


    @Override
    public void onCreate() {
        super.onCreate();
        if (LeakCanary.isInAnalyzerProcess(this)) {
            // This process is dedicated to LeakCanary for heap analysis.
            // You should not init your app in this process.
            return;
        }
        LeakCanary.install(this);
    }
}

3、操作app,如果有泄漏则会通知出现在手机通知栏中,打开后,可看到以下这种内存泄漏的界面

LeakCanary原理

1、RefWatcher.watch() 创建一个 KeyedWeakReference 到要被监控的对象。

2、然后在后台线程检查引用是否被清除,如果没有,调用GC。

3、如果引用还是未被清除,把 heap 内存 dump 到 APP 对应的文件系统中的一个 .hprof 文件中。

4、在另外一个进程中的 HeapAnalyzerService 有一个 HeapAnalyzer 使用HAHA 解析这个文件。

5、得益于唯一的 reference key, HeapAnalyzer 找到 KeyedWeakReference,定位内存泄露。

6、HeapAnalyzer 计算 到 GC roots 的最短强引用路径,并确定是否是泄露。如果是的话,建立导致泄露的引用链。

7、引用链传递到 APP 进程中的 DisplayLeakService, 并以通知的形式展示出来。

判断对象是否能被回收,是使用弱引用结合引用队列实现,当一个对象能被回收,则会被加入到关联的引用队列中。以此来判断对象是否泄漏

下面的代码是参照LeakCanary检测是否泄露的源码所写的一段检测代码,基本可以体现出leakCanary的泄露检测原理

class RefWatcher {

    private val referenceQueue = ReferenceQueue<Any>()

    private val retainedKeys = HashSet<String>()

    private val gcTrigger = GcTrigger.Default

    private val executor = Executors.newSingleThreadExecutor()

    /**
     * 在Activity destroy时,执行判断是否泄露
     */
    fun watch(obj : Any) {
        val key = UUID.randomUUID().toString()
        retainedKeys.add(key)
        val reference = KeyedWeakReference(key, obj, referenceQueue)
        executor.execute {
            //清除被加到队列中的对象
            removeReachableReferences()
            //第一次判断
            if(gone(reference)) {
                //没有泄露
            } else {
                //跑一遍gc,gc的同时会让线程挂起100ms
                gcTrigger.runGc()
                //再清除一次被加入ReferenceQueue中的引用
                removeReachableReferences()
                //再判断一次
                if(gone(reference)) {
                    //没有泄露
                } else{
                    //判断为泄露
                }
            }
            
        }
    }

    /**
     * 当一个对象可以被gc回收时,会被加入referenceQueue
     * 被加入到referenceQueue的引用,则判定对象不会泄露,从retainsKey中移除
     */
    private fun removeReachableReferences() {
        var ref  = referenceQueue.poll() as KeyedWeakReference
        while(ref != null) {
            retainedKeys.remove(ref.key)
            ref = referenceQueue.poll() as KeyedWeakReference
        }
    }

    /**
     * retainedKeys中不包含的,则判定为可回收,不会泄露
     */
    private fun gone(reference: KeyedWeakReference) : Boolean {
        return !retainedKeys.contains(reference.key)
    }

}

4、Matrix的ResourceCanary

ResourceCanary是腾讯的APM中检测内存泄漏的模块,该模块实现和leakCanary十分接近,有两个点不同:

1、leakCanary在二次判断前,执行gc,后线程挂起100秒,因为调用gc后,虚拟机不一定马上执行gc,但这种判断方法存在误判的可能,可能确实没有gc。ResourceCanary改良了做法,在gc前,设定一个普通可回收的对象的弱引用,将这个引用作为哨兵,当有gc发生,则这个对象被回收,反之,则这个对象还存活。依靠这个对象判断虚拟机是否真正执行了gc。

2、leakCanary分析的是完整的hprof文件,而ResourceCanary是先dump完整的hprof文件,后裁剪。比leakCanary多了一步裁剪工作,减小了hprof文件大小

但是,无论是leakCanary还是ResourceCanary都不适合线上直接使用,因为完整的hprof文件很大,可能高达几百兆,不适合线上执行。

5、字节跳动的Tailor

Tailor是一个内存快照裁剪压缩工具,通过hook技术在c层write时裁剪文件,不用存完整的hprof文件,避免dump下来完整的hprof文件,可线上使用

该库的具体实现可查看:https://github.com/bytedance/tailor

6、StrictMode

StriceMode是谷歌官方出的运行时检测工具,主要包含虚拟机策略和线程策略,虚拟机策略可以检测Activity泄漏、Sql对象泄漏,某个类实例个数是否超出等,检测结果可以从log中打印

        StrictMode.setVmPolicy(
                new StrictMode.VmPolicy.Builder()
                        //检测Activity泄漏
                        .detectActivityLeaks()
                        //检测数据库对象泄漏
                        .detectLeakedSqlLiteObjects()
                        //检测某个类的实例个数
                        .setClassInstanceLimit(PluginManager.class, 1)
                        //检测结果在log中打印,也可选择如果泄漏就退出应用
                        .penaltyLog()
                        .build());

三、内存抖动和内存泄漏

1、内存抖动

如何发现:使用memory profiler 观察,内存呈锯齿状,忽高忽低

常见原因:一般是程序频繁创建对象引起。或者是内存泄露导致了内存不足,再申请频繁引发gc导致

危害:频繁gc,导致卡顿

解决方案:通常可利用Memory Profiler观察内存抖动,查看内存分配较多,实例较多的部分,找出堆栈排查

2、内存泄漏

定义: 内存中存在没有用的对象回收不了

危害: 导致可用内存逐渐减少,严重时可能导致内存溢出

解决: 通过memory profiler 初步排查,可用内存是否逐渐减少,如果存在这种现象则可能有内存泄露。

在as.中使用堆转储功能下载hprof文件,通过platform tools中的工具转换格式后用mat打开分析。

通常找activity,查看存在对象个数,超过一个的不正常,用mat查看它的gc roots引用,找到原因处理

四、常见的内存泄漏原因

1、集合类

集合类添加元素后,在使用完后未及时清理集合,导致集合中的元素无法释放

List<Object> objectList = new ArrayList<>();        
       for (int i = 0; i < 10; i++) {
            Object o = new Object();
            objectList.add(o);
            //o对象已经没用了,但是objectList还存在,o无法释放
            o = null;
        }

2、静态变量

static Context mContext;

如果Activity被静态变量引用,则Activity会无法释放,需要特别注意

3、单例

单例这种情况我们特别容易忽略,需要养成习惯避免泄漏

public class SingleInstanceClass {
    
   private static SingleInstanceClass instance;
   private Context mContext;
   private SingleInstanceClass(Context context) {
       mContext = context;
   }
   
   public static SingleInstanceClass getInstance(Context context) {
       if (instance == null) {
           instance = new SingleInstanceClass(context);
       }
       return instance;
   }
}

这种情况下,context如果传的是Activity就会泄漏,应该传Application的Context

4、非静态内部类/匿名类

非静态内部类/匿名类会持有外部类的引用,而静态内部类不会

这种情况经常发生在使用Handler的情况时,Handler被Message引用,而消息可以延时触发,比如延时6秒触发。而在6秒内Activity已经销毁了。这时Handler还不能释放,而Handler是非静态内部类/匿名类情况下,则会引用Activity,这时会造成泄漏

5、小结

本文介绍了三种常见的内存问题,6种内存分析和检测的工具,内存抖动、内存泄漏的常见处理方式,以及Android中多种常见的由于编码不规范造成内存泄漏的原因。实际生产中,内存问题非常隐蔽难以发现,最好是配合线上线下处理,线下使用如leakCanary这种优秀工具检测,线上通过对重点模块内存,oom率,gc次数等进行回传,针对部分用户进行内存快照回传自动化分析监控等操作,来持续地以较低成本进行内存优化。