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

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

這篇文章會通過對 autolayout 內(nèi)部實(shí)現(xiàn)的探索和數(shù)據(jù)分析和對 autolayout 的性能問題做一個詳細(xì)的分析解寝,并在最后給出一個高性能 autolayout 的解決方案崎坊。開始看文章之前,可以先試試這個 demo ,使用 YYKit demo 數(shù)據(jù)做的微博 Feed 列表托嚣。使用我自己寫的異步繪制組件 Panda 和和 ‘a(chǎn)utolayout’ 框架 Layoutable 寫的 ,cell 代碼只有 五百多行,但是流暢度很高悍募。

Cassowary 算法性能

Autolayout 會將約束條件轉(zhuǎn)換成線性規(guī)劃問題,通過 Cassowary 算法求解線性規(guī)劃問題得到 frame。因此分析 autolayout 性能都繞不開 Cassowary 算法胡本。大部分分析最后都會給出結(jié)論 “autolayout 性能差是 cassowary 算法的多項式的時間復(fù)雜度造成的”。也有一些會給出 autolayout 的 benchmark 來證明 cassowary 算法的問題畸悬。但是

  1. Cassowary 是 1997 年就被發(fā)表并被稱作高效的線性方程求解算法侧甫,為什么 ‘8012’ 年了反而成了性能殺手?
  2. 如果是 Cassowary 算法的問題蹋宦,跑著 iOS 8 的 iPhone 6 應(yīng)該比實(shí)際表現(xiàn)更卡頓才合理披粟,畢竟算法時間復(fù)雜度不會隨著設(shè)備和系統(tǒng)升級下降。由于系統(tǒng)開銷造成的性能下降在 ios 設(shè)備升級的和過程中似乎額外的大了冷冗。

想到自己實(shí)現(xiàn) cassowary 算法和 autolayout 也是由對這兩個問題的不解引出的守屉。

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

線性規(guī)劃問題的求解很早就有通用解法--單純型法,有興趣的同學(xué)可以看看這篇文章 AutoLayout 中的線性規(guī)劃 - Simplex 算法
蒿辙⌒匕穑《算法導(dǎo)論》也有一章專門介紹單純型法的(所以誰說算法對 iOS 開發(fā)沒用??)敦捧。Cassowary 則是單純型法在用戶界面實(shí)踐中的應(yīng)用和改進(jìn)算法,解決一些實(shí)際使用的問題碰镜,最重要的增加了增量的概念(Autolayout 實(shí)現(xiàn)中 Cassowary 相關(guān)的代碼是以 NSIS 作為前綴的兢卵,IS 就是 incremental Simplex 增量單純型的縮寫 ),單純型法通過建立單純型表绪颖,在對單純形表進(jìn)行 pivot 和 optimize 操作得到最優(yōu)解秽荤;Cassowary 則是可以在已經(jīng)建立單純型表上,高效的進(jìn)行添加修改更新操作柠横。因?yàn)橛脩艚缑鎽?yīng)用中窃款,大部分約束已經(jīng)固定,界面變化只需要對其中的部分約束進(jìn)行更新或者進(jìn)行少量的增減操作牍氛。Cassowary 的高效是建立在增量跟新的基礎(chǔ)上的晨继。

完整介紹 Cassowary 需要很長篇幅,有時間單獨(dú)介紹搬俊,這里用數(shù)據(jù)說話

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

image
  • Autolayout: 是相對布局的耗時
  • Autolayout Nestlayout: 嵌套布局的耗時
  • update constant: 更新約束的耗時,即更新 NSLayoutConstraint 的 constant 常量紊扬。

因?yàn)檫@里沒有不含 UILabel,UIView 等有 intrincContentSize 的 UIView,update constant 基本就是 Cassowary 更新約束的耗時唉擂。Applelayout 和 Apple NestLayout 則也包含 UIView 創(chuàng)建餐屎,約束創(chuàng)建和求解的時間。

可以看到 update 約束是非常高效的玩祟, 80 個 view腹缩,160 條約束更新約束也只需要 2.5 個毫秒,這個數(shù)量在實(shí)際使用中基本上是用不到的空扎。實(shí)際使用中藏鹊,同時更新 40 個 view 80 條約束已經(jīng)算是很多的了,也只耗時 1.25 ms转锈。

