Android性能优化(一):内存优化

对象的声明周期

Java代码编译后生成的字节码.class文件从从文件系统中加载到虚拟机之后,便有了JVM上的Java对象,Java对象在JVM上运行有7个阶段。

Created(创建)

  • 为对象分配存储空间,然后进行构造
  • 从父类到子类的静态成员初始化,类的静态成员会在ClassLoader加载该类的时候进行。
  • 父类成员变量初始化,递归调用父类构造方法
  • 子类成员变量初始化,然后创建对象。

    InUse(使用)

    此时对象至少被一个强引用持有。

    Invisible(不可见)

    当一个对象处于不可见阶段时,说明程序本身不再持有该对象的任何强引用,虽然该对象仍然是存在的。简单的例子就是程序的执行已经超出了该对象的作用域了。但是,该对象仍可能被虚拟机下的某些已装载的静态变量线程或JNI等强引用持有,这些特殊的强引用称为“GC Root”。被这些GC Root强引用的对象会导致该对象的内存泄漏,因而无法被GC回收。

    Unreachable(不可达)

    该对象不再被任何强引用持有。

    Collected(收集)

    当GC已经对该对象的内存空间重新分配做好准备时,对象进入收集阶段,如果该对象重写了finalize()方法,则执行它。

    Finalized(终结)

    等待垃圾回收器回收该对象空间。

    Deallocated(对象空间重新分配)

    GC对该对象所占用的内存空间进行回收或者再分配,则该对象彻底消失。

内存回收机制

在Android的高级系统版本中,针对Heap空间有一个Generational Heap Memory的模型,其中将整个内存分为三个区域。Young Generation(年轻代)、Old Generation(年老代)、Permanent Generation(持久代)。

Young Generation

由一个Eden区和两个Survivor区组成,程序中生成的大部分新的对象都在Eden区中,当Eden区满时,还存活的对象将被复制到其中一个Survivor区,当此Survivor区满时,此区存活的对象又被复制到另一个Survivor区,当这个Survivor区也满时,会将其中存活的对象复制到年老代。

Old Generation

一般情况下,年老代中的对象生命周期都比较长。

Permanent Generation

用于存放静态的类和方法,持久代对垃圾回收没有显著影响。

处理过程总结

  • 对象创建后在Eden区。
  • 执行GC后,如果对象仍然存活,则复制到S0区。
  • 当S0区满时,该区域存活对象将复制到S1区,然后S0清空,接下来S0和S1角色互换。
  • 当第3步达到一定次数(系统版本不同会有差异)后,存活对象将被复制到Old Generation。
  • 当这个对象在Old Generation区域停留的时间达到一定程度时,它会被移动到Old Generation,最后累积一定时间再移动到Permanent Generation区域。

    执行GC占用的时间与Generation和Generation中的对象数量有关
    Young Generation < Old Generation < Permanent Generation
    Generation中的对象数量与执行时间成反比。

GC

GC类型

  • kGcCauseForAlloc:分配内存不够引起的GC,会Stop World。由于是并发GC,其它线程都会停止,直到GC完成。
  • kGcCauseBackground:内存达到一定阈值触发的GC,由于是一个后台GC,所以不会引起Stop World。
  • kGcCauseExplicit:显示调用时进行的GC,当ART打开这个选项时,使用System.gc时会进行GC。
    分析一下Android虚拟机中的GC日志。
    1
    D/dalvikvm(7030):GC_CONCURRENT freed 1049K, 60% free 2341K/9351K, external 3502K/6261K, paused 3ms 3ms

