同步屏障与异步消息

3 阅读15分钟

同步屏障与异步消息

面试重要度:⭐⭐⭐⭐⭐

考察频率:字节 85% | 阿里 70% | 腾讯 75%

一、核心概念(10-15%篇幅)

1.1 定义与作用

一句话定义: 同步屏障(Sync Barrier)是一种特殊的 Message,它没有 target(Handler 为 null),用于拦截同步消息,使异步消息优先执行。

为什么重要

  • Android UI 渲染依赖同步屏障保证 Vsync 信号的及时响应
  • 是理解 Choreographer 和 View 绘制流程的关键前置知识
  • 字节面试高频考点,区分中高级工程师的重要知识点
  • 理解 Handler 消息优先级机制的核心

核心特征

  • 同步屏障本身是一个 target == null 的特殊 Message
  • 只能通过 postSyncBarrier() 方法添加(hide API)
  • 添加后,同步消息被阻塞,异步消息可以继续执行
  • 必须通过 removeSyncBarrier() 移除,否则同步消息永远无法执行

1.2 消息类型对比

消息类型特征创建方式使用场景
同步消息isAsynchronous() = false默认创建的消息普通业务逻辑
异步消息isAsynchronous() = truesetAsynchronous(true)需要优先处理的任务
同步屏障target == nullpostSyncBarrier()系统级优先级控制

1.3 与其他概念的关系

同步屏障与 MessageQueue 的消息出队机制紧密相关(详见:../03-MessageQueue队列管理/02-消息出队next().md),在 next() 方法中通过判断 target == null 来识别同步屏障,从而实现消息优先级控制。


二、核心原理(50-60%篇幅)

2.1 工作机制

整体流程

postSyncBarrier() → 屏障插入队列 → next()检测到屏障 → 跳过同步消息 → 只取异步消息 → removeSyncBarrier()

关键步骤详解

  1. 添加同步屏障:调用 postSyncBarrier() 创建 target=null 的特殊 Message 并插入队列
  2. 消息循环检测next() 方法检测到队首是屏障消息(target == null
  3. 过滤同步消息:遍历链表跳过所有同步消息,只寻找异步消息
  4. 异步消息执行:取出异步消息返回给 Looper 分发执行
  5. 移除屏障:业务完成后调用 removeSyncBarrier() 恢复正常消息处理

状态图

正常状态:                    屏障状态:
┌─────────────────────┐      ┌─────────────────────┐
│ 同步1 → 同步2 → 异步1 │      │ 屏障 → 同步1 → 异步1 │
│   ↑                 │      │         ×      ↑    │
│  取出               │      │        阻塞   取出  │
└─────────────────────┘      └─────────────────────┘

2.2 源码分析

2.2.1 添加同步屏障 - postSyncBarrier()
// Android 11 源码:frameworks/base/core/java/android/os/MessageQueue.java

/**
 * @hide  // 注意:这是隐藏API,应用层无法直接调用
 */
public int postSyncBarrier() {
    return postSyncBarrier(SystemClock.uptimeMillis());
}

private int postSyncBarrier(long when) {
    synchronized (this) {
        // 步骤1:生成唯一的屏障令牌(用于后续移除)
        final int token = mNextBarrierToken++;

        // 步骤2:从对象池获取 Message(复用机制)
        final Message msg = Message.obtain();
        msg.markInUse();

        // 步骤3:关键!设置 when 但不设置 target
        // target 为 null 是同步屏障的唯一标识
        msg.when = when;
        msg.arg1 = token;  // 保存令牌,用于移除时匹配
        // 注意:这里没有 msg.target = xxx

        // 步骤4:按时间顺序插入队列(与普通消息相同逻辑)
        Message prev = null;
        Message p = mMessages;
        if (when != 0) {
            while (p != null && p.when <= when) {
                prev = p;
                p = p.next;
            }
        }

        // 步骤5:链表插入
        if (prev != null) {
            msg.next = p;
            prev.next = msg;
        } else {
            msg.next = p;
            mMessages = msg;  // 插入队首
        }

        return token;  // 返回令牌,调用方需保存用于移除
    }
}

源码解读

  • 设计意图:通过 target == null 这个特殊标记,在不改变 Message 数据结构的情况下实现屏障功能
  • token 作用:每个屏障有唯一标识,支持精确移除指定屏障
  • hide 注解:系统级 API,防止应用滥用导致消息阻塞
2.2.2 消息出队时处理同步屏障 - next()
// Android 11 源码:frameworks/base/core/java/android/os/MessageQueue.java

Message next() {
    // ... 省略 epoll 等待逻辑

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

            // 关键!检测同步屏障
            if (msg != null && msg.target == null) {
                // 步骤1:发现同步屏障,需要找异步消息
                // 遍历链表,跳过所有同步消息
                do {
                    prevMsg = msg;
                    msg = msg.next;
                } while (msg != null && !msg.isAsynchronous());
                // 循环结束:msg 为 null 或 msg 是异步消息
            }

            if (msg != null) {
                if (now < msg.when) {
                    // 消息未到执行时间,计算等待时长
                    nextPollTimeoutMillis = (int) Math.min(
                        msg.when - now, Integer.MAX_VALUE);
                } else {
                    // 步骤2:取出消息(可能是异步消息)
                    mBlocked = false;
                    if (prevMsg != null) {
                        // 从链表中间取出(异步消息场景)
                        prevMsg.next = msg.next;
                    } else {
                        // 从队首取出(正常场景)
                        mMessages = msg.next;
                    }
                    msg.next = null;
                    msg.markInUse();
                    return msg;
                }
            } else {
                // 没有消息(或只有同步消息被屏障阻塞)
                nextPollTimeoutMillis = -1;  // 无限等待
            }

            // ... IdleHandler 处理逻辑
        }
    }
}

