为什么会话列表不是查一下就有
用户打开 IM 之后,第一眼看到的往往就是会话列表。列表里的每一项看起来都很简单: 一个头像,一个名称,一条最后消息,一个时间,一个未读数,再加上置顶与否。但真正往下看,就会发现这些字段并不是从同一个地方直接查出来的。名称可能来自用户资料或群资料,最后一条消息来自消息链路,未读数来自另一套规则,排序又会同时受到新消息、置顶状态和同步更新的影响。
也正因为如此,会话系统在 IM SDK 里从来都不是“把消息摘要查出来”这么简单。它真正做的,是把消息、同步、在线通知、资料变更和多端状态这些分散事实,收敛成一个对上层稳定的会话对象。对用户来说,看到的是一个列表;对 SDK 来说,做成的是一套持续收敛状态的系统。
先看一张总图:

这张图最关键的地方在于,会话对象不是直接查出来的,而是多个来源共同收敛出来的结果。会话系统首先处理的是“怎么把这些来源收拢”,然后才是“怎么把列表展示出来”。
一、会话到底在收什么
如果把会话理解成消息列表的摘要页,就很容易低估它真正面对的复杂度。因为会话里最关键的几个字段,本来就来自不同事实源。消息进入之后,会影响最后一条消息和排序;未读规则变化之后,会影响未读数;资料更新之后,会改变名称和头像;同步包和在线通知又会继续推动会话状态往前走。会话系统做的并不是“从一个地方读出当前结果”,而是持续接住这些变化,再把它们整理成统一视图。
这意味着,会话模块处理的根本不是列表查询,而是状态收敛。它要回答的问题不是“查什么”,而是“当这么多变化同时到来时,业务侧最终该拿到什么”。从工程角度看,会话的难点也恰恰在这里。它不难在增删改查,而难在多个来源一起变化时,系统还能不能稳定产出同一个最终结果。
二、为什么会话不能只靠消息推出来
很多人第一次理解会话系统时,最自然的想法是: 既然会话列表里最显眼的是最后一条消息,那会话不就是消息的一个派生视图吗。这个理解只对了一半。消息当然是会话最重要的输入之一,但它不是唯一输入,也不足以单独撑起整个会话状态。
因为会话里还有很多东西不是单靠消息就能决定的。未读数有自己的一套规则,名称和资料会被用户信息或群信息更新影响,置顶和排序又有另外的优先级逻辑,多端已读状态和清空未读还会继续改变同一个会话对象。也就是说,消息是会话的一部分来源,但不是会话的全部依据。
这也是为什么,会话系统内部必须把“当前状态长什么样”“哪些变更有资格覆盖旧状态”“未读数应该怎么计算”“远端数据怎样拉回来”这些问题拆开处理。只有把这些矛盾分开,会话对象才有机会真正稳定下来。否则,只靠消息去硬推整个会话视图,最后一定会越推越乱。
三、为什么同步入口必须收紧
会话系统还有一个很关键的地方,就是它不能允许同步结果从系统各处零散进入。原因并不复杂: 会话状态本身已经足够敏感,如果同步入口再四处散落,系统很快就会失去对更新顺序的控制。哪些主数据应该先到,哪些变化应该后生效,哪些通知该覆盖缓存,哪些结果只应该被当成阶段信号,这些事情一旦没有统一入口,很容易就会变成“谁先到谁说了算”。
对会话这种最终视图层来说,这是非常危险的。因为会话系统接住的不是某一个局部字段,而是一组对外直接可见的结果。只要入口不统一,系统就很难再判断什么时候该开始观察主数据,什么时候该写入新的版本信息,什么时候才应该把变化真正抛给外部监听者。入口越分散,最后的状态就越难解释。
因此,会话系统必须把同步入口尽量收紧。它需要先把状态变化收拢到同一个地方,再决定如何更新当前会话对象。这样做的价值,不是让流程看起来更规整,而是让会话视图的收敛顺序始终可推理。 图 1 里把同步结果、消息事实、在线通知和资料变化先收进同一个会话系统入口,强调的也正是这种“先收口,再更新”的顺序控制。
四、为什么最后一条消息最难稳住
对用户来说,最后一条消息只是列表里的一行文案;但对 SDK 来说,它往往是整个会话系统里最难稳定的字段之一。因为它并不是简单地“有新消息就替换掉旧消息”。发送中的消息可能要先占住当前位置,发送失败的消息有时也还需要继续显示,删除最后一条消息之后要不要向前回看,撤回最后一条消息之后应该显示提示还是继续回填,导入一批本地消息之后每个会话又该认哪一条为最新,这些都不是一句“取最新消息”能解决的事情。
先看这张图:

