Android 性能优化第二篇, 内存优化是减少崩溃率工作中非常关键的一部分,由于Android有垃圾自动回收机制不需要手动干预,但也因此,经常出现内存问题如内存泄漏、内存泄漏引发 GC 频繁导致页面卡顿和内存溢出等问题,如果不了解内存是如何管理的以及如何优化,会难以排查问题。

这篇文章集结了很多文章精华部分(个人认为),在这里整理分享出更多更完整内存知识,所以这篇文章会很长,目录我会分的很细,方便查找,文末会贴出参考文章。

注意:如果出现图片不显示问题请移步 图片不显示问题

本文主要记载

  • 1 什么是内存?
  • 2 JVM 如何分配内存?
    • 2.1 所有线程共享的数据区域
    • 2.2 线程私有的数据区域
  • 3 JVM 如何管理内存?
    • 3.1 引用计数算法
    • 3.2 可达性算法
    • 3.3 引用介绍
    • 3.4 垃圾回收器
      • 3.4.1 标记-清除算法(Mark-Sweep)
      • 3.4.2 复制算法(Copying)
      • 3.4.3 标记-整理算法(Mark-Compact)
      • 3.4.4 分代收集算法(Generational Collection)
  • 4 什么是 Dalvik?
    • 4.1 Dalvik 与 JVM 的区别
    • 4.2 Dalvik 堆大小
  • 5 什么是 ART?
    • 5.1 ART 与 Dalvik 的区别
  • 6 内存引发的问题
    • 6.1 内存泄漏
      • 6.1.1 常见的内存泄漏
    • 6.2 低杀
    • 6.3 内存抖动
  • 7 Android Bitmap 内存分配的变化
  • 8 内存优化的两个误区
    • 8.1 内存占用越少越好
    • 8.2 Native 内存不用管

1 什么是内存?

内存是计算机中重要的部件之一,是与 CPU 进行沟通的桥梁,是 CPU 能直接寻址的存储空间,由半导体器件制成。

如果说数据是商品,那硬盘就是商店的仓库,内存就是商店的货架,仓库里的商品你是不能直接买的,你只能买货架上的商品。

每一个程序中使用的内存区域相当于是不同的货架,当一个货架上需要摆放的商品超过这个货架所能容纳的最大值,就会出现放不下的情况,也就是内存溢出。


2 JVM 如何分配内存?

2.1 所有线程共享的数据区域

  • (Java Heap)
    • Java 堆是 JVM 管理的内存中最大的一块内存区域。
    • 几乎所有的对象实例都是在堆中分配内存。
    • 此区域也是垃圾回收器(Garbage Collection)主要的作用区域,内存泄漏就发生在这个区域。
  • 方法区(Method Area)
    • 方法区存放的是 类信息、常量、静态变量
    • 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机会抛出内存溢出异常 OutOfMemoryError

