ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Android Handler机制深度解析:从消息队列到线程通信的底层原理

Android Handler机制深度解析:从消息队列到线程通信的底层原理

1. 项目概述:为什么Handler是Android开发的“任督二脉”?

干了这么多年Android开发,如果说有什么东西是面试必问、项目必用、原理又常常让人犯迷糊的,Handler机制绝对排得上号。它就像Android应用开发的“任督二脉”,打通了,你对整个应用的消息驱动模型、线程间通信、UI更新的理解就能上一个台阶;没打通,写出来的代码就可能埋下内存泄漏、ANR(应用无响应)的隐患,或者遇到一些诡异的线程问题无从下手。我见过不少开发者,用Handler发消息、切线程到主线程更新UI很熟练,但被问到“Looper是怎么无限循环而不卡死主线程的?”、“ThreadLocal在这里面扮演什么角色?”、“一个线程能有几个Looper?”时,就有点含糊其辞了。

今天,我们就抛开那些零散的博客和官方文档的片段式描述,把Handler、Looper、MessageQueue、Message以及ThreadLocal这“四大金刚”串起来,从源码和应用两个层面,彻底搞懂这套机制。我们的目标不是背诵源码,而是理解其设计哲学和实现精髓,让你在遇到诸如“Handler内存泄漏”、“子线程更新UI报错”、“消息延迟不准确”等问题时,能一眼看穿本质,快速定位解决。这篇文章会很长,但保证你看完能说一句:“原来如此,不过如此。”

2. Handler机制核心组件深度拆解

要理解整个机制,我们必须先认识舞台上的每一位“演员”,以及他们各自的职责和相互关系。整个Handler机制可以看作一个高效的消息处理流水线。

2.1 Message:消息的载体

Message是这条流水线上被传递的“包裹”。它不仅仅是一个简单的字符串或对象,而是一个结构化的数据容器。

核心属性解析:

  • what: 一个整型的用户自定义消息代码,用于区分不同的消息类型。这是识别消息意图最常用的字段。比如,你可以定义static final int MSG_UPDATE_UI = 1;
  • arg1,arg2: 两个整型参数,用于传递简单的整数值。传递开销比obj小,适合传递如进度值、状态码等。
  • obj: 一个Object类型的参数,可以传递任意对象。这里是最容易引发内存泄漏和类型安全问题的重灾区。如果传递了Activity等Context引用,并且消息被长时间延迟或积压,就会导致Activity无法被回收。
  • target: 一个Handler类型的引用,指向最终处理这条消息的Handler。当消息被Looper从队列中取出时,就是通过target.dispatchMessage(msg)来分发的。
  • callback: 一个Runnable对象。如果设置了callback,当消息被处理时,会优先执行这个Runnablerun()方法。
  • when: 消息应该被处理的绝对时间(基于SystemClock.uptimeMillis())。MessageQueue正是根据这个时间戳来对消息进行排序,实现延迟发送的功能。
  • next: 指向下一个Message的引用。这说明MessageQueue内部是一个单链表结构,Message是链表节点。
  • flags: 消息的标志位,例如FLAG_IN_USE表示消息正在被使用,FLAG_ASYNCHRONOUS表示这是一个异步消息(与同步屏障相关)。

关键技巧与“坑点”:

注意:强烈建议使用Message.obtain()Handler.obtainMessage()来获取Message实例,而不是直接new Message()。因为系统维护了一个Message对象池(链表结构),obtain()方法会从池中复用空闲的Message对象。这能有效减少频繁创建和销毁小对象带来的内存抖动,提升性能。消息被Looper处理完毕后,会调用recycleUnchecked()将其回收到池中。

一个常见的误区是认为obj字段传递了对象就会自动持有强引用导致泄漏。其实关键在于Handler的生命周期。如果Handler是Activity的非静态内部类(隐式持有Activity引用),并且发送了延迟消息,那么这条消息(通过target持有Handler)→ Handler → Activity 这条引用链就会阻止GC回收Activity。解决方案是使用静态内部类+弱引用,或者确保在Activity销毁时移除所有未处理的消息(handler.removeCallbacksAndMessages(null))。

2.2 MessageQueue:消息队列的管理者

