Android 性能优化5 - 内存优化

一、说明

Java 虚拟机拥有自动垃圾回收机制,Android 系统的内存垃圾也通过 GC 来自动回收,所以开发者平常不用过多的关心 Android 的内存管理,可以把更多的精力放到业务上。但为了整个系统的内存控制需要,Android 系统为每一个应用程序都设置一个硬性的 Dalvik Heap Size 最大限制阈值(这个阈值在不同的设备上会因为 RAM 的大小不同而有所差异),如果应用占用内存空间已经接近或超出这个阈值时,就会发生错误(OOM)或性能问题,所以内存的优化是尤为重要的。

二、内存不足造成哪些问题

1、卡顿

首先,Java 不像 C 和 C++ 一样需要开发者自己释放内存,在 Android 上,申请和释放内存都是由系统层承担。

应用在使用过程中随着内存增加,当达到一定的条件后,GC 开始释放内存,在 GC 过程中,任何其它工作中的线程都会暂停,包括 UI 线程。一般这种释放操作增加的一点耗时并不会影响应用的使用,如果大量这样重复的操作,其它工作的时间就会受到压缩,进而影响到应用的渲染工作。

比如发生在短时间内申请大量对象,但极少会释放。一旦接近限定阈值,垃圾回收活动就会启动,即使有时内存申请很小,它们仍然会给应用程序的堆内存造成压力,还是会启动垃圾回收。如果 GC 频繁的话就会消耗很多的时间,很容易造成卡顿。

2、OOM

OOM 全称是 Out Of Memory,即内存溢出,是因为实际使用内存超出最大限定值而报错。造成 OOM 主要有以下原因:

  • 申请的空间太大,超出剩余可分配内存。
  • 内存在某一阶段的峰值到达了内存空间的阈值,刚好在这个峰值时,需要申请一块较大的内存,就会由于堆内存空间不足而导致 OOM 异常。

3、影响存活率

除了导致 OOM 和 UI 卡顿,在应用程序后台运行时,特别是服务进程,即使通过一系列的方法提高进程的优先级,但如果内存占用过高,在系统资源紧张的情况下,仍然会被系统 kill 掉。

五、内存泄漏导致性能问题

内存泄露总体来说就是没有用的本该释放回收的对象,GC 却不能回收,就造成了内存泄露,当泄露越来越多,可用内存空间越来越少时,问题就来了。

如果想详细了解请参考:Android 性能优化3 - 解决内存泄露。

六、内存优化

1、合理利用对象引用

Java 中的引用分为强引用、软引用、弱引用、虚引用,合理利用这几种引用类型可以帮助 GC 更合理地回收。

2、AutoBoxing

说明:自动装箱的核心就是把基础数据类型换成对应的复杂类型,不需要开发者自己转换。
目的:

  • 为了能够让这些基础数据类型在大多数 Java 容器中运作,在泛型中,基本类型是不可以做泛型参数的。
  • 把一个基本类型包装成一个类,是可以使这个类型具有很多可以调用的方法。
// 不合法
List list = new ArrayList ()

// 合法
List list = new ArrayList ();

示例:

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

这样会消耗更多的性能,每次循环虚拟机都必须创建一个新的整数对象。
基本数据类型 int 只占4个字节,而 Integer 对象有16个字节,因此在内存和时间性能上都有额外的开销。

对于不必要的内存开销等问题应该及早发现,不要让问题积少成多。一般可以通过 TraceView 或 Memory Profiler 查看耗时,如果发现调用了大量的 integer.value,就说明发生了AutoBoxing,需要立即优化代码。

3、内存复用

