WXRecycleListComponent在iOS中的實現(xiàn)
概述
WXRecycleListComponent是阿里Weex團隊在17年下半年為了對超長列表能更好地進行展示,而提供的一個新的解決方案监婶,在Weex的0.18.0版本正式對外發(fā)布Release版本摆舟,號稱在內存使用上進行了較大的優(yōu)化改進罩驻,且具有較好的FPS铃将,內存和滑動流暢度當然是每個Weex使用者最為關心的話題剑令。我們知道之前Weex提供的WXListComponent組件透傳了Native的UITableView,在內存的控制上較普通的Scroller提升明顯赏寇,引入了只渲染可見區(qū)域捻撑,View內存回收復用等機制保證了內存使用率磨隘。但是WXListComponent對超長列表的顯示還是存在著內存和FPS問題,這篇文章就讓我們一起來看看WXRecycleListComponent這個新組件在iOS中的具體實現(xiàn)顾患,如何在這些方面提出了新的解決方案番捂。
整體架構
我們先從架構角度來對比傳統(tǒng)的WXRecycleListComponent組件
與WXRecycleListComponent組件有哪些區(qū)別?
最左邊的業(yè)務代碼角度來看江解,兩種架構是相同的結構设预,都是M個模塊+N條數據。而傳統(tǒng)的組件會在JSFramework層將N條數據解析生成為N個虛擬節(jié)點Virtual DOM犁河,再通過call native方法調用生成Native中的組件Component并渲染出具體的視圖鳖枕。WXListComponent中做的一個優(yōu)化點就是將不可見區(qū)域的view及時的回收,需要的時候再渲染桨螺,這里要特別注意宾符,這個操作級別是視圖(View)級別。這個優(yōu)化必然會比全部view都渲染好很多灭翔,但是這個流程本質并不是復用的概念魏烫,當數據量達到一定級別后,這個方案還是會存在內存上的問題,同時View的回收和渲染也同樣會降低幀率(FPS)哄褒。
對比下圖的WXListComponent架構中的JSFramwork部分稀蟋,這里做了較大的改變。不在根據數據來生成元素dom呐赡,而是根據模板來生成模板dom退客,然后再通過call native方法調用生成Native中可復用的Component,再把數據當成數據源链嘀,分別對應加載復用的Component萌狂。這與iOS列表(UITableView/UICollectionView)中真正的復用原理一致,生成多個復用的cell管闷,通過數據源來決定使用哪個cell粥脚,完成cell級別的復用窃肠。從全局方案的角度來開包个,這必然對整體內存和滑動流暢度都具有較大的提高。但是這打破了傳統(tǒng)的weex組件解析方案和流程冤留,必然會產生一定的工作量和不穩(wěn)定性碧囊,我們下文具體分析其內部實現(xiàn)。
WXRecycleListComponent在SDK的Component目錄中單獨有一個RecycleList目錄纤怒,目前共包含16個文件糯而。
我們從直觀上可以感受到WXRecycleListComponent整個實現(xiàn)方案還是具有一定的復雜性。在SDK中注冊Component的是WXRecycleListComponent類泊窘,我們先看下其interface中要實現(xiàn)的代理和屬性熄驼,大致了解下整個組件的一個框架。
@interface WXRecycleListComponent : WXScrollerComponent
@property(nonatomic, strong) WXRecycleListDataManager *dataManager;
@property(nonatomic, strong) WXRecycleListTemplateManager *templateManager;
@property(nonatomic, strong) WXRecycleListUpdateManager *updateManager;
@end
@interface WXRecycleListComponent () <WXRecycleListLayoutDelegate, WXRecycleListUpdateDelegate, UICollectionViewDelegateFlowLayout, UICollectionViewDataSource>
@end
通過上述代碼烘豹,我們看到組件內部實現(xiàn)了UICollectionViewDataSource和UICollectionViewDelegateFlowLayout的代理方法瓜贾,那么WXRecycleListComponent的列表展示所對應的Native組件為UICollectionView。其他3個Manager對象和2個自定義delegate的具體功能携悯,我們繼續(xù)展開介紹祭芦。
3個Manager對象:DataManager、TemplateManager憔鬼、UpdateManager
WXRecycleListDataManager
由于WXRecycleListComponent是基于UICollectionView的龟劲,頁面的顯示必然要使用到數據源,所以WXRecycleListDataManager的作用就是管理整個列表的數據源轴或,也就是架構圖中紫色的部分昌跌。列表數據的每次更新都要更新這個類中的數據源,這個類提供最基本的數據初始化照雁、數據更新蚕愤、查詢數量等功能。
@interface WXRecycleListDataManager : NSObject
- (instancetype)initWithData:(NSArray *)data;
- (void)updateData:(NSArray *)data;
- (NSArray *)data;
- (NSDictionary *)dataAtIndex:(NSInteger)index;
- (NSInteger)numberOfItems;
- (NSInteger)numberOfVirtualComponent;
- (NSDictionary*)virtualComponentDataWithId:(NSString*)componentId;
- (void)updateVirtualComponentData:(NSString*)componentId data:(NSDictionary*)data;
- (NSDictionary*)virtualComponentDataWithIndexPath:(NSIndexPath*)indexPath;
- (NSString*)virtualComponentIdWithIndexPath:(NSIndexPath*)indexPath;
- (void)deleteVirtualComponentAtIndexPaths:(NSArray<NSIndexPath*>*)indexPaths;
@end
WXRecycleListTemplateManager
WXRecycleListComponent的展示、回收與恢復是基于一群模板Cell的审胸,WXRecycleListTemplateManager的作用就是負責如何管理這些可回收的模板Cell。
- (void)addTemplate:(WXCellSlotComponent *)component;
- (WXCellSlotComponent *)dequeueCellSlotWithType:(NSString *)type forIndexPath:(NSIndexPath *)indexPath;
- (WXCellSlotComponent *)templateWithType:(NSString *)type;
WXRecycleListComponent會在_insertSubcomponent方法中烫扼,調用[addTemplate:]來添加模板類型,一個WXRecycleListComponent中的所有模板會存在一張Map表中,key為WXCellSlotComponent對象的templateCaseType屬性(對應Vue中<cell-slot>
的 case
或者 default
屬性)双絮,object就是具體的模板Cell組件WXCellSlotComponent。同時會為這個cell注冊一個ReuseId焚挠。后續(xù)只要通過templateCaseType就可以取到對應的模板信息了。
- (void)addTemplate:(WXCellSlotComponent *)component
{
NSString *templateType = component.templateCaseType;
[_templateTypeMap setObject:component forKey:templateType];
if (_collectionView) {
[self _registerCellClassForReuseID:templateType];
}
}
WXRecycleListUpdateManager
這個類主要負責處理UICollectionView視圖的更新管理。根據暴露出的方法可以清晰地看到,拿到newData硅急、appendingData與oldData就可以去更新UICollectionView視圖中的Cell。
- (void)updateWithNewData:(NSArray *)newData
oldData:(NSArray *)oldData
completion:(WXRecycleListUpdateCompletion)completion
animation:(BOOL)isAnimated;
- (void)updateWithAppendingData:(NSArray *)appendingData
oldData:(NSArray *)oldData
completion:(WXRecycleListUpdateCompletion)completion
animation:(BOOL)isAnimated;
那么凤壁,newData和appendingData有什么不同?我們具體看下這個類的實現(xiàn)。核心代碼在方法-performBatchUpdates里,以下代碼對源碼做了部分精簡徒仓。
- (void)performBatchUpdates
{
NSArray *newData = [self.newerData copy];
NSArray *oldData = [self.olderData copy];
NSArray *appendingData = [self.appendingData copy];
WXDiffResult *diffResult;
if (appendingData) {
newData = [oldData arrayByAddingObjectsFromArray:appendingData];
NSIndexSet *inserts = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(oldData.count, appendingData.count)];
// 對于appendingData精簡一次diff操作
diffResult = [[WXDiffResult alloc] initWithInserts:inserts deletes:nil updates:nil];
} else if (newData){
diffResult = [WXDiffUtil diffWithMinimumDistance:newData oldArray:oldData];
}
// 計算UICollectionView需要delete彤枢、insert缴啡、reload的indexPahts
WXRecycleListDiffResult *recycleListDiffResult = [self recycleListUpdatesByDiffResult:diffResult];
if (![diffResult hasChanges] && self.reloadIndexPaths.count == 0) {
return;
}
void (^updates)(void) = [^{
// WXRecycleListUpdateDelegate
[self.delegate updateManager:self willUpdateData:newData];
// 具體更新UICollectionView
[self applyUpdateWithDiffResult:recycleListDiffResult];
} copy];
void (^completion)(BOOL) = [^(BOOL finished) {
// WXRecycleListUpdateDelegate
[self.delegate updateManager:self didUpdateData:newData withSuccess:finished];
} copy];
// 最后批處理
[collectionView performBatchUpdates:updates completion:completion];
}
我們可以看到WXRecycleListUpdateManager類的本質作用就是,當Vue傳過來新的數據碘裕,通過這個類來diff新舊數據,然后更新視圖文兢。如果是appdening的Data,則直接insert到最后面兼呵,就不用走diff的算法了维苔,這里對性能上做了一點優(yōu)化,如果單純從功能上來說的話潮尝,個人認為其實這2個方法沒有本質上的區(qū)別。
對于diff新舊數據的處理乱凿,Weex實現(xiàn)中用到了萊文斯坦距離算法(Levenshtein Distance)來比較新舊數據的編輯距離,計算出如何以最少操作次數來更新UICollectionView,具體算法實現(xiàn)中使用了包含動態(tài)規(guī)劃思想的全矩陣迭代法绷落,這里不做具體展開姥闪,可以閱讀WXDiffUtil類中的方法。
// Using the levenshtein algorithm
+ (WXDiffResult *)diffWithMinimumDistance:(NSArray<id<WXDiffable>> *)newArray oldArray:(NSArray<id<WXDiffable>> *)oldArray;
2個代理:LayoutDelegate砌烁、UpdateDelegate
WXRecycleListLayoutDelegate
WXRecycleListLayoutDelegate定義在WXRecycleListLayout類中筐喳,WXRecycleListLayout繼承于UICollectionViewFlowLayout,主要是管理UICollectionView的視圖布局函喉。WXRecycleListLayoutDelegate在UICollectionViewFlowLayout中的layoutAttributesForElementsInRect方法中觸發(fā)避归,如果實現(xiàn)了該代理,那么在布局該Cell時會固定(stick)在頂部函似。
@protocol WXRecycleListLayoutDelegate
- (BOOL)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout isNeedStickyForIndexPath:(NSIndexPath *)indexPath;
@end
@interface WXRecycleListLayout : UICollectionViewFlowLayout
@property (nonatomic, weak) id<WXRecycleListLayoutDelegate> delegate;
@end
WXRecycleListUpdateDelegate
WXRecycleListUpdateDelegate定義在WXRecycleListUpdateManager.h類中槐脏,提供2個方法。分別在UICollectionView更新視圖前后時機觸發(fā)回調撇寞。
@protocol WXRecycleListUpdateDelegate
// 在UICollectionView更新視圖前產生回調
- (void)updateManager:(WXRecycleListUpdateManager *)manager willUpdateData:(id)newData;
// 在UICollectionView更新視圖完成后產生回調
- (void)updateManager:(WXRecycleListUpdateManager *)manager didUpdateData:(id)newData withSuccess:(BOOL)finished;
@end
WXRecycleListComponent.m中的實現(xiàn)
經過上述Manager對象和一些Delegate的介紹顿天,我們已經將WXRecycleListComponent分解為多個小模塊堂氯,而主的WXRecycleListComponent.m類職責就是如何將上述這些小模塊組合在一起使用。
在WXRecycleListComponent.m類中牌废,除了完成一些必要的WXComponent初始化行為和Load More事件處理之外咽白,主要處理一些組件暴露給前端的一些export method。
WX_EXPORT_METHOD(@selector(appendData:))
WX_EXPORT_METHOD(@selector(appendRange:))
WX_EXPORT_METHOD(@selector(insertData:data:))
WX_EXPORT_METHOD(@selector(updateData:data:))
WX_EXPORT_METHOD(@selector(removeData:count:))
WX_EXPORT_METHOD(@selector(moveData:toIndex:))
WX_EXPORT_METHOD(@selector(scrollTo:options:))
WX_EXPORT_METHOD(@selector(insertRange:range:))
WX_EXPORT_METHOD(@selector(setListData:))
另外的一大部分的代碼是實現(xiàn)UICollectionViewDataSource和UICollectionViewDelegateFlowLayout鸟缕。其中晶框,比較重要的部分在[collectionView:cellForItemAtIndexPath:]方法中,如何根據data和template生成(獲榷印)一個可以復用的cell并且綁定數據和渲染授段。以下對此方法做了一些精簡。
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
// 1. get the data relating to the cell
id data = [_dataManager dataAtIndex:indexPath.row];
// 2. get the template type specified by data
NSString * templateType = [self templateType:indexPath];
_templateManager.collectionView = collectionView;
// 3. dequeue a cell component by template type
UICollectionViewCell *cellView = [_collectionView dequeueReusableCellWithReuseIdentifier:templateType forIndexPath:indexPath];
WXCellSlotComponent *cellComponent = (WXCellSlotComponent *)cellView.wx_component;
if (!cellComponent) {
cellComponent = [_templateManager dequeueCellSlotWithType:templateType forIndexPath:indexPath];
cellView.wx_component = cellComponent;
WXPerformBlockOnComponentThread(^{
[super _insertSubcomponent:cellComponent atIndex:self.subcomponents.count];
});
}
// 4. binding the data to the cell component
[self _updateBindingData:data forCell:cellComponent atIndexPath:indexPath];
// 5. Add cell component's view to content view.
UIView *contentView = cellComponent.view;
if (contentView.superview == cellView.contentView) {
return cellView;
}
for (UIView *view in cellView.contentView.subviews) {
[view removeFromSuperview];
}
[cellView.contentView addSubview:contentView];
[self handleAppear];
return cellView;
}
1番甩、獲取當前行Cell所對應的數據源對象侵贵,由于WXRecycleListComponent的數據管理都是由WXRecycleListDataManager來負責,所以這里很直接的根據index來拿到數據源即可缘薛。
2窍育、獲取當前行的cell所屬模板類型。
3宴胧、所以這里我們可以根據模塊的名字來獲取到cell的對象漱抓,由于UICollectionView注冊的Cell必須為UICollectionViewCell或其子類,而WXCellSlotComponent->WXComponent->NSObject恕齐,所以這里有一個動態(tài)綁定的過程乞娄。又因為Cell隨著頁面的滑動,是會被回收的檐迟,那么所綁定的Component也會被清理掉补胚。所以,這里同樣需要對模板類WXCellSlotComponent進行回收管理追迟,模板的注冊和獲取都是通過WXRecycleListTemplateManager對象來負責處理的。
WXCellSlotComponent *cellComponent = (WXCellSlotComponent *)cellView.wx_component;
if (!cellComponent) {
cellComponent = [_templateManager dequeueCellSlotWithType:templateType forIndexPath:indexPath];
cellView.wx_component = cellComponent;
WXPerformBlockOnComponentThread(^{
//TODO: How can we avoid this?
[super _insertSubcomponent:cellComponent atIndex:self.subcomponents.count];
});
}
4骚腥、如何將數據源應用到相應的WXCellSlotComponent上敦间,并以正確的樣式進行展示,這是整個流程最為關鍵和精彩的部分束铭。下一個小節(jié)會具體介紹下這個綁定流程廓块。
5、將WXCellSlotComponent視圖添加到Cell的content視圖中契沫,用于最終的顯示带猴。
由第5步我們可以看到,Cell真實的size就是Component中view的size懈万,而由于第3步的介紹我們知道Cell與Component是可以被回收的拴清,一旦回收后靶病,size就需要重新計算,而計算size本身是比較費時的口予。所以娄周,在UICollectionViewDelegateFlowLayout的
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath;
里,對計算出來的size根據indexPath進行cache沪停,進一步提升整體的性能煤辨。
數據解析與綁定
我們通過以下例子來講解數據解析和綁定的過程,也可以通過Weex中的例子來理解整個過程木张。recycle-list標簽的模板語法我們不進行展開介紹众辨,主要介紹整個流程在iOS中的實現(xiàn)。
<template>
<recycle-list for="(item, i) in labels" switch="type">
<cell-slot v-if=true case="label">
<text>{{i}}33333</text>
</cell-slot>
</recycle-list>
</template>
<script>
export default {
data () {
return {
labels: [// 數據源
{ type: 'label' },
],
}
}
}
</script>
我們回到UICollectionView數據源方法中的第4步舷礼,其方法經過精簡后的實現(xiàn)如下鹃彻,入參為數據源data、當前的Cell模板對象cellComponent以及位置索引indexPath且轨。
- (void)_updateBindingData:(id)data forCell:(WXCellSlotComponent *)cellComponent atIndexPath:(NSIndexPath *)indexPath
{
id originalData = data;
if (!data[@"indexPath"] || !data[@"recycleListComponentRef"]) {
NSMutableDictionary * dataNew = [data mutableCopy];
dataNew[@"recycleListComponentRef"] = self.ref;
dataNew[@"indexPath"] = indexPath;
data = dataNew;
}
if ([originalData isKindOfClass:[NSDictionary class]] && _aliasKey &&!data[@"phase"]) {
data = @{_aliasKey:data,@"aliasKey":_aliasKey};
}
if (_indexKey) {
NSMutableDictionary *dataNew = [data mutableCopy];
dataNew[_indexKey] = @(indexPath.item);
data = dataNew;
}
WXPerformBlockSyncOnComponentThread(^{
[cellComponent updateCellData:[data copy]];
});
}
傳入數據源后浮声,我們會對數據源進行一些包裝,附加上aliasKey旋奢、index泳挥、indexPath、recycleListComponentRef至朗、type等值屉符,這些值都是在渲染真正組件內容時不可或缺的一些信息。包裝后的對象如下锹引。
{
aliasKey = item;
i = 0;
item = {
indexPath = "<NSIndexPath: 0xc000000000000016> {length = 2, path = 0 - 0}";
recycleListComponentRef = "_root";
type = label;
};
}
通過這些包裝完成的數據矗钟,我們就能根據已經注入的模板語法獲取到這個Cell具體要顯示的條件、規(guī)則和值嫌变,然后完成數據綁定吨艇,最終完成布局、渲染和顯示腾啥。
- (void)updateCellData:(NSDictionary *)data
{
[self updateBindingData:data];
[self triggerLayout];
}
那么东涡,模板語法的注入與解析轉換是在什么時候完成的?
其實在WXComponentManager中通過_buildComponentForData方法生成Component的時候倘待,會根據一些條件判斷是否為模板組件疮跑,如果是,則進行數據解析與綁定的行為凸舵。
WXComponentConfig *config = [WXComponentFactory configWithComponentName:type];
BOOL isTemplate = [config.properties[@"isTemplate"] boolValue] || (supercomponent && supercomponent->_isTemplate);
if (isTemplate) {
bindingProps = [self _extractBindingProps:&attributes];
bindingStyles = [self _extractBindings:&styles];
bindingAttibutes = [self _extractBindings:&attributes];
bindingEvents = [self _extractBindingEvents:&events];
}
模板語法表達式的解析和綁定感覺是整個方案中比較難懂的部分祖娘,其主要實現(xiàn)在WXComponent+DataBinding這個擴展類中,主要工作就是是對前端與客戶端之間通信的模板語法進行一步一步地解析轉換啊奄,細節(jié)工作比較繁瑣渐苏,其中一部分語法和表達式的解析代碼用C++完成掀潮,主要的相關的實現(xiàn)在WXJSASTParser.mm文件中,有一定的閱讀理解門檻整以。然后就是將相應表達式所要對應的行為定于在WXDataBindingBlock這個block中胧辽,然后存儲在一個bingdingMap中,即完成了整個綁定操作公黑。
總結
整個長列表復用方案在長列表的展示上具有較大的性能突破邑商,整體方案的設計思路上與傳統(tǒng)Weex組件架構有較大不同,充分利用了Native組件的復用機制凡蚜。最后再對整個框架創(chuàng)新之處進行一個總結人断。
- 前端不對列表結構進行展開
iOS系統(tǒng)的Native組件實現(xiàn)本身就具備了很優(yōu)異的交互效果和內存控制能力,以UITableView和UICollectionView為例朝蜘,其本身有著非常好的滾動流暢度恶迈、內存控制能力、View復用能力谱醇,那么我們就要最大化的去利用這些Native的能力暇仲,而不是花很大的時間和精力再重新造一個輪子去模仿它,盡可能的利用Native提供的優(yōu)良特性才是Weex最大的優(yōu)勢和價值副渴。
- 前端與客戶端之間的模板語法
復用機制的實現(xiàn)必然需要一套復用的模板奈附,如何將Vue的業(yè)務代碼轉換成一套Native復用的模板也是一項比較大的挑戰(zhàn)。整套語法的制定和解析過程還是有很大的工作量煮剧,有無數的細節(jié)工作需要不斷的實現(xiàn)和完善斥滤。