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牛逼的工程师们再给我们带来惊喜。