Skip to content

第 14 章:Android 兼容层——没有 Activity 的系统能力访问

scrcpy-server 通过 app_process 以 shell 身份启动。它既不是普通 APK 进程,也不是 system_server,却要访问显示、输入、剪贴板、相机和 ActivityManager。兼容层正是为填补这个“非标准运行环境”而存在。

一、app_process 模式带来的空洞

普通 Android app 由 Zygote/ActivityThread 建立 Application、Context、package identity 与生命周期。scrcpy server 直接指定 classpath 和 main class:

text
CLASSPATH=/data/local/tmp/scrcpy-server.jar
app_process / com.genymobile.scrcpy.Server ...

它没有 Activity,也没有 manifest 驱动的 Application 初始化。很多公开 API 内部仍假设 ActivityThread.currentActivityThread()、system context 或 attribution source 存在,因此直接调用会在特定版本/厂商 ROM 崩溃。

二、Shell 身份是能力边界

server 通常以 ADB shell UID 2000 运行,拥有普通三方 app 没有的输入/显示相关权限。若进程意外以 root 启动,Server.dropRootPrivileges() 主动 setuid(2000),因为 root 身份反而会破坏剪贴板行为,见 Server.java

这说明权限不是单调的“越高越好”;Android AppOps、package identity 与 UID 组合决定行为。

三、Workarounds 人工补齐 ActivityThread

静态初始化通过反射:

  1. 构造 android.app.ActivityThread
  2. 写入 sCurrentActivityThread
  3. 标记 mSystemThread=true
  4. 按版本填 ConfigurationController
  5. 构造 ApplicationInfo 与 bound application;
  6. 填入 system context。

入口见 Workarounds.apply()。这些字段不是 scrcpy 业务状态,而是让 Android framework 内部看到一个“足够像系统进程”的宿主环境。

四、品牌 workaround 为什么不能一刀切

兼容补丁本身也可能伤害别的设备。例如 ONYX 设备上填充 app info 会破坏视频镜像,所以代码按 Build.BRAND 跳过该步骤。Samsung 的 DisplayManager 路径又可能要求 Android 12 引入的 ConfigurationController。

这类代码的设计原则是:

  • 尽量晚、尽量局部应用;
  • 用 API level/brand 精确守卫;
  • 保留 issue 链接说明真实设备证据;
  • 不把 workaround 扩散进核心编码/控制代码。

五、FakeContext 提供最小身份

FakeContext 继承 ContextWrapper,关键行为包括:

  • package/op package 固定为 com.android.shell
  • Android 12+ 提供 shell UID 的 AttributionSource
  • application context 返回自身;
  • package context 仍返回自身;
  • 自建 ContentResolver,经 ActivityManager 获取 provider;
  • 对 clipboard/activity 等 system service 替换内部 mContext

它不是完整 Context 模拟器,只实现 scrcpy 使用到的 framework 假设。

六、ServiceManager:集中获取 Binder 服务

wrappers.ServiceManager 反射调用隐藏的 android.os.ServiceManager.getService(name),再调用 AIDL Stub 的 asInterface。各 wrapper 延迟创建并缓存:WindowManager、DisplayManager、InputManager、PowerManager、StatusBarManager、ClipboardManager、ActivityManager 等。

集中入口见 ServiceManager.java。DisplayManager getter 特别加 synchronized,因为 controller 与 video 主线程都可能首次访问。

七、Wrapper 吸收签名漂移

Android 隐藏 API 会跨版本改方法名、参数或返回值。wrapper 的价值是让业务层只调用稳定方法:

  • 先尝试当前版本签名;
  • 按 API level 选择旧/新方法;
  • 反射设置 display id 或 action button;
  • 统一异常记录和回退;
  • 必要时调用自行复制的 AIDL 接口。

例如 InputManager 集中处理 injectInputEventInputEvent.setDisplayId 和 Android 15 unique id association。

八、为什么要复制 AIDL 和占位类

源码包含 android.* 包下的 AIDL/Java 声明,例如 IDisplayWindowListenerIOnPrimaryClipChangedListener。它们不是重写 Android 实现,而是给编译器一个与系统 Binder ABI 对齐的接口形状;运行时真正对象来自系统服务。

这是一种“编译时 shim,运行时系统实现”的方式。风险在于隐藏 ABI 变化,因此相关 wrapper 必须按 Android 版本适配并在真机验证。

九、API level 约束应尽早失败

项目最低 API 是 21,但各能力有更高门槛:

  • playback audio 依赖较新 Android;
  • camera mirroring 要 Android 12;
  • new virtual display 与 IME policy 要 Android 10;
  • MediaCodec format 的 color range/priority/latency 按 API 守卫;
  • Android 15 才使用 input/display unique id association。

AndroidVersions 用带系统版本名的常量替代裸数字,见 AndroidVersions.java。总入口在创建资源前检查非法组合,给用户明确配置错误,而非让深层 reflection 崩溃。

十、CleanUp 为什么是独立线程

运行期间可能修改设备状态:show touches、stay awake、screen-off timeout、显示电源等。正常退出要恢复;客户端暴力断开或设备端发生异常时也应尽力恢复。

CleanUp 在独立线程监控配置并持有恢复责任,server finally 中先 interrupt 再 join。它还负责按选项删除 server jar。代码入口见 CleanUp.start()

把清理放到独立对象,而不是散落各功能 finally,使状态修改与恢复配对可审计。

十一、适配新 Android 版本的路线

  1. 先复现具体能力:显示、输入、剪贴板、相机还是 Activity 启动;
  2. 在 wrapper 边界检查系统签名变化,不先改业务层;
  3. 用 API level 守卫新路径,保留旧路径;
  4. 若是厂商差异,用最窄 brand/model 条件;
  5. 为纯解析/转换逻辑加 JVM test;
  6. 隐藏服务必须真机覆盖多个 API/厂商;
  7. 检查 shell identity/AppOps,而非只看 manifest 权限。

十二、本章小结

scrcpy 的 Android 能力来自 shell 身份与隐藏系统 API,可靠性来自把不稳定性集中在 Workarounds + FakeContext + wrappers。核心媒体与控制代码因此不必充满反射分支。最后一章回到工程外壳:两套构建系统如何产出一对锁步版本的发布物,测试又重点保护哪些边界。

基于 scrcpy 4.1 源码的独立学习笔记