☰
Android后台截屏改造:修改screencap源码绕过SurfaceFlinger限制
2026/10/17 12:35:11 网站建设 项目流程

1. 从一次自动化测试需求说起:为什么要动 screencap 的源码

做过 Android 自动化测试或者远程协助类工具的朋友,大概率都遇到过这样一个场景:应用在后台运行,或者屏幕处于锁屏、息屏状态,但业务上就是需要拿到当前屏幕的画面。这时候你打开终端敲一句adb shell screencap -p /sdcard/shot.png,十有八九会得到一个全黑或者纯色的图片,甚至直接报错退出。

这个现象背后的原因并不复杂。Android 原生的screencap命令走的是 SurfaceFlinger 的截图通道,它默认只抓取当前处于前台、可见且未被安全策略拦截的图层。一旦应用退到后台,或者系统判定当前界面涉及敏感内容(比如支付页面、密码输入框),截图结果就会被系统"保护"掉。对于做设备管理、远程运维、自动化测试框架的开发者来说,这个限制直接卡住了核心功能。

我最初接触这个需求,是在给一个自动化测试平台做设备端采集模块的时候。测试用例需要在应用切到后台之后,验证某些 UI 状态是否被正确保存,而验证手段就是对比前后两张屏幕截图。原生screencap完全满足不了,于是只能从 AOSP 源码里把screencap.cpp拎出来,改造成一个可以在后台稳定抓屏的版本。

这篇文章就把我改造screencap.cpp的完整思路、关键代码改动、编译集成方式,以及实测中踩过的坑,一次性讲清楚。适合有一定 Android 系统开发基础、需要做后台截屏或者系统级画面采集的读者。如果你只是想在普通应用里截个图,那用MediaProjectionAPI 就够了,不需要往下看;但如果你面对的是系统级、后台、无界面交互的场景,那这篇内容应该能帮你省下不少试错时间。

2. 原生 screencap 的工作链路与后台失效的根因

2.1 screencap 到底调用了什么

要改造一个东西,先得搞清楚它原本是怎么跑的。screencap.cpp位于 AOSP 的frameworks/base/cmds/screencap/目录下,它的核心逻辑其实非常薄,主要就是三件事:解析命令行参数、调用ScreenshotClient或者直接和 SurfaceFlinger 通信、把拿到的ScreenCapture数据编码成 PNG 写到文件或标准输出。

在较新的 Android 版本里,screencap底层依赖的是ISurfaceComposer::captureDisplay这个 binder 接口。SurfaceFlinger 在收到请求后,会遍历当前所有可见的图层,按照 Z 序合成一张位图返回。注意这里的关键词是"可见的图层"——这就是后台截图失效的第一层原因。

2.2 后台失效的三层原因

我把实测中遇到的后台截图失败原因归纳为三类,理解这三层,后面的改造方向就清晰了。

第一层是图层可见性过滤。SurfaceFlinger 在合成截图时,会跳过那些被标记为不可见或者被其他窗口完全遮挡的图层。应用退到后台后,它的主窗口通常会被设为不可见,自然就抓不到内容。

第二层是安全图层拦截。从 Android 5.0 开始,系统引入了FLAG_SECURE这个窗口标志。一旦某个窗口设置了它,SurfaceFlinger 在截图时会直接把对应区域涂黑。这是系统级的安全机制,防止恶意应用窃取敏感信息。

第三层是权限与 SELinux 限制。screencap命令本身需要READ_FRAME_BUFFER权限,在普通应用进程里是拿不到的。即使你通过Runtime.exec去调它,也会因为 SELinux 策略被拒绝。只有系统签名或者 root 权限的进程才能正常执行。

注意:第三层限制是硬性的,如果你的应用没有系统权限,改screencap.cpp也解决不了问题。改造的前提是你的程序运行在系统进程或者有 root 的环境里。

2.3 改造的核心思路

既然原生逻辑卡在"只抓可见图层"上,那改造方向就很明确了:绕过可见性判断,直接向 SurfaceFlinger 请求完整的显示缓冲区内容。具体来说,就是把captureDisplay的调用参数做调整,或者在更底层直接读取 framebuffer 设备节点。

我最终采用的是修改captureDisplay调用参数 + 增加后台重试机制的组合方案。前者解决"抓不到"的问题,后者解决"抓不稳"的问题。下面章节会分别展开。

3. 改造 screencap.cpp 的关键代码改动

3.1 定位需要修改的函数入口

