weex高性能list解析

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的子组件只能是cellheaderrefreshloading以及固定位置的组件。

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

提供的功能虽然没有UITableView强大,但都是实际使用最需要的功能。上面list的demo使用到了refreshcell以及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的:

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

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

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

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

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

原理实现

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

weex高性能list解析

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

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

在WXSDKEngine的源码中,可以知道list其实对应的是WXListComponentcell对应的是WXCellComponentheader对应的是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,用到了很多reloadRowsAtIndexPathsinsertRowsAtIndexPathsdeleteRowsAtIndexPaths等类似的方法,每个cell、header等的出现会让tableview发生变化。

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

  1. 准备数据
  2. reload table

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

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

上一篇:智能资产配置特训班课程过半,这些内容关键点你不能错过


下一篇:tcp/ip源码(18)——struct inet_sock中的cork用途