android 内存优化详解

目录

一、性能优化介绍

二、JVM,内存回收机制GC

2.1. JVM 内存模型

2.2. JVM 内存区域

2.2.1. 程序计数器(线程私有)

2.2.2.虚拟机栈(线程私有)

2.2.3.本地方法区(私有)

2.2.4.堆(Heap-线程共享)-运行时数据区

2.2.5. 方法区/永久代(线程共享)

2.3. JVM运行时内存

2.3.2. 老年代

2.3.3. 永久代

2.4. 垃圾回收与算法

2.6. JAVA 四种引用类型

2.7内存使用小结 

三、JAVA集合

3.1.接口继承关系实现

3.1.1.集合详细继承图

3.2 SparseArray

3.3 自动装箱拆箱

四、内存泄漏的常见情形及解决办法

4.2、避免内存泄漏(OOM)的一些措施

五、OnTrimMemory内存管理

5.1、OnTrimMemory回调的作用?

5.1.1、哪些组件可以实现OnTrimMemory回调?

5.1.2、OnTrimMemory回调中可以释放哪些资源?

六、LeakCanary内存泄漏工具

6.1 LeakCanary原理:

6.2 Implementation引入

6.3 本地导入源码工程

6.4 差异化编译

七、Android Studio 的Profiler内存分析工具

八、使用Memory Analyzer Tool精确定位内存泄漏之处


 

一、性能优化介绍

  1. 内存(内存溢出、崩溃)
  2. 流畅(卡顿)
  3. 耗损(耗电、流量、网络)
  4. 安装包(APK瘦身)

二、JVM,内存回收机制GC

2.1. JVM 内存模型

 

android 内存优化详解_第1张图片

 

2.2. JVM 内存区域

android 内存优化详解_第2张图片

JVM 内存区域主要分为线程私有区域【程序计数器、虚拟机栈、本地方法区】、线程共享区 域【JAVA 堆、方法区】、直接内存。

线程私有数据区域生命周期与线程相同, 依赖用户线程的启动/结束 而 创建/销毁(在 Hotspot VM 内, 每个线程都与操作系统的本地线程直接映射, 因此这部分内存区域的存/否跟随本地线程的 生/死对应)。

android 内存优化详解_第3张图片

2.2.1. 程序计数器(线程私有)

每条线程都要有一个独立的 程序计数器,这类内存也称为“线程私有”的内存。

正在执行 java 方法的话,计数器记录的是虚拟机字节码指令的地址(当前指令的地址)。如 果还是Native方法,则为空。

这个内存区域是唯一一个在虚拟机中没有规定任何OutOfMemoryError情况的区域。

2.2.2.虚拟机栈(线程私有)

每个方法在执行的同时都会创建一个栈帧(Stack Frame) 用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成 的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

栈帧( Frame)是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接 (Dynamic Linking)、 方法返回值和异常分派( Dispatch Exception)。栈帧随着方法调用而创

android 内存优化详解_第4张图片

2.2.3.本地方法区(私有)

本地方法区和Java Stack作用类似, 区别是虚拟机栈为执行Java方法服务, 而本地方法栈则为 Native方法服务, 如果一个VM实现使用C-linkage模型来支持Native调用, 那么该栈将会是一个 C栈,但HotSpot VM直接就把本地方法栈和虚拟机栈合二为一。

2.2.4.堆(Heap-线程共享)-运行时数据区

堆是被线程共享的一块内存区域,创建的对象和数组都保存在 Java 堆内存中,也是垃圾收集器进行 垃圾收集的重要的内存区域。由于现代VM采用分代收集算法, 因此Java堆从GC的角度还可以 细分为: 新生代( Eden 区 、 From Survivor 区 和 To Survivor 区 )和老年代。

2.2.5. 方法区/永久代(线程共享)

永久代(Permanent Generation), 用于存储被 JVM 加载的类信息、常量、静 态变量、即时编译器编译后的代码等数据. HotSpot VM把GC分代收集扩展至方法区, 即使用Java 堆的永久代来实现方法区, 这样HotSpot的垃圾收集器就可以像管理Java堆一样管理这部分内存, 而不必为方法区开发专门的内存管理器(永久带的内存回收的主要目标是针对常量池的回收和类型 的卸载, 因此收益一般很小)。

