Flexbox優化

mxn8704 7年前發布 | 10K 次閱讀 iOS開發 移動開發 flexbox

iOS 開發中給系統給我們提供了兩種布局,frame 布局 和 AutoLayout 布局,雖然frame布局性能很高 但面對現在越來越豐富的iPhone尺寸,為了適配各種機型需要寫更多的無營養代碼,造成代碼量增多。AutoLayout是iOS6 之后被引入的,由于其蹩腳、冗長的語法,直到出現一些優秀的開源第三方庫,自動布局 才真正意義上被應用到iOS 開發中。AutoLayout將我們從坐標中的點 面 思考帶到約束條件的思考,一條約束就是一個線性等式或不等式,一個視圖需要添加多個約束才能確定位置,這些約束最后組成一組線性方程組,再由Cassowary算法算出各個視圖的frame,所以性能的關鍵問題是解線性方程組的時間復雜度。

Flexbox

Flexbox 是W3C在2009年提出的一種新的前端頁面布局,目前,它已經得到了所有瀏覽器的支持。而最早將這一頁面布局方案引入iOS開發中的是開源庫 AsyncDisplayKit 。但隨著 React NativeWeex 在動態化領域的興起, 讓iOS開發越來越多的接觸到Flexbox 頁面布局。

Yoga

Yoga 是由C實現的Flexbox布局引擎,目前已經被用于在React Native 和 Weex 等開源項目中,性能和穩定性得到了很好的驗證。但不足的是Yoga只實現了W3C標準的一個子集。算法邏輯如下圖所示:

基于Yoga 引擎的Flexbox 布局優化

由于iOS 開發中frame布局的繁瑣及Auto Layout的性能問題,我們在開發中引入了Flexbox 布局,這樣不僅提高了布局的便利性和性能,而且可以與項目中的React Native 和Weex 保持一致的開發體驗。為了充分提高Flexbox 布局的性能和易用性我們維護一個開源的擴展FlexBoxLayout,支持鏈式調用,布局方便,虛擬視圖Div,TableView 支持自動高度、布局緩存,contentView緩存,和自動cache 失效機制

ScrollView 支持自適應contentSize,異步計算布局。

鏈式調用

- (void)layoutView {


  [_titleLabel fb_makeLayout:^(FBLayout *layout) {
    layout.margin.equalToEdgeInsets(UIEdgeInsetsMake(10, 0, 0, 0)).wrapContent();
  }];


  [_contentLabel fb_makeLayout:^(FBLayout *layout) {
    layout.margin.equalToEdgeInsets(UIEdgeInsetsMake(10, 0, 0, 0)).wrapContent();
  }];

  [_contentImageView fb_makeLayout:^(FBLayout *layout) {
    layout.margin.equalToEdgeInsets(UIEdgeInsetsMake(10, 0, 0, 0)).wrapContent();
  }];

  [_usernameLabel fb_makeLayout:^(FBLayout *layout) {
    layout.wrapContent().flexGrow.equalTo(@(1.0));
  }];

  [_timeLabel fb_makeLayout:^(FBLayout *layout) {
      layout.wrapContent().flexGrow.equalTo(@(1.0));
  }];

  FBLayoutDiv *div = [FBLayoutDiv layoutDivWithFlexDirection:FBFlexDirectionRow ];
  [div fb_makeLayout:^(FBLayout *layout) {
    layout.flexDirection.equalTo(@(FBFlexDirectionRow)).justifyContent.equalTo(@(FBJustifySpaceBetween)).alignItems.equalTo(@(FBAlignFlexStart)).margin.equalToEdgeInsets(UIEdgeInsetsMake(10, 0, 0, 0)).children(@[_usernameLabel,_timeLabel]);
  }];

  [self fb_makeLayout:^(FBLayout *layout) {
    layout.flexDirection.equalTo(@(FBFlexDirectionColumn)).margin.equalToEdgeInsets(UIEdgeInsetsMake(0, 15, 0, 15)).alignItems.equalTo(@(FBAlignFlexStart)).children(@[_titleLabel,_contentLabel,_contentImageView,div]);
  }];

}

