Frame AutoLayout
iOS 開發(fā)中給系統(tǒng)給我們提供了兩種布局碌尔,frame 布局 和 AutoLayout 布局懒浮,雖然frame布局性能很高 但面對現(xiàn)在越來越豐富的iPhone尺寸梭伐,為了適配各種機(jī)型需要寫更多的無營養(yǎng)代碼读整,造成代碼量增多狂秘。AutoLayout是iOS6 之后被引入的硕噩,由于其蹩腳假残、冗長的語法,直到出現(xiàn)一些優(yōu)秀的開源第三方庫炉擅,自動布局 才真正意義上被應(yīng)用到iOS 開發(fā)中辉懒。AutoLayout將我們從坐標(biāo)中的點(diǎn) 面 思考帶到約束條件的思考,一條約束就是一個線性等式或不等式谍失,一個視圖需要添加多個約束才能確定位置眶俩,這些約束最后組成一組線性方程組,再由Cassowary算法算出各個視圖的frame袱贮,所以性能的關(guān)鍵問題是解線性方程組的時間復(fù)雜度仿便。
Flexbox
Flexbox 是W3C在2009年提出的一種新的前端頁面布局,目前攒巍,它已經(jīng)得到了所有瀏覽器的支持嗽仪。而最早將這一頁面布局方案引入iOS開發(fā)中的是開源庫 AsyncDisplayKit。但隨著React Native與Weex在動態(tài)化領(lǐng)域的興起柒莉, 讓iOS開發(fā)越來越多的接觸到Flexbox 頁面布局闻坚。
Yoga
Yoga 是由C實(shí)現(xiàn)的Flexbox布局引擎,目前已經(jīng)被用于在React Native 和 Weex 等開源項目中兢孝,性能和穩(wěn)定性得到了很好的驗(yàn)證窿凤。但不足的是Yoga只實(shí)現(xiàn)了W3C標(biāo)準(zhǔn)的一個子集。算法邏輯如下圖所示:
基于Yoga 引擎的Flexbox 布局優(yōu)化
由于iOS 開發(fā)中frame布局的繁瑣及Auto Layout的性能問題跨蟹,我們在開發(fā)中引入了Flexbox 布局雳殊,這樣不僅提高了布局的便利性和性能,而且可以與項目中的React Native 和Weex 保持一致的開發(fā)體驗(yàn)窗轩。為了充分提高Flexbox 布局的性能和易用性我們維護(hù)一個開源的擴(kuò)展FlexBoxLayout夯秃,支持鏈?zhǔn)秸{(diào)用,布局方便痢艺,虛擬視圖Div仓洼,TableView 支持自動高度、布局緩存堤舒,contentView緩存色建,和自動cache 失效機(jī)制
ScrollView 支持自適應(yīng)contentSize,異步計算布局舌缤。 github鏈接請戳我箕戳。
鏈?zhǔn)秸{(diào)用
- (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 布局滑動性能優(yōu)化
UITableViewCell 自動計算高度 和 UITableView滑動性能一直是一個重要的性能優(yōu)化某残。FlexBoxLayout能自動計算cell高度,優(yōu)化了滑動性能漂羊,使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];
}
上面的代碼驾锰,做了以下幾件事:
- 開發(fā)者只需要關(guān)注Cell 的ContentView
會自動重利用cell卸留,開發(fā)者只需要實(shí)現(xiàn)cell的contentView具體內(nèi)容. - 自動計算高度
會根據(jù)Flexbox布局自動計算出高度 -
高度緩存機(jī)制
計算出的高度會自動進(jìn)行緩存走越,當(dāng)滾動cell,重利用cell耻瑟,后面對應(yīng)的index path cell的高度的詢問都會命中緩存旨指,減少cpu的計算任務(wù)。
- 高度緩存失效機(jī)制
當(dāng)數(shù)據(jù)源發(fā)生變化時喳整,調(diào)用以下幾個api
reloadData
insertSections:withRowAnimation:
deleteSections:withRowAnimation:
reloadSections:withRowAnimation:
moveSection:toSection:
insertRowsAtIndexPaths:withRowAnimation:
deleteRowsAtIndexPaths:withRowAnimation:
reloadRowsAtIndexPaths:withRowAnimation:
moveRowAtIndexPath:toIndexPath:
刷新頁面時谆构,會對已有的高度進(jìn)行失效處理,并重新計算新的緩存高度框都。
Demo 界面的刷新一直接近60FPS
性能對比簡單測試:
UIScrollView contentSize 自動計算
為了更好的適應(yīng)不同的屏幕尺寸搬素,我們在開發(fā)中會經(jīng)常用到scrollview進(jìn)行適配,針對flexbox布局scrollview的contentsize大小計算問題魏保,我們也進(jìn)行了封裝處理熬尺。給scrollview增加了一個contendiv 虛擬容器,所有的子視圖只需要添加到contendiv中進(jìn)行flexbox布局谓罗。
FBLayoutDiv *root = [FBLayoutDiv layoutDivWithFlexDirection:FBFlexDirectionRow
justifyContent:FBJustifySpaceAround
alignItems:FBAlignCenter
children:@[div1,div2]];
contentView.fb_contentDiv = root;
設(shè)置UIScrollView的fb_contentDiv屬性粱哼,當(dāng)Flexbox布局計算完成應(yīng)用到視圖上時,
在layoutSubviews函數(shù)中
UIScrollView的contentSize 會被設(shè)置大小檩咱。
Flexbox 異步計算
現(xiàn)在的iOS設(shè)備都是多核的揭措,為了充分利用設(shè)備的多核能力,將布局的計算放在后臺線程刻蚯,異步計算绊含,同時為了減少過多線程切換的開銷,在MainRunLoop即將休眠時把計算好的布局應(yīng)用到視圖上炊汹。
RunLoop 在運(yùn)行時躬充,當(dāng)切換時會發(fā)送通知:
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即將進(jìn)入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即將處理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即將處理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即將進(jìn)入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 剛從休眠中喚醒
kCFRunLoopExit = (1UL << 7), // 即將退出Loop
};
創(chuàng)建布局計算完成事件源,當(dāng)布局完成兵扬,發(fā)送事件源麻裳,喚醒MainRunLoop, 當(dāng)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);
}
當(dāng)RunLoop馬上要休眠時津坑,在_messageGroupRunLoopObserverCallback函數(shù)處理布局應(yīng)用。
使用FlexboxLayout
如果你能覺得這個工具能夠幫到你傲霸,他是容易整合到項目的
pod "FlexBoxLayout"
github地址