Android性能优化之被忽视的Memory Leaks

起因

写博客就像讲故事。得有起因,经过,结果,人物。地点和时间。今天就容我给大家讲一个故事。

人物呢。肯定是我了。

故事则发生在近期的这两天,地点在coder君上班的公司。那天无意中我发现了一个奇怪的现象,随着我点开我们App的页面,Memory Monitor中显示占用的内存越来越多(前面的页面已经finish掉了)。

咦?什么鬼?

经过

有了问题就解决嘛,俗话说的好。有bug要上。没有bug写个bug也要上。那究竟是是什么问题会引起这个现象呢?

Android中内存相关的问题无非就是这么几点:

  • Memory Leaks 内存泄漏
  • Memory Churn 内存抖动
  • OutOfMemory 内存溢出

阿西吧。细致想想怎么这么像内存泄漏呢。那究竟是不是呢?那我们就一点一点分析一下呗。

内存相关数据

关于内存我们可能想了解的数据大概有三点:

  • 总内存

    private String getTotalMemory() {
        String str1 = "/proc/meminfo";// 系统内存信息文件
        String str2;
        String[] arrayOfString;
        long initial_memory = 0;
        try {
            FileReader localFileReader = new FileReader(str1);
            BufferedReader localBufferedReader = new BufferedReader(
                    localFileReader, 8192);
            str2 = localBufferedReader.readLine();// 读取meminfo第一行,系统总内存大小
            arrayOfString = str2.split("\s+");
            for (String num : arrayOfString) {
                Log.i(str2, num + "	");
            }
            initial_memory = Integer.valueOf(arrayOfString[1]).intValue() * 1024;// 获得系统总内存,单位是KB,乘以1024转换为Byte
            localBufferedReader.close();
        } catch (IOException e) {
        }
        return Formatter.formatFileSize(getBaseContext(), initial_memory);// Byte转换为KB或者MB,内存大小规格化
    }
    
  • 系统当前可用内存

    private String getAvailMemory() {
        // 获取android当前可用内存大小
        ActivityManager am = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
        ActivityManager.MemoryInfo mi = new ActivityManager.MemoryInfo();
        am.getMemoryInfo(mi);
        //mi.availMem; 当前系统的可用内存
        return Formatter.formatFileSize(getBaseContext(), mi.availMem);// 将获取的内存大小规格化
    }
    
  • 我们能够使用的内存

    每个Android设备都会有不同的RAM总大小与可用空间。因此不同设备为app提供了不同大小的heap限制。你能够通过调用getMemoryClass())来获取你的app的可用heap大小。假设你的app尝试申请很多其它的内存,会出现OutOfMemory的错误。

    在一些特殊的情景下,你能够通过在manifest的application标签下加入largeHeap=true的属性来声明一个更大的heap空间。假设你这样做,你能够通过getLargeMemoryClass())来获取到一个更大的heap size。

    然而。能够获取更大heap的设计本意是为了一小部分会消耗大量RAM的应用(比如一个大图片的编辑应用)。不要轻易的由于你须要使用大量的内存而去请求一个大的heap size。

    仅仅有当你清楚的知道哪里会使用大量的内存而且为什么这些内存必须被保留时才去使用large heap. 因此请尽量少使用large heap。使用额外的内存会影响系统总体的用户体验,而且会使得GC的每次执行时间更长。

    在任务切换时,系统的性能会变得大打折扣。

    另外, large heap并不一定能够获取到更大的heap。在某些有严格限制的机器上,large heap的大小和通常的heap size是一样的。因此即使你申请了large heap。你还是应该通过执行getMemoryClass()来检查实际获取到的heap大小。

    private String getAllocationMemory() {
        // 获取系统分配的内存大小
        ActivityManager am = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
        //开启了android:largeHeap="true",米4系统能分配的内存为512M,不开启为128M
        //return  am.getLargeMemoryClass()+"";
        //return  am.getMemoryClass()+"";
    }       
    

Java中的四种引用

開始分析之前。有必要先了解下Java的内存分配与回收。

Java的数据类型分为两类:基本数据类型、引用数据类型。

基本数据类型的值存储在栈内存中,而引用数据类型须要开辟两块存储空间。一块在堆内存中。用于存储该类型的对象;还有一块在栈内存中。用于存储堆内存中该对象的引用。