2.2 线程私有的数据区域

  • 程序计数器(Program Counter Register)

    • 一块较小的内存空间,可看做是当前线程所执行的字节码的行号指示器。

    • 为了线程切换后能恢复到正确的执行位置,每条线程都有一个私有的程序计数器。

    • 如果线程在执行 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令地址;

    • 如果执行的是 Native 方法,这个计数器的值为空(Undefined)。

    • 程序计数器是唯一一个在 Java 虚拟机规范中没有规定任何 OOM 情况的区域。

  • 本地方法栈(Native Method Stack)

    • 本地方法栈与虚拟机栈的区别是虚拟机栈为 Java 方法服务,而本地方法栈为 Native 方法服务。
    • 与虚拟机栈一样,本地方法栈也会抛出 StackOverflowErrorOutOfMemoryError 异常。
  • 虚拟机栈(Virtual Machine Stack )

    • 每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
    • 虚拟机栈生命周期与线程相同。
    • 当执行 Java 方法时会进行压栈的操作,在栈中会保存局部变量、操作数栈和方法出口等信息。JVM 规定了栈的最大深度,如果线程请求执行方法时栈的深度大于规定的深度,就会抛出栈溢出异常 StackOverflowError
    • 如果虚拟机在扩展时无法申请到足够的内存,就会抛出内存溢出异常 OutOfMemoryError

    栈帧(Stack Frame)

    Java 程序出现异常时,程序会打印出对应的异常堆栈,通过这个堆栈我们可以知道方法的调用链路,而这个调用链路就是由一个个 Java 方法栈帧组成的。

    1. 局部变量表(Local Variable Table):里面的变量只在当前函数调用中有效,当函数调用结束后,随着函数栈帧的销毁,局部变量表也会随之销毁。

      局部变量表中存放的编译期可知的各种数据有:

      • 基本数据类型:如 boolean、char、int 等。

      • 对象引用:reference 类型,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置。

      • returnAddress类型:指向一条虚拟机指令的操作码。与前面介绍的那些数值类的原生类型不同,returnAddress 类型在 Java 语言之中并不存在相应的类型,也无法在程序运行期间更改 returnAddress 类型的值。

    2. 操作数栈(Operand Stack):操作数栈(Operand Stack)也叫操作栈,它主要用于保存计算过程的中间结果,同时作为计算过程中临时变量的存储空间。

    3. 动态连接(Dynamic Linking):每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接

    4. 方法返回地址:当一个方法开始执行后,只有两种方式可以退出这个方法,一种是正常完成出口,另一种是异常完成出口。

      • 正常完成出口(Normal Method Invocation Completion):执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者。是否有返回值和返回值的类型将根据遇到哪种方法返回指令来决定,这种退出方法的方式称为正常完成出口。
      • 异常完成出口(Abrupt Method Invocation Completion):在方法执行过程中遇到异常,并且这个异常没有在方法体内得到处理,就会导致方法退出,这种退出方式称为异常完成出口。一个方法使用异常完成出口的方式退出,任何值都不会返回给它的调用者。

      无论采用哪种退出方式,在方法退出后,都需要返回到方法被调用的位置,程序才能继续执行。


3 JVM 如何管理内存?

JVM 的管理主要有 引用计数算法可达性算法Java 引用 以及 垃圾回收器 知识点。

3.1 引用计数算法

给对象中添加一个引用计数器,每当有一个地方引用该对象时,计数器值加1;引用失效时,计数器值减1;任意时刻计数器为0的对象就是不可能再被使用的,表示该对象不存在引用关系。

优点:实现简单,判定效率也很高;
缺点:难以解决对象之间相互循环引用导致计数器值不等于0的问题。

3.2 可达性算法

判定对象是否是存活的

这个算法的基本思路就是通过一系列 GC Roots 对象作为起始点,从这些节点开始向下搜索,搜索走过的路径就叫引用链。
当一个对象到 GC Roots 没有任何引用链相连时(GC Roots 到这个对象不可达),则证明此对象是不可用的。

比如下图中的 object5、object6、object7,虽然它们互有关联,但是它们到 GC Roots 是不可达的,所以它们会被判定为可回收对象。

那么那些点可以作为 GC Roots 呢?一般来说,如下情况的对象可以作为 GC Roots

  1. 虚拟机栈

    虚拟机栈的栈帧中的局部变量表中引用的对象,比如某个方法正在使用的类字段。

  2. 方法区

    • 类静态属性引用的对象

    • 常量引用的对象

  3. 本地方法栈

    本地方法栈中 Native 方法引用的对象。

3.3 引用介绍

无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判定对象是否存活都与引用有关。

JDK 1.2 之后,Java 对引用的概念进行了扩充,将引用分为强引用软引用弱引用虚引用四种,这四种引用强度按顺序依次减弱。如果没有指定对象引用类型,默认是强引用

  1. 强引用
    • 强引用是指代码中普遍存在的,比如 Object obj = new Object() 这类引用。
    • 强引用可以直接访问目标对象。
    • 强引用指向的对象在任何时候都不会被系统回收,虚拟机即使抛出 OOM 异常,也不会回收强引用指向的对象。
      使用 obj = null 不会触发 GC,但是在下次 GC 的时候这个强引用对象就可以被回收了。
    • 强引用可能导致内存泄漏。
  2. 软引用
    • 软引用用于描述一些还有用但非必需的对象。
    • 对于软引用关联的对象,在系统即将发生内存溢出前,会把这些对象列入回收范围中进行二次回收。
    • 如果二次回收后还没有足够的内存,就会抛出内存溢出异常。
    • JDK 1.2 后,Java 提供了 SoftReference 类来实现软引用。
  3. 弱引用
    • 弱引用的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次 GC 前。
    • GC 时,只要发现弱引用,不管系统堆空间使用情况如何,都会将对象进行回收。
    • 软引用、弱引用适合保存可有可无的缓存数据。
    • JDK 1.2 后,提供了 WeakReference 类来实现弱引用。
  4. 虚引用
    • 一个对象是否有虚引用的存在,都不会对其生存时间构成影响,也无法通过虚引用取得一个对象实例。
    • 为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
    • JDK 1.2 后,提供了 PhantomReference 类来实现虚引用。

