Pride's Blog

Message你了解吗?

· Pride

面试官:“Handler你都了解吧?里面有一个获取Message对象的方法你清楚吗?”

我:“Message#obtain?”

面试官:“嗯,那为什么Android里面要设计这个东西呢,你了解吗?”

我:“可以防止重复创建减少性能损耗,充分利用消息池缓存这样”

面试官:“嗯....还有什么要补充的吗?”

内心OS:“emmmmm,你说的对,但是还差点意思,没说到我的点上.....”

一、Message起底§

    对性能和效率有了解的同学可能就会通过 handler.obtainMessage() 或 Message.obtain() 的方式去得到这个Message对象,使用Message.obtain()的好处是Message对象可以重复使用,可以免除一直new Message对象造成无谓的内存压力(不断新建销毁对象),

1. Message message = new Message()§

    最常见的创建对象的方式,每次需要Message对象的时候都创建一个新的对象,每次都要去堆内存开辟对象存储空间,对象使用完后JVM就会这个废弃的对象进行垃圾回收。到这里,你可能会说:我们平时使用对象不都是这样的嘛,有什么大惊小怪的?难道我们平时操作对象的方式都存在效率和性能的问题?

2. Message message = handler.obtainMessage()§

如上所述,这种方式来创建Message对象,效率会更高,那么何以见得?直接看源码

public final Message obtainMessage()
{
    return Message.obtain(this);
}
public static Message obtain(Handler h) {
    Message m = obtain();
    m.target = h;
    return m;
}

最终通过Message类的内部方法obtain(),获取到了这个Message对象。

3. Message message = Message.obtain()§

// 给Message加一个对象锁,不允许多个线程同时访问Message类和obtain方法
public static final Object sPoolSync = new Object();
// 存储我们循环利用Message的单链表
private static Message sPool;
// 单链表的链表的长度,即存储的Message对象的个数
private static int sPoolSize = 0;

public static Message obtain() {
    synchronized (sPoolSync) {
        if (sPool != null) {
            // 链表操作,将链表头节点移除作为重用的Message对象,第二个节点作为新链表(sPool)的头节点
            Message m = sPool;
            sPool = m.next;
            m.next = null;
            m.flags = 0; // clear in-use flag
            // 链表的长度减一,把第三步得到的Message返回,用于重复利用
            sPoolSize--;
            return m;
        }
    }
    return new Message();
}

message

整个逻辑串下来大概是这样:

  1. synchronized (sPoolSync):给对象加锁,保证同一时刻只有一个线程使用Message。
  2. if (sPool != null):判断sPool链表是否是空链表,如果是空,就直接创建一个Message对象返回;否则就进入第三步。
  3. 链表操作,将链表头节点移除作为重用的Message对象,第二个节点作为新链表(sPool)的头节点。
  4. 链表的长度减一,把第三步得到的Message返回,用于重复利用。

总结§

    通过obtain方法获取Message对象使得Message对象得到了复用,减少了每次获取Message时去申请空间的时间,同时也不会一直去创建新的对象,降低了JVM垃圾回收的压力,提高了效率。

二、面试官内心OS起底§

    说的都对,那面试官内心OS到底是怎么产生的呢?

    这就要说到,降低了JVM垃圾回收的压力,那假如不降低JVM垃圾回收的压力,会造成什么事情?

    这里就要介绍到「内存抖动」了,内存抖动是什么?可能在大多数时间大家听说的多数都是内存溢出(OOM)或者是内存泄露(Leak),没有太关注过内存抖动。

    在程序里,每创建一个对象,就会有一块内存分配给它;每分配一块内存,程序的可用内存也就少一块;当程序被占用的内存达到一定临界程度,GC 也就是垃圾回收器(Garbage Collector)就会出动,来释放掉一部分不再被使用的内存。Android 里的 Handler 在主线程和子线程交互的时候会创建大量的 Message 对象,这就会导致内存占用的迅速攀升;然后很快,当 Message 对象被 target 处理器使用完后可能就会触发 GC 的回收动作,也就是这些被你创建出来的对象被 GC 回收掉。垃圾内存太多了就被清理掉,这是 Java 的工作机制,这不是问题。问题在于,频繁创建这些对象会造成内存不断地攀升,在刚回收了之后又迅速涨起来,那么紧接着就是又一次的回收,对吧?这么往复下来,最终导致一种循环,一种在短时间内反复地发生内存增长和回收的循环。

    这种循环往复的状态就像是水波纹的颤动一样,它的专业称呼叫做 Memory Churn,Android 的官方文档里把它翻译做了内存抖动。所以内存抖动其实并不是我们的内存在整体地进行摇晃这样神奇的事情。

yep,就像下面这样。

内存抖动

三、怎么去优化内存抖动?§

    到这里为止,我们起底了面试官,知道了面试官其实更想知道的是,为什么要用 Message#obtain() ,是为了避免「内存抖动」的发生,在很多时候,造成抖动的原因不只是Message,有可能是你的自定义View或者一切需要大量吞吐内存空间的地方,对于这些地方需要记住一个宗旨,不要让代码干多余的事,如果是我们调用了系统的API导致了不合理地大量对象的创建,那么就要考虑这个系统API为什么会这样创建对象,有没有其他方法避免吗,从业务代码层来合理使用这个API,实在不行再考虑自定义api或者换个系统API。