运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版 本、字段、方法、接口等描述等信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加 载后存放到方法区的运行时常量池中。 Java 虚拟机对Class文件的每一部分(自然也包括常量 池)的格式都有严格的规定,每一个字节用于存储哪种数据都必须符合规范上的要求,这样才会 被虚拟机认可、装载和执行

2.3. JVM运行时内存

Java堆从GC的角度还可以细分为: 新生代( Eden 区 、 From Survivor 区 和To Survivor 区 )和老年代。

android 内存优化详解_第5张图片

2.3.1. 新生代

是用来存放新生的对象。一般占据堆的1/3空间。由于频繁创建对象,所以新生代会频繁触发 MinorGC进行垃圾回收。新生代又分为 Eden区、ServivorFrom、ServivorTo三个区。

2.3.1.1. Eden区

Java新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老 年代)。当Eden区内存不够的时候就会触发MinorGC,对新生代区进行 一次垃圾回收。

2.3.1.2. ServivorFrom

上一次GC的幸存者,作为这一次GC 的被扫描者。

2.3.1.3. ServivorTo

保留了一次MinorGC过程中的幸存者。

2.3.1.4.  MinorGC的过程(复制->清空->互换)

MinorGC采用复制算法。

1 ˖ eden 、servicorFrom 复制到ServicorTo ,年龄 + 1

首先,把Eden和ServivorFrom区域中存活的对象复制到ServicorTo区域(如果有对象的年 龄以及达到了老年的标准,则赋值到老年代区),同时把这些对象的年龄+1(如果 ServicorTo 不 够位置了就放到老年区);

2 ˖ 清空 eden 、ervicorFrom

清空Eden和ServicorFrom中的对象;

3 ˖ ServicorTo和 ServicorFrom交互

 ServicorTo和ServicorFrom互换,原ServicorTo成为下一次GC时的ServicorFrom 区。

2.3.2. 老年代

主要存放应用程序中生命周期长的内存对象。

    老年代的对象比较稳定,所以 MajorGC 不会频繁执行。在进行 MajorGC 前一般都先进行 了一次 MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。当无法找到足 够大的连续空间分配给新创建的较大对象时也会提前触发一次MajorGC进行垃圾回收腾出空间。

    MajorGC 采用标记清除算法:首先扫描一次所有老年代,标记出存活的对象,然后回收没 有标记的对象。MajorGC的耗时比较长,因为要扫描再回收。MajorGC 会产生内存碎片,为了减 少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。当老年代也满了装不下的 时候,就会抛出OOM(Out of Memory)异常。

2.3.3. 永久代

指内存的永久保存区域,主要存放 Class 和 Meta(元数据)的信息,Class 在被加载的时候被 放入永久区域,它和和存放实例的区域不同,GC 不会在主程序运行期对永久区域进行清理。所以这 也导致了永久代的区域会随着加载的Class的增多而胀满,终抛出OOM异常。

2.3.3.1. JAVA8与元数据

在Java8中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。元空间 的本质和永久代类似,元空间与永久代之间大的区别在于:元空间并不在虚拟机中,而是使用 本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入 native memory, 字符串池和类的静态变量放入 java 堆中,这样可以加载多少类的元数据就不再由 MaxPermSize控制, 而由系统的实际可用空间来控制。

2.4. 垃圾回收与算法

算法视频:https://haokan.baidu.com/v?vid=4381907901736430690&pd=bjh&fr=bjhauthor&type=video

android 内存优化详解_第6张图片

2.4.1. 垃圾回收算法

2.4.1.1.引用计数法

在 Java 中,引用和对象是有关联的。如果要操作对象则必须用引用进行。因此,很显然一个简单 的办法是通过引用计数来判断一个对象是否可以回收。简单说,即一个对象如果没有任何与之关 联的引用,即他们的引用计数都不为 0,则说明对象不太可能再被用到,那么这个对象就是可回收 对象。