3.4 垃圾回收器

垃圾回收器就是我们经常说到的 GC(Garbage Collector),当我们操作不当导致某块内存泄漏时,GC 就不能对这块内存进行回收。

Android 来说,进行 GC 时,所有线程都要暂停,包括主线程,16msAndroid 要求的每帧绘制时间,而当 GC 的时间超过 16ms,就会造成丢帧的情况,也就是界面卡顿。

垃圾回收器回收资源的方式就是垃圾回收算法

3.4.1 标记-清除算法(Mark-Sweep)

最基础的收集算法:分为 标记清除 两个阶段,首先,标记出所有需要回收的对象,然后统一回收所有被标记的对象。
这种方法有两个不足点:

  1. 标记和清除的效率都不高
  2. 标记清除后会产生大量不连续的内存碎片,内存碎片太多会导致当程序需要分配较大的对象时,无法找到足够的连续内存而不得不提前触发 ·GC

3.4.2 复制算法(Copying)

为了解决 标记-清除算法 效率问题,复制收集算法 出现了。

将内存按容量划分为大小相等的两块,每次只使用其中的一块,当这一块内存将用完了,就将还存活着的对象复制到另一块内存上面,然后再把已使用过的内存空间一次清理掉。

  • 优点:实现简单,运行高效;每次都是对整个半区进行内存回收,内存分配时也不需要考虑内存碎片等情况,只要移动堆顶指针,按顺序分配内存即可;
  • 缺点:粗暴的将内存一分为二,代价有点高。在对象存活率高时,要进行较多的复制操作,这时效率就变低了

3.4.3 标记-整理算法(Mark-Compact)

标记-整理算法 的标记过程与 标记-清除算法 一样,但后续步骤是让所有存活的对象向一端移动,然后直接清理掉边界外的内存。

  • 避免了 标记-清除算法 内存碎片;
  • 避免了 复制算法 50%的空间浪费;
  • 主要针对对象存活率高的老年代。

3.4.4 分代收集算法(Generational Collection)

现代商业虚拟机的垃圾回收都采用 分代收集算法 ,这种算法会根据对象存活周期的不同将内存划分为几块,这样就可以根据各个区域的特点采用最适当的收集算法。

堆内存可分为 新生区养老区永久存 储区三个区域。

  • 新生区(Young Generation Space)

    一个伊甸区(Eden space)两个幸存者区(Survivor space)区 组成。
    每次垃圾收集都有大批对象死去,只有少量存活,所以可以用复制算法。

    • 伊甸区

    大多数情况下,对象都是在伊甸区中分配的,当伊甸区没有足够的空间进行分配时,虚拟机将发起一次 Minor GC
    Minor GC 是指发生在新生区的垃圾收集动作,Minor GC 非常频繁,回收速度也比较快。
    当伊甸区的空间用完时,GC 会对伊甸区进行垃圾回收,然后把伊甸区剩下的对象移动到幸存 0 区。

    • 幸存 0 区

    如果幸存 0 区满了,GC 会对该区域进行垃圾回收,然后再把该区剩下的对象移动到幸存 1 区。

    • 幸存 1 区

    如果幸存 1 区满了,GC 会对该区域进行垃圾回收,然后把幸存 1 区中的对象移动到养老区。

  • 养老区(Tenure Generation Space)

对象存活率高、没有额外空间对它进行担保,就必须使用 标记-清理标记-整理算法 进行回收。

用于保存从新生区筛选出来的 Java 对象。
当幸存 1 区移动尝试对象到养老区,但是发现空间不足时,虚拟机会发起一次 Major GC
Major GC 的速度一般比 Minor GC 慢 10 倍以上。
大对象会直接进入养老区,比如很大的数字和很长的字符串。

  • 永久存储区(Permanent Space)

一个常驻内存区域,用于存放 JDK 自身携带的 Class Interface 元数据。
永久存储区存储的是运行环境必需的类信息,被装载进该区域的数据是不会被垃圾回收器回收掉的,只有 JVM 关闭时才会释放此区域的内存。