源码解读

  • 屏障检测条件msg.target == null 是唯一判断依据
  • 遍历策略:使用 do-while 跳过同步消息,直到找到异步消息或链表末尾
  • prevMsg 作用:记录前驱节点,用于从链表中间取出异步消息
  • 阻塞行为:如果屏障后没有异步消息,nextPollTimeoutMillis = -1 导致无限等待
2.2.3 移除同步屏障 - removeSyncBarrier()
// Android 11 源码:frameworks/base/core/java/android/os/MessageQueue.java

/**
 * @hide
 */
public void removeSyncBarrier(int token) {
    synchronized (this) {
        Message prev = null;
        Message p = mMessages;

        // 步骤1:遍历链表查找指定 token 的屏障
        // 条件:target == null(是屏障) && arg1 == token(匹配令牌)
        while (p != null && (p.target != null || p.arg1 != token)) {
            prev = p;
            p = p.next;
        }

        if (p == null) {
            // 未找到指定屏障
            throw new IllegalStateException(
                "The specified message queue synchronization "
                + "barrier token has not been posted or has already been removed.");
        }

        // 步骤2:从链表中移除屏障
        final boolean needWake;
        if (prev != null) {
            prev.next = p.next;
            needWake = false;  // 屏障不在队首,无需唤醒
        } else {
            mMessages = p.next;
            // 步骤3:判断是否需要唤醒
            // 移除队首屏障后,如果有同步消息待处理,需要唤醒
            needWake = mMessages == null || mMessages.target != null;
        }

        // 步骤4:回收 Message 到对象池
        p.recycleUnchecked();

        // 步骤5:唤醒阻塞的 next() 方法
        if (needWake && !mQuitting) {
            nativeWake(mPtr);
        }
    }
}

源码解读

  • 匹配逻辑:双重条件 target == null && arg1 == token 确保精确移除
  • 异常处理:重复移除或无效 token 会抛出 IllegalStateException
  • 唤醒条件:只有移除队首屏障且后续有同步消息时才需要唤醒

2.3 异步消息的设置方式

// Android 11 源码:frameworks/base/core/java/android/os/Message.java

/**
 * 设置消息为异步消息
 * @hide  // 同样是隐藏API
 */
public void setAsynchronous(boolean async) {
    if (async) {
        flags |= FLAG_ASYNCHRONOUS;
    } else {
        flags &= ~FLAG_ASYNCHRONOUS;
    }
}

public boolean isAsynchronous() {
    return (flags & FLAG_ASYNCHRONOUS) != 0;
}

// FLAG_ASYNCHRONOUS = 1 << 1 = 2

异步 Handler 构造方式

// Android 11 源码:frameworks/base/core/java/android/os/Handler.java

/**
 * @hide
 */
public Handler(@NonNull Looper looper, @Nullable Callback callback, boolean async) {
    mLooper = looper;
    mQueue = looper.mQueue;
    mCallback = callback;
    mAsynchronous = async;  // 标记此 Handler 发送的消息都是异步消息
}

