Android Handler 机制面试总结

发布于:2025-05-10 ⋅ 阅读:(14) ⋅ 点赞:(0)

一、Handler 机制核心原理

1. ThreadLocal 与线程绑定

真题

  • 为什么每个线程只能有一个 Looper?
  • ThreadLocal 是如何实现线程隔离的?

源码注释

public final class Looper {
    // 静态 ThreadLocal 存储每个线程的 Looper 实例
    static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<>();
    
    // 主线程 Looper(由 ActivityThread 自动创建)
    private static Looper sMainLooper; 

    // 初始化当前线程的 Looper
    public static void prepare() {
        prepare(false); // 主线程 quitAllowed 为 false
    }

    private static void prepare(boolean quitAllowed) {
        // 检查当前线程是否已存在 Looper
        if (sThreadLocal.get() != null) {
            throw new RuntimeException("Only one Looper may be created per thread");
        }
        // 将新创建的 Looper 存入当前线程的 ThreadLocal
        sThreadLocal.set(new Looper(quitAllowed));
    }

    // 获取当前线程的 Looper
    public static @Nullable Looper myLooper() {
        return sThreadLocal.get();
    }
}

面试回答

ThreadLocal 通过为每个使用该变量的线程都提供一个独立的变量副本,每个线程都可以独立地改变自己的副本,而不会影响其他线程所对应的副本。在 Looper 中,sThreadLocal.get() 会返回当前线程存储的 Looper 实例,从而实现线程隔离。若重复调用 prepare(),会因检测到已有 Looper 而抛出异常,保证每个线程仅有一个 Looper。

2. 消息池机制

真题

  • Message 为什么要复用?如何实现复用的?
  • 消息池的最大容量是多少?

源码注释

public final class Message implements Parcelable {
    // 消息池的头节点(静态变量,所有线程共享)
    private static Message sPool;
    // 消息池当前大小
    private static int sPoolSize = 0;
    // 消息池最大容量
    private static final int MAX_POOL_SIZE = 50;

    // 从消息池获取消息(优先复用)
    public static Message obtain() {
        synchronized (sPoolSync) {
            if (sPool != null) {
                Message m = sPool;
                sPool = m.next;  // 取出头节点
                m.next = null;   // 断开引用
                m.flags = 0;     // 重置标志位
                sPoolSize--;     // 池大小减1
                return m;
            }
        }
        return new Message();  // 池为空时创建新实例
    }

    // 回收消息到池中
    void recycleUnchecked() {
        if (isInUse()) {  // 确保消息未被使用
            throw new IllegalStateException("This message cannot be recycled because it is still in use.");
        }
        // 重置消息状态
        flags = FLAG_IN_USE;
        what = 0;
        arg1 = 0;
        arg2 = 0;
        obj = null;
        replyTo = null;
        sendingUid = -1;
        when = 0;
        target = null;
        callback = null;
        data = null;

        synchronized (sPoolSync) {
            if (sPoolSize < MAX_POOL_SIZE) {
                next = sPool;  // 插入链表头部
                sPool = this;
                sPoolSize++;   // 池大小加1
            }
        }
    }
}

面试回答

Message 复用主要是为了减少对象创建和垃圾回收的开销,提升性能。通过静态链表实现消息池,最大容量为 50。Message.obtain() 优先从池中获取复用,recycleUnchecked() 将消息回收至池中。这种设计在高频发送消息的场景(如动画、网络回调)中尤为重要,可显著降低内存抖动。

二、消息发送与处理机制

1. 消息入队逻辑

真题

  • MessageQueue 是如何保证线程安全的?
  • 为什么 MessageQueue 是单链表而非队列?

源码注释

boolean enqueueMessage(Message msg, long when) {
    // 消息必须有 target(即发送该消息的 Handler)
    msg.target = this;
    msg.workSourceUid = ThreadLocalWorkSource.getUid();

    synchronized (this) {  // 同步锁保证线程安全
        if (mQuitting) {  // 队列已退出,回收消息
            IllegalStateException e = new IllegalStateException(
                    msg.target + " sending message to a Handler on a dead thread");
            Log.w(TAG, e.getMessage(), e);
            msg.recycle();
            return false;
        }

        msg.markInUse();
        msg.when = when;  // 设置消息执行时间
        Message p = mMessages;  // 当前头节点
        boolean needWake;

        // 按 when 排序插入(新消息时间更早,插入头部)
        if (p == null || when == 0 || when < p.when) {
            msg.next = p;
            mMessages = msg;
            needWake = mBlocked;  // 若队列阻塞,需唤醒
        } else {
            // 遍历链表找到插入位置(按 when 升序)
            Message prev;
            for (;;) {
                prev = p;
                p = p.next;
                if (p == null || when < p.when) break;
            }
            msg.next = p;
            prev.next = msg;
            needWake = mBlocked && p.target == null && msg.isAsynchronous();
        }

        // 若队列阻塞且需要唤醒,通过 native 方法唤醒
        if (needWake) nativeWake(mPtr);
    }
    return true;
}

