求求不要再用 binder 跨进程同步 ui 了
前言-苦不堪言
最近在搞低端机上的性能优化,碰上一堆头疼的问题。有一个特别坑的玩意,那就是谷歌自己写的一套绝世“好代码“,用户手机上经常用到的负一屏的基础架构。本来我两年前开始搞这玩意的时候觉得没啥,滚动同步 UI 嘛,而且还要跨进程,那看起来 binder
通信不就是不二之选:桌面滚动到边缘触发 EdgeEffect
,EdgeEffect
来通过 binder
通知负一屏滚动了多少,然后负一屏那边在回调真正的滚动进度回桌面。这一套行云流水的操作下来可以说是没什么问题吧,至少其他厂家也在用,谷歌自己也在用。
但是啊,但是,坑爹的来了,这一套在高端机上没啥问题,高端机性能好嘛,我之前也是一直在搞高端机的需求开发,低端机接触得不多(或者说没接触过)。当跑在低端机上的时候,问题就开始显现出来了。倒也不是出在 binder
的问题上,手机再卡,oneway binder
其实也不会耗费多长时间(至少我是没遇到过 oneway binder
长耗时的情况)。问题是什么呢,就是通过这种方式衔接滚动时,桌面跟负一屏之间并不存在帧同步。尤其在低端机上,GPU
的性能本身就不太好,很容易出现 buffer
堆积在 SurfaceFlinger
端的问题。如果桌面跟负一屏这两个进程的 buffer
一个正常渲染,而另一个出现堆积比较严重的情况时,会有什么现象呢?那当然是两者的 UI 并不会渲染为相同的进度,毕竟对于同一滚动进度而言,一个已经渲染好了送往 SurfaceFlinger
合成,另一个还苦苦卡在 dequeueBuffer
等待拿到 buffer
去调用 GPU
渲染。
该怎么办呢
最佳解决方案当然还是利用到 BLASTBufferQueue
的帧同步,毕竟谷歌出这玩意不是白出的,目前大量的场景都有用到,包括我正在负责的桌面应用窗口动画。BLASTBufferQueue
的细节我就先不深究了,只需要知道它是先 dequeueBuffer
获取缓冲区,然后调用 GPU
进行绘制合成,最后通过 mergePendingTransactions
把之前合并进来的事务一块发往 SurfaceFlinger
渲染就可以了。为什么能做到帧同步呢,就是因为它把事务跟 buffer
相关的事务是一块发往 SurfaceFlinger
端的,都一块发送了,那当然是一块合成的了。
但这玩意也不是谁都会用谁都能用好的,需要知道什么时候去合并事务。之前我另一个同事有一个需求,需要对一个 Surface
进行几何变化操作,搞了几天实在是搞不定,就来找我了。其实本身也不是什么很难的问题,就是生成事务的时间节点不对。仔细想想 ViewRootImpl
的 vsync
处理流程,它是先去处理 MotionEvent
,然后做动画,最后才去 performTraversal
来遍历 View
树然后合成。使用 BLASTBufferQueue
一个很经典的用法就是:
1 | viewRootImpl.registerRtFrameCallback(frame -> { |
调用 registerRtFrameCallback
会往 ThreadedRenderer
里面注册 FrameDrawingCallback
,每次帧绘制完成之后就会回调到这里,而且在 ThreadedRenderer#updateRootDisplayList
里面会收集这些回调,并通过 setFrameCallback
来设置回调监听。也就是说,你在 ThreadedRenderer#updateRootDisplayList
之后调用的 registerRtFrameCallback
,只能等到下下帧才会被收集。因此,你要想在当前帧的 View
相对应的位置去映射一个相关的位置到 Transaction
(例如通过 Matrix
设置)的话,那么你调用 registerRtFrameCallback
就只能在绘制流程之前去调用,不然你这次 transaction
就没法合并进即将到来的下一帧里。
回到原来的问题
你问我卡顿最后怎么解决的?无可奉告(摆手)
当然了,我还是希望能更换目前的负一屏方案的,谷歌那套该扫进垃圾桶了,现在有 SurfaceControlViewHost
能跨进程渲染的,好用不伤脑,而且还能拿到 SurfaceControl
来确保可以做到帧同步。
求求你换方案吧,只要是我能做到的我什么都愿意做(你这个人真是满脑子只想着自己呢)
求求不要再用 binder 跨进程同步 ui 了
https://gitofleonardo.github.io/2025/03/20/Android_plz_do_not_use_binder_to_sync_ui/