列表滾動中伙判,一般情況下頁面加載的時候 cell 和 約束已經(jīng)創(chuàng)建,性能應(yīng)該主要和更新約束相關(guān)(更新約束包括 UILabel黑忱。UIView 更改 text ,image 造成的 size 變化宴抚,更新系統(tǒng)默認(rèn)的約束;也包括手動調(diào)整 NSLayoutConstraint 的 constant 屬性等)甫煞。為什么實(shí)際表現(xiàn)卻差很多呢菇曲?

Autolayout 設(shè)計問題

Autolayout 構(gòu)建在 Cassowary 之上,但是 autolayout 的一些機(jī)制沒有充分利用 Cassowary 更新高效的特點(diǎn)抚吠。我們可以通過私有類和方法來研究系統(tǒng)內(nèi)部的實(shí)現(xiàn)常潮。這里有一個網(wǎng)站 iOS SDK Header Dump 可以查看 iOS 的私有頭文件。其中 NSIS 開頭的類都是 Autolayout 相關(guān)的頭文件楷力。我把 iOS 11 Autolayout 相關(guān)的頭文件下載下來并做成了一個可以運(yùn)行的工程喊式》趸В可以 hook 內(nèi)部實(shí)現(xiàn)或者打印變量來觀察系統(tǒng)的調(diào)用,可以這里下載 ExplorAutolayout 岔留。后面一些測試代碼會基于這個工程夏哭。

  1. NSContentSizeLayoutConstraint

    這是 FDTemplateLayoutCell profile 的一段結(jié)果,展開部分是 cellForRowAIndex 里運(yùn)行的代碼献联。

    image

    理論上 cellForRowAIndex 是不需要創(chuàng)建 NSLayoutConstraint 的竖配,畢竟 cell 已經(jīng)創(chuàng)建過了, 更新數(shù)據(jù)的時候代碼中并沒有新加約束。但這里創(chuàng)建了 UIContentSizeLayoutConstraint 對象里逆,UIContentSizeLayoutConstraint 繼承自 NSLayoutConstraint,是專門用來約束 contentSize 的約束进胯。

    來一段測試代碼,我們在 NSLayoutConstraint 對象創(chuàng)建的時候輸出創(chuàng)建的約束類型:

    // 子類化 UIlabel原押,每次調(diào)用 intrinsicContentSize 輸出大小
    @implementation TestLabel
    
    - (CGSize)intrinsicContentSize{  
        NSLog(@"width: %f, height: %f",size.width,size.height);
        return [super intrinsicContentSize];
    }
    
    @end
    
    // 替換 NSLayoutConstraint init 方法胁镐,每次輸出創(chuàng)建的類型
    @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 給一個寬度約束,然后設(shè)置 text, layoutIfNeeded 強(qiáng)制布局 輸出結(jié)果:

    width: 1073741824.000000, height: 20.500000
    New NSContentSizeLayoutConstraint
    New NSContentSizeLayoutConstraint
    width: 296.500000, height: 41.000000
    New NSContentSizeLayoutConstraint
    New NSContentSizeLayoutConstraint
    

    創(chuàng)建的兩個約束是根據(jù) intrinsicContentSize 值給的寬度和高度約束。也就是每次 intrinsicContentSize 變化的時候诸衔,Autolayout 都會創(chuàng)建兩個新的 NSContentSizeLayoutConstraint 約束分別約束寬和高盯漂,添加到 NSISEnginer 中求解, 而不是直接更新已經(jīng)創(chuàng)建好的約束。

    水果公司一邊告訴我們重新添加約束比更新約束低效署隘,一邊在頻繁調(diào)用的地方用著低效的方法??宠能。

  2. systemLayoutSizeFittingSize

    NSContentSizeLayoutConstraint 只是蘋果浪費(fèi) Cassowary 算法優(yōu)點(diǎn)的一個地方亚隙,

    • 看另一組不包含 intrinsicContentSizeUIView 的數(shù)據(jù)磁餐,都是單純的更新約束,區(qū)別只在于有沒有添加到 window 上,以及強(qiáng)制布局的方法:

      image
      • Apple constant 是 view 沒有并添加到 window 上阿弃,更新約束后調(diào)用 layoutIfNeeded 的數(shù)據(jù)诊霹。
      • Apple In Window constant是把 view 添加到當(dāng)前 window 上,更新約束后調(diào)用 layoutIfNeeded 的數(shù)據(jù)
      • SystemFitSize constant 是調(diào)用 systemlayoutFitSize 獲取高度的數(shù)據(jù)渣淳。

      同樣是更新約束脾还,耗時差距卻非常大,添加到 window 上再調(diào)用 layoutIfNeeded 的耗時遠(yuǎn)小于沒有加到 window 上入愧。同樣沒有加到 window 上鄙漏,systemlayoutFitSize 耗時又要小于 layoutIfNeeded.

    • 再以 FDTemplateLayoutCell 為例,我們在同一方法中同事調(diào)用 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 下

      image

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

    • 看一下 autolayout 調(diào)用的過程:

      替換 NSISEnginerNSISEnginer 就是 autolayout 的 線性規(guī)劃求解器)的 init 方法棺蛛,每次創(chuàng)建 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);
      }
      

      調(diào)用方法觀察輸出:

        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");
        }
      

      打印結(jié)果是

       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 之前, 調(diào)用 layoutIfNeededsystemLayoutSizeFittingSize 每次都會創(chuàng)建 NSISEnginer;添加到 window 上以后旁赊,layoutIfNeeded 并不會創(chuàng)建 NSISEnginer, 而systemLayoutSizeFittingSize 還是每次都會創(chuàng)建 NSISEnginer桦踊。創(chuàng)建新的 NSISEnginer 則意味著對應(yīng)的所有約束,也會重新添加到 NSISEnginer,重新進(jìn)行優(yōu)化求解终畅,這時候的耗時就變成了初次添加約束的時間籍胯。在列表的使用中竟闪,我們一般會在 heightForRowAtIndexPath 中創(chuàng)建一個不會添加到 window 上的 cell 調(diào)用 systemLayoutSizeFittingSize 來計算高度。這個的計算耗時就要比 cellForRowAtIndexPath 中的耗時大很多杖狼。

      image

    systemLayoutSizeFittingSize 會重新創(chuàng)建 NSISEnginer和 WWDC 《High performance Autolayout》 所講也是一致的炼蛤。使用 systemLayoutSizeFittingSize 時,Autolayout 會創(chuàng)建新的 NSISEnginer 對象,重新添加約束求解本刽,然后釋放掉 NSISEnginer 對象鲸湃。而對于 layoutIfNeeded 也很好理解,Autolayout 中子寓,一個 window 層級下的 view 會共用 window 節(jié)點(diǎn)的 NSISEnginer 對象暗挑,沒有添加到 window 上的 view 沒有父 window 也就沒辦法共用,只能重新創(chuàng)建.

    image

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

    那么能不能在算高度時候把 cell 添加到 window 上鲜屏,隱藏烹看,然后用 layoutIfNeeded 來提高效率?

    ??:呵呵 ??

    systemLayoutSizeFittingSize 對計算做了優(yōu)化,計算好以后不會對 view 的 frame 進(jìn)行操作洛史,也就避免 layer 調(diào)整的相關(guān)耗時惯殊。所以同樣是創(chuàng)建 NSISEnginer 重新添加約束, systemLayoutSizeFittingSizelayoutIfNeeded 要高效也殖;添加到 window 上以后土思,layoutIfNeeded 計算的效率高于 systemLayoutSizeFittingSize,但是 setFrame 和觸發(fā)的 layer 相關(guān)操作又會有額外的耗時,不一定會比直接使用 systemLayoutSizeFittingSize 耗時少 忆嗜。

    image