2.4.1.2. 可达性分析

为了解决引用计数法的循环引用问题,Java 使用了可达性分析的方法。通过一系列的“GC roots” 对象作为起点搜索。如果在“GC roots”和一个对象之间没有可达路径,则称该对象是不可达的。要注意的是,不可达对象不等价于可回收对象,不可达对象变为可回收对象至少要经过两次标记 过程。两次标记后仍然是可回收对象,则将面临回收。

2.4.2. 标记清除算法Mark-Sweep

基础的垃圾回收算法,分为两个阶段,标注和清除。标记阶段标记出所有需要回收的对象,清 除阶段回收被标记的对象所占用的空间。如图

android 内存优化详解_第7张图片

2.4.3. 复制算法(copying)

为了解决Mark-Sweep算法内存碎片化的缺陷而被提出的算法。按内存容量将内存划分为等大小 的两块。每次只使用其中一块,当这一块内存满后将尚存活的对象复制到另一块上去,把已使用 的内存清掉,如图:

android 内存优化详解_第8张图片

2.4.4.标记整理算法(Mark-Compact)

结合了以上两个算法,为了避免缺陷而提出。标记阶段和Mark-Sweep算法相同,标记后不是清 理对象,而是将存活对象移向内存的一端。然后清除端边界外的对象。如图:

android 内存优化详解_第9张图片

2.5.1、年轻世代

目前大部分JVM的GC 对于新生代都采取Copying算法,因为新生代中每次垃圾回收都要 回收大部分对象,即要复制的操作比较少,但通常并不是按照1:1来划分新生代。一般将新生代 划分为一块较大的Eden空间和两个较小的Survivor空间(From Space, To Space),每次使用 Eden空间和其中的一块Survivor空间,当进行回收时,将该两块空间中还存活的对象复制到另 一块Survivor空间中。

android 内存优化详解_第10张图片

2.5.2、老年代 标记复制算法

而老年代因为每次只回收少量对象,因而采用Mark-Compact算法。

1. JAVA虚拟机提到过的处于方法区的永生代(Permanet Generation),它用来存储class类, 常量,方法描述等。对永生代的回收主要包括废弃常量和无用的类。

2. 对象的内存分配主要在新生代的Eden Space和Survivor Space的From Space(Survivor目 前存放对象的那一块),少数情况会直接分配到老生代。

3. 当新生代的Eden Space和From Space空间不足时就会发生一次GC,进行GC后,Eden Space和From Space区的存活对象会被挪到To Space,然后将Eden Space和From Space进行清理。

4. 如果To Space无法足够存储某个对象,则将这个对象存储到老生代。

5. 在进行GC后,使用的便是Eden Space和To Space了,如此反复循环。

6. 当对象在Survivor区躲过一次GC 后,其年龄就会+1。默认情况下年龄到达15 的对象会被 移到老生代中。

2.5.3、永久代 

用 途 :用来装载Class,方法等信息,默认为64M(Android的运行时应用分配的内存),不会被回收。

对象来源 :像Hibernate,Spring这类喜欢AOP动态生成类的框架,往往会生成大量的动态代理类,因此我们经常遇到java.lang.OutOfMemoryError:PermGen space的错误,这就是Permanent代内存耗尽所导致的错误。

回收频率 :不会被回收。

2.6. JAVA 四种引用类型

详细讲解:https://www.cnblogs.com/liyutian/p/9690974.html

android 内存优化详解_第11张图片

2.6.1. Strong reference强引用

在 Java 中常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引 用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即 使该对象以后永远都不会被用到JVM也不会回收。因此强引用是造成Java内存泄漏的主要原因之 一。

2.6.2. SoftReference软引用

软引用需要用 SoftReference 类来实现,对于只有软引用的对象来说,当系统内存足够时它 不会被回收,当系统内存空间不足时它会被回收。软引用通常用在对内存敏感的程序中。

2.6.3. WeakReference弱引用

弱引用需要用WeakReference 类来实现,它比软引用的生存期更短,对于只有弱引用的对象 来说,只要垃圾回收机制一运行,不管JVM的内存空间是否足够,总会回收该对象占用的内存。

