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

直播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绘制一像素的线

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,424评论 25 707
  • (1)Time Profiler:用来测量被方法/函数打断的CPU使用情况。 (2)Core Animation:...
    钱嘘嘘阅读 1,455评论 2 6
  • java 接口的意义-百度 规范、扩展、回调 抽象类的意义-乐视 为其子类提供一个公共的类型封装子类中得重复内容定...
    交流电1582阅读 2,207评论 0 11
  • 站在教室窗前,林三儿和女朋友黄儿闹别扭后,心烦气躁,忍不住用作业本纸折了架纸飞机,扔了出去。他没想到,就因为这个举...
    扉鱼与金紫阅读 334评论 0 1
  • 高中的时候,家乡有一家著名的理发连锁店,开遍大街小巷,由一位女士创办,她是家乡的企业家创业之典型。零几年的时候,洗...
    讲话学习阅读 545评论 0 0