
前阵子帮团队评估一套IM方案又把Telegram Android源码整个翻了一遍。坦白讲第一次打开这个项目的人多半会在几秒后陷入一种“代码量巨大且组织方式奇怪”的震撼中没有标准的多模块结构函数越长越自信到处都是直接改全局状态。但这套代码能撑起恶劣网络环境下的流畅聊天、高频消息同步和复杂的媒体传输靠的绝对不是运气。这篇解析文档的目标就是给你一张地图核心模块在哪、机制怎么运转、从哪里切入读得最有效率。适合想把大型Android项目读懂、正在造IM轮子、或者对高性能自绘UI感兴趣的开发者。下面全部基于Telegram Android开源客户端当前公开代码的工程事实展开我会尽量把“为什么这么设计”讲透而不是泛泛介绍。1. 先在地图层面搞懂仓库布局与架构选型逻辑1.1 包结构与质量入口怎么快速找到你想要的功能Telegram Android客户端拿到手之后第一步不是急着看代码而是先在整体上弄清各个包大致负责什么。项目名是TMessagesProj主包名是org.telegram下面几个关键包几乎承担了全部核心职责org.telegram.messenger核心业务逻辑包括账号管理、消息中心、本地存储、图片缓存、通知服务、全局初始化。这是整个客户端的大脑。org.telegram.uiActivity、Fragment、聊天界面、设置页等UI层内容。所有你能看到的界面都在这里。org.telegram.ui.Components自绘控件库。列表、按钮、气泡、表情面板、弹窗等大量非标准控件都在这里是整个项目最值得读的部分之一。org.telegram.tgnet网络接入层。负责MTProto协议带的底层连接、请求封装、TL序列化反序列化。org.telegram.mtprotoMTProto协议本身的加密、数据包编排、会话密钥管理等。org.telegram.ui.Cells聊天列表和会话界面中用到的各种自定义Cell相当于消息item的视觉载体。如果你用Android Studio直接打开工程会看到它还依赖了一批native库用CMake构建。这些native代码主要负责sqlcipher加密SQLite、一些密码学运算、音频处理等读Java层时不需要深入native细节但要心里有数很多看似Java实现的函数底层其实走了JNI。实际工程里直接打开源码时最影响体验的是它没有严格的模块边界org.telegram.messenger下的类互相直接引用非常常见。比如MessagesController会直接调用ConnectionsManager、MessagesStorage、NotificationCenter这些类之间基本就是“全局对象互相调用”的关系。理解这一点比一开始就指望看到清晰的MVC或MVVM分层要现实得多。1.2 为什么放弃标准多模块架构大规模单模块的取舍现在Android开发的主流做法是多模块化common、network、repository、feature层依赖方向清晰编译缓存友好。Telegram Android不是这样它更像是把整个应用装进一个巨大的Gradle模块里。这不是开发者偷懒而是有明确动机的。第一个动机是热路径访问效率。IM的聊天列表和消息发送是极高频率的路径。如果消息状态更新要跨模块、跨仓库层层层转发中间多几次接口封装和对象拷贝在低端机上就能感受到明显卡顿。Telegram的做法是让核心类全局可见消息从网络层回调到UI层可能只是“某个静态单例直接设置内存状态同时发一个通知”的事路径极短。第二个动机是历史演进。这个项目从非常早的年代开始就是单模块结构后来逐步添加功能时很难在不伤筋动骨的情况下拆成干净的多模块。与其冒着重构出线上bug的风险去追架构时髦不如把精力花在消息同步正确性和渲染性能上。这是很务实的工程决策。第三个动机是进程模型。Telegram Android大量使用多进程例如图片加载有单独进程、推送有单独进程、账号相关也有独立进程。模块间的通信本来就依赖跨进程Binder和全局文件锁如果你把业务代码拆成了模块反而会让跨进程调用的逻辑边界更难维护。所以读这个项目时我建议不要用“按模块读”的思路而是用“按能力域读”消息收发、UI渲染、本地存储、网络连接、媒体处理。每个能力域内部是高度内聚的域之间则是通过事件和全局状态关联。下文的章节就是按这个思路组织的。2. 消息处理中枢状态流转与模块解耦的底层逻辑2.1 MessagesController全局会话状态的唯一掌舵人在Telegram的架构里MessagesController大概是核心中的核心。它负责的是全部会话和消息的“权威内存状态”当前有哪些对话、每个对话的最新消息、未读计数、用户和群组信息、在线状态、正在输入状态、针式消息等。几乎你能想到的一切聊天相关数据在内存里都有一份归它管。它的设计思路可以理解为“单一可信来源”。比如你打开聊天界面界面本身并不持有消息数组而是从MessagesController拿一个引用同时通过NotificationCenter订阅消息变更事件界面接收到事件后更新UI。这样做最大的好处是多个界面聊天列表、聊天详情、媒体查看器等能保持数据一致不会出现两个页面各存一份消息列表、互相同步不过来的问题。它同时还负责转发网络事件。举个例子当某条消息被对端设备标记为已读时网络层不会直接调用UI层的某个接口而是先把更新交到MessagesController由它来决定这条消息当前在内存里的状态再通过事件通知UI刷新。这种“网络事件→中心状态→界面刷新”的单向数据流在大型IM里是非常必要的它把“数据从哪来”和“界面怎么展示”彻底解耦了是Telegram能同时支持聊天列表、搜索、跨设备同步而不会乱掉的关键。MessagesController的另一个职责是消息发送管理。你在输入框按发送UI层并不直接跟网络层交互。消息先被写入一个发送队列进入本地数据库这样即使进程被杀消息也不会丢然后才走网络发送。网络返回成功后它会替换消息ID、更新发送状态。整个过程对用户是无感的但对代码阅读者来说理清“发送前的持久化→发送中→发送成功→替换临时ID”是一条非常好的追踪路径。2.2 NotificationCenter 与消息状态机驱动UI更新的暗线如果只看类的调用关系Telegram的代码会显得很绕MessagesController更新完状态之后怎么通知到ChatActivity的答案就是NotificationCenter这个全局事件总线。它是整个项目里最值得借鉴的模块之一。NotificationCenter本质上是一个单例观察者容器。业务层可以通过addObserver注册对某类事件的关注例如“消息已读”“收到新消息”“对话列表已更新”“图片加载完成”。某个对象触达了状态变化后调用postNotificationName把事件广播出去。所有注册者都会同步收到回调。这种设计让网络层、存储层、UI层之间不必要互相持有强引用也避免了层层接口回调带来的复杂度。但这里要特别提醒一点Telegram里的NotificationCenter回调和Android的EventBus不完全一样。它要求观察者必须显式注册、显式反注册而且回调往往是同步执行的。如果某个回调里做了耗时操作是会卡住触达线程的。所以读代码时你常会看到“postNotificationName之后马上return主线程里再处理”这种写法本质是避免不必要的锁竞争和耗时操作。消息的状态机设计则是理解整个IM逻辑的钥匙。常规消息至少经历这几个状态草稿/发送中、已发送到服务器此时拿到服务器ID、已投递给对方设备、已读、失败。Telegram用一些常量来表示状态每次网络层返回新的更新就推进一次。你一条消息发出去UI气泡上的时钟图标从“发送中”变成“单个对勾”再变成“双对勾”背后就是这套状态机在驱动。理解状态机的价值不只是为了读源码。你自己做IM时如果一开始就能把消息状态定义清晰后面做重发、未读补偿、多端同步都会顺畅很多。Telegram这里最值得学的是状态变更必须走同一个入口而不是零散地在各个回调里直接改UI。3. UI自绘体系极致滚动性能背后的设计代价3.1 聊天气泡与列表为什么放弃经典RecyclerView方案Telegram的聊天界面如果从外边看和一般IM没什么区别消息气泡、时间线、输入框。但它的内部实现和主流做法差异巨大——聊天气泡和消息列表大量使用自定义Canvas绘制而不是传统的XML布局RecyclerView ViewHolder。为什么不直接用RecyclerView这里得先说清楚IM场景的特殊性。聊天列表的Item不是固定高度的气泡会随文本长度、图片尺寸、引用内容变化会话中还会插入日期分隔条、服务消息、置顶消息等异构元素。如果老老实实按标准方式做主线程要频繁measure和layout。消息频繁更新已读、发送状态、编辑还会不断触发局部刷新RecyclerView的notifyItemChanged在极端情况下会成为卡顿来源。Telegram的办法是大幅减少系统measure/layout的参与。它把气泡文本的测量、尺寸缓存、背景绘制全部收拢到自绘控件内部很多情况下甚至直接在dispatchDraw阶段用Canvas画文字和图形。这样列表滚动的每一帧CPU的大部分工作都花在有效绘制上而不是布局计算上。聊天列表和会话列表用的也是自定义LayoutManager的思路控制Item的回收复用、处理异形Item、在快速滑动时做图片加载优先级调度。你可以把它理解成给RecyclerView装了一个“专门给IM优化过的大脑”而不是直接套用默认实现。这里我不建议你在自己项目里无脑复刻这套方案。自绘UI的开发成本和维护成本极高。但Telegram的价值在于它展示了性能瓶颈的可能解法当标准组件不满足时你可以下沉到绘制层做优化。读源码时多留意“哪些是性能逼出来的设计”这才是收获最大的部分。3.2 自绘组件族的成本继承体系、触摸事件与位图复用自绘UI不是简单的“用Canvas画个圆”。Telegram在ui.Components包里维护了一整套自绘控件体系从基础按钮、滑动条、文本视图到复杂的PhotoViewer、表情面板、贴纸键盘。它们大多是继承自View或FrameLayout但内部很多渲染逻辑完全绕过系统默认的background/normal机制。这套体系最明显的特点是父类非常重。很多基础组件里揉进了触摸反馈、状态保存、可见性判断、圆角裁剪等能力。你继承它的时候会发现需要遵守不少隐含约定。例如自绘按钮对触摸事件的判断往往不依赖系统onTouchEvent的默认逻辑而是自己算触摸点是否落在某个形状内部。如果忽略这套约定很容易出现“点击区域不对”或者“涟漪效果不触发”的问题。位图复用也是自绘体系里必须处理的课题。聊天气泡背景里有大量圆角和阴影如果每一条消息都创建一个新的Bitmap内存很快爆掉。Telegram大量使用预先绘制好的位图切片和NinePatch风格的复用色彩变化则通过ColorFilter实现。图片加载方面图片加载器会结合ImageView的最终显示尺寸来降采样避免把一张几千万像素的原图直接塞进内存。读这部分代码时要接受一个现实它不是给初学者准备的教材里面充满了性能导向的“不优雅”。但如果你想解决实际问题比如列表首屏慢、滑动掉帧、大图加载OOMTelegram的实现方案可以给你很多具体灵感。4. 数据持久层千万级消息量的分库与缓存策略4.1 hash分表与写事务合并本地数据库的内存与延迟平衡Telegram的本地数据库使用SQLCipher加密核心是消息存储。这里最有借鉴价值的是它的分表策略和写放大控制做IM或者做离线缓存的人都很值得看。消息数据被按照某种hash规则分散到了多张表里。具体做法是对对话ID做hash后取模得到表后缀然后每条消息落到对应表。这样设计的目的很朴素单表数据量不会无限膨胀查询和写入都能维持稳定性能。如果你的消息量是百万级乃至千万级一张大表在频繁增删查改的情况下性能下降非常明显拆表能有效缓解。本地存储的写入也不是“来一条写一条”。IM场景的消息到达往往是一批一批的如果每条消息都立即触发一次数据库事务IO开销会非常大而且主线程的卡顿感明显。Telegram的存储层会把多条写操作合并到一个事务里批量执行。这个思路和很多正规IM的做法一致但它实现得比较彻底——存储层在独立线程跑UI线程永远不直接执行DB操作命令都通过队列异步派发。和缓存关联还有一层设计内存里维护了最近的对话和消息列表查询时先看内存内存没有才查数据库数据库没有才发网络请求。这个“内存优先、磁盘兜底、网络兜底”的三级查找模型是整个数据层的主干思路。你自己的数据库缓存设计完全可以照这个模式搭一版。读存储代码时容易忽略的是数据结构迁移问题。Telegram的表结构经历过多次升级每一次升级都对应一段迁移逻辑。如果你在二次开发中动了数据库字段一定要保留原升级链否则老用户升级后会出现数据不兼容的崩溃。这一点在二次开发章节还会再说。4.2 三级缓存与按需降采样一张图片的加载流水线媒体缓存是IM里另一个核心话题。Telegram在图片加载上采用的方案并不依赖第三方图片库而是有一套自己的ImageLoader体系策略上可以概括为“三级缓存按需降采样”。三级缓存指的是网络层、磁盘缓存、内存缓存。网络请求到的图片会先落到磁盘再加载进内存。这个顺序和主流图片库一致但Telegram对内存缓存的管理更激进限制内存缓存大小优先保证UI流畅而不是永远持有已经滚出屏幕的位图。按需降采样则是Telegram的高明之处。聊天列表里的缩略图、聊天气泡里的普通图、查看大图时的原图这三种场景需要的图片精度是完全不同的。如果都加载原图几张大图就可能吃掉几十MB内存。Telegram的做法是解码时根据目标View的尺寸计算采样率代码里常能看到根据图片的显示区域宽高和文件实际尺寸计算inSampleSize的逻辑。这样既保证视觉清晰又控制内存峰值。这套图片加载体系对普通App来说显得过于复杂但如果你想处理“用户头像列表秒滑不卡”“聊天里大量图片不乱跳”“弱网下图片加载有优先级”这类问题它给出的答案是经过实战验证的。我建议不用完全照搬它的实现但要理解它的调度核心图片加载一定是分优先级的一定是按显示需求降采样一定是三级缓存配合。5. 网络同步引擎连接复用、数据中心与增量更新5.1 连接的抽象与复用一条连接里怎么装下所有请求网络层是Telegram技术体系里最神秘也最硬核的部分。Android端把MTProto协议的客户端实现主要放在tgnet包里核心类是连接管理和状态管理。这里先不谈加密细节只讲连接架构。移动网络环境下频繁创建TCP连接成本极高。Telegram的做法是让一个数据中心内维持少量长连接所有请求和响应都通过这条连接多路复用。每个请求会被赋予独立的ID回应到达后再按照ID分发回调用方。这样一批请求并发发出时物理层只有一条或少数几条链路在跑能效和速度都比“一个请求一条连接”好得多。这里有个容易误解的点一个连接里同时跑多个请求会不会互相干扰不会。MTProto协议会在每个数据包里编排好sessionId、messageId、seqNo。接收方拿到数据时先按会话归属拆分再按messageId做排序和去重。这种设计不只是为了省连接它同时解决了三个问题减少握手次数、让多请求并发变成现实、对不稳定的移动网络有更好的容忍度。弱网处理也是这套连接引擎的重点。连接对象要负责超时检测、心跳维活、断线重连。你经常看到的现象是“切换WiFi和移动网络时聊天消息自动补回来”背后就是网络层感知到连接状态变化后对未完成请求做了重发或重新同步。这一整套连接生命周期管理值得任何做长连接业务的团队仔细临摹。读的时候建议把重点放在“客户端怎么感知网络状态变化”“一个请求从发起到超时被重试中间经历了哪些状态”“服务器返回的ack和真正的业务结果怎么区分”这三条线上。搞清楚这些你对IM底层网络模型的认知会上一个台阶。5.2 数据中心的切换与消息补偿弱网下的可靠性设计Telegram是典型的多数据中心架构客户端在启动时会根据账号所属位置被指配到一个数据中心并与该中心建立会话。源码里你能看到数据中心信息的本地持久化应用重启之后直接恢复上次使用的数据中心和会话凭据不必重新来一次完整握手。这背后是新会话创建、老会话恢复、密钥协商等一套流程虽然复杂但逻辑相当清晰。多数据中心带来的另一个问题是数据的重同步。用户在设备A读了消息在设备B要同步未读状态发出去的消息在弱网下只发了一半断网重连后要补发。这些场景不能简单靠“重新拉一次全部消息”解决那样既慢又费流量。Telegram的做法是通过增量更新机制客户端和服务端维护各自的同步点启动或网络恢复后只拉取从上次同步点以来的差异数据。阅读这部分源码时我对你有个建议不要试图把每个协议字段都记住而是关注“可靠性靠什么保证”这一层。你会发现它的核心思路就是三条请求必须能去重、响应必须能排序、状态必须能补偿。这三条原则对所有需要可靠消息投递的系统都适用不管你是做IM、做推送还是做数据同步。还要注意网络层把“业务数据”和“协议元数据”分得很清楚。业务层收到的是解包后的TL对象而协议层的确认、序列号、重传都被隔离在底层。这种分层隔离让上层业务不用操心底层可靠性也让底层可以独立做优化。它也是读完这个项目后非常值得带走的设计思想之一。6. 源码阅读地图与二次开发切入点6.1 三条阅读路径与第一步怎么走源码已经在你面前从哪里入口最有效我根据自己的阅读经验整理了三条路径分别对应不同学习目标。路径A理解“发一条消息发生了什么”。从MessagesController的发送消息方法入手追踪一条消息如何落入本地数据库、如何进入网络发送队列、网络层如何组包发出、收到响应后如何替换临时ID、如何广播UI更新。这条路径能让你建立IM全链路印象是最推荐的切入点。路径B理解“聊天界面怎么渲染”。从ChatActivity的onCreate开始看消息Adapter如何构造Cell、如何从MessagesController拿数据、收到NotificationCenter事件后如何局部刷新列表、滑动时的图片加载优先级如何调整。这条路径侧重UI性能适合对自定义列表渲染感兴趣的人。路径C理解“App是怎么启动的”。从ApplicationLoader的onCreate开始看全局初始化顺序账号管理、数据库打开、网络层启动、UI托管等。这条路径比较枯燥但对整体把握最有效建议前两条路径都读完之后再回头读它。第一步我建议不要开Android Studio而是先打开源码目录对着包结构建立印象再用类名搜索功能找到几个核心类快速浏览类里的公开方法形成“大概有哪些能力”的感觉。只有当你心里有了这些类的作用轮廓再进入具体链路读才不会迷失。6.2 二次开发最容易踩的坑如果你准备基于Telegram Android客户端做二次开发我有几条从实践中总结的避坑建议。第一不要随意改包路径和进程模型。Telegram用多进程是有意的不同进程负责不同职责。改动启动顺序或进程划分轻则功能异常重则消息收发错乱。如果你只是想做界面定制尽量在现有框架上做加法不要推翻底层。第二数据库升级逻辑必须保留。用户手机上可能带着老版本的数据。你要加的字段或表必须放到迁移逻辑末尾并保持原有迁移顺序不变。很多人就是在升级链上动了手脚导致老用户升级后数据库崩溃。同时不要动SQLCipher的加密密钥和加密方式否则会直接把用户数据变成不可读。第三自绘控件的触摸事件、状态保存、可见性判断都有隐含约定。自定义UI时先继承现有的Component类而不是从View直接新建。直接继承View你会缺失很多现成的背景处理、状态处理和事件分发逻辑后续会让你很痛苦。第四全局状态非常多测试容易受影响。做自动化测试时要注意进程隔离和全局静态状态清理。Telegram的核心逻辑高度依赖全局单例如果你想对它做单元测试需要额外搭建一套可注入的状态环境否则用例之间会互相干扰。第五构建环境要提前配置好。整个项目需要NDK和CMake编native库首次构建比较耗时。如果你的机器内存不大建议在gradle.properties里适当调整JVM堆大小同时给Studio多分一点内存不然编译过程中很容易卡死。最后再分享几个我实际读下来的体会这套代码我前前后后读了三轮。第一轮想找“标准答案”失败得很彻底第二轮带着具体业务问题去读比如“消息已读状态怎么同步”“弱网怎么重发”收获巨大第三轮开始看它每个关键模块怎么组织终于理解了前面那些看起来“不优雅”的设计每一处背后几乎都对应一个线上性能或可靠性问题。我自己的经验是读这种大型项目千万别从第一个文件顺着一行行看。先把地图画好再按链路往下钻。一个很实用的小技巧是用代码搜索工具追某个常量的出现位置比如你想知道消息发送状态都有哪些值直接搜状态名一路点开调用链会比任何教程都更直观地看到状态机全貌。Telegram Android的源码是一所免费的大型工程学校前提是你能接受它先给你一个下马威再慢慢让你体会到细节里的功夫。