Handler 的 Message 实例怎么获得?为什么不是直接 new?

本文详细解析了Android中Message Pool的底层原理,包括obtain()方法的工作机制,如何通过复用减少内存消耗,以及在实际场景中的动态缓存和回收策略。特别关注了Message的申请、执行、回收过程及其对内存管理的影响。

在这里插入图片描述

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 对象缓存起来
<think> 我们正在讨论的是之前实现的全局Toast工具类中的Handler使用问题。 在之前的代码中,我们使用了: Handler(Looper.getMainLooper()).post { ... } 这样每次调用showToast时,如果不在主线程,就会创建一个新的Handler对象。 问题:Handler每次new好吗? 回答:虽然技术上可行,但每次创建新的Handler实例不是最佳实践。因为创建Handler对象会有一定的开销(虽然很小),而且如果频繁调用,可能会产生多个不必要的Handler实例。更好的做法是复用同一个Handler实例。 改进方案:我们可以将Handler对象作为单例的一部分,只初始化一次。 修改后的代码示例: ```kotlin object ToastUtils { private lateinit var appContext: Context private val mainHandler by lazy { Handler(Looper.getMainLooper()) } // 使用lazy初始化,确保只创建一次 fun init(context: Context) { appContext = context.applicationContext } fun showToast(message: String, duration: Int = Toast.LENGTH_SHORT) { if (Looper.myLooper() == Looper.getMainLooper()) { Toast.makeText(appContext, message, duration).show() } else { mainHandler.post { Toast.makeText(appContext, message, duration).show() } } } } ``` 解释: - 使用`private val mainHandler`并配合`lazy`初始化,确保Handler只有一个实例。 - 这样在多次调用showToast时,如果需要在子线程中显示Toast,都会使用同一个Handler实例来post到主线程。 优点: - 避免了重复创建Handler的开销。 - 更符合资源管理的最佳实践。 注意:HandlerAndroid中需要注意内存泄漏问题,但这里我们使用的是主线程的LooperLooper.getMainLooper()),并且我们的Handler是静态的(在单例对象中),所以不会持有外部Activity的引用,因此不会造成内存泄漏。 结论:不推荐每次new Handler,应该复用同一个Handler实例。 §§相关问题§§1. 为什么在单例中使用Handler(Looper.getMainLooper())不会导致内存泄漏?2. 除了使用Handler,还有哪些方法可以确保代码在主线程执行(例如使用runOnUiThread)?3. 使用lazy初始化Handler相比直接初始化有何优缺点?4. 在Kotlin中,如何正确销毁单例中的Handler以避免潜在问题?5. 如果在非主线程频繁调用showToast,使用单例Handler和每次new Handler在性能上有多大差异? </think> 在 Android Kotlin 中,每次调用 `new Handler()` 创建新实例**不是最佳实践**,尤其在高频调用场景。以下是详细分析和改进方案: ### 问题分析 1. **资源开销**: ```kotlin Handler(Looper.getMainLooper()).post { ... } // 每次调用都创建新对象 ``` 每次 `new Handler()` 会产生: - 微小但可累积的内存分配(约 0.5KB/次) - 对象创建/垃圾回收的 CPU 开销 2. **线程安全隐患**: - 并发调用时可能创建多个冗余 Handler - Handler 内部依赖 `MessageQueue`,多次创建无实际收益 ### 优化方案(推荐 ✅) **复用单例 Handler**: ```kotlin object ToastUtils { private lateinit var appContext: Context // 单例 Handler(Lazy 初始化) private val mainHandler by lazy { Handler(Looper.getMainLooper()) } fun showToast(...) { ... mainHandler.post { // 复用同一个 Handler Toast.makeText(...).show() } } } ``` ### 性能对比 | 方案 | 内存开销 | GC 压力 | 推荐场景 | |---------------------|----------|---------|------------------| | 复用单例 Handler | 恒定 ~0.5KB | 低 | **推荐全局使用** | | 每次 new Handler() | 递增累积 | 高 | 极低频调用 | ### 替代方案 ```kotlin // 方法1:使用 View.post() activity.runOnUiThread { ... } // 方法2:协程主线程调度 CoroutineScope(Dispatchers.Main).launch { ... } ``` > 📌 **结论**:在工具类中**优先复用单例 Handler**,避免高频调用时的性能损耗。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

TechMerger

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值