【车载 Android】实践跨进程 UI 融合渲染

0 阅读6分钟

随着智能座舱的持续发展,车载 Android 系统早已不再是简单的“平板上车”。现代车载系统追求的是多屏联动多应用无缝融合以及极致的视觉一致性

在实际场景中,我们经常遇到这样的需求:海外地图应用由第三方厂商提供,而空调、座椅控制等 HMI由车企自研。为了系统的稳定与安全,这些应用通常运行在不同的进程中。然而,在 UI 表现上,我们可能需要将“通话控制面板”嵌入到“SystemUI”中,或者将“音乐卡片”显示在“桌面 ”上。

这种跨进程 UI 融合不仅要求像素层面的叠加,更要求交互事件的精准分发和渲染性能的零损耗。本文将探讨 Android 11+ 引入的 SurfaceControlViewHost 如何成为解决这一问题的核心利器。


背景与痛点

在传统的 Android 架构下,实现真正的跨进程 UI 融合面临着以下三大瓶颈:

1. 内存与进程的物理隔绝

Android 的安全机制(沙盒模型)决定了进程 A 无法直接访问进程 B 的 View 对象。

如果尝试通过反射或其他手段在 Host 进程创建 Provider 进程的 View,得到的只是一个“空壳”。所有的业务逻辑、数据绑定依然留在原进程。如果强行序列化 View 状态进行同步,跨进程通信(IPC)的频繁调用会导致严重的性能抖动。

2. 触控事件的路由难题

UI 融合不仅仅是“看起来在一起”,更要“点起来没问题”。

当进程 B 的 UI 嵌套在进程 A 的窗口内时,系统的 InputDispatcher 默认会将点击事件分发给最顶层的窗口。如果使用传统的 SurfaceView 或透明 Activity 覆盖,事件往往会被宿主拦截,导致嵌入的 UI “看得见点不动”。手动通过 AIDL 转发坐标信息不仅延迟高,且难以处理多点触控和复杂的滑动冲突。

3. 渲染管线的性能开销

传统的跨进程 UI 方案(如 RemoteViews 或虚拟显示器 VirtualDisplay)在车载环境下性能捉襟见肘。

  • RemoteViews:仅支持基础控件,无法实现复杂的自定义绘图。
  • VirtualDisplay:本质是录屏和流传输,涉及大量的像素拷贝和编解码过程,对车载芯片的 GPU 和带宽压力极大,低端车型存在明显的感知延迟。

SurfaceControlViewHost 简介

为了打破上述瓶颈,Android 11 引入了 SurfaceControlViewHost,专门用于在不同进程间嵌入视图内容。其核心思想是:

  • Provider(内容提供方) :创建 Surface,在独立进程中实现视图的业务逻辑
  • Host(宿主方) :接收 Surface,在自己的窗口中显示
  • SurfacePackage:作为跨进程传递 Surface 的载体

具有以下优势:

  • GPU 合成:它基于 SurfaceControl 进行硬件合成,图像直接提交给系统的 SurfaceFlinger,无需跨进程拷贝像素。
  • InputChannel 共享:通过传递 hostToken,它能建立一个原生的输入通道,使嵌入的 View 像本地 View 一样精准响应点击和滑动。

Android 11 中SurfaceControlViewHost作为系统 API 仅限系统应用使用,Android 12 正式公开。


SurfaceControlViewHost 实践

本示例演示了如何在 ProviderApp 中实现“来电”业务逻辑与 UI 交互,并将其无缝嵌入 HostApp 进行显示。完成后的界面效果如下。

再次强调,HostApp界面上展示的电话UI实际位于ProviderService中,HostApp没有集成ProvierApp的任何SDK。

1.交互流程解析

跨进程融合分为三个关键阶段:

  • 连接阶段:Host 绑定 Provider 服务,通过 AIDL 获取通信接口 。
  • 嵌入阶段:Host 提供 hostToken;Provider 创建 SurfaceControlViewHost 及真实的 View 树(如 BluetoothCallView),并将生成的 SurfacePackage 回传给 Host 渲染。
  • 渲染与交互阶段:Provider 渲染数据直投 SurfaceFlinger;系统根据 hostToken 识别点击区域并直接转发触控事件至 Provider 进程。

暂时无法在飞书文档外展示此内容

2.技术实现要点

AIDL 通信接口:定义 ISurfaceProvider 用于传输 SurfacePackage 和缩放状态。该接口不参与逐帧渲染,仅在初始化或状态变更时调用,性能优势显著


