表情消息是 IM 产品里使用频率最高的消息类型之一,也是带动聊天气氛、塑造品牌感的常用手段。但接入时很多团队会在一个问题上纠结:表情到底应该怎么发——是把 emoji 直接当文本,还是把表情包当成图片消息上传?这两种做法对应完全不同的实现路径和成本。这篇基于 ZEGO ZIM 官方“发送表情消息”最佳实践,拆解两种方案各自的实现方式与适用边界。
表情消息的两种形态,对应两条实现路径
ZIM 对表情消息的处理方式,取决于表情的类型。表情消息通常分两种,实现路径完全不同。
| 表情类型 | 实现方式 | 说明 |
|---|---|---|
| Emoji 表情 | 直接作为文本消息发送 | SDK 原生透传,无需额外接口 |
| 自定义表情包 | 业务侧自建表情库 + 自定义消息(type = 200)承载 | ZIM 只负责消息收发与存储 |
先明确 ZIM 的边界:ZIM 不提供表情面板、表情资源管理和表情渲染,表情库的维护(资源上传、版本管理、下发客户端)由业务客户端自行实现,ZIM 承担的是“把表情作为消息发出去、收进来、存进历史消息”的角色。理清这条边界,下面的实现方案就很好理解了。
方案一:Emoji 直接走文本消息,零额外接入
Emoji 就是普通 Unicode 字符,直接发文本消息(type = 1)即可,SDK 原生透传,不需要任何额外接口。对大多数只需要系统 emoji 的场景,这条路成本最低。发送时有几个限制需要提前知道:
| 限制 | 值 | 对表情的影响 |
|---|---|---|
| 消息大小 | 文本消息默认上限 2 KB(可联系技术支持配置至 32 KB) | Emoji 采用 UTF-8 编码后单字符最多 4 字节,2 KB 约可容纳 500 个 emoji + 文字 |
| 发送限频 | 10 次/秒(间隔 100 ms) | 连发表情时注意客户端节流 |
| 空白消息 | 空内容返回错误 6000001 | 纯 emoji 属于合法内容,可正常发送 |
| 渲染兼容性 | 与 ZIM 无关 | 老操作系统上的新版本 Unicode emoji 可能显示为方框,表情面板建议按客户端操作系统的版本进行过滤 |
方案二:自定义表情包,用引用式设计承载高频表情
品牌化的系列表情、大贴图、GIF 不在系统 emoji 里,需要业务自己定义。ZIM 的推荐做法是统一用自定义消息(type = 200)承载,消息体只放“表情 ID + 资源 URL + 宽高”,资源本体由业务 CDN 承载。
之所以用引用式设计,是因为表情是引用型资源——同一表情会被成千上万次发送。如果每次发送都上传图片,会占用大量富媒体存储;而自定义消息只发引用,同一表情发送一万次也只有一万条小消息,资源本体始终只有 CDN 上一份。
{
"packId": "pack_2026_spring",
"emojiId": "e_10086",
"url": "https://cdn.example.com/emoji/e_10086.gif",
"width": 240,
"height": 240
}
消息以自定义消息发出后,接收端能否正常解析,取决于对方的 SDK 版本:
| 接收端 SDK 版本 | 表现 | 建议处理 |
|---|---|---|
| ≥ 2.8.0 | 正常收到自定义消息 | 按 subType 解析渲染 |
| [2.0.0, 2.8.0) | 可收到消息,但类型未知、无法获取内容 | UI 显示“[表情]”占位,引导升级 |
| 1.x.x | 收不到该消息 | 无需处理 |
表情库的维护,是业务侧的核心工作
ZIM 只负责消息收发与存储,表情库的“资源上传、版本管理、下发客户端”需要业务侧自己实现。一个可用的表情库,核心是下面四个动作:
- 清单下发:启动或进入表情面板时请求表情包清单,按 version 对比本地缓存,增量下载新表情到本地目录;
- 发送只引用:发送时只引用 emojiId + url,不携带资源本体;
- 接收回写缓存:接收渲染时优先查本地表情库(按 emojiId),本地没有再按 url 走 CDN 加载,加载成功后写回本地缓存;
- 下架处理:清单中移除即可,历史消息中的表情按 URL 仍可加载;若要彻底失效需 CDN 侧撤资源,注意历史消息观感。
{
"packId": "pack_2026_spring",
"version": 5,
"emojis": [
{ "emojiId": "e_10086", "file": "e_10086.gif", "width": 240, "height": 240, "order": 1 }
],
"cdnBase": "https://cdn.example.com/emoji/pack_2026_spring/"
}
接入最佳实践
把上面两条路径落地时,以下要点直接影响接入质量和用户体验:
- 消息体瘦身:自定义消息默认上限 2 KB(可配置至 32 KB),表情消息体只放 ID + URL + 宽高,不要把 base64 图片塞进消息体。
- subType 规划:表情包消息分配独立 subType(如 3),与红包、投票等其他自定义消息区分,接收端按 subType 分发解析。
- 宽高占位:消息体带 width/height,接收端先按宽高占位再异步加载,避免聊天页滚动跳动。
- GIF 注意:ZIM 不做 GIF 解析,GIF 播放由客户端 UI 组件实现,大 GIF 建议压缩后上传 CDN。
- 限频与节流:连发表情同样受 10 次/秒限频(文本/自定义消息同级),表情面板建议做点击节流。
- 历史消息:表情消息可存储为历史消息,翻看历史时按消息体中的 URL/本地缓存渲染即可。
- 低版本降级:接收端 < 2.8.0 显示“[表情]”占位,不要尝试对未知类型消息做内容解析。
- searchedContent:发送自定义表情时建议设置 searchedContent(如“[表情:开心]”),本地搜索才搜得到这条消息。
接入示例
Emoji 直接作为文本消息发送:
ZIMTextMessage textMessage = new ZIMTextMessage("今天天气不错 😄🏖️");
zim.sendMessage(textMessage, toUserID, ZIMConversationType.PEER, config, new ZIMMessageSentFullCallback() {
@Override public void onMessageAttached(ZIMMessage message) {}
@Override public void onMessageSent(ZIMMessage message, ZIMError errorInfo) {}
@Override public void onMediaUploadingProgress(ZIMMediaMessage message, long currentFileSize, long totalFileSize) {}
@Override public void onMultipleMediaUploadingProgress(ZIMMultipleMessage message, long currentFileSize, long totalFileSize, int messageInfoIndex, long currentIndexFileSize, long totalIndexFileSize) {}
});
自定义表情包发送与接收端分发:
// 发送:消息体只放"表情 ID + 资源 URL + 宽高"
String stickerJson = "{\"packId\":\"pack_2026_spring\",\"emojiId\":\"e_10086\"," +
"\"url\":\"https://cdn.example.com/emoji/e_10086.gif\",\"width\":240,\"height\":240}";
ZIMCustomMessage stickerMessage = new ZIMCustomMessage(stickerJson, 3); // subType = 3:业务自定义的表情包消息
stickerMessage.searchedContent = "[表情:开心]"; // 建议设置:本地搜索只取该字段
zim.sendMessage(stickerMessage, toUserID, ZIMConversationType.PEER, config, new ZIMMessageSentFullCallback() {
@Override public void onMessageAttached(ZIMMessage message) {}
@Override public void onMessageSent(ZIMMessage message, ZIMError errorInfo) {}
@Override public void onMediaUploadingProgress(ZIMMediaMessage message, long currentFileSize, long totalFileSize) {}
@Override public void onMultipleMediaUploadingProgress(ZIMMultipleMessage message, long currentFileSize, long totalFileSize, int messageInfoIndex, long currentIndexFileSize, long totalIndexFileSize) {}
});
// 接收端:统一消息回调中按 type / subType 分发解析
zim.setEventHandler(new ZIMEventHandler() {
@Override
public void onMessageReceived(ZIM zim, ZIMMessageReceivedEventResult result) {
for (ZIMMessage msg : result.getMessageList()) {
if (msg.getType() == ZIMMessageType.CUSTOM) {
ZIMCustomMessage custom = (ZIMCustomMessage) msg;
if (custom.subType == 3) {
// JSON 解析:优先查本地表情库(按 emojiId),本地没有再按 url 加载并回写缓存
}
}
}
}
});
结语
表情消息的实现,本质是两条路径的选择:系统 emoji 直接走文本消息,成本最低;品牌化表情包走自定义消息的引用式设计,把高频资源放在 CDN 上,消息体始终轻量。ZIM 在这条链路上负责可靠的消息收发、存储与历史同步,表情库和渲染由业务侧按自己的产品形态自建。接入时的关键,是把消息体瘦身、subType 规划、低版本降级这些细节做对。
本文基于 ZIM 开发者中心“发送表情消息”最佳实践整理,接口与限制以官方文档为准:ZIM Android 发送表情消息
本文来自作者投稿,版权归原作者所有。如需转载,请注明出处:https://www.nxrte.com/jishu/im/72613.html