2.6.4. PhatomReference虚引用

虚引用需要PhantomReference类来实现,它不能单独使用,必须和引用队列联合使用。虚 引用的主要作用是跟踪对象被垃圾回收的状态。

2.6.5. ReferenceQueue引用队列

软/弱引用技术可以用来实现高速缓冲器:

android 内存优化详解_第12张图片

ReferenceQueue

android 内存优化详解_第13张图片

2.7内存使用小结 

实际开发中常用的小技巧:

1、尽量使用直接变量,例如:String javaStr = “XXX”;

2、使用 StringBuilder 和 StringBuffer 进行字符串连接等操作;

3、尽早释放无用对象;

4、尽量少使用静态变量;

5、缓存常用的对象:可以使用开源的开源缓存实现,例如:OSCache,Ehcache;

6、尽量不使用 finalize() 方法;

7、在必要的时候可以考虑使用软引用 SoftReference。

三、JAVA集合

3.1.接口继承关系实现

集合类存放于Java.util包中,主要有3种:set(集)、list(列表含Queue)和map(映射)。

1. Collection:Collection是集合List、Set、Queue的基本的接口。

2. Iterator:迭代器,可以通过迭代器遍历集合中的数据

3. Map:是映射表的基础接口

android 内存优化详解_第14张图片

3.1.1.集合详细继承图

 

android 内存优化详解_第15张图片

android 内存优化详解_第16张图片

3.2 SparseArray

SparseArray采用时间换取空间的方式来提高手机App的运行效率,这也是其与HashMap的区别;HashMap通过空间换取时间,查找迅速;

  • SparseArray的key为int,value为Object。
  • 在Android中,数据长度小于千时,用于替换HashMap
  • 相比与HashMap,其采用 时间换空间 的方式,使用更少的内存来提高手机APP的运行效率.
  • android 内存优化详解_第17张图片

 

优点:

  1. 占用内存小,没有额外Entry对象
  2. 没有Auto-boxing

缺点:

  1. 不支持任意类型key,只支持数字类型(int,long)
  2. 数据条很多是效率低于HashMap,二分法查找数据

 

android 内存优化详解_第18张图片

LongSparseLongArray

3.3 自动装箱拆箱

// Generic version

Integer total = 0;

for (int i = 150; i < 1000; i++) {

  total += i;

}



// Generic version

Integer total = 0;

for (int i =150; i < 1000; i++) {

  //total += i;

  // create new Integer()

  // push in new value

  // add to total

}



public static Character valueOf(char c) {        

 return c < 128 ? SMALL_VALUES[c] : new Character(c);     

} 

 

缺点:

  1. 这占用更多的内存,因为整形只有 4 字节,整数对象有 16 字节;
  2. 创建对象需要耗费更多性能。不仅在循环中会出现这样的问题,当你在集合中使用基础类型时,也会出现这样的问题,特别地,对于 HashMap 这样的容器,只要你使用了基础类型,在进行插入、编辑或检索时就会产生一个基础类型和装箱对象。

 

List list = new ArrayList(); list.add(1); 

    1. 解决装箱拆箱

为了避免 HashMap 的自动装箱行为,Android 系统提供了 SparseBoolMap,SparseIntMap,SparseLongMap,LongSparseMap 等容器,可减少运行时间开支,减少内存使用。它们与 HashMap 的简单对比如下:

android 内存优化详解_第19张图片

四、内存泄漏的常见情形及解决办法

4.1、 静态变量引起的内存泄漏

在java中静态变量的生命周期是在类加载时开始,类卸载时结束,Static成员作为GC Roots,如果一个对象被static声明,这个对象会一直存活直到程序进程停止。即在android中其生命周期是在进程启动时开始,进程死亡时结束。所以在程序的运行期间,如果进程没有被杀死,静态变量就会一直存在,不会被回收掉。那么静态变量强引用了某个Activity中变量,那么这个Activity就同样也不会被释放,即便是该Activity执行了onDestroy(不要将执行onDestroy和被回收划等号)。