interface ISurfaceProvider {

    /**
     * 异步获取 SurfacePackage
     * @param hostToken Host 端的 Window token
     * @param width 请求宽度
     * @param height 请求高度
     * @param callback 回调接口
     */
    void getSurfacePackageAsync(IBinder hostToken, int width, int height, ISurfaceProviderCallback callback);

    /**
     * 设置缩放状态
     * @param scaleState 缩放状态: 0=正常, 1=缩小(长条)
     */
    void setScaleState(int scaleState);
}

/**
 * SurfaceProvider 回调接口
 * 用于异步返回 SurfacePackage 和状态变化通知
 */
interface ISurfaceProviderCallback {

    /**
     * SurfacePackage 准备就绪回调
     */
    oneway void onSurfacePackageReady(in SurfacePackage surfacePackage);

    /**
     * 缩放动画结束回调
     * @param scaleState 当前缩放状态: 0=正常, 1=缩小(长条)
     * @param finalWidth 最终宽度
     * @param finalHeight 最终高度
     */
    oneway void onScaleStateChanged(int scaleState, int finalWidth, int finalHeight);
}
 /**
* SurfaceProvider 服务管理器
* 封装服务绑定、解绑和接口调用逻辑
*/
class SurfaceProviderManager(private val context: Context) {

    /**
* 获取 SurfacePackage(异步版本)
* 避免主线程阻塞,推荐使用此方法
*
* @param hostToken Host 端的 Window token
* @param width 请求宽度
* @param height 请求高度
* @param onResult 回调,返回 SurfacePackage 或 null
*/
fun getSurfacePackageAsync(hostToken: IBinder, width: Int, height: Int, onResult: (SurfaceControlViewHost.SurfacePackage?) -> Unit) {
        val provider = surfaceProvider
        if (provider == null) {
            Log.e(TAG, "Service not connected")
            onResult(null)
            return
        }

        try {
            val callback = object : ISurfaceProviderCallback.Stub() {
                override fun onSurfacePackageReady(surfacePackage: SurfaceControlViewHost.SurfacePackage?) {
                    Log.d(TAG, "Async SurfacePackage received")
                    onResult(surfacePackage)
                }

                override fun onScaleStateChanged(scaleState: Int, finalWidth: Int, finalHeight: Int) {
                    // 动画状态变化回调,可用于同步调整容器大小
                    Log.d(TAG, "Scale state changed: state=$scaleState, size=${finalWidth}x$finalHeight")
                    callback?.onScaleStateChanged(scaleState, finalWidth, finalHeight)
                }
            }

            provider.getSurfacePackageAsync(hostToken, width, height, callback)
        } catch (e: RemoteException) {
            Log.e(TAG, "getSurfacePackageAsync error: ${e.message}")
            onResult(null)
        } catch (e: Exception) {
            Log.e(TAG, "getSurfacePackageAsync error: ${e.message}")
            onResult(null)
        }
    }

    /**
* 设置缩放状态
*/
fun setScaleState(scaleState: Int) {
        val provider = surfaceProvider
        if (provider == null) {
            Log.e(TAG, "Service not connected, cannot set scale state")
            return
        }

        try {
            provider.setScaleState(scaleState)
        } catch (e: Exception) {
            Log.e(TAG, "setScaleState error: ${e.message}")
        }
    }
}

Provider 侧实现:作为“UI 管家”,它在后台进程持有一个隐形窗口,负责将包含动画的复杂 View 转换为系统可识别的 SurfaceControl

// 当前回调接口(用于动画结束通知)
private var currentCallback: ISurfaceProviderCallback? = null

// 当前 ProviderView 引用
private var currentBluetoothCallView: BluetoothCallView? = null

// 嵌入式视图(使用热更新,避免频繁重建)
private var surfaceControlViewHost: SurfaceControlViewHost? = null
private var currentHostToken: IBinder? = null