当中引用类型变量分为四类:

  • 强引用

    最经常使用的引用形式。把一个对象赋给一个引用类型变量,则为强引用。

    仅仅要一个引用是强引用,则垃圾回收器永远都无法回收这个对象的内存空间,除非JVM终止。

  • 软引用

    当内存资源充足的时候,垃圾回收器不会回收软引用相应的对象的内存空间;但当内存资源紧张时,软引用所相应的对象就会被垃圾回收器回收。

    //创建一个Student类型的软引用
    
    SoftReference<Student> sr = new SoftReference<Student>(new Student());
    
  • 弱引用

    无论JVM内存资源是否紧张,仅仅要垃圾回收器执行,弱引用所相应的对象就会被释放。

  • 虚引用

    虚引用等于没有引用,无法通过虚引用訪问其相应的对象。

    软引用和弱引用在其对象被回收之后。这些引用会被加入到引用队列中去;而虚引用在其对象被回收之前,虚引用就被加入到引用队列中去了。因此虚引用能够在其对象被释放之前进行一些操作。

    虚引用和引用队列绑定的方法:

    //创建引用队列  
    ReferenceQueue<String> queue = new ReferenceQueue<String>();  
    //创建虚引用,并绑定引用队列  
    PhantomReference<String> str = new PhantomReference<String>("啦啦啦",queue);   
    

Garbage Collection Android中的垃圾回收

Android系统会在适当的时机触发GC操作,一旦进行GC操作,就会将一些不再使用的对象进行回收

执行GC操作的时候,全部线程的不论什么操作都会须要暂停。等待GC操作完毕之后,其它操作才干够继续执行。

通常来说,单个的GC并不会占用太多时间,可是大量不停的GC操作则会显著占用帧间隔时间(16ms)。假设在帧间隔时间里面做了过多的GC操作,那么自然其它相似计算。渲染等操作的可用时间就变得少了

Memory Leaks内存泄漏

内存泄漏表示的是不再用到的对象由于被错误引用而无法进行回收。发生内存泄漏会导致Memory Generation中的剩余可用Heap Size越来越小,这样会导致频繁触发GC,更进一步引起性能问题。

总结起来事实上非常easy:存在无效的引用!

内存泄露能够引发非常多的问题。常见的内存泄露导致问题例如以下:

  • 应用卡顿。响应速度慢(内存占用高时JVM虚拟机会频繁触发GC);

  • 应用被从后台进程干为空进程;

  • 应用莫名的崩溃(也就是超过了HeepSize阈值引起OOM)。

内存泄漏分析工具

看到这些问题。突然发现好像离真相越来越近了0.0。

想要更加清楚地实时知晓当前应用程序的内存使用情况,我们须要通过一些工具来实现。比較好用的工具有两种:

  • Memory Analyzer Tool
  • LeakCanary

以下我们分开介绍。

Memory Analyzer Tool

Memory Analysis Tools(点我下载)是一个专门分析Java堆数据内存引用的工具,我们能够使用它方便的定位内存泄露原因,核心任务就是找到GC ROOT位置。

接下来说下使用步骤。

抓取内存信息

AndriodStudio中抓取内存信息还是非常方便的,有两种方法:

  • 使用Android Device Monitor

    点击Android Studio工具栏上的Tool–>Android Device Monitor

    在Android Device Monitor界面中选在你要分析的应用程序的包名,点击Update Heap来更新统计信息,然后点击Cause GC就可以查看当前堆的使用情况,点击Dump HPROF file,将该应用当前的内存信息保存成hprof文件,放在桌面就可以。操作例如以下图

  • 直接获取

    Android Studio的最新版本号能够直接获取hprof文件,可是注意在使用之前一定要手动点击 Initiate GCbutton手动触发GC。这样抓到的内存使用情况就是不包含Unreachable对象的。

    稍等片刻,生成的文件会出如今captures中。然后选择文件,点击右键转换成标准的hprof文件。就能够在MAT中打开了。

使用MAT工具查看分析

这里我写了个简单的demo来測试,这个demo一共同拥有两个页面,在跳转到第二个页面之后,新开一个现成去打印activity信息。

/**
 * 打印ActivityName
 */

public void printActivityName() {

    for (int i = 0; i < 100; i++) {

        new Thread(new Runnable() {

            @Override

            public void run() {

                while (true)

                    try {

                        Thread.sleep(1000 * 30);

                        Log.e(ActivityHelper.class.getSimpleName(), ((Activity) mContext).getClass().getSimpleName());

                    } catch (InterruptedException e) {

                        e.printStackTrace();

                    }

            }

        }).start();

    }

}   

多次进入SecondActivity之后会发现内存一直在增长,并没有减少。

而且log里会不停的输出log,打印当前activity的name。

在MAT中打开抓取到的文件后如图

