iOS 语音聊天室(Agora RTC+RTM)优化指南

1 阅读8分钟

适用场景:海外语音直播 | 技术栈: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%
扩展性插件化、配置化、事件总线新增功能无需修改核心代码