直播APP公屏優(yōu)化記錄
標(biāo)簽(空格分隔): iOS
直播APP頻道公屏優(yōu)化方案一些心得(未完)
做類似映客這種APP,頻道性能問題是一個(gè)大問題遂鹊。
公屏實(shí)現(xiàn)方案是<code>UITableView</code>,然后自定義不同的<code>UITableViewCell</code>子類浮创,在需要的時(shí)候去加載。<code>UITaleViewCell</code>繼承如下圖所示現(xiàn)在在做直播APP,公屏上要的聊天記錄摔踱,總是影響性能的一大部分原因钧椰,外加上 頻道里面會有其他的操作,比如:倒計(jì)時(shí)杏瞻,送禮物所刀,視頻本身,用戶操作等等捞挥。下面記錄一下iOS客戶端本人的優(yōu)化經(jīng)歷
<code>XXXBaseCell</code>做一些基礎(chǔ)的樣式設(shè)置 <code>XXMessageCell</code>普通的聊天文本展示砌函,<code>XXGiftCell</code>送禮物的頻道內(nèi)部提醒斩披。最開始使用的是自動布局的方式做<code>UI</code>,
直奔主題,說優(yōu)化
去掉自動布局的方案讹俊,原因是自動布局本身就是一個(gè)很復(fù)雜的算法垦沉。如果自動布局使用的不太好,還有可能造成離屏渲染仍劈,重復(fù)計(jì)算厕倍,像素重合的問題。
在數(shù)據(jù)Model中高度計(jì)算贩疙,并且緩存起來讹弯,橫豎屏情況下况既,高度保證只計(jì)算一次。并且計(jì)算高度的任務(wù)放在后臺组民。
/*
*baseModel
*/
@interface XXXChannelChat : NSObject
@property (nonatomic, assign) XXXChannelChatType chatType;
@property (nonatomic, assign) CGFloat height;
@property (nonatomic, assign) CGFloat fullScreenHeight;
/**
* 豎屏顯示內(nèi)容 橫屏顯示內(nèi)容
*/
@property (nonatomic, strong) NSAttributedString *attributedString;
@property (nonatomic, strong) NSAttributedString *fullScreenString;
/**
* 當(dāng)前的高度 根據(jù)橫豎屏
*
* @return 高度
*/
- (CGFloat)currentHeight;
@end
每個(gè)數(shù)據(jù)Model做一個(gè)計(jì)算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];
//設(shè)置Frame 記得加判斷frame是否相等
self.label.frame = self.layout.labelFrame;
}
這里的XXModel 應(yīng)該從上面的BaseModel 繼承棒仍。這里只是舉個(gè)栗子。公屏消息 或者 送禮物邪乍, 或者 關(guān)注的消息過來的時(shí)候 先去初始化<code>XXXLayout</code>,<strong>當(dāng)然放在后臺線程</strong>
然后在每個(gè)Cell的<code>layoutSubviews</code>函數(shù)中去設(shè)置對應(yīng)的<code>Frame</code>
TIPS:因?yàn)樯婕暗蕉嗑€程,多以要防止一些在應(yīng)該在主線程的操作放在后臺对竣,可以給UIView 加個(gè)分類庇楞,專門去做判斷,比如:
使用runTime把系統(tǒng)的函數(shù)跟下面函數(shù)交換一下否纬。很容易檢測出來吕晌。
- (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];
}
因?yàn)橛?jì)算的Frame難免會有比如 50.669
這種數(shù)字 像素對齊問題會有,影響渲染效果:所以做一些像素對齊的處理很有必要临燃,如下:每一次設(shè)置Frame之前都要先調(diào)用一下<code>roundPixelRect</code>函數(shù)(ps:設(shè)置之前先調(diào)用CGRectEqualToRect
函數(shù)進(jìn)行判斷睛驳,畢竟對象屬性調(diào)整是非常消耗CPU的。所以能不調(diào)增就盡量不調(diào)整)膜廊。
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));
}
預(yù)先申請一些Model的空間乏沸,大頻率去刷UITableView ,不斷的申請對CPU負(fù)荷也很大爪瓜。所以蹬跃,進(jìn)入頻道頁面的時(shí)候,延遲1秒接受公屏消息铆铆,在后臺申請好UITableViewCell 對應(yīng)的Model空間蝶缀,
//在后臺線程預(yù)先申請100個(gè)數(shù)據(jù)Model
//不用去初始化Model 的數(shù)據(jù),ARC環(huán)境下會自動初始化為0 或者 NULL
//GCDQueue 是自己寫的一個(gè)方便操作GCD的工具
[GCDQueue executeInLowPriorityGlobalQueue:^{
for(int i = 0; i < 100; ++ i) {
XXXChannelTextMessage *message = [XXXChannelTextMessage new];
if (message) {
[self.messageSet addObject:message];
}
}
}];
/*
** 對象不用的時(shí)候薄货,同樣捕捉到后臺線程去釋放翁都。能重用盡量重用!谅猾!
*/
<code>UITableView</code>刷新頻率要控制柄慰,這里使用的RAC,如果對效率要求到極致,可以不用RAC,畢竟消息轉(zhuǎn)發(fā)的層數(shù)太多税娜。這里如果有消息先煎,1秒刷新一次,4s這類機(jī)型巧涧,2秒刷新一次J硇!谤绳!實(shí)際上的效果不提明顯占锯,可能是我們APP的頻道人數(shù)不夠多袒哥!
- (void)reloadTableView
{
if (self.reloadDisposeable) {//如果當(dāng)前有更新任務(wù),直接返回
return;
}
static NSTimeInterval timer = 1.0f;
static dispatch_once_t pre;
dispatch_once(&pre,^{
//如果有必要消略,區(qū)分一下5C.低端設(shè)備刷新頻率控制
if ([SystemInfoUtility iosScreenResolution] == UIDevice_iPhone4SRes) {
timer *= 2;
}
});
//timer秒之后更新Tableview
self.reloadDisposeable = [[RACScheduler mainThreadScheduler] afterDelay:timer
schedule:^{
[self __update];
}];
}
- (void)__update {
if (self.reloadDisposeable) {//結(jié)束標(biāo)記
[self.reloadDisposeable dispose];
self.reloadDisposeable = nil;
}
VIPPerformBlockOnMainThread(^{
[self.tableView reloadData];//更新TableView
[self scrollMessageTableToBottomIfNeeded:NO];
});
}
盡量不使用__weak ,會增加把對象存入weak表的操作堡称,weak對象也會加入autoreleasepool 中!
模擬器上觀察卡頓的條件要經(jīng)常打開看艺演!
調(diào)試階段却紧,引入KMCGeigerCounter
來檢測界面的卡頓情況。雖然這個(gè)本身就會存在一點(diǎn)點(diǎn)性能問題
引入 MLeaksFinder
觀察內(nèi)存泄漏胎撤。當(dāng)然最后還是要使用XCode 提供的工具再檢測一下是否有內(nèi)存泄漏晓殊。
頻道消息超過一定范圍,及時(shí)清理一些(放在后臺線程中清理)伤提,或者全部巫俺。然后Model記得重用。
做的一些Test: 比較OC中循環(huán)遍歷的幾種方式肿男,雖然網(wǎng)上已經(jīng)有很多比較了 比如 大神的這篇 ios中集合遍歷方法的比較和技巧但是介汹,由于我們操作集合的對象不同,而且牽扯到多線程舶沛,所以自己又比較了一翻嘹承。結(jié)論也跟大神的一致。有一點(diǎn)如庭,不要亂用<code>NSLog</code>
適當(dāng)?shù)氖褂镁彺?/h3>
使用<code>NSCache</code>對使用頻率比較高的進(jìn)行緩存赶撰,之所以選擇NSCache是因?yàn)镹SCache的又是比較明顯:
NSCache類結(jié)合了各種自動刪除策略,以確保不會占用過多的系統(tǒng)內(nèi)存柱彻。如果其它應(yīng)用需要內(nèi)存時(shí)豪娜,系統(tǒng)自動執(zhí)行這些策略。當(dāng)調(diào)用這些策略時(shí)哟楷,會從緩存中刪除一些對象瘤载,以最大限度減少內(nèi)存的占用。
NSCache是線程安全的卖擅,我們可以在不同的線程中添加鸣奔、刪除和查詢緩存中的對象,而不需要鎖定緩存區(qū)域惩阶。
不像NSMutableDictionary對象挎狸,一個(gè)緩存對象不會拷貝key對象。
比如:公屏的消息要經(jīng)過過濾率的断楷。用戶比較多的時(shí)候锨匆,大部分時(shí)候發(fā)的消息都一樣:比如:6666 999 這樣子的。連續(xù)幾百個(gè)冬筒,幾千個(gè)恐锣。每次過濾都會創(chuàng)建一個(gè)XML格式的對象去判斷里面包含的類型能不能顯示茅主,頻繁的申請空間,容易發(fā)熱土榴,對內(nèi)存也是浪費(fèi)诀姚。所以可以緩存:
//過濾Text能不能顯示
- (BOOL)filterAndAddChannelTexts:(NSString *)text
{
//text為空顯示
if (!text) {
return YES;
}
//清除text兩邊的空格
NSString *cleanString = [text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
if (!cleanString) {
return YES;
}
//緩存對象
//以為僅僅只是存放BOOL值,所以不設(shè)置大小
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;
}
//創(chuàng)建XML對象進(jìn)行過濾
……
使用<code>NSCache</code>對使用頻率比較高的進(jìn)行緩存赶撰,之所以選擇NSCache是因?yàn)镹SCache的又是比較明顯:
NSCache類結(jié)合了各種自動刪除策略,以確保不會占用過多的系統(tǒng)內(nèi)存柱彻。如果其它應(yīng)用需要內(nèi)存時(shí)豪娜,系統(tǒng)自動執(zhí)行這些策略。當(dāng)調(diào)用這些策略時(shí)哟楷,會從緩存中刪除一些對象瘤载,以最大限度減少內(nèi)存的占用。
NSCache是線程安全的卖擅,我們可以在不同的線程中添加鸣奔、刪除和查詢緩存中的對象,而不需要鎖定緩存區(qū)域惩阶。
不像NSMutableDictionary對象挎狸,一個(gè)緩存對象不會拷貝key對象。
比如:公屏的消息要經(jīng)過過濾率的断楷。用戶比較多的時(shí)候锨匆,大部分時(shí)候發(fā)的消息都一樣:比如:6666 999 這樣子的。連續(xù)幾百個(gè)冬筒,幾千個(gè)恐锣。每次過濾都會創(chuàng)建一個(gè)XML格式的對象去判斷里面包含的類型能不能顯示茅主,頻繁的申請空間,容易發(fā)熱土榴,對內(nèi)存也是浪費(fèi)诀姚。所以可以緩存:
//過濾Text能不能顯示
- (BOOL)filterAndAddChannelTexts:(NSString *)text
{
//text為空顯示
if (!text) {
return YES;
}
//清除text兩邊的空格
NSString *cleanString = [text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
if (!cleanString) {
return YES;
}
//緩存對象
//以為僅僅只是存放BOOL值,所以不設(shè)置大小
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;
}
//創(chuàng)建XML對象進(jìn)行過濾
……
當(dāng)然其他地方需要緩存的也盡量緩存一下玷禽。
使用RunLoop 把影響主線程的操作赫段,分不同的時(shí)間段,提交到主線程矢赁,
- (void)XXXAddMessage {
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFStringRef runLoopMode = kCFRunLoopDefaultMode;
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopBeforeWaiting, true, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity _) {
//提交一個(gè) 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刷新或者糯笙,用戶操作界面的時(shí)候任務(wù)就會取消
<code>XXAssertMainThread</code> 宏實(shí)現(xiàn)
//必須是主線程執(zhí)行。
#define XXAssertMainThread() NSAssert([NSThread isMainThread], @"This method must be called on the main thread")
Core Graphics繪制會有很大的性能開銷坯台,所以頻道頻繁創(chuàng)建的視圖炬丸,會避免使用瘫寝! - 如果對視圖實(shí)現(xiàn)了-drawRect:方法蜒蕾,或者CALayerDelegate的-drawLayer:inContext:方法,那么在繪制任何東西之前都會產(chǎn)生一個(gè)巨大的性能開銷焕阿。為了支持對圖層內(nèi)容的任意繪制咪啡,Core Animation必須創(chuàng)建一個(gè)內(nèi)存中等大小的寄宿圖片。然后一旦繪制結(jié)束之后暮屡,必須把圖片數(shù)據(jù)通過IPC傳到渲染服務(wù)器撤摸。在此基礎(chǔ)上,Core Graphics繪制就會變得十分緩慢褒纲,所以在一個(gè)對性能十分挑剔的場景下這樣做十分不好准夷。 所以實(shí)現(xiàn)起來越簡單越好!如果有大量使用莺掠,值得考慮有沒有更好的方案衫嵌!
使用<code>instruments</code>觀察性能,耗時(shí)間的地方彻秆!CPU GPU使用率楔绞。
GPU使用率過高的情況下可以把UIImage 的解碼一些操作放在后臺線程,提前解碼到內(nèi)存唇兑。
盡量使用輕量級的控件酒朵。UILabel 可以使用 layer來代替,UIImageView 如果沒有其他交互使用layer也足夠了
盡可能的合并網(wǎng)絡(luò)請求扎附。相同的網(wǎng)絡(luò)請求次數(shù)過多蔫耽,頻率過高。
盡可能重用控件留夜,數(shù)據(jù)针肥!
控制線程的數(shù)目饼记。針對業(yè)務(wù),某些業(yè)務(wù)某些線程慰枕!
之所以做優(yōu)化是因?yàn)轭l道里面人多的時(shí)候具则,公屏消息多,4s 5c 這樣子的機(jī)器會卡頓具帮。甚至頻道里面人超過2萬的時(shí)候高性能的機(jī)器也會發(fā)燙博肋,發(fā)熱 在做優(yōu)化的過程中,參考了下面的連接蜂厅。
參考鏈接:
每個(gè)版本APP做到最后必須做的事情
繪制像素到屏幕上匪凡,一定要搞懂!>蛟场病游!
繪制像素到屏幕上
YY大神的文章,要多看幾遍才行
iOS保持界面流暢
iOS繪制一像素的線