前言

本文仅供学习探讨之用,如果侵犯了您的权益请联系我删除。

AndroidSurfaceImgui

AndroidSurfaceImgui 是一个在 Android 上的纯Native方案实现使用 Surface(同 SurfaceView) 来绘制 Dear ImGui 的库,目前支持Android5 ~ Android16。

后续为了方便称为 AImGui。

最终效果

最终效果

起因

在 AImGui 中,用来创建 Surface 的核心部件是 ANativeWindowCreator.h,其原理是调用libgui.so的 API SurfaceComposerClient::CreateSurface来实现的。

在实际应用的时候,发现从 Android14 开始,使用 ANativeWindowCreator.h 创建的 Surface 无法被 scrcpy 之类的录屏软件录制到(包含但不限于系统录屏)。

具体表现为 Surface 能在物理设备上正常显示,但录屏软件无法捕捉到 Surface 以及 Surface 中的任何内容。

原因分析

基础介绍

scrcpy 和常见的录屏软件(MediaProject)的原理基本都是通过创建一个虚拟的显示屏幕 Virtual Display,然后给这个虚拟屏设置一个 Surface 作为 Buffer 并通过事务提交到系统。

这样每当有内容需要显示的时候,系统就会将内容绘制到这个虚拟屏上(实际就是 Surface 上),然后通过异步的方式去读出这个 Buffer 的每一帧数据,再将帧数据进行媒体流编码,保存成视频文件或者直接网络推流。


AImGui 创建的 Surface 的底层是 SurfaceFlinger 中的 Layer,创建 Surface 的本质就是在 SurfaceFlinger 中创建一个 Layer,然后通过 Binder 机制将 Layerhandle 传递给应用层,应用层再通过 handle 来操作 Layer

// Reference to the SurfaceFlinger layer that was used to create this
// surface. This is only populated when the Surface is created from
// a BlastBufferQueue.
sp<IBinder> mSurfaceControlHandle;

摸索

一开始我认为是 SurfaceFlinger 在 Android14+ 上做了什么 Layer 权限相关的改动,于是开始分析 Android14 SurfaceFlinger 的源码,尝试分析是合成链上的哪里出了问题。

最终发现在 Output::ensureOutputLayerIfVisible 中,我们创建的 Layer 被过滤掉了,并没有出现在虚拟屏的合成链上。

// Only consider the layers on this output
if (!includesLayer(layerFE)) {
    return;
}

于是我尝试绕过限制,直接修改此处的代码,当判断是我们自己的 Layer 时,继续执行后续步骤,然后编译 SurfaceFlinger,并进行验证。

然而我们的 Layer 并没有出现在虚拟屏幕上,于是又经过了一通修改测试,发现在能想到的路径上都是存在我们的 Layer 的,说明确实参与了合成 Output::composeSurfaces,但是没显示出来不知道为啥。

于是我又将除了我自己的 Layer 之外的所有 Layer 都过滤掉,然后虚拟屏上终于是看到了点希望

希望

但是发现 Layer 只会渲染一次,后续不会再更新,像是读取了初次渲染的缓存。这说明在合成链上还有其他地方对 Layer 进行了判断并将其过滤掉。

此外,当我释放一个原本渲染目标属于虚拟屏的 Layer(例如壁纸 Layer)时,我们的 Layer 会被覆盖掉。即使强行设置 Layer 的 Z 序为 0x7fffffff 并取消各种奇怪的标志位(flags),问题依然存在。


在继续追踪 Layer 的过滤逻辑时,我尝试分析 Output::composeSurfaces 前后的调用链,试图找到更多的线索。然而,经过一天的测试和头脑风暴后,依然没有找到明确的解决方案。

分析01

曙光

细品 dumpsys display 的 dump 信息后,发现 scrcpy 的 Virtual DisplaymCurrentLayerStack 居然和物理屏的 mCurrentLayerStack 不一样。