private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
        long uptimeMillis) {
    msg.target = this;
    msg.workSourceUid = ThreadLocalWorkSource.getUid();

    // 关键:如果 Handler 是异步的,自动设置消息为异步
    if (mAsynchronous) {
        msg.setAsynchronous(true);
    }
    return queue.enqueueMessage(msg, uptimeMillis);
}

2.4 系统级应用 - Choreographer

Choreographer 是同步屏障的典型使用者

// Android 11 源码:frameworks/base/core/java/android/view/Choreographer.java

private void scheduleFrameLocked(long now) {
    if (!mFrameScheduled) {
        mFrameScheduled = true;

        if (USE_VSYNC) {
            // 步骤1:添加同步屏障,保证 Vsync 回调优先执行
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();

            // 步骤2:发送异步消息,等待 Vsync 信号
            scheduleVsyncLocked();
        }
    }
}

void doFrame(long frameTimeNanos, int frame) {
    synchronized (mLock) {
        // ... 处理帧逻辑
    }

    try {
        // 执行各类回调:INPUT → ANIMATION → TRAVERSAL
        doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
        doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
        doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
        doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
    } finally {
        // 步骤3:帧处理完成,移除同步屏障
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
    }
}

流程图

requestLayout()
      ↓
scheduleTraversals()
      ↓
postSyncBarrier() ← 添加屏障,阻塞同步消息
      ↓
scheduleVsyncLocked() ← 注册 Vsync 回调(异步消息)
      ↓
等待 Vsync 信号...
      ↓
doFrame() ← Vsync 到来,优先执行
      ↓
measure → layout → draw
      ↓
removeSyncBarrier() ← 移除屏障,恢复同步消息

2.5 重要细节与边界条件

细节1:屏障不会自动移除

  • 同步屏障必须显式调用 removeSyncBarrier() 移除
  • 忘记移除会导致同步消息永远无法执行,造成应用无响应

细节2:多个屏障可以共存

  • 每个屏障有唯一 token,可以按顺序或指定 token 移除
  • 嵌套添加屏障时需要按相反顺序移除

细节3:屏障对 IdleHandler 的影响

  • 当存在同步屏障且没有异步消息时,不会执行 IdleHandler
  • 因为队列中仍有同步消息待处理,不算"空闲"

细节4:应用层无法直接使用

  • postSyncBarrier()@hide API
  • 可通过反射调用,但不推荐(破坏封装,版本兼容性差)

三、实际应用(15-20%篇幅)

3.1 典型场景

场景1:View 绘制优先级保证

  • 需求:屏幕刷新(16.6ms)必须及时响应,不能被普通消息延迟
  • 使用方式:Choreographer 在 Vsync 到来前添加屏障
  • 注意事项:屏障存在期间的同步消息会被延迟

场景2:自定义高优先级任务(需反射)

  • 需求:某些关键任务需要优先于其他消息执行

  • 使用方式:反射调用 postSyncBarrier() + 异步 Handler

  • 注意事项:

    • 必须保证移除屏障,否则同步消息永久阻塞
    • 反射方式在高版本 Android 可能受限

3.2 反射使用示例(仅供理解原理)

// 警告:仅用于学习,生产环境不推荐
public class SyncBarrierHelper {

    public static int postSyncBarrier(MessageQueue queue) {
        try {
            Method method = MessageQueue.class.getDeclaredMethod(
                "postSyncBarrier");
            method.setAccessible(true);
            return (int) method.invoke(queue);
        } catch (Exception e) {
            return -1;
        }
    }

