Skip to content

第 2 章:项目结构——259 个文件如何分成可读模块

源码树不是架构图,但命名、目录与构建清单会暴露真实边界。本章把 scrcpy 按“运行职责”重新分区,并给出从哪里下手的源码地图。

一、顶层目录的真实角色

路径职责是否进入运行时
app/桌面端 C 客户端、测试、平台适配、资源
server/Android Java 服务端、AIDL、单元测试
doc/官方用户功能文档
release/多平台编译、测试、打包、校验和构建时
config/Android checkstyle 配置构建时
gradle/Gradle wrapper构建时
assets/README 展示资源

顶层 meson.build 同时进入 appserver 子目录,因此发布物虽然跨 C/Java 两套工具链,仍由 Meson 作为总入口协调。

二、客户端:按数据方向分区

app/src 根目录看似“扁平”,实际上可按五个方向理解。

2.1 编排与配置

  • main.c:进程入口,终端、locale、日志、CLI 解析;
  • cli.c/.h:约 3,600 行选项定义与帮助文本;
  • options.c/.h:默认值、枚举和解析辅助;
  • scrcpy.c/.h:总装配、事件循环、逆序清理;
  • server.c/.h:选择设备、推送 jar、建 tunnel、启动设备端。

阅读原则:main.c 只回答“用户输入怎样变成 options”,scrcpy.c 才回答“系统怎样被组装”。进程入口最终调用 scrcpy(&args.opts),见 main.c

2.2 媒体接收与分流

  • demuxer:解析 codec id、session meta 与帧头;
  • packet_merger:为 H.264/H.265 把 config 包并入后续媒体包;
  • decoder:FFmpeg packet → frame;
  • trait/packet_*:压缩包层的 source/sink 接口;
  • trait/frame_*:解码帧层的 source/sink 接口;
  • recorder:压缩包封装进 MP4/MKV 等容器;
  • v4l2_sink:解码帧送往 Linux 虚拟摄像头。

trait 目录是客户端最小却最关键的抽象层。sc_packet_source_add_sink() 只保存实现了 ops 表的指针,见 packet_source.c。它相当于 C 语言手写的接口与依赖注入。

2.3 播放与 UI

  • screen:窗口、渲染、快捷键入口、坐标映射;
  • texture:把 FFmpeg 帧更新到 SDL 纹理;
  • frame_buffer:单槽最新帧缓冲;
  • video_regulator:按 PTS 调度视频帧;
  • audio_player:SDL 音频 stream;
  • audio_regulator:环形缓冲、静音填充、重采样补偿;
  • opengl:查询与渲染能力辅助。

这里有两个容易混淆的“regulator”:视频 regulator 决定什么时候交付帧,音频 regulator 决定输出多少采样。前者调度,后者通过重采样微调速率。

2.4 输入与控制

  • input_manager:把 SDL 事件翻译成内部输入语义;
  • keyboard_sdk / mouse_sdk:生成 Android 控制消息;
  • controller:有界队列、序列化、发送线程与反向 receiver;
  • control_msg / device_msg:双向协议编解码;
  • hid/:HID report 描述与跨后端共享的状态机;
  • uhid/:经控制 socket 在设备端创建 Linux UHID;
  • usb/:通过 libusb 使用 Android Open Accessory HID;
  • trait/*_processor.h:键盘、鼠标、手柄策略接口。

控制路径最容易因目录分散而失焦。要始终按 SDL → input_manager → processor → controller/USB → Android 追踪。

2.5 基础设施与平台层

  • adb/:命令执行、设备列表解析、reverse/forward;
  • util/:线程、锁、socket、进程、字符串、容器、时钟、中断;
  • sys/unixsys/win:文件与子进程差异;
  • android/:在 C 端复制必要的 Android keycode/input 常量。

util 不是普通“杂项”。scrcpy 不引入大型平台运行库,所以可中断 socket、跨平台进程、条件变量和向量容器都在这里实现。第 13 章会重点读 intrprocess_observer

三、服务端:按 Android 能力分区

server/src/main/java/com/genymobile/scrcpy 的层次更清楚:

核心职责代表对象
根包入口、参数、清理、兼容补丁Server, Options, CleanUp, Workarounds
device连接、封帧、设备操作DesktopConnection, Streamer, Device
video三种采集源、约束、编码、重置SurfaceCapture, SurfaceEncoder
audio采集源、PCM 读取、编码或直传AudioCapture, AudioEncoder
control消息解析、输入注入、剪贴板、UHIDController, ControlMessageReader
display显示信息、属性跟踪、resize debounceDisplayPropertiesTracker
wrappers反射访问 Android 隐藏系统服务ServiceManager, InputManager
model值对象与 codec 配置Size, Position, CodecOption
opengl角度/仿射变换的 GPU 过滤OpenGLRunner, AffineOpenGLFilter
utilIO、二进制、命令、日志、设置IO, Binary, Settings

服务端入口 Server.scrcpy() 只做对象选择和并发启动,具体能力沿包边界下沉。

四、三组“成对文件”

跨进程系统最有效的阅读法,是寻找协议两端的成对实现:

桌面端Android 端契约
server.cDesktopConnection.javasocket 名称、建立方向与顺序
demuxer.cStreamer.javacodec id、session/packet 12 字节头
control_msg.cControlMessageReader.java桌面到设备控制消息
device_msg.cDeviceMessageWriter.java设备到桌面反向消息
options.h / server.cOptions.java服务端参数名、默认值和类型
uhid/*.cUhidManager.javaHID 描述、输入与 output report

任何协议改动若只改一端,编译通常仍能通过,运行时才会错位。因此这些成对文件就是事实上的协议 schema。

五、构建清单也是架构证据

客户端 app/meson.build 明确列出核心 .c 文件,并按平台条件添加:

  • Linux 且启用 v4l2 才编译 v4l2_sink.c
  • 启用 usb 才编译 AOA 与 libusb 路径;
  • Windows 使用 sys/win 并链接 ws2_32
  • Unix 使用 sys/unix
  • 基础依赖始终是 FFmpeg 四个库与 SDL 3。

服务端则由 Gradle 产出一个 APK 结构的构建结果,再被当作 scrcpy-server 推送;Meson 的 server/meson.build 允许使用预构建 server,从而在没有 Android SDK 时只构建客户端。

六、从大文件进入,还是从小接口进入

最大的几个文件是 cli.cscreen.cserver.cinput_manager.cscrcpy.c 和服务端 Controller.java。直接逐行读很容易迷失。更好的顺序是:

  1. 先读小接口:packet_sink.hframe_sink.h、三个 processor;
  2. 再读总装配:scrcpy.cServer.java
  3. 按一条数据流进入大文件的相关分支;
  4. 最后回看失败路径与清理标签。

举例:读 screen.c 前先知道它既是 frame_sink,又拥有 input_manager,于是“视频消费”和“输入生产”这两个看似不相干的职责就有了窗口这一共同边界。

七、本章小结

源码结构可以压缩成一句话:客户端以显式装配 + C 接口表组织多种数据消费者,服务端以能力包 + 抽象采集源组织 Android 功能,两端靠六组成对文件维持协议。下一章开始运行程序,观察所有组件以什么顺序活起来,又如何安全死去。

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