深入理解 Autolayout 與列表效能 -- 背鍋的 Cassowary 和偷懶的 CPU

nangezao發表於2018-10-29

這篇文章會通過對 autolayout 內部實現的探索和資料分析和對 autolayout 的效能問題做一個詳細的分析,並在最後給出一個高效能 autolayout 的解決方案。開始看文章之前,可以先試試這個 demo ,使用 YYKit demo 資料做的微博 Feed 列表。使用我自己寫的非同步繪製元件 Panda 和和 ‘autolayout’ 框架 Layoutable 寫的 ,cell 程式碼只有 五百多行,但是流暢度很高。

Cassowary 演算法效能

Autolayout 設計問題

Text layout 對效能的影響

CPU 排程對列表效能的影響

Autolayout 的一些結論

Panda

Cassowary 演算法效能

Autolayout 會將約束條件轉換成線性規劃問題,通過 Cassowary 演算法求解線性規劃問題得到 frame。因此分析 autolayout 效能都繞不開 Cassowary 演算法。大部分分析最後都會給出結論 “autolayout 效能差是 cassowary 演算法的多項式的時間複雜度造成的”。也有一些會給出 autolayout 的 benchmark 來證明 cassowary 演算法的問題。但是

  1. Cassowary 是 1997 年就被髮表並被稱作高效的線性方程求解演算法,為什麼 ‘8012’ 年了反而成了效能殺手?
  2. 如果是 Cassowary 演算法的問題,跑著 iOS 8 的 iPhone 6 應該比實際表現更卡頓才合理,畢竟演算法時間複雜度不會隨著裝置和系統升級下降。由於系統開銷造成的效能下降在 ios 裝置升級的和過程中似乎額外的大了。

想到自己實現 cassowary 演算法和 autolayout 也是由對這兩個問題的不解引出的。

Cassowary is an incremental constraint solving toolkit that efficiently solves systems of linear equalities and inequalities.

線性規劃問題的求解很早就有通用解法--單純型法,有興趣的同學可以看看這篇文章 AutoLayout 中的線性規劃 - Simplex 演算法 。《演算法導論》也有一章專門介紹單純型法的(所以誰說演算法對 iOS 開發沒用?)。Cassowary 則是單純型法在使用者介面實踐中的應用和改進演算法,解決一些實際使用的問題,最重要的增加了增量的概念(Autolayout 實現中 Cassowary 相關的程式碼是以 NSIS 作為字首的,IS 就是 incremental Simplex 增量單純型的縮寫 ),單純型法通過建立單純型表,在對單純形表進行 pivot 和 optimize 操作得到最優解;Cassowary 則是可以在已經建立單純型表上,高效的進行新增修改更新操作。因為使用者介面應用中,大部分約束已經固定,介面變化只需要對其中的部分約束進行更新或者進行少量的增減操作。Cassowary 的高效是建立在增量跟新的基礎上的。

完整介紹 Cassowary 需要很長篇幅,有時間單獨介紹,這裡用資料說話

一組 benchmark: (MacBook Pro 2016 i5,iPhone6S 模擬器)

深入理解 Autolayout 與列表效能 -- 背鍋的 Cassowary 和偷懶的 CPU

  • Autolayout: 是相對佈局的耗時
  • Autolayout Nestlayout: 巢狀佈局的耗時
  • update constant: 更新約束的耗時,即更新 NSLayoutConstraint 的 constant 常量。

因為這裡沒有不含 UILabel,UIView 等有 intrincContentSize 的 UIView,update constant 基本就是 Cassowary 更新約束的耗時。Applelayout 和 Apple NestLayout 則也包含 UIView 建立,約束建立和求解的時間。

可以看到 update 約束是非常高效的, 80 個 view,160 條約束更新約束也只需要 2.5 個毫秒,這個數量在實際使用中基本上是用不到的。實際使用中,同時更新 40 個 view 80 條約束已經算是很多的了,也只耗時 1.25 ms。