The Enginer is a layout cache and dependency tracker

Cassowary 的增量更新機(jī)制其實(shí)也算是某種程度上的緩存機(jī)制己儒,重新創(chuàng)建 Enginer 的設(shè)計也就丟掉了 cache 的能力,降低了性能捆毫。

Text layout 對性能的影響

雖然由于上述種種問題闪湾, 但如上圖所示 heightForRowAtIndexPath 里調(diào)用 systemLayoutSizeFittingSize 再加上 cellForRowAtIndexPath 里調(diào)用 layoutIfNeeded 總耗時看起來也并不是很多,40 個 view 左右耗時也不到 4 ms绩卤,看起來還可以途样,為什么實(shí)際使用起來表現(xiàn)卻差很多呢。

  1. text layout 才是性能殺手

    1. 以 FDTemplateLayoutCell demo濒憋,為例何暇,我們對同一個 cell 連續(xù)執(zhí)行三次一樣的代碼,

      [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"];
      
      

      結(jié)果差距很大

      image

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

      如果把第一遍展開的話跋炕,就會發(fā)現(xiàn)大部分時間都是在文字上:

      image

      后面兩遍因?yàn)楹偷谝槐榈臄?shù)據(jù)一樣赖晶,不會觸發(fā)文字相關(guān)的操作。計算的時間只占了 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 循環(huán)中,除了輸出的描述文案遏插,代碼是一樣的捂贿,Panda 的實(shí)現(xiàn)中,會把已經(jīng)創(chuàng)建的 TextKit 組成的 TextRender 對象緩存起來胳嘲,并且是不可變厂僧。再次出現(xiàn)相同的文字會從緩存取。

      第一次 for 循環(huán)中了牛,不存在相應(yīng)的 TextRender 對象颜屠,每次都需要創(chuàng)建新的 TextRender 對象并進(jìn)行 layout

      第二次 for 循環(huán)中,因?yàn)榈谝淮斡嬎氵^程中已經(jīng)緩存了 TextRender鹰祸,基本上只是單純?nèi)≈岛?Cassowary 更新約束計算甫窟。

      結(jié)果:(iPhone6 , iOS 12)

    image

    - Panda FirstPass 是第一個 for 循環(huán)數(shù)據(jù)
    - Panda SecondPass 是第二個 for 循環(huán)數(shù)據(jù)
    - YYKit 則是 YYKit 手算 frame 的數(shù)據(jù)
    - 縱坐標(biāo)是耗時

    同樣更新數(shù)據(jù),同樣的 update 約束蛙婴,同樣的 Panda Layout 數(shù)據(jù)相差卻非常大粗井。而且第二次數(shù)據(jù)更加平穩(wěn)

    對于 Panda Layou,相差的數(shù)據(jù)基本就是 text layout 的時間街图。第一次 Layout 平均數(shù)據(jù) 5.94浇衬,第二次平均數(shù)據(jù) 1.44. text layout 占了總耗時的 70%-80%。

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

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

    其實(shí)上一個 NSContentSizeLayoutConstraint 的輸出結(jié)果中已經(jīng)給出部分答案耘擂,只設(shè)置一次 text,卻輸出了兩次 intrinsicContentSize,而且結(jié)果也不一樣絮姆。 檢查一下 UIView 的私有方法醉冤,會發(fā)現(xiàn)一個_needsDoubleUpdateConstraintsPass 的方法,返回值為 true 的話滚朵,會調(diào)用兩次 intrinsicContentSize 方法冤灾。

    • 手撕的 frame 時候開發(fā)人員需要額外注意計算順序前域。比如計算一個多行的 UILabel辕近,可能會先把左右兩邊相關(guān)的寬度計算好,這樣可以知道 UILabel 最大寬度匿垄,或者直接指定 UILabel 的最大寬度移宅,使用 size(withAttributes:) 進(jìn)行一次 text layout 就可以把文字大小算出來。
    • Autolayout 使開發(fā)者免去了操心布局順序的負(fù)擔(dān)(這也是 Autolayout 一個比較核心的優(yōu)點(diǎn))椿疗, 導(dǎo)致更新 UILabel 的約束時不能直接確定 UILabel 的最大寬度漏峰,怎么解決換行的問題?(iOS 6 的時候需要手動設(shè)置 preferrdMaxLayoutWidth届榄,很多時候會造成很大困擾浅乔,因?yàn)椴⒉皇悄敲慈菀状_定)。 對于多行文字的 UILabel,Autolayout 會進(jìn)行兩邊 layout. 第一次 layout 會先假設(shè)文本可以一行展示完,進(jìn)行一次 text layout 靖苇,計算一行文字的大小席噩,更新 UILabel 的 size 約束。size 的寬高約束都不是 required 的贤壁,外部如果有對寬度相關(guān)的約束的話悼枢,也不會沖突。整個 view 層級一次布局結(jié)束之后脾拆,所有 view 的寬度就確定了馒索,第二遍 layout 再以當(dāng)前寬度再做一次 text layout ,更新文本寬高。這樣 autolayout 文本的多行文字 textLayout 過程就要比手算 frame 多一倍名船。多行文本 layout 一般耗時更長绰上。多出來一次的 text layout 的耗時就很多了了。
    image

    textlayout 耗時占比很大渠驼,這也是為什么蘋果推薦重寫 UIlable 的 intrinsicContentSize 方法渔期,然后約束寬高的方式來避免 text layout。但是實(shí)際使用中能這樣優(yōu)化的場景并不多渴邦。

  2. 主線程運(yùn)行的影響

    關(guān)于列表性能優(yōu)化疯趟,大家比較喜歡說的就是 frame 比 autolayout 快,其實(shí)更重要的是 frame 相對 autolayout 可以減少一些重復(fù)計算谋梭,以及把耗時操作丟到后臺線程信峻。

    1. 手算 frame 可以放到后臺線程,從而避免了主線程的 text layout瓮床。
    2. 手算 frame 只會 layout 一遍盹舞,autolayout heightForRowAtIndexPathcellForRowAtIndexPath 都需要計算,這個多出來的的計算和 text layout 就更多了隘庄。