`

UITableView Flexbox 布局滑動性能優化

UITableViewCell 自動計算高度 和 UITableView滑動性能一直是一個重要的性能優化。FlexBoxLayout能自動計算cell高度,優化了滑動性能,使UITableView在滑動時的幀率接近60FPS.

[self.tableView fb_setCellContnetViewBlockForIndexPath:^UIView *(NSIndexPath *indexPath) {
   return [[FBFeedView alloc]initWithModel:weakSelf.sections[indexPath.section][indexPath.row]];
 }];

 ....

 - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
   return [self.tableView fb_heightForIndexPath:indexPath];
 }

 - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
   return [self.tableView fb_cellForIndexPath:indexPath];
 }

上面的代碼,做了以下幾件事:

  • 開發者只需要關注Cell 的ContentView
    會自動重利用cell,開發者只需要實現cell的contentView具體內容.
  • 自動計算高度
    會根據Flexbox布局自動計算出高度
  • 高度緩存機制
    計算出的高度會自動進行緩存,當滾動cell,重利用cell,后面對應的index path cell的高度的詢問都會命中緩存,減少cpu的計算任務。


  • 高度緩存失效機制
    當數據源發生變化時,調用以下幾個api
reloadData
insertSections:withRowAnimation:
deleteSections:withRowAnimation:
reloadSections:withRowAnimation:
moveSection:toSection:
insertRowsAtIndexPaths:withRowAnimation:
deleteRowsAtIndexPaths:withRowAnimation:
reloadRowsAtIndexPaths:withRowAnimation:
moveRowAtIndexPath:toIndexPath:

刷新頁面時,會對已有的高度進行失效處理,并重新計算新的緩存高度。

Demo 界面的刷新一直接近60FPS

性能對比簡單測試:

UIScrollView contentSize 自動計算

為了更好的適應不同的屏幕尺寸,我們在開發中會經常用到scrollview進行適配,針對flexbox布局scrollview的contentsize大小計算問題,我們也進行了封裝處理。給scrollview增加了一個contendiv 虛擬容器,所有的子視圖只需要添加到contendiv中進行flexbox布局。

FBLayoutDiv *root = [FBLayoutDiv layoutDivWithFlexDirection:FBFlexDirectionRow
                                             justifyContent:FBJustifySpaceAround
                                                 alignItems:FBAlignCenter
                                                   children:@[div1,div2]];

 contentView.fb_contentDiv = root;

設置UIScrollView的fb_contentDiv屬性,當Flexbox布局計算完成應用到視圖上時,

在layoutSubviews函數中

UIScrollView的contentSize 會被設置大小。

Flexbox 異步計算

現在的iOS設備都是多核的,為了充分利用設備的多核能力,將布局的計算放在后臺線程,異步計算,同時為了減少過多線程切換的開銷,在MainRunLoop即將休眠時把計算好的布局應用到視圖上。

RunLoop 在運行時,當切換時會發送通知:

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry         = (1UL << 0), // 即將進入Loop
    kCFRunLoopBeforeTimers  = (1UL << 1), // 即將處理 Timer
    kCFRunLoopBeforeSources = (1UL << 2), // 即將處理 Source
    kCFRunLoopBeforeWaiting = (1UL << 5), // 即將進入休眠
    kCFRunLoopAfterWaiting  = (1UL << 6), // 剛從休眠中喚醒
    kCFRunLoopExit          = (1UL << 7), // 即將退出Loop
};

創建布局計算完成事件源,當布局完成,發送事件源,喚醒MainRunLoop, 當MainRunLoop處理完成了所有事件,馬上要休眠時,批量處理計算好的布局。

CFRunLoopObserverRef observer;

CFRunLoopRef runLoop = CFRunLoopGetMain();

CFOptionFlags activities = (kCFRunLoopBeforeWaiting | kCFRunLoopExit);

observer = CFRunLoopObserverCreate(NULL,
                                   activities,
                                   YES,
                                   INT_MAX,
                                   &_messageGroupRunLoopObserverCallback,
                                   NULL);

if (observer) {
  CFRunLoopAddObserver(runLoop, observer, kCFRunLoopCommonModes);
  CFRelease(observer);
}

CFRunLoopSourceContext  *sourceContext = calloc(1, sizeof(CFRunLoopSourceContext));

sourceContext->perform = &sourceContextCallBackLog;

 _runLoopSource = CFRunLoopSourceCreate(NULL, 0, sourceContext);

if (_runLoopSource) {
  CFRunLoopAddSource(runLoop, _runLoopSource, kCFRunLoopCommonModes);
}

當RunLoop馬上要休眠時,在_messageGroupRunLoopObserverCallback函數處理布局應用。

使用FlexboxLayout

如果你能覺得這個工具能夠幫到你,他是容易整合到項目的

pod “FlexBoxLayout”

 

 

來自:http://lrd.ele.me/2017/03/13/iOS-Flexbox/

 

 本文由用戶 mxn8704 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!