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而已。

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

时间: 2024-09-19 02:23:28

weex高性能list解析的相关文章

实现高性能Java解析器

备注: 本篇文章是关于先前相同主题文章的最新版本.先前文章主要介绍创建高性能解析器的一些要点,但它吸收了读者的一部分批评建议.原来的文章进行了全面修订,并补充了相对完整的代码.我们希望你喜欢本次更新. 如果你没有指定数据或语言标准的或开源的Java解析器, 可能经常要用Java实现你自己的数据或语言解析器.或者,可能有很多解析器可选,但是要么太慢,要么太耗内存,或者没有你需要的特定功能.或者开源解析器存在缺陷,或者开源解析器项目被取消诸如此类原因.上述原因都没有你将需要实现你自己的解析器的事实重

高性能JavaScript解析

第一章 可响应的界面 总结 JavaScript和界面更新在同一个进程里进行,所以一次只能其中之一被执行.这意味着当JavaScript在执行时界面无法响应用户输入,反之亦然.要有效的操控UI线程,意味着不能让JavaScript花太长时间执行而影响了用户体验.由此,请记住以下规则: 1.每一个JavaScript任务都应该在100毫秒内执行完毕.长时间的执行会使界面不能即使更新,从而使用户得到负面的体验. 2.在JavaScript执行时,不同的浏览器对用户的交互表现出不同的行为.但不管怎样,

云栖Android精华文章合集

云栖Android精彩文章整理自各位技术大咖们关于Android的精彩分享,本文将云栖Android精彩文章整理成为一个合集,以便于大家学习参考.Weex.apk瘦身.开发资源.应用维护.内存管理,一切尽在云栖Android精华文章合集. 云课堂: Android平台页面路由框架ARouter最佳实践 聚能聊: Android_Studio_那些年你常用的神奇快捷键及遇到的糗事儿 文章干货: 安全: APP漏洞扫描器之未使用地址空间随机化 [安全攻防挑战]Androidapp远程控制实战 你必须

网易严选App感受Weex开发

自打出生的那一天起,WEEX就免不了被拿来同React Native"一决高下"的命运.React Native宣称「Learn Once, Write Anywhere」,而WEEX宣称「Write Once, Run Everywhere」.在我看来,并没有谁更好,只有谁更合适.下面我将围绕WEEX入门进行讲解. (如果你尚不了解React Native,并想简单入门,可以阅读[整理]ReactNative快速入门笔记) 网易严选App感受Weex开发 什么都不说,先给你感受下we

Weex Android 动画揭秘

背景 在目前常见的交互方式中,动画扮演了一个重要的角色. 在 Weex 框架下,Weex 的动画需要屏蔽 CSS/JS 动画与 Android 动画系统的差异,并尽可能的达到60FPS. 本文阐述了在 Android 上实现高性能CSS/JS动画过程中所遇到的问题/相关数学知识及解决方案.本文使用的前端 DSL 为 Weex vue 1.0或 Weex Vue 2.0. 现状与问题 在 Weex 环境下, 一个典型的动画在前端DSL中的写法如下: animation = weex.require

Python模拟新浪微博登录

看到一篇Python模拟新浪微博登录的文章,想熟悉一下其中实现方式,并且顺便掌握python相关知识点. 代码 下面的代码是来自上面这篇文章,并稍作修改添加了一些注释. # -*- coding: utf-8 -* import urllib2 import urllib import cookielib import lxml.html as HTML class Fetcher(object): def __init__(self, username=None, pwd=None, cook

专访QQ大数据团队,谈分布式计算系统开发

NoSQL是笔者最早接触大数据领域的相关知识,因此在大家都在畅谈Hadoop.Spark时,笔者仍然保留着NoSQL博文的阅读习惯.在偶尔阅读一篇Redis博文过程中,笔者发现了 jacksu的个人博客,并在其中发现了大量的分布式系统操作经验,从而通过他的引荐了解了QQ成立之初后台3个基础团队之一的QQ运营组,这里我们一起走进. QQ大数据团队 CSDN:首先,请介绍一下您的团队? 聂晶:我们团队是社交网络事业群/社交网络运营部/数据中心/平台开发二组,前身是QQ成立之初后台3个基础团队之一的Q

SD-WAN和IP网络演进探讨

今天的SD-WAN 对于绝大部分的企业而言,IT系统已经成为了企业运营中的一个关键基础设施,这其中网络部分Internet的接入.企业分支/合作伙伴之间的VPN连接是IT化基础设施中重要的部件.而MPLS VPN专线接入价格高昂,对于大部分企业是笔不菲的支出;同时对于运行关键业务的企业总部或重要站点,往往需要连接到多个运营商或采用多种接入方式以提供网络冗余的保护,进一步增加了WAN接入的成本.科技进步的实质就是降低成本,使得组织和个人单纯的需要和欲望变成可以支付得起的需求,从而释放购买力,创造新

核高基移动智能终端操作系统研发课题公布

新浪科技讯 8月9日凌晨消息,工信部昨日下发核高基2012年"移动智能终端操作系统研发"专题申报通知,组织企事业单位及科研单位申报相关课题,并最多给予1000万元中央财政支持. 核高基<2012年"移动智能终端操作系统研发"专题申报指南>显示,可以申请的课题有两项,分别为:面向移动智能终端的芯片IP核设计:面向移动互联网的Web中间件研发及应用. 其中"面向移动智能终端的芯片IP核设计"课题资金资助方式为前补助,中央财政支持资金不多于