ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SurfaceControlViewHost跨进程UI渲染:原理、实践与避坑指南

SurfaceControlViewHost跨进程UI渲染:原理、实践与避坑指南 每次Android大版本更新SurfaceFlinger和WindowManager这两个老牌模块总有一些新玩法。SurfaceControlViewHost在Android 12API 31亮相时我第一反应是这不就是把View塞进一块独立的SurfaceControl吗直到我在一个跨进程UI渲染项目里真正用了它才意识到这套机制解决的不只是换一种挂载方式这么简单。它把View的渲染输出直接从WMS的窗口层级里抽离出来交给了更底层的SurfaceFlinger去合成绕开了一堆传统窗口管理上的约束。如果你也遇到过这些需求——把一块直播间画面贴到游戏浮窗里、自己实现一套画中画面板、在车机仪表盘上叠加自定义小组件、或者让RemoteViews干不了的自定义View跨进程显示——SurfaceControlViewHost就是当前最值得啃的技术方案。这篇文章会从API使用、核心机制、常见坑位三个维度展开里面的经验全部来自我实际跑过的项目和翻过的源码。1. 为什么需要SurfaceControlViewHost传统方案的痛点和新的设计思路1.1 从WindowManager.addView说起它哪里不够用在没有SurfaceControlViewHost之前想在一段独立的Surface上渲染自己的View最常规的思路就是通过WindowManager.addView把一个View挂到系统窗口层级里。核心代码如下WindowManager wm (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); WindowManager.LayoutParams params new WindowManager.LayoutParams( width, height, WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSLUCENT ); wm.addView(myView, params);这段代码看起来简单但跨进程场景下问题就来了。addView这套机制背后依赖WMS去分配WindowToken、计算Z序、做窗口动画整个链路里每一步都涉及跨进程Binder调用而且是串行处理的。当App想把自己的一段UI渲染到另一个进程管理的Surface上时要么通过AIDL把View的Bitmap传来传去要么就得在远端进程里重新构建一套窗口体系这两种方式的实时性和内存开销都很难看。我自己踩过最明显的一个坑手机投屏类App里渲染一块操作面板用addView方式挂到系统窗口后拖动面板时明显感觉比App内部动画慢半拍。原因很简单窗口参数的变更要经过WMS重新布局再等下一帧Vsync信号到来中间走了一大圈。SurfaceControlViewHost的思路则是完全换了一条路它不再经过WMS的窗口层级管理而是让SurfaceFlinger直接合成本地创建的SurfaceControl。1.2 SurfaceControlViewHost到底解决什么问题一句话概括SurfaceControlViewHost的能力它允许你在一个进程中创建一个View hierarchy然后把这块View的渲染结果以SurfacePackage的形式交给另一个进程由另一个进程决定这块内容最终显示在哪个SurfaceControl上并参与最终的SurfaceFlinger合成。这样说可能有点抽象我们来拆解一下。SurfaceControlViewHost内部做的事情本质上是三件在内容提供方进程里创建一个独立的SurfaceControl并把自己的View attach到这块Surface上View的绘制结果直接输出到这块独立的Surface上。通过SurfacePackage把这块Surface的跨进程引用传递出去SurfacePackage内部封装了SurfaceControl和相关的Binder对象是Parcelable的可以放进Bundle或通过AIDL接口传输。在宿主进程里拿到SurfacePackage后可以通过AttachedSurfaceControl的接口把这块SurfaceControl attach到宿主窗口层级里之后SurfaceFlinger就会像合成普通窗口一样合成这个Surface。它绕过了WMS那套WindowToken、窗口动画、窗口层级管理逻辑走的是一条更底层的路直接操作SurfaceControl直接和SurfaceFlinger打交道。这带来的最直接好处是性能更好延迟更低因为链路更短另一个好处是它天然支持跨进程传递而不需要你在两个进程之间手动同步View状态或者Bitmap数据。提示Android 12之前也有类似的机制叫SurfaceView的跨进程传递但SurfaceView只能承载视频或相机预览这类由独立Surface渲染的内容不能承载完整的View hierarchy。SurfaceControlViewHost是第一个把完整View hierarchy作为可跨进程传递对象的方案。1.3 适用的场景和不适用场景我认识到SurfaceControlViewHost的真正价值是在一个多进程架构的App里。当时产品要把一个用Flutter写的控制面板嵌入到一个纯原生渲染的游戏界面里而且二者还不一定在同一个进程。先说适用场景跨进程UI嵌入。比如宿主App是C/游戏引擎渲染需要嵌入一段原生的Android View二者不在同一个进程。画中画模式的自定义控制面板。PiP窗口本身是系统管理的但面板内容如果还要能交互用SurfaceControlViewHost可以做到低延迟。多显示设备上的自定义UI。比如车机、双屏设备上需要把一个进程的内容投射到另一个Display上。直播间浮窗、游戏内嵌WebView这类悬浮UI。它比全局悬浮窗更轻量而且不依赖SYSTEM_ALERT_WINDOW权限当然权限模型要看具体使用场景。不适用场景也要说清楚只是想在同一个进程内部做自定义悬浮窗完全没必要用SurfaceControlViewHost直接用addView或者一个普通的Dialog就行。需要系统级的窗口管理能力比如需要窗口焦点、输入法交互、返回键拦截等SurfaceControlViewHost非常轻量不提供这些窗口管理级别的能力它有自己的输入传递机制但和真正的窗口焦点模型不一样。如果目标设备是Android 11及以下这套API不可用还得回归老方案。2. 从零到一把SurfaceControlViewHost跑起来2.1 宿主侧和服务侧的角色划分SurfaceControlViewHost涉及的术语有点多先把角色理清楚宿主Host接收SurfacePackage的一端负责把SurfaceControl attach到自己的窗口层级里。内容提供方Provider创建SurfaceControlViewHost、承载View hierarchy的一端负责渲染输出。SurfacePackage跨进程传递的包裹内部包含SurfaceControl的引用。官方文档里演示时经常用宿主App RemoteViewService这种结构其实你不用把问题想得太复杂。更通用的理解是只要你能拿到SurfacePackage自己创建、从AIDL接口里接收、从Intent里解析你就能把它attach到任何一个AttachedSurfaceControl上。我实际项目里的结构长这样一个独立的SDK模块负责创建SurfaceControlViewHost并承载View通过AIDL接口把SurfacePackage传递给主App进程主App进程把它挂载到自己的DecorView所在的SurfaceControl上。关键代码内容提供方// 内容提供方进程创建Host并承载View SurfaceControlViewHost host new SurfaceControlViewHost( context, context.getDisplay(), token // 这个token很关键见下文 ); // 把View attach到host上 host.setView(contentView, width, height); SurfaceControlViewHost.SurfacePackage surfacePackage host.getSurfacePackage(); // 通过AIDL跨进程把surfacePackage传给宿主 iRemoteInterface.attachSurfacePackage(surfacePackage);宿主的AIDL实现里接收SurfacePackage并attachOverride public void attachSurfacePackage(SurfaceControlViewHost.SurfacePackage surfacePackage) { ViewRootImpl viewRoot getCurrentViewRoot(); // 自己的ViewRootImpl SurfaceControl hostSurfaceControl viewRoot.getSurfaceControl(); hostSurfaceControl.addChild(surfacePackage.getSurfaceControl()); // 或者用WindowManager.LayoutParams里的token方式稍后再展开 }结合我的经验直接操作SurfaceControl的addChild是最容易理解的方式。宿主窗口中任何一块SurfaceControl都可以作为父节点把SurfacePackage里的SurfaceControl挂进来之后Surfacebook组件就会出现在父节点的坐标系里跟着父节点一起移动、显示、隐藏。2.2 关键参数详解width、height和token构造SurfaceControlViewHost的时候需要三个参数Context、Display和一个token。大多数教程对这个token一笔带过但它实际非常关键。这个token的类型是IBinder它本质上是一个许可证表明你有权利在这块Display上创建SurfaceControl并获得合成能力。官方做法是从宿主进程的WindowManager.LayoutParams里取token// 宿主进程准备token WindowManager.LayoutParams params getWindow().getAttributes(); IBinder token params.token; // 把token传给内容提供方 iRemoteInterface.createSurfaceHost(token);然后内容提供方使用这个token来构造SurfaceControlViewHost。我在第一次做的时候没有传token直接传了null结果是创建不报错但后面合成的Surface永远不在屏幕上出现排查了很久才发现是token缺失导致SurfaceControl无法正常attach到系统的合成层级里。width和height的单位是像素不是dp这个要留意。而且这两个值决定的是SurfaceControl的大小View会按照这个大小去做measure和layout所以你在远端创建View的时候要避免使用MATCH_PARENT这类相对值最好直接给具体像素值。2.3 使用SurfacePackage完成跨进程传输SurfacePackage实现了Parcelable接口所以它可以直接放进Bundle、Intent或者AIDL方法参数里传递。这里的实现层面有几个注意事项第一SurfacePackage传递的是Binder引用所以跨进程传递几乎没有拷贝成本真正拷贝的只是Parcel里的引用句柄Surface的Buffer分配还是在内容提供方进程里完成由SurfaceFlinger统一管理。第二同一个SurfacePackage可以被多次attach也可以被attach到多个宿主窗口。这在某些场景下很实用比如一块内容要同时显示在主屏幕和副屏上。但你需要注意SurfaceControlViewHost的view只能属于一个host如果两个宿主窗口都要显示同一块内容通常会选择创建两个SurfaceControlViewHost而不是共用一个SurfacePackage。第三SurfacePackage用完后要调用onRelease()释放不要依赖GC。它是Parcelable在跨进程传递过程中可能出现引用计数混乱主动release是最稳妥的方式。我在实际项目中遇到过一个诡异的现象SurfacePackage在宿主进程里收到了但显示黑屏后来发现是我在内容提供方进程里提前把SurfaceControlViewHost给release掉了而宿主还没有完成attach操作。所以生命周期管理一定要做好确保宿主attach成功后再释放provider端的host。3. 深入核心机制SurfaceControlViewHost的渲染与合成回顾3.1 View是怎么画到SurfaceControl上的我们知道普通Activity里的View渲染依赖ViewRootImpl和WindowManagerService的relayout流程。SurfaceControlViewHost的设计有点类似但它的ViewRootImpl是Android 12之后引入的ViewRootImpl的一个变体它不需要经过WMS的relayout而是直接和SurfaceControl打交道。在源码层面SurfaceControlViewHost内部会创建一个ViewRootImpl实例并调用setView方法mViewRootImpl new ViewRootImpl(context, display); mViewRootImpl.setView(contentView, new WindowManager.LayoutParams(...), token);正是因为SurfaceControlViewHost内部有一个完整的ViewRootImpl所以你在host里setView的View是具备完整的View体系工作能力的——measure、layout、draw、invalidate、事件分发都能正常工作甚至还能使用Choreographer做帧回调。但它和普通窗口的最大区别在于ViewRootImpl在传统窗口里需要WMS分配窗口和Surface而在SurfaceControlViewHost里ViewRootImpl直接attach到一个预先创建好的SurfaceControl上这个SurfaceControl由内容提供方自己创建并持有WMS完全不参与。3.2 跨进程合成与SurfaceFlinger理解SurfaceControlViewHost的跨进程合成机制可以从SurfaceControl这个对象入手。一个SurfaceControl本质上是SurfaceFlinger中的一个Layer的句柄你可以把它理解为合成器里的一个图层描述符。SurfaceFlinger的合成模型本身是支持不同进程创建SurfaceControl的因为BufferQueue的生产者进程和SurfaceFlinger的合成进程天然是分离的。SurfaceControlViewHost能跨进程显示的关键在于内容提供方进程创建一个SurfaceControl并且用View渲染出内容内容Buffer通过BufferQueue交给SurfaceFlinger。SurfacePackage把这个SurfaceControl的引用跨进程Binder句柄传给宿主进程。宿主进程拿到引用后把这个SurfaceControl attach到自己的窗口层级里也就是把自己窗口的SurfaceControl作为父节点。SurfaceFlinger在做每一帧合成时发现宿主窗口Layer下面挂着一个子Layer于是把子Layer的Buffer也一起合成。整个过程里数据流是单向的提供方的View画到Buffer里Buffer给到SurfaceFlingerSurfaceFlinger再根据宿主窗口的层级关系做最终合成。宿主进程其实不知道这块Surface里面的内容是什么它只负责把SurfaceControl挂到正确的位置。这也是为什么这套方案的性能表现优于传统方式不像addView需要一系列Binder调用跨越进程边界协商窗口参数SurfaceControlViewHost的跨进程边界在对象传递时只发生一次后续的每帧内容更新走的是BufferQueue的共享内存通路性能损耗被压到了最低。3.3 Input事件如何跨进程传递跨进程View渲染只是一半如果连点击事件都传不了这套机制实用性就差很多。SurfaceControlViewHost也有自己的输入事件传递链路但这块API在Android 12/12L上还不够成熟Android 13之后才比较完善。核心机制是这样的宿主窗口收到InputEvent之后如果事件落在SurfacePackage对应的SurfaceControl区域里宿主会把这个事件转交给内容提供方的View hierarchy来处理。这个能力依赖InputTransferToken它随SurfacePackage一起传递相当于内容提供方在系统输入系统里注册了一个接收者标识。开发时的注意点如果宿主窗口没有焦点Input事件可能不会正确传递需要确保宿主窗口具备接收input的能力不是FLAG_NOT_FOCUSABLE。事件坐标是相对SurfaceControl左上角的不是相对宿主窗口的你需要自己处理坐标偏移或者在View层面用hit test来验证。我在Android 12L的设备上实测多点触控偶尔会出现事件丢失升级到Android 13之后才稳定下来。如果你必须兼容Android 12的设备交互类场景要提前做充分压测。4. 可用的场景与限制从TaskFragment到自定义画中画面板4.1 官方的TaskFragment和SurfaceControlViewHostAndroid 12L为平板和大屏引入了TaskFragment这是一种把Task拆分成多个可独立调整大小的容器的机制。在TaskFragment的实现内部就有SurfaceControlViewHost的身影每个TaskFragment会用SurfaceControlViewHost来承载和显示自己的内容从而在不需要创建新Task、不经过WMS窗口层级的情况下显示一块独立UI。TaskFragment的具体使用细节我们这里不展开但你可以从中看到SurfaceControlViewHost的定位它不是用来替代WindowManager的通用方案而是为某些特定场景提供一个轻量、高效、可跨进程的View挂载方式。4.2 自定义画中画控制面板再看另一个我实际做过的场景视频画中画窗口的自定义控制面板。Android的PictureInPicture模式在Android 8.0引入了setActions但Actions能放的东西有限而且控制面板的样式无法自定义。在Android 12及之后的高版本上你可以用SurfaceControlViewHost配合MediaSession的setCustomExtras或类似机制把一块完全自定义的View渲染出来并覆盖在PiP窗口的控制区域上。具体做法是把SurfaceControlViewHost创建的SurfacePackage通过MediaSession的setSessionActivity或者你自己定的AIDL接口传给SystemUI进程由SystemUI把它attach到PiP窗口对应的Layer上。这样你就能做出完全自定义的进度条、按钮、背景模糊面板。当然这条路走起来没那么轻松。PiP窗口的尺寸系统可控你的Surface的大小需要和系统计算的PiP窗口尺寸保持一致否则会出现显示不全或拉伸。这部分我建议你在真机上对照dumpsys window displays输出做尺寸校准。4.3 哪些场景暂时还不能用与SpaceControlViewHost本身的可靠性无关但有一些限制是我在翻源码和实测中确认过的它不能自己在屏幕上创建一个独立窗口。SurfaceControlViewHost创建的是一个附着关系如果你没有宿主窗口SurfacePackage无处可放。它不支持View的动画跨进程同步。ViewPropertyAnimator这类动画在提供方进程执行时帧率不受宿主进程Vsync信号控制偶尔会出现掉帧感。它的Surface和宿主窗口是父子Layer关系所以Surface的Z序永远在宿主窗口的内容之上不能插入到宿主窗口图层之间。系统级的安全弹窗、输入法、截屏保护这类涉及WindowManager全局策略的功能都无法应用。5. 常见问题与排查技巧实录5.1 黑屏SurfacePackage已经attach了但看不到内容这是最常见的问题。我排查的思路一般分三步第一检查SurfacePackage是否是有效的。在传递过程中Parcel化时如果SurfaceControl引用不可用了SurfacePackage持有一个null的底层引用attach后不会报错但也不会显示。这时候可以在提供方进程里打印日志确认getSurfacePackage()返回的对象.isValid()是true。第二检查attach时机。如果在attach之前surfacebookPackage对应的SurfaceControlViewHost已经被releaseSurfacePackage虽然还保留了Binder引用但SurfaceControl已经没有配对的BufferQueue了合成器拿不到内容Buffer表现同样是黑屏。第三检查size。如果SurfaceControl的size为0或者View没有正确measure导致Surface上没有被绘制内容也会黑屏。可以直接用dumpsys SurfaceFlinger查看对应Layer的size和activeBuffer信息adb shell dumpsys SurfaceFlinger | grep -A 10 SurfaceControlViewHost5.2 CrashDetached surface 相关异常SurfaceControlViewHost的view和传统Activity一样都受ViewRootImpl的detach机制约束。如果你在释放host后继续尝试操作view或者宿主进程在detach之后还往SurfaceControl上addChild很容易触发IllegalStateException: surface is not valid或者更隐蔽的native crash。我的建议是把SurfaceControlViewHost的生命周期和宿主Activity的onStop/onStart绑定而不是依赖finalize或者onDestroy。onDestroy并不保证一定会被系统调用比如系统杀死应用但onStop一定会在页面离开前台时触发你可以在这个时机安全地移除SurfaceControl的child。5.3 输入事件收不到如果你已经完成了attachUI也显示了但点击事件没有传递到你的View上优先排查两个点宿主ViewRootImpl是否设置了LayoutParams.flags | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN因为输入区域的映射需要宿主窗口的Bounds信息。内容提供方的ViewRootImpl是否允许接收touch事件。SurfaceControlViewHost内部对rootView的focusable设置有自己的默认值不一定是true你需要在setView时传对应的LayoutParams。5.4 跨进程传输的安全性与权限模型补充一点安全性上把SurfacePackage传给不信任的进程是有风险的因为拿到SurfacePackage后可以在自己的窗口里展示提供方的内容甚至在特定条件下注入输入事件。商业项目里一定要对提供SurfacePackage的AIDL接口做权限校验signature级别的permission或至少调用方包名校验。6. 写在最后这套API的演进和我的实际体验SurfaceControlViewHost从Android 12诞生到现在API稳定性已经比较好了但它的定位始终是面向系统集成商和高级应用开发者的进阶API不是所有App都适合用。它的应用形态也很明确要么是跨进程嵌入最主流要么是宿主进程内部另起炉灶式的View挂载少见但有奇效。我自己的最终建议是如果你的应用场景和它匹配值得花时间把这套机制吃透如果只是做个普通浮窗那就别折腾了。真要做之前家里至少备一台Android 13的设备Android 12L上的兼容性问题远比你想的多。动手顺序上也不用复杂先跑通一个最小demo在同一个进程里创建SurfaceControlViewHost并显示再改造成跨进程最后再叠加输入事件和生命周期管理层面的复杂度这样踩坑的时候定位起来会快很多。
返回列表