第 2 章:项目结构——259 个文件如何分成可读模块
源码树不是架构图,但命名、目录与构建清单会暴露真实边界。本章把 scrcpy 按“运行职责”重新分区,并给出从哪里下手的源码地图。
一、顶层目录的真实角色
| 路径 | 职责 | 是否进入运行时 |
|---|---|---|
app/ | 桌面端 C 客户端、测试、平台适配、资源 | 是 |
server/ | Android Java 服务端、AIDL、单元测试 | 是 |
doc/ | 官方用户功能文档 | 否 |
release/ | 多平台编译、测试、打包、校验和 | 构建时 |
config/ | Android checkstyle 配置 | 构建时 |
gradle/ | Gradle wrapper | 构建时 |
assets/ | README 展示资源 | 否 |
顶层 meson.build 同时进入 app 和 server 子目录,因此发布物虽然跨 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/unix与sys/win:文件与子进程差异;android/:在 C 端复制必要的 Android keycode/input 常量。
util 不是普通“杂项”。scrcpy 不引入大型平台运行库,所以可中断 socket、跨平台进程、条件变量和向量容器都在这里实现。第 13 章会重点读 intr 与 process_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 | 消息解析、输入注入、剪贴板、UHID | Controller, ControlMessageReader |
display | 显示信息、属性跟踪、resize debounce | DisplayPropertiesTracker |
wrappers | 反射访问 Android 隐藏系统服务 | ServiceManager, InputManager |
model | 值对象与 codec 配置 | Size, Position, CodecOption |
opengl | 角度/仿射变换的 GPU 过滤 | OpenGLRunner, AffineOpenGLFilter |
util | IO、二进制、命令、日志、设置 | IO, Binary, Settings |
服务端入口 Server.scrcpy() 只做对象选择和并发启动,具体能力沿包边界下沉。
四、三组“成对文件”
跨进程系统最有效的阅读法,是寻找协议两端的成对实现:
| 桌面端 | Android 端 | 契约 |
|---|---|---|
server.c | DesktopConnection.java | socket 名称、建立方向与顺序 |
demuxer.c | Streamer.java | codec id、session/packet 12 字节头 |
control_msg.c | ControlMessageReader.java | 桌面到设备控制消息 |
device_msg.c | DeviceMessageWriter.java | 设备到桌面反向消息 |
options.h / server.c | Options.java | 服务端参数名、默认值和类型 |
uhid/*.c | UhidManager.java | HID 描述、输入与 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.c、screen.c、server.c、input_manager.c、scrcpy.c 和服务端 Controller.java。直接逐行读很容易迷失。更好的顺序是:
- 先读小接口:
packet_sink.h、frame_sink.h、三个 processor; - 再读总装配:
scrcpy.c与Server.java; - 按一条数据流进入大文件的相关分支;
- 最后回看失败路径与清理标签。
举例:读 screen.c 前先知道它既是 frame_sink,又拥有 input_manager,于是“视频消费”和“输入生产”这两个看似不相干的职责就有了窗口这一共同边界。
七、本章小结
源码结构可以压缩成一句话:客户端以显式装配 + C 接口表组织多种数据消费者,服务端以能力包 + 抽象采集源组织 Android 功能,两端靠六组成对文件维持协议。下一章开始运行程序,观察所有组件以什么顺序活起来,又如何安全死去。