第 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:
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
静态初始化通过反射:
- 构造
android.app.ActivityThread; - 写入
sCurrentActivityThread; - 标记
mSystemThread=true; - 按版本填
ConfigurationController; - 构造
ApplicationInfo与 bound application; - 填入 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 集中处理 injectInputEvent、InputEvent.setDisplayId 和 Android 15 unique id association。
八、为什么要复制 AIDL 和占位类
源码包含 android.* 包下的 AIDL/Java 声明,例如 IDisplayWindowListener 与 IOnPrimaryClipChangedListener。它们不是重写 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 版本的路线
- 先复现具体能力:显示、输入、剪贴板、相机还是 Activity 启动;
- 在 wrapper 边界检查系统签名变化,不先改业务层;
- 用 API level 守卫新路径,保留旧路径;
- 若是厂商差异,用最窄 brand/model 条件;
- 为纯解析/转换逻辑加 JVM test;
- 隐藏服务必须真机覆盖多个 API/厂商;
- 检查 shell identity/AppOps,而非只看 manifest 权限。
十二、本章小结
scrcpy 的 Android 能力来自 shell 身份与隐藏系统 API,可靠性来自把不稳定性集中在 Workarounds + FakeContext + wrappers。核心媒体与控制代码因此不必充满反射分支。最后一章回到工程外壳:两套构建系统如何产出一对锁步版本的发布物,测试又重点保护哪些边界。