private val binder = object : ISurfaceProvider.Stub() {

    override fun getSurfacePackageAsync(hostToken: IBinder, width: Int, height: Int, callback: ISurfaceProviderCallback?) {
        // 保存回调引用
        currentCallback = callback
        mainHandler.post {
try {
                if (surfaceControlViewHost == null || currentHostToken != hostToken) {
                    // 首次创建或 token 变更,需要重建
                    Log.d(TAG, "Creating new SurfaceControlViewHost")
                    surfaceControlViewHost?.release()

                    val wm = getSystemService(Context.WINDOW_SERVICE) as android.view.WindowManager
                    surfaceControlViewHost = SurfaceControlViewHost(this@SurfaceProviderService, wm.defaultDisplay, hostToken)
                    currentHostToken = hostToken

                    // 创建新的 ProviderView
                    currentBluetoothCallView = null  // 先清空旧引用
                    val embeddedView = BluetoothCallView(this@SurfaceProviderService)
                    currentBluetoothCallView = embeddedView
                    surfaceControlViewHost?.setView(embeddedView, width, height)

                    // 根据当前状态设置初始 UI
                    val initialStateInt = if (targetScaleState == ScaleState.COMPACT) 1 else 0
                    embeddedView.setInitialState(initialStateInt)
                } else {
                    Log.d(TAG, "Relayout existing SurfaceControlViewHost: ${width}x${height}")
                    surfaceControlViewHost?.relayout(width, height)
                }

                val surfacePackage = surfaceControlViewHost?.surfacePackage
callback?.onSurfacePackageReady(surfacePackage)
            } catch (e: Exception) {
                Log.e(TAG, "Error in async getSurfacePackage: ${e.message}")
                callback?.onSurfacePackageReady(null)
            }
        }
}

    override fun setScaleState(state: Int) {
        Log.d(TAG, "setScaleState: $state")
        val newState = if (state == 0) ScaleState.NORMAL else ScaleState.COMPACT
if (targetScaleState == newState) return
        targetScaleState = newState
        val stateInt = if (newState == ScaleState.COMPACT) 1 else 0
        mainHandler.post {
currentBluetoothCallView?.transitionToState(stateInt, currentCallback)
        }
}
}

Host 侧实现:使用 SurfaceView 在布局中占位,通过 setChildSurfacePackage 接收远程渲染指令 ,通过 hostToken 明确告知系统:“该区域允许外部进程绘制”。

// 嵌入远程视图
private fun embedRemoteView() {
    if (!providerManager.isConnected()) {
        Toast.makeText(this, R.string.service_not_connected, Toast.LENGTH_SHORT).show()
        return
    }

    // 等待 SurfaceView 附加到 Window
    if (!binding.surfaceView.isAttachedToWindow) {
        binding.surfaceView.viewTreeObserver.addOnWindowAttachListener(
            object : android.view.ViewTreeObserver.OnWindowAttachListener {
                override fun onWindowAttached() {
                    binding.surfaceView.viewTreeObserver.removeOnWindowAttachListener(this)
                    performEmbed()
                }

                override fun onWindowDetached() {}
            }
        )
    } else {
        performEmbed()
    }
}

private fun performEmbed() {
    val width = EmbeddedViewConfig.VIEW_WIDTH_NORMAL
    val height = EmbeddedViewConfig.VIEW_HEIGHT_NORMAL
    // 设置 SurfaceView 为 onTop 模式,确保在其他 View 之上,否则可能无法透传点击事件
    binding.surfaceView.setZOrderOnTop(true)
    // 获取 hostToken
    val token = binding.surfaceView.hostToken ?: return

    providerManager.getSurfacePackageAsync(token, width, height) { surfacePackage ->
runOnUiThread {
if (surfacePackage != null) {
                binding.surfaceView.setChildSurfacePackage(surfacePackage)
                Log.d(TAG, "Embedded async: ${width}x$height")
                Toast.makeText(
                    this@HostActivity,
                    R.string.embedded_successfully,
                    Toast.LENGTH_SHORT
).show()
                isEmbedded = true
                isScaledDown = false
                updateButtonStates()
            } else {
                Log.e(TAG, "Failed to get SurfacePackage")
                Toast.makeText(
                    this@HostActivity,
                    R.string.embedded_failed,
                    Toast.LENGTH_SHORT
).show()
            }
        }
}
}

总结

SurfaceControlViewHost为车载 Android 跨进程 UI 融合提供了高效、原生的解决方案。它在保持进程隔离安全性的同时,实现了零拷贝的 GPU 硬件合成和原生触控事件分发,是实现多应用无缝融合、极致视觉一致性的关键技术。

以上就是本文的所有内容,希望对你有所帮助。

本项目源代码:gitcode.com/linkwj/Surf…

相关 API 文档:

(本篇文章使用 Gemini 3 Pro 辅助完成全文编写)