GC_CONCURRENT 是当前GC时的类型,GC日志中有以下几种类型:

  • GC_CONCURRENT:当应用程序中的Heap内存占用上升时(分配对象大小超过384k),避免Heap内存满了而触发的GC。如果发现有大量的GC_CONCURRENT出现,说明应用中可能一直有大于384k的对象被分配,而这一般都是一些临时对象被反复创建,可能是对象复用不够所导致的。
  • GC_FOR_MALLOC:这是由于Concurrent GC没有及时执行完,而应用又需要分配更多的内存,这时不得不停下来进行Malloc GC。
  • GC_EXTERNAL_ALLOC:这是为external分配的内存执行的GC。
  • GC_HPROF_DUMP_HEAP:创建一个HPROF profile的时候执行。
  • GC_EXPLICIT:显示调用了System.GC()。(尽量避免)
  • freed 1049k:表明在这次GC中回收了多少内存。
  • 60% free 2341k/9351K:表明回收后60%的Heap可用,存活的对象大小为2341kb,heap大小是9351kb。
  • external 3502/6261K:是Native Memory的数据。存放Bitmap Pixel Data(位图数据)或者堆以外内存(NIO Direct Buffer)之类的数据。第一个值说明在Native Memory中已分配3502kb内存,第二个值是一个浮动的GC阈值,当分配内存达到这个值时,会触发一次GC。
  • paused 3ms 3ms:表明GC的暂停时间,如果是Concurrent GC,会看到两个时间,一个开始,一个结束,且时间很短,如果是其他类型的GC,很可能只会看到一个时间,且这个时间是相对比较长的。并且,越大的Heap Size在GC时导致暂停的时间越长。

    在Dalvik虚拟机下,GC的操作都是并发的,也就意味着每次触发GC都会导致其它线程暂停工作(包括UI线程)。而在ART模式下,GC时不像Dalvik仅有一种回收算法,ART在不同的情况下会选择不同的回收算法,比如Alloc内存不够时会采用非并发GC,但在Alloc后,发现内存达到一定阈值时又会触发并发GC。所以在ART模式下,并不是所有的GC都是非并发的。
    总体来看,在GC方面,与Dalvik相比,ART更为高效,不仅仅是GC的效率,大大地缩短了Pause时间,而且在内存分配上对大内存分配单独的区域,还能有算法在后台做内存整理,减少内存碎片。因此,在ART虚拟机下,可以避免较多的类似GC导致的卡顿问题。

Young Generation GC

由于其对象存活时间短,因此基于Copying算法(扫描出存活的对象,并复制到一块新的完全未使用的控件中)来回收。新生代采用空闲指针的方式来控制GC触发,指针保持最后一个分配的对象在Young Generation区间的位置,当有新的对象要分配内存时,用于检查空间是否足够,不够就触发GC。

Old Generation GC

由于其对象存活时间较长,比较稳定,因此采用Mark(标记)算法(扫描出存活的对象,然后再回收未被标记的对象,回收后对空出的空间要么合并,要么标记出来便于下次分配,以减少内存碎片带来的效率损耗)来回收。

垃圾收集算法

  • 引用计数法
  • 可达性分析法

垃圾回收算法

标记-清除算法

  1. 标记所有需要回收的对象
  2. 统一回收所有被标记的对象。

    优点:实现比较简单。缺点:标记、清除效率不高。容易产生大量内存碎片。

复制算法

  1. 将内存划分为大小相等的两块。
  2. 一块内存用完之后复制存活对象到另一块。
  3. 清理另一块内存。

    优点:实现简单,运行高效,每次仅需遍历标记一半的内存区域。缺点:会浪费一半的空间,代价大。

标记-整理算法

  1. 标记过程与 标记-清除算法 一样。
  2. 存活对象往一端进行移动。
  3. 清理其余内存。

    优点:避免 标记-清除 导致的内存碎片。避免复制算法的空间浪费。

分代收集算法

现在 主流的虚拟机 一般用的比较多的还是分代收集算法,它具有如下 特点:

  • 结合多种算法优势。
  • 新生代对象存活率低,使用 复制算法。
  • 老年代对象存活率高,使用 标记-整理算法。

内存问题

内存抖动

内存波动图形呈 锯齿张、GC导致卡顿。
这个问题在 Dalvik虚拟机 上会 更加明显,而 ART虚拟机 在 内存管理跟回收策略 上都做了 大量优化,内存分配和GC效率相比提升了5~10倍,所以 出现内存抖动的概率会小很多。

为什么会内存抖动?

  • 频繁创建对象,导致内存不足及碎片(不连续)。
  • 不连续的内存片无法被分配,导致OOM。

内存抖动常见案例

字符串使用加号拼接
  1. 使用StringBuilder替代。
  2. 初始化时设置容量,减少StringBuilder的扩容。
资源复用
  1. 使用 全局缓存池,以 重用频繁申请和释放的对象。
  2. 注意 结束 使用后,需要 手动释放对象池中的对象。
减少不合理的对象创建
  1. ondraw、getView 中创建的对象尽量进行复用。
  2. 避免在循环中不断创建局部变量。
使用合理的数据结构
  1. 使用 SparseArray类族、ArrayMap 来替代 HashMap。

    内存抖动解决实战

    这里我们假设有这样一个场景:点击按钮使用 handler 发送一个空消息,handler 的 handleMessage 接收到消息后创建内存抖动,即在 for 循环创建 100个容量为10万 的 strings 数组并在 30ms 后继续发送空消息。
    一般使用 Memory Profiler (表现为 频繁GC、内存曲线呈锯齿状)结合代码排查即可找到内存抖动出现的地方。
    通常的技巧就是着重查看 循环或频繁被调用 的地方。