Textlayout 在計算和渲染過程占的比重很大踢步,也是很多 app 即使 cell 高度用 frame 算,沒有做 text layout 相關(guān)緩存或者異步 Label 也會不流暢的原因丑掺。單純做計算的優(yōu)化获印,不做 text layout 緩存的布局框架一般實(shí)際表現(xiàn)都不會太好。

CPU 調(diào)度對列表性能的影響

上面的 benchmark 是針對 iPhone 6 的, 數(shù)據(jù)其實(shí)已經(jīng)很不錯了街州,更好的設(shè)備豈不是要逆天兼丰?

看一組 iPhneX 的數(shù)據(jù) (iPhoneX , iOS 12)

image

即使第一次 layout,Panda 和 YYKit 平均耗時只有 1.34 毫秒,只更新約束更是只需要 0.287 毫秒唆缴。(這個數(shù)據(jù)遠(yuǎn)好于 2016 MacBook Pro 的表現(xiàn))鳍征。時間寬裕度很大,看起來即使 autolayout 的耗時多個一兩倍問題也不大面徽。

Apple: 呵呵??

benchmark 出來的耗時其實(shí)一般和實(shí)際運(yùn)行是不一樣艳丛。同樣 iOS 12 iPhoneX ,如果對列表進(jìn)行快速滑動的話,是可以到達(dá) benchmark 的數(shù)據(jù);如果滑動的不是很快的氮双,上面 0.x,1.x ms 的耗時旺聚,很多就變成了 6 - 9 ms 左右。

