How do I get a Massage instance of Handler? Why not just new?

Keywords: Java Android app Handler

Basically all by calling get ()

There are many ways to use Message, whether it's Handler#obtainMessage(), Message#obtain(), or even Handler#postRunnable(), which is essentially a static method to call 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() is also public, but get () is more recommended

Get () is not a brainless new instance, but the first out of a Pool that caches messages. It is used frequently in handlers and in complex scenarios where messages are multiplexed to reduce memory consumption and improve efficiency.

    public static Message obtain() {
        synchronized (sPoolSync) { // Synchronization lock added to Pool
            if (sPool != null) {
                Message m = sPool; // Return Head, or sPool
                sPool = m.next; // Update Head before returning
                m.next = null; // Also point the original next to empty
                m.flags = 0; // clear in-use flag
                sPoolSize--; // Decrease the length of the record Pool
                return m;
            }
        }
        return new Message(); // Newone if no Message has been cached
    }

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

Note: Although named Pool, it essentially maintains the Head (sPool) and updates the single-chain list pointed to by the next.

Cache to Pool when recycle

When the message is executed, it is recycle d: specifically, it is cached to the sPool property, and the recorded poolSize is increased, updating the next Message instance pointed to by next before caching.

    /*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 has an upper limit of 50
                next = sPool; // Use the previously saved Head as next
                sPool = this; // Use current Message as Head
                sPoolSize++; // Self-adding length
            }
        }
    }

Verify the upper limit of Pool size

Messages contain requests, executions, Recycle s, and caches. Handler s are generally more complex to use. Few requests start after one Message has been cached, more often when the previous Message is waiting for execution or execution, and many other messages start requesting again. In this case, the Message in the Pool Will be less and less used, then start to keep new.

Cyclically, the size of a Pool increases, the number of Messagesages stored increases, and the next Message request is picked up individually from the Pool. However, the size of a Pool does not always accumulate, but when recycle finds that the size has exceeded the agreed maximum of 50, the current Message instance is no longer cached, there is no static application, and GC is waiting for it.Recycle.

Let's verify this: for example, send 50 Delay Message s and one Delay older Message to Handler at the same time, print Pool's Size when the first 50 messages are executed, and print Size when the fifty messages are executed. Finally, request another Message to see Pool's Size before and after it runsSituation.

    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)
    }

Send code for multiple messages and individual messages, and Pool Size for printing messages.

Note: The Print Size reflection method may be blocked by a non-public API, remember to temporarily close the check of the non-public API by entering a command (adb shell settings put global hidden_api_policy 0) before executing.

    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 has not cached any messages at the beginning, so it has kept 51 instances, cached 50 messages to Pool after the first 50 messages have been executed, and when the fifty-first message is executed, it will no longer be cached inside, so the current Size is fixed at 50. Message requests follow-upIf so, take one from the pool, run out and put it back.

It is also possible to see from the log:

    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

summary

It is very rare for a Pool to cache 50 messages in a row, and even if this happens, 50 messages are enough to be reused. In this rare case, a Super-Extreme state of more than 50 messages is requested all at once, plus a new Message. At least this extremely low probability always consumes more memory than the unlimited number of cached messages.Well done!

So get () is more recommended, summarizing:

  1. Cached Message s can be retrieved from Pool
  2. You can also cache old Message objects when they are used

Posted by PDP11 on Mon, 13 Sep 2021 13:10:07 -0700