weex高性能list解析

简介: [weex](https://alibaba.github.io/weex/doc/tutorial.html)是alibaba出品的用于移动端跨平台开发界面的框架,类似react-native。 而ListView在移动端界面的开发中是非常重要的组件,无论是H5还是react-native都因为ListView的低性能而饱受非议。那么到底是什么样的实现让weex能拥有与众不同的ListView

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原理

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

目录
相关文章
|
3天前
|
消息中间件 存储 缓存
高性能、高可靠性!Kafka的技术优势与应用场景全解析
**Kafka** 是一款高吞吐、高性能的消息系统,擅长日志收集、消息传递和用户活动跟踪。其优点包括:零拷贝技术提高传输效率,顺序读写优化磁盘性能,持久化保障数据安全,分布式架构支持扩展,以及客户端状态维护确保可靠性。在实际应用中,Kafka常用于日志聚合、解耦生产者与消费者,以及实时用户行为分析。
13 3
|
8天前
|
缓存 前端开发 JavaScript
【前端性能优化】深入解析重绘和回流,构建高性能Web界面
【前端性能优化】深入解析重绘和回流,构建高性能Web界面
18 1
|
11天前
|
存储 安全 Java
深入解析Java HashMap的高性能扩容机制与树化优化
深入解析Java HashMap的高性能扩容机制与树化优化
11 1
|
1月前
|
缓存 负载均衡 网络协议
使用Go语言开发高性能服务的深度解析
【5月更文挑战第21天】本文深入探讨了使用Go语言开发高性能服务的技巧,强调了Go的并发性能、内存管理和网络编程优势。关键点包括:1) 利用goroutine和channel进行并发处理,通过goroutine池优化资源;2) 注意内存管理,减少不必要的分配和释放,使用pprof分析;3) 使用非阻塞I/O和连接池提升网络性能,结合HTTP/2和负载均衡技术;4) 通过性能分析、代码优化、缓存和压缩等手段进一步提升服务性能。掌握这些技术能帮助开发者构建更高效稳定的服务。
|
1月前
|
SQL 运维 监控
面经:Presto/Trino高性能SQL查询引擎解析
【4月更文挑战第10天】本文深入探讨了大数据查询引擎Trino(现称Trino)的核心特性与应用场景,适合面试准备。重点包括:Trino的分布式架构(Coordinator与Worker节点)、连接器与数据源交互、查询优化(CBO、动态过滤)及性能调优、容错与运维实践。通过实例代码展示如何解释查询计划、创建自定义连接器以及查看查询的I/O预期。理解这些知识点将有助于在面试中脱颖而出,并在实际工作中高效处理数据分析任务。
94 12
|
1月前
|
负载均衡 Go 调度
使用Go语言构建高性能的Web服务器:协程与Channel的深度解析
在追求高性能Web服务的今天,Go语言以其强大的并发性能和简洁的语法赢得了开发者的青睐。本文将深入探讨Go语言在构建高性能Web服务器方面的应用,特别是协程(goroutine)和通道(channel)这两个核心概念。我们将通过示例代码,展示如何利用协程处理并发请求,并通过通道实现协程间的通信和同步,从而构建出高效、稳定的Web服务器。
|
1月前
|
消息中间件 存储 监控
解析RocketMQ:高性能分布式消息队列的原理与应用
RocketMQ是阿里开源的高性能分布式消息队列,具备低延迟、高吞吐和高可靠性,广泛应用于电商、金融等领域。其核心概念包括Topic、Producer、Consumer、Message和Name Server/Broker。RocketMQ支持异步通信、系统解耦、异步处理和流量削峰。关键特性有分布式架构、顺序消息、高可用性设计和消息事务。提供发布/订阅和点对点模型,以及消息过滤功能。通过集群模式、存储方式、发送和消费方式的选择进行性能优化。RocketMQ易于部署,可与Spring集成,并与Kafka等系统对比各有优势,拥有丰富的生态系统。
328 4
|
1月前
|
存储 安全 Java
深入解析Java List接口及其实现类
深入解析Java List接口及其实现类
|
1月前
|
Python
Python 基础知识:什么是 Python 的列表解析(List Comprehension)?
Python 基础知识:什么是 Python 的列表解析(List Comprehension)?
|
1月前
|
前端开发 网络协议 Dubbo
Netty - 回顾Netty高性能原理和框架架构解析
Netty - 回顾Netty高性能原理和框架架构解析
268 0

推荐镜像

更多