内存泄漏

Android系统虚拟机的垃圾回收是通过虚拟机GC机制来实现的。GC会选择一些还存活的对象作为内存遍历的根节点GC Roots,通过对GC Roots的可达性来判断是否需要回收。内存泄漏就是 在当前应用周期内不再使用的对象被GC Roots引用,导致不能回收,使实际可使用内存变小。简言之,就是 对象被持有导致无法释放或不能按照对象正常的生命周期进行释放。一般来说,可用内存减少、频繁GC,容易导致内存泄漏。

常见的内存泄漏场景

资源性对象未关闭

对于资源性对象不再使用时,应该立即调用它的close()函数,将其关闭,然后再置为null。例如Bitmap等资源未关闭会造成内存泄漏,此时我们应该在Activity销毁时及时关闭。

注册对象未注销

例如BraodcastReceiver、EventBus未注销造成的内存泄漏,我们应该在Activity销毁时及时注销。

类的静态变量持有大数据对象

尽量避免使用静态变量存储数据,特别是大数据对象,建议使用数据库存储。

单例造成的内存泄漏

优先使用Application的Context,如需使用Activity的Context,可以在传入Context时使用弱引用进行封装,然后,在使用到的地方从弱引用中获取Context,如果获取不到,则直接return即可。

非静态内部类的静态实例

该实例的生命周期和应用一样长,这就导致该静态实例一直持有该Activity的引用,Activity的内存资源不能正常回收。此时,我们可以将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例,如果需要使用Context,尽量使用Application Context,如果需要使用Activity Context,就记得用完后置空让GC可以回收,否则还是会内存泄漏。

Handler临时性内存泄漏

Message发出之后存储在MessageQueue中,在Message中存在一个target,它是Handler的一个引用,Message在Queue中存在的时间过长,就会导致Handler无法被回收。如果Handler是非静态的,则会导致Activity或者Service不会被回收。并且消息队列是在一个Looper线程中不断地轮询处理消息,当这个Activity退出时,消息队列中还有未处理的消息或者正在处理的消息,并且消息队列中的Message持有Handler实例的引用,Handler又持有Activity的引用,所以导致该Activity的内存资源无法及时回收,引发内存泄漏。解决方案如下所示:

  1. 使用一个静态Handler内部类,然后对Handler持有的对象(一般是Activity)使用弱引用,这样在回收时,也可以回收Handler持有的对象。
  2. 在Activity的Destroy或者Stop时,应该移除消息队列中的消息,避免Looper线程的消息队列中有待处理的消息需要处理。

需要注意的是,AsyncTask内部也是Handler机制,同样存在内存泄漏风险,但其一般是临时性的。对于类似AsyncTask或是线程造成的内存泄漏,我们也可以将AsyncTask和Runnable类独立出来或者使用静态内部类。

Handler的错误使用

Handler是开发中异步处理最常使用到的工具之一,但是如果错误地使用Handler 也极容易产生内存泄漏。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class MainActivity extends AppCompatActivity {

private final Handler myHandler = new Handler(){
@Override
public void handleMessage(Message msg) {
//doSomething
}
};

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

myHandler.postDelayed(new Runnable() {
@Override
public void run() {
//doSomething
}
},60*10*1000);
}
}

怎么样,看起来是不是合情合理,但是其中隐藏着一个重要的隐患,当我们创建出一个Handler 的时候,代码中的mHandler为Handler 的非静态内部类的实例,所以mHandler 持有对外部类,即Activity 的引用。并且Handler 中的Looper不断轮询消息队列中的message,message 又持有mHandler的引用,但mHandler这玩意儿又持有Activity 的引用,所以这下倒好,因为你一个message 导致我整个Activity都无法被回收,你说气人不气人。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class MainActivity extends AppCompatActivity {

private final MyHandler mHandler = new MyHandler(this);

private static Runnable sRunnable = new Runnable() {
@Override
public void run() {
//doSomething
}
};

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mHandler.postDelayed(sRunnable, 60 * 10);

this.finish();
}

private static class MyHandler extends Handler {

private final WeakReference<MainActivity> mActivity;

public MyHandler(MainActivity activity) {
this.mActivity = new WeakReference<MainActivity>(activity);
}

@Override
public void handleMessage(Message msg) {
MainActivity activity = mActivity.get();

if (activity != null) {
//doSomething
}

}
}
}
容器中的对象没清理造成的内存泄漏

在退出程序之前,将集合里的东西clear,然后置为null,再退出程序

WebView