4.1.1、单例模式

单例模式需要持有上下文的引用的时,传入短生命周期的上下文对象,引起的Context内存泄漏

public class Singleton {

    private Context mContext;

    private volatile static Singleton mInstance;



    public static Singleton getInstance(Context mContext) {

        if (mInstance == null) {

            synchronized (Singleton.class) {

                if (mInstance == null)

                    mInstance = new Singleton(mContext);

            }
        }

        return mInstance;

    }

}

解决这类问题方法两种:寻找与该静态变量生命周期差不多的替代对象和将强引用方式改成弱(软)引用

public class Singleton {

    private Context mContext;

    private volatile static Singleton mInstance;



    public static Singleton getInstance(Context mContext) {

        if (mInstance == null) {

            synchronized (Singleton.class) {

                if (mInstance == null)

                    mInstance = new Singleton(mContext.getApplicationContext());//将传入的mContext转换成Application的context   

            }

        }

        return mInstance;

    }

}

Application 的 context 不是万能的,所以也不能随便乱用,对于有些地方则必须使用 Activity 的 Context,对于Application,Service,Activity三者的Context的应用场景如下

android 内存优化详解_第20张图片

4.1.2、非静态内部类

非静态内部类默认持有外部类实例的强引用引起的内存泄漏内部类(包含非静态内部类 和 匿名类) 都会默认持有外部类实例的强引用,因此可以随意访问外部类。但如果这个非静态内部类实例做了一些耗时的操作或者声明了一个静态类型的变量,就会造成外围对象不会被回收,从而导致内存泄漏。通常这类问题的解决思路有:

  • 将内部类变成静态内部类
  • 如果有强引用Activity中的属性,则将该属性的引用方式改为弱引用。
  • 在业务允许的情况下,及时回收,比如当Activity执行onStop、onDestory时,结束这些耗时任务。

4.1.3、匿名内部线程执行耗时操作引起的内存泄漏

public class MainActivity extends Activity {

    @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

        test();

    }

    public void test() {

        //匿名内部类会引用其外围实例MainActivity.this,所以会导致内存泄漏    

        new Thread(new Runnable() {

            @Override

            public void run() {

                while (true) {

                    try {

                        Thread.sleep(1_000);

                    } catch (InterruptedException e) {

                        e.printStackTrace();

                    }

                }

            }

        }).start();

    }

}

改为静态内部类即可

public static void test() {

    //静态内部类不会持有外部类实例的引用

    new Thread(new Runnable() {

        @Override

        public void run() {

            while (true) {

                try {

                    Thread.sleep(1_000);

                } catch (InterruptedException e) {

                    e.printStackTrace();

                }

            }

        }

    }).start();

}

4.1.4、 Handler引起的内存泄漏

mHandler 为匿名内部类实例,会引用外围对象MainActivity .this,若该Handler在Activity退出时依然还有消息需要处理,那么这个Activity就不会被回收,尤其是延迟处理时mHandler.postDelayed。

public class MainActivity extends Activity {

        

         private Handler mHandler = new Handler() {

        public void handleMessage(Message msg) {

            ...

        };

    };

         ...

}

针对Handler引起的内存泄漏,可以把Handler改为静态内部类,对于外部Activity的引用改为弱引用方式,并且在相关生命周期方法中及时移除掉未处理的Message和回调.

public class MainActivity extends Activity {

         private void doOnHandleMessage(){}

         //1、将Handler改成静态内部类。  

    private static class MyHandler extends Handler {

        //2将需要引用Activity的地方,改成弱引用。    

        private WeakReference mInstance;

        public MyHandler(MainActivity activity) {

            this.mInstance = new WeakReference(activity);

        }



        @Override

        public void handleMessage(Message msg) {

            super.handleMessage(msg);

            MainActivity activity = mInstance == null ? null : mInstance.get();

            //如果Activity被释放回收了,则不处理这些消息      

            if (activity == null || activity.isFinishing()) {

                return;

            }

            activity.doOnHandleMessage();

        }

}



@Override

