直播业务性能优化

Author Avatar
XibHe 9月 20, 2021
  • 在其它设备中阅读本文章

在上一篇文章详细阐述了如何从 0 到 1 开发一个完整的直播功能。本篇接着上一篇,主要对直播中遇到的一些性能问题、交互体验问题的优化方案。在某些用户的手机上会出现电量消耗过快、提示手机温度过高。

手机温度过高提示

根据实际情况,导致直播过程中手机发热、卡顿等问题的原因大致分为以下几个方面:

CPU 高占用率

直播拉流需要不断的访问网络,网络活动会唤起需要长时间周期性供电的无线电模组。CPU 使用率超过 20% 就会快速耗干电池电量。因此,需要针对直播中实时获取的不同拉流状态进行区别处理。

[MCInfoGatherManager sharedInstance].livePlayerWrapper.playErrorBlock = ^(int event, NSString *msg) {
        // 暂停中(或异常)
        [MCLiteKitLoading mcStartLoadingInView:weakSelf.playToolBarView];
        // 停止播放音视频流
        [[MCInfoGatherManager sharedInstance].livePlayerWrapper stopPlay];
};
  • 直播,网络断连,且经多次重连抢救无效,可以放弃治疗

  • 直播,获取加速拉流地址失败。这是由于您传给 (这是由于传给 liveplayer 的加速流地址中没有携带 txTime 和 txSecret 签名,或者是签名计算的不对。出现这个错误时,liveplayer 会放弃拉取加速流转而拉取 CDN 上的视频流,从而导致延迟很大)

  • 其它直播异常情况

当出现以上几种直播异常情况时,进行停止播放音视频流的操作,不再反复尝试连接网络拉流。直至收到正常流播放状态的回调,才重新开始联网拉流播放。

直播消息列表刷新频次

在直播间中,短时间内直播间用户聊天信息达到一个峰值后,此时如果不做任何处理,就会大频率的去刷新 UITableView ,造成 CPU 占用过大。因此,需要结合多种刷新机制,对聊天列表进行优化:

  • 节流处理。即,直播间消息列表在收到新消息后,进行节流处理。

所谓节流,就是规定在一个单位时间内,只能触发一次列表刷新函数。如果这个单位时间内触发多次函数,只有一次生效。这样就是降低了消息列表刷新及相关逻辑处理的频率。

  • 设置消息缓存机制。采用两个缓存池,一个是用于维护列表当前数据所存储的池,暂时叫 B 池;另一个用于接收处理好的 IM 数据缓存池,暂时叫 A 池;当有新 IM 消息过来时,把处理好的数据存入到 A 缓存池中。在结合后面会提到的数据加载及展示逻辑,把 A 缓存池中的数据合并到 B 缓存池中,然后,清空 A 缓存池数据。

  • 数据加载及展示逻辑。用户手动滑动消息列表,查看历史消息后收到新消息时的交互处理:监听或记录用户滑动屏幕的状态,是否正在滑动消息列表及消息列表是否正在滑动。如果正在滑动消息列表,此时无论是否收到新消息,都不滚动到列表底部;如果此时监听到滚动停止,则定时 3 秒后,此时,若缓存的消息数组中有新消息,则滚动到列表底部,缓存的消息数组中没有新消息,则不做处理。

在直播中点击进入沉浸式后,特殊处理 IM 消息列表,此时,存储新消息但不做新消息的高度计算、富文本样式设置、消息列表刷新等耗时操作,待下次关闭沉浸式后,统一处理前一次存储的消息及新收到的消息(默认当前消息列表最多插入 300 条消息,超出 300 条清除前 100 条数据)

审查项目代码并借助 Instruments 工具监测内存、时间探查、电量消耗

  • GIF 图片加载尺寸过大的图片时,对 CPU 性能的消耗