Display Devices: size=2
    DisplayDeviceInfo{"Built-in Screen": uniqueId="local:4619827259835644672", ...
        mAdapter=LocalDisplayAdapter
        mUniqueId=local:4619827259835644672
        mDisplayToken=android.os.BinderProxy@7f7d2e9
 ===>>  mCurrentLayerStack=0
        mCurrentFlags=1
        mCurrentOrientation=0
        ...

    DisplayDeviceInfo{"scrcpy": uniqueId="virtual:com.android.shell,2000,scrcpy,0", ...
        mAdapter=VirtualDisplayAdapter
        mUniqueId=virtual:com.android.shell,2000,scrcpy,0
        mDisplayToken=android.os.BinderProxy@ffdfb6e
 ===>>  mCurrentLayerStack=2
        mCurrentFlags=0
        mCurrentOrientation=0
        ...

LayerStack 是 SurfaceFlinger 中用来管理 Layer 的一个重要属性,决定了 Layer 属于哪个显示屏。Layer 只有在 LayerStack 匹配时才会被渲染到对应的显示屏上。

在 Android14 之前,可以通过 SurfaceControl.setDisplayLayerStack 来将虚拟屏的 LayerStack 设置为和物理屏一样的 LayerStack,从而让虚拟屏和物理屏共用一个合成链。事实上 scrcpy 也是这么做的。

然而在 Android14+ 上,这个做法至少在 dumpsys display 中显示的结果中表明为无效的,虚拟屏会被强制分配一个新的独立的 LayerStack


得知这一细节后,我尝试在创建自己的 Layer 时,将其 LayerStack 设置为与 scrcpy 的 Virtual DisplayLayerStack 一致。

曙光

如图所示,这样的修改确实解决了问题。我们的 Layer 成功出现在了 scrcpy 的录屏画面中,并且能够正常更新。

黎明

显而易见,在修改 LayerStack 后,虽然能正确显示在虚拟屏上,但是物理屏上又没有了。

为了实现既能在物理屏显示又能在虚拟屏显示的效果,又去研究了下状态栏、壁纸、导航栏这些系统 Layer 能被同时显示的原因。发现它们是通过 SurfaceFlingerMirror 功能,将物理屏的内容镜像到虚拟屏上从而实现的。

解决方案

镜像方案

  1. 镜像 Layer:当检查到有新的 LayerStack 创建(出现)时,使用 SurfaceComposerClient::MirrorSurface 对我们自已创建的 Layer 进行镜像。
  2. 创建宿主 Layer:创建一个新的空白 Layer,使用 Transaction::SetLayerStack 绑定到虚拟显示屏对应的 LayerStack。
  3. 重挂 Layer:使用 Transaction::Reparent 将镜像 Layer 挂到宿主 Layer 下。
  4. 提交事务:通过 .Show().Apply() 应用此次变动。

这个策略确保了 Layer 能够稳定进入虚拟显示屏的合成链,解决了 Android 14 及以上版本的合成限制问题。

虚拟显示屏监听挑战

实现中另一个挑战是:如何优雅、实时地监听虚拟显示屏的创建/销毁。

尝试过的方法

  • SurfaceComposerClient::getPhysicalDisplayIds:仅返回物理显示屏。
  • popen("dumpsys display", "r"):轮询解析系统命令,虽然有效,但不够优雅。

可行方案总结

方案 是否实时 是否优雅 是否纯 Native
Java DisplayManager.registerDisplayListener + JNI 回调 ✅ 实时 ✅ 优雅 ❌(需 Java)
定时轮询 dumpsys display ❌ 轮询 ⚠️ 一般

暂时没找到更好的方案,最终只能采用轮询的方式,定时调用 dumpsys display 来解析虚拟显示屏的 LayerStack,然后进行镜像操作。

不使用 Java 的原因主要是因为本项目的主旨是纯 Native 实现,不依赖 Java。

总结

其实第一次在 Output::ensureOutputLayerIfVisible 发现 Layer 被过滤掉的时候,我就应该反应过来是 LayerStack 的原因,只能说是经验不足,没有及时想到这个方向。

不过好在最后也是”踏破铁鞋无觅处”,在 dumpsys display 中找到了线索,最终解决了问题。

轮询调用 dumpsys display 的方案其实让我很难受,但是暂时也没找到更好的方案,只能后续再想办法优化吧。

所有代码更改已同步至 AndroidSurfaceImgui,欢迎各位大佬一起交流学习。

结语

也是终于有空处理这些东西了。

那就这样了,有缘再见~