面试回答

  1. 线程安全:通过 synchronized (this) 保证同一时间只有一个线程能修改消息队列,确保多线程发送消息时不会冲突。
  2. 单链表优势:单链表可按 when 灵活插入,支持延迟消息和异步消息插队,比传统 FIFO 队列更适合消息调度场景。
2. 消息分发优先级

真题

  • Handler 处理消息的优先级顺序是怎样的?
  • 如何让一个消息优先被处理?

源码注释

public void dispatchMessage(Message msg) {
    // 优先级1:Message 自带的 callback(通过 post(Runnable) 设置)
    if (msg.callback != null) {
        handleCallback(msg);
    } 
    // 优先级2:Handler 的 Callback(构造函数传入)
    else if (mCallback != null) {
        if (mCallback.handleMessage(msg)) return;  // 若 Callback 处理成功,不再继续
    }
    // 优先级3:用户重写的 handleMessage 方法
    handleMessage(msg);
}

private static void handleCallback(Message message) {
    message.callback.run();  // 执行 Runnable
}

面试回答

消息处理优先级为:Message.callback > Handler.Callback > Handler.handleMessage。若需让消息优先处理,可:

 
  1. 使用 post(Runnable) 让消息自带 callback;
  2. 设置 Handler 的 Callback 并返回 true 拦截消息;
  3. 将消息设为异步(setAsynchronous(true)),可在同步屏障中优先执行。

三、阻塞唤醒机制与性能优化

1. 消息循环与阻塞原理

真题

  • Looper.loop () 为什么不会阻塞主线程?
  • MessageQueue.next () 是如何实现阻塞的?

源码注释

public static void loop() {
    final Looper me = myLooper();  // 获取当前线程的 Looper
    if (me == null) {
        throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
    }
    final MessageQueue queue = me.mQueue;

    for (;;) {  // 无限循环
        Message msg = queue.next();  // 可能阻塞
        if (msg == null) return;  // 队列退出

        // 分发消息到对应的 Handler
        msg.target.dispatchMessage(msg);

        // 回收消息到池
        msg.recycleUnchecked();
    }
}

Message next() {
    final long ptr = mPtr;  // 指向 native 层的 MessageQueue
    int pendingIdleHandlerCount = -1;
    int nextPollTimeoutMillis = 0;

    for (;;) {
        // 关键阻塞方法:通过 native 层实现休眠
        nativePollOnce(ptr, nextPollTimeoutMillis);

        synchronized (this) {
            final long now = SystemClock.uptimeMillis();
            Message prevMsg = null;
            Message msg = mMessages;

            // 处理屏障消息(同步屏障)
            if (msg != null && msg.target == null) {
                // 找到下一个异步消息
                do {
                    prevMsg = msg;
                    msg = msg.next;
                } while (msg != null && !msg.isAsynchronous());
            }

            if (msg != null) {
                if (now < msg.when) {
                    // 消息未到执行时间,计算等待时间
                    nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                } else {
                    // 取出消息
                    mBlocked = false;  // 标记为非阻塞状态
                    if (prevMsg != null) prevMsg.next = msg.next;
                    else mMessages = msg.next;
                    msg.next = null;
                    return msg;
                }
            } else {
                // 无消息,永久阻塞
                nextPollTimeoutMillis = -1;
            }

            // 检查队列是否退出
            if (mQuitting) {
                dispose();
                return null;
            }
        }
    }
}

面试回答

Looper.loop () 不会导致 ANR 的核心原因是:

 
  1. 当队列无消息时,nativePollOnce 通过 epoll 机制进入内核休眠,释放 CPU 资源;
  2. 消息入队时通过 nativeWake 写入管道数据,唤醒休眠线程;
  3. ANR 的触发条件是主线程在规定时间内未处理完特定类型的消息(如输入事件),而非 Looper 循环本身。