通过使用 Instruments 工具分析,定位到 App 首页 CMS 模块存在未能完全释放的对象,最后定位到 FLAnimatedImageView 三方图片展示库,在首页加载 gif 图片时,在 gif 过大的情况下,会牺牲 CPU 性能来保证内存的低占用率。 因此,要么限制上传 GIF 动图的大小;要么使用 SDWebImage 加载gif图片,但是用 SDWebImage 去加载缓存图片,会出现内存增多,SDWebImage 对多gif显示内存消耗过高,需要优化一下。gif播放其实就是一张一张图片返回去,我们要写一个方法,只要不断地取出当前的那一张图片,这样就可以有效的避免内存中存储了大量图片.那如何实现不断地去取呢,我们可以开一个定时器,定时器不断的去调用我们写的方法,不断获取图片并赋值给 imageView.

  • 轮播图自动滚动定时器未及时释放

如果是从首页进入的直播间,此时,首页正好有正在滚动的轮播图。在进入直播间后,通过 Instruments 的 Allocations 工具定位到首页轮播图定时器有可能存在问题,在对比首页有轮播图与没有轮播图内存增长情况后发现,配置了轮播图的模块短时间内存增长更多。对象释放的不及时,会在内存中保留一段时间,导致内存的增长。因此,可以在需要页面消失时候停止定时器,在页面出现重新开启定时器,如果是 view 可以重写视图生命周期方法:

- (void)willMoveToWindow:(UIWindow *)newWindow {
    [super willMoveToWindow:newWindow];
    if (!newWindow) {
        [self invalidateTimeEvent];
    }  else {
        [self startTimeEvent];
    }
}
- (void)didMoveToSuperview {
    [super didMoveToSuperview];
    [self handleAutoTimer];
}

- (void)didMoveToWindow {
    [super didMoveToWindow];
    [self handleAutoTimer];
}

在生命周期方法中做停止/开启逻辑。

低端机型的适配

低端机型未适配不同分辨率,在高码率、高分辨率下导致 CPU 占用过高。视频分辨率、码率成正比,比如720P、1080P 视频更容易发热。这一点在低端机型上尤为明显。可以在进入直播间时根据当前手机的型号、电量对某些低端机型做出特殊处理,如果判断为低端机型。就使用低分辨率的流程模式(360)。

  1. 自动模式:如果您不太确定您的主要场景是什么,可以直接选择这个模式
    把 TXLivePlayConfig 中的 setAutoAdjustCache 开关打开,即为自动模式。在该模式下播放器会根据当前网络情况,对延迟进行自动调节(默认情况下播放器会在1秒 - 5秒这个区间内自动调节延迟大小,您可以通过 setMinCacheTime 和 setMaxCacheTime 对默认值进行修改),以保证在足够流畅的情况下尽量降低观众跟主播端的延迟,确保良好的互动体验。

  2. 极速模式:主要适用于秀场直播等互动性高,并且对延迟要求比较苛刻的场景
    极速模式设置方法是 setMinCacheTime = setMaxCacheTime = 1s,自动模式跟极速模式的差异只是 MaxCacheTime 有所不同 (极速模式的 MaxCacheTime 一般比较低,而自动模式的 MaxCacheTime 则相对较高 ),这种灵活性主要得益于 SDK 内部的自动调控技术,可以在不引入卡顿的情况下自动修正延时大小,而 MaxCacheTime 反应的就是调节速度:MaxCacheTime 的值越大,调控速度会越发保守,卡顿概率就会越低。

  3. 流畅模式:主要适用于游戏直播等大码率高清直播场景。
    当把播放器中的 setAutoAdjustCache 开关关闭,即为流畅模式,在该模式下播放器采取的处理策略跟 Adobe Flash 内核的缓存策略如出一辙:当视频出现卡顿后,会进入 loading 状态直到缓冲区蓄满,之后进入 playing 状态,直到下一次遭遇无法抵御的网络波动。默认情况下缓冲大小为5秒,您可以通过 setCacheTime 进行更改。
    在延迟要求不高的场景下,这种看似简单的模式会更加可靠,因为该模式本质上就是通过牺牲一点延迟来降低卡顿率。

