RunLoop總結:RunLoop的應用場景(三)滾動視圖流暢性優化

今天要講的RunLoop的應用場景可能太簡單了,因此東西比較少。由於跟UITableView、UICollectionView等的滑動優化有關,就順便總結一下會影響UITableView、UICollectionView等視圖滑動流暢的因素。php

參考資料

好的書籍都是值得反覆看的,那好的文章,好的資料也值得咱們反覆看。咱們在不一樣的階段來相同的文章或資料或書籍都能有不一樣的收穫,那它就是好文章,好書籍,好資料。 關於iOS 中的RunLoop資料很是的少,如下資料都是很是好的。html

  • CF框架源碼(這是一份很重要的源碼,能夠看到CF框架的每一次迭代,咱們能夠下載最新的版原本分析,或與如下文章對比學習。目前最新的是CF-1153.18.tar.gz)
  • RunLoop官方文檔(學習iOS的任何技術,官方文檔都是入門或深刻的極好手冊;咱們也能夠在Xcode--->Help--->Docementation and API Reference --->搜索RunLoop---> Guides(59)--->《Threading Programming Guide:Run Loops》這篇便是)
  • 深刻理解RunLoop(不要看到右邊滾動條很長,其實文章佔篇幅2/5左右,下面有不少的評論,可見這篇文章的火熱)
  • RunLoop我的小結 (這是一篇總結的很通俗容易理解的文章)
  • sunnyxx線下分享RunLoop(這是一份關於線下分享與討論RunLoop的視頻,備用地址:pan.baidu.com/s/1pLm4Vf9)
  • iPhonedevwiki中的CFRunLoop(commonModes中其實包含了三種Mode,咱們一般知道兩種,還有一種是啥,你知道麼?)
  • 維基百科中的Event loop(能夠看看這篇文章瞭解一下事件循環)

應用場景

讓UITableView、UICollectionView等延遲加載圖片。下面就拿UITableView來舉例說明: UITableView 的 cell 上顯示網絡圖片,通常須要兩步,第一步下載網絡圖片;第二步,將網絡圖片設置到UIImageView上。git

爲了避免影響滑動,第一步,咱們通常都是放在子線程中來作,這個不作贅述。github

第二步,通常是回到主線程去設置。有了前兩篇文章關於Mode的切換,想必你已經知道怎麼作了。 就是在爲圖片視圖設置圖片時,在主線程設置,並調用performSelector:withObject:afterDelay:inModes:方法。最後一個參數,僅設置一個NSDefaultRunLoopMode算法

UIImage *downloadedImage = ....;
[self.myImageView performSelector:@selector(setImage:) withObject:downloadedImage afterDelay:0 inModes:@[NSDefaultRunLoopMode]];
複製代碼

固然,即便是讀取沙盒或者bundle內的圖片,咱們也能夠運用這一點來改善視圖的滑動。可是若是UITableView上的圖片都是默認圖,彷佛也不是很好,你須要本身來權衡了。數據庫

有一個很是好的關於設置圖片視圖的圖片,在RunLoop切換Mode時優化的例子:RunLoopWorkDistribution 先看一下界面佈局:數組

1002.png

