Skip to content

第 3 章:启动与生命周期——从 main() 到事件循环

实时系统最难的往往不是“开始工作”,而是部分初始化失败和对端断开时仍能完整退出。scrcpy 把这两件事放在同一条显式生命周期主线上。

一、桌面进程入口做了什么

main.c 的职责被刻意限制为进程级准备:

  1. 记录主线程身份;
  2. 在 Windows 设置 UTF-8 输出、必要时重建标准输出;
  3. 初始化 locale 与 socket 网络层;
  4. 解析 CLI 为 scrcpy_cli_args
  5. 处理 --help--version--list-* 等早退分支;
  6. 配置日志级别;
  7. 调用 scrcpy(&args.opts)
  8. 销毁 CLI 动态数据与网络层。

这里不启动 ADB、不创建窗口,也不碰 FFmpeg。这样 CLI 解析可以在无设备环境中单元测试,进程级清理也不会与运行组件混在一起。

二、struct scrcpy 是组合根

scrcpy() 使用一个静态 struct scrcpy 容纳 server、screen、demuxer、decoder、recorder、controller、file pusher、USB、UHID 与 timeout 等对象,见 scrcpy.c

它扮演“组合根”(composition root):

  • 决定有哪些组件;
  • 把 source 与 sink 接起来;
  • 选择 SDK/UHID/AOA 的具体实现;
  • 决定启动顺序;
  • 持有清理所需的状态。

这让依赖图不依赖服务定位器。代价是 scrcpy() 超过 700 行,但系统拓扑因此集中可见。

三、第一阶段:只启动 server 线程

scrcpy() 先只初始化 SDL events,生成 31 位随机 scid,把客户端 options 映射为 sc_server_params,然后调用 sc_server_start(),见 scrcpy() 320–448 行

为什么不立刻初始化窗口和解码器?因为在知道设备是否选中、server 是否成功启动、哪些 socket 是否连通之前,这些资源没有输入。先建链可把失败面压缩在 server 线程内。

主线程并非阻塞在普通 join 上,而是在 await_for_server() 中等待 SDL 事件:

  • SC_EVENT_SERVER_CONNECTED:继续装配;
  • SC_EVENT_SERVER_CONNECTION_FAILED:失败退出;
  • SDL_EVENT_QUIT:用户在等待时取消。

因此即使 ADB 较慢,Ctrl+C/窗口退出仍能被处理。事件等待逻辑见 await_for_server()

四、server 线程的启动状态机

run_server() 可整理为:

adb start-server → 选择设备 / 解析 ANDROID_SERIAL → 可选切换或连接 TCP/IP → adb push scrcpy-server → 创建唯一 socket 名(scrcpy_ + scid) → 建立 adb reverse,失败则回退 forward → adb shell app_process 启动 Java Server → 观察设备端进程 → 接受/连接 video、audio、control socket → 通知主线程 connected → 等待 stop 条件

scid 同时解决两件事:让多个 scrcpy 实例不争用同一个 socket 名;让 ADB tunnel 能准确指向这一次 server。它被格式化为 8 位十六进制,并在两端生成 scrcpy_<scid> 名称。

五、第二阶段:按能力装配客户端图

连接成功后,scrcpy() 不是固定创建所有组件,而是按 options 计算需要:

5.1 packet 层

  • video=true 才创建 video demuxer;
  • audio=true 才创建 audio demuxer;
  • playback 或 V4L2 需要 decoder;
  • record 直接作为 demuxer 的 packet sink;
  • decoder 也作为 packet sink。

关键装配代码在 scrcpy.c 532–589 行。这使以下组合自然成立:

选项组合packet sink是否解码
普通播放decoder
只录制recorder
播放 + 录制decoder + recorder是,但录像用原包
V4L2decoder

5.2 frame 层

decoder 创建后,screen、audio regulator/player 或 V4L2 被挂成 frame sink。frame 的生命周期由 decoder 推进,UI 使用单帧缓冲跨线程接收。

5.3 控制层

control=true 时创建 controller,再按选项实例化一个键盘、鼠标和手柄 processor。scrcpy.c 使用 kp/mp/gp 三个接口指针隐藏具体实现,见 输入后端选择段

六、Android 端如何启动任务

设备端 main() 会:设置未捕获异常处理器、必要时从 root 降到 shell UID、准备可退出的主 Looper、解析 options、关闭标准流干扰并初始化日志,见 Server.internalMain()

进入 Server.scrcpy() 后:

  1. 校验 Android 版本与能力组合;
  2. 可选启动 CleanUp 守护线程;
  3. 应用设备兼容 workaround;
  4. 打开 DesktopConnection
  5. 按 control/audio/video 创建 AsyncProcessor
  6. 给每个 processor 注册统一终止回调;
  7. 启动后进入主 Looper.loop()
  8. 任一 fatal processor 结束就安全退出 Looper;
  9. finally 中 stop → shutdown socket → join → close。

这里的 Completion 既是计数器也是故障汇聚器:正常情况下等所有 processor 完成;任何 fatal error 可提前结束主 Looper。

七、第三阶段:进入 SDL 事件循环

客户端所有 producer 启动后进入 event_loop()。它只识别少量系统级事件:

事件来源结果
device disconnecteddemuxer/controller返回专用断开码
demuxer error音视频接收线程失败
controller error控制发送/接收线程失败
recorder error录制线程失败
AOA open errorUSB 线程失败
time limit reachedtimeout 线程成功退出
SDL quit用户成功退出
其他 SDL 事件窗口/输入交给 screen

线程不直接结束主流程,而是投递事件给主线程。这避免跨线程销毁 SDL 对象,也使退出原因集中映射为进程返回码。

八、失败回滚为什么用布尔标记

scrcpy() 为几乎每个资源维护 initialized / started 标记。C 没有 RAII,且任一步都可能失败;标记使单一 end: 区域能安全判断:

  • 未初始化对象不能 destroy;
  • 已初始化但未 start 的对象不应 join;
  • 已 start 的 producer 必须先 stop,再 join;
  • sink 的销毁必须晚于最后一个可能 push 的 producer。

例如源码明确要求等 video demuxer 完成后才能销毁 screen,否则 screen 可能在销毁后收到新帧,见 scrcpy.c 清理段

九、退出是启动的逆拓扑,不只是逆序

真实清理顺序考虑的是“谁还可能调用谁”:

  1. 停止 timeout、controller、recorder 等主动线程;
  2. 请求 server 停止并中断 socket,让阻塞 IO 醒来;
  3. join demuxer,保证不再向 packet/frame sink 推送;
  4. join USB/AOA;
  5. 此后才销毁 screen;
  6. join/destroy controller、recorder、file pusher;
  7. join server 线程;
  8. 最终关闭 socket、ADB 和同步原语。

所以退出并非简单把初始化列表倒过来,而是对依赖图做反向拓扑排序。

十、设备端的最后保险

关闭 socket 通常会让 Android 端阻塞调用返回。但某些睡眠设备不会及时唤醒,因此客户端等待 1 秒后会主动终止设备端进程,见 server.c watchdog

服务端 main()finally 又会显式 System.exit(status),因为 Android SDK 内部可能遗留非 daemon 线程,见 Server.main()。两端都不把“线程最终会自己退出”当作可靠假设。

十一、本章小结

scrcpy 的生命周期设计可以概括为四点:分阶段装配、主线程事件汇聚、显式资源状态、依赖驱动的逆拓扑清理。下一章深入第一阶段内部:ADB 如何既负责部署,又被改造成三个 socket 的透明隧道。

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