Android滑的动点不动问题排查(Choreographer详解)

排查过程:

问题回顾:

在实习的时候,看到过一个问题,在APP内部,用户点击”展开更多回复”按钮无响应,内容无法展开,但在此过程中能够滑动页面,loading动画正常渲染,就只是分页加载不出来,等待10-20s之后会有时能加载出来。

初步排查:

  • 首先排除主线程卡顿问题,因为能够滑动页面,动画也可以渲染。
  • 是否是业务代码异步操作等逻辑造成?
    • 通过打log来判断点击以后,是否通过RxJava异步卡顿造成,发现不是
    • 动画一直流程,和日志也没有关系
  • 是否是同步屏障导致?

同步屏障

同步屏障其实是和handler,渲染等有关的一个概念,总的来说就是在消息机制下,建立一个屏障,优先让异步消息先处理。这样说有点抽象,我们可以通过代码来理解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public int postSyncBarrier() {
return postSyncBarrier(SystemClock.uptimeMillis());
}

private int postSyncBarrier(long when) {
// Enqueue a new sync barrier token.
// We don't need to wake the queue because the purpose of a barrier is to stall it.
synchronized (this) {
final int token = mNextBarrierToken++;
final Message msg = Message.obtain();
msg.markInUse();
msg.when = when;
msg.arg1 = token;

Message prev = null;
Message p = mMessages;
if (when != 0) {
while (p != null && p.when <= when) {
prev = p;
p = p.next;
}
}
if (prev != null) { // invariant: p == prev.next
msg.next = p;
prev.next = msg;
} else {
msg.next = p;
mMessages = msg;
}
return token;
}
}

其实在这里,我们看到了关键的

1
2
msg.when = when;
msg.arg1 = token;

那message的这些关键词到底代表什么呢?

字段名 类型 作用 / 含义
what int 消息的标识符,用于区分不同类型的消息。你可以自定义常量来表示不同的业务含义,例如 MSG_LOAD_MORE = 1MSG_REFRESH = 2
target Handler 表示这条消息要发送给哪个 Handler。在 Handler.sendMessage(msg) 时会自动赋值为该 Handler 实例。
arg1 int 用于传递一个整型参数,通常是附加数据。你自己定义含义,比如分页的页码、状态码等。
arg2 int 也是整型参数,和 arg1 搭配使用。
obj Object 可以附带一个任意类型的对象,比如你要传递一个 ListStringBitmap 等。
replyTo Messenger 用于进程间通信 IPC 的场景中,指明“回信”地址。
when long 表示消息应该被处理的时间(毫秒)。通常由 Handler.postAtTime() 设置。
callback Runnable 如果设置了该字段,则在消息被处理时,执行这个 Runnable 而不是走 Handler.handleMessage()

这里做了一个总结,我们发现在源码中,并没有对target关键字进行赋值,也就是没有指定要发给哪个handler

所以默认是target == null, 它不会被任何 Handler 处理

Message对象通过next字段串联成单向链表,通过when,也就是不同的时机,插入到不同的位置。

在源码中,还标记了一个token,通过这个token来标记这个barrier。

1
2
3
4
5
6
7
8
Message prev = null;
Message p = mMessages;
if (when != 0) {
while (p != null && p.when <= when) {
prev = p;
p = p.next;
}
}

这是一个按照 when 时间查找插入点的循环。如果 when = 0(立刻生效),会直接插入到最前面。否则会往后找,找到第一个 when > 当前 msg 的位置

1
2
3
4
5
6
7
if (prev != null) { // 插入到 prev 和 p 之间
msg.next = p;
prev.next = msg;
} else { // 插入链表头部
msg.next = p;
mMessages = msg;
}

所以:同步屏障是按时间顺序插入到消息队列中的,不是简单加到头或尾。

那既然是让异步的消息来处理,那什么是异步消息,同步消息呢,为什么需要内存屏障呢?

在 Android 的 MessageQueue 中:

  • 同步消息(默认):message.target != null && !message.isAsynchronous()
    → 会被同步屏障(sync barrier)阻塞,不能通过 barrier
  • 异步消息message.isAsynchronous() == true
    → 可以穿越同步屏障,优先被处理(用于动画、输入、绘制)

其实就是为了动画能够更快的进入加载,这个过程其实是指的动画等doframe与vsncy的一个机制