Handler线程模型与内存

1) 线程模型:Handler 绑定 Looper,跨线程=投递到对方队列

  • 一个线程最多一个 Looper ,里面有一个 MessageQueue ;Looper.loop() 在该线程串行取消息执行。
  • Handler(Looper) 决定了回调在哪个线程跑 ;不管你从哪个线程 post/send,最终都在 Handler 绑定的 Looper 线程执行。
  • 跨线程切换 :就是把任务丢进目标线程的 MessageQueue。
scss 复制代码
// 切到主线程
val main = Handler(Looper.getMainLooper())
main.post { /* 这里一定在 UI 线程 */ }

// 专用后台线程
val ht = HandlerThread("worker").apply { start() }
val worker = Handler(ht.looper)
worker.post { /* 后台串行任务 */ }

并发 vs 串行:同一个 Looper 里的任务严格按队列执行,不会并行;想并行要多个线程或线程池。


2) Message 复用与内存

  • 为什么用 obtain :Message.obtain()/Handler.obtainMessage() 走对象池(框架维护的小池),避免频繁 new。
  • 发送后不要再改 msg :入队后可能被并发消费,自行复用/手动 recycle() 会踩内存;让框架在派发后自动回收。
  • 尽量少放大对象 :obj 指向大 bitmap/大数组,会被队列强引用直到执行完。推荐用 what/arg1/arg2 或放轻量 token,大对象交给共享缓存或弱引用。
scss 复制代码
// 推荐:轻量 payload
handler.obtainMessage(WHAT_SAVE, id /*arg1*/, 0).sendToTarget()

3) 生命周期与泄漏(重点)

3.1 非静态内部类 Handler / Runnable 泄漏 Activity

  • 非静态内部类、匿名 Runnable 会隐式持有外部 Activity 。如果消息延时很久 或队列被阻塞,就可能在页面退出后仍被引用 → 泄漏。
  • 解法 A:静态 + WeakReference
scala 复制代码
static class UiHandler extends Handler {
    private final WeakReference<Activity> ref;
    UiHandler(Activity a) { super(Looper.getMainLooper()); ref = new WeakReference<>(a); }
    @Override public void handleMessage(Message msg) {
        Activity act = ref.get(); if (act == null) return;
        // ...
    }
}
  • 解法 B:生命周期清理(最实用)****

    在 onDestroy()(或 onStop())清掉尚未执行的消息/回调:

kotlin 复制代码
class MyActivity : AppCompatActivity() {
    private val h = Handler(Looper.getMainLooper())
    private val token = Any() // 批量标记

    override fun onCreate(b: Bundle?) {
        super.onCreate(b)
        h.postAtTime({ /* ... */ }, token, SystemClock.uptimeMillis() + 5_000)
    }

    override fun onDestroy() {
        h.removeCallbacksAndMessages(null)      // 全部清除
        // 或只清此页相关:
        // h.removeCallbacksAndMessages(token)
        super.onDestroy()
    }
}
  • 说明:postAtTime(Runnable, token, when) / removeCallbacksAndMessages(token) 用同一 token成组移除。

  • 解法 C:直接用生命周期感知(优先选):

    • View.post {}:View 销毁后其 Handler 也随之失效;

    • 协程 lifecycleScope + Dispatchers.Main,在 onDestroy 自动取消;

    • LifecycleOwner + DefaultLifecycleObserver 中统一清理。

3.2 延时消息在页面退出后仍触发

  • 根因:消息与 Runnable 被队列持有。
  • 规避:退出时清理(如上);避免长延时;必要时把任务迁到进度可控的后台组件(WorkManager/Service)。

4) 退出与清理(后台 Looper 线程)

  • Looper.quit()立刻 退出,队列里未到期消息会被丢弃
  • Looper.quitSafely() :处理完所有到期的同步消息后退出(更温和)。
  • HandlerThread 清理一定在不用时 quitSafely() + join(),确保线程结束、释放栈和队列:
scss 复制代码
val ht = HandlerThread("worker").apply { start() }
val h = Handler(ht.looper)

// ... 使用

h.removeCallbacksAndMessages(null) // 先清队列(可选)
ht.quitSafely()                    // 请求退出
ht.join()                          // 等线程收尾,防泄漏
  • 不可复活 Looper :quit 之后这个 Looper 就"死"了;要用就新建 HandlerThread。对已退出的 Looper 发消息会抛 IllegalStateException。

5) 进阶:优先级与阻塞的影响(简述)

  • 同一 Looper 下所有 Handler 共享一个队列 ,按 when 与入队顺序执行;某个回调里做耗时阻塞后续所有消息(包括 UI 绘制),造成卡顿/ANR。
  • UI 线程要保持小而快:重活丢后台;必要时用 IdleHandler 做低优先级清理。
  • 不要滥用 sendMessageAtFrontOfQueue / "异步消息"插队,以免打乱渲染节拍。

6) 小抄(Anti-pattern vs Best Practice)

Anti-pattern

  • 非静态内部 Handler/匿名 Runnable 持有 Activity。

  • Message.obj 塞大对象,或循环复用已发送的 Message。

  • HandlerThread 不退出;在 UI 线程执行重活。

Best Practice

  • Handler(Looper.getMainLooper()) + onDestroy() 里 removeCallbacksAndMessages(null)。
  • 用 Message.obtain()/Handler.obtainMessage();payload 轻量化。
  • 后台串行:HandlerThread(用完 quitSafely()+join());并行:线程池/协程。
  • Lifecycle/Coroutine 优先:viewLifecycleOwner.lifecycleScope.launch { ... }。

一句话总结:Handler 决定"在哪个线程执行"Message 用 obtain 复用且轻量化页面销毁要清队列 ;后台 Looper 线程记得 quitSafely()+join() 。把这四点做到位,既稳又不漏。

相关推荐
汤姆Tom2 小时前
CSS 新特性与未来趋势
前端·css·面试
南北是北北2 小时前
Thread ↔ Looper ↔ MessageQueue ↔ Handler ↔ Message之间的关系
面试
南北是北北3 小时前
list并发与共享
面试
南北是北北3 小时前
泛型的三种型变类型:逆变,协变和不变
面试
ShooterJ4 小时前
Mysql小表驱动大表优化原理
数据库·后端·面试
小时前端4 小时前
🚀 面试必问的8道JavaScript异步难题:搞懂这些秒杀90%的候选人
javascript·面试
Takklin4 小时前
JavaScript 面试笔记:作用域、变量提升、暂时性死区与 const 的可变性
javascript·面试
知其然亦知其所以然4 小时前
面试官一开口就问:“你了解MySQL水平分区吗?”我当场差点懵了……
后端·mysql·面试
老马啸西风4 小时前
力扣 LC27. 移除元素 remove-element
算法·面试·github