美文网首页
UITableView 优化记录- 一年前的项目了

UITableView 优化记录- 一年前的项目了

作者: 杨柳小易 | 来源:发表于2017-02-14 23:20 被阅读70次

    直播APP公屏优化记录

    标签(空格分隔): iOS


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

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

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

    公屏实现方案是<code>UITableView</code>,然后自定义不同的<code>UITableViewCell</code>子类,在需要的时候去加载。<code>UITaleViewCell</code>继承如下图所示 这里写图片描述

    <code>XXXBaseCell</code>做一些基础的样式设置 <code>XXMessageCell</code>普通的聊天文本展示,<code>XXGiftCell</code>送礼物的频道内部提醒。最开始使用的是自动布局的方式做<code>UI</code>,

    直奔主题,说优化

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

    在数据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 继承。这里只是举个栗子。公屏消息 或者 送礼物, 或者 关注的消息过来的时候 先去初始化<code>XXXLayout</code>,<strong>当然放在后台线程</strong>
    然后在每个Cell的<code>layoutSubviews</code>函数中去设置对应的<code>Frame</code>

    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之前都要先调用一下<code>roundPixelRect</code>函数(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];
                }
            }
        }];
    /*
    ** 对象不用的时候,同样捕捉到后台线程去释放。能重用尽量重用!!
    */
    

    <code>UITableView</code>刷新频率要控制,这里使用的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中集合遍历方法的比较和技巧但是,由于我们操作集合的对象不同,而且牵扯到多线程,所以自己又比较了一翻。结论也跟大神的一致。有一点,不要乱用<code>NSLog</code>

    适当的使用缓存

    使用<code>NSCache</code>对使用频率比较高的进行缓存,之所以选择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刷新或者,用户操作界面的时候任务就会取消

    <code>XXAssertMainThread</code> 宏实现

    //必须是主线程执行。
    #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绘制就会变得十分缓慢,所以在一个对性能十分挑剔的场景下这样做十分不好。 所以实现起来越简单越好!如果有大量使用,值得考虑有没有更好的方案!

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

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

    尽可能重用控件,数据!

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

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

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

    iOS APP性能优化

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

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

    相关文章

      网友评论

          本文标题:UITableView 优化记录- 一年前的项目了

          本文链接:https://www.haomeiwen.com/subject/ybmfwttx.html