
前两天有个朋友找我吐槽,说他开发的社交APP被用户疯狂投诉,原因是发的消息不支持换字体,更气人的是隔壁竞品刚上了这个功能。用户原话是:”现在谁还发纯文字啊朋友圈都能换字体了。”我听完心想,这事儿确实得重视起来。
说实话,我在即时通讯领域摸爬滚打这么多年,见过太多团队在字体样式这种”小功能”上翻车。有的是前端代码写得稀里糊涂,用户发个加粗消息能把整个聊天界面卡出翔;有的是协议设计有bug,导致安卓和iOS互相看不懂对方发的斜体文本。所以今天我就把这个话题掰开揉碎了讲讲,从原理到实现再到坑位预警,争取让你一次性把这块儿弄清楚。
你可能会想,不就是改个字体大小、加个颜色嘛,能有多复杂?这话要是让负责IM协议的程序员听到,他能跟你倒一肚子苦水。
首先得明白一个前提:即时通讯和普通App有个本质区别——它是个实时协作系统。你在手机上发一条加粗消息,这条消息要经过你的客户端处理、网络传输、服务器转发,最终落到对方手机上渲染显示。这中间任何一个环节出问题,消息就会变成一坨乱码或者直接丢失样式。
举个具体点的例子。假设你发了一句”今晚八点老地方见”,其中”八点”需要加粗斜体。这个样式信息得跟着文本一起传过去吧?但怎么传?传哪些字段?对方收到后怎么识别哪段文字该用加粗、哪段该用斜体?这些问题的答案直接决定了你的实现难度和用户体验。
更要命的是,手机屏幕尺寸千奇百怪。同样一段加粗文字,在iPhone 14 Pro上显示得挺好,到了某些安卓机上可能就换行错乱甚至直接截断。还有用户会抱怨:”我发的红色文字对方看是黑色的?”这背后是色彩空间管理的坑我就不展开说了,反正够你喝一壶的。

说了这么多困难,接下来聊聊到底怎么实现。我从客户端和协议两个层面来说,这样你脑子里能有个完整的图景。
先说各个平台怎么渲染富文本。这是用户能直接看到的部分,也是最容易翻车的地方。
iOS这边相对省心,NSAttributedString基本能满足大部分需求。这个类允许你给文字的任意区间设置不同的属性,包括字体、字号、颜色、下划线、背景色,甚至还有字间距和段落样式。原理其实不复杂:你在 attributedString 里标记好每个字符区间的样式信息,系统在渲染的时候会自动读取这些信息并应用到对应的文字上。但有个坑要注意:iOS 15之后系统对行高的处理逻辑有变化,如果你用了自定义行高,可能会遇到文字重叠的问题。
Android这边稍微麻烦点。TextView本身只支持单一字体样式,Android 5.0之后引入了 SpannableStringBuilder,这个类和iOS的NSAttributedString思路差不多,也是通过设置span来实现区间样式。常用的span类型包括 ForegroundColorSpan(前景色)、BackgroundColorSpan(背景色)、StyleSpan(粗体斜体)、AbsoluteSizeSpan(绝对字号)、RelativeSizeSpan(相对字号)等等。不过SpannableStringBuilder的性能问题经常被吐槽,如果你的聊天消息特别长,频繁更新 SpannableStringBuilder 可能会导致界面卡顿。解决方案是局部刷新,只更新有变化的那部分内容,别整个消息列表都重绘。
Web端实现方式就更多了。现在主流的做法是用contenteditable属性加上execCommand命令,这套组合拳能让你直接在网页上实现富文本编辑。不过这个方案依赖浏览器实现,不同浏览器的表现可能不太一致。另一种更现代的做法是用div模拟编辑区域,自己监听键盘事件并手动维护光标位置和样式状态。这种方案更灵活但开发成本也更高,适合对富文本功能要求比较极致的场景。
前端渲染只是最后一棒,在这之前你得把样式信息通过网传过去。这就涉及到富文本消息的协议设计了。
先说最朴素的做法:直接传HTML标签。比如”加粗斜体“,后端收到什么就转发什么。这种方案实现起来确实快,但你很快就会遇到各种问题。首先是安全性,用户可以往消息里塞script标签来搞XSS攻击;其次是不同平台对HTML标签的支持程度不一样,iOS解析这些标签可能没问题,安卓那边可能就把样式信息丢掉了。