    public static void removeSyncBarrier(MessageQueue queue, int token) {
        try {
            Method method = MessageQueue.class.getDeclaredMethod(
                "removeSyncBarrier", int.class);
            method.setAccessible(true);
            method.invoke(queue, token);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

3.3 最佳实践

推荐做法

  1. 理解原理即可,不要在应用层使用同步屏障
  2. 如需优先级控制,考虑使用独立的 HandlerThread
  3. 对于 UI 相关任务,信任系统的 Choreographer 机制

常见错误

  1. 添加屏障后忘记移除 → 导致 ANR
  2. 在子线程添加主线程的屏障 → 需要正确获取主线程 MessageQueue
  3. 滥用异步消息 → 破坏消息顺序,可能导致逻辑错误

四、面试真题解析(20-25%篇幅)

4.1 基础必答题(P5必须掌握)


【高频题1】什么是同步屏障?它的作用是什么?

标准答案(30秒) : 同步屏障是一种特殊的 Message,它的 target 为 null。当 MessageQueue 检测到同步屏障时,会跳过所有同步消息,只处理异步消息。它的作用是实现消息优先级控制,保证高优先级任务(如 UI 渲染)能够及时执行。

深入展开(追问后) : 在 MessageQueue.next() 方法中,会判断队首消息的 target 是否为 null。如果是,说明遇到了同步屏障,此时会遍历链表查找异步消息。这种机制被 Choreographer 用于保证 Vsync 信号的及时响应,确保 UI 绑定能在 16.6ms 内完成。

面试官追问

  • 追问1:同步屏障如何添加和移除?

    • 答:通过 MessageQueue.postSyncBarrier() 添加,返回一个 token;通过 removeSyncBarrier(token) 移除。这两个都是 hide API,应用层无法直接调用。
  • 追问2:如果忘记移除同步屏障会怎样?

    • 答:同步消息会被永久阻塞,导致 ANR。因为 next() 方法会一直跳过同步消息寻找异步消息,如果没有异步消息就会无限等待。

【高频题2】同步消息和异步消息有什么区别?

标准答案(30秒) : 默认创建的 Message 都是同步消息。异步消息需要通过 Message.setAsynchronous(true) 设置,或者使用异步 Handler 发送。区别在于:当存在同步屏障时,同步消息会被阻塞,而异步消息可以继续执行。

深入展开(追问后) : Message 内部通过 flags 字段的 FLAG_ASYNCHRONOUS 位来标记是否是异步消息。在 next() 方法中通过 msg.isAsynchronous() 判断。普通开发中很少直接使用异步消息,它主要被系统用于 UI 渲染等高优先级场景。

面试官追问

  • 追问1:如何创建异步 Handler?

    • 答:Handler 构造函数有一个 async 参数,设为 true 即可。但这是 hide API。在 Android 9 之后可以使用 Handler.createAsync(Looper) 方法。
  • 追问2:异步消息和同步屏障必须配合使用吗?

    • 答:是的。单独的异步消息没有优先级意义,只有在同步屏障存在时,异步消息才能"插队"执行。

【高频题3】MessageQueue.next() 如何处理同步屏障?

标准答案(30秒) : next() 方法会检查队首消息的 target 是否为 null。如果是 null,说明是同步屏障,会进入一个 do-while 循环遍历链表,跳过所有同步消息,直到找到异步消息或到达链表末尾。

深入展开(追问后) : 具体代码逻辑:首先 if (msg != null && msg.target == null) 检测屏障;然后 do { prevMsg = msg; msg = msg.next; } while (msg != null && !msg.isAsynchronous()) 遍历寻找异步消息。prevMsg 记录前驱节点,用于从链表中间取出异步消息。

面试官追问

  • 追问1:为什么用 target == null 来标识同步屏障?

    • 答:这是一种巧妙的设计。正常消息必须有 target 用于分发,而屏障本身不需要分发,所以用 null 作为特殊标记,不需要额外字段。
  • 追问2:如果屏障后面没有异步消息会怎样?

    • 答:msg 会遍历到 null,然后 nextPollTimeoutMillis 设为 -1,导致 nativePollOnce() 无限阻塞,直到有新的异步消息入队或屏障被移除。

4.2 进阶加分题(P6/P6+)


【进阶题1】Choreographer 是如何使用同步屏障的?为什么要这样设计?

参考答案: Choreographer 在 scheduleTraversals() 中调用 postSyncBarrier() 添加屏障,然后通过异步消息注册 Vsync 回调。当 Vsync 信号到来时,doFrame() 方法执行完 UI 遍历后调用 removeSyncBarrier() 移除屏障。

这样设计的原因:UI 渲染必须在 16.6ms(60fps)内完成,如果主线程有大量同步消息,可能延迟 Vsync 回调的执行,导致掉帧。通过同步屏障,保证 Vsync 相关的异步消息能够优先执行,不受其他消息影响。

追问:如果 doFrame() 执行时间超过 16.6ms 会怎样?

  • 答:会导致掉帧。但同步屏障至少保证了 Vsync 回调能及时开始执行。掉帧是 doFrame() 内部逻辑(measure/layout/draw)耗时过长导致的,这需要通过优化布局层级、减少过度绘制等方式解决。

【进阶题2】同步屏障机制存在什么风险?如何避免?

参考答案: 主要风险:

  1. 消息饿死:如果屏障长时间不移除,同步消息会被无限期阻塞
  2. 内存泄漏:被阻塞的 Message 持有外部引用无法释放
  3. ANR:关键的同步消息无法执行,导致应用无响应

系统的避免策略:

  1. Choreographer 在 finally 块中移除屏障,确保异常情况也能移除
  2. 屏障添加和移除通常在同一方法调用链中,减少遗漏风险
  3. 系统 API 是 hide 的,防止应用层误用

追问:应用层如何实现类似的优先级控制?

  • 答:1)使用独立的 HandlerThread 处理高优先级任务;2)使用 Handler.postAtFrontOfQueue() 插入队首(但仍是同步消息);3)考虑使用协程的调度器实现优先级控制。