MessageQueue,顾名思义,是一个消息队列。但它不是一个简单的先进先出(FIFO)队列,而是一个基于when(执行时间)排序的优先级队列,内部通过单链表实现。

核心职责:

  1. 入队(enqueueMessage:当调用handler.sendMessage()post(Runnable)时,最终会走到这里。方法会根据消息的when时间,将其插入到链表合适的位置,保证链表按执行时间从早到晚排序。
  2. 出队(next:这是Looper循环的核心。next()方法是一个可能会阻塞(进入休眠)的方法。它的逻辑是:
    • 如果队列为空,或者队首消息的执行时间when还没到,线程就会进入休眠状态。
    • 休眠时间 = 队首消息的when- 当前时间。
    • 当有新的消息入队,或者休眠时间到了,线程会被唤醒,next()方法返回队首的消息给Looper

同步屏障与异步消息:这是MessageQueue的一个高级特性。正常情况下,所有消息都是同步的,按顺序执行。但有时需要优先处理某些高优先级任务(比如UI绘制)。

  • 同步屏障:通过postSyncBarrier()插入一个targetnull的特殊消息。这个屏障会挡住其后所有的同步消息
  • 异步消息:标志了FLAG_ASYNCHRONOUS的消息。当队列头部遇到同步屏障时,它会跳过所有被挡住的同步消息,去寻找下一个异步消息来执行。
  • 应用:View的绘制流程Choreographer就使用了异步消息来确保渲染信号能被优先处理,避免被业务逻辑消息阻塞造成卡顿。

“坑点”实录:next()方法内部的休眠-唤醒机制依赖于Linux的epoll机制来监控一个文件描述符(主要是用于输入事件)。这个设计非常高效,但也是理解“Looper不死循环”的关键:当没有消息需要处理时,线程会释放CPU进入休眠,而不是空转浪费资源。这回答了开头的第一个疑问。

2.3 Looper:消息循环的引擎

如果说MessageQueue是仓库,Looper就是不知疲倦的搬运工和调度员。它的核心就是一个loop()方法。

核心工作流程:

  1. 准备:通过Looper.prepare()初始化当前线程的Looper(和MessageQueue),并将其存储到线程单例变量中。
  2. 循环:调用Looper.loop(),开启一个无限for (;;)循环。
  3. 取消息:在循环中,不断调用MessageQueue.next()获取下一条消息。如果队列为空,next()会阻塞,loop()也随之阻塞,线程休眠。
  4. 派发:拿到消息后,调用msg.target.dispatchMessage(msg),将消息交还给发送它的Handler去处理。
  5. 回收:消息处理完毕后,调用msg.recycle()将其回收到对象池。

一个线程有几个Looper?这是经典面试题。答案是:至多一个Looper.prepare()方法内部会检查当前线程是否已存在Looper,如果存在则抛出异常(“Only one Looper may be created per thread”)。这个单例是通过ThreadLocal来保证的,我们稍后详解。主线程(UI线程)的Looper在应用启动时由ActivityThreadmain()方法自动创建并开启循环,所以我们不需要手动处理。

loop()方法为什么不卡死主线程?结合MessageQueue.next()的说明,答案就很清晰了:当没有消息时,next()方法会触发nativePollOnce()进入Native层的休眠,此时主线程会释放CPU资源。当有新的消息到达(例如触摸事件、绘制信号、我们发送的Handler消息)时,会通过写入管道(pipe)文件描述符来唤醒epoll等待,next()方法返回,loop()继续处理消息。这个“等待-执行”模型,使得主线程既能及时响应事件,又能在空闲时不消耗算力。

2.4 Handler:消息的发送者与处理者

Handler是我们开发者最直接打交道的组件。它身兼二职:向某个线程的消息队列发送消息,以及在该线程中处理分发到的消息

发送消息:我们常用的sendMessage()post(Runnable)系列方法,最终都会将消息或Runnable包装成Message,并赋值target为当前Handler,然后调用MessageQueue.enqueueMessage()将其放入与当前线程关联的Looper对应的消息队列中。这里的关键是Handler必须关联一个带有Looper的线程,否则在构造时就会报错(“Can‘t create handler inside thread that has not called Looper.prepare()”)。

处理消息:消息被Looper取出后,会回调HandlerdispatchMessage(Message msg)方法。这个方法的分发优先级非常重要:

  1. 首先检查msg.callback是否不为空(即通过post(Runnable)方式发送的消息),如果是,则直接执行Runnable.run()
  2. 否则,检查HandlermCallback(一个Callback接口)是否不为空,如果是,则尝试让mCallback.handleMessage(msg)处理。如果该方法返回true,则处理结束;返回false,则继续。
  3. 最后,才调用我们通常重写的handleMessage(Message msg)方法。

这个优先级设计提供了灵活性:你可以通过post(Runnable)快速执行一段代码;也可以通过给Handler设置一个公共的Callback来集中处理或拦截某些消息;当然,最传统的还是子类化并重写handleMessage

内存泄漏的根源:非静态内部类(包括匿名内部类)会隐式持有外部类实例的引用。如果这个外部类是Activity,而Handler又可能被延迟消息持有(消息在MessageQueue中排队),那么就会导致Activity无法在销毁时被GC回收。务必使用静态内部类+弱引用(WeakReference)来持有Activity上下文,并在onDestroy中调用handler.removeCallbacksAndMessages(null)

2.5 ThreadLocal:线程隔离的秘密武器

ThreadLocal是整个机制能实现“线程关联”的基石。它并不是用来解决多线程共享变量的问题,恰恰相反,它是为每个线程提供独立的变量副本,实现了线程间的数据隔离。

在Looper中的应用:Looper类中有一个静态的ThreadLocal<Looper>对象:sThreadLocal

  • Looper.prepare(): 会创建一个Looper对象,并将其存入sThreadLocal.set(new Looper())。这个set操作,是将Looper实例与当前执行prepare()的线程绑定。
  • Looper.myLooper(): 内部就是sThreadLocal.get(),它会返回与当前线程绑定的那个唯一的Looper实例。
  • Looper.getMainLooper(): 这是一个特例,它返回的是主线程的Looper,这个引用在prepareMainLooper()时被保存在一个静态变量中,全局可访问。

原理浅析:ThreadLocal内部有一个ThreadLocalMap,这个Map是Thread类的一个字段(threadLocals)。你可以把它想象成每个线程自带的一个“小抽屉”(Map)。当调用threadLocal.set(value)时,实际上是以当前ThreadLocal实例为Key,以value为Value,存入当前线程的“小抽屉”里。get()时也是从当前线程的“小抽屉”里取。因此,不同线程访问同一个ThreadLocal对象,拿到的是各自线程存储的值,互不干扰。

正是通过ThreadLocalHandler机制完美实现了消息队列的线程隔离:每个有Looper的线程都有自己的消息队列,Handler在哪个线程创建,就默认关联哪个线程的Looper,从而实现了“在A线程发送消息,在B线程处理消息”的经典跨线程通信模型。

3. Handler机制完整工作流程与源码追踪

现在我们把所有零件组装起来,看一条消息从发送到处理的完整旅程。我们以在子线程中通过Handler发送消息到主线程更新UI这个最典型的场景为例。

3.1 场景设定与初始化

假设我们在主线程创建了一个Handler,并在子线程中用它发送消息。

// 在主线程中 private Handler mHandler = new Handler(Looper.getMainLooper()) { @Override public void handleMessage(@NonNull Message msg) { // 这里运行在主线程,可以安全更新UI if (msg.what == MSG_UPDATE_TEXT) { mTextView.setText((String) msg.obj); } } }; // 在子线程中 new Thread(new Runnable() { @Override public void run() { // 模拟耗时操作 String result = doNetworkRequest(); Message msg = mHandler.obtainMessage(MSG_UPDATE_TEXT, result); mHandler.sendMessage(msg); // 发送消息到主线程 } }).start();

初始化阶段:

  1. 应用启动,主线程(ActivityThread)的main方法执行,调用Looper.prepareMainLooper()创建主线程的LooperMessageQueue,并存入静态变量和当前线程的ThreadLocal
  2. 调用Looper.loop(),主线程进入无限消息循环。
  3. 我们在主线程创建mHandler,构造方法中通过Looper.getMainLooper()获取到主线程的Looper,并关联其MessageQueue

3.2 消息发送流程详解

当子线程调用mHandler.sendMessage(msg)时,发生以下调用链:

  1. Handler.sendMessage(Message msg)->Handler.sendMessageDelayed(msg, 0)->Handler.sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis)

  2. sendMessageAtTime方法核心代码:

    public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) { MessageQueue queue = mQueue; // 这里就是主线程的MessageQueue if (queue == null) { // ... 异常处理 } return enqueueMessage(queue, msg, uptimeMillis); }
  3. Handler.enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis):

    private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg, long uptimeMillis) { msg.target = this; // 关键!将消息的target指向当前Handler if (mAsynchronous) { msg.setAsynchronous(true); } return queue.enqueueMessage(msg, uptimeMillis); // 调用MessageQueue的入队方法 }

    这里的关键是msg.target = this,它建立了消息与处理者之间的链接。

  4. MessageQueue.enqueueMessage(Message msg, long when):

    • 这是一个synchronized方法,保证入队操作的线程安全。
    • 设置msg.when = when
    • 根据when时间,将消息插入到链表合适的位置(链表按when从小到大排序)。
    • 如果新消息被插到了队列头部(即它是下一个要执行的消息),或者当前队列是空的,它会调用nativeWake()唤醒可能正在next()方法中休眠的Looper线程(这里是主线程)。

至此,消息已经从子线程安全地进入了主线程的消息队列。这个过程是线程安全的,因为MessageQueue.enqueueMessage是同步方法。

3.3 消息循环与处理流程详解

主线程的Looper.loop()一直在运行,它调用MessageQueue.next()取消息:

  1. MessageQueue.next():

    • 如果队列为空,调用nativePollOnce(ptr, nextPollTimeoutMillis)进入休眠,nextPollTimeoutMillis为-1表示无限等待直到被唤醒。
    • 当子线程入队消息并nativeWake()后,主线程被唤醒。
    • next()方法从链表头部取出消息(此时msg.when应该已经<=当前时间),将其从链表中移除,然后返回该消息。
  2. Looper.loop()拿到返回的msg,执行核心分发逻辑:

    public static void loop() { // ... 获取当前线程Looper和MessageQueue for (;;) { Message msg = queue.next(); // 可能会阻塞 if (msg == null) { // 没有消息,Looper退出 return; } // 关键分发行! try { msg.target.dispatchMessage(msg); } finally { // ... } // 回收消息到对象池 msg.recycleUnchecked(); } }
  3. Handler.dispatchMessage(Message msg):

    public void dispatchMessage(@NonNull Message msg) { if (msg.callback != null) { // 情况1: post(Runnable)发送的消息 handleCallback(msg); } else { if (mCallback != null) { // 情况2: 设置了Callback接口,并让其处理 if (mCallback.handleMessage(msg)) { return; } } // 情况3: 交给子类实现的handleMessage方法 handleMessage(msg); } }

    在我们的例子中,消息是通过sendMessage发送的,msg.callbacknull。我们没有设置mCallback,所以最终会调用到我们重写的handleMessage(Message msg)方法。此时,代码执行上下文已经切换到了主线程,因此可以安全地操作UI。

  4. 我们的handleMessage方法执行,更新TextView的文本。

  5. dispatchMessage执行完毕,返回到loop(),调用msg.recycleUnchecked()将消息所有字段清空并放回全局消息池,供后续obtain()复用。

  6. loop()开始下一次循环,继续调用queue.next()等待下一条消息。

整个流程的精妙之处在于:通过MessageQueue这个线程安全的队列作为中转站,配合Looper的循环和ThreadLocal的线程绑定,完美地将消息的生产(发送)消费(处理)解耦,并实现了跨线程通信。子线程只负责生产和投递“任务指令”(消息),主线程负责按顺序执行这些指令,从而保证了UI操作的线程安全性。

4. 高级特性、应用场景与性能调优

理解了基础原理,我们来看看一些高级特性和实际应用中如何用好Handler。

4.1 IdleHandler:利用空闲时间

IdleHandlerMessageQueue的一个接口,允许你在消息队列空闲(没有立即需要处理的消息)时执行一些低优先级的任务。

Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() { @Override public boolean queueIdle() { // 在主线程空闲时执行 doSomeLowPriorityWork(); return true; // 返回true表示下次空闲继续执行,false表示执行一次后移除 } });

应用场景:延迟初始化、批量数据预处理、垃圾回收后的资源整理等。注意:不要在queueIdle()中执行耗时操作,否则会阻塞后续消息的处理。

4.2 同步屏障与异步消息的实战

前面提到过同步屏障。在View绘制中,Choreographer会post一个异步消息(MSG_DO_FRAME)来触发下一帧的绘制。为了确保绘制消息不被业务逻辑阻塞,在ViewRootImpl中会设置同步屏障。

模拟使用(API隐藏,需反射,慎用):

// 插入同步屏障 Method method = MessageQueue.class.getDeclaredMethod(“postSyncBarrier”); int token = (int) method.invoke(Looper.getMainLooper().getQueue()); // 发送异步消息 Message asyncMsg = mHandler.obtainMessage(MSG_HIGH_PRIORITY); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) { asyncMsg.setAsynchronous(true); } mHandler.sendMessageAtFrontOfQueue(asyncMsg); // 尽量插队 // 移除同步屏障 Method removeMethod = MessageQueue.class.getDeclaredMethod(“removeSyncBarrier”, int.class); removeMethod.invoke(Looper.getMainLooper().getQueue(), token);

实际开发中,我们很少直接操作同步屏障,但理解它有助于我们明白为什么UI绘制拥有高优先级,以及为什么Handler的延迟消息时间不绝对精确(因为可能有异步消息插队)。

4.3 HandlerThread与IntentService

系统为我们封装了两个基于Handler机制的实用类:

  • HandlerThread:一个自带Looper的线程。它继承了Thread,在run()方法中调用了Looper.prepare()Looper.loop()。我们可以通过getLooper()获取它的Looper来创建关联到此线程的Handler,从而轻松实现一个具有消息处理能力的后台线程。
    HandlerThread workerThread = new HandlerThread(“MyWorker”); workerThread.start(); Handler workerHandler = new Handler(workerThread.getLooper()); workerHandler.post(() -> { /* 在workerThread执行 */ });
  • IntentService(已废弃,但思想经典):一个用于处理异步请求的Service。它在onCreate()中创建了一个HandlerThread和一个关联的Handler。当通过startService()启动时,Intent被封装成消息发送到该Handler,在后台线程依次执行onHandleIntent()。它天然提供了后台执行和任务队列的功能。

4.4 性能调优与最佳实践

  1. 使用obtainMessage:如前所述,复用Message对象,减少GC。
  2. 精确使用延迟消息sendMessageDelayedpostDelayed。对于周期性任务,考虑使用postDelayed配合递归调用,或者更好的选择是ScheduledExecutorService
  3. 及时清理消息:在Activity/Fragment的onDestroy中,调用handler.removeCallbacksAndMessages(null)移除所有未处理的消息,这是避免内存泄漏的标准动作
  4. 避免在Handler中处理耗时操作:Handler的handleMessage运行在它关联的Looper线程上。如果在主线程Handler中处理耗时操作,会直接导致UI卡顿甚至ANR。务必确保在主线程Handler中只做轻量级工作,尤其是UI更新。
  5. 谨慎使用sendMessageAtFrontOfQueue:这个方法会将消息插入队列头部,可能打乱消息顺序,影响其他消息的时序,非必要不使用。
  6. 考虑替代方案:对于复杂的后台任务调度、生命周期感知的异步操作,现代Android开发更推荐使用Kotlin协程+ViewModel+LiveData,或者RxJava。它们提供了更声明式、更易于测试和生命周期管理的异步处理方式。但Handler作为底层基石,其原理依然至关重要。

5. 典型问题排查与实战“踩坑”记录

理论最终要服务于实践。下面是我在多年开发中遇到的几个典型Handler相关问题及解决思路。

5.1 ANR(Application Not Responding)

现象:应用无响应,系统弹出ANR对话框。与Handler相关的可能原因

  1. 主线程Handler处理消息耗时过长:这是最常见的原因。在handleMessagepost(Runnable)中执行了网络请求、大量文件IO、复杂计算等。
  2. 同步屏障阻塞:虽然少见,但如果错误地设置了同步屏障未移除,且没有异步消息,会导致所有同步消息被永久阻塞。排查
    • 查看ANR日志(/data/anr/traces.txt),找到主线程(通常叫“main”)的堆栈。堆栈顶部的代码就是导致阻塞的元凶。
    • 检查所有在主线程中执行的代码,特别是Handler消息处理、ViewonDrawActivity的生命周期回调等。解决:将耗时操作移到子线程(如使用AsyncTaskThreadPoolExecutor、协程等),主线程仅负责调度和更新UI。

5.2 内存泄漏

现象:Activity销毁后,仍然被持有,无法被GC回收,反复操作后可能导致OOM。根源:如前所述,非静态内部类Handler隐式持有外部Activity引用 + 延迟消息未移除。排查:使用Android Profiler或LeakCanary等工具检测。解决

  1. 标准写法:使用静态内部类 + 弱引用。
    private static class SafeHandler extends Handler { private final WeakReference<MyActivity> mActivityRef; SafeHandler(MyActivity activity) { mActivityRef = new WeakReference<>(activity); } @Override public void handleMessage(@NonNull Message msg) { MyActivity activity = mActivityRef.get(); if (activity != null && !activity.isFinishing()) { // 处理消息,使用activity前务必判空 } } }
  2. 生命周期管理:在Activity的onDestroy中移除回调。
    @Override protected void onDestroy() { super.onDestroy(); mHandler.removeCallbacksAndMessages(null); // 移除所有 // 或者针对性地移除 mHandler.removeMessages(WHAT_CODE); }

5.3 消息延迟不准确

现象:使用postDelayed(runnable, 1000),但任务并不是在精确的1秒后执行。原因

  1. 消息队列排队:Handler消息是按顺序处理的。如果前面有耗时消息,后面的延迟消息就必须等待。
  2. 系统休眠:设备进入休眠时,SystemClock.uptimeMillis()会暂停,唤醒后才继续。而postDelayed基于uptimeMillis,所以实际延迟会变长。如果需要精确的实时时间间隔,应使用基于System.currentTimeMillis()sendMessageAtTime,但要注意时钟可能被用户修改。
  3. 异步消息插队:同步屏障下的异步消息会优先执行。应对:Handler的延迟消息适用于对时间精度要求不高的场景(如UI动画、简单的轮询)。对于需要精确计时或调度的任务,应使用ScheduledExecutorServiceAlarmManager(用于跨进程/唤醒休眠)。

5.4 “Can‘t create handler inside thread...”异常

现象:在子线程中直接new Handler()抛出此异常。原因:当前线程没有调用Looper.prepare()初始化Looper。解决

  1. 如果就想在这个子线程处理消息:先调用Looper.prepare(),再new Handler(),最后调用Looper.loop()启动循环。记得在合适的时候调用Looper.quit()退出循环。
  2. 如果想把消息发到主线程处理:使用主线程的Looper创建Handler:new Handler(Looper.getMainLooper())
  3. 使用HandlerThread

5.5 主线程Looper.loop()为什么不会导致ANR?

这是一个经典的面试题。我们已经从原理上解释了:因为loop()本身不执行耗时操作,它的核心queue.next()在没有消息时会释放CPU进入休眠。ANR的触发是因为在单个消息的处理过程中(或者连续多个消息的处理累积)占用了主线程太长时间(通常前台5秒,后台10秒),导致系统监控超时。loop()循环本身不是ANR的原因,在循环里执行的某一个handleMessage方法太慢才是。系统输入事件(如按键、触摸)也是通过发送消息到主线程队列来处理的,如果主线程被一个耗时消息阻塞,无法及时处理输入事件,就会触发ANR。

Handler机制是Android异步编程的基石,从底层系统事件分发到上层应用逻辑调度,无处不在。吃透它,不仅能让你在面试中游刃有余,更能让你在开发中写出更健壮、高效的代码。理解其核心——线程隔离的消息队列与循环,你会发现很多其他框架(如EventBus、RxJava的Scheduler)的设计思想都与之有异曲同工之妙。希望这篇长文能帮你把这块知识真正串联起来,形成稳固的知识体系。如果在实践中遇到其他古怪的Handler相关问题,不妨再从这几个核心组件的关系入手分析,多半都能找到答案。

返回列表