image
image

CPU 達(dá)到最好性能是需要時間的眶蕉,benchmark 過程計算比較集中砰粹, CPU 一直處于高性能狀態(tài)。但是滑的慢一點(diǎn)的話造挽,可能 CPU 性能還沒起來計算就結(jié)束了碱璃。然后 CPU 開始偷懶。剛好性能下去以后另一計算過程又開始了饭入。而且 iOS 12 這個已經(jīng)優(yōu)化過了嵌器,iOS11 和 iOS 10 表現(xiàn)更差。做 benchmark 的有時候也會有一個有趣的現(xiàn)象谐丢,如果有幾組數(shù)據(jù)需要測試爽航,在同一段代碼里調(diào)用這些方法進(jìn)行測試,方法的調(diào)用順序?qū)?benchmark 出來的數(shù)據(jù)影響特別大乾忱。放在第一個的方法耗時會被大大增加讥珍。

Autolayout 一些結(jié)論

總結(jié)一下,autolayout 性能不好并不是以前經(jīng)痴粒看到的是因?yàn)?cassowary 算法差導(dǎo)致的

  1. cassowary 算法性能并沒有太大問題衷佃,update 很高效,計算耗時并不多蹄葱。
  2. autolayout 的實(shí)現(xiàn)沒有充分發(fā)揮 cassowary 的優(yōu)點(diǎn)氏义,沒有父 window 的 view 重新創(chuàng)建 NSISEnginer 以及更新 intrincContentSize 需要重新創(chuàng)建和添加 NSLayoutConstraint 的設(shè)計加重了計算的負(fù)擔(dān)
  3. cassowary 算法占整體耗時并不多,text layout 對性能的影響大于 cassowary,autolayout 只能把 textlayout 放主線图云,使得 text layout 的耗時對流暢度的影響不可避免惯悠。
  4. autolayout 重復(fù)的計算,重復(fù)的 text layout 使得整體耗時增加很多竣况。
  5. CPU 調(diào)度使得計算可用時間很少克婶。

Panda

為了解決上述問題,我用 swift 實(shí)現(xiàn)了一套異步繪制和 layout 組件 Panda帕翻。

Panda 包含第三個部分:

  1. Cassowary Cassowary 算法
  2. Layoutable Autolayout API
  3. Panda 異步繪制組件