列表滾動中,一般情況下頁面載入的時候 cell 和 約束已經建立,效能應該主要和更新約束相關(更新約束包括 UILabel。UIView 更改 text ,image 造成的 size 變化,更新系統預設的約束;也包括手動調整 NSLayoutConstraint 的 constant 屬性等)。為什麼實際表現卻差很多呢?

Autolayout 設計問題

Autolayout 構建在 Cassowary 之上,但是 autolayout 的一些機制沒有充分利用 Cassowary 更新高效的特點。我們可以通過私有類和方法來研究系統內部的實現。這裡有一個網站 iOS SDK Header Dump 可以檢視 iOS 的私有標頭檔案。其中 NSIS 開頭的類都是 Autolayout 相關的標頭檔案。我把 iOS 11 Autolayout 相關的標頭檔案下載下來並做成了一個可以執行的工程。可以 hook 內部實現或者列印變數來觀察系統的呼叫,可以這裡下載 ExplorAutolayout 。後面一些測試程式碼會基於這個工程。

  1. NSContentSizeLayoutConstraint

    這是 FDTemplateLayoutCell profile 的一段結果,展開部分是 cellForRowAIndex 裡執行的程式碼。

    深入理解 Autolayout 與列表效能 -- 背鍋的 Cassowary 和偷懶的 CPU

    理論上 cellForRowAIndex 是不需要建立 NSLayoutConstraint 的,畢竟 cell 已經建立過了, 更新資料的時候程式碼中並沒有新加約束。但這裡建立了 UIContentSizeLayoutConstraint 物件,UIContentSizeLayoutConstraint 繼承自 NSLayoutConstraint,是專門用來約束 contentSize 的約束。

    來一段測試程式碼,我們在 NSLayoutConstraint 物件建立的時候輸出建立的約束型別:

    // 子類化 UIlabel,每次呼叫 intrinsicContentSize 輸出大小
    @implementation TestLabel
    
    - (CGSize)intrinsicContentSize{  
        NSLog(@"width: %f, height: %f",size.width,size.height);
        return [super intrinsicContentSize];
    }
    
    @end
    
    // 替換 NSLayoutConstraint init 方法,每次輸出建立的型別
    @implementation NSLayoutConstraint (methodSwizze)
    
    + (void)load{
       [self replace:@selector(init) byNew:@selector(new_init)];
    }
    
    - (instancetype)new_init{
        NSLog(@"New %@",[self class]);
        return [self new_init];
    }
    
    @end  
    
    複製程式碼

    一個多行文字的 label 給一個寬度約束,然後設定 text, layoutIfNeeded 強制佈局 輸出結果:

    width: 1073741824.000000, height: 20.500000
    New NSContentSizeLayoutConstraint
    New NSContentSizeLayoutConstraint
    width: 296.500000, height: 41.000000
    New NSContentSizeLayoutConstraint
    New NSContentSizeLayoutConstraint
    複製程式碼

    建立的兩個約束是根據 intrinsicContentSize 值給的寬度和高度約束。也就是每次 intrinsicContentSize 變化的時候,Autolayout 都會建立兩個新的 NSContentSizeLayoutConstraint 約束分別約束寬和高,新增到 NSISEnginer 中求解, 而不是直接更新已經建立好的約束。

    水果公司一邊告訴我們重新新增約束比更新約束低效,一邊在頻繁呼叫的地方用著低效的方法?。

  2. systemLayoutSizeFittingSize

    NSContentSizeLayoutConstraint 只是蘋果浪費 Cassowary 演算法優點的一個地方,

    • 看另一組不包含 intrinsicContentSizeUIView 的資料,都是單純的更新約束,區別只在於有沒有新增到 window 上,以及強制佈局的方法:

      深入理解 Autolayout 與列表效能 -- 背鍋的 Cassowary 和偷懶的 CPU

      • Apple constant 是 view 沒有並新增到 window 上,更新約束後呼叫 layoutIfNeeded 的資料。
      • Apple In Window constant是把 view 新增到當前 window 上,更新約束後呼叫 layoutIfNeeded 的資料
      • SystemFitSize constant 是呼叫 systemlayoutFitSize 獲取高度的資料。

      同樣是更新約束,耗時差距卻非常大,新增到 window 上再呼叫 layoutIfNeeded 的耗時遠小於沒有加到 window 上。同樣沒有加到 window 上,systemlayoutFitSize 耗時又要小於 layoutIfNeeded.

    • 再以 FDTemplateLayoutCell 為例,我們在同一方法中同事呼叫 systemLayoutSizeFittingSizelayoutIfNeeded

      - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
      
          [self measure:^{
            [self configureCell:self.cell atIndexPath:indexPath];
            [self.cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
          } log:@"heightForRow"];
      
          FDFeedCell *cell = [tableView dequeueReusableCellWithIdentifier:@"FDFeedCell"];
          [self measure:^{
              [self configureCell:cell atIndexPath:indexPath];
              [cell.contentView layoutIfNeeded];
          } log:@"cellForRowAtIndexPath"];
      
          return cell;
      }
      複製程式碼

      profile 下

      深入理解 Autolayout 與列表效能 -- 背鍋的 Cassowary 和偷懶的 CPU

      systemLayoutSizeFittingSize 總耗時 276 ms, layoutIfNeeded 總耗時 161 ms 多了 70% 的耗時

    • 看一下 autolayout 呼叫的過程:

      替換 NSISEnginerNSISEnginer 就是 autolayout 的 線性規劃求解器)的 init 方法,每次建立 NSISEnginer 列印 New NSISEnginer

      + (void)load{
          [self replace:@selector(init) byNew:@selector(new_init)];
      }
      
      - (id)new_init{
          NSLog(@"New NSISEnginer");
          return [self new_init];
      }
      
      ...
      
      @implementation NSObject(methodExchange)
      
      + (void)replace:(SEL)old byNew:(SEL)new{
          Method oldMethod = class_getInstanceMethod([self class], old);
          Method newMethod = class_getInstanceMethod([self class], new);
      
          method_exchangeImplementations(oldMethod, newMethod);
      }
      複製程式碼

      呼叫方法觀察輸出:

        UIView * view3 = [[UIView alloc] init];
      
        view3.translatesAutoresizingMaskIntoConstraints = false;
        NSLayoutConstraint *c3 =  [view3.widthAnchor constraintEqualToConstant:10];
        c3.priority = UILayoutPriorityDefaultHigh;
        c3.active = true;
      
        for(NSUInteger i = 0; i < 3; i++){
           [view3 setNeedsLayout];
           [view3 layoutIfNeeded];
           NSLog(@"View3LayoutIfNeeded");
         }
      
        for(NSUInteger i = 0; i < 3; i++){
          [view3 setNeedsLayout];
          [view3 systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
          NSLog(@"No superview systemLayoutSizeFittingSize");
        }
      
        [self.view addSubview:view3];
      
        for(NSUInteger i = 0; i < 3; i++){
          c3.constant = rand()%20;
          [view3 setNeedsLayout];
          [view3 layoutIfNeeded];
          NSLog(@"View3LayoutIfNeededSecondPass");
        }
      
        for(NSUInteger i = 0; i < 3; i++){
          c3.constant = rand()%20;
          CGSize size = [view3 systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
          NSLog(@"w :%f",size.width);
          NSLog(@"systemLayoutSizeFittingSize");
        }
      複製程式碼

      列印結果是

       View3LayoutIfNeeded
       New NSISEnginer
       View3LayoutIfNeeded
       New NSISEnginer 
       View3LayoutIfNeeded
       New NSISEnginer
      
       No superview systemLayoutSizeFittingSize
       New NSISEnginer
       No superview systemLayoutSizeFittingSize
       New NSISEnginer
       No superview systemLayoutSizeFittingSize
       New NSISEnginer
      
       View3LayoutIfNeededSecondPass
       View3LayoutIfNeededSecondPass
       View3LayoutIfNeededSecondPass
      
       systemLayoutSizeFittingSize
       New NSISEnginer
       systemLayoutSizeFittingSize
       New NSISEnginer
       systemLayoutSizeFittingSize
       New NSISEnginer
      複製程式碼

      可以看到,沒有新增到 window 之前, 呼叫 layoutIfNeededsystemLayoutSizeFittingSize 每次都會建立 NSISEnginer;新增到 window 上以後,layoutIfNeeded 並不會建立 NSISEnginer, 而systemLayoutSizeFittingSize 還是每次都會建立 NSISEnginer。建立新的 NSISEnginer 則意味著對應的所有約束,也會重新新增到 NSISEnginer,重新進行優化求解,這時候的耗時就變成了初次新增約束的時間。在列表的使用中,我們一般會在 heightForRowAtIndexPath 中建立一個不會新增到 window 上的 cell 呼叫 systemLayoutSizeFittingSize 來計算高度。這個的計算耗時就要比 cellForRowAtIndexPath 中的耗時大很多。

    深入理解 Autolayout 與列表效能 -- 背鍋的 Cassowary 和偷懶的 CPU

    systemLayoutSizeFittingSize 會重新建立 NSISEnginer和 WWDC 《High performance Autolayout》 所講也是一致的。使用 systemLayoutSizeFittingSize 時,Autolayout 會建立新的 NSISEnginer 物件,重新新增約束求解,然後釋放掉 NSISEnginer 物件。而對於 layoutIfNeeded 也很好理解,Autolayout 中,一個 window 層級下的 view 會共用 window 節點的 NSISEnginer 物件,沒有新增到 window 上的 view 沒有父 window 也就沒辦法共用,只能重新建立.

    深入理解 Autolayout 與列表效能 -- 背鍋的 Cassowary 和偷懶的 CPU

    而在 WWDC 介紹中 systemLayoutSizeFitting 是提供給 autolayout 和 frame 混合使用的,也不建議常用,似乎不是給計算高度來用的。

    那麼能不能在算高度時候把 cell 新增到 window 上,隱藏,然後用 layoutIfNeeded 來提高效率?

    ?:呵呵 ?

    systemLayoutSizeFittingSize 對計算做了優化,計算好以後不會對 view 的 frame 進行操作,也就避免 layer 調整的相關耗時。所以同樣是建立 NSISEnginer 重新新增約束, systemLayoutSizeFittingSizelayoutIfNeeded 要高效;新增到 window 上以後,layoutIfNeeded 計算的效率高於 systemLayoutSizeFittingSize,但是 setFrame 和觸發的 layer 相關操作又會有額外的耗時,不一定會比直接使用 systemLayoutSizeFittingSize 耗時少 。

    深入理解 Autolayout 與列表效能 -- 背鍋的 Cassowary 和偷懶的 CPU

The Enginer is a layout cache and dependency tracker

Cassowary 的增量更新機制其實也算是某種程度上的快取機制,重新建立 Enginer 的設計也就丟掉了 cache 的能力,降低了效能。

Text layout 對效能的影響

雖然由於上述種種問題, 但如上圖所示 heightForRowAtIndexPath 裡呼叫 systemLayoutSizeFittingSize 再加上 cellForRowAtIndexPath 裡呼叫 layoutIfNeeded 總耗時看起來也並不是很多,40 個 view 左右耗時也不到 4 ms,看起來還可以,為什麼實際使用起來表現卻差很多呢。

  1. text layout 才是效能殺手

    1. 以 FDTemplateLayoutCell demo,為例,我們對同一個 cell 連續執行三次一樣的程式碼,

      [self measure:^{
          [self configureCell:self.cell atIndexPath:indexPath];
          [self.cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
       } log:@"heightForRow"];
      
      [self measure:^{
          [self configureCell:self.cell atIndexPath:indexPath];
          [self.cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
      } log:@"heightForRow"];
      
      [self measure:^{
          [self configureCell:self.cell atIndexPath:indexPath];
          [self.cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
      } log:@"heightForRow"];
      
      複製程式碼

      結果差距很大

      深入理解 Autolayout 與列表效能 -- 背鍋的 Cassowary 和偷懶的 CPU

      第一遍耗時 231 ms,後面兩遍只有 98,87 毫秒

      如果把第一遍展開的話,就會發現大部分時間都是在文字上:

      深入理解 Autolayout 與列表效能 -- 背鍋的 Cassowary 和偷懶的 CPU

      後面兩遍因為和第一遍的資料一樣,不會觸發文字相關的操作。計算的時間只佔了 30%-40%

    2. 以我們的微博 demo layout 做一個 benchmak

      for status in self.statusViewModels{
        measureTime(desc: "without text layout cache", action: {
          self.statusNode.update(status)
          self.statusNode.layoutIfNeeded()
          status.layoutValues = self.statusNode.layoutValues
          status.height = self.statusNode.frame.height
        })
      }
      
      for status in self.statusViewModels{
        measureTime(desc: "without text layout cache", action: {
          self.statusNode.update(status)
          self.statusNode.layoutIfNeeded()
          status.layoutValues = self.statusNode.layoutValues
          status.height = self.statusNode.frame.height
        })
      }
      複製程式碼

      兩個 for 迴圈中,除了輸出的描述文案,程式碼是一樣的,Panda 的實現中,會把已經建立的 TextKit 組成的 TextRender 物件快取起來,並且是不可變。再次出現相同的文字會從快取取。

      第一次 for 迴圈中,不存在相應的 TextRender 物件,每次都需要建立新的 TextRender 物件並進行 layout

      第二次 for 迴圈中,因為第一次計算過程中已經快取了 TextRender,基本上只是單純取值和 Cassowary 更新約束計算。

      結果:(iPhone6 , iOS 12)

      深入理解 Autolayout 與列表效能 -- 背鍋的 Cassowary 和偷懶的 CPU

      • Panda FirstPass 是第一個 for 迴圈資料
      • Panda SecondPass 是第二個 for 迴圈資料
      • YYKit 則是 YYKit 手算 frame 的資料
      • 縱座標是耗時

      同樣更新資料,同樣的 update 約束,同樣的 Panda Layout 資料相差卻非常大。而且第二次資料更加平穩

      對於 Panda Layou,相差的資料基本就是 text layout 的時間。第一次 Layout 平均資料 5.94,第二次平均資料 1.44. text layout 佔了總耗時的 70%-80%。

  2. Autolayout 要比手算多一些 Text layout過程

    text layout 耗時最多, 使用 autolayout 會比 手算 frame 多一部分 text layout 過程

    其實上一個 NSContentSizeLayoutConstraint 的輸出結果中已經給出部分答案,只設定一次 text,卻輸出了兩次 intrinsicContentSize,而且結果也不一樣。 檢查一下 UIView 的私有方法,會發現一個_needsDoubleUpdateConstraintsPass 的方法,返回值為 true 的話,會呼叫兩次 intrinsicContentSize 方法。

    • 手撕的 frame 時候開發人員需要額外注意計算順序。比如計算一個多行的 UILabel,可能會先把左右兩邊相關的寬度計算好,這樣可以知道 UILabel 最大寬度,或者直接指定 UILabel 的最大寬度,使用 size(withAttributes:) 進行一次 text layout 就可以把文字大小算出來。
    • Autolayout 使開發者免去了操心佈局順序的負擔(這也是 Autolayout 一個比較核心的優點), 導致更新 UILabel 的約束時不能直接確定 UILabel 的最大寬度,怎麼解決換行的問題?(iOS 6 的時候需要手動設定 preferrdMaxLayoutWidth,很多時候會造成很大困擾,因為並不是那麼容易確定)。 對於多行文字的 UILabel,Autolayout 會進行兩邊 layout. 第一次 layout 會先假設文字可以一行展示完,進行一次 text layout ,計算一行文字的大小,更新 UILabel 的 size 約束。size 的寬高約束都不是 required 的,外部如果有對寬度相關的約束的話,也不會衝突。整個 view 層級一次佈局結束之後,所有 view 的寬度就確定了,第二遍 layout 再以當前寬度再做一次 text layout ,更新文字寬高。這樣 autolayout 文字的多行文字 textLayout 過程就要比手算 frame 多一倍。多行文字 layout 一般耗時更長。多出來一次的 text layout 的耗時就很多了了。

    深入理解 Autolayout 與列表效能 -- 背鍋的 Cassowary 和偷懶的 CPU

    textlayout 耗時佔比很大,這也是為什麼蘋果推薦重寫 UIlable 的 intrinsicContentSize 方法,然後約束寬高的方式來避免 text layout。但是實際使用中能這樣優化的場景並不多。

  3. 主執行緒執行的影響

    關於列表效能優化,大家比較喜歡說的就是 frame 比 autolayout 快,其實更重要的是 frame 相對 autolayout 可以減少一些重複計算,以及把耗時操作丟到後臺執行緒。

    1. 手算 frame 可以放到後臺執行緒,從而避免了主執行緒的 text layout。
    2. 手算 frame 只會 layout 一遍,autolayout heightForRowAtIndexPathcellForRowAtIndexPath 都需要計算,這個多出來的的計算和 text layout 就更多了。

Textlayout 在計算和渲染過程佔的比重很大,也是很多 app 即使 cell 高度用 frame 算,沒有做 text layout 相關快取或者非同步 Label 也會不流暢的原因。單純做計算的優化,不做 text layout 快取的佈局框架一般實際表現都不會太好。

CPU 排程對列表效能的影響

上面的 benchmark 是針對 iPhone 6 的, 資料其實已經很不錯了,更好的裝置豈不是要逆天?

看一組 iPhneX 的資料 (iPhoneX , iOS 12)

深入理解 Autolayout 與列表效能 -- 背鍋的 Cassowary 和偷懶的 CPU

即使第一次 layout,Panda 和 YYKit 平均耗時只有 1.34 毫秒,只更新約束更是隻需要 0.287 毫秒。(這個資料遠好於 2016 MacBook Pro 的表現)。時間寬裕度很大,看起來即使 autolayout 的耗時多個一兩倍問題也不大。

Apple: 呵呵?

benchmark 出來的耗時其實一般和實際執行是不一樣。同樣 iOS 12 iPhoneX ,如果對列表進行快速滑動的話,是可以到達 benchmark 的資料;如果滑動的不是很快的,上面 0.x,1.x ms 的耗時,很多就變成了 6 - 9 ms 左右。

深入理解 Autolayout 與列表效能 -- 背鍋的 Cassowary 和偷懶的 CPU

深入理解 Autolayout 與列表效能 -- 背鍋的 Cassowary 和偷懶的 CPU

CPU 達到最好效能是需要時間的,benchmark 過程計算比較集中, CPU 一直處於高效能狀態。但是滑的慢一點的話,可能 CPU 效能還沒起來計算就結束了。然後 CPU 開始偷懶。剛好效能下去以後另一計算過程又開始了。而且 iOS 12 這個已經優化過了,iOS11 和 iOS 10 表現更差。做 benchmark 的有時候也會有一個有趣的現象,如果有幾組資料需要測試,在同一段程式碼裡呼叫這些方法進行測試,方法的呼叫順序對 benchmark 出來的資料影響特別大。放在第一個的方法耗時會被大大增加。

Autolayout 一些結論

總結一下,autolayout 效能不好並不是以前經常看到的是因為 cassowary 演算法差導致的

  1. cassowary 演算法效能並沒有太大問題,update 很高效,計算耗時並不多。
  2. autolayout 的實現沒有充分發揮 cassowary 的優點,沒有父 window 的 view 重新建立 NSISEnginer 以及更新 intrincContentSize 需要重新建立和新增 NSLayoutConstraint 的設計加重了計算的負擔
  3. cassowary 演算法佔整體耗時並不多,text layout 對效能的影響大於 cassowary,autolayout 只能把 textlayout 放主線,使得 text layout 的耗時對流暢度的影響不可避免。
  4. autolayout 重複的計算,重複的 text layout 使得整體耗時增加很多。
  5. CPU 排程使得計算可用時間很少。

Panda

為了解決上述問題,我用 swift 實現了一套非同步繪製和 layout 元件 Panda

Panda 包含第三個部分:

  1. Cassowary Cassowary 演算法
  2. Layoutable Autolayout API
  3. Panda 非同步繪製元件

Cassowary 是單純的線性規劃求解器;Layoutable 是在 Cassowary 之上構建的 'autolayout' ,底層上實現了類似 NSLayoutConstraint ,NSLayoutAnchor 類似的 LayoutConstraint 和 Anchor,也封裝了更高階的 API 方便使用。Layoutable 提供 Layoutable 協議,任何實現了 Layoutable 的物件都可以使用 autolayout,比如 UIView,CALayer,或者其他自定義物件; Panda 則是實現了 Layoutable 協議的非同步繪製元件,提供非同步繪製,文字 layout 快取,和通用的 FlowLayout,StackLayout 複合佈局控制元件。

Panda 基本上解決了上面提到的問題

  1. Panda 裡的 ViewNode 物件不繼承自 UIView,計算高度的時候 不需要建立 view,也不操作 layer,開銷更小;可以繁重把 text layout 計算從主執行緒剝離出去
  2. 預設會快取住 text layout 物件和結果,減少 text layout 計算過程,即使再次 layout 也不需要再 text layout 上耗時
  3. 不會重新建立線性方程求解器和新增約束;更新 intrincContentSize 不會重新建立約束,只會更新約束常量。重複利用 Cassowary 的優勢。
  4. 對於多行文字,提供 fixedWidth 優化屬性,大部分情況下可以避免一部分 text layout
  5. 支援非同步繪製,利用多執行緒提高效率。
  6. 算高度的時候也可以快取住所有子 view 的 frame,然後在 cellForRowAIndexPath 中可以禁止自動佈局,直接使用快取資料,防止重複計算。

Panda 使用也很簡單, ViewNode,TextNode,ImageNode 分別代替 UIView,UILabel 和 UIImage,然後就可以像 autolayout 一樣佈局

let node = ViewNode()
let node1 = ViewNode()
let node2 = TextNode()

textNode.text = "hehe"

node.addSubnode(node1)
node.addSubnode(node2)

node1.size == (30,30)
node2.size == (40,40)
  
[node,node1].equal(.centerY,.left)  
/// 等價於
/// node.left == node1.left
/// node.centerY == node2.centerY
/// 或者 
/// node.left.equalTo(node1.left)
/// node.centerY.equalTo(node1.centerY)

[node2,node].equal(.top,.bottom,.centerY,.right)
[node1,node2].space(10, axis: .horizontal)

/// 支援約束優先順序
node.width == 100 ~.strong 
node.height == 200 ~ 760.0
update constant

/// 更新約束
let c =  node.left ==  10
c.constant = 100
  
複製程式碼

在上面提到的微博 Feed demo 中,只用 500 行程式碼就可以實現非常流程的列表。開發效率和執行效率都遠超手算 frame。程式碼更少,維護起來更方便。

對比 Texture(或者說 AsyncDisplayKit), Panda

  1. 整合成本更低。Panda 程式碼更少;使用上也不需要替換 UITabelView 或者 cell ,只需要實現 contentView 內容即可。
  2. 學習成本更低,API 和 思想上和 autolayout 都是一致的,對於 autolayout 使用者基本零門檻
  3. 完全 Swift 實現,對於使用 swift 的專案更友好。
  4. 開發效率和執行效率不輸 Texture

相關文章