4 什么是 Dalvik?

DalvikDalvik Virtual MachineDalvik 虚拟机)的简称,是 Android 平台的核心组成部分之一。Android 4.4 之前都是使用 Dalvik

Androd 中,每一个应用都运行在一个 Dalvik VM 实例中,每一个 Dalvik VM 都运行在一个独立的进程空间,这种机制使得 Dalvik 运行在有限的内存中同时运行多个进程。

4.1 Dalvik 与 JVM 的区别

Dalvik 不是 Java 虚拟机,它并不是按照 Java 虚拟机规范实现的,两者之间并不兼容。

  1. 架构

    • JVM 是基于栈的,需要在栈中读取数据,所需的指令会更多,这样会导致速度慢,不适合性能优先的移动设备。

    • Dalvik 是基于寄存器的,指令更紧凑和简洁。

    由于显式指定了操作数,所以基于寄存器的指令会比基于栈的指令要大,但是由于指令数的减少,总的代码数不会增加多少。

  2. 执行代码不同

    • Java SE 程序中,Java 类会被编译成一个或多个 .class 文件,然后打包成 jar 文件,JVM 会通过对应的 .class 文件和 jar 文件获取对应的字节码。

    • Dalvik 会用 dx 工具将所有的 .class 文件转换为一个 .dex 文件,然后会从该 .dex 文件读取指令和数据。

  3. 共享机制

    • Dalvik 拥有预加载—共享机制,不同应用之间在运行时可以共享相同的类,拥有更高的效率。

    • JVM 不存在这种共享机制,不同的程序,打包后的程序都是彼此独立的,即使包中使用了同样的类,运行时也是单独加载和运行的,无法进行共享。

4.2 Dalvik 堆大小

每一个手机厂商都可以设定设备中每一个进程能够使用的堆大小,有关进程堆大小的值有下面三个。

如果我们想看堆内存大小应该怎么办呢?

1
adb shell getprop dalvik.vm.heapsize
  • dalvik.vm.heapstartsize

    堆分配的初始值大小,这个值越小,系统内存消耗越慢,但是当应用扩展这个堆,导致 GC 和堆调整时,应用会变慢。

    这个值越大,应用越流畅,但是可运行的应用也会相对减少。

  • dalvik.vm.heapgrowthlimit

    如果在清单文件中声明 largeHeaptrue,则 App 使用的内存到 heapsize 才会 OOM,否则达到 heapgrowthlimit 就会 OOM

  • dalvik.vm.heapsize

    进程可用的堆内存最大值,一旦应用申请的内存超过这个值,就会 OOM


5 什么是 ART?

ART 的全称是 Android Runtime,是从 Android 4.4 开始新增的 应用运行时环境,用于替代 Dalvik 虚拟机。

Dalvik VMART 都可以支持已转换为 .dex(Dalvik Executable)格式的 Java 应用程序的运行。

Dalvik 是为 32 位 CPU 设计的,而 ART 支持 64 位并兼容 32 位 CPU,这也是 Dalvik 被淘汰的主要原因。

5.1 ART 与 Dalvik 的区别

  1. 预编译

    • Dalvik 中的应用每次运行时,字节码都需要通过即时编译器 JIT 转换为机器码,这会使得应用的运行效率降低。

    • ART 中,系统在安装应用时会进行一次预编译,将字节码预先编译成机器码并存储在本地,这样应用就不用在每次运行时执行编译了,运行效率也大大提高。

  2. GC

    • Dalvik 采用的垃圾回收算法是标记-清除算法,启动垃圾回收机制会造成两次暂停(一次在遍历阶段,另一次在标记阶段)。

    • 而在 ART 下,GC 速度比 Dalvik 要快,这是因为应用本身做了垃圾回收的一些工作,启动 GC 后,不再是两次暂停,而是一次暂停。

      而且 ART 使用了一种新技术(packard pre-cleaning),在暂停前做了许多事情,减轻了暂停时的工作量。


6 内存引发的问题

  1. 内存造成的第一个问题是异常。异常包括 OOM、内存分配失败这些崩溃,也包括因为整体内存不足导致应用被杀死、设备重启等问题。
  2. 内存造成的第二个问题是卡顿。内存泄漏 导致 Java 内存不足会频繁 GC,常见现象是 内存抖动,这个问题在 Dalvik 虚拟机会更加明显。而 ART 虚拟机在内存管理跟回收策略上都做大量优化
  3. 除了频繁 GC 造成卡顿之外,物理内存不足时系统会触发 low memory killer 机制,系统负载过高是造成卡顿的另外一个原因。

