适用场景:海外语音直播 | 技术栈:Swift 5.9+ / Agora RTC 4.3+ / Agora RTM 2.2+ | 优化维度:网络、体验、运行、内存、耗电、可维护性、扩展性
一、网络连接稳定性优化(MENA 场景核心)
1.1 核心痛点
- MENA 地区:延迟 200-600ms、丢包 5%-20%、运营商互通差、跨境链路不稳定
- 常见问题:进房失败、中途断连、信令丢失、音频卡顿、麦位状态不同步
1.2 优化方案
1.2.1 多节点智能接入与 fallback
// RTC 配置全球节点,优先中东本地节点
let config = AgoraRtcEngineConfig()
config.appId = AppConfig.agoraAppId
config.areaCode = [.GLOB, .MENA] // 全球+中东双区域
config.logConfig.fileSizeInKB = 10 * 1024 // 增大日志便于排查
// 开启 DNS 预解析和多 IP fallback
rtcEngine?.setParameters("{"rtc.enable_dns_cache": true}")
rtcEngine?.setParameters("{"rtc.enable_multi_ip_fallback": true}")
1.2.2 自定义重连策略(覆盖默认)
// RTC 重连配置
rtcEngine?.setParameters("{"rtc.reconnect_max_delay": 30000}") // 最大重连间隔30s
rtcEngine?.setParameters("{"rtc.reconnect_total_timeout": 120000}") // 总超时2分钟
// RTM 自定义重连(RTM 2.x 默认重连较弱)
final class RTMReconnectManager {
private var retryCount = 0
private let maxRetryCount = 5
private var retryTask: Task<Void, Never>?
func handleConnectionError(_ error: Error) {
guard retryCount < maxRetryCount else {
// 重连失败,通知用户手动重连
NotificationCenter.default.post(name: .RTMReconnectFailed, object: nil)
return
}
let delay = pow(2.0, Double(retryCount)) // 指数退避
retryTask = Task { [weak self] in
try? await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
guard let self = self else { return }
do {
try await AgoraRTMService.shared.relogin()
self.retryCount = 0
} catch {
self.retryCount += 1
self.handleConnectionError(error)
}
}
}
}
1.2.3 信令与音频流分离保障
- 音频流优先:RTC 通道独立,不受 RTM 信令阻塞影响
- 关键信令确认机制:上麦 / 下麦 / 禁麦等核心信令增加 ACK 确认
// 带 ACK 的信令发送
func sendReliableSignal(_ signal: RTMSignal) async throws {
let messageId = UUID().uuidString
var signalWithId = signal
signalWithId.messageId = messageId
// 发送信令并等待 ACK
try await rtmService.sendBinarySignal(try JSONEncoder().encode(signalWithId))
// 超时等待 3s,未收到 ACK 则重发
try await withThrowingTaskGroup(of: Void.self) { group in
group.addTask {
try await Task.sleep(nanoseconds: 3 * 1_000_000_000)
throw NSError(domain: "SignalTimeout", code: -1)
}
group.addTask {
for await ack in ackStream where ack.messageId == messageId {
return
}
}
try await group.next()
group.cancelAll()
}
}
1.2.4 网络状态分级与动态策略
enum NetworkQualityLevel: Int {
case excellent = 0
case good = 1
case poor = 2
case bad = 3
case disconnected = 4
}
// 根据网络质量动态调整策略
func updateNetworkStrategy(_ quality: NetworkQualityLevel) {
switch quality {
case .excellent, .good:
rtcEngine?.setAudioProfile(.speechStandard, scenario: .chatRoomEntertainment)
rtmService.setMessagePriority(.high)
case .poor:
rtcEngine?.setAudioProfile(.speechLowQuality, scenario: .chatRoomEntertainment)
rtcEngine?.setParameters("{"che.audio.enable_agc": true}")
case .bad, .disconnected:
// 关闭非核心功能,只保留基础语音
rtcEngine?.setAudioProfile(.speechLowestQuality, scenario: .chatRoomEntertainment)
showWeakNetworkTip()
}
}
二、体验流畅度优化
2.1 音频体验优化(核心)
2.1.1 音频参数精准配置(MENA 语音专用)
// 语音房最优配置:16kHz 采样率、32kbps 码率、单声道
rtcEngine?.setAudioProfile(.speechStandard, scenario: .chatRoomEntertainment)
// 开启全链路音频优化
rtcEngine?.enableAudioVolumeIndication(200, smooth: 3, reportVad: true)
rtcEngine?.setParameters("{"che.audio.enable_ns": true}") // 降噪
rtcEngine?.setParameters("{"che.audio.enable_aec": true}") // 回声消除
rtcEngine?.setParameters("{"che.audio.enable_agc": true}") // 自动增益
2.1.2 音频焦点与冲突处理
// 全局音频会话管理,处理系统铃声、电话等中断
final class AudioSessionManager {
func setupAudioSession() {
NotificationCenter.default.addObserver(
self,
selector: #selector(handleAudioInterruption),
name: AVAudioSession.interruptionNotification,
object: nil
)
}
@objc private func handleAudioInterruption(_ notification: Notification) {
guard let userInfo = notification.userInfo,
let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
let type = AVAudioSession.InterruptionType(rawValue: typeValue) else { return }
switch type {
case .began:
// 中断开始:暂停本地音频,静音
rtcEngine?.muteLocalAudioStream(true)
case .ended:
// 中断结束:恢复音频
try? AVAudioSession.sharedInstance().setActive(true)
rtcEngine?.muteLocalAudioStream(false)
@unknown default: break
}
}
}
2.2 UI 流畅度优化
2.2.1 聊天列表高性能渲染
// 1. Cell 高度预计算
final class ChatMessageHeightCache {
private var cache: [String: CGFloat] = [:]
func height(for message: ChatMessage, width: CGFloat) -> CGFloat {
if let height = cache[message.messageId] {
return height
}
let height = calculateMessageHeight(message, width: width)
cache[message.messageId] = height
return height
}
}
// 2. 异步绘制 Cell 内容
class ChatMessageCell: UITableViewCell {
private let contentLabel = UILabel()
func configure(with message: ChatMessage) {
DispatchQueue.global().async {
let attributedText = self.generateAttributedText(message)
DispatchQueue.main.async {
self.contentLabel.attributedText = attributedText
}
}
}
}
2.2.2 交互防抖与状态锁
// 防止快速重复点击上麦按钮
private var isRequestingMic = false
func didTapRequestMic() {
guard !isRequestingMic else { return }
isRequestingMic = true
Task {
defer { isRequestingMic = false }
do {
try await interactor.requestMic(index: currentMicIndex)
} catch {
showError(message: "上麦失败")
}
}
}
三、运行稳定性优化
3.1 状态机严格管控(核心)
// 房间状态机,禁止非法状态转换
enum RoomState {
case idle
case joining
case connected
case reconnecting
case leaving
case disconnected
func canTransition(to newState: RoomState) -> Bool {
switch (self, newState) {
case (.idle, .joining): return true
case (.joining, .connected), (.joining, .disconnected): return true
case (.connected, .reconnecting), (.connected, .leaving): return true
case (.reconnecting, .connected), (.reconnecting, .disconnected): return true
case (.leaving, .disconnected): return true
case (.disconnected, .idle): return true
default: return false
}
}
}
// 状态转换统一入口
func transition(to newState: RoomState) {
guard currentState.canTransition(to: newState) else {
assertionFailure("非法状态转换: (currentState) -> (newState)")
return
}
currentState = newState
// 通知所有模块状态变更
NotificationCenter.default.post(name: .RoomStateChanged, object: newState)
}
3.2 SDK 异常统一捕获
// RTC 错误统一处理
extension VoiceChatInteractor: AgoraRtcEngineDelegate {
func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) {
Logger.error("RTC 错误: (errorCode.rawValue)")
switch errorCode {
case .joinChannelRejected:
transition(to: .disconnected)
presenter?.showError(message: "进房被拒绝")
case .tokenExpired:
// 自动刷新 token 并重连
Task {
do {
let newToken = try await fetchNewToken()
try await rtcService.renewToken(newToken)
} catch {
transition(to: .disconnected)
}
}
default: break
}
}
}
3.3 后台运行稳定性
// Info.plist 配置
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
// 后台保活加固
func setupBackgroundMode() {
NotificationCenter.default.addObserver(
self,
selector: #selector(handleAppDidEnterBackground),
name: UIApplication.didEnterBackgroundNotification,
object: nil
)
}
@objc private func handleAppDidEnterBackground() {
// 退后台时保持音频会话活跃
try? AVAudioSession.sharedInstance().setActive(
true,
options: .notifyOthersOnDeactivation
)
// 关闭非核心功能,减少 CPU 占用
rtcEngine?.enableVideo(false)
rtcEngine?.setParameters("{"rtc.enable_background_mode": true}")
}
四、内存优化
4.1 SDK 资源及时释放
// 退出房间时彻底销毁 SDK 实例
func leaveRoom() async throws {
transition(to: .leaving)
// 1. 停止所有音频流
rtcEngine?.muteLocalAudioStream(true)
rtcEngine?.stopLocalAudio()
// 2. 退出 RTC 和 RTM 房间
async let rtcLeave: () = rtcService.leaveRoom()
async let rtmLeave: () = rtmService.leaveChannel()
async let rtmLogout: () = rtmService.logout()
await rtcLeave
try await rtmLeave
try await rtmLogout
// 3. 销毁 SDK 引擎(关键!防止内存泄漏)
AgoraRtcEngineKit.destroy()
rtcService.destroy()
// 4. 清理所有缓存和代理
rtcService.delegate = nil
rtmService.delegate = nil
messageHeightCache.removeAll()
transition(to: .disconnected)
}
4.2 UI 组件内存管理
// 聊天图片缓存限制
final class ImageCacheManager {
static let shared = ImageCacheManager()
private let cache = NSCache<NSString, UIImage>()
private init() {
cache.countLimit = 100 // 最多缓存 100 张图片
cache.totalCostLimit = 50 * 1024 * 1024 // 最大 50MB
}
func setImage(_ image: UIImage, forKey key: String) {
cache.setObject(image, forKey: key as NSString, cost: Int(image.size.width * image.size.height))
}
func image(forKey key: String) -> UIImage? {
cache.object(forKey: key as NSString)
}
}
// 滚动时暂停图片加载
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
ImageDownloader.shared.isSuspended = true
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if !decelerate {
ImageDownloader.shared.isSuspended = false
}
}
4.3 循环引用预防
- 所有代理使用
weak var声明 - 闭包中使用
[weak self]或[unowned self] - 避免持有强引用的定时器,使用
Timer.scheduledTimer(withTimeInterval:repeats:block:)并在deinit中销毁 - 退出页面时主动断开所有通知监听
五、耗电优化
5.1 音频参数优化
// 观众模式下关闭本地采集,只接收远端音频
func switchToAudienceMode() {
let options = AgoraRtcChannelMediaOptions()
options.clientRoleType = .audience
options.publishMicrophoneTrack = false
options.autoSubscribeAudio = true
rtcEngine?.updateChannel(with: options)
// 关闭本地音频处理
rtcEngine?.stopLocalAudio()
}
// 弱网下降低音频码率
func adjustAudioQualityForWeakNetwork() {
rtcEngine?.setAudioProfile(.speechLowestQuality, scenario: .chatRoomEntertainment)
rtcEngine?.setParameters("{"che.audio.disable_high_quality_audio": true}")
}
5.2 网络与 CPU 优化
- 减少轮询请求,使用 RTM 信令或推送代替
- 合并网络请求,减少 TCP 握手次数
- 避免主线程频繁计算,将复杂逻辑放到后台线程
- 减少 UI 刷新频率,聊天列表每次只刷新新增的消息
5.3 后台耗电优化
- 退后台时关闭所有视频流、音量指示、动画效果
- 降低 RTC 后台编码码率
- 关闭非核心的日志上报和统计功能
- 避免后台频繁唤醒 APP
六、可维护性优化
6.1 严格分层与模块化
VoiceChatModule/
├── Base/ # 无业务侵入的基础组件
├── Capability/ # SDK 能力封装层(纯能力,无业务)
│ ├── AgoraRTCService.swift
│ └── AgoraRTMService.swift
├── Business/ # 业务逻辑层(VIPER)
│ ├── Entity/ # 数据模型
│ ├── Interactor/# 业务逻辑
│ ├── Presenter/ # 调度层
│ └── Router/ # 路由层
└── View/ # 纯 UI 层
6.2 协议化编程
// 所有模块基于协议定义,方便替换和测试
protocol RTCServiceProtocol {
func joinRoom(roomId: String, userId: String, token: String) async throws
func leaveRoom() async
func switchToAnchor()
func switchToAudience()
}
// 未来替换声网时,只需实现该协议即可
class TencentRTCService: RTCServiceProtocol {
// 实现协议方法
}
6.3 统一日志与错误体系
// 分级日志系统
enum LogLevel: Int {
case debug = 0
case info = 1
case warning = 2
case error = 3
}
final class Logger {
static func debug(_ message: String) { log(level: .debug, message: message) }
static func info(_ message: String) { log(level: .info, message: message) }
static func warning(_ message: String) { log(level: .warning, message: message) }
static func error(_ message: String) { log(level: .error, message: message) }
private static func log(level: LogLevel, message: String) {
#if DEBUG
print("[(level)] (message)")
#endif
// 上报到远程日志系统
}
}
七、扩展性优化
7.1 插件化架构
// 插件协议
protocol VoiceChatPlugin {
func pluginDidLoad(in room: VoiceRoom)
func pluginDidUnload()
func handleEvent(_ event: VoiceChatEvent)
}
// 插件管理器
final class VoiceChatPluginManager {
private var plugins: [VoiceChatPlugin] = []
func registerPlugin(_ plugin: VoiceChatPlugin) {
plugins.append(plugin)
plugin.pluginDidLoad(in: currentRoom!)
}
func unregisterPlugin(_ plugin: VoiceChatPlugin) {
plugins.removeAll { $0 === plugin }
plugin.pluginDidUnload()
}
func dispatchEvent(_ event: VoiceChatEvent) {
plugins.forEach { $0.handleEvent(event) }
}
}
// 礼物插件示例
class GiftPlugin: VoiceChatPlugin {
func pluginDidLoad(in room: VoiceRoom) {
// 初始化礼物面板
}
func handleEvent(_ event: VoiceChatEvent) {
if case .didReceiveGift(let gift) = event {
// 播放礼物动画
}
}
}
7.2 配置化驱动
// 服务器下发房间配置
struct RoomConfig: Codable {
let maxMicCount: Int
let enableGift: Bool
let enablePK: Bool
let audioQuality: String
let maxUserCount: Int
}
// 动态加载功能
func loadRoomFeatures(_ config: RoomConfig) {
if config.enableGift {
pluginManager.registerPlugin(GiftPlugin())
}
if config.enablePK {
pluginManager.registerPlugin(PKPlugin())
}
}
7.3 事件总线解耦
// 全局事件总线
enum VoiceChatEvent {
case didJoinRoom(VoiceRoom)
case didLeaveRoom
case didUserJoin(String)
case didUserLeave(String)
case didReceiveGift(Gift)
case didReceiveMessage(ChatMessage)
}
// 模块间通过事件通信,避免直接依赖
final class EventBus {
static let shared = EventBus()
private var handlers: [String: [(VoiceChatEvent) -> Void]] = [:]
func subscribe<T: AnyObject>(_ observer: T, eventType: VoiceChatEvent.Type, handler: @escaping (T, VoiceChatEvent) -> Void) {
let key = String(describing: eventType)
let wrappedHandler: (VoiceChatEvent) -> Void = { [weak observer] event in
guard let observer = observer else { return }
handler(observer, event)
}
handlers[key, default: []].append(wrappedHandler)
}
func publish(_ event: VoiceChatEvent) {
let key = String(describing: type(of: event))
handlers[key]?.forEach { $0(event) }
}
}
八、优化效果检查表
| 优化维度 | 检查项 | 预期效果 |
|---|---|---|
| 网络稳定性 | 多节点 fallback、自定义重连、信令 ACK | 进房成功率 >99%,断连自动恢复率 >95% |
| 体验流畅度 | 音频参数优化、UI 异步绘制、交互防抖 | 音频卡顿率 <2%,UI 帧率稳定 60fps |
| 运行稳定性 | 状态机管控、异常统一处理、后台保活 | 崩溃率 <0.1%,后台运行时长>2 小时 |
| 内存优化 | SDK 资源及时释放、图片缓存限制 | 峰值内存 <150MB,无内存泄漏 |
| 耗电优化 | 音频参数优化、后台功能裁剪 | 连续语音 1 小时耗电 <15% |
| 可维护性 | 分层架构、协议化、统一日志 | 新增功能开发周期缩短 50% |
| 扩展性 | 插件化、配置化、事件总线 | 新增功能无需修改核心代码 |