软解码 OR 硬解码

开启硬件加速,

    _player = [[TXLivePlayer alloc] init];
    // 开启硬件加速(默认为 NO)
    [_player setEnableHWAcceleration:YES];

点赞等高频动画

控制点赞频率,尤其是收到自定义点赞消息时,由于是连续的触发点赞。此时,若不去控制点赞动画绘制的次数,就会连续不停的触发点赞动画,消耗性能。因此,这里需要做一个针对点赞动效的频率控制逻辑,同时需要将下发的点赞消息依次触发,以实现连续不断的点赞效果。

- (instancetype)initWithFrame:(CGRect)frame {
    if (self = [super initWithFrame:frame]) {
        // 1.创建一个串行队列,保证for循环依次执行
        _serialQueue = dispatch_queue_create("animationSerialQueue", DISPATCH_QUEUE_SERIAL);
        // 2.创建一个数目为1的信号量,用于“卡”for循环,等上次循环结束在执行下一次的for循环
        _sema = dispatch_semaphore_create(1);
    }
    return self;
}
#pragma mark - 点赞消息
- (void)messagePraiseWithLikeCount:(NSInteger)likeCount animationCount:(NSInteger)animationCount likeTitle:(NSString *)likeTitle {
  if (animationCount > 0) {
        MCWeakSelf
        // 3.异步执行任务
        dispatch_async(_serialQueue, ^{
            // 客户端当接收到点赞通知时,判断数量num是否大于20,如果小于等于20,则绘制动画num次,如果大于20次,则只绘制20
            for (NSInteger i = 0; i < animationCount; i++) {

                MCStrongSelf
                dispatch_async(dispatch_get_main_queue(), ^{
                    [strongSelf showLikeHeartStartRect:strongSelf.likeButton.frame animationKey:@"HearLikeMessage" withStartBlock:^{
                        NSLog(@"本次耗时操作完成,信号量+1 %@\n",[NSThread currentThread]);
                        // 5.本次for循环的异步任务执行完毕,这时候要发一个信号,若不发,下次操作将永远不会触发
                        dispatch_semaphore_signal(self->_sema);
                    }];
                });

                // 4.开始执行for循环,让信号量-1,这样下次操作须等信号量>=0才会继续,否则下次操作将永久停止
                dispatch_semaphore_wait(self->_sema, DISPATCH_TIME_FOREVER);
                NSLog(@"信号量等待中\n");
            }
        });
    }
}
  1. 创建一个串行队列,保证for循环依次执行

  2. 创建一个数目为 1 的信号量,用于“卡”for循环,等上次循环结束在执行下一次的for循环

  3. 基于一个串行队列,异步执行任务

  4. 开始执行for循环,让信号量-1,这样下次操作须等信号量 >=0 才会继续,否则下次操作将永久停止

  5. 本次 for 循环的异步任务执行完毕,这时候要发一个信号,若不发,下次操作将永远不会触发

客户端当接收到点赞通知时,判断数量 num 是否大于 20,如果小于等于20,则绘制动画 num 次,如果大于 20 次,则最多绘制 20 次点赞动画。

业务层不合理的设计

  • 直播间管理器与聊天消息管理器分割开,二者可以合并为一套统一的直播间消息管理器,以处理直播间各种消息(IM 登录直播间、加入直播群组、加入直播间、点赞消息、普通消息、其它自定义消息)

  • 生成的直播播放器管理工具,未能将播放状态、异常的处理逻辑彻底聚合在一起,需要进一步完善。

优化前后对比

CPU 占用率,优化前

CPU 优化前

优化后,

CPU 优化后

参考文档

蘑菇街直播实践

映客直播iOS App 性能优化实践

直播间聊天消息列表卡顿优化

iOS 性能优化💡被压测卡爆的语音房间

–EOF–

若无特别说明,本篇文章均为原创,转载请保留链接,谢谢!