    protected void onDestroy() {

        //3在Activity退出的时候移除回调    

        super.onDestroy();

        handler.removeCallbacksAndMessages(null);

    }

4.1.5、集合类中只执行添加操作,而没有对应的移除操作

集合类如果仅仅有添加元素的方法,而没有相应的删除机制,导致内存被占用。如果这个集合类是全局性的变量 (比如类中的静态属性,全局性的 map 等即有静态引用或 final 一直指向它),那么没有相应的删除机制,很可能导致集合所占用的内存只增不减。

4.1.6、资源未关闭引起的内存泄漏

当使用了IO资源、BraodcastReceiver、Cursor、Bitmap、自定义属性attr等资源时,当不需要使用时,需要及时释放掉,若没有释放,则会引起内存泄漏

4.1.7、注册和反注册没有成对使用引起的内存泄漏

比如说调用了View.getViewTreeObserver().addOnXXXListener ,而没有调用View.getViewTreeObserver().removeXXXListener。

##5、无限循环动画没有及时停止引起的内存泄漏

在Activity中播放属性动画中的一类无限循环动画,没有在ondestory中停止动画,Activity会被动画持有而无法释放

4.2、避免内存泄漏(OOM)的一些措施

  • 合理使用一些轻量级的数据结构,在Android中很多情况下,我们可以优先考虑使用SparseArray或者ArrayMap替代HashMap等传统Java的数据结构,相比起这些Android特意开发的ArrayMap、SparseArray的容器,传统容器消耗更多的资源和相对低效些。因为HashMap自身需要一个额外的实例对象来记录Mapping操作导致更加消耗内存,同时SparseArray避免了key和value的自动装箱,从而避免了自动拆箱。
  • 合理使用Bitmap,尽量减小BitMap的内存占用。在把图片加载到内存之前,可以根据需求对原始图片进行缩放处理,避免加载过大的图片,另外解码时使用合适的编码格式。
  • 尽量复用Bitmap对象、ViewHolder等对象
  • 在使用上下文的时候,尽量优先使用Appcation的Context,而非Activity的Context,当然局部的Dialog必须使用Activity的Context。
  • 使用软引用来避免Handler的泄漏
  • 谨慎使用static和单例对象,因为两者的生命周期都是与App的生命周期一样,如果不是需要全生命周期存在就尽量避免过度使用单例和static。
  • 注意原始WebView的泄漏(Android系统中一个失败的控件,不仅仅兼容性存在问题,泄漏也是一大败笔),如果一定使用原始WebView,最好在单独的进程中运行

五、OnTrimMemory内存管理

OnTrimMemory 回调是 Android 4.0 之后提供的一个API,这个 API 是提供给开发者的,它的主要作用是提示开发者在系统内存不足的时候,通过处理部分资源来释放内存,从而避免被 Android 系统杀死。这样应用在下一次启动的时候,速度就会比较快。
本文通过问答的方式,从各个方面来讲解 OnTrimMemory 回调的使用过程和效果。

5.1、OnTrimMemory回调的作用?

OnTrimMemory是Android在4.0之后加入的一个回调,任何实现了ComponentCallbacks2接口的类都可以重写实现这个回调方法.OnTrimMemory的主要作用就是指导应用程序在不同的情况下进行自身的内存释放,以避免被系统直接杀掉,提高应用程序的用户体验.
    Android系统会根据不同等级的内存使用情况,调用这个函数,并传入对应的等级:
TRIM_MEMORY_UI_HIDDEN 表示应用程序的所有UI界面被隐藏了,即用户点击了Home键或者Back键导致应用的UI界面不可见.这时候应该释放一些资源.
TRIM_MEMORY_UI_HIDDEN 这个等级比较常用,和下面六个的关系不是很强,所以单独说.

5.1.1、下面3个等级是当我们的应用程序真正运行时的回调:

  1. TRIM_MEMORY_RUNNING_MODERATE 

表示应用程序正常运行,并且不会被杀掉。但是目前手机的内存已经有点低了,系统可能会开始根据LRU缓存规则来去杀死进程了。

