阅读 547

Handler 的 Message 实例怎么创建,为什么不是直接 new?

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

本文同时参与「掘力星计划」,赢取创作大礼包,挑战创作激励金!

Message#obtain() 大家都不陌生,也能说出来是原理是 Pool 的复用。但鲜少留意细节,本文带你了解一下其深层次的原理。

基本都是调用 obtain() 来获得

使用 Message 的方式有很多种,无论是 Handler#obtainMessage() 还是 Message#obtain(),甚至是 Handler#postRunnable(),本质上都是调用 Message 的静态方法 obtain()。

    public final Message obtainMessage() {
        return Message.obtain(this);
    }
复制代码
    public final boolean post(@NonNull Runnable r) {
       return  sendMessageDelayed(getPostMessage(r), 0);
    }

    private static Message getPostMessage(Runnable r) {
        Message m = Message.obtain();
        ...
    }
复制代码

Message() 也是 public 的,但更建议 obtain()

obtain() 不是无脑的 new 一个实例,而是从缓存 Message 的 Pool 中取出第一个,在 Handler 频繁使用,Message 多且繁杂的场景中复用 Message 减少内存消耗,提高效率。

    public static Message obtain() {
        synchronized (sPoolSync) { // 给 Pool 加的同步锁
            if (sPool != null) {
                Message m = sPool; // 将 Head 即 sPool 返回
                sPool = m.next; // 返回之前将 Head 更新
                m.next = null; // 同时将原有的 next 指向清空
                m.flags = 0; // clear in-use flag
                sPoolSize--; // 将记录 Pool 的长度自减
                return m;
            }
        }
        return new Message(); // 没有缓存过 Message 的话 new 一个
    }

    /** Constructor (but the preferred way to get a Message is to call {@link #obtain() Message.obtain()}).
    */
    public Message() {
    }
复制代码

备注:虽然命名为 Pool,但本质是维护 Head(sPool)和更新 next 指向的单链表。

recycle 时候会缓存到 Pool

等 Message 执行完之后会 recycle 它:具体处理是将它缓存到 sPool 属性,同时记录的 poolSize 增加,在缓存前会先更新 next 指向的下个 Message 实例。

    /*package*/ Message next;
    public static final Object sPoolSync = new Object();
    private static Message sPool;
    private static int sPoolSize = 0;

    void recycleUnchecked() {
        ...
        synchronized (sPoolSync) {
            if (sPoolSize < MAX_POOL_SIZE) { // Pool 有 50 的上限
                next = sPool; // 将之前存的 Head 作为 next
                sPool = this; // 将当前的 Message 作为 Head
                sPoolSize++; // 长度自加
            }
        }
    }
复制代码

验证 Pool size 的上限

Message 包含着申请、执行、Recycle 和缓存几种操作。Handler 的使用一般较为繁杂,一个 Message 缓存完之后另一个才开始申请的情况几乎很少,更多的是前一个 Message 在等待执行或执行中的时候其他多个 Message 又开始申请。这样子的话,Pool 中的 Message 会越来越不够用,则开始持续 new。

循环往复,Pool 的 size 会越来越大,存放的 Message 会越来越多,下一次 Message 的申请就从这个 Pool 里逐个领取。但 Pool 的 size 并非一直累加,但 recycle 的时候发现 size 已经超过约定的 50 上限的时候,不再将当前的 Message 实例缓存,没有了静态应用,等待他的则是 GC 的回收。

我们来验证一下这种情形:比如向 Handler 同时 send 50 个 Delay Message 和 1 个 Delay 更久的 Message,等前 50 个 Message 执行完的时候打印一下 Pool 的 Size,第 51 个 Message 执行完的时候也打印一下 Size。最后呢再申请一个 Message,查看它运行前和结束两个时间点 Pool 的 Size 情况。

    private fun startHandlerThread() {
        val handlerThread = HandlerThread("Test Handler thread")
        handlerThread.start()

        testHandler = Handler( handlerThread.looper ) { msg ->
            when (msg.what) {
                50 -> mainHandler?.post {
                        getMessagePoolSize(msg).let { Log.d("MainActivity", "50 finished and pool size:$it") }
                }
                51 -> mainHandler?.post {
                    getMessagePoolSize(msg).let { Log.d("MainActivity", "51 finished and pool size:$it") }
                    sendMessage(testHandler, 52)
                    getMessagePoolSize(msg).let { Log.d("MainActivity", "52 waiting and pool size:$it") }
                }
                52 -> mainHandler?.post {
                    getMessagePoolSize(msg).let { Log.d("MainActivity", "52 finished and pool size:$it") }
                }
            }
            true
        }

        sendMessages(testHandler)
        sendMessage(testHandler, 51)
    }