MAT中提供了非常多的功能,这里我们仅仅要学习几个最经常使用的就能够了。上图最中央的那个饼状图展示了最大的几个对象所占内存的比例,这张图中提供的内容并不多。我们能够忽略它。红色框中有两个非常实用的工具是我们经常使用的。

Histogram能够列出内存中每个对象的名字、数量以及大小。

Dominator Tree会将全部内存中的对象按大小进行排序,而且我们能够分析对象之间的引用结构。

我们先来看Histogram

我们应该怎样去分析内存泄漏呢?

即分析大内存的对象。

可是假如我们有目标对象的话,左上角值支持正則表達式的,我们输入SecondActivity。这里我们看到。我们有5个SecondActivity的实例。由于我们引用SecondActivity的现成没有销毁。导致会有非常多实例。

接下来对着SecondActivity右键 -> List objects -> with incoming references查看详细SecondActivity实例。例如以下图所看到的:

假设想要查看内存泄漏的详细原因,能够对着随意一个MainActivity的实例右键 -> Path to GC Roots -> exclude weak references,结果例如以下图所看到的:

能够看到红色框中,由于我们的线程持有SecondActivity的实例。全部导致内存泄漏。

此外。我们能够选择以我们项目的包结构的形式来查看

接下来我们看下Dominator Tree。

关于Dominator Tree我们须要注意三点:

  • 首先Retained Heap表示这个对象以及它所持有的其它引用(包含直接和间接)所占的总内存,因此从上图中看,前两行的Retained Heap是最大的。我们分析内存泄漏时,内存最大的对象也是最应该去怀疑的。
  • 带有黄点的对象就表示是能够被GC Roots訪问到的,依据上面的解说,能够被GC Root訪问到的对象都是无法被回收的。
  • 并非全部带黄点的对象都是泄漏的对象,有些对象系统须要一直使用。本来就不应该被回收。我们能够注意到。有些带黄点的对象最右边会写一个System Class,说明这是一个由系统管理的对象,并非由我们自己创建并导致内存泄漏的对象。

如今我们能够对着我们想查看的内容点击右键 -> Path to GC Roots -> exclude weak references,为什么选择exclude weak references呢?由于弱引用是不会阻止对象被垃圾回收器回收的。所以我们这里直接把它排除掉。然后一步一步分析。

LeakCanary

leakcanary是一个开源项目,一个内存泄露自己主动检測工具,是著名的GitHub开源组织Square贡献的,它的主要优势就在于自己主动化过早的发觉内存泄露、配置简单、抓取贴心,缺点在于还存在一些bug,只是正常使用百分之九十情况是OK的,其核心原理与MAT工具相似。

由于配置十分简单。这里就不多说了,官方文档。

我们看下分析结果

简单直白!

常见内存泄漏情况

  • 构造Adapter时。没有使用缓存的 convertView

  • Bitmap对象不在使用时调用recycle()释放内存

  • Context使用不当造成内存泄露:不要对一个Activity Context保持长生命周期的引用。

    尽量在一切能够使用应用ApplicationContext取代Context的地方进行替换。

  • 非静态内部类的静态实例easy造成内存泄漏:即一个类中假设你不能够控制它当中内部类的生命周期(譬如Activity中的一些特殊Handler等)。则尽量使用静态类和弱引用来处理(譬如ViewRoot的实现)。

  • 警惕线程未终止造成的内存泄露。譬如在Activity中关联了一个生命周期超过Activity的Thread,在退出Activity时切记结束线程。一个典型的样例就是HandlerThread的run方法是一个死循环,它不会自己结束,线程的生命周期超过了Activity生命周期。我们必须手动在Activity的销毁方法中中调运thread.getLooper().quit();才不会泄露。

  • 对象的注冊与反注冊没有成对出现造成的内存泄露。譬如注冊广播接收器、注冊观察者(典型的譬如数据库的监听)等。

  • 创建与关闭没有成对出现造成的泄露;譬如Cursor资源必须手动关闭,WebView必须手动销毁。流等对象必须手动关闭等。

  • 不要在执行频率非常高的方法或者循环中创建对象(比方onmeasure),能够使用HashTable等创建一组对象容器从容器中取那些对象。而不用每次new与释放。

  • 避免代码设计模式的错误造成内存泄露;譬如循环引用,A持有B。B持有C。C持有A,这种设计谁都得不到释放。

结果

真相仅仅有一个,那就是确实是由于内存泄漏才出现我遇到的情况。程序猿嘛,谁还不踩个坑,跳出来,拍拍身上的灰尘。总结一下,过两天又是一条棒棒的coder。源代码

原文地址:https://www.cnblogs.com/claireyuancy/p/7354166.html