緒論
本文對于網(wǎng)上講爛的東西,盡量不再贅述。遇到的時候盡量以一句話概括其原理。
我想以提出問題的方式作為文章暗線,探討卡頓會出現(xiàn)的原因,以及我們的各種實際優(yōu)化方案。并且探討怎么樣的優(yōu)化比較適合應用到實際場景中的,避免過度優(yōu)化。
比如YYKit在文章中寫到因為CoreText 對象占用內(nèi)存較少,所以盡量選擇CoreText直接生產(chǎn)UI控件,我認為這就是過度優(yōu)化了。原因在于寫代碼速度降低,并且更不容易維護。用越底層的東西寫,性能當然更好,可是對于一些甚至覺得富文本都寫起來都很麻煩的同學來說,所有控件用CoreText實現(xiàn)實在是不易接受,代碼寫起來會比較麻煩,下一個人維護也會很頭疼。
這其中就涉及到了怎么樣的優(yōu)化才是平衡點。所以才有了這一篇自己和自己探討的文章。
網(wǎng)絡,緩存,渲染
從一個空白的tabview到最后呈現(xiàn)完畢的tabview的整一個過程,無非包含以上副標題的三個過程。那讓我們來思考一下這三個過程中,我們能做的優(yōu)化有什么。
網(wǎng)絡網(wǎng)絡
網(wǎng)絡的請求的優(yōu)化點有以下3點:如何優(yōu)化網(wǎng)絡,緩存DNS地址等等。
將獲取的JSon數(shù)據(jù)如何才能更高效地解析成Model,包括一些盡量不要使用KVC,使用Setter/Getter方法,或直接調(diào)用實際變量等等。
實際上這一部分我們會做的比較少。大部分用的第三方的控件HappyDns,AFNetWoring,YYModel等。
主要處理的還是網(wǎng)絡情況極差的情況下,數(shù)據(jù)Model遲遲未拿到,如何避免白屏時間過長:
一般就是做了Model的緩存,高度的緩存,圖片的緩存,進入的時候優(yōu)先展示緩存的內(nèi)容,請求網(wǎng)絡返回之后,通過對比數(shù)據(jù)再去更新最新的UI。但是刷新數(shù)據(jù)的時候一般會有閃一下的情況發(fā)生,網(wǎng)易新聞的處理方式應該是如果緩存沒有過期,新數(shù)據(jù)到來的時候不主動刷新刷新列表,顯示有信息,用戶主動下拉刷新新數(shù)據(jù),這也是實際的應用場景下的方案。
還有對于要下載的圖片,如果由于運營的失誤,上傳了極大的高分辨率圖,會出現(xiàn)不可預見的問題。原因如下:
SDWebImage 中 decodeImageWithImage這個方法用于對圖片進行解壓縮并且緩存起來,以保證tableviews/collectionviews 交互更加流暢,但是如果是加載高分辨率圖片的話,會適得其反,有可能造成上G的內(nèi)存消耗,對于高分辨率的圖片,應該禁止解壓縮操作,相關的代碼處理為:
項目中,高清圖涉及到的地方,禁用解壓縮,調(diào)用完畢再恢復原設置即可。這樣既能保證高分辨率圖不crash,也能保證其他地方,普通圖片依舊可以通過解壓縮進行優(yōu)化。
SDWebImage 為什么要對圖片進行解壓縮并且緩存起來。原因如下:
iOS 通常會在真正顯示才會移交GPU解碼圖片,對于一個較大的圖片,無論是直接或間接使用 UIImageView 或者繪制到 Core Graphics 中,都需要對圖片進行解壓(主線程),為了避免渲染的時候在主線程提交大量的解碼操作,所以都做了提前在異步線程的解壓和緩存。
常見的做法是在后臺線程先把圖片繪制到 CGBitmapContext 中,然后從 Bitmap 直接創(chuàng)建圖片。
所以無論是SDWebImage還是其他的圖片庫,大都都做了這個操作。
所以對于圖片下載的處理,我們客戶端需要自己做一層優(yōu)化也可以說是防護。一般圖片服務器保存在七牛,又拍,阿里云上的,都有提供下載縮略圖的API,我也因此做了一個工具類。對不同服務器存儲的圖片,按照不同的規(guī)則做裁剪和縮略。
我在測試服務器上遇到過這樣的問題,因為沒有做縮略客戶端會因為解碼的過程,內(nèi)存爆炸,Crash。
緩存高度或者布局
在網(wǎng)絡中已經(jīng)提到了緩存這一部分。在這里討論設置Cell高度的幾種方法優(yōu)缺點。
具體問題具體分析。
如果Cell高度和布局是確定的,我們初始化tabview高度的時候就可以直接給rowheight。這可以避免不斷去調(diào)用高度的代理方法。
甚至整個cell的渲染緩存也可以通過調(diào)用API來開啟,這種方式可以降低離屏渲染的不斷性能開銷。
那如果高度是微信朋友圈甚至微博的那種不確定,甚至是不斷改變的高度呢?
事實上 iOS7以后已經(jīng)有估算高度的API,那問題是不是都解決了?
坑爹的estimatedRowHeight
If the table contains variable height rows, it might be expensive to calculate all their heights when the table loads. Using estimation allows you to defer some of the cost of geometry calculation from load time to scrolling time.
說的很美好,事實上:
"
1 設置估算高度后,contentSize.height 根據(jù)“cell估算值 x cell個數(shù)”計算,這就導致滾動條的大小處于不穩(wěn)定的狀態(tài),contentSize 會隨著滾動從估算高度慢慢替換成真實高度,肉眼可見滾動條突然變化甚至“跳躍”。
2 若是有設計不好的下拉刷新或上拉加載控件,或是 KVO 了 contentSize 或 contentOffset 屬性,有可能使表格滑動時跳動。
3 估算高度設計初衷是好的,讓加載速度更快,那憑啥要去侵害滑動的流暢性呢,用戶可能對進入頁面時多零點幾秒加載時間感覺不大,但是滑動時實時計算高度帶來的卡頓是明顯能體驗到的,個人覺得還不如一開始都算好了呢(iOS8更過分,即使都算好了也會邊劃邊計算)
"
一句話:別用!
所以我們一般做的是使用一個NSDictionry緩存高度
或者對緩存高度的通常做法是使用一個NSDictionry緩存Cell的高度:
NSInteger layoutId = [[self.tvHomeDataSource[indexPath.row] layoutId] integerValue];
float height = 0.0;
switch (layoutId) {
case 1:
{
MThemeFirst *model = self.tvHomeDataSource[indexPath.row];
if (model.title.length > 0) {
height = [[self.dicRow objectForKey:@"1-1"] floatValue];
}else {
height = [[self.dicRow objectForKey:@"1-0"] floatValue];
}
}
break;
case 2:
height = [[self.dicRow objectForKey:@"2"] floatValue];
break;
case 3:
height = [[self.dicRow objectForKey:@"3"] floatValue];
break;
case 4:
height = [[self.dicRow objectForKey:@"4"] floatValue];
break;
case 5:
height = [[self.dicRow objectForKey:@"5"] floatValue];;
break;
}
return height;
在YYkit中,對性能敏感的APP中我覺得這是一個很值得做的操作。因為把復雜的布局和數(shù)據(jù)處理工作都交給WBStatusLayout了,不僅能很顯著地提高流暢度,而且可讀性高,耦合度低。
渲染渲染
接下來就到了重頭戲--渲染。以上做的所有操作,都是為了能夠保證60幀的渲染,沒有出現(xiàn)問題。
屏幕顯示圖像的原理:無論是LCD還是老古董CRT,基本原理基本相同。
引用“iOS 保持界面流暢的技巧”:
CPU 計算好顯示內(nèi)容提交到 GPU,GPU 渲染完成后將渲染結(jié)果放入幀緩沖區(qū),隨后視頻控制器會按照 VSync 信號逐行讀取幀緩沖區(qū)的數(shù)據(jù),經(jīng)過可能的數(shù)模轉(zhuǎn)換傳遞給顯示器顯示。
為了解決一個幀緩沖區(qū)的刷新和讀取效率,引入了雙緩沖機制。
那么又如何保證兩個緩沖區(qū)提交的內(nèi)容完美銜接呢,因為如果顯示器的刷新頻率和GPU 的渲染速度不一致,就會覆蓋已經(jīng)渲染好的幀棧。
于是又引入了垂直同步(V-Sync)。
V-Sync 的主要作用就是保證只有在幀緩沖區(qū)中的圖像被渲染之后,后備緩沖區(qū)中的內(nèi)容才可以被拷貝到幀緩沖區(qū)中。
那么問題就來了如何保證在1/60s內(nèi)我們需要的下一幀時機到來的時候。下一幀已經(jīng)準備好了。答:無法保證。
因為某些原因下一幀沒有準備好(UI 渲染需要時間較長,無法按時提交結(jié)果),或者一些需要密集計算的處理放在了主線程中執(zhí)行,導致主線程被阻塞,無法渲染 UI 界面。
就會出現(xiàn)平時出現(xiàn)的卡頓的主要原因。
那么離屏渲染為什么會卡幀?
知道了性能瓶頸在哪里,再想如何去優(yōu)化。
都說直接切圓角會發(fā)生離屏渲染,那么離屏渲染是什么?為什么切圓角會產(chǎn)生離屏渲染,為什么離屏渲染會產(chǎn)生卡頓。
離屏渲染,指的是GPU在當前屏幕緩沖區(qū)以外新開辟一個緩沖區(qū)進行渲染操作。
離屏渲染的整個過程,需要多次切換上下文環(huán)境:先是從當前屏幕(On-Screen)切換到離屏(Off-Screen);等到離屏渲染結(jié)束以后,將離屏緩沖區(qū)的渲染結(jié)果顯示到屏幕上有需要將上下文環(huán)境從離屏切換到當前屏幕。而上下文環(huán)境的切換是要付出很大代價的。
為了避免這種情況,可以嘗試開啟 CALayer.shouldRasterize 屬性,但這會把原本離屏渲染的操作轉(zhuǎn)嫁到 CPU 上去。
但是如果這個紋理之后還能復用到的話,這樣的操作是可取的,并不是所有的離屏渲染都是要干掉的。
所以更多時候我們應該通過異步線程,使用Core Graphic裁剪完畢,再回到主線程更新UI。如果是固定的樣式,可以使用.9的方式拉伸
并且如果我們重 寫了DrawRect方法,并且使用任何Core Graphics的技術進行了繪制操作,就涉及到了CPU渲染。整個渲染過程由CPU在App內(nèi)同步地完成,渲染得到的Bitmap(位圖)最后再交由GPU用于顯示。
即使發(fā)生了離屏渲染,GPU的速度也未必會比CPU差,因為對于圖像處理,通常用硬件會更快,因為GPU使用圖像對高度并行浮點運算做了優(yōu)化。這也是為什么現(xiàn)在大多數(shù)的直播都開始支持硬解碼功能。
至于為什么圓角,陰影等會產(chǎn)生離屏,很多文章中都沒有提到:
當使用圓角,陰影,遮罩的時候,圖層屬性的混合體被指定為在未預合成之前不能直接在屏幕中繪制,即當主屏的還沒有繪制好的時候,所以就需要屏幕外渲染,最后當主屏已經(jīng)繪制完成的時候,再將離屏的內(nèi)容轉(zhuǎn)移至主屏上。這里就發(fā)了兩次切換上下文環(huán)境的過程,當前屏幕到屏幕外,屏幕外再回到當前屏幕。這個解釋可以在iOS核心動畫高級技巧(非常好的一本書)中看到。
渲染UI為什么要在主線程?
一般來說App 主線程開始在 CPU 中計算顯示內(nèi)容,比如視圖的創(chuàng)建、布局計算、圖片解碼、文本繪制等。隨后 CPU 會將計算好的內(nèi)容提交到 GPU 去,由 GPU 進行變換、合成、渲染。這些操作都發(fā)生在主線程。
那么我們就會問為什么iOS 為什么必須在主線程中操作UI?
因為UIKit不是線程安全的。
1.兩個線程同時設置同一個背景圖片,那么很有可能因為當前圖片被釋放了兩次而導致應用崩潰。
2.兩個線程同時設置同一個UIView的背景顏色,那么很有可能渲染顯示的是顏色A,而此時在UIView邏輯樹上的背景顏色屬性為B。
3.兩個線程同時操作view的樹形結(jié)構:在線程A中for循環(huán)遍歷并操作當前View的所有subView,然后此時線程B中將某個subView直接刪除,這就導致了錯亂還可能導致應用崩潰。
那為什么蘋果設計的時候不去保證UIKit線程安全?
可能是像UIKit這樣大的框架上確保線程安全是一個重大的任務,會帶來巨大的成本。
那么我們是不是真的無法將以上的所有操作移到異步線程去了呢?答案當然是否定的。
首先圖片解碼這一步我們之前就提到,SDWebimage就做了提前的解碼操作。
并且了解到 AsyncDisplayKit這個框架的同學可能知道,他做的就是將圖片解碼、布局以及其它 UI 操作全部移出主線程,只在最后一步提交到GPU的過程在提交給主線程進行。
但是無法簡單使用AsyncDisplayKit的原因是因為他換了視圖基類,沒有采用UIView,創(chuàng)建了 ASDisplayNode 類,ASDisplayNode 是線程安全的。
但如果是一個存在已久的APP,如果要將所有的UIView替換成ASDisplayNode ,這個工作量可能就太大了。
他太重了,但是我們可以借鑒它的一些思路,將其做的一些優(yōu)化簡單地抽成我們能使用的功能。
所以我們能做的優(yōu)化有什么?
我覺得能采用的大概有如下幾點:
1 通過字典緩存高度,或者更好地緩存布局。存在一個布局類緩存布局和Model的一些復雜計算。
2 圖片要做好縮略圖。
3 一些復雜的創(chuàng)建和計算都不應該放在主線程操作,而像NSDateFormatter最好有一個工具類,千萬不要頻繁創(chuàng)建,可以發(fā)現(xiàn)肉眼可察覺的卡頓。
4 一些對圓角的處理,或者IM中切頭像的操作,都應該通過異步線程,使用Core Graphic裁剪完畢,再回到主線程更新UI。如果是固定的樣式(即不是從服務器下發(fā)的圖片),直接找UI切圖也未必是一個很Low的做法。如果要做長度拉伸,可以使用.9的方式拉伸。
5 盡量減少視圖層級。甚至可以將所以的視圖異步繪制成一張圖片來減少層級。視圖盡量不要設置透明。
6 圖片提前解碼,不要到提交GPU的時候再去解碼,前者可以在后臺線程操作,后者只能在主線程。
7 沒有交互的控件盡量直接使用CALayer,不要使用UIView。
8 Autolayout的實現(xiàn)是通過對線性方程組或者不等式的求解,也會產(chǎn)生額外的性能消耗,但是似乎放棄Autolayout也不是明智之舉。
9像墨客一樣,在scrollview結(jié)束滑動的代理方法中判斷,哪幾個cell是要繪制的,判斷不展示的圖片可以用灰色的透明塊展示。(實際應該被會產(chǎn)品經(jīng)理砍死,中間快速滑動過程中有一大段的空白)
(void)drawCell:(VVeboTableViewCell *)cell withIndexPath:(NSIndexPath *)indexPath{
NSDictionary *data = [datas objectAtIndex:indexPath.row];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
[cell clear];
cell.data = data;
if (needLoadArr.count>0&&[needLoadArr indexOfObject:indexPath]==NSNotFound) {
[cell clear];
return;
}
if (scrollToToping) {
return;
}
[cell draw];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
VVeboTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
if (cell==nil) {
cell = [[VVeboTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:@"cell"];
}
[self drawCell:cell withIndexPath:indexPath];
return cell;
}
//按需加載 - 如果目標行與當前行相差超過指定行數(shù),只在目標滾動范圍的前后指定3行加載。
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset{
NSIndexPath *ip = [self indexPathForRowAtPoint:CGPointMake(0, targetContentOffset->y)];
NSIndexPath *cip = [[self indexPathsForVisibleRows] firstObject];
NSInteger skipCount = 8;
if (labs(cip.row-ip.row)>skipCount) {
NSArray *temp = [self indexPathsForRowsInRect:CGRectMake(0, targetContentOffset->y, self.width, self.height)];
NSMutableArray *arr = [NSMutableArray arrayWithArray:temp];
if (velocity.y<0) {
NSIndexPath *indexPath = [temp lastObject];
if (indexPath.row+3<datas.count) {
[arr addObject:[NSIndexPath indexPathForRow:indexPath.row+1 inSection:0]];
[arr addObject:[NSIndexPath indexPathForRow:indexPath.row+2 inSection:0]];
[arr addObject:[NSIndexPath indexPathForRow:indexPath.row+3 inSection:0]];
}
} else {
NSIndexPath *indexPath = [temp firstObject];
if (indexPath.row>3) {
[arr addObject:[NSIndexPath indexPathForRow:indexPath.row-3 inSection:0]];
[arr addObject:[NSIndexPath indexPathForRow:indexPath.row-2 inSection:0]];
[arr addObject:[NSIndexPath indexPathForRow:indexPath.row-1 inSection:0]];
}
}
[needLoadArr addObjectsFromArray:arr];
}
}
其他的點有些沒想到,有些則需要在更高的流暢敏感度下再去優(yōu)化,應由業(yè)務推動技術的優(yōu)化。
過早的優(yōu)化都是魔鬼。
歡迎diss
iOS 保持界面流暢的技巧
優(yōu)化UITableViewCell高度計算的那些事
我正在看的一本書:《iOS核心動畫高級技巧》對GPU和CPU在渲染中的介紹
CPU VS GPU
iOS離屏渲染優(yōu)化
繪制像素到屏幕上
Getting Pixels onto the Screen