  1. TRIM_MEMORY_RUNNING_LOW 

表示应用程序正常运行,并且不会被杀掉。但是目前手机的内存已经非常低了,我们应该去释放掉一些不必要的资源以提升系统的性能,同时这也会直接影响到我们应用程序的性能。

  1. TRIM_MEMORY_RUNNING_CRITICAL 

表示应用程序仍然正常运行,但是系统已经根据LRU缓存规则杀掉了大部分缓存的进程了。这个时候我们应当尽可能地去释放任何不必要的资源,不然的话系统可能会继续杀掉所有缓存中的进程,并且开始杀掉一些本来应当保持运行的进程,比如说后台运行的服务。
当应用程序是缓存的,则会收到以下几种类型的回调:

  1. TRIM_MEMORY_BACKGROUND 

表示手机目前内存已经很低了,系统准备开始根据LRU缓存来清理进程。这个时候我们的程序在LRU缓存列表的最近位置,是不太可能被清理掉的,但这时去释放掉一些比较容易恢复的资源能够让手机的内存变得比较充足,从而让我们的程序更长时间地保留在缓存当中,这样当用户返回我们的程序时会感觉非常顺畅,而不是经历了一次重新启动的过程。

  1. TRIM_MEMORY_MODERATE 

表示手机目前内存已经很低了,并且我们的程序处于LRU缓存列表的中间位置,如果手机内存还得不到进一步释放的话,那么我们的程序就有被系统杀掉的风险了。

  1. TRIM_MEMORY_COMPLETE 

表示手机目前内存已经很低了,并且我们的程序处于LRU缓存列表的最边缘位置,系统会最优先考虑杀掉我们的应用程序,在这个时候应当尽可能地把一切可以释放的东西都进行释放。

5.1.1哪些组件可以实现OnTrimMemory回调?

  1. Application.onTrimMemory()
  2. Activity.onTrimMemory()
  3. Fragement.OnTrimMemory()
  4. Service.onTrimMemory()
  5. ContentProvider.OnTrimMemory()

5.1.2OnTrimMemory回调中可以释放哪些资源?

通常在架构阶段就要考虑清楚,我们有哪些东西是要常驻内存的,有哪些是伴随界面存在的.一般情况下,有下面几种资源需要进行释放:
缓存 缓存包括一些文件缓存,图片缓存等,在用户正常使用的时候这些缓存很有作用,但当你的应用程序UI不可见的时候,这些缓存就可以被清除以减少内存的使用.比如第三方图片库的缓存.
一些动态生成动态添加的View. 这些动态生成和添加的View且少数情况下才使用到的View,这时候可以被释放,下次使用的时候再进行动态生成即可.比如原生桌面中,会在OnTrimMemory的TRIM_MEMORY_MODERATE等级中,释放所有AppsCustomizePagedView的资源,来保证在低内存的时候,桌面不会轻易被杀掉.

六、LeakCanary内存泄漏工具

6.1 LeakCanary原理:

http://vjson.com/wordpress/leakcanary%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%E7%AC%AC%E4%B8%80%E8%AE%B2.html

在Application中进行初始化即可,当可能导致内存泄漏的时候会自动提示对应的泄漏点

public class ExampleApplication 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);

    // Normal app init code...

  }

}

android 内存优化详解_第21张图片

6.2 Implementation引入

LeakCanary是Square开源一个检测内存泄漏的框架,使用起来很简单,只需要两步:在build.gradle中引入库

dependencies {

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

  releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op: 1.6.1'

}

6.3 本地导入源码工程

该方式可以用于android源码编译工程(Setting,SystemUi)

include ':leakcanary-watcher'
include ':leakcanary-analyzer'
include ':leakcanary-android'
include ':leakcanary-android-no-op'

implementation project(':leakcanary-android')

 

6.4 差异化编译

Debug版本添加检测工具,Release版本不添加工具

debug {
    java.srcDirs = ['src','debug_src']
    manifest.srcFile "debug_src/AndroidManifest.xml"
}
release {
    manifest.srcFile "AndroidManifest.xml"
}

七、Android Studio 的Profiler内存分析工具