在Android系统中,有些数据是可以复用的,并且在开发过程中,系统也提供了可利用的接口或方法。比如以下三种复用的场景,通过内存复用可以有效减少应用的内存开销。

  1. 有效利用系统自带的资源:应用系统本身内置的资源,比如一些通用的字符串、颜色、常用Icon,动画样式和简单布局一定程度上可以减少内存开销,还可以减少应用程序自身负重,减少 APK 大小,并且复用性更好。
  2. 视图复用:有大量重复的子组件的,可以使用 ViewHolder 实现 ConvertView 复用。
  3. 对象池:可以在设计程序时显式地在程序中创建对象池,然后实现复用逻辑,对相同的类型数据使用同一块内存空间,也可以利用系统框架既有的具有复用特性的组件减少对象的重复创建,从而减少内存的分配与回收。
  4. Bitmap 对象的复用:利用 Bitmap 中的 inBitmap 的高级特性,提高 Android 系统在 Bitmap 的分配与释放效率,不仅可以达到内存复用,还提高了读写速度。该特性可以在已经存在的内存区域申请一块区域来存放 bitmap。

4、枚举类型

使用枚举类型(Enums)来定义常量,会使代码更易读并且更安全,但在性能上却比普通常量定义差很多。

Android系统在应用启动后,会给应用单独分配一块内存。应用的 dex code、Heap 以及运行时的内存分配都会在这块内存中。而使用枚举类型的 dex size 是普通常量定义的 dex size 的13倍以上(只是 dex code增加),同时,运行时的内存分配,一个 enum 值的声明会消耗至少20 bytes,这还没有算上其中的对象数组需要保持对 enum 值的引用。每个枚举项都会被声明成一个静态变量,并被赋值。因此,当应用程序中的代码或包含的 Lib 中大量使用 enum 时,对本身内存小的手机会带来不可忽视的影响。

枚举的最大优点是类型安全,但在 Android 平台上,枚举的内存开销是直接定义常量的三倍以上,所以 Android 的官方文档也提醒了开发者尽量避免使用枚举类型,同时提供注解的方式检查类型安全,目前提供了int型和String型两种注解方式:IntDef 和 StringDef,用来提供编译期的类型检查。

public class Test {
    public static final int LEVEL1 = 1;
    public static final int LEVEL2 = 2;
    
    void main() {
        aaa(LEVEL1);
    }
    
    void aaa(@TestInterface int level) { ... }
    
    @IntDef({LEVEL1, LEVEL2})
    @Retention(RetentionPolicy.SOURCE)
    public @interface TestInterface { ... }
}

通过以上方法可以替代枚举类型的安全性。

5、LruCache

LruCache 在 android.util 包下(android-support-v4 的包中提供),可以翻译为最近最少使用缓存。
它用强引用保存需要缓存的对象,它内部维护一个队列(实际上是 LinkedHashMap 内部的双向链表,不支持线程安全,LruCache对它进行封装,添加了线程安全操作)。
当其中的一个值被访问时,它被放到队列的尾部,当缓存将满时,队列头部的值(也就是最近最少被访问的)被丢弃,之后可以被垃圾回收。

6、图片内存优化

由于 Android 为每个进程分配的存储空间有有限的,而图片又是占用内存空间很大的一块,如果没有合理的优化好图片,很可能由于内存的过量占用从而导致性能问题。

由于该块的内容较多,我将其单独放到一篇文章中详细说明,如果想详细了解可参考:
Android 内存优化4 - 图片优化

七、检测

介绍几个检测内存问题的工具:

  • Memory Profiler:可以检测内存抖动
  • LeakCanary:检测内存泄露

六、总结

总体来说内存优化具有以下意义:

  • 减少OOM,提高应用稳定性。
  • 减少卡顿,提高应用流畅度。
  • 减少内存占用,提高应用后台运行时的存活率。
  • 减少异常发生,减少代码逻辑隐患。

七、推荐

  1. Android 性能优化1 - 启动优化
  2. Android 性能优化2 - 绘制优化
  3. Android 性能优化3 - 解决内存泄露
  4. Android 内存优化4 - 图片优化
  5. Android 性能优化5 - 内存优化
  6. Android 性能优化6 - Hybrid 应用启动优化

你可能感兴趣的:(Android-性能优化,Android,性能优化,内存优化)