出海语音社交App的设计架构
高级软件工程师(iOS)
概述
这是一款面向日本市场的出海语音社交 App。玩家先在这里按游戏找队友开黑,再通过私聊 / 群聊、语音房、动态社区沉淀关系,后期加入了虚拟形象。客户端为纯 Swift,多人协作、周级发版。
下面按业务结构、技术架构、关键流程和几处设计来写。增长和互动相关的模块用高亮标出:招募卡、动态发布与视频、小纸船(漂流瓶)、全局分享与深链回流、麦位表情、节日活动。网络层、即时通讯和语音房等公共能力写在后面。
业务全景
同一套客户端也可以按业务模块来看。上面按用户路径串起来,下面按业务模块切开:IM、语音房、Feed、个人中心、活动与增长、商城与装扮。
1. 业务结构
1.1 产品演进
阶段一以招募卡、找人和语音房为主;阶段二补上即时通讯、群组、动态社区和小纸船;阶段三加入 3D 形象、装扮和虚拟房间。每次转型都会改首页入口和核心链路,客户端靠模块化目录和稳定的公共基础层扛住变化(见第 2 节)。
1.2 模块一览
| 模块 | 职责(一句话) | 主要技术点 |
|---|---|---|
| 招募卡 | 玩家发布「找队友」卡片,可点赞、看照片、分享到站外 | 多样式卡片容器、长图截图分享、H5 招募卡互通 |
| 找人 / 条件筛选 | 按游戏、条件推荐玩伴,滑卡浏览 | 卡片分页、筛选页与话题选择页、红点逻辑 |
| 小纸船(漂流瓶) | 扔一条「纸船」,随机被陌生人捞到并开聊 | 全局悬浮可拖拽入口、删除区手势、状态与首次引导 |
| 动态发布 | 发图文/视频/音频动态,带话题 | 多媒体选择与裁剪、上传进度、后台发布、草稿 |
| 动态列表 / 视频播放 | 广场与关注流浏览、视频播放 | 多类型 Cell、视频播放页的播放状态管理 |
| 全局分享 + 深链 | 话题/动态/招募卡分享到站外,点开回到 App 指定页 | 动态短链、参数协议、跨端消息类型对齐 |
| 麦位表情 | 语音房里给麦上用户发互动表情 | 动画资源包下载 / 校验 / 解压缓存、Tab 交互 |
| 节日活动 | 万圣节等限时活动、糖果奖励 | 后台时间窗配置、活动 UI、调试面板、在线时长统计与糖果奖励 |
| IM 会话列表 | 最近会话、在线好友、推荐用户 | 数据驱动 TableView、排序与未读;其中 推荐用户区块 |
| 私聊 / 群聊 | 文本、图片、语音、视频、自定义卡片消息 | 第三方 IM SDK、消息 ViewModel 工厂、几十种自定义消息;其中 视频消息的导出、重发与杀进程恢复 |
| 公开群 / 群精选 / 群活动 | 兴趣群、标记精选消息、群活动预约 | 分页列表、会话内卡片刷新、话题关联进群;其中 群精选与群活动的多数页面 |
| 个人主页 / 标签 | 资料、兴趣标签、注销账号 | 标签控件、Apple 账号注销流程 |
| 登录与新手引导 | 多方式登录、选游戏、填资料 | 第三方登录 SDK、引导流程 |
| 语音房 | 多人连麦、房间管理、房内玩法 | 第三方音视频 SDK、麦位状态、屏幕共享扩展 |
| 虚拟形象 / 装扮 / 内购 | 3D 形象、装扮购买 | Unity 集成、内购 |
| 基础设施 | 网络、路由、配置、通用 UI、日志监控 | 见第 2 节 |
模块之间的依赖是单向的:业务模块调用基础层,基础层不反向依赖某个具体玩法。
2. 技术架构
2.1 分层
从上到下是四层:页面壳、业务模块、基础设施、第三方能力。业务模块只依赖基础设施暴露的接口。
技术栈从中心向外分四支:界面、网络在右侧,连接、工程在左侧。
2.2 网络层
- 声明式接口:路径、方法和参数集中在接口枚举里,按业务拆成扩展。新增一种接口,就是加一个
case。 - 泛型一行调用:请求单个对象和请求数组两个入口,内部完成状态码、业务码和 JSON→
Model。调用方只处理成功模型和失败(code, 文案)。 - 错误码:一个枚举覆盖网络、登录、房间、组队、群组、预约、纸船等领域;登录过期、被踢下线、账号封停统一触发登出,并用一次性标志避免多个请求各登出一次。
- 多环境:正式、测试、开发、提审可在调试包里切换。提审环境依赖全局配置先回来,否则会打错环境。
- 鉴权是自定义请求头加签名。
配置门解决的是冷启动时几十个请求同时依赖同一份配置。状态只有四种:
2.3 长连接
- 即时通讯:第三方 IM SDK 之上有统一的登录、收发、会话和推送。业务卡片(招募卡、房间、动态、纸船、活动预约等)走自定义消息,类型码与 Android 共用一张表,未知类型降级为「不支持」。
- 实时音频:第三方音视频 SDK 负责语音房连麦。屏幕共享用独立的
App Extension(系统录屏)推流。
2.4 登录与会话
即时通讯的登录和登出单独成篇。本地凭证与当前账号一致就自动登录,否则向业务服务换凭证再登录,失败则换新凭证重试,最多 5 次,放弃时要清掉进行中的标志;封禁或其他设备登录会先提示再登出,登录失效类错误只发一次登出通知,登出时取消上传并复位状态。业务侧凭证如何判断有效、如何刷新,这篇不写。见 登录与会话。
2.5 缓存与持久化
| 方式 | 用途 |
|---|---|
| 对象归档 | 当前用户信息、圈子数据,冷启动先读本地 |
| SQLite(FMDB) | 内购交易记录等结构化数据 |
| 偏好设置 | 开关、引导是否展示过、红点时间戳 |
| 系统钥匙串 | 登录凭证 |
| 图片缓存 | Kingfisher / SDWebImage,支持 WebP |
| 资源包缓存 | 麦位动画等:下载、内容校验、解压到以校验和命名的目录 |
互动表情体积大、上新勤,不打进安装包。播放前按内容校验和找本地目录,没有再下载。
2.6 UI:数据驱动的列表
列表页继承同一个列表模型基类:它自己做 TableView 的数据源和代理。每个 CellModel 声明对应的 Cell、复用标识和高度,基类第一次遇到该类型时自动注册。加一种卡片,就是加一对 CellModel 和 Cell,不必改 cellForRow 的 if/else。会话里的推荐用户、群精选和个人主页都是这样拼的。聊天消息同样由工厂按类型生成消息 ViewModel。布局用 SnapKit,部分复杂列表用 IGListKit。
2.7 几处取舍
| 问题 | 另一种做法 | 采用的做法 | 原因 |
|---|---|---|---|
| 多种业务同时上传 | 每种媒体一个全局分组 | 每次上传一个独立分组 | 避免后一次上传覆盖前一次的队列和回调 |
| 媒体消息的上传状态 | 单独建一张任务表 | 写进消息的本地扩展,随本地库落盘 | 状态和消息同一生命周期,删除消息时状态一起消失 |
| 麦位动画缓存 | 按下载地址做目录 | 按内容校验和做目录 | 地址变了仍能命中,内容变了一定会重下 |
| 日志出口 | 各处直接打到文件或崩溃平台 | 一个门面按级别和包类型分发 | 正式包不打过细的调试日志,远程只收需要关注的级别 |
2.8 依赖与工程
- 主要依赖:Moya、RxSwift、HandyJSON、SnapKit、Kingfisher、SDWebImage、IGListKit、MJRefresh、YYText、Lottie、FMDB、Tiercel(下载)、SSZipArchive、钥匙串封装库、DeviceKit。
- 海外 SDK:Apple / Google / Twitter / LINE 登录、Firebase(崩溃、性能、分析、动态链接、存储)、Sentry、AppsFlyer 归因。
- 工程:CocoaPods;主 App 和屏幕共享扩展两个
Target。GitLab CI 加 Fastlane:推到专用打包分支即打测试包、上传内部分发,并上传符号表。SwiftLint 统一风格。分支从develop到测试包,再打tag合入主分支。日 / 英 / 中三套文案。
2.9 安全(概念级)
- 登录凭证在系统钥匙串;请求带签名。服务端返回登录失效时全局登出。
- 陌生人私聊有条数限制,超过进入审核。发布内容走服务端校验。
- 日志里不能带会话凭证。请求头进日志之前要按字段脱敏。
3. 关键流程
3.1 招募卡:发布、分享、回流
站外分享先落到可预览的页面,再打开 App。动态和话题用双层短链:内层负责回到详情,外层负责标题、描述和配图。进群类链接还可以在进群之后继续打开指定页。
3.2 动态:点完即走
失败则把稿子留在内存草稿里,可以重试。草稿不落盘,杀进程后这次发布不会自动续上。这和聊天媒体消息「状态随本地库落盘」是两条不同的选择。
3.3 小纸船:随机匹配后开聊
3.4 聊天媒体:先落库,再补传
带图或视频的消息如果在上传中被杀进程,再打开会永远转圈。做法是把上传状态写进消息的本地扩展,随即时通讯的本地库一起落盘。再次进入会话时按这些状态补传缺的资源,齐了再投递。
视频消息被杀进程后永久转圈的修复在这条状态上:导出产物和相册标识记在本地扩展里,重进会话时能接着走,而不是停在转圈。
3.5 深链:多入口,一道门闸
自定义 scheme、通用链接、推送、扫码和端内点击都进同一个解析。解析成功先写入一个待执行槽位,而不是立刻 push。登录、是否已进首页、网络、是否正在匹配,任一条件不满足就留着槽位,等下一次时机再试。
动态和话题的双层短链,以及进群后再打开指定页,都接在这套解析后面:外层负责站外预览,内层负责回到详情。
4. 几处值得展开的设计
下面按技术深度挑选。高亮标出其中的设计要点。完整过程见各篇案例。
案例 1:图文和视频消息的可靠发送
- 问题:图片、视频要先导出再上传再投递。杀进程或断网之后,消息会永远转圈。
- 方案:上传状态写进消息本地扩展,随本地库落盘。重进会话时只补传缺的资源,齐了再投递。视频消息被杀进程后永久转圈的修复
- 详细案例:/ios/cases/im-media-reliable-send
案例 2:配置门与错误码
- 问题:冷启动时大量请求同时发出,但都依赖一份全局配置。配置没回来就会打错环境。
- 方案:业务请求先过配置门。多个并发请求只拉一次配置,其余排队。错误码收成一套;登录失效只登出一次。
- 详细案例:/ios/cases/network-layer-config-gate
案例 3:上传分组与动态后台发布
- 问题:每种媒体只有一个全局上传分组时,发动态、换头像和回传日志会互相覆盖。
- 方案:管理器、分组、任务三层。凭证在组内复用,失败自动重试,结果带回执。后来改成每次上传一个独立分组。动态发布页和视频压缩做在这套框架上:点发布后页面关掉,列表先出现占位和进度。
- 详细案例:/ios/cases/upload-pipeline-evolution
案例 4:自定义消息与跨端类型码
- 问题:五十多种业务卡片走自定义消息。两端类型码曾经对不上,老版本收到新类型不能崩。
- 方案:双端一张类型码表,解码器注册到 SDK,再用
ViewModel工厂选气泡。未知类型降级为「不支持」。与 Android 对齐类型码、话题分享改号,以及公开群预览顺序和历史消息。旧类型码的本地消息在后续版本里兼容显示。 - 详细案例:/ios/cases/im-custom-message-protocol
案例 5:互动表情的内容寻址缓存
- 问题:麦位动画体积大、更新勤,不能打进安装包。
- 方案:后台配新资源,客户端按地址下载,用内容校验和核对,再解压到以校验和命名的目录。命中就复用。不支持的格式版本直接跳过。下载、校验与缓存
- 详细案例:/ios/cases/resource-pack-md5-cache
案例 6:日志门面
- 问题:海外用户的问题很难当场复现,需要本地日志、崩溃前因,以及把那台设备上的日志拿回来。
- 方案:一个门面同时写本地文件、崩溃平台面包屑和远程事件。正式包不打过细的调试级别。上次崩溃后,网络恢复时自动回传音视频 SDK 的日志。服务端也可以点名拉一段时间的日志。麦位表情的下载日志按下载、校验、解压、解析分段记下结果。
- 详细案例:/ios/cases/logging-observability
案例 7:深链路由
- 问题:入口很多,但跳转时机往往还没满足:未登录、还没进首页、正在匹配或没有网络。
- 方案:所有入口汇入一个解析。结果放进单槽,门闸通过后只执行一次。动态和话题先给站外预览,再回 App 指定页。双层短链,以及进群后再打开指定页
- 详细案例:/ios/cases/deeplink-route-dispatch
案例 8:会话与登录登出
- 问题:业务会话、即时通讯票、房间音视频票是三套凭证。客户端不在本地判断业务会话是否过期,也没有刷新后再重放请求。
- 方案:过期、被踢或封停都收成一次登出通知,并用一次性标志去重。主动退出和强制登出走的清理不一样。游客号靠钥匙串在卸装后恢复。
- 详细案例:/ios/cases/session-login-logout
5. 已经落地的优化
- 招募卡截图清晰度:提高绘制倍率,截图容器从列表改为可滚动容器,截图前先刷新。
- 动态发布后台化与进度:发布页拆出子控件,进度条加在主窗口上,只在白名单页面显示。
- 视频播放:封面按宽高自适应并限制比例,进入订阅页前暂停。
- 打招呼素材:改为进入聊天页时再下载,避免和冷启动抢带宽。
- 推荐用户列表:调整缓存和刷新时机,避免新用户第一次进去是空的、退出再进又不加载。
- 小红点:群精选拉到数据后同步一次红点,避免旧版本升级后红点还在。
6. 还可以做的
- 拆分语音房主控制器:按麦位、消息、玩法拆开,用状态机管房间状态。
- 模块边界:业务目录按包拆开,用协议和路由解耦。
- 网络层:从回调和全局单例迁到
async/await,请求能取消,读请求按签名去重。 - 列表:用
Diffable Data Source做差量刷新。 - 通知:全局通知改成强类型事件,减少隐式耦合。
- 可观测性:发布、分享回流、进房这几条链路补端到端耗时。
- 测试:为列表模型和协议解析补单元测试,并在持续集成里真正跑起来。
7. 重点模块与设计
每一条都是「要解决什么、设计是什么、为什么这样」。细节在对应案例里。
- 招募卡从站外回到指定页。 问题是分享出去的卡片既要能预览,又要回到 App 里的那张卡。设计是整卡截图加带参数的短链,点击后交给路由。招募卡的发布、截图和分享。见 深链路由。
- 动态点完即走。 问题是视频又大又慢,用户不该停在发布页。设计是提交后关页,列表先放占位,上传完成再换成正式动态。动态发布与视频压缩。见 上传与动态发布。
- 小纸船从随机匹配到私聊。 问题是两个陌生人要有一个轻的开场,而不是先加好友。设计是扔出、捞起、回复纸船卡片,再建立会话。悬浮入口和开聊。
- 聊天里的图片和视频不能永远转圈。 问题是上传中被杀掉以后,本地没有可恢复的状态。设计是状态跟消息一起落盘,重进会话只补传缺口。视频消息的可靠发送。见 可靠发送。
- 五十多种卡片要跨端对齐。 问题是类型码一旦不一致,另一端就显示成不支持。设计是一张类型码表加工厂,未知类型降级。类型码对齐和话题分享。见 自定义消息。
- 麦位动画不进安装包。 问题是资源大、上新勤。设计是按内容校验和下载、核对、缓存。资源包缓存。见 资源包。
- 节日活动按时间窗开关。 问题是玩法限时,不能靠发版。设计是后台下发时间窗,客户端只负责活动和奖励的界面。活动界面与在线时长奖励。
- 冷启动不能打错环境。 问题是首页、消息和个人页同时发请求,但都要等配置。设计是配置门把并发收成一次拉取。见 网络层。
- 线上问题要能把日志拿回来。 问题是崩溃报告看不到非崩溃的失败。设计是分级落盘、崩溃后自动回传,以及服务端点名拉日志。麦位表情下载日志。见 日志。
- 三套凭证不要混成一张票。 问题是业务请求、即时通讯和语音房各自要凭证,过期方式也不一样。设计是业务会话过期就登出,即时通讯票可以换,房间票到期只记下来。见 登录与会话。