所以现在主流的做法是用结构化的协议来描述样式信息。简单说就是定义一个消息体结构,里面除了文本内容,还要记录每段文字的样式起止位置和具体属性。比如下面这个例子:
| 字段名 | 类型 | 说明 |
| content | string | 消息原文 |
| styles | array | 样式数组 |
| styles[i].start | int | 样式起始位置(字符索引) |
| styles[i].end | int | 样式结束位置(字符索引) |
| styles[i].type | string | 样式类型(bold/italic/underline等) |
| styles[i].color | string | 文字颜色(十六进制) |
这样设计的好处是协议和渲染层分离,不管是在iOS、Android还是Web上,只要按照相同的规则解析,就能得到一致的显示效果。缺点是协议体积会比纯文本大一些,毕竟多了样式信息的开销。不过对于即时通讯这种场景来说,这点开销基本可以忽略不计。
还有一种更省流量的做法是用标记字符。比如用”加粗“这样的语法,本质上也是一种结构化表达,只是换成了更紧凑的形式。具体选哪种方案要看你的实际情况,如果对流量敏感就用标记字符,如果追求协议的扩展性和可读性就用JSON结构。
光会显示还不够,用户总得能编辑这些样式吧?富文本编辑器的实现复杂度可就比单纯渲染高出一个量级了。
最核心的难点在于光标管理。当用户选中一段文字并点击加粗按钮时,你得准确知道选区的起止位置,然后把对应的样式信息写入到上述的styles数组里。这个过程中用户的选中区域可能会变化,比如拖动选择、点击定位、手势缩放等等,每种操作都要正确更新选区状态。
移动端还有一个容易被忽视的问题:软键盘弹出时编辑器的表现。很多App在弹出键盘后编辑器位置错乱,或者光标跑到了奇怪的位置,这都是因为没有正确处理键盘弹起事件导致的。解决方案一般是监听键盘事件并动态调整编辑器的高度和位置,确保用户在输入时能看到自己正在编辑的内容。
如果你觉得从零开发编辑器太费劲,也可以考虑集成现成的富文本组件。市场上有很多开源方案可供选择,但要注意它们对移动端的适配程度。有些组件设计之初就是面向Web的,在手机上用起来会有各种兼容性问题。建议在正式集成之前先用真实设备测试核心场景,特别是编辑长文本时的性能表现。
理论说了这么多,再分享几个实际开发中容易踩的坑,都是血泪经验。
富文本消息一多,渲染性能立刻成为瓶颈。特别是那些聊天记录特别长的老用户,每次下拉加载历史消息都像在看幻灯片。这里面有几个优化点值得关注:
这是一个老生常谈但又不得不谈的问题。同一个样式在iOS上显示正常,到了安卓上可能就变了样。原因各个平台对字体渲染的处理逻辑不一样,特别是涉及行高、字间距、段落间距这些细微的参数时,差异会被放大。
解决方案是定义一套平台无关的样式规范,然后各平台按照这个规范来实现渲染。比如统一规定行高是字号的1.2倍,段落间距是行高的0.5倍之类的。具体数值可以根据你的字体特性和设计稿来调整,关键是各平台都要遵守同一套规则。
声网在这方面积累了不少经验,他们的即时通讯解决方案就把跨平台一致性作为重点优化方向。毕竟如果连基础的文字显示都做不到统一,用户体验肯定会打折扣。
富文本消息的协议体积比纯文本大,传输过程中出问题的概率也更高。如果用户在发送富文本消息时网络不稳定,消息可能只发出去一半,结果对方收到的就是一段没头没脑的文字加几个孤立的样式标记。
所以富文本消息最好也要有重传机制。发送方在本地要保留消息的完整数据,如果收到服务端的失败通知或者超时未确认,就要自动重试。接收方那边也要做好校验,发现消息格式不对要及时上报给发送方,让它重新发一份完整的。
还有一个点是要考虑消息的增量更新。比如用户编辑了一条已经发出去的消息,想把某个词的字体从加粗改成斜体,这时候服务端应该是更新已有消息而不是重新创建一条,不然聊天记录里会出现两条重复的消息,体验很糟糕。
唠了这么多,其实想说的就是一句话:字体样式看起来是个小功能,但做好它需要考虑的东西一点都不少。从前端的渲染性能到协议的体积控制,从跨平台的一致性到网络传输的可靠性,每个环节都有可能是坑。
不过也不用太焦虑,没人是把这些一次性做到完美的。关键是要在设计阶段就把这些问题想清楚,后面迭代的时候就不会手忙脚乱。如果你们团队在即时通讯这块经验有限,借助成熟的服务商来渡过早期阶段也是明智的选择。毕竟用户不会管你是自研还是采购的,他们只关心用起来顺不顺手。
好了,今天就聊到这儿。如果你正在开发类似的功能,遇到什么具体问题可以再交流,咱们下回接着说。