复制代码

发送多个 Message 和单个 Message 的代码,以及打印 Message 的 Pool Size。

注意:打印 Size 的反射方法可能遭遇非公开 API 的拦截,执行之前记得输入命令(adb shell settings put global hidden_api_policy 0)临时关闭非公开 API 的检查。

    private fun sendMessages(handler: Handler) {
        for (i in 1..50) {
            Message.obtain().let {
                it.what = i
                handler.sendMessageDelayed(it, 2000L)
            }
        }
    }

    private fun sendMessage(handler: Handler, what: Int) {
        Message.obtain().let {
            it.what = what
            handler.sendMessageDelayed(it, 2500L)
        }
    }

    private fun getMessagePoolSize(message: Message): Int {
        var size = 0
        try {
            val clazz = Class.forName("android.os.Message")
            val field = clazz.getDeclaredField("sPoolSize")
            field.isAccessible = true
            size = field[message] as Int
        } catch (e: Exception) { Log.e("MainActivity", "getMessagePoolSize exception:$e") }
        return size
    }
复制代码

刚开始的时候 Handler 尚未缓存任何 Message,所以一直 new 了 51 个实例,等前 50 个 Message 都执行好之后向 Pool 缓存了 50 个 Message,第 51 个 Message 执行完的时候就不会再往里面缓存了,所以此刻的 Size 固定为 50。后续申请 Message 的话,则从池子里取一个,用完再放回去。

从日志也可能看出来:

    D/MainActivity: Current thread:Thread[Test Handler thread,5,main] Message[1] handle
    D/MainActivity: Current thread:Thread[Test Handler thread,5,main] Message[2] handle
    ...
    D/MainActivity: Current thread:Thread[Test Handler thread,5,main] Message[49] handle
    D/MainActivity: Current thread:Thread[Test Handler thread,5,main] Message[50] handle
    D/MainActivity: 50 finished and pool size:50
    D/MainActivity: Current thread:Thread[Test Handler thread,5,main] Message[51] handle
    D/MainActivity: 51 finished and pool size:50
    D/MainActivity: 52 waiting and pool size:49
    D/MainActivity: Current thread:Thread[Test Handler thread,5,main] Message[52] handle
    D/MainActivity: 52 finished and pool size:50
复制代码

Pool 连续缓存了 50 个 Message 的情况非常少见,即便真得发生了这种情况,50 个 Message 也足够复用了。在这种极少的情况之下又发生了一下子又申请超过 50 个 Message 的超极端状态,额外再 new Message 就是了。起码这个极低概率下的内存消耗总比无上限的缓存 Message 来得好!

总结

除了 Message,Android 系统当中还有很多类也采用 obtain() 来获取实例,比如 MotionEventKeyEventAccessibilityEvent等。

  • MotionEvent 和 KeyEvent 的 obtain() 的原理和 Message 几乎完全一致,都是使用单链表来维护实例的复用,不同的是 Event 池子的 Size 上限为 10
  • AccessibilityEvent 则稍稍不同,obtain() 是从 SynchronizedPool 里取得实例,其本质上是固定长度的数组,每次从最后一个元素获取和存入实例,感兴趣的朋友可看看源码

obtain 的实现思路非常巧妙,了解它不仅仅是应付别人的提问,更是在日后的编码当中应用上这种实现思路,尤其是可以用在频繁使用某个实例的场景。

再次总结下 obtain 的好处:

  1. 可以从 Pool 里取出缓存好的 Message
  2. 也可以在使用过的时候将旧的 Message 对象缓存起来
文章分类
Android
文章标签