Profiler是Android Sutdio内置的一个检测内存泄漏的工具,使用Profiler第一步就是通过“Profiler app”运行APP
android 内存优化详解_第22张图片
然后首先看到如下界面
android 内存优化详解_第23张图片
点击Memory之后

android 内存优化详解_第24张图片

  1. 强制执行垃圾收集事件的按钮。
  2. 捕获堆转储的按钮,用于捕获堆内存快照hprof文件。
  3. 记录内存分配的按钮,点击一次记录内存的创建情况再点击一次停止记录。
  4. 放大时间线的按钮。
  5. 跳转到实时内存数据的按钮。
  6. 事件时间线显示活动状态、用户输入事件和屏幕旋转事件。
  7. 内存使用时间表,其中包括以下内容:
  8. •  每个内存类别使用多少内存的堆栈图,如左边的y轴和顶部的颜色键所示。
  9. •  虚线表示已分配对象的数量,如右侧y轴所示。

10 • 每个垃圾收集事件的图标。

启动APP之后,我们在执行一些操作之后(一些可以初步判断内存泄漏的操作),然后开始捕获hprof文件,首先得先点击请求执行GC按钮——>点击Dump java heap按钮 捕获hprof日志文件稍等片刻即可成功捕获日志(当然一次dump可能并不能发现内存泄漏,可能每次我们dump的结果都不同,那么就需要多试几次,然后结合代码来排查),然后直接把这个XXX.hprof文件拖到Android Studio就可解析到如下信息:
android 内存优化详解_第25张图片
通过上图可以得知内存中对象的个数(通常大于1就有可能是内存泄漏了需要结合自身的情况)、所占空间大小、引用组占的内存大小等基本信息,点击具体某个节点,比如说此处点击MainActivity下,选中某个任务然后点击自动分析任务按钮,还可以得到
android 内存优化详解_第26张图片
通过Android Profiler可以初步定位到能内存泄漏的地方,不过这可能需要重复去测试捕获hprof文件,再去分析,不过性能优化永远不是一蹴而就的事情,也没有任何墨守成规的步骤,除了借助hprif文件之外必须结合到实际的代码中去体会。

、使用Memory Analyzer Tool精确定位内存泄漏之处

在Android Studio 的Profiler 上发现为何会内存泄漏相对于MAT来说麻烦些,所以MAT更容易精确定位到内存泄漏的地方及原因,MAT 是基于Eclipse的一个检测内存泄漏的最专业的工具,也可以单独下载安装MAT,在使用MAT之前我们需要把Android Studio捕获的hprof文件转换一下,使用SDK路径下的platform-tools文件夹下hprof-conv 的工具就可以转成MAT 需要的格式。

//-z选项是为了排除不属于app的内存,比如Zygote

hprof-conv -z xxxx.hprof xxxx.hprof

执行上面那句简单的命令之后就可以得到MAT支持的格式,用MAT打开后
android 内存优化详解_第27张图片
还可以切换为直方图显示形式(这里会显示所有对象的信息),假如说我们知道了可能是MainActivity引起的泄漏,这里可以直接通过搜索栏直接过滤(往往这也是在做内存泄漏检测比较难的地方,这需要耐心还有运气)
android 内存优化详解_第28张图片
然后想选中的对象上右键选择

android 内存优化详解_第29张图片

弹出的对话框还可以显示很多信息,这里不一一介绍,这里只使用“Merge Shortest Path GC Roots”这个功能可以显示出对象的引用链(因为发生内存泄漏是因为对象还是GC Roots可达,所以需要分析引用链),然后可以直接选择“exclude all phantom/weak/soft ect references ” 排除掉软弱虚引用,接着就可以看到完整的引用链(下层对象被上层引用) android 内存优化详解_第30张图片

  • shallow heap——指的是某一个对象所占内存大小。
  • retained heap——指的是一个对象与所包含对象所占内存的总大小。
  • out查看这个对象持有的外部对象引用
  • incoming查看这个对象被哪些外部对象引用

 

你可能感兴趣的:(总结,1024程序员节,内存优化,jvm内存解析,内存分析)