6.1 内存泄漏

内存泄漏指的是,当一块内存没有被使用,但无法被 GC 时的情况。

内存泄漏的表现就是可用内存逐渐减少,无法被回收的内存逐渐累积,直到无更多可用内存可申请时,就会导致 OOM

6.1.1 常见的内存泄漏

  1. 非静态内部类

    • 泄漏场景

      非静态内部类会持有外部类的实例,比如匿名内部类。

      匿名内部类指的是一个对象名称的类,但是在字节码中,它还是会有构造函数的,而它的构造函数中会包含外部类的实例。

      比如在 Activity 中以匿名内部类的方式声明 HandlerAsyncTask,当 Activity 关闭时,由于 Handler 持有 Activity 的强引用,导致 GC 无法对 Activity 进行回收。

      当我们通过 Handler 发送消息时,消息会加入到 MessageQueue 队列中交给 Looper 处理,当有消息还没发送完毕时,Looper 会一直运行,在这个过程中会一直持有 Handler,而 Handler 又持有外部类 Activity 的实例,这就导致了 Activity 无法被释放。

    • 解决

      HandlerAsyncTask 声明为静态内部类,并且使用 WeakReference 包住 Activity,这样 Handler 拿到的就是一个 Activity 的弱引用,GC 就可以回收 Activity。这种方式适用于所有匿名内部类导致的内存泄漏问题。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      public static class MyHandler extends Handler {
      Activity activity;
      public MyHandler(Activity activity) {
      activity = new WeakReference<>(activity).get();
      }

      @Override
      public void handleMessage(Message message) {
      // ...
      }
      }
  2. 静态变量

    • 泄漏场景

      静态变量导致内存泄漏的原因是因为 长生命周期对象 持有了 短生命周期对象 的引用,导致短生命周期对象无法被释放。

      比如一个单例持有了 Activity 的引用,而 Activity 的生命周期可能很短,用户一打开就关闭了,但是单例的生命周期往往是与应用的生命周期相同的。

    • 解决

      如果单例需要 Context, 可以考虑使用 ApplicationContext,这样单例持有的 Context 引用就是与应用的生命周期相同的了。

  3. 资源未释放

    • 泄漏场景

      忘了注销 BroadcastReceiver

      打开了数据库游标(Cursor)忘了关闭

      打开流忘了关闭

      创建了 Bitmap 但是调用 recycle 方法回收 Bitmap 使用的内存

      使用 RxJava 忘了在 Activity 退出时取消任务

      使用协程忘了在 Activity 退出时取消任务

  4. Webview

    • 泄漏场景

      不同的 Android 版本的 Webview 会有差异,加上不同厂商定制 ROMWebview 的差异,导致 Webview 存在很大的兼容问题。

      一般情况下,在应用中只要使用一次 Webview,它占用的内存就不会被释放。

    • 解决

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      @Override
      protected void onDestroy() {
      if( mWebView!=null) {
      // 如果先调用destroy()方法,则会命中if (isDestroyed()) return;这一行代码,需要先onDetachedFromWindow(),再
      // destory()
      ViewParent parent = mWebView.getParent();
      if (parent != null) {
      ((ViewGroup) parent).removeView(mWebView);
      }
      mWebView.stopLoading();
      // 退出时调用此方法,移除绑定的服务,否则某些特定系统会报错
      mWebView.getSettings().setJavaScriptEnabled(false);
      mWebView.clearHistory();
      mWebView.clearView();
      mWebView.removeAllViews();
      mWebView.destroy();
      }
      super.on Destroy();
      }

6.2 低杀

在内存不足时,这种机制就会针对于所有进程进行回收(优先级由高到低,优先回收低优先级)

