出海语音社交App的设计架构

高级软件工程师(iOS)

  • Swift
  • UIKit
  • MVVM(数据驱动 TableView)
  • Moya + RxSwift + HandyJSON
  • 第三方 IM SDK
  • 第三方音视频 SDK
  • Firebase(Crashlytics / Analytics / Dynamic Links)
  • CocoaPods / Fastlane / GitLab CI

概述

这是一款面向日本市场的出海语音社交 App。玩家先在这里按游戏找队友开黑,再通过私聊 / 群聊、语音房、动态社区沉淀关系,后期加入了虚拟形象。客户端为纯 Swift,多人协作、周级发版。

下面按业务结构、技术架构、关键流程和几处设计来写。增长和互动相关的模块用高亮标出:招募卡、动态发布与视频、小纸船(漂流瓶)、全局分享与深链回流、麦位表情、节日活动。网络层、即时通讯和语音房等公共能力写在后面。

业务全景

运营与商业化内容社区社交关系找人与组队拉新与回流
多方式登录新手引导全局分享与深链找人与筛选招募卡小纸船私聊 / 群聊公开群与群活动语音房连麦动态发布话题 / 广场节日活动内购 / 装扮虚拟形象

同一套客户端也可以按业务模块来看。上面按用户路径串起来,下面按业务模块切开:IM、语音房、Feed、个人中心、活动与增长、商城与装扮。

商城与装扮活动与增长个人中心Feed语音房IM
私聊与群聊消息通知语音房连麦动态话题与广场个人资料关注与好友招募与纸船分享与深链节日活动内购资源包与装扮虚拟形象

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 分层

从上到下是四层:页面壳、业务模块、基础设施、第三方能力。业务模块只依赖基础设施暴露的接口。

第三方基础设施业务模块页面壳
页面控制器Cell 与自定义视图列表模型分享 / 纸船 / 活动 / 上传网络层即时通讯封装路由与深链全局配置持久化日志与崩溃IM SDK音视频 SDK崩溃与分析

技术栈从中心向外分四支:界面、网络在右侧,连接、工程在左侧。

客户端界面SwiftUIKit数据驱动列表网络MoyaRxSwiftHandyJSON连接第三方 IM第三方音视频工程CocoaPodsFastlaneGitLab CI

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:数据驱动的列表

点击
接口或会话数据组装分组模型单元格模型 类型 / 复用标识 / 高度列表基类自动注册单元格按模型渲染
ListViewModel组装分组充当数据源和代理CellModel单元格类型复用标识高度数据Cell按模型渲染

列表页继承同一个列表模型基类:它自己做 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 小纸船:随机匹配后开聊

即时通讯业务服务客户端用户B用户A即时通讯业务服务客户端用户B用户A写下纸船创建纸船打开 App是否有可捞的纸船有出现可拖拽的悬浮入口捞起并回复发送纸船卡片进入私聊

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 里的那张卡。设计是整卡截图加带参数的短链,点击后交给路由。招募卡的发布、截图和分享。见 深链路由。
  • 动态点完即走。 问题是视频又大又慢,用户不该停在发布页。设计是提交后关页,列表先放占位,上传完成再换成正式动态。动态发布与视频压缩。见 上传与动态发布。
  • 小纸船从随机匹配到私聊。 问题是两个陌生人要有一个轻的开场,而不是先加好友。设计是扔出、捞起、回复纸船卡片,再建立会话。悬浮入口和开聊。
  • 聊天里的图片和视频不能永远转圈。 问题是上传中被杀掉以后,本地没有可恢复的状态。设计是状态跟消息一起落盘,重进会话只补传缺口。视频消息的可靠发送。见 可靠发送。
  • 五十多种卡片要跨端对齐。 问题是类型码一旦不一致,另一端就显示成不支持。设计是一张类型码表加工厂,未知类型降级。类型码对齐和话题分享。见 自定义消息。
  • 麦位动画不进安装包。 问题是资源大、上新勤。设计是按内容校验和下载、核对、缓存。资源包缓存。见 资源包。
  • 节日活动按时间窗开关。 问题是玩法限时,不能靠发版。设计是后台下发时间窗,客户端只负责活动和奖励的界面。活动界面与在线时长奖励。
  • 冷启动不能打错环境。 问题是首页、消息和个人页同时发请求,但都要等配置。设计是配置门把并发收成一次拉取。见 网络层。
  • 线上问题要能把日志拿回来。 问题是崩溃报告看不到非崩溃的失败。设计是分级落盘、崩溃后自动回传,以及服务端点名拉日志。麦位表情下载日志。见 日志。
  • 三套凭证不要混成一张票。 问题是业务请求、即时通讯和语音房各自要凭证,过期方式也不一样。设计是业务会话过期就登出,即时通讯票可以换,房间票到期只记下来。见 登录与会话。