打开screencap.cpp,你会看到主函数main里根据参数分支调用不同的截图逻辑。核心的截图函数通常叫captureDisplay或者screenshot,它内部会构造一个DisplayCaptureArgs结构体,然后传给SurfaceComposerClient。

我改造时重点关注的是DisplayCaptureArgs里的几个字段:displayToken、width、height、useIdentityTransform,以及最关键的captureArgs中的layerStack和uid。原生代码通常把uid设成调用者的 uid,这会导致 SurfaceFlinger 按照该 uid 的可见性策略来过滤图层。

3.2 调整 captureDisplay 的调用参数

我的做法是把uid强制设为 0(也就是 root 的 uid),同时把layerStack设为HWC_DISPLAY_PRIMARY对应的值。这样 SurfaceFlinger 在合成时就不会按照普通应用的可见性规则来过滤,而是以系统级视角抓取整个主显示器的内容。

代码改动大致如下(基于 Android 10 的 AOSP 源码结构):

// 改造前 DisplayCaptureArgs captureArgs; captureArgs.displayToken = displayToken; captureArgs.width = width; captureArgs.height = height; captureArgs.useIdentityTransform = true; // 改造后 DisplayCaptureArgs captureArgs; captureArgs.displayToken = displayToken; captureArgs.width = width; captureArgs.height = height; captureArgs.useIdentityTransform = true; captureArgs.uid = 0; // 关键:以系统 uid 抓取,绕过可见性过滤 captureArgs.layerStack = 0; // 主显示层栈

这里要说明一下uid = 0的作用。SurfaceFlinger 在captureDisplay内部会调用getLayerStack和权限校验逻辑,当 uid 为 0 时,它会认为调用者是系统进程,从而跳过针对普通应用的图层可见性检查。这是整个改造中最关键的一行。

3.3 增加后台抓取的重试与超时机制

光改参数还不够。实测发现,当屏幕处于息屏状态时,SurfaceFlinger 可能返回一个空的 capture 结果,或者直接超时。这时候需要加一层重试逻辑。

我的做法是在调用captureDisplay之后,检查返回的ScreenCapture对象是否有效,如果无效就等待 100 毫秒后重试,最多重试 5 次。同时给整个截图操作加一个 2 秒的超时保护,避免在异常情况下卡死。