进程优先级:

  1. 前台进程:优先级最高的进程,是正在于用户交互的进程

    • 进程持有一个与用户交互的 Activity(该 Activity 的 onResume 方法被调用)
    • 进程持有一个 Service,[Service 与用户正在交互的 Activity 绑定] [Service 调用了 startForeground() 方法(前台服务)] [Service 正在执行以下生命周期函数(onCreate、onStart、onDestroy )]
    • 进程持有一个 BroadcastReceiver,这个 BroadcastReceiver 正在执行它的 onReceive() 方法
  2. 可见进程:不含有任何前台组件,但用户还能再屏幕上看见它

    • 进程持有一个 Activity,这个 Activity 处于 pause 状态
    • 进程持有一个 Service 这个 Service 和一个可见的 Activity 绑定。
    • 可见进程是非常重要的进程,除非前台进程已经把系统的可用内存耗光,否则系统不会终止可见进程。
  3. 服务进程:可能在播放音乐或在后台下载文件,除非系统内存不足,否则系统会尽量维持服务进程的运行。

    • 如果一个进程中运行着一个 Service,并且这个 service 是通过 startService 开启的,那这个进程就是一个服务进程。
  4. 后台进程

    系统会把后台进程(Background Process)保存在一个 LruCache 列表中,因为终止后台进程对用户体验影响不大,所以系统会酌情清理部分后台进程。

    你可以在 ActivityonSaveInstanceState() 方法中保存一些数据,以免在应用在后台被系统清理掉后,用户已输入的信息被清空,导致要重新输入。

    • 当进程持有一个用户不可见的 ActivityActivityonStop() 方法被调用),但是 onDestroy 方法没有被调用,这个进程就会被系统认定为后台进程。
  5. 空进程:当一个进程不包含任何活跃的应用组件,则被系统认定为是空进程。

    系统保留空进程的目的是为了加快下次启动进程的速度。

6.3 内存抖动

当我们在短时间内频繁创建大量临时对象时,就会引起内存抖动,比如在一个 for 循环中创建临时对象实例。

内存检测表现如下

预防的方法

  1. 尽量避免在循环体中创建对象
  2. 尽量不要在自定义 ViewonDraw() 方法中创建对象,因为这个方法会被频繁调用
  3. 对于能够复用的对象,可以考虑使用对象池把它们缓存起来

7 Android Bitmap 内存分配的变化

  • Android 3.0 之前,Bitmap 对象放在 Java 堆,而像素数据是放在 Native 内存中。如果不手动调用 recycleBitmap Native 内存的回收完全依赖 finalize 函数回调,这个时机不太可控。
  • Android 3.0~Android 7.0Bitmap 对象和像素数据统一放到 Java 堆中,这样就算我们不调用 recycleBitmap 内存也会随着对象一起被回收。不过 Bitmap 是内存消耗的大户,把它的内存放到 Java 堆中似乎不是那么美妙。即使是华为 Mate 20,最大的 Java 堆限制也才到 512MB,可能我的物理内存还有 5GB,但是应用还是会因为Java 堆内存不足导致 OOMBitmap 放到 Java 堆的另外一个问题会引起大量的 GC,对系统内存也没有完全利用起来。
  • 有没有一种实现,可以将 Bitmap 内存放到 Native 中,也可以做到和对象一起快速释放,同时 GC 的时候也能考虑这些内存防止被滥用?NativeAllocationRegistry 可以一次满足你这三个要求,Android 8.0 正是使用这个辅助回收 Native 内存的机制,来实现像素数据放到 Native 内存中。Android 8.0 还新增了硬件位图 Hardware Bitmap,它可以减少图片内存并提升绘制效率。

8 内存优化的两个误区

8.1 内存占用越少越好。

  1. 有些人认为内存是洪水猛兽,占用越少应用的性能越好,这种认识在具体的优化过程中很容易“用力过猛”。
  2. 应用是否占用了过多的内存,跟设备、系统和当时情况有关,而不是 300MB400MB 这样一个绝对的数值。当系统内存充足的时候,我们可以多用一些获得更好的性能。当系统内存不足的时候,希望可以做到“用时分配,及时释放”

8.2 Native 内存不用管

  1. 虽然 Android 8.0 重新将 Bitmap 内存放回到 Native 中,那么我们是不是就可以随心所欲地使用图片呢?
  2. 答案当然是否定的。正如前面所说当系统物理内存不足时,lmk 开始杀进程,从后台、桌面、服务、前台,直到手机重启。

感谢

极客时间 Android开发高手课

慕课 Top团队大牛带你玩转Android性能分析与优化

探索 Android 内存优化方法

探索 Java 内存管理机制

Android性能优化(三)之内存管理