2. 同步屏障机制

真题

  • 什么是同步屏障?有什么应用场景?
  • 如何实现一个 UI 刷新的高优先级消息?

源码注释

// MessageQueue 中的同步屏障插入方法
public int postSyncBarrier() {
    return postSyncBarrier(SystemClock.uptimeMillis());
}

private int postSyncBarrier(long when) {
    synchronized (this) {
        final int token = mNextBarrierToken++;
        final Message msg = Message.obtain();
        msg.markInUse();
        msg.when = when;
        msg.arg1 = token;  // 使用 arg1 存储 token

        // 同步屏障消息的 target 为 null
        Message prev = null;
        Message p = mMessages;
        if (when != 0) {
            // 按时间排序插入
            while (p != null && p.when <= when) {
                prev = p;
                p = p.next;
            }
        }
        if (prev != null) {
            msg.next = p;
            prev.next = msg;
        } else {
            msg.next = p;
            mMessages = msg;
        }
        return token;
    }
}

// next() 方法中处理同步屏障的逻辑
if (msg != null && msg.target == null) {
    // 找到下一个异步消息
    do {
        prevMsg = msg;
        msg = msg.next;
    } while (msg != null && !msg.isAsynchronous());
}

面试回答

同步屏障是一种特殊消息(target 为 null),插入后会阻塞所有同步消息,仅允许异步消息通过。应用场景包括:

 
  1. UI 刷新:Choreographer 通过同步屏障确保 Vsync 信号触发的绘制任务优先执行;
  2. 优先级调度:通过 Message.setAsynchronous(true) 将关键消息设为异步,避免被普通消息阻塞。

四、内存泄漏与线程通信

1. Handler 内存泄漏

真题

  • 为什么非静态内部类 Handler 会导致内存泄漏?
  • 如何正确在 Activity 中使用 Handler?

错误代码示例

public class MainActivity extends AppCompatActivity {
    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            // 更新 UI
            textView.setText("Update");
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        // 发送延迟消息
        mHandler.postDelayed(() -> {
            // 模拟耗时操作
        }, 10000);
    }
}

正确实现方案

public class MainActivity extends AppCompatActivity {
    private TextView textView;
    // 使用静态内部类 + 弱引用
    private static class MyHandler extends Handler {
        private final WeakReference<MainActivity> activityWeakReference;
        
        public MyHandler(MainActivity activity) {
            activityWeakReference = new WeakReference<>(activity);
        }
        
        @Override
        public void handleMessage(Message msg) {
            MainActivity activity = activityWeakReference.get();
            if (activity != null) {
                activity.textView.setText("Update");
            }
        }
    }
    
    private final MyHandler mHandler = new MyHandler(this);

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        textView = findViewById(R.id.textView);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        // 移除所有未处理的消息和回调
        mHandler.removeCallbacksAndMessages(null);
    }
}

面试回答

非静态内部类会隐式持有外部类的引用。若 Handler 中有延迟消息未处理,而 Activity 已销毁,Handler 会继续持有 Activity 引用,导致 GC 无法回收 Activity。正确做法是:

 
  1. 使用静态内部类 + 弱引用;
  2. 在 Activity 的 onDestroy 中移除所有消息(removeCallbacksAndMessages(null))。
2. 线程间通信

真题

  • 如何在子线程中更新 UI?
  • 除了 Handler,还有哪些线程通信方式?

多场景解决方案

// 方式1:主线程创建 Handler,子线程发送消息
public class MainActivity extends AppCompatActivity {
    private Handler mHandler = new Handler(Looper.getMainLooper()) {
        @Override
        public void handleMessage(Message msg) {
            // 更新 UI
        }
    };

    public void startBackgroundTask() {
        new Thread(() -> {
            // 子线程执行耗时操作
            mHandler.sendEmptyMessage(1);  // 发送消息到主线程
        }).start();
    }
}

// 方式2:Activity.runOnUiThread
new Thread(() -> {
    // 耗时操作
    runOnUiThread(() -> {
        // 更新 UI
    });
}).start();

// 方式3:View.post
view.post(() -> {
    // 更新 view
});

