21、聊天与消息:文字聊天集成、表情与图片发送、消息历史记录
视频会议里,光有画面和声音其实不够。你想想看,开会时突然网络卡顿,对方说的话听不清怎么办?或者你想在会议中分享一个链接、一段代码,口头说一遍大家记不住。这时候,文字聊天就成了救命稻草。
我在做第一个WebRTC项目时,就踩过这个坑。当时觉得“视频通话才是核心”,聊天功能随便搞了个简单的EditText加发送按钮。结果上线后用户反馈最多的居然是“聊天消息发不出去”、“表情显示成方框”。嗯,从那以后我再也不敢小看聊天模块了。
这一章,我们就来搞定Android平台上的文字聊天集成。包括基础的消息收发、表情支持、图片发送,以及消息历史记录的本地持久化。
21.1 聊天消息的数据结构设计
先想清楚消息长什么样。我习惯用一个统一的Message类来承载所有类型:
public class ChatMessage {
public static final int TYPE_TEXT = 0;
public static final int TYPE_EMOJI = 1;
public static final int TYPE_IMAGE = 2;
public static final int TYPE_FILE = 3;
private String messageId; // 唯一ID
private String senderId; // 发送者ID
private String senderName; // 发送者昵称
private int type; // 消息类型
private String content; // 文本内容或图片URL
private long timestamp; // 时间戳(毫秒)
private boolean isSent; // 是否已发送
private boolean isRead; // 是否已读
}
为什么要有messageId?因为消息去重。WebRTC的数据通道是UDP的,虽然我们通常用可靠模式,但偶尔还是会收到重复消息。用UUID生成唯一ID,接收端根据ID去重,这是基本操作。
21.2 通过WebRTC DataChannel发送文字消息
WebRTC除了传输音视频,还提供了DataChannel。这东西本质上是建立在P2P连接上的双向数据管道。用它来传聊天消息,再合适不过。
初始化DataChannel的代码大概是这样:
// 创建DataChannel配置
DataChannel.Init dcInit = new DataChannel.Init();
dcInit.ordered = true; // 保证消息顺序
dcInit.negotiated = false; // 自动协商
dcInit.id = 1; // 通道ID
// 在PeerConnection上创建DataChannel
DataChannel dataChannel = peerConnection.createDataChannel("chat", dcInit);
// 设置监听器
dataChannel.registerObserver(new DataChannel.Observer() {
@Override
public void onMessage(DataChannel.Buffer buffer) {
// 读取消息
ByteBuffer data = buffer.data;
byte[] bytes = new byte[data.remaining()];
data.get(bytes);
String message = new String(bytes, StandardCharsets.UTF_8);
// 解析并展示
handleIncomingMessage(message);
}
@Override
public void onStateChange() {
// 通道状态变化
}
});
发送消息时,把ChatMessage对象序列化成JSON字符串,然后通过DataChannel发送:
public void sendMessage(ChatMessage msg) {
if (dataChannel == null || dataChannel.state() != DataChannel.State.OPEN) {
// 通道未就绪,缓存消息
pendingMessages.add(msg);
return;
}
String json = new Gson().toJson(msg);
ByteBuffer byteBuffer = ByteBuffer.wrap(json.getBytes(StandardCharsets.UTF_8));
DataChannel.Buffer buffer = new DataChannel.Buffer(byteBuffer, false);
dataChannel.send(buffer);
}
21.3 表情与图片发送
表情发送其实有两种思路:
- 方案一: 把表情当作特殊文本,用类似
[微笑]这样的标记,客户端本地映射成图片。优点是省流量,缺点是表情库更新麻烦。 - 方案二: 直接发送Unicode Emoji字符。Android原生支持,不需要额外处理。我推荐这个方案。
图片发送就复杂一些。WebRTC DataChannel虽然可以传二进制,但单次传输大小有限制(通常16KB-64KB)。一张高清图动不动几MB,直接传会失败。
我的做法是分两步:
- 先把图片上传到自己的文件服务器或云存储(比如七牛、OSS)
- 然后通过DataChannel发送图片的URL
接收端拿到URL后,用Glide或Coil加载显示。
// 发送图片消息
public void sendImageMessage(Uri imageUri) {
// 1. 压缩图片
Bitmap bitmap = compressImage(imageUri, 720, 1280);
// 2. 上传到服务器
String url = uploadToServer(bitmap);
// 3. 构造消息
ChatMessage msg = new ChatMessage();
msg.type = ChatMessage.TYPE_IMAGE;
msg.content = url;
// 4. 发送
sendMessage(msg);
}
21.4 消息历史记录的本地存储
聊天记录不能只存在内存里。App一关就没了,用户肯定骂娘。我一般用Room数据库来存。
先定义Entity:
@Entity(tableName = "chat_messages")
public class MessageEntity {
@PrimaryKey
public String messageId;
public String senderId;
public String senderName;
public int type;
public String content;
public long timestamp;
public boolean isSent;
public boolean isRead;
public String sessionId; // 会话ID,区分不同会议
}
然后写DAO:
@Dao
public interface MessageDao {
@Query("SELECT * FROM chat_messages WHERE sessionId = :sessionId ORDER BY timestamp ASC")
LiveData<List<MessageEntity>> getMessages(String sessionId);
@Insert(onConflict = OnConflictStrategy.REPLACE)
void insertMessage(MessageEntity message);
@Query("DELETE FROM chat_messages WHERE sessionId = :sessionId")
void clearSession(String sessionId);
}
收到消息时,先存数据库,再更新UI。这样即使App被系统杀掉,重启后聊天记录还在。
21.5 知识体系结构图
下面这张图展示了本章聊天模块的核心逻辑:
从图中可以看到,消息从发送端出发,经过DataChannel传输,到达接收端。接收端做反序列化、去重、UI更新,同时把消息存到Room数据库。下次打开App时,直接从数据库加载历史记录。
21.6 实际项目中的避坑经验
最后分享几个我实际踩过的坑:
- 消息顺序问题: DataChannel的ordered模式虽然保证顺序,但如果发送端连续发送多条消息,接收端处理时如果用了异步线程,顺序可能乱。我建议接收端用一个单线程的Handler来处理消息入队。
- 大消息拆分: 超过64KB的消息,DataChannel可能会失败。我一般限制单条消息不超过16KB。图片URL通常很短,没问题。但如果要传长文本,建议分片发送。
- 数据库写入频率: 如果会议中消息刷屏,每一条都立刻写入数据库,会导致IO压力。我习惯用批量插入,每500ms或积累10条消息写一次。
- 表情兼容性: 不同Android版本对Emoji的支持不一样。Android 7以下有些表情显示为方框。我建议用EmojiCompat库来做兼容处理。
好了,聊天消息这块的核心内容就这些。说白了就是:设计好数据结构,用DataChannel传JSON,图片走URL,本地用Room存。把这些搞定了,一个能用的聊天功能就有了。
公众号:蓝海资料掘金营,微信deep3321