Android 内存空间优化

在解决OOM之前,先学习一下避免OOM吧。

在解决这些问题的时候经常会有一个现实的问题,就是无论什么方法,都需要找到一个平衡点。这是这些绝对存在的编程方法的乐趣所在。说明白一点就是小的时候班里学习好的同学(我)能用五种方法解出一道数学题,我们需要在这五种方法中间做比较。不同的是编程这道题的数据是可变的,所以最优解可能也是可变的。

编程的乐趣就在这里

一.对象引用

1.强引用

最常用的类型,只要被引用,就不会被GC回收。被new object强引用赋值之后,之后当这个object被释放之后,对象才会被释放掉。

在内存不足时,系统会出现OOM的错误,不会被回收。

因此,在强引用对象不需要再使用时,请及时进行释放或转换成弱引用

2.软引用

软引用是在保持引用对象的同时,在虚拟机报出内存不足之前,清除所有的软引用对象。清除软引用对象顺序是垃圾回收器(GC)的算法以及GC运行时的内存数量来决定的。如果本来需要清理两个软引用对象,在清理了A对象之后,内存完全足够,那么B对象将不会被GC回收。

整个使用过程大概就是:内存足够,软引用存在;内存不足时,并且此时没有可回收对象,那么GC就回收软引用对象。这个逻辑使得软引用对象来实现内存敏感的高速缓存。

软引用可以和一个引用队列(ReferenceQueue)一起使用。在被GC回收之后,jvm就会把这个被回收的对象加入与之关联的引用队列中。

需要注意的是,只有非强引用会被放到引用队列中。因为强引用并不会被回收!

点击跳转的链接中,请注意看最后一段:

As seen from above the Reference queue actually holds within it the WeakReference which lost its heap object to clean up. The WeakReference does not have any association to the memory object. The get call above returns null. Unlike with finalize when we can make the object alive again, with the ReferenceQ there is no way to reach the released java object. Reference Queues are just for References that got freed by garbage collection. They cannot be used to make alive our objects again. They can only be used to notify our code about the loss of memory objects referred to by these non- strong references.

3.弱引用

弱引用的一个典型用处就是规范化映射(canonicalized mapping)。此外用于生命周期长,且重新创造开销不高的对象进行使用。

只要弱引用被GC扫描到的弱可及对象,都会被回收。但是因为GC的优先级比较低,所以它不会很快发现自己所管辖的内存区块的弱引用对象。

另外,可能在运行多次GC之后,才能扫描到弱引用对象。

4.虚引用

虚引用只能用于跟踪即将被引用对象进行的收集。可以执行Pre-mortem进行清除操作。它必须与引用队列(ReferenceQueue)一起使用。

虚引用配合ReferenceQueue可以构造一个通知机制。当GC确定某个对象被回收时,会把它的Phantom Reference对象放在RreferenceQueue上。这个时候会有一个通知,这个通知用来表明虚引用对象对象已经结束,可供收集了。这可以提示开发者或者用户在这一块内存被回收之前进行自定义操作。

二.减少不必要的内存开销

1.Autoboxing

首先说一下什么是autoboxing

简单来说,就是把基本数据类型转换为复杂数据类型

例如:

Character ch = 'a';
Integer in = 0;

实际上,上图中的'a'是一个char类型的对象,0是一个int类型的对象,基本数据类型和复杂数据类型对应关系为:

Primitive type Wrapper class
boolean Boolean
byte Byte
char Character
float Float
int Integer
long Long
short Short
double Double

复杂数据类型包含了基本数据类型的方法,但可使用于范型类型。但是复杂数据类型占用是要比基本数据类型大的,例如Integer占用了16字节的内存,int对象占用了4字节。在操作一个或者少量对象时基本无差,但是在一个for循环或者在操作一个Hashmap对象时差别比较大,特别是在操作Hashmap对象时,每次增删改查,都需要重新对数据进行循环autoboxing操作。

监听方法,我们可以在 TraceView 中,找到任何大量调用 java.lang.Integer.valueOf 的地方,说明这里进行了大量的自动装箱操作,我们可以使用Android系统提供的 SparseBoolMap,SparseIntMap,SparseLongMap,LongSparseMap 等容器对Hashmap进行替换!

