weex高性能list解析

    xiaoxiao2025-09-17  15

    weex是alibaba出品的用于移动端跨平台开发界面的框架,类似react-native。而ListView在移动端界面的开发中是非常重要的组件,无论是H5还是react-native都因为ListView的低性能而饱受非议。那么到底是什么样的实现让weex能拥有与众不同的ListView性能呢?

    List示例

    首先,让我们一起来看看weex下如何使用list。

    <template> <div> <list class="list"> <refresh class = "refresh-view" display="{{refresh_display}}" onrefresh="onrefresh"> <text if="{{(refresh_display==='hide')}}"> ↓ pull to refresh </text> <loading-indicator class="indicator"></loading-indicator> </refresh> <cell onappear="onappear" ondisappear="ondisappear" class="row" repeat="{{rows}}" index="{{$index}}"> <div class="item"> <text class="item-title">row {{id}}</text> </div> </cell> <loading class="loading-view" display="{{loading_display}}" onloading="onloading"> <text if="{{(loading_display==='hide')}}">↑ Loadmore </text> <loading-indicator class="indicator"></loading-indicator> </loading> </list> </div> </template>

    根据weex的文档,list的子组件只能是cell、header、refresh、loading以及固定位置的组件。

    cell:决定list中每个cell的样子header:当list滑到顶部的时候,会吸在顶部refresh:下拉刷新loading:上拉加载更多

    提供的功能虽然没有UITableView强大,但都是实际使用最需要的功能。上面list的demo使用到了refresh,cell以及loading子组件。

    <style> ... </style>

    list的样式并不在本文的分析范畴,所以这里就pass了。

    <script> module.exports = { methods: { onappear: function (e) { ... }, ondisappear:function (e) { ... }, onrefresh: function(e) { ... }, onloading: function() { ... }, }, data: { refresh_display: 'hide', loading_display: 'hide', appearMin:1, appearMax:1, appearIds:[], rows:[ {id: 1}, {id: 2}, {id: 3}, {id: 4}, {id: 5}, {id: 6}, {id: 7}, {id: 8}, {id: 9}, {id: 10}, {id: 11}, {id: 12}, {id: 13}, {id: 14}, {id: 15}, {id: 16}, {id: 17}, {id: 18}, {id: 19}, {id: 20}, {id: 21}, {id: 22}, {id: 23}, {id: 24}, {id: 25}, {id: 26}, {id: 27}, {id: 28}, {id: 29} ], moreRows: [ {id: 30}, {id: 31}, {id: 32}, {id: 33} ] } } </script>

    js部分定义了相关的回调,其中需要特别关注下的是repeat="{{rows}}",其根据rows提供的数据重复创建多个cell。

    list和UITableView的对比

    先来看下在iOS中我们是如何使用UITableView的:

    继承UITableViewCell,实现自定义的Cell样式。初始化UITableView,设置DataSource和Delegate。实现DataSource,主要是设置UITableView的Section数目,每个Section的Cell数目,以及每个Cell的样式。实现Delegate,主要是实现在操作UITableView时候的一些委托,比如tableView:didSelectRowAtIndexPath:等。

    相比之下,weex的就简单多了:

    实现cell样式。(对应于iOS自定义Cell的实现)按需实现refresh或者loading或者其他。(UITableView默认没有下拉刷新和加载更多,一般通过UIScrollView+SVPullToRefresh的扩展来实现)设置数据,实现回调。(对应于iOS实现DataSource和实现Delegate,不过显然功能弱一些)

    其实从这里我们应该能够推断出一点什么了。没错, weex的高性能list和其他框架不一样的地方就在于Cell的重用,也就是充分利用了UITableView或者RecycleView的重用机制实现了性能的优化。

    以上结论还只是猜测(虽然我们都知道这是必由之路),那我们就继续扒扒代码看个清楚。

    原理实现

    如上demo的三个文件会被weex编译成一个js文件,然后通过jsframework调用到native,盗用个图,大家或许可以明白一些。其实一点都不复杂,就是JSCore或者V8做了一个桥,让native能和js共享一个context而已。

    js通过桥告诉了Native现在有list组件,子组件有cell、refresh和loading。下面就直接扒native的代码看。

    weex中用两个概念,一个是模块(module),一个是组件(component),前者主要是功能的,例如存储,而后者主要是视图,比如div这样的。很显然list、cell等都是组件类别的。

    在WXSDKEngine的源码中,可以知道list其实对应的是WXListComponent,cell对应的是WXCellComponent,header对应的是WXHeaderComponent等等。

    // WXSDKEngine.m [self registerComponent:@"list" withClass:NSClassFromString(@"WXListComponent") withProperties:nil]; [self registerComponent:@"header" withClass:NSClassFromString(@"WXHeaderComponent")]; [self registerComponent:@"cell" withClass:NSClassFromString(@"WXCellComponent")]; [self registerComponent:@"loading" withClass:NSClassFromString(@"WXLoadingComponent")]; [self registerComponent:@"refresh" withClass:NSClassFromString(@"WXRefreshComponent")];

    WXComponent

    因为即将讨论的都是组件,那就必须要先了解下weex的组件系统。其实不论是weex也好,react-native也好,还是具备组件化能力的框架,都是有类似的组件系统的。

    那当我们在说组件系统的时候,我们到底在说什么呢?在weex中其实就是weex的组件基类 —— WXComponent。下面挑重点看看weex的组件系统都有哪些功能。

    // 组件的初始化函数: // + ref:每个实例化组件都是自己在jsContext中的唯一的标识 // + type:组件的类型,默认register的时候的名字就是type // + styles:css编译出来决定样式的字典 // + attributes:属性字典 // + events:事件系统 // + weexInstance:weex SDK全局只有一个实例,这里就是传入这个实例(真心为了性能不择手段) - (instancetype)initWithRef:(NSString *)ref type:(NSString*)type styles:(nullable NSDictionary *)styles attributes:(nullable NSDictionary *)attributes events:(nullable NSArray *)events weexInstance:(WXSDKInstance *)weexInstance; // 子组件 @property (nonatomic, readonly, strong, nullable) NSArray<WXComponent *> *subcomponents; // 父组件 @property (nonatomic, readonly, weak, nullable) WXComponent *supercomponent; // 通过flexbox计算之后的frame @property(nonatomic, readonly, assign) CGRect calculatedFrame; // 一堆生命周期函数 - (void)viewWillLoad; - (void)viewDidLoad; - (void)viewWillUnload; - (void)viewDidUnload; // 调整组件结构 - (void)insertSubview:(WXComponent *)subcomponent atIndex:(NSInteger)index; - (void)removeFromSuperview; - (void)moveToSuperview:(WXComponent *)newSupercomponent atIndex:(NSUInteger)index; // 事件相关 - (void)fireEvent:(NSString *)eventName params:(nullable NSDictionary *)params; - (void)fireEvent:(NSString *)eventName params:(nullable NSDictionary *)params domChanges:(nullable NSDictionary *)domChanges; - (void)addEvent:(NSString *)eventName; - (void)removeEvent:(NSString *)eventName; // 更新样式 - (void)updateStyles:(NSDictionary *)styles; // 更新属性 - (void)updateAttributes:(NSDictionary *)attributes;

    下面的这个代码比较能说明问题,UIView和CALayer都是和WXComponent一一对应的。这就是weex的组件系统和iOS的组件系统建立联系的地方。

    @interface UIView (WXComponent) @property (nonatomic, weak) WXComponent *wx_component; @property (nonatomic, weak) NSString *wx_ref; @end @interface CALayer (WXComponent) @property (nonatomic, weak) WXComponent *wx_component; @end

    之上说的只是weex组件系统的一部分,组件系统还有一个非常重要个功能是布局。在weex中,这一部分的功能是通过WXComponent+Layout来实现的,布局系统使用的是flexbox。列举几个主要是函数。

    // 布局计算完毕 - (void)_frameDidCalculated:(BOOL)isChanged; // 根据父类的绝对位置计算frame,如果frame改变的话,将自己加到dirtyComponents里面,进而通知 - (void)_calculateFrameWithSuperAbsolutePosition:(CGPoint)superAbsolutePosition gatherDirtyComponents:(NSMutableSet<WXComponent *> *)dirtyComponents; // 布局结束 - (void)_layoutDidFinish;

    WXCellComponent

    下面我们来看看Cell组件的实现。

    @interface WXCellComponent : WXComponent @property (nonatomic, strong) NSString *scope; @property (nonatomic, weak) WXListComponent *list; @end

    可以发现,每个cell组件都隶属于特定的list,文档中也是这么说的,cell必须是list的子组件。

    仔细查看WXCellComponent的实现可以发现,其是没有什么特别特殊的地方,其与其他组件最大的不同就是对应有list组件,其所有的回调都会相应的调用list的方法,更新list中对自己的状态。比如:

    - (void)_frameDidCalculated:(BOOL)isChanged { [super _frameDidCalculated:isChanged]; if (isChanged) { [self.list cellDidLayout:self]; } } - (void)_removeFromSupercomponent { [super _removeFromSupercomponent]; [self.list cellDidRemove:self]; }

    refresh、loading以及header等都是类似的组件,这里就不详述了,有兴趣的同学可以查看源码阅读。

    WXListComponent

    WXListComponent是本文的主角,放在最后出场也算是压轴了,首先来看一下头文件。非常简单,类似所有的ListView,都是继承自ScrollView,其还包括了一些针对cell操作的api,上面源码中的cellDidLayout就是在这里定义的。

    @interface WXListComponent : WXScrollerComponent - (void)cellDidRemove:(WXCellComponent *)cell; - (void)cellDidLayout:(WXCellComponent *)cell; - (void)headerDidLayout:(WXHeaderComponent *)header; - (void)cellDidRendered:(WXCellComponent *)cell; - (void)cell:(WXCellComponent *)cell didMoveToIndex:(NSUInteger)index; @end

    其实到这里我们已经知道cell、header、refresh、loading等都是如何根据js代码生成native组件的,现在,我们还不知道的是,list是怎么把他们拼在一起的。下面的代码就能说明这一切。

    从这里我们可以看到,ListComponent是依赖了tableview的。

    @implementation WXListComponent { __weak UITableView * _tableView; // Only accessed on component thread NSMutableArray<WXSection *> *_sections; // Only accessed on main thread NSMutableArray<WXSection *> *_completedSections; NSUInteger _previousLoadMoreRowNumber; }

    从这里我们可以看到,list、cell、header、loading以及fixed-component是如何通过组件系统联系起来的。

    - (void)_insertSubcomponent:(WXComponent *)subcomponent atIndex:(NSInteger)index { // 子组件如果是cell if ([subcomponent isKindOfClass:[WXCellComponent class]]) { ((WXCellComponent *)subcomponent).list = self; // 子组件如果是header } else if ([subcomponent isKindOfClass:[WXHeaderComponent class]]) { ((WXHeaderComponent *)subcomponent).list = self; // 除了上述两个,子组件只能是refresh 、loading或者fixed-component } else if (![subcomponent isKindOfClass:[WXRefreshComponent class]] && ![subcomponent isKindOfClass:[WXLoadingComponent class]] && subcomponent->_positionType != WXPositionTypeFixed) { WXLogError(@"list only support cell/header/refresh/loading/fixed-component as child."); return; } [super _insertSubcomponent:subcomponent atIndex:index]; // 构造section NSIndexPath *indexPath = [self indexPathForSubIndex:index]; if (_sections.count <= indexPath.section) { WXSection *section = [WXSection new]; if ([subcomponent isKindOfClass:[WXHeaderComponent class]]) { section.header = (WXHeaderComponent*)subcomponent; } //TODO: consider insert header at middle [_sections addObject:section]; NSUInteger index = [_sections indexOfObject:section]; // section的数目是有header和cell在template中出现的顺序决定的,具体可以查看函数`indexPathForSubIndex:` NSIndexSet *indexSet = [NSIndexSet indexSetWithIndex:index]; WXSection *completedSection = [section copy]; // 这里很重要,当你最终组合除了indexSet之后,调用_tableView的`insertSections`来更新tableView。 [self.weexInstance.componentManager _addUITask:^{ [_completedSections addObject:completedSection]; WXLogDebug(@"Insert section:%ld", (unsigned long)[_completedSections indexOfObject:completedSection]); [UIView performWithoutAnimation:^{ [_tableView insertSections:indexSet withRowAnimation:UITableViewRowAnimationNone]; }]; }]; } }

    再举另外一个例子。

    - (void)cellDidLayout:(WXCellComponent *)cell { WXAssertComponentThread() ; NSUInteger index = [self.subcomponents indexOfObject:cell]; NSIndexPath *indexPath = [self indexPathForSubIndex:index]; NSInteger sectionNum = indexPath.section; NSInteger row = indexPath.row; NSMutableArray *sections = _sections; WXSection *section = sections[sectionNum]; WXAssert(section, @"no section found for section number:%ld", sectionNum); NSMutableArray *completedSections; BOOL isReload = [section.rows containsObject:cell]; if (!isReload) { [section.rows insertObject:cell atIndex:row]; // deep copy completedSections = [[NSMutableArray alloc] initWithArray:sections copyItems:YES];; } // 和上面非常类似,如果不是reload的话,就直接调用tableview insert,否则的话就调用tableview reload。 [self.weexInstance.componentManager _addUITask:^{ if (!isReload) { WXLogDebug(@"Insert cell:%@ at indexPath:%@", cell.ref, indexPath); _completedSections = completedSections; [UIView performWithoutAnimation:^{ [_tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationNone]; }]; } else { WXLogInfo(@"Reload cell:%@ at indexPath:%@", cell.ref, indexPath); [UIView performWithoutAnimation:^{ [_tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationNone]; }]; } }]; }

    再来看一下TableView的DataSource,

    - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return _completedSections.count; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return ((WXSection *)[_completedSections wx_safeObjectAtIndex:section]).rows.count; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { WXLogDebug(@"Getting cell at indexPath:%@", indexPath); static NSString *reuseIdentifier = @"WXTableViewCell"; UITableViewCell *cellView = [_tableView dequeueReusableCellWithIdentifier:reuseIdentifier]; if (!cellView) { cellView = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:reuseIdentifier]; cellView.backgroundColor = [UIColor clearColor]; } else { } WXCellComponent *cell = [self cellForIndexPath:indexPath]; if (!cell) { return cellView; } if (cell.view.superview == cellView.contentView) { return cellView; } for (UIView *view in cellView.contentView.subviews) { [view removeFromSuperview]; } [cellView.contentView addSubview:cell.view]; WXLogDebug(@"Created cell:%@ view:%@ cellView:%@ at indexPath:%@", cell.ref, cell.view, cellView, indexPath); return cellView; }

    总结

    WeexSDK关于List的细节还非常多,但通过上面的分析,我们已经大致清楚了Weex是如何利用UITableView来实现重用Cell,提升性能的,稍微总结一下。

    规定语法,list组件的子组件只能是cell、header、refresh、loading已经fixed-component当指定cell、header、refresh、loading和fixed-component的时候,组件系统都会根据css计算出这些子组件的布局。cell和header是比较特殊的组件,他们持有list的引用,会在自身发生变化的时候调用list组件方法更新list状态,而且他们出现的顺序会决定最终tableview的section数目和每个section的row的数目。

    其实这里比较不一样的是weex会频繁的更新tableview,用到了很多reloadRowsAtIndexPaths、insertRowsAtIndexPaths、deleteRowsAtIndexPaths等类似的方法,每个cell、header等的出现会让tableview发生变化。

    再来看一下正常情况下我们使用UITableView。

    准备数据reload table

    对table的改变次数远远少于weex的方案,因此weex应该还有是不少改进的地方。

    在我的理解,tableview重用最核心的就是cell模板的复用,是不是可以想办法让js能定义cell模板,然后native就根据数据给的id来使用相应的模板来渲染list,从而避免了需要先渲染cell然后再来决定list的显示,期待weex牛逼的工程师们再给我们带来惊喜。

    最新回复(0)