如何使用这套文档
这不是 API 手册,也不是“每个目录一句话”的索引。它的目标是帮助你建立一个能预测代码位置和运行行为的模型:看到一个现象时,你大致知道状态属于谁、事件从哪里来、谁负责持久化,以及在哪一层下断点最划算。
每章的固定结构
每章都尽量回答四层问题:
- 是什么:先给对象关系和职责边界;
- 怎么跑:用真实调用链串起关键函数;
- 为什么:解释一致性、生命周期、性能或跨平台方面的取舍;
- 怎么验证:给出断点、日志与不变量,避免停在“看懂代码”的错觉里。
源码链接都指向上游 v7.0.6 tag。正文使用 Telegram/SourceFiles/... 作为路径起点;lib_rpl、lib_base 等子模块在当前源码包中可能只保留子模块入口,因此相关章节以它们在主项目中的调用方式为重点。
三条推荐路线
路线 A:第一次接触 tdesktop
按 01 → 03 → 04 → 05 → 06 → 07 → 08 → 09 阅读。你会先建立所有权树,再把请求、更新和界面串起来。不要一上来钻进 history_widget.cpp:它超过一万行,但只是许多状态汇合后的显示层。
路线 B:带着真实问题
先把现象归类:
| 现象 | 第一站 | 第二站 |
|---|---|---|
| 启动卡住、重复进程、账号未恢复 | 第 3 章 | 第 11 章 |
| 请求没有回调、DC/代理异常 | 第 4 章 | 第 3 章 |
| 消息漏了、顺序错了、重复了 | 第 5 章 | 第 7 章 |
| 点击发送后本地状态不一致 | 第 8 章 | 第 5 章 |
| 页面跳转或订阅泄漏 | 第 9 章 | 第 6 章 |
| 下载慢、进度停住、缓存失效 | 第 10 章 | 第 11 章 |
| 通话状态和 UI 不一致 | 第 12 章 | 第 9 章 |
路线 C:准备改代码
先读第 2 章确认生成文件与手写文件边界;再读目标子系统章节;最后读第 13、14 章确认平台差异、构建目标和验证策略。Telegram Desktop 的常见成本不是“不会写 C++”,而是误改了错误的层、绕过了状态机或忘记让订阅跟随 owner 的 lifetime。
四个阅读纪律
1. 区分所有权、引用和观察
std::unique_ptr 通常暴露所有权树;not_null<T*> 表示非空借用;base::weak_ptr / base::weak_qptr 用于跨异步边界;rpl::lifetime 决定订阅何时终止。读一个类时先看成员字段,再看方法体,往往比从构造函数一路单步更快。
2. 区分命令与事实
用户点击“发送”是命令;本地插入 sending item 是预测;服务端 Updates 才是被确认的事实。类似地,界面调用 showPeerHistory 是导航命令,真正的页面状态由 memento、active entry 和 data producer 共同决定。
3. 区分全局序列和频道序列
全局 Updates 有 seq/pts/qts/date,频道还维护自己的 PTS。看到缺口恢复时,先确认代码走的是 updates.getDifference 还是 updates.getChannelDifference,不要把两套恢复路径混为一谈。
4. 区分内存实体与视图元素
HistoryItem 是消息领域对象;HistoryView::Element 是它在某个视图中的表现;同一 item 可能没有 main view,也可能因虚拟化而暂时没有可见 element。UI bug 不一定等于数据缺失。
如何跟着源码验证
一个实用的验证循环:
- 从章节给出的入口函数下断点;
- 只记录对象身份(account/session/peer/fullMsgId)和状态转移;
- 确认事件发生在哪个线程;
- 找到下一层的“唯一事实源”,而不是追每个 signal;
- 在退出页面、切账号或失败重试后再走一遍,验证清理路径。
不要用行号当架构
行号只是固定版本的定位工具。真正值得记住的是类的所有权、调用方向和状态不变量。升级版本后先搜索符号,不要机械套用旧行号。