这张图最值得注意的,不是回填步骤本身,而是它说明“最后一条消息”根本不是一个被动字段。它需要判断这次变化是否真的影响当前结果,需要决定是否回看历史,需要在找不到上一条消息时给出新的稳定结果。换句话说,最后一条消息不是查出来的,而是被持续维护出来的。
这也是为什么,会话系统里必须存在专门的一致性保护和回填机制。只要没有这层安排,一旦删除、撤回、导入和本地发送这些事件交错出现,最后一条消息就会很快失去稳定性,而整个会话列表也会开始抖动。
五、为什么未读数必须单独算
很多实现喜欢把未读数当成会话对象上的一个普通数字字段,仿佛新消息来了就加一,已读了就减掉。但在 IM 系统里,未读数从来都不是这么线性的东西。收到新消息会影响它,标记已读会影响它,清空某个会话或某一类会话未读也会影响它,多端已读时间同步之后,它还要跟着一起变化。也就是说,未读数背后其实是一组规则,而不是一个静态数字。
先看未读规则图:

这张图说明的重点只有一个: 未读数并不是某个地方顺手改一下就行,而是必须经过统一规则中心去计算和分发。只有这样,系统才有机会在收到新消息、批量清空、多端同步这些不同场景里,始终得到同一套一致结果。
这件事对上层尤其重要。因为调用方真正关心的,只是“当前未读是多少”和“什么时候变了”。如果未读规则散落在消息、会话甚至业务层各处,调用方虽然还能看到数字,但这个数字很快就会开始漂。对会话系统来说,把未读数独立出来,本质上是在保护整个会话视图的一致性。
六、为什么版本判断才是真正的护栏
如果继续追问,会话系统凭什么能在同步包、在线通知、本地发送、删除撤回、资料变更这些更新交错到来时仍然维持稳定,真正的答案并不在某一条具体业务规则上,而在于系统有没有能力判断“这次更新到底该不该覆盖当前结果”。
这就是版本判断存在的意义。它不是性能优化,也不是锦上添花的缓存技巧,而是一致性护栏。会话系统需要知道,某次变化影响的是哪一个字段,这个字段当前手里握着的版本是什么,以及这次新变化有没有资格把它替换掉。只有在这个问题被回答清楚之后,会话对象才不会被乱序更新反复冲刷。
从这个意义上说,会话系统真正难的地方,不只是有很多数据来源,而是这些来源在时间上并不整齐。同步包可能稍晚才到,在线通知可能先一步落下,本地发送又会立刻改变最后一条消息。如果没有一层稳定的版本判断在中间兜住,整个会话视图就会被这些交错变化迅速打乱。
七、再看会话
回到这篇文章最初的问题,鸿蒙云信 IMSDK 的会话系统之所以不能被理解成一个列表服务,是因为它承担的根本不是“把会话查出来”。它真正做的,是把消息、同步、在线通知、资料变更、未读规则和多端状态这些分散事实,持续收敛成一个对上层稳定的会话对象。同步入口要收紧,最后一条消息要能回填,未读数要有统一规则,更新之间还要有版本判断兜住,这些安排合在一起,才构成了会话系统真正的价值。
因此,理解这一篇的关键,并不是记住会话系统里有哪些内部对象,而是先建立一个更重要的认识:会话模块真正负责的,是把分散变化收敛成稳定状态。只有先看到这一点,后面进入数据库篇时,才会更容易理解为什么这种稳定状态最终还必须有一套可靠的本地存储与恢复机制去承接。