sp<ScreenCapture> screenCapture; status_t err; int retryCount = 0; const int maxRetries = 5; do { err = composer->captureDisplay(captureArgs, &screenCapture); if (err == NO_ERROR && screenCapture != nullptr && screenCapture->getWidth() > 0) { break; } usleep(100000); // 100ms retryCount++; } while (retryCount < maxRetries); if (err != NO_ERROR || screenCapture == nullptr) { fprintf(stderr, "captureDisplay failed after %d retries\n", maxRetries); return 1; }

这段重试逻辑看起来简单,但在实际场景里非常有用。特别是设备刚息屏或者刚切换应用的那一瞬间,SurfaceFlinger 的合成状态还没稳定,第一次抓取经常拿到空数据,重试一两次就能成功。

3.4 输出格式的兼容处理

原生screencap支持-p参数输出 PNG 格式,这个逻辑在改造后要保留。但后台抓取时,有时候拿到的ScreenCapture数据格式和前台不一致,需要做一层兼容。我的处理方式是统一走ScreenCapture::asBitmap()拿到SkBitmap,然后再用SkImageEncoder编码成 PNG。这样无论底层返回什么格式,输出都是标准的 PNG 文件。

4. 编译、集成与权限配置的完整流程

4.1 在 AOSP 源码树中编译

改造完代码之后,编译方式取决于你的环境。如果你有完整的 AOSP 源码树,直接在frameworks/base/cmds/screencap/目录下执行mm或者mmm命令即可。编译产物是一个名为screencap的可执行文件,位于out/target/product/<device>/system/bin/目录下。

如果你没有完整源码树,只有部分源码,那就需要手动写一个Android.mk或者Android.bp。我用的是Android.bp,内容大致如下:

cc_binary { name: "screencap_custom", srcs: ["screencap.cpp"], shared_libs: [ "libcutils", "libutils", "libbinder", "libui", "libgui", "libsurfaceflinger_client", "libskia", ], cflags: [ "-Wall", "-Werror", ], }

这里要注意libsurfaceflinger_client这个库,不同 Android 版本名字可能不一样,Android 10 之后有些接口被合并到了libgui里。编译报错找不到符号时,优先检查这个依赖。

4.2 推送到设备并设置权限

编译出来的screencap_custom需要推到设备的/system/bin/目录下。由于/system分区通常是只读的,你需要先adb remount或者用mount -o rw,remount /system重新挂载。

推送之后,权限设置是重中之重。可执行文件需要755权限,同时 SELinux 上下文要正确。我踩过的坑是:文件推上去了,权限也设了,但一执行就报Permission denied,查dmesg才发现是 SELinux 拦截。

adb push screencap_custom /system/bin/ adb shell chmod 755 /system/bin/screencap_custom adb shell chcon u:object_r:system_file:s0 /system/bin/screencap_custom

chcon这行是关键,把文件的 SELinux 上下文设成system_file,这样系统进程才能正常执行它。如果你的设备 SELinux 是 enforcing 模式,不做这一步基本必挂。

4.3 在系统服务中调用

如果你的截图逻辑是集成在系统服务里的,那更推荐直接用 binder 调用,而不是Runtime.exec去执行可执行文件。系统服务本身就有READ_FRAME_BUFFER权限,直接调SurfaceComposerClient的接口效率更高,也避免了 SELinux 的麻烦。

我在一个系统服务里集成时,用的是ScreenshotClient的封装类,把上面改造过的captureDisplay逻辑包成一个 AIDL 接口,供上层应用调用。这样上层应用不需要任何特殊权限,只要绑定这个系统服务就能拿到后台截图。

提示:系统服务方案的前提是你的应用和系统服务在同一个进程或者有 binder 通道。普通第三方应用无法直接绑定系统级服务,这条路只适合系统应用或者有系统签名的场景。

5. 实测中遇到的五个典型问题与排查过程

5.1 截图全黑:先查图层可见性再查安全标志

改造完成后第一次实测,拿到的图片是全黑的。我当时的排查顺序是:先确认captureDisplay返回值是否正常,再检查ScreenCapture的宽高是否为零,最后才怀疑到图层可见性上。

排查下来发现,虽然我把uid设成了 0,但目标应用的窗口设置了FLAG_SECURE。这种情况下,即使 uid 是 0,SurfaceFlinger 依然会把对应区域涂黑。这是系统级安全机制,无法通过改screencap.cpp绕过。

解决办法有两个:一是让目标应用去掉FLAG_SECURE(如果是自己开发的应用);二是在系统层面修改 SurfaceFlinger 的合成逻辑,但这涉及系统安全策略,不建议轻易动。

5.2 息屏状态下抓取超时

第二个问题是设备息屏后,captureDisplay调用会卡住,大约 5 秒后才返回超时错误。这是因为息屏时显示管线进入了低功耗状态,SurfaceFlinger 的合成线程可能被挂起。

我的处理方式是在截图前先通过PowerManager的wakeUp接口短暂唤醒屏幕,或者直接给显示管线发一个setPowerMode请求,把它从低功耗模式拉回来。当然,这需要DEVICE_POWER权限。如果权限不够,那就只能接受息屏时截图失败,在业务层做降级处理。

5.3 多显示器场景下的抓取错乱

在支持外接显示器的设备上,我发现抓到的画面有时候是副屏的内容,而不是主屏。原因是captureDisplay的displayToken参数没有指定明确。原生代码里这个 token 是从SurfaceComposerClient::getInternalDisplayToken()拿的,但在多显示器环境下,这个接口返回的可能是最后活跃的那个显示器。

修复方式是在调用前显式指定主显示器的 token,通过getPhysicalDisplayToken(PhysicalDisplayId)来获取。这个细节在单屏设备上完全不会暴露,但一到多屏场景就会出问题。

5.4 高频截图导致 SurfaceFlinger 压力过大

我的业务场景需要每秒抓 3 到 5 帧,连续跑几分钟后,设备开始明显发热,SurfaceFlinger 的 CPU 占用飙升到 40% 以上。原因是每次captureDisplay都会触发一次完整的图层合成,这个开销在后台持续跑是扛不住的。

优化手段是降低抓取频率,同时在两次抓取之间加一个短暂的休眠。另外,如果业务允许,可以把截图分辨率降下来,比如只抓取屏幕的 1/4 区域,这样合成开销会小很多。我在代码里加了一个downscale参数,把宽高各缩小一半,CPU 占用直接降到了 15% 左右。

5.5 不同 Android 版本的接口差异

从 Android 9 到 Android 13,captureDisplay的接口签名改了好几次。Android 10 引入了DisplayCaptureArgs结构体,Android 12 又把captureDisplay拆成了captureDisplay和captureLayers两个接口。这意味着同一份改造代码在不同版本上编译会报错。

我的应对策略是用条件编译,通过__ANDROID_API__宏来区分不同版本的代码路径。虽然麻烦,但这是跨版本兼容最稳妥的方式。如果只针对单一版本,那就简单很多,直接按那个版本的接口写就行。

问题现象根因解决方向
截图全黑FLAG_SECURE 或图层不可见检查安全标志,确认 uid 设置
息屏超时显示管线低功耗唤醒屏幕或降级处理
多屏错乱displayToken 不明确显式指定主屏 token
高频发热合成开销大降频、降分辨率
编译报错接口版本差异条件编译适配

6. 后台截图的替代方案与选型建议

6.1 MediaProjection:普通应用的首选

如果你的应用是普通第三方应用,没有系统权限,那MediaProjectionAPI 是唯一合规的路径。它通过用户授权的方式拿到屏幕采集权限,可以持续获取屏幕画面。缺点是必须弹出授权对话框,而且部分定制系统对后台采集有限制。

MediaProjection的优势在于不需要改系统源码,不需要 root,兼容性好。缺点是首次使用需要用户手动授权,而且授权状态在设备重启后会失效,需要重新授权。对于面向普通用户的应用,这是可以接受的;但对于无人值守的自动化场景,就不太合适了。

6.2 系统服务方案:系统应用的利器

如果你有系统签名,那直接在系统服务里集成改造后的截图逻辑是最优解。没有授权弹窗,没有 SELinux 拦截,性能也最好。我最终的生产方案就是走的这条路,把改造后的captureDisplay封装成一个 AIDL 服务,上层应用通过 binder 调用。

这个方案的代价是你需要维护一个系统服务,并且要处理服务生命周期、binder 死亡通知、并发调用等问题。代码量比单纯改screencap.cpp大不少,但稳定性和性能是最好的。

6.3 选型对比表

方案权限要求后台能力性能适用场景
原生 screencaproot/系统弱中调试、临时抓取
改造 screencaproot/系统强中系统工具、脚本
MediaProjection用户授权中中普通应用录屏
系统服务集成系统签名强高系统应用、自动化平台

选型时先问自己三个问题:你的应用有没有系统权限?需不需要在息屏或后台状态下抓取?对性能要求高不高?三个问题的答案基本就能定位到合适的方案。

7. 几个容易被忽略的细节与个人经验

改造screencap.cpp这件事,代码本身不算复杂,真正花时间的是环境配置和问题排查。我把自己踩过的几个细节单独拎出来说说。

第一个是编译时的头文件路径。AOSP 里screencap.cpp依赖的头文件散落在frameworks/native/include、system/core/include等多个目录,手动写Android.bp时很容易漏掉某个include_dirs,导致编译报找不到头文件。我的建议是直接从 AOSP 原有的Android.bp里复制依赖列表,再按需删减,比从零写要靠谱得多。

第二个是SELinux 策略的调试。遇到avc: denied的时候,不要急着去改策略文件,先用adb shell dmesg | grep avc看清楚是哪条规则被拒绝了。很多时候只是文件上下文不对,chcon一下就能解决,不需要动te文件。如果确实需要加策略,也要遵循最小权限原则,只开放必要的访问。

第三个是截图数据的生命周期。ScreenCapture对象持有的是 SurfaceFlinger 的共享内存引用,如果你在拿到数据后没有及时释放,会导致 SurfaceFlinger 的内存持续增长。我在代码里用sp<>智能指针管理,确保每次截图完成后引用计数归零。这个细节在低频调用时看不出来,高频跑几个小时就会暴露。

第四个是多线程并发调用。如果你的截图服务会被多个线程同时调用,一定要加锁。SurfaceFlinger 的captureDisplay接口本身不是线程安全的,并发调用会导致返回数据错乱甚至崩溃。我用的是一个简单的互斥锁,把整个截图流程串行化,虽然牺牲了一点并发性能,但稳定性提升明显。

最后分享一个实用技巧:在调试阶段,可以把每次截图的耗时和返回状态打到 logcat 里,用adb logcat -s ScreencapCustom过滤查看。这样一旦出现问题,能快速定位是超时、返回空数据还是权限被拒。这个日志我在生产环境也保留了,只是把级别降到了 debug,方便出问题时排查。

这套改造方案我在几个不同 Android 版本的设备上都跑过,核心逻辑是通用的,主要差异在接口签名和 SELinux 策略上。如果你也在做类似的后台采集需求,希望这些经验能帮你少走点弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询