一個Cell裏有兩個Label,和三個imageView,這裏的圖片是很是高清的(2034 × 1525),一個界面最多有18張圖片。爲了表現出卡頓的效果,我先本身實現了一下Cell,主要示例代碼:緩存

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *identifier = @"cellId";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
    if (cell == nil) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
    }
    for (NSInteger i = 1; i <= 5; i++) {
        [[cell.contentView viewWithTag:i] removeFromSuperview];
    }
    
    UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(5, 5, 300, 25)];
    label.backgroundColor = [UIColor clearColor];
    label.textColor = [UIColor redColor];
    label.text = [NSString stringWithFormat:@"%zd - Drawing index is top priority", indexPath.row];
    label.font = [UIFont boldSystemFontOfSize:13];
    label.tag = 1;
    [cell.contentView addSubview:label];
    
    UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(105, 20, 85, 85)];
    imageView.tag = 2;
    NSString *path = [[NSBundle mainBundle] pathForResource:@"spaceship" ofType:@"jpg"];
    UIImage *image = [UIImage imageWithContentsOfFile:path];
    imageView.contentMode = UIViewContentModeScaleAspectFit;
    imageView.image = image;
    NSLog(@"current:%@",[NSRunLoop currentRunLoop].currentMode);
    [cell.contentView addSubview:imageView];
    
    UIImageView *imageView2 = [[UIImageView alloc] initWithFrame:CGRectMake(200, 20, 85, 85)];
    imageView2.tag = 3;
    UIImage *image2 = [UIImage imageWithContentsOfFile:path];
    imageView2.contentMode = UIViewContentModeScaleAspectFit;
    imageView2.image = image2;
    [cell.contentView addSubview:imageView2];
    
    UILabel *label2 = [[UILabel alloc] initWithFrame:CGRectMake(5, 99, 300, 35)];
    label2.lineBreakMode = NSLineBreakByWordWrapping;
    label2.numberOfLines = 0;
    label2.backgroundColor = [UIColor clearColor];
    label2.textColor = [UIColor colorWithRed:0 green:100.f/255.f blue:0 alpha:1];
    label2.text = [NSString stringWithFormat:@"%zd - Drawing large image is low priority. Should be distributed into different run loop passes.", indexPath.row];
    label2.font = [UIFont boldSystemFontOfSize:13];
    label2.tag = 4;
    
    UIImageView *imageView3 = [[UIImageView alloc] initWithFrame:CGRectMake(5, 20, 85, 85)];
    imageView3.tag = 5;
    UIImage *image3 = [UIImage imageWithContentsOfFile:path];
    imageView3.contentMode = UIViewContentModeScaleAspectFit;
    imageView3.image = image3;
    [cell.contentView addSubview:label2];
    [cell.contentView addSubview:imageView3];

    return cell;
}
複製代碼

而後在滑動的時候,順便打印出當前的runloopMode,打印結果是:性能優化

2016-12-08 10:34:31.450 TestDemo[3202:1791817] current:UITrackingRunLoopMode
2016-12-08 10:34:31.701 TestDemo[3202:1791817] current:UITrackingRunLoopMode
2016-12-08 10:34:32.184 TestDemo[3202:1791817] current:UITrackingRunLoopMode
2016-12-08 10:34:36.317 TestDemo[3202:1791817] current:UITrackingRunLoopMode
2016-12-08 10:34:36.601 TestDemo[3202:1791817] current:UITrackingRunLoopMode
2016-12-08 10:34:37.217 TestDemo[3202:1791817] current:UITrackingRunLoopMode
複製代碼

能夠看出,爲imageView設置image,是在UITrackingRunLoopMode中進行的,若是圖片很大,圖片解壓縮和渲染確定會很耗時,那麼卡頓就是必然的。bash

查看實時幀率,咱們能夠在Xcode 中選擇真機調試,而後 Product -->Profile-->Core Animation

而後點擊開始監測便可:

下面就是幀率:

這裏就可使用先使用上面的方式作一次改進。

[imageView performSelector:@selector(setImage:) withObject:image afterDelay:0 inModes:@[NSDefaultRunLoopMode]];
複製代碼

能夠保證在滑動起來順暢,但是停下來以後,渲染還未完成時,繼續滑動就會變的卡頓。 在切換到NSDefaultRunLoopMode中,一個runloop循環要解壓和渲染18張大圖,耗時確定超過50ms(1/60s)。

咱們能夠繼續來優化,一次runloop循環,僅渲染一張大圖片,分18次來渲染,這樣每一次runloop耗時就比較短了,滑動起來就會很是順暢。這也是RunLoopWorkDistribution中的作法。

簡單描述一下這種作法: 首先建立一個單例,單例中定義了幾個數組,用來存要在runloop循環中執行的任務,而後爲主線程的runloop添加一個CFRunLoopObserver,當主線程在NSDefaultRunLoopMode中執行完任務,即將睡眠前,執行一個單例中保存的一次圖片渲染任務。關鍵代碼看 DWURunLoopWorkDistribution類便可。

一點UITableView滑動性能優化擴展

影響UITableView的滑動,有哪些因素呢?

關於這一點,人眼能識別的幀率是60左右,這也就是爲何,電腦屏幕的最佳幀率是60Hz。 屏幕一秒鐘會刷新60次(屏幕在一秒鐘會從新渲染60次),那麼每次刷新界面之間的處理時間,就是1/60,也就是1/60秒。也就是說,全部會致使計算、渲染耗時的操做都會影響UITableView的流暢。下面舉例說明:

1.在主線程中作耗時操做

耗時操做,包括從網絡下載、從網絡加載、從本地數據庫讀取數據、從本地文件中讀取大量數據、往本地文件中寫入數據等。(這一點,相信你們都知道,要儘可能避免在主線程中執行,通常都是建立一個子線程來執行,而後再回到主線程)