WebView都存在内存泄漏的问题,在应用中只要使用一次WebView,内存就不会被释放掉。我们可以为WebView开启一个独立的进程,使用AIDL与应用的主进程进行通信,WebView所在的进程可以根据业务的需要选择合适的时机进行销毁,达到正常释放内存的目的。

使用ListView时造成的内存泄漏

在构造Adapter时,使用缓存的convertView。

内存泄漏监控

一般使用LeakCanary进行内存泄漏的监控即可。

常用的内存泄漏分析工具

通过第三方库LeakCanary,和Android Studio自带的MAT。

内存溢出

即OOM,OOM时会导致程序异常。Android设备出厂以后,java虚拟机对单个应用的最大内存分配就确定下来了,超出这个值就会OOM。单个应用可用的最大内存对应于 /system/build.prop 文件中的 dalvik.vm.heapgrowthlimit。
此外,除了因内存泄漏累积到一定程度导致OOM的情况以外,也有一次性申请很多内存,比如说 一次创建大的数组或者是载入大的文件如图片的时候会导致OOM。而且,实际情况下 很多OOM就是因图片处理不当 而产生的。

内存优化

对象引用

强引用

如果一个对象具有强引用,GC就绝对不会回收它。当内存空间不足时,JVM会抛出OOM错误。

软引用

如果一个对象只具有软引用,则内存空间足够,GC时就不会回收它;如果内存不足,就会回收这些对象的内存。可用来实现内存敏感的高速缓存。
软引用可以和一个ReferenceQueue(引用队列)联合使用,如果软引用引用的对象被垃圾回收器回收,JVM会把这个软引用加入与之关联的引用队列中。
软引用是用来描述一些有用但并不是必需的对象,在Java中用java.lang.ref.SoftReference类来表示。对于软引用关联着的对象,只有在内存不足的时候JVM才会回收该对象。因此,这一点可以很好地用来解决OOM的问题,并且这个特性很适合用来实现缓存:比如网页缓存、图片缓存等。

弱引用

在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间是否足够,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
这里要注意,可能需要运行多次GC,才能找到并释放弱引用对象。
弱引用也是用来描述非必需对象的,当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示。

虚引用

只能用于跟踪即将对被引用对象进行的收集。虚拟机必须与ReferenceQueue类联合使用。因为它能够充当通知机制。
虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期。在java中用java.lang.ref.PhantomReference类表示。如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。

要注意的是,虚引用必须和引用队列关联使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会把这个虚引用加入到与之 关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

减少不必要的内存开销

AutoBoxing

自动装箱的核心就是把基础数据类型转换成对应的复杂类型。在自动装箱转化时,都会产生一个新的对象,这样就会产生更多的内存和性能开销。如int只占4字节,而Integer对象有16字节,特别是HashMap这类容器,进行增、删、改、查操作时,都会产生大量的自动装箱操作。
检测方式:使用TraceView查看耗时,如果发现调用了大量的integer.value,就说明发生了AutoBoxing。

内存复用
  • 资源复用:通用的字符串、颜色定义、简单页面布局的复用。
  • 视图复用:可以使用ViewHolder实现ConvertView复用。
  • 对象池:显示创建对象池,实现复用逻辑,对相同的类型数据使用同一块内存空间。
  • Bitmap对象的复用:使用inBitmap属性可以告知Bitmap解码器尝试使用已经存在的内存区域,新解码的bitmap会尝试使用之前那张bitmap在heap中占据的pixel data内存区域。
使用最优的数据类型

parseArray、SparseBooleanArray、LongSparseArray,使用这些API可以让我们的程序更加高效。HashMap 工具类会相对比较 低效,因为它 需要为每一个键值对都提供一个对象入口,而 SparseArray 就 避免 掉了 基本数据类型转换成对象数据类型的时间。

使用 IntDef和StringDef 替代枚举类型

使用枚举类型的dex size是普通常量定义的dex size的13倍以上,同时,运行时的内存分配,一个enum值的声明会消耗至少20bytes。
枚举最大的优点是类型安全,但在Android平台上,枚举的内存开销是直接定义常量的三倍以上。所以Android提供了注解的方式检查类型安全。目前提供了int型和String型两种注解方式:IntDef和StringDef,用来提供编译期的类型检查。
使用IntDef和StringDef需要在Gradle配置中引入相应的依赖包:

1
compile 'com.android.support:support-annotations:22.0.0'

LruCache

