直播APP公屏优化

来源:互联网 发布:数据结构考研算法 编辑:程序博客网 时间:2024/04/30 19:47

直播APP频道公屏优化方案一些心得(未完)

做类似映客这种APP,频道性能问题是一个大问题。

现在在做直播APP,公屏上要的聊天记录,总是影响性能的一大部分原因,外加上 频道里面会有其他的操作,比如:倒计时,送礼物,视频本身,用户操作等等。下面记录一下iOS客户端本人的优化经历

公屏实现方案是UITableView,然后自定义不同的UITableViewCell子类,在需要的时候去加载。UITaleViewCell继承如下图所示这里写图片描述XXXBaseCell做一些基础的样式设置 XXMessageCell普通的聊天文本展示,XXGiftCell送礼物的频道内部提醒。最开始使用的是自动布局的方式做UI,

直奔主题,说优化

去掉自动布局的方案,原因是自动布局本身就是一个很复杂的算法。如果自动布局使用的不太好,还有可能造成离屏渲染,重复计算,像素重合的问题。

在数据Model中高度计算,并且缓存起来,横竖屏情况下,高度保证只计算一次。并且计算高度的任务放在后台。

/* *baseModel*/@interface XXXChannelChat : NSObject@property (nonatomic, assign) XXXChannelChatType chatType;@property (nonatomic, assign) CGFloat height;@property (nonatomic, assign) CGFloat fullScreenHeight;/** *  竖屏显示内容 横屏显示内容 */@property (nonatomic, strong) NSAttributedString *attributedString;@property (nonatomic, strong) NSAttributedString *fullScreenString;/** *  当前的高度 根据横竖屏 * *  @return 高度 */- (CGFloat)currentHeight;@end

每个数据Model做一个计算Layout的Class.比如:

@interface XXModel : NSObject@property (nonatomic, strong) NSString *text;@property (nonatomic, strong) NSString *senderName;@end@interface XXXLayout : NSObject- (id)initWithModel:(XXModel *)model;//普通的Frame@property (nonatomic, readonly) CGRect textFrame;//全屏的frame@property (nonatomic, readonly) CGRect fullScreenFrame;@end- (void)layoutSubviews {    [super layoutSubviews];    //设置Frame 记得加判断frame是否相等    self.label.frame = self.layout.labelFrame;}

这里的XXModel 应该从上面的BaseModel 继承。这里只是举个栗子。公屏消息 或者 送礼物, 或者 关注的消息过来的时候 先去初始化XXXLayout,当然放在后台线程
然后在每个Cell的layoutSubviews函数中去设置对应的Frame

TIPS:因为涉及到多线程,多以要防止一些在应该在主线程的操作放在后台,可以给UIView 加个分类,专门去做判断,比如:

