UITableView 優(yōu)化記錄- 一年前的項(xiàng)目了

直播APP公屏優(yōu)化記錄

標(biāo)簽(空格分隔): iOS


直播APP頻道公屏優(yōu)化方案一些心得(未完)

做類似映客這種APP,頻道性能問題是一個(gè)大問題遂鹊。

現(xiàn)在在做直播APP,公屏上要的聊天記錄摔踱,總是影響性能的一大部分原因钧椰,外加上 頻道里面會有其他的操作,比如:倒計(jì)時(shí)杏瞻,送禮物所刀,視頻本身,用戶操作等等捞挥。下面記錄一下iOS客戶端本人的優(yōu)化經(jīng)歷

公屏實(shí)現(xiàn)方案是<code>UITableView</code>,然后自定義不同的<code>UITableViewCell</code>子類浮创,在需要的時(shí)候去加載。<code>UITaleViewCell</code>繼承如下圖所示
這里寫圖片描述

<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)行過濾
    ……

當(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做到最后必須做的事情

iOS APP性能優(yōu)化

繪制像素到屏幕上匪凡,一定要搞懂!>蛟场病游!

繪制像素到屏幕上
YY大神的文章,要多看幾遍才行
iOS保持界面流暢
iOS繪制一像素的線

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末稠通,一起剝皮案震驚了整個(gè)濱河市衬衬,隨后出現(xiàn)的幾起案子婶肩,更是在濱河造成了極大的恐慌利凑,老刑警劉巖欲芹,帶你破解...
    沈念sama閱讀 222,865評論 6 518
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件眨层,死亡現(xiàn)場離奇詭異鲸鹦,居然都是意外死亡腺占,警方通過查閱死者的電腦和手機(jī)圾旨,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,296評論 3 399
  • 文/潘曉璐 我一進(jìn)店門摘悴,熙熙樓的掌柜王于貴愁眉苦臉地迎上來碌识,“玉大人碾篡,你說我怎么就攤上這事》げ停” “怎么了开泽?”我有些...
    開封第一講書人閱讀 169,631評論 0 364
  • 文/不壞的土叔 我叫張陵,是天一觀的道長胖烛。 經(jīng)常有香客問我眼姐,道長,這世上最難降的妖魔是什么佩番? 我笑而不...
    開封第一講書人閱讀 60,199評論 1 300
  • 正文 為了忘掉前任众旗,我火速辦了婚禮,結(jié)果婚禮上趟畏,老公的妹妹穿的比我還像新娘贡歧。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,196評論 6 398
  • 文/花漫 我一把揭開白布利朵。 她就那樣靜靜地躺著律想,像睡著了一般。 火紅的嫁衣襯著肌膚如雪绍弟。 梳的紋絲不亂的頭發(fā)上技即,一...
    開封第一講書人閱讀 52,793評論 1 314
  • 那天,我揣著相機(jī)與錄音樟遣,去河邊找鬼而叼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛豹悬,可吹牛的內(nèi)容都是我干的葵陵。 我是一名探鬼主播,決...
    沈念sama閱讀 41,221評論 3 423
  • 文/蒼蘭香墨 我猛地睜開眼瞻佛,長吁一口氣:“原來是場噩夢啊……” “哼脱篙!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起伤柄,我...
    開封第一講書人閱讀 40,174評論 0 277
  • 序言:老撾萬榮一對情侶失蹤绊困,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后响迂,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體考抄,經(jīng)...
    沈念sama閱讀 46,699評論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡细疚,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,770評論 3 343
  • 正文 我和宋清朗相戀三年蔗彤,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片疯兼。...
    茶點(diǎn)故事閱讀 40,918評論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡然遏,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出吧彪,到底是詐尸還是另有隱情待侵,我是刑警寧澤,帶...
    沈念sama閱讀 36,573評論 5 351
  • 正文 年R本政府宣布姨裸,位于F島的核電站秧倾,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏傀缩。R本人自食惡果不足惜那先,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,255評論 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望赡艰。 院中可真熱鬧售淡,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,749評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至汤纸,卻和暖如春衩茸,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背贮泞。 一陣腳步聲響...
    開封第一講書人閱讀 33,862評論 1 274
  • 我被黑心中介騙來泰國打工递瑰, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人隙畜。 一個(gè)月前我還...
    沈念sama閱讀 49,364評論 3 379
  • 正文 我出身青樓抖部,卻偏偏與公主長得像,于是被迫代替她去往敵國和親议惰。 傳聞我的和親對象是個(gè)殘疾皇子慎颗,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,926評論 2 361

推薦閱讀更多精彩內(nèi)容