求求不要再用 binder 跨进程同步 ui 了

前言-苦不堪言

最近在搞低端机上的性能优化,碰上一堆头疼的问题。有一个特别坑的玩意,那就是谷歌自己写的一套绝世“好代码“,用户手机上经常用到的负一屏的基础架构。本来我两年前开始搞这玩意的时候觉得没啥,滚动同步 UI 嘛,而且还要跨进程,那看起来 binder 通信不就是不二之选:桌面滚动到边缘触发 EdgeEffectEdgeEffect 来通过 binder 通知负一屏滚动了多少,然后负一屏那边在回调真正的滚动进度回桌面。这一套行云流水的操作下来可以说是没什么问题吧,至少其他厂家也在用,谷歌自己也在用。

但是啊,但是,坑爹的来了,这一套在高端机上没啥问题,高端机性能好嘛,我之前也是一直在搞高端机的需求开发,低端机接触得不多(或者说没接触过)。当跑在低端机上的时候,问题就开始显现出来了。倒也不是出在 binder 的问题上,手机再卡,oneway binder 其实也不会耗费多长时间(至少我是没遇到过 oneway binder 长耗时的情况)。问题是什么呢,就是通过这种方式衔接滚动时,桌面跟负一屏之间并不存在帧同步。尤其在低端机上,GPU 的性能本身就不太好,很容易出现 buffer 堆积在 SurfaceFlinger 端的问题。如果桌面跟负一屏这两个进程的 buffer 一个正常渲染,而另一个出现堆积比较严重的情况时,会有什么现象呢?那当然是两者的 UI 并不会渲染为相同的进度,毕竟对于同一滚动进度而言,一个已经渲染好了送往 SurfaceFlinger 合成,另一个还苦苦卡在 dequeueBuffer 等待拿到 buffer 去调用 GPU 渲染。

该怎么办呢

最佳解决方案当然还是利用到 BLASTBufferQueue 的帧同步,毕竟谷歌出这玩意不是白出的,目前大量的场景都有用到,包括我正在负责的桌面应用窗口动画。BLASTBufferQueue 的细节我就先不深究了,只需要知道它是先 dequeueBuffer 获取缓冲区,然后调用 GPU 进行绘制合成,最后通过 mergePendingTransactions 把之前合并进来的事务一块发往 SurfaceFlinger 渲染就可以了。为什么能做到帧同步呢,就是因为它把事务跟 buffer 相关的事务是一块发往 SurfaceFlinger 端的,都一块发送了,那当然是一块合成的了。

但这玩意也不是谁都会用谁都能用好的,需要知道什么时候去合并事务。之前我另一个同事有一个需求,需要对一个 Surface 进行几何变化操作,搞了几天实在是搞不定,就来找我了。其实本身也不是什么很难的问题,就是生成事务的时间节点不对。仔细想想 ViewRootImplvsync 处理流程,它是先去处理 MotionEvent,然后做动画,最后才去 performTraversal 来遍历 View 树然后合成。使用 BLASTBufferQueue 一个很经典的用法就是:

1
2
3
viewRootImpl.registerRtFrameCallback(frame -> {
viewRootImpl.mergeWithNextTransaction(t, frame)
})

调用 registerRtFrameCallback 会往 ThreadedRenderer 里面注册 FrameDrawingCallback,每次帧绘制完成之后就会回调到这里,而且在 ThreadedRenderer#updateRootDisplayList 里面会收集这些回调,并通过 setFrameCallback 来设置回调监听。也就是说,你在 ThreadedRenderer#updateRootDisplayList 之后调用的 registerRtFrameCallback,只能等到下下帧才会被收集。因此,你要想在当前帧的 View 相对应的位置去映射一个相关的位置到 Transaction (例如通过 Matrix 设置)的话,那么你调用 registerRtFrameCallback 就只能在绘制流程之前去调用,不然你这次 transaction 就没法合并进即将到来的下一帧里。

回到原来的问题

你问我卡顿最后怎么解决的?无可奉告(摆手)

当然了,我还是希望能更换目前的负一屏方案的,谷歌那套该扫进垃圾桶了,现在有 SurfaceControlViewHost 能跨进程渲染的,好用不伤脑,而且还能拿到 SurfaceControl 来确保可以做到帧同步。

求求你换方案吧,只要是我能做到的我什么都愿意做(你这个人真是满脑子只想着自己呢)

作者

Hhvvg

发布于

2025-03-20

更新于

2025-03-21

许可协议