最近最少使用缓存,使用强引用保存需要缓存的对象,它内部维护了一个由LinkedHashMap组成的双向列表,不支持线程安全,LruCache对它进行了封装,添加了线程安全操作。当其中的一个值被访问时,它被放到队列的尾部,当缓存将满时,队列头部的值(最近最少被访问的)被丢弃,之后可以被GC回收。
除了普通的get/set方法之外,还有sizeOf方法,它用来返回每个缓存对象的大小。此外,还有entryRemoved方法,当一个缓存对象被丢弃时调用的方法,当第一个参数为true:表明环处对象是为了腾出空间而被清理时。否则,表明缓存对象的entry被remove移除或者被put覆盖时。

图片内存优化
  • 设置位图的规格:当显示小图片或对图片质量要求不高时可以考虑使用RGB_565,用户头像或圆角图片一般可以尝试ARGB_4444。通过设置inPreferredConfig参数来实现不同的位图规格,代码如下所示:

    1
    2
    3
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inPreferredConfig = Bitmap.Config.RGB_565;
    BitmapFactory.decodeStream(is, null, options);
  • inSampleSize:位图功能对象中的inSampleSize属性实现了位图的缩放功能,代码如下所示:

    1
    2
    3
    4
    BitampFactory.Options options = new BitmapFactory.Options();
    // 设置为4就是宽和高都变为原来1/4大小的图片
    options.inSampleSize = 4;
    BitmapFactory.decodeSream(is, null, options);
  • inScaled,inDensity和inTargetDensity实现更细的缩放图片:当inScaled设置为true时,系统会按照现有的密度来划分目标密度,代码如下所示:

    1
    2
    3
    4
    5
    BitampFactory.Options options = new BitampFactory.Options();
    options.inScaled = true;
    options.inDensity = srcWidth;
    options.inTargetDensity = dstWidth;
    BitmapFactory.decodeStream(is, null, options);

上述三种方案的缺点:使用了过多的算法,导致图片显示过程需要更多的时间开销,如果图片很多的话,就影响到图片的显示效果。最好的方案是结合这两个方法,达到最佳的性能结合,首先使用inSampleSize处理图片,转换为接近目标的2次幂,然后用inDensity和inTargetDensity生成最终想要的准确大小,因为inSampleSize会减少像素的数量,而基于输出密码的需要对像素重新过滤。但获取资源图片的大小,需要设置位图对象的inJustDecodeBounds值为true,然后继续解码图片文件,这样才能生产图片的宽高数据,并允许继续优化图片。总体的代码如下所示:

1
2
3
4
5
6
7
8
9
BitmapFactory.Options options = new BitampFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeStream(is, null, options);
options.inScaled = true;
options.inDensity = options.outWidth;
options.inSampleSize = 4;
Options.inTargetDensity = desWith * options.inSampleSize;
options.inJustDecodeBounds = false;
BitmapFactory.decodeStream(is, null, options);

图片放置优化

只需要UI提供一套高分辨率的图,图片建议放在drawable-xxhdpi文件夹下,这样在低分辨率设备中图片的大小只是压缩,不会存在内存增大的情况。如若遇到不需缩放的文件,放在drawable-nodpi文件夹下。

在App可用内存过低时主动释放内存

在App退到后台内存紧张即将被Kill掉时选择重写 onTrimMemory/onLowMemory 方法去释放掉图片缓存、静态缓存来自保。

item被回收不可见时释放掉对图片的引用
  • ListView:因此每次item被回收后再次利用都会重新绑定数据,只需在ImageView onDetachFromWindow的时候释放掉图片引用即可。
  • RecyclerView:因为被回收不可见时第一选择是放进mCacheView中,这里item被复用并不会只需bindViewHolder来重新绑定数据,只有被回收进mRecyclePool中后拿出来复用才会重新绑定数据,因此重写Recycler.Adapter中的onViewRecycled()方法来使item被回收进RecyclePool的时候去释放图片引用。
避免创作不必要的对象

例如,我们可以在字符串拼接的时候使用StringBuffer,StringBuilder。

自定义View中的内存优化

例如,在onDraw方法里面不要执行对象的创建,一般来说,都应该在自定义View的构造器中创建对象。

其它的内存优化注意事项

除了上面的一些内存优化点之外,这里还有一些内存优化的点我们需要注意,如下所示:

  • 尽使用static final 优化成员变量。
  • 使用增强型for循环语法。
  • 在没有特殊原因的情况下,尽量使用基本数据类型来代替封装数据类型,int比Integer要更加有效,其它数据类型也是一样。
  • 在合适的时候适当采用软引用和弱引用。
  • 采用内存缓存和磁盘缓存。
  • 尽量采用静态内部类,可避免潜在由于内部类导致的内存泄漏。
您的支持是我原创的动力