// 方式4:AsyncTask(已弃用,但原理类似)
private class MyTask extends AsyncTask<Void, Void, String> {
    @Override
    protected String doInBackground(Void... voids) {
        return "result";
    }

    @Override
    protected void onPostExecute(String result) {
        // 更新 UI
    }
}

面试回答

线程间通信方式包括:

 
  1. Handler:最基础方式,通过消息队列实现线程安全通信;
  2. Activity.runOnUiThread:内部封装了 Handler,简化调用;
  3. View.post:View 持有主线程 Handler,可直接切换到主线程;
  4. AsyncTask:封装了线程池和 Handler,适合短期异步任务;
  5. LiveData:基于观察者模式,自动在主线程回调;
  6. RxJava:通过 Scheduler 灵活切换线程。

五、进阶问题与扩展考点

1. Handler 与 Choreographer 的关系

真题

  • Choreographer 是如何利用 Handler 机制的?
  • 为什么 UI 刷新要通过 Choreographer 而非直接 Handler?

解析

  • Choreographer 原理
    1. 向 VSYNC 信号注册回调;
    2. VSYNC 到来时,通过 Handler 发送异步消息(带有同步屏障);
    3. 优先执行绘制任务,确保在 16ms 内完成一帧渲染。

源码关键

// Choreographer 内部使用 Handler 发送异步消息
private final class FrameHandler extends Handler {
    public FrameHandler(Looper looper) {
        super(looper);
    }

    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {
            case MSG_DO_FRAME:
                doFrame(System.nanoTime(), 0);
                break;
            case MSG_DO_SCHEDULE_VSYNC:
                doScheduleVsync();
                break;
            case MSG_DO_SCHEDULE_CALLBACK:
                doScheduleCallback(msg.arg1);
                break;
        }
    }
}

// 发送异步消息
private void sendFrameCallback(int callbackType, long dueTimeNanos) {
    Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, callbackType, 0);
    msg.setAsynchronous(true);  // 设置为异步消息
    mHandler.sendMessageAtTime(msg, dueTimeNanos / TimeUtils.NANOS_PER_MS);
}

面试回答

Choreographer 通过 Handler 发送异步消息,并配合同步屏障确保绘制任务优先执行。相比直接使用 Handler,Choreographer 的优势在于:

 
  1. 与屏幕刷新率(VSYNC)同步,避免丢帧;
  2. 通过同步屏障提升绘制优先级,减少卡顿;
  3. 统一管理动画、输入和绘制的时间点,优化性能。
2. HandlerThread 的应用场景

真题

  • HandlerThread 与普通 Thread 有何区别?
  • 何时应该使用 HandlerThread?

源码关键

public class HandlerThread extends Thread {
    int mPriority;
    int mTid = -1;
    Looper mLooper;
    private @Nullable Handler mHandler;

    public HandlerThread(String name) {
        super(name);
        mPriority = Process.THREAD_PRIORITY_DEFAULT;
    }

    @Override
    protected void onLooperPrepared() {
        // Looper 准备好后调用
    }

    @Override
    public void run() {
        mTid = Process.myTid();
        Looper.prepare();  // 创建 Looper
        synchronized (this) {
            mLooper = Looper.myLooper();
            notifyAll();  // 通知 Looper 已创建
        }
        Process.setThreadPriority(mPriority);
        onLooperPrepared();
        Looper.loop();  // 启动消息循环
        mTid = -1;
    }

    // 获取 Looper
    public Looper getLooper() {
        if (!isAlive()) return null;
        synchronized (this) {
            while (isAlive() && mLooper == null) {
                try {
                    wait();  // 等待 Looper 创建完成
                } catch (InterruptedException e) {}
            }
        }
        return mLooper;
    }

    // 终止消息循环
    public boolean quit() {
        Looper looper = getLooper();
        if (looper != null) {
            looper.quit();
            return true;
        }
        return false;
    }
}

面试回答

HandlerThread 是一个自带 Looper 的线程,相比普通 Thread:

 
  1. 内置 Looper 和 MessageQueue,可直接创建 Handler;
  2. 提供 getLooper() 方法获取线程的 Looper;
  3. 适合需要长期运行并处理消息的场景(如文件操作、网络请求)。
 

应用场景

 
  • 后台任务需要串行执行(如数据库操作);
  • 定时任务(配合 sendMessageDelayed);
  • 不需要高并发的异步处理。

网站公告

今日签到

点亮在社区的每一天
去签到