如果你使用 Allocation Tracker(分配追踪器)发现同一个地方分配了大量整数对象 java.lang.Integer 时,同样要注意。

另外,装箱操作把基础数据类型转化为复杂数据类型是为了能够让对象在更多java容器中进行操作。

另外有unboxing,这里不再过多赘述,提供官方示例进行参考:

import java.util.ArrayList;
import java.util.List;
public class Unboxing {

    public static void main(String[] args) {
        Integer i = new Integer(-8);

        // 1. Unboxing through method invocation
        int absVal = absoluteValue(i);
        System.out.println("absolute value of " + i + " = " + absVal);

        List<Double> ld = new ArrayList<>();
        ld.add(3.1416);    // Π is autoboxed through method invocation.

        // 2. Unboxing through assignment
        double pi = ld.get(0);
        System.out.println("pi = " + pi);
    }

    public static int absoluteValue(int i) {
        return (i < 0) ? -i : i;
    }
}

2.复用

a)系统资源的使用

android本身提供了大量的资源,例如字符串,icon,颜色和简单布局。

b)视图的服用

出现大量重复的子组件时,可以考虑一下视图的复用。例如用viewholder实现Convertview的复用。

c)对象池

在设计程序初期,就考虑实现对象池的概念。相同类型的对用可以使用同一内存空间。

d)bitmap对象的复用

用bitmap中的inBitmap属性,可以提高bitmap在android系统中的分配和释放速度,不仅可以达到内存复用,还提高了效率。

三.数据类型

1.枚举

首先要说一下枚举和直接定义常量的区别。

枚举结构清晰,易读,并且代码更安全。但是资源占用比自定义常量大了不是一点半点。

总结来说,枚举=垃圾

Android在app运行后会为其分配一块内存,存放dex,heap和运行时所需的内存都被分配在这块内存区域上。单单在dex code这一块,enum占用的内存时直接定义常量的13×以上…这还只是dex,一个enum的值的生命需要消耗20字节(bytes)以上的内存,而且对象数组要hold住所有的enum对象…

但是相对于enum的类型安全,自定义常量需要执行一些判断之后才能满足类似的操作。Android系统提供了IntDefStringDef来进行帮助:

public static final int TONIGHT_1 = 1;
public static final int TONIGHT_2 = 2;
@IntDef({TONIGHT_1,TONIGHT_2})
@Retention(RetentionPolicy.SOURCE)
public @interface PER_TONIGHT{
}

public static int getNight(@PER_TONIGHT int night){
    switch(night){
        case TONIGHT_1:
            return 1;
        case TONIGHT_2:
            return 2;
        default:
            throw new IllegalArgumentException("unkonw night ")
    }
}

以上可以看出,把PER_TONIGHT注解刀getNight中时,会对其参数进行限制,可以替代enum的用法。此外还有LongDef

2.LruCache

LruCache意为长时间没有用的缓存(Least Recently Used Cache)。它使用强引用把最近没有使用的缓存hold住,内部维护了一个队列。当其中的一个值被访问时,他会被放到队列的尾部;当缓存较多或者将满时,队列就从头部开始把值丢弃,当作垃圾回收。

注意LruCache的容量,容量太大时容易OOM,容量太小则无用。

3.HashMap和ArrayMap

这不是一个热门的知识点,但是这是一个必考的知识点。

https://blog.mindorks.com/android-app-optimization-using-arraymap-and-sparsearray-f2b4e2e3dc47

四.其他优化和图片优化

这个太多了,建议使用第三方插件,小张也不想写了。

参考

另外还有腾讯bugly的一篇很好的文章
https://mp.weixin.qq.com/s/2MsEAR9pQfMr1Sfs7cPdWQ

注释

关于‘内部维护了一个队列’

实际上是LinkedHashMap内部的双向链表,本身不支持线程安全,LruCache对其封装,添加了线程安全操作。

关于LinkedHashMap:

A LinkedHashMap is a combination of hash table and linked list. It has a predictable iteration order (a la linked list), yet the retrieval speed is that of a HashMap. The order of the iteration is determined by the insertion order, so you will get the key/values back in the order that they were added to this Map. You have to be a bit careful here, since re-inserting a key does not change the original order.

https://docs.oracle.com/javase/8/docs/api/java/util/LinkedHashMap.html

发表评论