使用runTime把系统的函数跟下面函数交换一下。很容易检测出来。- (void)XX_setNeedLayout {#ifdef DEBUG    XXAssertMainThread();#endif    [self lv_setNeedLayout];}- (void)XX_setNeedsDisplay {#ifdef DEBUG    XXAssertMainThread();#endif    [self XX_setNeedsDisplay];}- (void)XX_setNeedsDisplayInRect:(CGRect)rect {#ifdef DEBUG    XXAssertMainThread();#endif    [self XX_setNeedsDisplayInRect:rect];}

因为计算的Frame难免会有比如 50.669这种数字 像素对齐问题会有,影响渲染效果:所以做一些像素对齐的处理很有必要,如下:每一次设置Frame之前都要先调用一下roundPixelRect函数(ps:设置之前先调用CGRectEqualToRect函数进行判断,毕竟对象属性调整是非常消耗CPU的。所以能不调增就尽量不调整)。

static inline CGFloat screenScale() {    static CGFloat screenScale = 0.0;    static dispatch_once_t onceToken;    dispatch_once(&onceToken, ^{        if ([NSThread isMainThread]) {            screenScale = [[UIScreen mainScreen] scale];        } else {            dispatch_sync(dispatch_get_main_queue(), ^{                screenScale = [[UIScreen mainScreen] scale];            });        }    });    return screenScale;}static inline CGFloat roundPixelValue(CGFloat value) {    CGFloat scale = screenScale();    return round(value * scale) / scale;}static inline CGRect roundPixelRect(CGRect rect) {    return CGRectMake(roundPixelValue(rect.origin.x),                      roundPixelValue(rect.origin.y),                      roundPixelValue(rect.size.width),                      roundPixelValue(rect.size.height));}

预先申请一些Model的空间,大频率去刷UITableView ,不断的申请对CPU负荷也很大。所以,进入频道页面的时候,延迟1秒接受公屏消息,在后台申请好UITableViewCell 对应的Model空间,

//在后台线程预先申请100个数据Model//不用去初始化Model 的数据,ARC环境下会自动初始化为0 或者 NULL//GCDQueue 是自己写的一个方便操作GCD的工具[GCDQueue executeInLowPriorityGlobalQueue:^{        for(int i = 0; i < 100; ++ i) {            XXXChannelTextMessage *message = [XXXChannelTextMessage new];            if (message) {                [self.messageSet addObject:message];            }        }    }];/*** 对象不用的时候,同样捕捉到后台线程去释放。能重用尽量重用!!*/

UITableView刷新频率要控制,这里使用的RAC,如果对效率要求到极致,可以不用RAC,毕竟消息转发的层数太多。这里如果有消息,1秒刷新一次,4s这类机型,2秒刷新一次!!!实际上的效果不提明显,可能是我们APP的频道人数不够多!

- (void)reloadTableView{    if (self.reloadDisposeable) {//如果当前有更新任务,直接返回        return;    }    static NSTimeInterval timer = 1.0f;    static dispatch_once_t pre;    dispatch_once(&pre,^{        //如果有必要,区分一下5C.低端设备刷新频率控制        if ([SystemInfoUtility iosScreenResolution] == UIDevice_iPhone4SRes) {            timer *= 2;        }    });    //timer秒之后更新Tableview    self.reloadDisposeable = [[RACScheduler mainThreadScheduler] afterDelay:timer                                                                   schedule:^{                                                                       [self __update];                                                                   }];}- (void)__update {    if (self.reloadDisposeable) {//结束标记        [self.reloadDisposeable dispose];        self.reloadDisposeable = nil;    }    VIPPerformBlockOnMainThread(^{        [self.tableView reloadData];//更新TableView        [self scrollMessageTableToBottomIfNeeded:NO];    });}

尽量不使用__weak ,会增加把对象存入weak表的操作,weak对象也会加入autoreleasepool 中!

这里写图片描述模拟器上观察卡顿的条件要经常打开看!

调试阶段,引入KMCGeigerCounter 来检测界面的卡顿情况。虽然这个本身就会存在一点点性能问题

引入 MLeaksFinder 观察内存泄漏。当然最后还是要使用XCode 提供的工具再检测一下是否有内存泄漏。

频道消息超过一定范围,及时清理一些(放在后台线程中清理),或者全部。然后Model记得重用。

做的一些Test: 比较OC中循环遍历的几种方式,虽然网上已经有很多比较了 比如 大神的这篇 ios中集合遍历方法的比较和技巧但是,由于我们操作集合的对象不同,而且牵扯到多线程,所以自己又比较了一翻。结论也跟大神的一致。有一点,不要乱用NSLog

适当的使用缓存

使用NSCache对使用频率比较高的进行缓存,之所以选择NSCache是因为NSCache的又是比较明显:

NSCache类结合了各种自动删除策略,以确保不会占用过多的系统内存。如果其它应用需要内存时,系统自动执行这些策略。当调用这些策略时,会从缓存中删除一些对象,以最大限度减少内存的占用。
NSCache是线程安全的,我们可以在不同的线程中添加、删除和查询缓存中的对象,而不需要锁定缓存区域。
不像NSMutableDictionary对象,一个缓存对象不会拷贝key对象。

比如:公屏的消息要经过过滤率的。用户比较多的时候,大部分时候发的消息都一样:比如:6666 999 这样子的。连续几百个,几千个。每次过滤都会创建一个XML格式的对象去判断里面包含的类型能不能显示,频繁的申请空间,容易发热,对内存也是浪费。所以可以缓存:

//过滤Text能不能显示- (BOOL)filterAndAddChannelTexts:(NSString *)text{//text为空显示    if (!text) {        return YES;    }    //清除text两边的空格    NSString *cleanString = [text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];    if (!cleanString) {        return YES;    }    //缓存对象    //以为仅仅只是存放BOOL值,所以不设置大小    static NSCache *cache = nil;    static dispatch_once_t onceToken;    dispatch_once(&onceToken, ^{        cache = [NSCache new];    });    NSString *origin = [text copy];    NSNumber *number = [cache objectForKey:text];    if (number) {    //直接返回大小        return number.boolValue;    }    //创建XML对象进行过滤    ……

当然其他地方需要缓存的也尽量缓存一下。

使用RunLoop 把影响主线程的操作,分不同的时间段,提交到主线程,

- (void)XXXAddMessage {    CFRunLoopRef runLoop = CFRunLoopGetCurrent();    CFStringRef runLoopMode = kCFRunLoopDefaultMode;    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopBeforeWaiting, true, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity _) {        //提交一个 NSDefaultRunLoopMode 到runLoop        [self performSelector:@selector(AddMessage)                     onThread:[NSThread mainThread]                   withObject:nil                waitUntilDone:NO                        modes:@[NSDefaultRunLoopMode]];        CFRunLoopRemoveObserver(runLoop, observer, kCFRunLoopDefaultMode);        CFRelease(observer);    });    CFRunLoopAddObserver(runLoop, observer, runLoopMode);}- (void)AddMessage {    //addMessage操作}

在有UI刷新或者,用户操作界面的时候任务就会取消

XXAssertMainThread 宏实现

//必须是主线程执行。#define XXAssertMainThread() NSAssert([NSThread isMainThread], @"This method must be called on the main thread")

Core Graphics绘制会有很大的性能开销,所以频道频繁创建的视图,会避免使用! - 如果对视图实现了-drawRect:方法,或者CALayerDelegate的-drawLayer:inContext:方法,那么在绘制任何东西之前都会产生一个巨大的性能开销。为了支持对图层内容的任意绘制,Core Animation必须创建一个内存中等大小的寄宿图片。然后一旦绘制结束之后,必须把图片数据通过IPC传到渲染服务器。在此基础上,Core Graphics绘制就会变得十分缓慢,所以在一个对性能十分挑剔的场景下这样做十分不好。 所以实现起来越简单越好!如果有大量使用,值得考虑有没有更好的方案!

使用instruments观察性能,耗时间的地方!CPU GPU使用率。
GPU使用率过高的情况下可以把UIImage 的解码一些操作放在后台线程,提前解码到内存。
尽量使用轻量级的控件。UILabel 可以使用 layer来代替,UIImageView 如果没有其他交互使用layer也足够了

尽可能的合并网络请求。相同的网络请求次数过多,频率过高。

尽可能重用控件,数据!

控制线程的数目。针对业务,某些业务某些线程!

之所以做优化是因为频道里面人多的时候,公屏消息多,4s 5c 这样子的机器会卡顿。甚至频道里面人超过2万的时候高性能的机器也会发烫,发热 在做优化的过程中,参考了下面的连接。
参考链接:

每个版本APP做到最后必须做的事情

iOS APP性能优化

绘制像素到屏幕上,一定要搞懂!!!

绘制像素到屏幕上
YY大神的文章,要多看几遍才行
iOS保持界面流畅
iOS绘制一像素的线

0 0
原创粉丝点击