
1. 为什么好友列表和聊天记录值得用本地数据库来存做过联网社交功能的人大概都踩过同一个坑好友列表和聊天记录一开始图省事直接塞进PlayerPrefs或者序列化成 JSON 丢到Application.persistentDataPath下功能跑通那一刻觉得挺美等到好友数量破百、聊天记录攒了几千条问题就全冒出来了。读取要全量反序列化改一条记录要重写整个文件查询某个好友的历史消息只能靠遍历稍微手滑写坏一次文件整份数据直接报废。这不是危言耸听我自己就在一个模拟项目里亲眼见过聊天记录文件涨到十几兆之后进入会话界面卡了将近两秒。所以当项目标题落到“好友列表、聊天记录使用本地数据库储存”这个点上时我第一反应是终于有人愿意把这类结构化数据交给正经的关系型数据库来管了。而 SimpleSQL 配合 SQLite 的组合恰好是 Unity 生态里门槛最低、落地最快的一条路。SQLite 本身是一个嵌入式关系型数据库引擎不需要独立服务进程整个数据库就是一个文件随应用走SimpleSQL 则是在 Unity 里对 SQLite 做了一层封装让你能用接近原生 SQL 的方式建表、增删改查还能直接在 Inspector 里可视化地管理表结构。这套方案能解决什么问题简单说三件事结构化存储好友有 ID、昵称、头像、备注、分组聊天记录有会话 ID、发送者、内容、时间戳、已读状态这些字段天然适合表结构、增量读写改一条记录只动一行不用重写整个文件、高效查询按会话查最近 50 条消息一条带索引的 SQL 就搞定。适合谁来参考正在做单机社交模拟、离线聊天 Demo、或者需要本地缓存好友与消息数据的 Unity 开发者无论你是刚接触数据库的新手还是想从 JSON 方案迁移过来的老手这套思路都能直接抄。需要提前说明的是标题里没有给出具体的表结构、字段类型和代码细节下面涉及的表设计、参数选择、操作步骤都是基于我在实际项目中最常用的合理方案补全的你可以根据自己的业务字段做增删但设计逻辑和避坑点是通用的。2. 整体方案设计与技术选型拆解2.1 为什么是 SQLite 而不是 JSON 或 PlayerPrefs先把选型这件事说透因为很多人一上来就纠结工具却没想清楚数据形态。好友列表和聊天记录属于典型的关系型、可增长、需检索的数据。JSON 文件适合配置项、存档快照这类“整体读、整体写”的场景一旦数据量上去全量序列化的开销和文件损坏风险都会放大。PlayerPrefs 更不用提它本质是键值对存个音量、画质还行拿来存聊天记录属于杀鸡用牛刀还杀不动。SQLite 的优势在于它是真正的数据库支持事务、支持索引、支持条件查询、支持分页。聊天记录这种“按会话倒序取最近 N 条”的需求用ORDER BY timestamp DESC LIMIT 50一句话解决换成 JSON 你得自己排序再截断。而且 SQLite 单文件存储备份就是复制一个文件迁移成本极低。SimpleSQL 的价值则在于抹平了 Unity 里接 SQLite 的繁琐。原生接法要自己处理各平台的 native 库、路径、线程SimpleSQL 把这些包好了还提供了 Inspector 里的表管理界面建表、加字段、看数据都能可视化操作对不熟悉 SQL 的开发者非常友好。2.2 表结构设计的核心思路好友和聊天记录是两张主表但设计时要想清楚它们的关系。好友表存“人”聊天记录表存“消息”消息通过好友 ID 或会话 ID 关联到人。这里有个常见分歧聊天记录到底按“好友”关联还是按“会话”关联如果是一对一聊天直接存好友 ID 就够了如果未来要支持群聊建议抽象出会话概念消息表存会话 ID会话表再关联参与者。为了兼顾扩展性我倾向于后者但为了不让示例过度复杂下面用“好友 ID 会话标识”的折中方案。字段类型上SQLite 是动态类型但建表时仍建议写清楚INTEGER、TEXT、INTEGER时间戳存 Unix 毫秒。主键用自增INTEGER PRIMARY KEY好友的业务 ID 单独建唯一索引避免重复添加。时间戳统一存毫秒整数比存字符串好排序、好比较。2.3 数据读写的分层设计直接在 UI 脚本里写 SQL 是最容易埋雷的做法。我的习惯是分三层数据访问层封装 SimpleSQL 的增删改查、业务逻辑层好友添加、消息发送、已读标记、表现层列表渲染、滚动加载。这样做的理由是SQL 语句集中管理便于优化和排查业务逻辑可以单独测试UI 层只关心数据展示。很多项目后期难维护就是因为 SQL 散落在各个按钮回调里改一个字段要全局搜索。3. 核心细节解析与实操要点3.1 环境准备与 SimpleSQL 接入SimpleSQL 的接入流程不复杂但有几个细节容易翻车。导入插件后需要在场景里挂一个SimpleSQLManager组件它是整个数据库操作的中枢。数据库文件名建议用固定名称比如social_local.db路径交给插件管理它会自动落到各平台合适的可写目录。这里要注意不要自己拼Application.persistentDataPath再传给插件除非你明确知道插件在移动端的路径处理逻辑否则容易出现编辑器能跑、真机找不到库的情况。建表有两种方式一是在 Inspector 里用可视化界面建二是用代码执行CREATE TABLE IF NOT EXISTS。我推荐后者因为表结构应该跟着代码走方便版本管理和团队协作。可视化界面适合调试时快速看数据不适合作为建表的唯一来源。// 建表语句示例放在初始化流程里执行 string createFriendTable CREATE TABLE IF NOT EXISTS friend ( id INTEGER PRIMARY KEY AUTOINCREMENT, friend_uid TEXT NOT NULL UNIQUE, nickname TEXT, avatar_url TEXT, remark TEXT, group_name TEXT, last_active INTEGER DEFAULT 0 );; string createMessageTable CREATE TABLE IF NOT EXISTS message ( id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, sender_uid TEXT NOT NULL, content TEXT, msg_type INTEGER DEFAULT 0, timestamp INTEGER NOT NULL, is_read INTEGER DEFAULT 0 );; string createIndex CREATE INDEX IF NOT EXISTS idx_msg_session_time ON message(session_id, timestamp DESC);;上面这段里friend_uid加了UNIQUE防止同一好友重复入库消息表建了session_id timestamp的复合索引这是查询最近消息的关键没有索引的话数据量一大查询就会全表扫描。msg_type用来区分文本、图片、系统提示is_read用 0/1 表示未读已读都是社交场景的常规设计。3.2 好友数据的增删改查要点好友的“添加”操作要处理重复问题。直接INSERT遇到唯一约束会报错稳妥做法是用INSERT OR REPLACE或者先查再决定插入还是更新。我一般用后者因为需要区分“新好友”和“更新资料”两种业务语义前者可能要触发欢迎消息后者只是刷新昵称头像。查询好友列表时如果好友数量可能上千不要一次性全查出来渲染。虽然本地数据库查询很快但 UI 列表一次性生成上千个节点照样卡。正确做法是分页查询配合列表复用LIMIT和OFFSET是基础但OFFSET在深分页时性能会下降更好的方式是用“上一页最后一条的 id”作为游标即WHERE id lastId ORDER BY id LIMIT 20。删除好友时要注意级联问题。删了好友他的聊天记录要不要一起删这属于业务决策但技术上要么手动删两张表要么建外键加ON DELETE CASCADE。SQLite 默认不开启外键约束需要在连接时执行PRAGMA foreign_keys ON这一点很多人不知道导致以为建了外键就自动级联结果数据残留。3.3 聊天记录的分页与索引策略聊天记录是数据量增长最快的表设计时就要为“长期使用”做准备。核心查询有两个进入会话时取最近 N 条以及上滑加载更早的历史。前者用ORDER BY timestamp DESC LIMIT 50后者用WHERE timestamp lastTimestamp ORDER BY timestamp DESC LIMIT 50。注意这里用时间戳做游标而不是OFFSET因为新消息不断插入OFFSET分页会导致重复或遗漏。索引方面(session_id, timestamp DESC)的复合索引能同时服务这两个查询。但索引不是越多越好每条索引都会增加写入开销聊天记录写入频繁索引过多会拖慢发送消息的响应。我的经验是只建必要的复合索引单字段索引能省则省。还有一个细节消息内容如果可能很长比如长文本、富文本考虑把大字段拆到单独的扩展表主表只存摘要和引用避免主表行过大影响扫描效率。当然普通聊天场景几十上百字的文本没必要拆这里只是提醒你在设计时留个心眼。3.4 事务与批量写入的正确姿势同步大量历史消息、批量导入好友时逐条INSERT会非常慢因为每次写入都可能触发磁盘同步。SQLite 的正确做法是包在一个事务里BEGIN之后批量插入最后COMMIT这样磁盘同步只发生一次速度能提升一个数量级。SimpleSQL 支持执行多条语句你可以把批量插入拼成一条多值INSERT或者循环执行但用事务包裹。注意事务里如果中途出错一定要ROLLBACK否则可能留下半截数据。SimpleSQL 的 API 里通常有事务相关的封装用之前先看文档确认别自己裸写BEGIN又忘了提交。4. 实操过程与核心环节实现4.1 初始化流程的完整落地初始化要保证幂等也就是重复执行不出错。流程大致是挂载SimpleSQLManager、设置数据库名、执行建表语句、执行索引语句、必要时做数据迁移。数据迁移这块很多人忽略比如 v1 版本好友表没有remark字段v2 要加就得用ALTER TABLE friend ADD COLUMN remark TEXT并且用PRAGMA user_version记录版本号启动时对比版本决定是否执行迁移脚本。没有版本管理后期加字段只能让用户重装体验极差。// 版本迁移的简化思路 int currentVersion GetUserVersion(); // 通过 PRAGMA user_version 读取 if (currentVersion 2) { ExecuteSQL(ALTER TABLE friend ADD COLUMN remark TEXT;); SetUserVersion(2); }4.2 发送一条消息的完整链路用户点发送链路是生成消息对象会话 ID、发送者、内容、时间戳、未读状态→ 写入 message 表 → 更新会话表的最后消息摘要和时间 → 刷新 UI。这里的关键是先落库再刷 UI还是先刷 UI 再落库我建议先落库落库成功再更新界面因为本地写入失败的概率虽低但存在磁盘满、权限问题先刷 UI 会造成“看起来发出去了其实没存”的假象。写入用参数化查询别拼字符串防止内容里的引号把 SQL 搞坏这也是安全习惯。string sql INSERT INTO message (session_id, sender_uid, content, msg_type, timestamp, is_read) VALUES (?, ?, ?, ?, ?, ?);; // 通过 SimpleSQL 的参数绑定接口传入对应值4.3 会话列表的聚合查询会话列表要展示每个会话的最后一条消息和未读数这需要聚合查询。可以用子查询取每个会话的最新消息也可以用GROUP BY配合MAX(timestamp)。数据量大时更高效的做法是在会话表里冗余存“最后消息摘要”和“最后时间”发消息时同步更新这样会话列表查询就是简单的SELECT * FROM session ORDER BY last_time DESC不用每次聚合扫描消息表。这是典型的用写入时的少量冗余换读取时的大幅性能提升社交类应用普遍这么做。4.4 已读状态的批量更新进入会话后要把该会话的未读消息标记为已读用UPDATE message SET is_read 1 WHERE session_id ? AND is_read 0。注意加is_read 0条件避免无谓地重写已经是已读的行减少写入量。如果未读消息很多这条更新可能耗时可以放到协程或后台线程执行别阻塞主线程导致进入会话卡顿。5. 常见问题与排查技巧实录5.1 高频问题速查表问题现象可能原因排查与解决编辑器正常真机找不到数据库路径处理不当或库未正确打包用插件默认路径检查各平台 native 库是否随包查询越来越慢缺少索引或全表扫描用EXPLAIN QUERY PLAN看是否走索引补复合索引批量导入极慢逐条写入无事务用事务包裹批量插入删除好友后消息残留外键未开启或未手动清理执行PRAGMA foreign_keys ON或手动删两表加字段后旧数据读不出未做版本迁移用user_version管理迁移脚本消息内容含引号导致写入失败字符串拼接 SQL改用参数化查询5.2 几个只有踩过才知道的坑第一个坑是多线程访问。SQLite 本身支持多连接但 SimpleSQL 的封装不一定线程安全后台线程写、主线程读同时进行可能出问题。稳妥做法是所有数据库操作收敛到一个队列或主线程执行或者确认插件文档明确支持并发再放开。第二个坑是数据库文件膨胀。聊天记录只增不减文件会越来越大。定期清理策略要有比如只保留最近半年的消息或者提供“清空聊天记录”入口。清理时用DELETE后记得VACUUM回收空间否则文件大小不会缩小。第三个坑是时间戳时区。统一存 Unix 毫秒UTC展示时再转本地时区别在存储层混用本地时间否则跨时区或夏令时会出诡异问题。提示调试阶段可以临时把数据库文件复制出来用任意 SQLite 可视化工具打开检查数据比在代码里打日志高效得多。5.3 性能优化的经验边界本地数据库的优化有个度。好友几千、消息几万条的规模上面这些索引和分页策略足够用不必上复杂的缓存层。真正需要警惕的是“每次 UI 刷新都查一次库”正确做法是数据变更时查一次结果缓存在内存列表里UI 从内存读。数据库是持久化层不是高频读取的缓存把这两者混为一谈是性能问题的常见根源。我在实际项目里的体会是这套方案最值钱的地方不是 SQLite 本身多强而是它逼着你把数据关系想清楚。表结构一旦定好增删改查都是顺理成章的事后期加功能也不用推倒重来。如果你现在还在用 JSON 存聊天记录找个下午迁移过来改动量比想象中小收益却会随着数据增长越来越明显。