直播APP公屏优化记录
标签(空格分隔): iOS
直播APP频道公屏优化方案一些心得(未完)
做类似映客这种APP,频道性能问题是一个大问题。
公屏实现方案是<code>UITableView</code>,然后自定义不同的<code>UITableViewCell</code>子类,在需要的时候去加载。<code>UITaleViewCell</code>继承如下图所示现在在做直播APP,公屏上要的聊天记录,总是影响性能的一大部分原因,外加上 频道里面会有其他的操作,比如:倒计时,送礼物,视频本身,用户操作等等。下面记录一下iOS客户端本人的优化经历
<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做到最后必须做的事情
绘制像素到屏幕上,一定要搞懂!!!
绘制像素到屏幕上
YY大神的文章,要多看几遍才行
iOS保持界面流畅
iOS绘制一像素的线