Cassowary 是單純的線性規(guī)劃求解器鸠补;Layoutable 是在 Cassowary 之上構(gòu)建的 'autolayout' 萝风,底層上實(shí)現(xiàn)了類似 NSLayoutConstraint 嘀掸,NSLayoutAnchor 類似的 LayoutConstraint 和 Anchor,也封裝了更高級的 API 方便使用。Layoutable 提供 Layoutable 協(xié)議规惰,任何實(shí)現(xiàn)了 Layoutable 的對象都可以使用 autolayout睬塌,比如 UIView,CALayer,或者其他自定義對象; Panda 則是實(shí)現(xiàn)了 Layoutable 協(xié)議的異步繪制組件,提供異步繪制揩晴,文本 layout 緩存,和通用的 FlowLayout,StackLayout 復(fù)合布局控件勋陪。

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

  1. Panda 里的 ViewNode 對象不繼承自 UIView,計算高度的時候 不需要創(chuàng)建 view,也不操作 layer,開銷更辛蚶肌诅愚;可以繁重把 text layout 計算從主線程剝離出去
  2. 默認(rèn)會緩存住 text layout 對象和結(jié)果,減少 text layout 計算過程劫映,即使再次 layout 也不需要再 text layout 上耗時
  3. 不會重新創(chuàng)建線性方程求解器和添加約束违孝;更新 intrincContentSize 不會重新創(chuàng)建約束,只會更新約束常量泳赋。重復(fù)利用 Cassowary 的優(yōu)勢雌桑。
  4. 對于多行文本,提供 fixedWidth 優(yōu)化屬性祖今,大部分情況下可以避免一部分 text layout
  5. 支持異步繪制,利用多線程提高效率。
  6. 算高度的時候也可以緩存住所有子 view 的 frame疹味,然后在 cellForRowAIndexPath 中可以禁止自動布局蚪黑,直接使用緩存數(shù)據(jù),防止重復(fù)計算徐绑。

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)

/// 支持約束優(yōu)先級
node.width == 100 ~.strong 
node.height == 200 ~ 760.0
update constant

/// 更新約束
let c =  node.left ==  10
c.constant = 100
  

在上面提到的微博 Feed demo 中,只用 500 行代碼就可以實(shí)現(xiàn)非常流程的列表泵三。開發(fā)效率和運(yùn)行效率都遠(yuǎn)超手算 frame耕捞。代碼更少,維護(hù)起來更方便烫幕。

對比 Texture(或者說 AsyncDisplayKit), Panda

  1. 集成成本更低俺抽。Panda 代碼更少;使用上也不需要替換 UITabelView 或者 cell ,只需要實(shí)現(xiàn) contentView 內(nèi)容即可较曼。
  2. 學(xué)習(xí)成本更低磷斧,API 和 思想上和 autolayout 都是一致的,對于 autolayout 使用者基本零門檻
  3. 完全 Swift 實(shí)現(xiàn)捷犹,對于使用 swift 的項目更友好弛饭。
  4. 開發(fā)效率和運(yùn)行效率不輸 Texture
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市萍歉,隨后出現(xiàn)的幾起案子侣颂,更是在濱河造成了極大的恐慌,老刑警劉巖枪孩,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件憔晒,死亡現(xiàn)場離奇詭異藻肄,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)拒担,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進(jìn)店門嘹屯,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人从撼,你說我怎么就攤上這事州弟。” “怎么了低零?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵呆馁,是天一觀的道長。 經(jīng)常有香客問我毁兆,道長浙滤,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任气堕,我火速辦了婚禮纺腊,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘茎芭。我一直安慰自己揖膜,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布梅桩。 她就那樣靜靜地躺著壹粟,像睡著了一般。 火紅的嫁衣襯著肌膚如雪宿百。 梳的紋絲不亂的頭發(fā)上趁仙,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天,我揣著相機(jī)與錄音垦页,去河邊找鬼雀费。 笑死,一個胖子當(dāng)著我的面吹牛痊焊,可吹牛的內(nèi)容都是我干的盏袄。 我是一名探鬼主播,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼薄啥,長吁一口氣:“原來是場噩夢啊……” “哼辕羽!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起垄惧,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤刁愿,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后赘艳,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體酌毡,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡克握,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年蕾管,在試婚紗的時候發(fā)現(xiàn)自己被綠了枷踏。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡掰曾,死狀恐怖旭蠕,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情旷坦,我是刑警寧澤掏熬,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站秒梅,受9級特大地震影響旗芬,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜捆蜀,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一疮丛、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧辆它,春花似錦誊薄、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至飒筑,卻和暖如春片吊,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背协屡。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工定鸟, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人著瓶。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓联予,卻偏偏與公主長得像,于是被迫代替她去往敵國和親材原。 傳聞我的和親對象是個殘疾皇子沸久,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評論 2 345

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