2.動態計算UITableViewCell的高度,時間太久

在iOS7以前,每個Cell的高度,只會計算一次,後面再次滑到這個Cell這裏,都會讀取緩存的高度,也即高度計算的代理方法不會再執行。可是到了iOS8,不會再緩存Cell的高度了,也就是說每次滑到某個Cell,代理方法都會執行一次,從新計算這個Cell的高度(iOS 9之後沒測試過)。 因此,若是計算Cell高度的這個過程過於複雜,或者某個計算使用的算法耗時很長,可能會致使計算時間大於1/60,那麼必然致使界面的卡頓,或不流暢。

關於這一點,我之前的作法是在Cell中定義一個public方法,用來計算Cell高度,而後計算完高度後,將高度存儲在Cell對應的Model中(Model裏定義一個屬性來存高度),而後在渲染Cell時,咱們依然須要動態計算各個子視圖的高度。(多是沒用什麼太過複雜的計算或算法,時間都很短滑動也順暢)

其實,更優的作法是:再定義一個ModelFrame對象,在子線程請求服務器接口返回後,轉換爲對象的同時,也把各個子視圖的frame計算好,存在ModelFrame中,ModelFrame 和 Model 合併成一個Model存儲到數組中。這樣在爲Cell各個子控件賦值時,僅僅是取值、賦值,在計算Cell高度時,也僅僅是加法運算。

3.界面中背景色透明的視圖過多

爲何界面中背景色透明的視圖過多會影響UITableView的流暢?

不少文章中都提到,可使用模擬器--->Debug--->Color Blended Layers來檢測透明背景色,把透明背景色改成與父視圖背景色同樣的顏色,這樣來提升渲染速度。

簡單說明一下,就是屏幕上顯示的全部東西,都是經過一個個像素點呈現出來的。而每個像素點都是經過三原色(紅、綠、藍)組合呈現出不一樣的顏色,最終纔是咱們看到的手機屏幕上的內容。在 iPhone5 的液晶顯示器上有1,136×640=727,040個像素,所以有2,181,120個顏色單元。在15寸視網膜屏的 MacBook Pro 上,這一數字達到15.5百萬以上。全部的圖形堆棧一塊兒工做以確保每次正確的顯示。當你滾動整個屏幕的時候,數以百萬計的顏色單元必須以每秒60次的速度刷新,這是一個很大的工做量。

每個像素點的顏色計算是這樣的:

R = S + D * (1 - Sa)

結果的顏色 是子視圖這個像素點的顏色 + 父視圖這個像素點的顏色 * (1 - 子視圖的透明度) 固然,若是有兩個兄弟視圖疊加,那麼上面的中文解釋可能並不貼切,只是爲了更容易理解。

若是兩個兄弟視圖重合,計算的是重合區域的像素點: 結果的顏色 是 上面的視圖這個像素點的顏色 + 下面這個視圖該像素點的顏色 * (1 - 上面視圖的透明度)

只有當透明度爲1時,上面的公式變爲R = S,就簡單的多了。不然的話,就很是複雜了。

每個像素點是由三原色組成,例如父視圖的顏色和透明度是(Pr,Pg,Pb,Pa),子視圖的顏色顏色和透明度是(Sr,Sg,Sb,Sa),那麼咱們計算這個重合區域某像素點的顏色,須要先分別計算出紅、綠、藍。

Rr = Sr + Pr * (1 - Sa),

Rg = Sg + Pg * (1 - Sa),

Rb = Sb + Pb * (1 - Sa)。

若是父視圖的透明度,即Pa = 1,那麼這個像素的顏色就是(Rr,Rg,Rb)。

可是,若是父視圖的透明Pa 不等 1,那麼咱們須要將這個結果顏色當作一個總體做爲子視圖的顏色,再去與父視圖組合計算顏色,如此遞推。

因此設置不透明時,能夠爲GPU節省大量的工做,減小大量的消耗。

更加詳細的說明,能夠看繪製像素到屏幕上這篇文章,這是一篇關於繪製像素的很是棒👍的文章,我反覆看了三遍。

4.主線程RunLoop切換到UITrackingRunLoopMode時,視圖有過多的修改

這也就是上面介紹的RunLoop的使用,避免在主線程RunLoop切換到UITrackingRunLoopMode時,修改視圖。

Have Fun!

相關文章
相關標籤/搜索