【进阶题3】为什么 postSyncBarrier() 是 hide API?

参考答案: 设计考量:

  1. 安全性:滥用同步屏障会导致消息阻塞和 ANR,普通开发者难以正确使用
  2. 封装性:这是系统级的优化机制,与 Choreographer、ViewRootImpl 等系统组件紧密耦合
  3. 兼容性:hide API 可以在后续版本中修改实现细节,不影响公开 API 契约
  4. 替代方案:应用层有其他方式实现优先级控制,不需要直接操作同步屏障

追问:Android 9 之后的 Handler.createAsync() 为什么可以公开?

  • 答:createAsync() 只是创建异步 Handler,发送的消息带有异步标记。但没有同步屏障配合,异步消息和同步消息没有优先级区别,不会造成阻塞风险,所以可以公开。

4.3 实战场景题


【场景题】线上发现偶发 ANR,堆栈显示卡在 MessageQueue.next(),如何排查是否与同步屏障有关?

问题分析: ANR 卡在 next() 可能原因:

  1. 同步屏障未移除,且没有异步消息
  2. 消息处理耗时过长
  3. Native 层 epoll_wait 异常

排查思路

  1. 检查堆栈:确认是阻塞在 nativePollOnce() 还是 synchronized 锁等待
  2. 分析 trace:查看是否有大量待处理的同步消息
  3. 代码审查:搜索项目中是否有反射调用 postSyncBarrier()
  4. 系统日志:检查 Choreographer 相关日志,确认 doFrame() 是否正常完成

解决方案

  1. 如果是自定义代码添加的屏障,确保在 try-finally 中移除
  2. 如果是系统屏障,检查 View 绘制流程是否有异常
  3. 添加监控:Hook MessageQueue,检测屏障存在时间

五、对比与总结

5.1 关键 API 对比

API/方法作用可见性使用场景
postSyncBarrier()添加同步屏障@hide系统级优先级控制
removeSyncBarrier(token)移除同步屏障@hide恢复正常消息处理
Message.setAsynchronous(true)设置异步消息@hide配合屏障使用
Handler.createAsync(Looper)创建异步Handlerpublic (API 28+)发送异步消息
postAtFrontOfQueue()插入队首public提高优先级(仍是同步消息)

5.2 核心要点速记

一句话记忆: 同步屏障是 target 为 null 的特殊 Message,用于阻塞同步消息让异步消息优先执行,是 Android UI 渲染及时响应的关键保障。

3个关键点

  1. 同步屏障通过 target == null 标识,在 next() 中被识别
  2. 屏障存在时,会遍历跳过同步消息,只取异步消息
  3. Choreographer 使用屏障保证 Vsync 回调优先执行

面试官最爱问

  1. 同步屏障的实现原理和 next() 的处理逻辑
  2. Choreographer 如何使用同步屏障保证 UI 渲染
  3. 同步屏障的风险和应用层的替代方案

六、关联知识点

前置知识

  • MessageQueue 消息出队机制(详见:../03-MessageQueue队列管理/02-消息出队next().md
  • Handler 基本工作流程(详见:../01-Handler基础/03-Handler工作流程.md

后续扩展

  • Choreographer 工作原理
  • ViewRootImpl 与 View 绘制流程
  • Vsync 信号机制

相关文件

  • ../03-MessageQueue队列管理/02-消息出队next().md - next() 方法完整逻辑
  • ../03-MessageQueue队列管理/04-epoll机制.md - Native 层阻塞唤醒
  • ../06-IdleHandler/IdleHandler原理与应用.md - 空闲消息处理机制