flutter好用的轮子推荐二-点赞按钮动画

前言

Flutter是谷歌的移动UI框架,可以快速在iOS和Android上构建高质量的原生用户界面。

IT界著名的尼古拉斯·高尔包曾说:轮子是IT进步的阶梯!热门的框架千篇一律,好用轮子万里挑一!Flutter作为这两年开始崛起的跨平台开发框架,其第三方生态相比其他成熟框架还略有不足,但轮子的数量也已经很多了。本系列文章挑选日常app开发常用的轮子分享出来,给大家提高搬砖效率,同时也希望flutter的生态越来越完善,轮子越来越多。

本系列文章准备了超过50个轮子推荐,工作原因,尽量每1-2天出一篇文章。

tip:本系列文章合适已有部分flutter基础的开发者,入门请戳:flutter官网

正文

轮子

  • 轮子名称:like_button
  • 轮子概述:推特点赞效果带数量滚动动画
  • 轮子作者:zmtzawqlp
  • 推荐指数:★★★★
  • 常用指数:★★★★
  • 效果预览:

安装

dependencies:
  like_button: ^0.1.9
import 'package:like_button/like_button.dart';

用法介绍

用法很简单,就一个widget:

LikeButton()

带数字:

LikeButton(likeCount:520)

自定义图标:

LikeButton(
    likeBuilder: (bool isLiked){
        return Icon(Icons.person);
    },
)

自定义图标+数字:

LikeButton(
    likeBuilder: (bool isLiked){
        return Icon(Icons.person);
    },
    likeCount:520
)

自定义图标+自定义泡泡颜色+数字:

LikeButton(
    likeBuilder: (bool isLiked){
        return Icon(Icons.person,color: isLiked ? Colors.blue : Colors.grey,);
    },
    likeCount:520,
    circleColor:CircleColor(start: Color(0xff00ddff), end: Color(0xff0099cc)),
    bubblesColor: BubblesColor(
        dotPrimaryColor: Color(0xff33b5e5),
        dotSecondaryColor: Color(0xff0099cc),
    ),
)

自定义图标+自定义泡泡颜色+数字修饰:

LikeButton(
    likeBuilder: (bool isLiked){
        return Icon(Icons.person,color: isLiked ? Colors.blue : Colors.grey,);
    },
    likeCount:520,
    circleColor:CircleColor(start: Color(0xff00ddff), end: Color(0xff0099cc)),
    bubblesColor: BubblesColor(
        dotPrimaryColor: Color(0xff33b5e5),
        dotSecondaryColor: Color(0xff0099cc),
    ),
    countBuilder: (int count, bool isLiked, String text) {
        var color = isLiked?Colors.red:Colors.grey;
        Widget result;
        result = Text(
            text,
            style: TextStyle(color: color,fontSize: 20),
        );
        return result;
    },
    countDecoration:(Widget count){
        return Row(
            mainAxisAlignment: MainAxisAlignment.center,
            crossAxisAlignment: CrossAxisAlignment.center,
            children: <Widget>[
                count,
                SizedBox(
                    width: 10.0,
                ),
                Text(
                    "loves",
                    style: TextStyle(color: Colors.indigoAccent),
                )
            ],
        );
    }
)

请求时改变状态

LikeButton(
    onTap: (bool isLiked) 
    {
        return onLikeButtonTap(isLiked, item);
    }
)

这是一个异步回调,你可以等待服务返回之后再改变状态。也可以先改变状态,请求失败之后重置回状态

Future<bool> onLikeButtonTap(bool isLiked, TuChongItem item) {
    ///send your request here
    ///
    final Completer<bool> completer = new Completer<bool>();
    Timer(const Duration(milliseconds: 200), () {
        item.isFavorite = !item.isFavorite;
        item.favorites =
            item.isFavorite ? item.favorites + 1 : item.favorites - 1;

        // if your request is failed,return null,
        completer.complete(item.isFavorite);
    });
    return completer.future;
}

详细参数

结尾

flutter好用的轮子推荐一:列表动画

前言

Flutter是谷歌的移动UI框架,可以快速在iOS和Android上构建高质量的原生用户界面。

IT界著名的尼古拉斯·高尔包曾说:轮子是IT进步的阶梯!热门的框架千篇一律,好用轮子万里挑一!Flutter作为这两年开始崛起的跨平台开发框架,其第三方生态相比其他成熟框架还略有不足,但轮子的数量也已经很多了。本系列文章挑选日常app开发常用的轮子分享出来,给大家提高搬砖效率,同时也希望flutter的生态越来越完善,轮子越来越多。

本系列文章准备了超过50个轮子推荐,工作原因,尽量每1-2天出一篇文章。

tip:本系列文章合适已有部分flutter基础的开发者,入门请戳:flutter官网

正文

轮子

  • 轮子名称:flutter_staggered_animations
  • 轮子概述:轻松的将交错的动画添加到您ListView,GridView,Column和Row
  • 推荐指数:★★★★
  • 常用指数:★★★
  • 效果预览:

安装

flutter_staggered_animations: "^0.1.2"
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';

用法介绍

flutter_staggered_animations提供三个类: – Animation – AnimationConfiguration – AnimationLimiter

以及四个默认动画类型: – FadeInAnimation 渐隐渐现动画 – SlideAnimation 滑动动画 – ScaleAnimation 缩放动画 – FlipAnimation 翻转动画 (动画可嵌套成组合动画)

在ListView中使用

动画外部由AnimationLimiter组件包裹,ListView的子项由AnimationConfiguration.staggeredList来创建:

int count=20;

@override
Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
            title: Text("ListView"),
        ),
        body: AnimationLimiter(
            child:ListView.builder(
                itemCount: count,
                itemBuilder: (context,index){
                    return AnimationConfiguration.staggeredList(
                        position: index,
                        duration: const Duration(milliseconds: 375),
                        child: SlideAnimation( //滑动动画
                            verticalOffset: 50.0,
                            child: FadeInAnimation( //渐隐渐现动画
                                child: Container(
                                    margin: EdgeInsets.all(5),
                                    color: Theme.of(context).primaryColor,
                                    height: 60,
                                ),
                            ),
                        ),
                    );
                },
            ),
        ),

    );
}

在GridView中使用

动画外部由AnimationLimiter组件包裹,GridView的子项由AnimationConfiguration.staggeredGrid来创建:

int count=20;

@override
Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
            title: Text("GridView"),
        ),
        body: 
        AnimationLimiter(
            child:GridView.builder(
                itemCount: count,
                gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                    //横轴元素个数
                    crossAxisCount: 3,
                    //纵轴间距
                    mainAxisSpacing: 10.0,
                    //横轴间距
                    crossAxisSpacing: 10.0,
                    //子组件宽高长度比例
                    childAspectRatio: 1.0
                ),
                itemBuilder: (context,index){
                    return AnimationConfiguration.staggeredGrid(
                        columnCount:count,
                        position: index,
                        duration: const Duration(milliseconds: 375),
                        child: SlideAnimation(
                            verticalOffset: 50.0,
                            child: FadeInAnimation(
                                child: Container(
                                    color: Theme.of(context).primaryColor,
                                ),
                            ),
                        ),
                    );
                },
            ),
        ),

    );
}

在Column中使用

动画外部由AnimationLimiter组件包裹,Column的子项由AnimationConfiguration.toStaggeredList来创建:

int count=10;

@override
Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
            title: Text("Column"),
        ),
        body: 
        AnimationLimiter(
            child:Column(
                children:AnimationConfiguration.toStaggeredList(
                    duration: const Duration(milliseconds: 375),
                    childAnimationBuilder:(widget) => SlideAnimation(
                        verticalOffset: 50.0,
                        child: FadeInAnimation(
                            child: widget,
                        ),
                    ),
                    children: ChildrenList()
                )
            ),
        ),

    );
}

List<Widget> ChildrenList(){
    List<Widget> childs=[];
    for (var i = 0; i < count; i++) {
        childs.add(Container(
            margin: EdgeInsets.all(5),
            color: Theme.of(context).primaryColor,
            height: 60,
        ));
    }
    return childs;
}

在Row中使用

动画外部由AnimationLimiter组件包裹,Row的子项由AnimationConfiguration.toStaggeredList来创建:

int count=5;

@override
Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
            title: Text("Row"),
        ),
        body: 
        AnimationLimiter(
            child:Container(
                height: 60,
                child: Row(
                    children:AnimationConfiguration.toStaggeredList(
                        duration: const Duration(milliseconds: 375),
                        childAnimationBuilder:(widget) => SlideAnimation(
                            verticalOffset: 50.0,
                            child: FadeInAnimation(
                                child: widget,
                            ),
                        ),
                        children: ChildrenList()
                    )
                ),
            ),
        ),

    );
}

List<Widget> ChildrenList(){
    List<Widget> childs=[];
    for (var i = 0; i < count; i++) {
        childs.add(Container(
            margin: EdgeInsets.only(right:5),
            color: Theme.of(context).primaryColor,
            width: 60,
        ));
    }
    return childs;
}

结尾

flutter好用的轮子推荐七-flutter圆形或线型进度条

前言

Flutter是谷歌的移动UI框架,可以快速在iOS和Android上构建高质量的原生用户界面。

IT界著名的尼古拉斯·高尔包曾说:轮子是IT进步的阶梯!热门的框架千篇一律,好用轮子万里挑一!Flutter作为这两年开始崛起的跨平台开发框架,其第三方生态相比其他成熟框架还略有不足,但轮子的数量也已经很多了。本系列文章挑选日常app开发常用的轮子分享出来,给大家提高搬砖效率,同时也希望flutter的生态越来越完善,轮子越来越多。

本系列文章准备了超过50个轮子推荐,工作原因,尽量每1-2天出一篇文章。

tip:本系列文章合适已有部分flutter基础的开发者,入门请戳:flutter官网

正文

轮子

  • 轮子名称:percent_indicator
  • 轮子概述:flutter一个圆形和线形的进度条.
  • 轮子作者:diego.velasquez.lopez@gmail.com
  • 推荐指数:★★★★
  • 常用指数:★★★
  • 效果预览:

功能概述

  • 圆形百分比进度
  • 线形百分比进度
  • 切换动画
  • 自定义动画的持续时间
  • 基于百分比值的进度
  • 进度和背景色
  • 自定义大小
  • 左,右或居中子项用于线性百分比指示器
  • 圆形百分比指示器的顶部,底部或中心widget
  • 使用渐变色

安装

dependencies:
  percent_indicator: ^2.1.1+1
import 'package:percent_indicator/percent_indicator.dart';

使用

圆形进度

基础使用

new CircularPercentIndicator(
    radius: 60.0, //大小
    lineWidth: 5.0,//指示线条大小
    percent: 1.0,//当前进度
    center: new Text("100%"),//中心widget 可以是文字 或其他widget 如何icon
    progressColor: Colors.green, //进度条颜色
    ....
)
头部标题+icon内容+背景色:
```dart
new CircularPercentIndicator(
    radius: 100.0,
    lineWidth: 10.0,
    percent: 0.8,
    header: new Text("Icon header"),
    center: new Icon(
        Icons.person_pin,
        size: 50.0,
        color: Colors.blue,
    ),
    backgroundColor: Colors.grey,
    progressColor: Colors.blue,
)

头部标题+动画:

new CircularPercentIndicator(
    radius: 130.0,
    animation: true,
    animationDuration: 1200,
    lineWidth: 15.0,
    percent: 0.4,
    center: new Text("40 hours",style:new TextStyle(fontWeight: FontWeight.bold, fontSize: 20.0),),
    circularStrokeCap: CircularStrokeCap.butt,
    backgroundColor: Colors.yellow,
    progressColor: Colors.red,
),

底部文案+动画+圆角截断:

new CircularPercentIndicator(
    radius: 120.0,
    lineWidth: 13.0,
    animation: true,
    percent: 0.7,
    center: new Text(
    "70.0%",
    style:
        new TextStyle(fontWeight: FontWeight.bold, fontSize: 20.0),
    ),
    footer: new Text(
    "Sales this week",
    style:
        new TextStyle(fontWeight: FontWeight.bold, fontSize: 17.0),
    ),
    circularStrokeCap: CircularStrokeCap.round,
    progressColor: Colors.purple,
)

线型进度

基础使用:

new LinearPercentIndicator(
    width: 300,
    lineHeight: 14.0,
    percent: 0.5,
    backgroundColor: Colors.grey,
    progressColor: Colors.blue,
),

线型进度+进度数字:

new LinearPercentIndicator(
    width: 300,
    lineHeight: 14.0,
    percent: 0.5,
    center: Text(
    "50.0%",
    style: new TextStyle(fontSize: 12.0),
    ),
    trailing: Icon(Icons.mood),
    linearStrokeCap: LinearStrokeCap.roundAll,
    backgroundColor: Colors.grey,
    progressColor: Colors.blue,
)

线型进度+进度数字+左右内容+动画+矩形边框:

new LinearPercentIndicator(
    width: 200,
    animation: true,
    animationDuration: 1000,
    lineHeight: 20.0,
    leading: new Text("左侧内容"),
    trailing: new Text("右侧内容"),
    percent: percent,
    center: Text((percent*100).toString()+'%'),
    linearStrokeCap: LinearStrokeCap.butt,
    progressColor: Colors.red,
)

更多用法可自行参考轮子文档中的参数定制。

Flutter的原理及美团的实践

导读

Flutter是Google开发的一套全新的跨平台、开源UI框架,支持iOS、Android系统开发,并且是未来新操作系统Fuchsia的默认开发套件。自从2017年5月发布第一个版本以来,目前Flutter已经发布了近60个版本,并且在2018年5月发布了第一个“Ready for Production Apps”的Beta 3版本,6月20日发布了第一个“Release Preview”版本。

初识Flutter

Flutter的目标是使同一套代码同时运行在Android和iOS系统上,并且拥有媲美原生应用的性能,Flutter甚至提供了两套控件来适配Android和iOS(滚动效果、字体和控件图标等等),为了让App在细节处看起来更像原生应用。

在Flutter诞生之前,已经有许多跨平台UI框架的方案,比如基于WebView的Cordova、AppCan等,还有使用HTML+JavaScript渲染成原生控件的React Native、Weex等。

基于WebView的框架优点很明显,它们几乎可以完全继承现代Web开发的所有成果(丰富得多的控件库、满足各种需求的页面框架、完全的动态化、自动化测试工具等等),当然也包括Web开发人员,不需要太多的学习和迁移成本就可以开发一个App。同时WebView框架也有一个致命(在对体验&性能有较高要求的情况下)的缺点,那就是WebView的渲染效率和JavaScript执行性能太差。再加上Android各个系统版本和设备厂商的定制,很难保证所在所有设备上都能提供一致的体验。

为了解决WebView性能差的问题,以React Native为代表的一类框架将最终渲染工作交还给了系统,虽然同样使用类HTML+JS的UI构建逻辑,但是最终会生成对应的自定义原生控件,以充分利用原生控件相对于WebView的较高的绘制效率。与此同时这种策略也将框架本身和App开发者绑在了系统的控件系统上,不仅框架本身需要处理大量平台相关的逻辑,随着系统版本变化和API的变化,开发者可能也需要处理不同平台的差异,甚至有些特性只能在部分平台上实现,这样框架的跨平台特性就会大打折扣。

Flutter则开辟了一种全新的思路,从头到尾重写一套跨平台的UI框架,包括UI控件、渲染逻辑甚至开发语言。渲染引擎依靠跨平台的Skia图形库来实现,依赖系统的只有图形绘制相关的接口,可以在最大程度上保证不同平台、不同设备的体验一致性,逻辑处理使用支持AOT的Dart语言,执行效率也比JavaScript高得多。

Flutter同时支持Windows、Linux和macOS操作系统作为开发环境,并且在Android Studio和VS Code两个IDE上都提供了全功能的支持。Flutter所使用的Dart语言同时支持AOT和JIT运行方式,JIT模式下还有一个备受欢迎的开发利器“热刷新”(Hot Reload),即在Android Studio中编辑Dart代码后,只需要点击保存或者“Hot Reload”按钮,就可以立即更新到正在运行的设备上,不需要重新编译App,甚至不需要重启App,立即就可以看到更新后的样式。

在Flutter中,所有功能都可以通过组合多个Widget来实现,包括对齐方式、按行排列、按列排列、网格排列甚至事件处理等等。Flutter控件主要分为两大类,StatelessWidget和StatefulWidget,StatelessWidget用来展示静态的文本或者图片,如果控件需要根据外部数据或者用户操作来改变的话,就需要使用StatefulWidget。State的概念也是来源于Facebook的流行Web框架React,React风格的框架中使用控件树和各自的状态来构建界面,当某个控件的状态发生变化时由框架负责对比前后状态差异并且采取最小代价来更新渲染结果。

Hot Reload

在Dart代码文件中修改字符串“Hello, World”,添加一个惊叹号,点击保存或者热刷新按钮就可以立即更新到界面上,仅需几百毫秒:

Flutter通过将新的代码注入到正在运行的DartVM中,来实现Hot Reload这种神奇的效果,在DartVM将程序中的类结构更新完成后,Flutter会立即重建整个控件树,从而更新界面。但是热刷新也有一些限制,并不是所有的代码改动都可以通过热刷新来更新:

  1. 编译错误,如果修改后的Dart代码无法通过编译,Flutter会在控制台报错,这时需要修改对应的代码。
  2. 控件类型从StatelessWidgetStatefulWidget的转换,因为Flutter在执行热刷新时会保留程序原来的state,而某个控件从stageless→stateful后会导致Flutter重新创建控件时报错“myWidget is not a subtype of StatelessWidget”,而从stateful→stateless会报错“type ‘myWidget’ is not a subtype of type ‘StatefulWidget’ of ‘newWidget’”。
  3. 全局变量和静态成员变量,这些变量不会在热刷新时更新。
  4. 修改了main函数中创建的根控件节点,Flutter在热刷新后只会根据原来的根节点重新创建控件树,不会修改根节点。
  5. 某个类从普通类型转换成枚举类型,或者类型的泛型参数列表变化,都会使热刷新失败。

热刷新无法实现更新时,执行一次热重启(Hot Restart)就可以全量更新所有代码,同样不需要重启App,区别是restart会将所有Dart代码打包同步到设备上,并且所有状态都会重置。

Flutter插件

Flutter使用的Dart语言无法直接调用Android系统提供的Java接口,这时就需要使用插件来实现中转。Flutter官方提供了丰富的原生接口封装:

在Flutter中,依赖包由Pub仓库管理,项目依赖配置在pubspec.yaml文件中声明即可(类似于NPM的版本声明Pub Versioning Philosophy),对于未发布在Pub仓库的插件可以使用git仓库地址或文件路径:

dependencies: 
  url_launcher">=0.1.2 <0.2.0"
  collection"^0.1.2"
  plugin1: 
    git: 
      url"git://github.com/flutter/plugin1.git"
  plugin2: 
    path: ../plugin2/

以shared_preferences为例,在pubspec中添加代码:

dependencies:
  flutter:
    sdk: flutter

  shared_preferences"^0.4.1"

脱字号“^”开头的版本表示和当前版本接口保持兼容的最新版,^1.2.3 等效于 >=1.2.3 <2.0.0而 ^0.1.2 等效于 >=0.1.2 <0.2.0,添加依赖后点击“Packages get”按钮即可下载插件到本地,在代码中添加import语句就可以使用插件提供的接口:

import 'package:shared_preferences/shared_preferences.Dart';

class _MyAppState extends State<MyAppCounter{
  int _count = 0;
  static const String COUNTER_KEY = 'counter';

  _MyAppState() {
    init();
  }

  init() async {
    var pref = await SharedPreferences.getInstance();
    _count = pref.getInt(COUNTER_KEY) ?? 0;
    setState(() {});
  }

  increaseCounter() async {
    SharedPreferences pref = await SharedPreferences.getInstance();
    pref.setInt(COUNTER_KEY, ++_count);
    setState(() {});
  }
...

Dart

Dart是一种强类型、跨平台的客户端开发语言。具有专门为客户端优化、高生产力、快速高效、可移植(兼容ARM/x86)、易学的OO编程风格和原生支持响应式编程(Stream & Future)等优秀特性。Dart主要由Google负责开发和维护,在2011年10启动项目,2017年9月发布第一个2.0-dev版本。

Dart本身提供了三种运行方式:

  1. 使用Dart2js编译成JavaScript代码,运行在常规浏览器中(Dart Web)。
  2. 使用DartVM直接在命令行中运行Dart代码(DartVM)。
  3. AOT方式编译成机器码,例如Flutter App框架(Flutter)。

Flutter在筛选了20多种语言后,最终选择Dart作为开发语言主要有几个原因:

  1. 健全的类型系统,同时支持静态类型检查和运行时类型检查。
  2. 代码体积优化(Tree Shaking),编译时只保留运行时需要调用的代码(不允许反射这样的隐式引用),所以庞大的Widgets库不会造成发布体积过大。
  3. 丰富的底层库,Dart自身提供了非常多的库。
  4. 多生代无锁垃圾回收器,专门为UI框架中常见的大量Widgets对象创建和销毁优化。
  5. 跨平台,iOS和Android共用一套代码。
  6. JIT & AOT运行模式,支持开发时的快速迭代和正式发布后最大程度发挥硬件性能。

在Dart中,有一些重要的基本概念需要了解:

  • 所有变量的值都是对象,也就是类的实例。甚至数字、函数和null也都是对象,都继承自Object类。
  • 虽然Dart是强类型语言,但是显式变量类型声明是可选的,Dart支持类型推断。如果不想使用类型推断,可以用dynamic类型。
  • Dart支持泛型,List表示包含int类型的列表,List则表示包含任意类型的列表。
  • Dart支持顶层(top-level)函数和类成员函数,也支持嵌套函数和本地函数。
  • Dart支持顶层变量和类成员变量。
  • Dart没有public、protected和private这些关键字,使用下划线“_”开头的变量或者函数,表示只在库内可见。参考库和可见性

DartVM的内存分配策略非常简单,创建对象时只需要在现有堆上移动指针,内存增长始终是线形的,省去了查找可用内存段的过程:

Dart中类似线程的概念叫做Isolate,每个Isolate之间是无法共享内存的,所以这种分配策略可以让Dart实现无锁的快速分配。

Dart的垃圾回收也采用了多生代算法,新生代在回收内存时采用了“半空间”算法,触发垃圾回收时Dart会将当前半空间中的“活跃”对象拷贝到备用空间,然后整体释放当前空间的所有内存:

整个过程中Dart只需要操作少量的“活跃”对象,大量的没有引用的“死亡”对象则被忽略,这种算法也非常适合Flutter框架中大量Widget重建的场景。

Flutter Framework

Flutter的框架部分完全使用Dart语言实现,并且有着清晰的分层架构。分层架构使得我们可以在调用Flutter提供的便捷开发功能(预定义的一套高质量Material控件)之外,还可以直接调用甚至修改每一层实现(因为整个框架都属于“用户空间”的代码),这给我们提供了最大程度的自定义能力。Framework底层是Flutter引擎,引擎主要负责图形绘制(Skia)、文字排版(libtxt)和提供Dart运行时,引擎全部使用C++实现,Framework层使我们可以用Dart语言调用引擎的强大能力。

分层架构

Framework的最底层叫做Foundation,其中定义的大都是非常基础的、提供给其他所有层使用的工具类和方法。绘制库(Painting)封装了Flutter Engine提供的绘制接口,主要是为了在绘制控件等固定样式的图形时提供更直观、更方便的接口,比如绘制缩放后的位图、绘制文本、插值生成阴影以及在盒子周围绘制边框等等。

Animation是动画相关的类,提供了类似Android系统的ValueAnimator的功能,并且提供了丰富的内置插值器。Gesture提供了手势识别相关的功能,包括触摸事件类定义和多种内置的手势识别器。GestureBinding类是Flutter中处理手势的抽象服务类,继承自BindingBase类。

Binding系列的类在Flutter中充当着类似于Android中的SystemService系列(ActivityManager、PackageManager)功能,每个Binding类都提供一个服务的单例对象,App最顶层的Binding会包含所有相关的Bingding抽象类。如果使用Flutter提供的控件进行开发,则需要使用WidgetsFlutterBinding,如果不使用Flutter提供的任何控件,而直接调用Render层,则需要使用RenderingFlutterBinding。

Flutter本身支持Android和iOS两个平台,除了性能和开发语言上的“native”化之外,它还提供了两套设计语言的控件实现Material & Cupertino,可以帮助App更好地在不同平台上提供原生的用户体验。

渲染库(Rendering)

Flutter的控件树在实际显示时会转换成对应的渲染对象(RenderObject)树来实现布局和绘制操作。一般情况下,我们只会在调试布局,或者需要使用自定义控件来实现某些特殊效果的时候,才需要考虑渲染对象树的细节。渲染库主要提供的功能类有:

abstract class RendererBinding extends BindingBase with ServicesBindingSchedulerBindingHitTestable { ... }
abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin implements HitTestTarget {
abstract class RenderBox extends RenderObject { ... }
class RenderParagraph extends RenderBox { ... }
class RenderImage extends RenderBox { ... }
class RenderFlex extends RenderBox with ContainerRenderObjectMixin<RenderBoxFlexParentData>,
                                        RenderBoxContainerDefaultsMixin<RenderBoxFlexParentData>,
                                        DebugOverflowIndicatorMixin { ... }

RendererBinding是渲染树和Flutter引擎的胶水层,负责管理帧重绘、窗口尺寸和渲染相关参数变化的监听。RenderObject渲染树中所有节点的基类,定义了布局、绘制和合成相关的接口。RenderBox和其三个常用的子类RenderParagraphRenderImageRenderFlex则是具体布局和绘制逻辑的实现类。

在Flutter界面渲染过程分为三个阶段:布局、绘制、合成,布局和绘制在Flutter框架中完成,合成则交由引擎负责:

控件树中的每个控件通过实现RenderObjectWidget#createRenderObject(BuildContext context) → RenderObject方法来创建对应的不同类型的RenderObject对象,组成渲染对象树。因为Flutter极大地简化了布局的逻辑,所以整个布局过程中只需要深度遍历一次:

渲染对象树中的每个对象都会在布局过程中接受父对象的Constraints参数,决定自己的大小,然后父对象就可以按照自己的逻辑决定各个子对象的位置,完成布局过程。

子对象不存储自己在容器中的位置,所以在它的位置发生改变时并不需要重新布局或者绘制。子对象的位置信息存储在它自己的parentData字段中,但是该字段由它的父对象负责维护,自身并不关心该字段的内容。同时也因为这种简单的布局逻辑,Flutter可以在某些节点设置布局边界(Relayout boundary),即当边界内的任何对象发生重新布局时,不会影响边界外的对象,反之亦然:

布局完成后,渲染对象树中的每个节点都有了明确的尺寸和位置,Flutter会把所有对象绘制到不同的图层上:

因为绘制节点时也是深度遍历,可以看到第二个节点在绘制它的背景和前景不得不绘制在不同的图层上,因为第四个节点切换了图层(因为“4”节点是一个需要独占一个图层的内容,比如视频),而第六个节点也一起绘制到了红色图层。这样会导致第二个节点的前景(也就是“5”)部分需要重绘时,和它在逻辑上毫不相干但是处于同一图层的第六个节点也必须重绘。为了避免这种情况,Flutter提供了另外一个“重绘边界”的概念:

在进入和走出重绘边界时,Flutter会强制切换新的图层,这样就可以避免边界内外的互相影响。典型的应用场景就是ScrollView,当滚动内容重绘时,一般情况下其他内容是不需要重绘的。虽然重绘边界可以在任何节点手动设置,但是一般不需要我们来实现,Flutter提供的控件默认会在需要设置的地方自动设置。

控件库(Widgets)

Flutter的控件库提供了非常丰富的控件,包括最基本的文本、图片、容器、输入框和动画等等。在Flutter中“一切皆是控件”,通过组合、嵌套不同类型的控件,就可以构建出任意功能、任意复杂度的界面。它包含的最主要的几个类有:

class WidgetsFlutterBinding extends BindingBase with GestureBindingServicesBindingSchedulerBinding,
            PaintingBindingRendererBindingWidgetsBinding { ... }
abstract class Widget extends DiagnosticableTree { ... }
abstract class StatelessWidget extends Widget { ... }
abstract class StatefulWidget extends Widget { ... }
abstract class RenderObjectWidget extends Widget { ... }
abstract class Element extends DiagnosticableTree implements BuildContext { ... }
class StatelessElement extends ComponentElement { ... }
class StatefulElement extends ComponentElement { ... }
abstract class RenderObjectElement extends Element { ... }
...

基于Flutter控件系统开发的程序都需要使用WidgetsFlutterBinding,它是Flutter的控件框架和Flutter引擎的胶水层。Widget就是所有控件的基类,它本身所有的属性都是只读的。RenderObjectWidget所有的实现类则负责提供配置信息并创建具体的RenderObjectElementElement是Flutter用来分离控件树和真正的渲染对象的中间层,控件用来描述对应的element属性,控件重建后可能会复用同一个element。RenderObjectElement持有真正负责布局、绘制和碰撞测试(hit test)的RenderObject对象。

StatelessWidgetStatefulWidget并不会直接影响RenderObject创建,只负责创建对应的RenderObjectWidgetStatelessElementStatefulElement也是类似的功能。

它们之间的关系如下图:

如果控件的属性发生了变化(因为控件的属性是只读的,所以变化也就意味着重新创建了新的控件树),但是其树上每个节点的类型没有变化时,element树和render树可以完全重用原来的对象(因为element和render object的属性都是可变的):

但是,如果控件树种某个节点的类型发生了变化,则element树和render树中的对应节点也需要重新创建:

外卖全品类页面实践

在调研了Flutter的各项特性和实现原理之后,外卖计划灰度上线Flutter版的全品类页面。对于将Flutter页面作为App的一部分这种集成模式,官方并没有提供完善的支持,所以我们首先需要了解Flutter是如何编译、打包并且运行起来的。

Flutter App构建过程

最简单的Flutter工程至少包含两个文件:

运行Flutter程序时需要对应平台的宿主工程,在Android上Flutter通过自动创建一个Gradle项目来生成宿主,在项目目录下执行flutter create .,Flutter会创建ios和android两个目录,分别构建对应平台的宿主项目,Android目录内容如下:

此Gradle项目中只有一个app module,构建产物即是宿主APK。Flutter在本地运行时默认采用Debug模式,在项目目录执行flutter run即可安装到设备中并自动运行,Debug模式下Flutter使用JIT方式来执行Dart代码,所有的Dart代码都会打包到APK文件中assets目录下,由libflutter.so中提供的DartVM读取并执行:

kernel_blob.bin是Flutter引擎的底层接口和Dart语言基本功能部分代码:

third_party/dart/runtime/bin/*.dart
third_party/dart/runtime/lib/*.dart
third_party/dart/sdk/lib/_http/*.dart
third_party/dart/sdk/lib/async/*.dart
third_party/dart/sdk/lib/collection/*.dart
third_party/dart/sdk/lib/convert/*.dart
third_party/dart/sdk/lib/core/*.dart
third_party/dart/sdk/lib/developer/*.dart
third_party/dart/sdk/lib/html/*.dart
third_party/dart/sdk/lib/internal/*.dart
third_party/dart/sdk/lib/io/*.dart
third_party/dart/sdk/lib/isolate/*.dart
third_party/dart/sdk/lib/math/*.dart
third_party/dart/sdk/lib/mirrors/*.dart
third_party/dart/sdk/lib/profiler/*.dart
third_party/dart/sdk/lib/typed_data/*.dart
third_party/dart/sdk/lib/vmservice/*.dart
flutter/lib/ui/*.dart

platform.dill则是实现了页面逻辑的代码,也包括Flutter Framework和其他由pub依赖的库代码:

flutter_tutorial_2/lib/main.dart
flutter/packages/flutter/lib/src/widgets/*.dart
flutter/packages/flutter/lib/src/services/*.dart
flutter/packages/flutter/lib/src/semantics/*.dart
flutter/packages/flutter/lib/src/scheduler/*.dart
flutter/packages/flutter/lib/src/rendering/*.dart
flutter/packages/flutter/lib/src/physics/*.dart
flutter/packages/flutter/lib/src/painting/*.dart
flutter/packages/flutter/lib/src/gestures/*.dart
flutter/packages/flutter/lib/src/foundation/*.dart
flutter/packages/flutter/lib/src/animation/*.dart
.pub-cache/hosted/pub.flutter-io.cn/collection-1.14.6/lib/*.dart
.pub-cache/hosted/pub.flutter-io.cn/meta-1.1.5/lib/*.dart
.pub-cache/hosted/pub.flutter-io.cn/shared_preferences-0.4.2/*.dart

kernel_blob.bin和platform.dill都是由flutter_tools中的bundle.dart中调用KernelCompiler生成。

在Release模式(flutter run --release)下,Flutter会使用Dart的AOT运行模式,编译时将Dart代码转换成ARM指令:

kernel_blob.bin和platform.dill都不在打包后的APK中,取代其功能的是(isolate/vm)_snapshot_(data/instr)四个文件。snapshot文件由Flutter SDK中的flutter/bin/cache/artifacts/engine/android-arm-release/darwin-x64/gen_snapshot命令生成,vm_snapshot_*是Dart虚拟机运行所需要的数据和代码指令,isolate_snapshot_*则是每个isolate运行所需要的数据和代码指令。

Flutter App运行机制

Flutter构建出的APK在运行时会将所有assets目录下的资源文件解压到App私有文件目录中的flutter目录下,主要包括处理字符编码的icudtl.dat,还有Debug模式的kernel_blob.bin、platform.dill和Release模式下的4个snapshot文件。默认情况下Flutter在Application#onCreate时调用FlutterMain#startInitialization来启动解压任务,然后在FlutterActivityDelegate#onCreate中调用FlutterMain#ensureInitializationComplete来等待解压任务结束。

Flutter在Debug模式下使用JIT执行方式,主要是为了支持广受欢迎的热刷新功能:

触发热刷新时Flutter会检测发生改变的Dart文件,将其同步到App私有缓存目录下,DartVM加载并且修改对应的类或者方法,重建控件树后立即可以在设备上看到效果。

在Release模式下Flutter会直接将snapshot文件映射到内存中执行其中的指令:

在Release模式下,FlutterActivityDelegate#onCreate中调用FlutterMain#ensureInitializationComplete方法中会将AndroidManifest中设置的snapshot(没有设置则使用上面提到的默认值)文件名等运行参数设置到对应的C++同名类对象中,构造FlutterNativeView实例时调用nativeAttach来初始化DartVM,运行编译好的Dart代码。

打包Android Library

了解Flutter项目的构建和运行机制后,我们就可以按照其需求打包成AAR然后集成到现有原生App中了。首先在andorid/app/build.gradle中修改:

简单修改后我们就可以使用Android Studio或者Gradle命令行工具将Flutter代码打包到aar中了。Flutter运行时所需要的资源都会包含在aar中,将其发布到maven服务器或者本地maven仓库后,就可以在原生App项目中引用。

但这只是集成的第一步,为了让Flutter页面无缝衔接到外卖App中,我们需要做的还有很多。

图片资源复用

Flutter默认将所有的图片资源文件打包到assets目录下,但是我们并不是用Flutter开发全新的页面,图片资源原来都会按照Android的规范放在各个drawable目录,即使是全新的页面也会有很多图片资源复用的场景,所以在assets目录下新增图片资源并不合适。

Flutter官方并没有提供直接调用drawable目录下的图片资源的途径,毕竟drawable这类文件的处理会涉及大量的Android平台相关的逻辑(屏幕密度、系统版本、语言等等),assets目录文件的读取操作也在引擎内部使用C++实现,在Dart层面实现读取drawable文件的功能比较困难。Flutter在处理assets目录中的文件时也支持添加多倍率的图片资源,并能够在使用时自动选择,但是Flutter要求每个图片必须提供1x图,然后才会识别到对应的其他倍率目录下的图片:

flutter:
  assets:
    - images/cat.png
    - images/2x/cat.png
    - images/3.5x/cat.png
new Image.asset('images/cat.png');

这样配置后,才能正确地在不同分辨率的设备上使用对应密度的图片。但是为了减小APK包体积我们的位图资源一般只提供常用的2x分辨率,其他分辨率的设备会在运行时自动缩放到对应大小。针对这种特殊的情况,我们在不增加包体积的前提下,同样提供了和原生App一样的能力:

  1. 在调用Flutter页面之前将指定的图片资源按照设备屏幕密度缩放,并存储在App私有目录下。
  2. Flutter中使用时通过自定义的WMImage控件来加载,实际是通过转换成FileImage并自动设置scale为devicePixelRatio来加载。

这样就可以同时解决APK包大小和图片资源缺失1x图的问题。

Flutter和原生代码的通信

我们只用Flutter实现了一个页面,现有的大量逻辑都是用Java实现,在运行时会有许多场景必须使用原生应用中的逻辑和功能,例如网络请求,我们统一的网络库会在每个网络请求中添加许多通用参数,也会负责成功率等指标的监控,还有异常上报,我们需要在捕获到关键异常时将其堆栈和环境信息上报到服务器。这些功能不太可能立即使用Dart实现一套出来,所以我们需要使用Dart提供的Platform Channel功能来实现Dart→Java之间的互相调用。

以网络请求为例,我们在Dart中定义一个MethodChannel对象:

import 'dart:async';
import 'package:flutter/services.dart';
const MethodChannel _channel = const MethodChannel('com.sankuai.waimai/network');
Future<Map<Stringdynamic>> post(String path, [Map<Stringdynamic> form]) async {
  return _channel.invokeMethod("post", {'path': path, 'body': form}).then((result) {
    return new Map<Stringdynamic>.from(result);
  }).catchError((_) => null);
}

然后在Java端实现相同名称的MethodChannel:

public class FlutterNetworkPlugin implements MethodChannel.MethodCallHandler {

    private static final String CHANNEL_NAME = "com.sankuai.waimai/network";

    @Override
    public void onMethodCall(MethodCall methodCall, final MethodChannel.Result result) {
        switch (methodCall.method) {
            case "post":
                RetrofitManager.performRequest(post((String) methodCall.argument("path"), (Map) methodCall.argument("body")),
                        new DefaultSubscriber<Map>() {
                            @Override
                            public void onError(Throwable e) {
                                result.error(e.getClass().getCanonicalName(), e.getMessage(), null);
                            }

                            @Override
                            public void onNext(Map stringBaseResponse) {
                                result.success(stringBaseResponse);
                            }
                        }, tag);
                break;

            default:
                result.notImplemented();
                break;
        }
    }
}

在Flutter页面中注册后,调用post方法就可以调用对应的Java实现:

loadData: (callback) async {
    Map<String, dynamicdata = await post("home/groups");
    if (data == null) {
      callback(false);
      return;
    }
    _data = AllCategoryResponse.fromJson(data);
    if (_data == null || _data.code != 0) {
      callback(false);
      return;
    }
    callback(true);
  }),

SO库兼容性

Flutter官方只提供了四种CPU架构的SO库:armeabi-v7a、arm64-v8a、x86和x86-64,其中x86系列只支持Debug模式,但是外卖使用的大量SDK都只提供了armeabi架构的库。

虽然我们可以通过修改引擎src根目录和third_party/dart目录下build/config/arm.gnithird_party/skia目录下的BUILD.gn等配置文件来编译出armeabi版本的Flutter引擎,但是实际上市面上绝大部分设备都已经支持armeabi-v7a,其提供的硬件加速浮点运算指令可以大大提高Flutter的运行速度,在灰度阶段我们可以主动屏蔽掉不支持armeabi-v7a的设备,直接使用armeabi-v7a版本的引擎。

做到这点我们首先需要修改Flutter提供的引擎,在Flutter安装目录下的bin/cache/artifacts/engine下有Flutter下载的所有平台的引擎:

我们只需要修改android-arm、android-arm-profile和android-arm-release下的flutter.jar,将其中的lib/armeabi-v7a/libflutter.so移动到lib/armeabi/libflutter.so即可:

cd $FLUTTER_ROOT/bin/cache/artifacts/engine
for arch in android-arm android-arm-profile android-arm-release; do
  pushd $arch
  cp flutter.jar flutter-armeabi-v7a.jar # 备份
  unzip flutter.jar lib/armeabi-v7a/libflutter.so
  mv lib/armeabi-v7a lib/armeabi
  zip -d flutter.jar lib/armeabi-v7a/libflutter.so
  zip flutter.jar lib/armeabi/libflutter.so
  popd
done

这样在打包后Flutter的SO库就会打到APK的lib/armeabi目录中。在运行时如果设备不支持armeabi-v7a可能会崩溃,所以我们需要主动识别并屏蔽掉这类设备,在Android上判断设备是否支持armeabi-v7a也很简单:

public static boolean isARMv7Compatible() {
    try {
        if (SDK_INT >= LOLLIPOP) {
            for (String abi : Build.SUPPORTED_32_BIT_ABIS) {
                if (abi.equals("armeabi-v7a")) {
                    return true;
                }
            }
        } else {
            if (CPU_ABI.equals("armeabi-v7a") || CPU_ABI.equals("arm64-v8a")) {
                return true;
            }
        }
    } catch (Throwable e) {
        L.wtf(e);
    }
    return false;
}

灰度和自动降级策略

Horn是一个美团内部的跨平台配置下发SDK,使用Horn可以很方便地指定灰度开关:

在条件配置页面定义一系列条件,然后在参数配置页面添加新的字段flutter即可:

因为在客户端做了ABI兜底策略,所以这里定义的ABI规则并没有启用。

Flutter目前仍然处于Beta阶段,灰度过程中难免发生崩溃现象,观察到崩溃后再针对机型或者设备ID来做降级虽然可以尽量降低影响,但是我们可以做到更迅速。外卖的Crash采集SDK同时也支持JNI Crash的收集,我们专门为Flutter注册了崩溃监听器,一旦采集到Flutter相关的JNI Crash就立即停止该设备的Flutter功能,启动Flutter之前会先判断FLUTTER_NATIVE_CRASH_FLAG文件是否存在,如果存在则表示该设备发生过Flutter相关的崩溃,很有可能是不兼容导致的问题,当前版本周期内在该设备上就不再使用Flutter功能。

除了崩溃以外,Flutter页面中的Dart代码也可能发生异常,例如服务器下发数据格式错误导致解析失败等等,Dart也提供了全局的异常捕获功能:

import 'package:wm_app/plugins/wm_metrics.dart';

void main() {
  runZoned(() => runApp(WaimaiApp()), onError: (Object obj, StackTrace stack) {
    uploadException("$obj\n$stack");
  });
}

这样我们就可以实现全方位的异常监控和完善的降级策略,最大程度减少灰度时可能对用户带来的影响。

分析崩溃堆栈和异常数据

Flutter的引擎部分全部使用C/C++实现,为了减少包大小,所有的SO库在发布时都会去除符号表信息。和其他的JNI崩溃堆栈一样,我们上报的堆栈信息中只能看到内存地址偏移量等信息:

*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
Build fingerprint: 'Rock/odin/odin:7.1.1/NMF26F/1527007828:user/dev-keys'
Revision: '0'
Author: collect by 'libunwind'
ABI: 'arm64-v8a'
pid: 28937, tid: 29314, name: 1.ui  >>> com.sankuai.meituan.takeoutnew <<<
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0

backtrace:
    r0 00000000  r1 ffffffff  r2 c0e7cb2c  r3 c15affcc
    r4 c15aff88  r5 c0e7cb2c  r6 c15aff90  r7 bf567800
    r8 c0e7cc58  r9 00000000  sl c15aff0c  fp 00000001
    ip 80000000  sp c0e7cb28  lr c11a03f9  pc c1254088  cpsr 200c0030
    #00 pc 002d7088  /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so
    #01 pc 002d5a23  /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so
    #02 pc 002d95b5  /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so
    #03 pc 002d9f33  /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so
    #04 pc 00068e6d  /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so
    #05 pc 00067da5  /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so
    #06 pc 00067d5f  /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so
    #07 pc 003b1877  /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so
    #08 pc 003b1db5  /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so
    #09 pc 0000241c  /data/data/com.sankuai.meituan.takeoutnew/app_flutter/vm_snapshot_instr

单纯这些信息很难定位问题,所以我们需要使用NDK提供的ndk-stack来解析出具体的代码位置:

ndk-stack -sym PATH [-dump PATH]
Symbolizes the stack trace from an Android native crash.
  -sym PATH   sets the root directory for symbols
  -dump PATH  sets the file containing the crash dump (default stdin)

如果使用了定制过的引擎,必须使用engine/src/out/android-release下编译出的libflutter.so文件。一般情况下我们使用的是官方版本的引擎,可以在flutter_infra页面直接下载带有符号表的SO文件,根据打包时使用的Flutter工具版本下载对应的文件即可。比如0.4.4 beta版本:

$ flutter --version # version命令可以看到Engine对应的版本 06afdfe54e
Flutter 0.4.4 • channel beta • https://github.com/flutter/flutter.git
Framework • revision f9bb4289e9 (5 weeks ago) • 2018-05-11 21:44:54 -0700
Engine • revision 06afdfe54e
Tools • Dart 2.0.0-dev.54.0.flutter-46ab040e58
$ cat flutter/bin/internal/engine.version # flutter安装目录下的engine.version文件也可以看到完整的版本信息 06afdfe54ebef9168a90ca00a6721c2d36e6aafa
06afdfe54ebef9168a90ca00a6721c2d36e6aafa

拿到引擎版本号后在https://console.cloud.google.com/storage/browser/flutter_infra/flutter/06afdfe54ebef9168a90ca00a6721c2d36e6aafa/ 看到该版本对应的所有构建产物,下载android-arm-release、android-arm64-release和android-x86目录下的symbols.zip,并存放到对应目录:

执行ndk-stack即可看到实际发生崩溃的代码和具体行数信息:

ndk-stack -sym flutter-production-syms/06afdfe54ebef9168a90ca00a6721c2d36e6aafa/armeabi-v7a -dump flutter_jni_crash.txt 
********** Crash dump: **********
Build fingerprint: 'Rock/odin/odin:7.1.1/NMF26F/1527007828:user/dev-keys'
pid: 28937, tid: 29314, name: 1.ui  >>> com.sankuai.meituan.takeoutnew <<<
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0
Stack frame #00 pc 002d7088  /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so: Routine minikin::WordBreaker::setText(unsigned short const*, unsigned int) at /b/build/slave/Linux_Engine/build/src/out/android_release/../../flutter/third_party/txt/src/minikin/WordBreaker.cpp:55
Stack frame #01 pc 002d5a23  /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so: Routine minikin::LineBreaker::setText() at /b/build/slave/Linux_Engine/build/src/out/android_release/../../flutter/third_party/txt/src/minikin/LineBreaker.cpp:74
Stack frame #02 pc 002d95b5  /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so: Routine txt::Paragraph::ComputeLineBreaks() at /b/build/slave/Linux_Engine/build/src/out/android_release/../../flutter/third_party/txt/src/txt/paragraph.cc:273
Stack frame #03 pc 002d9f33  /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so: Routine txt::Paragraph::Layout(double, bool) at /b/build/slave/Linux_Engine/build/src/out/android_release/../../flutter/third_party/txt/src/txt/paragraph.cc:428
Stack frame #04 pc 00068e6d  /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so: Routine blink::ParagraphImplTxt::layout(double) at /b/build/slave/Linux_Engine/build/src/out/android_release/../../flutter/lib/ui/text/paragraph_impl_txt.cc:54
Stack frame #05 pc 00067da5  /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so: Routine tonic::DartDispatcher<tonic::IndicesHolder<0u>, void (blink::Paragraph::*)(double)>::Dispatch(void (blink::Paragraph::*)(double)) at /b/build/slave/Linux_Engine/build/src/out/android_release/../../topaz/lib/tonic/dart_args.h:150
Stack frame #06 pc 00067d5f  /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so: Routine void tonic::DartCall<void (blink::Paragraph::*)(double)>(void (blink::Paragraph::*)(double), _Dart_NativeArguments*) at /b/build/slave/Linux_Engine/build/src/out/android_release/../../topaz/lib/tonic/dart_args.h:198
Stack frame #07 pc 003b1877  /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so: Routine dart::NativeEntry::AutoScopeNativeCallWrapperNoStackCheck(_Dart_NativeArguments*, void (*)(_Dart_NativeArguments*)) at /b/build/slave/Linux_Engine/build/src/out/android_release/../../third_party/dart/runtime/vm/native_entry.cc:198
Stack frame #08 pc 003b1db5  /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so: Routine dart::NativeEntry::LinkNativeCall(_Dart_NativeArguments*) at /b/build/slave/Linux_Engine/build/src/out/android_release/../../third_party/dart/runtime/vm/native_entry.cc:348
Stack frame #09 pc 0000241c  /data/data/com.sankuai.meituan.takeoutnew/app_flutter/vm_snapshot_instr

Dart异常则比较简单,默认情况下Dart代码在编译成机器码时并没有去除符号表信息,所以Dart的异常堆栈本身就可以标识真实发生异常的代码文件和行数信息:

FlutterException: type '_InternalLinkedHashMap<dynamic, dynamic>' is not a subtype of type 'num' in type cast
#0      _$CategoryGroupFromJson (package:wm_app/lib/all_category/model/category_model.g.dart:29)
#1      new CategoryGroup.fromJson (package:wm_app/all_category/model/category_model.dart:51)
#2      _$CategoryListDataFromJson.<anonymous closure> (package:wm_app/lib/all_category/model/category_model.g.dart:5)
#3      MappedListIterable.elementAt (dart:_internal/iterable.dart:414)
#4      ListIterable.toList (dart:_internal/iterable.dart:219)
#5      _$CategoryListDataFromJson (package:wm_app/lib/all_category/model/category_model.g.dart:6)
#6      new CategoryListData.fromJson (package:wm_app/all_category/model/category_model.dart:19)
#7      _$AllCategoryResponseFromJson (package:wm_app/lib/all_category/model/category_model.g.dart:19)
#8      new AllCategoryResponse.fromJson (package:wm_app/all_category/model/category_model.dart:29)
#9      AllCategoryPage.build.<anonymous closure> (package:wm_app/all_category/category_page.dart:46)
<asynchronous suspension>
#10     _WaimaiLoadingState.build (package:wm_app/all_category/widgets/progressive_loading_page.dart:51)
#11     StatefulElement.build (package:flutter/src/widgets/framework.dart:3730)
#12     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:3642)
#13     Element.rebuild (package:flutter/src/widgets/framework.dart:3495)
#14     BuildOwner.buildScope (package:flutter/src/widgets/framework.dart:2242)
#15     _WidgetsFlutterBinding&BindingBase&GestureBinding&ServicesBinding&SchedulerBinding&PaintingBinding&RendererBinding&WidgetsBinding.drawFrame (package:flutter/src/widgets/binding.dart:626)
#16     _WidgetsFlutterBinding&BindingBase&GestureBinding&ServicesBinding&SchedulerBinding&PaintingBinding&RendererBinding._handlePersistentFrameCallback (package:flutter/src/rendering/binding.dart:208)
#17     _WidgetsFlutterBinding&BindingBase&GestureBinding&ServicesBinding&SchedulerBinding._invokeFrameCallback (package:flutter/src/scheduler/binding.dart:990)
#18     _WidgetsFlutterBinding&BindingBase&GestureBinding&ServicesBinding&SchedulerBinding.handleDrawFrame (package:flutter/src/scheduler/binding.dart:930)
#19     _WidgetsFlutterBinding&BindingBase&GestureBinding&ServicesBinding&SchedulerBinding._handleDrawFrame (package:flutter/src/scheduler/binding.dart:842)
#20     _rootRun (dart:async/zone.dart:1126)
#21     _CustomZone.run (dart:async/zone.dart:1023)
#22     _CustomZone.runGuarded (dart:async/zone.dart:925)
#23     _invoke (dart:ui/hooks.dart:122)
#24     _drawFrame (dart:ui/hooks.dart:109)

Flutter和原生性能对比

虽然使用原生实现(左)和Flutter实现(右)的全品类页面在实际使用过程中几乎分辨不出来:

但是我们还需要在性能方面有一个比较明确的数据对比。

我们最关心的两个页面性能指标就是页面加载时间和页面渲染速度。测试页面加载速度可以直接使用美团内部的Metrics性能测试工具,我们将页面Activity对象创建作为页面加载的开始时间,页面API数据返回作为页面加载结束时间。

从两个实现的页面分别启动400多次的数据中可以看到,原生实现(AllCategoryActivity)的加载时间中位数为210ms,Flutter实现(FlutterCategoryActivity)的加载时间中位数为231ms。考虑到目前我们还没有针对FlutterView做缓存和重用,FlutterView每次创建都需要初始化整个Flutter环境并加载相关代码,多出的20ms还在预期范围内:

因为Flutter的UI逻辑和绘制代码都不在主线程执行,Metrics原有的FPS功能无法统计到Flutter页面的真实情况,我们需要用特殊方法来对比两种实现的渲染效率。Android原生实现的界面渲染耗时使用系统提供的FrameMetrics接口进行监控:

public class AllCategoryActivity extends WmBaseActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            getWindow().addOnFrameMetricsAvailableListener(new Window.OnFrameMetricsAvailableListener() {
                List<Integer> frameDurations = new ArrayList<>(100);
                @Override
                public void onFrameMetricsAvailable(Window window, FrameMetrics frameMetrics, int dropCountSinceLastInvocation) {
                    frameDurations.add((int) (frameMetrics.getMetric(TOTAL_DURATION) / 1000000));
                    if (frameDurations.size() == 100) {
                        getWindow().removeOnFrameMetricsAvailableListener(this);
                        L.w("AllCategory", Arrays.toString(frameDurations.toArray()));
                    }
                }
            }, new Handler(Looper.getMainLooper()));
        }
        super.onCreate(savedInstanceState);
        // ...
    }
}

Flutter在Framework层只能取到每帧中UI操作的CPU耗时,GPU操作在Flutter引擎内部实现,所以要修改引擎来监控完整的渲染耗时,在Flutter引擎目录下src/flutter/shell/common/rasterizer.cc文件中添加:

void Rasterizer::DoDraw(std::unique_ptr<flow::LayerTree> layer_tree) {
  if (!layer_tree || !surface_) {
    return;
  }

  if (DrawToSurface(*layer_tree)) {
    last_layer_tree_ = std::move(layer_tree);
#if defined(OS_ANDROID)
    if (compositor_context_->frame_count().count() == 101) {
      std::ostringstream os;
      os << "[";
      const std::vector<TimeDelta> &engine_laps = compositor_context_->engine_time().Laps();
      const std::vector<TimeDelta> &frame_laps = compositor_context_->frame_time().Laps();
      size_t i = 1;
      for (auto engine_iter = engine_laps.begin() + 1, frame_iter = frame_laps.begin() + 1;
           i < 101 && engine_iter != engine_laps.end(); i++, engine_iter++, frame_iter++) {
        os << (*engine_iter + *frame_iter).ToMilliseconds() << ",";
      }
      os << "]";
      __android_log_write(ANDROID_LOG_WARN, "AllCategory", os.str().c_str());
    }
#endif
  }
}

即可得到每帧绘制时真正消耗的时间。测试时我们将两种实现的页面分别打开100次,每次打开后执行两次滚动操作,使其绘制100帧,将这100帧的每帧耗时记录下来:

for (( i = 0; i < 100; i++ )); do
    openWMPage allcategory
    sleep
    adb shell input swipe 500 1000 500 300 900
    adb shell input swipe 500 1000 500 300 900
    adb shell input keyevent 4
done

将测试结果的100次启动中每帧耗时取平均値,得到每帧平均耗时情况(横坐标轴为帧序列,纵坐标轴为每帧耗时,单位为毫秒):

Android原生实现和Flutter版本都会在页面打开的前5帧超过16ms,刚打开页面时原生实现需要创建大量View,Flutter也需要创建大量Widget,后续帧中可以重用大部分控件和渲染节点(原生的RenderNode和Flutter的RenderObject),所以启动时的布局和渲染操作都是最耗时的。

10000帧(100次×100帧每次)中Android原生总平均値为10.21ms,Flutter总平均値为12.28ms,Android原生实现总丢帧数851帧8.51%,Flutter总丢帧987帧9.87%。在原生实现的触摸事件处理和过度绘制充分优化的前提下,Flutter完全可以媲美原生的性能。

总结

Flutter目前仍处于早期阶段,也还没有发布正式的Release版本,不过我们看到Flutter团队一直在为这一目标而努力。虽然Flutter的开发生态不如Android和iOS原生应用那么成熟,许多常用的复杂控件还需要自己实现,有的甚至会比较困难(比如官方尚未提供的ListView.scrollTo(index)功能),但是在高性能和跨平台方面Flutter在众多UI框架中还是有很大优势的。

开发Flutter应用只能使用Dart语言,Dart本身既有静态语言的特性,也支持动态语言的部分特性,对于Java和JavaScript开发者来说门槛都不高,3-5天可以快速上手,大约1-2周可以熟练掌握。

在开发全品类页面的Flutter版本时我们也深刻体会到了Dart语言的魅力,Dart的语言特性使得Flutter的界面构建过程也比Android原生的XML+JAVA更直观,代码量也从原来的900多行减少到500多行(排除掉引用的公共组件)。Flutter页面集成到App后APK体积至少会增加5.5MB,其中包括3.3MB的SO库文件和2.2MB的ICU数据文件,此外业务代码1300行编译产物的大小有2MB左右。

Flutter本身的特性适合追求iOS和Android跨平台的一致体验,追求高性能的UI交互效果的场景,不适合追求动态化部署的场景。Flutter在Android上已经可以实现动态化部署,但是由于Apple的限制,在iOS上实现动态化部署非常困难,Flutter团队也正在和Apple积极沟通。

一文秒懂Flutter跨平台演进及架构

一、移动跨平台技术演进

1. 引言

移动互联网发展十余年,伴随着 Android、iOS 等智能手机的不断普及,移动端已逐步取代 PC 端,成为兵家必争之地。正所谓“得移动端者得天下”,移动端已成为互联网领域最大的流量分发入口,一大批互联网公司正是在这大趋势下崛起。

2. 为什么需要跨平台技术

伴随着移动互联网的高速发展,公司间竞争越来越激烈,如何将好想法快速落地、快速试错,成为备受关注的问题。提升研发效率、缩短研发周期,保障产品快速试错并能快速迭代新功能,让新产品新功能以最快的速度同时抵达 Android、iOS 等多端用户。

众所周知,Android 应用采用 Java 或 Kotlin 编写,iOS 应用采用 Objective-C 或 Swift 编写,Web 端采用 HTML /CSS/JavaScript 编写。当需要开发支持多端的应用,每一端都需要独立研发、测试,一直到上线,以及后续的维护工作,工作量成倍增涨,势必延长研发周期。

为了解决多端独立开发的问题,跨平台技术便应运而生,各大互联网公司为此都投入大量人力,于是出现了各种跨平台技术框架,面对移动领域的跨平台技术方案的层出不穷,又该如何做技术选型呢?

3. 移动端技术选型

作为移动端的跨端技术方案,所关注无外乎以下这4个方面:研发效率、动态性、多端一致性、性能体验。

字节跳动技术整理:一文秒懂Flutter 跨平台演进及架构

 

  1. 研发效率:最大化代码复用,减少多端差异的适配工作量,降低开发成本,专注业务开发,实现“write once,run everywhere”的终极目标。效率提升是贯穿整个业务的生命周期线,即便业务上线后,可持续降低后续的维护成本,加快新feature的迭代速度,这是一个持续的效率收益。当然,这里不得不说,任何一门新技术在开发启动学习阶段会有一些成本,但上手后的收益是长期的。
  2. 动态化:突破渠道的更新频率,可快速迭代新功能,这一点不仅是跨平台技术的诉求,也是Native技术必备的杀手锏,这也是评估跨端技术的一个重要考核点。
  3. 多端一致性:好产品在多端UI设计上,往往是整体风格统一,所以业务方采用原生各自独立开发完成后,还需额外花不少时间来修改UI以保证多端一致性;可见,各端独立实现开发方式,带来的效率滞后,不仅仅是Android和iOS各开发一份代码的工作量,还有双端UI的一致性对齐的工作。
  4. 性能体验:一般地,跨端技术方案拥有以上多重优势,但在性能方面比原生流畅更差些。牺牲部分体验换来效率提升,这一点也是情理之中,试想一下,跨平台技术方案同时兼得这4点,那么原生技术恐怕已退出历史舞台,早已是跨平台技术的天下,所以往往跨平台技术的性能优劣便成为核心指标。

4. 跨平台技术划分

对研发效率和体验的不断追逐,移动端的跨平台技术方框架层出不穷,然则天下武功众多,万变不离其宗,从其核心本质来划分,可大致分为以下三大类:

字节跳动技术整理:一文秒懂Flutter 跨平台演进及架构

 

  1. Web技术:主要依赖于WebView的技术,功能支持受限,性能体验很差,比如PhoneGap、Cordova、小程序。
  2. 原生渲染:使用JavaScript作为编程语言,通过中间层转化为原生控件来渲染UI界面,比如React Native、Weex。
  3. 自渲染技术:自行实现一套渲染框架,可通过调用skia等方式完成自渲染,而不依赖于原生控件,比如Flutter、Unity。

5. 跨平台技术演进

跨平台技术,一直以来是每一个有追求的开发者所追逐的梦想,同时也是守旧者的噩梦,跨平台的多端一体化方案势必颠覆现有的原生各端独立开发模式,接下来列举众多的跨平台技术中最为关键的几个技术方案的演进阶段。

字节跳动技术整理:一文秒懂Flutter 跨平台演进及架构

 

从上图可以看出,技术演进过程大致分以下三个阶段: 第一阶段,采用WebView技术绘制界面的Hybrid混合开发技术,通过JS Bridge 将系统部分能力暴露给 JS 调用,其缺点是性能较差,功能受限,扩展性差,不适合交互复杂的场景,比如Cordova。 第二阶段,针对WebView界面性能等问题,于是绘制交还原生渲染,仅仅通过JS调用原生控件,相比WebView技术性能体验更好,这是目前绝大部分跨平台框架的设计思路,比如React Native、Weex。另外,最近小程序也比较火,第一和第二阶段的融合,依然采用WebView作为渲染容器,通过限制Web技术栈的子集,规范化组件使用,并逐步引入原生控件代表WebView渲染,以提升性能。 第三阶段,虽然通过桥接技术使用原生控件解决了功能受限问题,提升性能体验,但相比原生体验差距还是比较大,以及处理平台差异性非常耗费人力。于是Flutter提出自带渲染引擎的解决方案,尽可能减少不同平台间的差异性, 同时媲美原生的高性能体验,因此业界对 Flutter有着极高的关注度。

面对现有的如此多跨平台方案,为何当下最火的跨平台技术是Flutter,有哪些优势呢?

RN、Weex均使用JavaScript作为编程语言,JavaScript作为前端开发语言,在跨平台开发中可谓大放异彩,利用web技术不仅能开发出网站,也可以开发手机端web应用和移动端应用程序,似有一统三界(Android、iOS、Web)的趋势,这就是大家常说的“大前端”时代。这些技术方案流畅度不太好,平台一致性较差,至今还没能大面积取代原生开发。

Flutter是以Dart语言编写,开发体验更接近客户端,从大家使用反馈来看也是如此,Flutter开发环境这一套的流程对于前端开发来说并不太友好。Flutter的定位同样是多端一体化,但是以客户端为首,先磨平Android和iOS双端开发体验,再逐步向Web端渗透,从Flutter规划的Roadmap也能看出,Flutter for web目前仍处于预览版,Flutter客户端方向都已经如火如荼上线了不少应用。

在此之前,大家常说“大前端”,对于Flutter技术,在笔者看来称之为“大移动端”更贴切,Flutter的UI框架优先支持客户端(Android/iOS)应用的同时,然后再适配Web端。移动互联网时代,不少公司都制定“移动优先”的战略,甚至只开发移动端,没有Web端。移动互联网的时代造就“大移动端”,Flutter作为一款能做到媲美原生的高性能跨平台技术方案,或许一统天下。

在跨平台技术领域,只要挑战在,技术就不会停滞,伴随着技术不断演进与革新,终将走向美好。

6. Flutter技术优势

Flutter是彻底的跨平台方案,既没有采用webView,也没有采用JS桥接原生控件,而是自行实现一套UI框架,在引擎底层通过Skia渲染到屏幕。对于UI之外所需要使用的移动设备自身提供的服务,比如相机、定位、屏幕触摸等,则采用Platform Channels跟原生系统通信的方式来实现。

对于Flutter优势,回到前面讲到移动端技术选型的4要素,研发效率、动态性、多端一致性、性能体验,分别对应下面这一组词语。

字节跳动技术整理:一文秒懂Flutter 跨平台演进及架构

 

  1. 高效率:采用dart语言编写代码,虽然刚开始上手需要点时间,但熟练后效率比较高。一套代码适用多个平台(Android、iOS、Web),以及高效的Hot Reload能快速辅助调试;
  2. 动态化:2017年3月苹果下发警告邮件,禁止JSPatch等 iOS App热更新方案,从此iOS动态化成为一个不宜公开讨论的话题。同样地,Flutter引擎在某一个官方版本对动态化做过一些尝试,但后续基于风险考虑移除,当然并没有阻碍大家对技术的探索,这里不方便展开讨论;
  3. 高一致性:实现UI像素级的控制,Flutter渲染引擎依靠跨平台Skia图形库来实现,仅依赖系统图形绘制相关的接口,比如未来Android会支持vulkan,iOS会支持metal,这些都是通过skia封装调用。可最大程度上保证不同平台的体验一致性,见下图所示。

字节跳动技术整理:一文秒懂Flutter 跨平台演进及架构

 

  1. 高性能:渲染性能优于现有的各种跨平台框架,可媲美原生性能的跨平台技术方案,Dart代码执行效率比JS高,通过AOT编译成平台原生代码,渲染采用自渲染skia方案,既不需要JS Bridge桥接,也不需要Art虚拟机参与。再从渲染原理来看看Flutter的高性能的底气在哪里。

字节跳动技术整理:一文秒懂Flutter 跨平台演进及架构

 

图解:

  • Android原生框架,通过调用Java Framework层,再调用到skia来渲染界面;
  • 其他跨平台方案(如RN),通过JSBridge中间层来将JS写的APP转换成相应的原生渲染逻辑,可见比Native代码增加了更多逻辑,性能逊色差于原生框架;
  • Flutter框架,APP通过调用Dart Framework层,再直接调用到skia来渲染界面,并没有经过原生Framework过程,可见其渲染性能并不会弱于Native技术,这是一个性能上限很高的跨平台技术。

当然,不得不说目前的Flutter确实不够尽善尽美,会存在一些不够尽善尽美之处,比如生态不够健全,包体积问题,但其该方案的上限比较高,想象空间比较大,相信更多开发者参与进来,经过更多打磨,未来会做得更好。

7. 业界发展近况

2017年5月Google I/O大会正式对外公布Flutter,到2018年12月发布Flutter1.0,引发全球大量的开发者和企业开始研究Flutter。StackOverflow 2019年的全球开发者文件调查中,Flutter被评选为最受开发者欢迎的框架之一,超过了TensorFlow和Node.js。

字节跳动技术整理:一文秒懂Flutter 跨平台演进及架构

 

到目前,全球越来越多的公司已经在大家耳熟能详的知名APP中使用Flutter技术并落地,尤其国内知名互联网公司对Flutter投入度很大,社区也是非常活跃。

字节跳动技术整理:一文秒懂Flutter 跨平台演进及架构

 

8. Flutter未来趋势

目前Flutter主要在移动端Android/iOS双端跨端,Flutter 的愿景是成为一个多端运行的 UI 框架,能够支持不仅仅是移动端,还包括Web、桌面、甚至嵌入式设备。在2019 Google I/O 开发者大会上推出的使用 Flutter 开发 Web 应用的框架,同年9月发布Flutter 1.9,并将Flutter web合入Flutter主仓库。

字节跳动技术整理:一文秒懂Flutter 跨平台演进及架构

 

从架构图看,Flutter采用同一个Dart Framework层来统一Flutter C++引擎和Web引擎,最终可以运行在Android,iOS,Browser上,从Flutter引擎代码不难看出Flutter也是支持Fuchsia操作系统。

Fuchsia是Google内部正在开发的一款新的操作系统,采用Flutter作为系统默认的UI框架,也就是说Flutter天然支持Fuchsia,这无疑让Flutter在众多的跨平台方案更有优势。

从Fuchsia技术架构来看,内核层zircon的基础LK是专为嵌入式应用中小型系统设计的内核,代码简洁,适合嵌入式设备和高性能设备,比如IOT、移动可穿戴设备等,目前这些领域还没有标准化级别的垄断者。以及在框架层中有着语音交互、云端以及智能化等模块,由此笔者揣测未来Fuchsia率先应用在音控等智能嵌入式设备。

字节跳动技术整理:一文秒懂Flutter 跨平台演进及架构

 

目前大家普遍比较看好的未来两个技术就是5G和IoT时代。对于5G的需求,很大程度上是因为移动互联网发展到“IoT时代”的阶段。这个发展阶段,全球上网设备的数量可能会达到500亿个。随着5G+IOT时代的到来,现在大家比较关注的Flutter包大小也同样不再是一个问题,或许Flutter技术的生命期比客户端更长,或许Fuchsia正在驰骋IOT疆场,你所掌握的Flutter技术栈可以无缝迁移,一次弯道超车的机会。

到此,介绍完跨平台技术演进以及Flutter的优势。看到这,相信你可能对Flutter技术有一定兴趣,为了能让大家快速了解Flutter内部原理而不枯燥,Gityuan通过一系列图来帮大家从整体架构来快速理解Flutter。

二、Flutter引擎架构

1. Flutter技术架构

先来看看Flutter整体的技术架构,分为四层,从上之下依次是Dart APP,Dart Framework, C++ Engine,Platform。

字节跳动技术整理:一文秒懂Flutter 跨平台演进及架构

 

Flutter架构最核心的便是Framework(框架)和Engine(引擎):

  • Flutter Framework层:用Dart编写,封装整个Flutter架构的核心功能,包括Widget、动画、绘制、手势等功能,有Material(Android风格UI)和Cupertino(iOS风格)的UI界面, 可构建Widget控件以及实现UI布局。
  • Flutter Engine层:用C++编写,用于高质量移动应用的轻量级运行时环境,实现了Flutter的核心库,包括Dart虚拟机、动画和图形、文字渲染、通信通道、事件通知、插件架构等。引擎渲染采用的是2D图形渲染库Skia,虚拟机采用的是面向对象语言Dart VM,并将它们托管到Flutter的嵌入层。shell实现了平台相关的代码,比如跟屏幕键盘IME和系统应用生命周期事件的交互。不同平台有不同的shell,比如Android和iOS的shell。

2. Flutter编译产物

看完Flutter内部架构,或许你好奇,Flutter不用Android/iOS的本地语言技术开发,Dart编写完的代码如何让不同系统可以识别,最终编译后得到的产物是什么呢?

字节跳动技术整理:一文秒懂Flutter 跨平台演进及架构

 

Flutter产物分为Dart业务代码和Engine代码各自生成的产物,图中的Dart Code包含开发者编写的业务代码,Engine Code是引擎代码,如果并没有定制化引擎,则无需重新编译引擎代码。

一份Dart代码,可编译生成双端产物,实现跨平台的能力。经过编译工具处理后可生成双端产物,图中便是release模式的编译产物,Android产物是由vm、isolate各自的指令段和数据段以及flutter.jar组成的app.apk,iOS产物是由App.framework和Flutter.framework组成的Runner.app。

这个过程涉及frontend_server、gen_snapshot、xcrun、ninja编译工具。frontend_server前端编译器会进行词法分析、语法分析以及相关全局转换等工作,将dart代码转换为AST(抽象语法树),并生成app.dill格式的dart kernel。gen_snapshot经过CHA、内联等一系列执行流的优化,根据中间代码生成优化后的FlowGraph对象,再转换为具体相应系统架构(arm/arm64等)的二进制指令。

3. Flutter引擎启动

既然了解了Flutter的编译产物,那你或许又好奇,Flutter这台引擎如何发动的,怎么跟Native衔接呢?

字节跳动技术整理:一文秒懂Flutter 跨平台演进及架构

 

这里以Android为例,熟悉Android的开发者,应该都了解APP启动过程,会执行Application和Activity的onCreate()方法,FlutterApplication和FlutterActivity的onCreate()方法正是连接Native和Flutter的枢纽。

  • FlutterApplication.java的onCreate过程主要完成初始化配置、加载引擎libflutter.so、注册JNI方法;
  • FlutterActivity.java的onCreate过程,通过FlutterJNI的AttachJNI()方法来初始化引擎Engine、Dart虚拟机、Isolate、taskRunner等对象。再经过层层处理最终调用main.dart中main()方法,执行runApp(Widget app)来处理整个Dart业务代码。

Flutter引擎启动中会创建有4个TaskRunner以及创建虚拟机,分别来看看它们的工作原理。

4. TaskRunner工作原理

Flutter引擎启动过程,会创建UI/GPU/IO这3个线程,会为这些线程依次创建MessageLoop对象,启动后处于epoll_wait等待状态。对于Flutter的消息机制跟Android原生的消息机制有很多相似之处,都有消息(或者任务)、消息队列(或任务队列)以及Looper;有一点不同的是Android有一个Handler类,用于发送消息以及执行回调方法,相对应Flutter中有着相近功能的便是TaskRunner。

字节跳动技术整理:一文秒懂Flutter 跨平台演进及架构

 

上图是从源码中提炼而来的任务处理流程,比官方流程图更容易理解一些复杂流程的时序问题,后续会专门讲解个中原由。Flutter的任务队列处理机制跟Android的消息队列处理相通,只不过Flutter分为Task和MicroTask两种类型,引擎和Dart虚拟机的事件以及Future都属于Task,Dart层执行scheduleMicrotask()所产生的属于Microtask。

每次Flutter引擎在消费任务时调用FlushTasks()方法,遍历整个延迟任务队列delayed_tasks_,将已到期的任务加入task队列,然后开始处理任务。

  • Step 1: 检查task,当task队列不为空,先执行一个task;
  • Step 2: 检查microTask,当microTask不为空,则执行microTask;不断循环Step 2 直到microTask队列为空,再回到执行Step 1;

可简单理解为先处理完所有的Microtask,然后再处理Task。因为scheduleMicrotask()方法的调用自身就处于一个Task,执行完当前的task,也就意味着马上执行该Microtask。

了解了其工作机制,再来看看这4个Task Runner的具体工作内容。

  • Platform Task Runner:运行在Android或者iOS的主线程,尽管阻塞该线程并不会影响Flutter渲染管道,平台线程建议不要执行耗时操作;否则可能触发watchdog来结束该应用。比如Android、iOS都是使用平台线程来传递用户输入事件,一旦平台线程被阻塞则会引起手势事件丢失。
  • UI Task Runner: 运行在ui线程,比如1.ui,用于引擎执行root isolate中的所有Dart代码,执行渲染与处理Vsync信号,将widget转换生成Layer Tree。除了渲染之外,还有处理Native Plugins消息、Timers、Microtasks等工作;
  • GPU Task Runner:运行在gpu线程,比如1.gpu,用于将Layer Tree转换为具体GPU指令,执行设备GPU相关的skia调用,转换相应平台的绘制方式,比如OpenGL, vulkan, metal等。每一帧的绘制需要UI Runner和GPU Runner配合完成,任何一个环节延迟都可能导致掉帧;
  • IO Task Runner:运行在io线程,比如1.io,前3个Task Runner都不允许执行耗时操作,该Runner用于将图片从磁盘读取出来,解压转换为GPU可识别的格式后,再上传给GPU线程。为了能访问GPU,IO Runner跟GPU Runner的Context在同一个ShareGroup。比如ui.image通过异步调用让IO Runner来异步加载图片,该线程不能执行其他耗时操作,否则可能会影响图片加载的性能。

5. Dart虚拟机工作

Flutter引擎启动会创建Dart虚拟机以及Root Isolate。DartVM自身也拥有自己的Isolate,完全由虚拟机自己管理的,Flutter引擎也无法直接访问。Dart的UI相关操作,是由Root Isolate通过Dart的C++调用,或者是发送消息通知的方式,将UI渲染相关的任务提交到UIRunner执行,这样就可以跟Flutter引擎相关模块进行交互。

何为Isolate,从字面上理解是“隔离”,isolate之间是逻辑隔离的。Isolate中的代码也是按顺序执行,因为Dart没有共享内存的并发,没有竞争的可能性,故不需要加锁,也没有死锁风险。对于Dart程序的并发则需要依赖多个isolate来实现。

字节跳动技术整理:一文秒懂Flutter 跨平台演进及架构

 

图解:

  • isolate堆是运该isolate中代码分配的所有对象的GC管理的内存存储;
  • vm isolate是一个伪isolate,里面包含不可变对象,比如null,true,false;
  • isolate堆能引用vm isolate堆中的对象,但vm isolate不能引用isolate堆;
  • isolate彼此之间不能相互引用;
  • 每个isolate都有一个执行dart代码的Mutator thread,一个处理虚拟机内部任务(比如GC, JIT等)的helper thread; 可见,isolate是拥有内存堆和控制线程,虚拟机中可以有很多isolate,但彼此之间内存不共享,无法直接访问,只能通过dart特有的Port端口通信;isolate除了拥有一个mutator控制线程,还有一些其他辅助线程,比如后台JIT编译线程、GC清理/并发标记线程;

6. Widget架构概览

Flutter引擎启动后执行Dart业务,是通过runApp(Widget app)方法,那Widget又是什么呢?

字节跳动技术整理:一文秒懂Flutter 跨平台演进及架构

 

Widget是所有Flutter应用程序的基石,Widget可以是一个按钮,一种字体或者颜色,一个布局属性等,在Flutter的UI世界可谓是“万物皆Widget”。常见的Widget子类为StatelessWidget(无状态)和StatefulWidget(有状态);

  • StatelessWidget:内部没有保存状态,UI界面创建后不会发生改变;
  • StatefulWidget:内部有保存状态,当状态发生改变,调用setState()方法会触发StatefulWidget的UI发生更新,对于自定义继承自StatefulWidget的子类,必须要重写createState()方法。

三棵树

字节跳动技术整理:一文秒懂Flutter 跨平台演进及架构

 

图解:

  • Widget是为Element描述需要的配置, 负责创建Element,决定Element是否需要更新。Flutter Framework通过差分算法比对Widget树前后的变化,决定Element的State是否改变。当重建Widget树后并未发生改变, 则Element不会触发重绘,则就是Widget树的重建并不一定会触发Element树的重建。
  • Element表示Widget配置树的特定位置的一个实例,同时持有Widget和RenderObject,负责管理Widget配置和RenderObject渲染。Element状态由Flutter Framework管理, 开发人员只需更改Widget即可。
  • RenderObject表示渲染树的一个对象,负责真正的渲染工作,比如测量大小、位置、绘制等都由RenderObject完成。

可见,开发者通过Widget配置,Framework通过比对Widget配置来更新Element,最后调度RenderObject Tree完成布局排列和绘制。

7. 渲染原理

Dart的UI采用Widget来实现,最终转换为RenderObject,那界面又是如何渲染的呢?

字节跳动技术整理:一文秒懂Flutter 跨平台演进及架构

 

渲染过程,UI线程完成布局、绘制操作,生成Layer Tree;GPU线程执行合成并光栅化后交给GPU来处理,其中几个关键步骤:

  • Animate: 遍历_transientCallbacks,执行动画回调方法;
  • Build: 对于dirty的元素会执行build构造,没有dirty元素则不会执行,对应于buildScope()
  • Layout: 计算渲染对象的大小和位置,对应于flushLayout(),这个过程可能会嵌套再调用build操作;
  • Compositing bits: 更新具有脏合成位的任何渲染对象, 对应于flushCompositingBits();
  • Paint: 将绘制命令记录到Layer, 对应于flushPaint();
  • Compositing: 将Compositing bits发送给GPU, 对应于compositeFrame();

GPU线程通过skia向GPU硬件绘制一帧的数据,GPU将帧信息保存到FrameBuffer里面,然后视频控制器会根据VSync信号从FrameBuffer取帧数据传递给显示器,从而显示出最终的画面。

8. Platform Channels

Flutter框架提供了UI的控件支持,对于APP除了UI还有其他依赖于Native平台的支持,比如调用Camera的功能,该怎么办呢?为此,Flutter通过提供Platform Channel的功能,使得Dart代码具备与Native交互的能力。

字节跳动技术整理:一文秒懂Flutter 跨平台演进及架构

 

Platform Channel用于Flutter与Native之间的消息传递,整个过程的消息与响应是异步执行,不会阻塞用户界面。Flutter引擎框架已完成桥接的通道,这样开发者只需在Native层编写定制的Android/iOS代码,即可在Dart代码中直接调用,这也就是Flutter Plugin插件的一种形式。

三、Flutter源码解读

笔者(Gityuan)之前一直从事于Android操作系统底层研发工作,今年刚接触Flutter,Flutter作为一门全新的跨平台技术框架,不断深究会发现这是一个小型系统,涉及到的技术很广:

  • 编译技术如何将dart代码转换为AST(抽象语法树),如何汇编转换为机器码,打包产物是什么?
  • Flutter这台引擎如何发动的,怎么跟Native原生系统衔接运行,如何识别产物并加载到内存?
  • 引擎启动后,TaskRunner如何分发任务,跟原生系统消息机制有什么关系?
  • Dart虚拟机如何管理内存,跟isolate又有什么关系?
  • 开发者编写的Widget控件如何渲染到屏幕上?
  • Flutter如何通过plugin支持移动设备提供的服务?

这些疑问在前面都逐一简要解答,如果仅仅是用Flutter做业务开发,并不需要掌握这么深度技术,不过,知其然知其所以然,能让你游刃有余。

本文同步在我的公号《跨平台技术演进及Flutter未来》,另外为大家创建了Flutter微信交流群,想加入可添加Gityuan微信好友,备注flutter即可,如发现任何文章有问题欢迎大家直接跟我反馈。

启动篇

  • 深入理解Flutter引擎启动
  • 深入理解Dart虚拟机启动
  • 深入理解Flutter应用启动

通信篇

  • 深入理解Flutter消息机制
  • 深入理解Flutter的Platform Channel机制
  • 深入理解Flutter异步Future机制
  • 深入理解Flutter的Isolate创建过程

渲染篇

  • Flutter渲染机制—UI线程
  • Flutter渲染机制—GPU线程
  • 深入理解setState更新机制
  • 深入理解Flutter动画原理

后续笔者将持续研究与梳理Flutter内部机制的文章。

Flutter中SQLite数据库的使用

同时支持android和ios

支持事务和批量操作
支持插入/查询/更新/删除操作
在iOS和Android上的后台线程中执行数据库操作

1.添加依赖

dependencies:
  ...
  sqflite: any
Dart

2.导入依赖

import 'package:sqflite/sqflite.dart';
Dart

3.支持SQL查询

// 获取本地SQLite数据库
var databasesPath = await getDatabasesPath();
String path = join(databasesPath, "demo.db");

// 删除数据库
await deleteDatabase(path);

// 打开数据库
Database database = await openDatabase(path, version: 1,
    onCreate: (Database db, int version) async {
  // 当打开数据库的时候创建一张表
  await db.execute(
      "CREATE TABLE Test (id INTEGER PRIMARY KEY, name TEXT, value INTEGER, num REAL)");
});

// 开启事务,增加两条记录
await database.transaction((txn) async {
  int id1 = await txn.rawInsert(
      'INSERT INTO Test(name, value, num) VALUES("some name", 1234, 456.789)');
  print("inserted1: $id1");
  int id2 = await txn.rawInsert(
      'INSERT INTO Test(name, value, num) VALUES(?, ?, ?)',
      ["another name", 12345678, 3.1416]);
  print("inserted2: $id2");
});

// 更新一条记录
int count = await database.rawUpdate(
    'UPDATE Test SET name = ?, VALUE = ? WHERE name = ?',
    ["updated name", "9876", "some name"]);
print("updated: $count");

// 获取Test表的数据
List<Map> list = await database.rawQuery('SELECT * FROM Test');
List<Map> expectedList = [
  {"name": "updated name", "id": 1, "value": 9876, "num": 456.789},
  {"name": "another name", "id": 2, "value": 12345678, "num": 3.1416}
];
print(list);
print(expectedList);
assert(const DeepCollectionEquality().equals(list, expectedList));

// 获取记录的数量
count = Sqflite
    .firstIntValue(await database.rawQuery("SELECT COUNT(*) FROM Test"));
assert(count == 2);

// 删除一条记录
count = await database
    .rawDelete('DELETE FROM Test WHERE name = ?', ['another name']);
assert(count == 1);

// 关闭数据库
await database.close();
Dart

4.用法示例

final String tableTodo = "todo";
final String columnId = "_id";
final String columnTitle = "title";
final String columnDone = "done";

class Todo {
  int id;
  String title;
  bool done;

  Map<String, dynamic> toMap() {
    var map = <String, dynamic>{
      columnTitle: title,
      columnDone: done == true ? 1 : 0
    };
    if (id != null) {
      map[columnId] = id;
    }
    return map;
  }

  Todo();

  Todo.fromMap(Map<String, dynamic> map) {
    id = map[columnId];
    title = map[columnTitle];
    done = map[columnDone] == 1;
  }
}

class TodoProvider {
  Database db;

  Future open(String path) async {
    db = await openDatabase(path, version: 1,
        onCreate: (Database db, int version) async {
      await db.execute('''
create table $tableTodo ( 
  $columnId integer primary key autoincrement, 
  $columnTitle text not null,
  $columnDone integer not null)
''');
    });
  }

  Future<Todo> insert(Todo todo) async {
    todo.id = await db.insert(tableTodo, todo.toMap());
    return todo;
  }

  Future<Todo> getTodo(int id) async {
    List<Map> maps = await db.query(tableTodo,
        columns: [columnId, columnDone, columnTitle],
        where: "$columnId = ?",
        whereArgs: [id]);
    if (maps.length > 0) {
      return new Todo.fromMap(maps.first);
    }
    return null;
  }

  Future<int> delete(int id) async {
    return await db.delete(tableTodo, where: "$columnId = ?", whereArgs: [id]);
  }

  Future<int> update(Todo todo) async {
    return await db.update(tableTodo, todo.toMap(),
        where: "$columnId = ?", whereArgs: [todo.id]);
  }

  Future close() async => db.close();
}
Dart

注意事项

1.Transaction(事务)

当使用transaction对象访问数据时,不能再使用database对象访问数据库
await database.transaction((txn) async {
  // 完全Ok
  await txn.execute("CREATE TABLE Test1 (id INTEGER PRIMARY KEY)");

  // 不能在transaction对象里面使用database对象,这会发生死锁
  // await database.execute("CREATE TABLE Test2 (id INTEGER PRIMARY KEY)");
});
Dart

2.支持批量操作

batch = db.batch();
batch.insert("Test", {"name": "item"});
batch.update("Test", {"name": "new_item"}, where: "name = ?", whereArgs: ["item"]);
batch.delete("Test", where: "name = ?", whereArgs: ["item"]);
results = await batch.commit();
Dart
这些操作返回结果,都会有一些开销;如果你不考虑结果,可以使用:
await batch.commit(noResult: true);
Dart
事务期间,直到事务被提交,批量操作才能提交
await database.transaction((txn) async {
  var batch = txn.batch();
  //...
  // 实际提交将在事务启动时发生
  await batch.commit();
});
Dart

3.表和列的名字

通常避免使用SQLite的关键字作为表名或者列名,例如以下其中之一:
"add","all","alter","and","as","autoincrement","between","case","check","collate","commit","constraint","create","default","deferrable","delete","distinct","drop","else","escape","except","exists","foreign","from","group","having","if","in","index","insert","intersect","into","is","isnull","join","limit","not","notnull","null","on","or","order","primary","references","select","set","table","then","to","transaction","union","unique","update","using","values","when","where"
Dart
否则要使用双引号转义,例如:
db.rawQuery('SELECT * FROM "table"');
db.query("table", columns: ["group"], where: '"group" = ?', whereArgs: ["my_group"]);
Dart

4.支持的列类型

INTEGER   相当于dart类型中int
 REAL     相当于dart类型中num
 TEXT     相当于dart类型中String
 BLOB     相当于dart类型中Uint8List

Flutter的flutter_calendar日历的使用

 

 

添加依赖:

flutter_calendar: ^0.0.1

项目中导入

import 'package:flutter_calendar/flutter_calendar.dart';

例子

复制代码
import 'package:flutter/material.dart';
import 'package:flutter_calendar/flutter_calendar.dart';

main() {
  runApp(new CalendarViewApp());
}

class CalendarViewApp extends StatelessWidget {
  void handleNewDate(date) {
    print("handleNewDate ${date}");
  }

  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Flutter Demo',
      theme: new ThemeData(
        primarySwatch: Colors.purple,
      ),
      home: new Scaffold(
        appBar: new AppBar(
          title: new Text('Flutter Calendar'),
        ),
        body: new Container(
          margin: new EdgeInsets.symmetric(
            horizontal: 5.0,
            vertical: 10.0,
          ),
          child: new ListView(
            shrinkWrap: true,
            children: <Widget>[
              new Text('The Default Calendar:'),
              new Calendar(
                onSelectedRangeChange: (range) =>
                    print("Range is ${range.item1}, ${range.item2}"),
                onDateSelected: (date) => handleNewDate(date),
              ),
              new Divider(
                height: 50.0,
              ),
              new Text('The Expanded Calendar:'),
              new Calendar(
                onSelectedRangeChange: (range) =>
                    print("Range is ${range.item1}, ${range.item2}"),
                isExpandable: true,
              ),
              new Divider(
                height: 50.0,
              ),
              new Text('A Custom Weekly Calendar:'),
              new Calendar(
                onSelectedRangeChange: (range) =>
                    print("Range is ${range.item1}, ${range.item2}"),
                isExpandable: true,
                dayBuilder: (BuildContext context, DateTime day) {
                  return new InkWell(
                    onTap: () => print("OnTap ${day}"),
                    child: new Container(
                      decoration: new BoxDecoration(
                          border: new Border.all(color: Colors.black38)),
                      child: new Text(
                        day.day.toString(),
                      ),
                    ),
                  );
                },
              ),
            ],
          ),
        ),
      ),
    );
  }
}
复制代码

Flutter中的事件总线(EventBus)

前言

Flutter作为跨平台开发技术、Flutter以其美观、快速、高效、开放等优势迅速俘获人心,但很多FLutter兴趣爱好者进阶学习缺少资料,今天我把搜集和整理的这份学习资料分享给有需要的人 领取地址:

Flutter:从认识到实战

EventBus介绍

在Android事件总线库中,EventBus库是最广泛之一。 看看Android EventBus库的说明 官网

  • 发布者/订阅者模式
  • 集中式通信
  • 低耦合,简化组件通信

自定义EventBus

我们要实现一个Flutter版的EventBus!功能如下:

  • 注册Subscriber到EventBus
  • Publisher post Event,分发给Subscriber
  • EventBus可以注册多种类型的Event

Demo地址 入口 main_eventbus.dart

接口定义

//发布者接口
abstract class IPublisher {
  void post<T>(T event);
}
//订阅者:函数对象
typedef ISubscriber<T> = void Function(T event);

//集中式通信,
//1.IEventBus继承IPublisher,分发数据
//2.IEventBus注册和取消注册ISubscriber
abstract class IEventBus extends IPublisher {

  void register<T>(ISubscriber<T> subscriber);

  void unregister<T>(ISubscriber<T> subscriber);
}
复制代码

接口实现1

Type typeOf<T>() => T;

class MyEventBus1 implements IEventBus {
  //我们用map存放我们的订阅者。不同订阅者订阅的Event类型可能是不同的
  Map<Type, List<Function>> map = new Map();

  @override
  void register<T>(ISubscriber<T> subscriber) {
    Type type = typeOf<T>();
    if (!map.containsKey(type)) {
      map[type] = new List();
    }
    map[type].add(subscriber);
  }

  @override
  void unregister<T>(ISubscriber<T> subscriber) {
    Type type = typeOf<T>();
    if (map.containsKey(type)) {
      map[type].remove(subscriber);
    }
  }

  //发布
  @override
  void post<T>(T event) {
    Type type = typeOf<T>();
    if (map.containsKey(type)) {
      var subscribers = map[type];
      subscribers?.forEach((subscriber) => subscriber?.call(event));
    }
  }
}
复制代码

接口实现2

class MyEventBus2 implements IEventBus {
  List<Function> subscribers = new List();

  @override
  register<T>(ISubscriber<T> subscriber) {
    if (!subscribers.contains(subscriber)) {
      subscribers.add(subscriber);
    }
  }

  @override
  unregister<T>(ISubscriber<T> subscriber) {
    if (subscribers.contains(subscriber)) {
      subscribers.remove(subscriber);
    }
  }

  @override
  post<T>(T event) {
    var ints = subscribers.whereType<ISubscriber<T>>();
    ints?.forEach((subscriber) => subscriber?.call(event));
  }
}
复制代码

功能测试

class EventX {}

class EventY {}

main() {
  testEventBus(new MyEventBus1());
  print("--------------------");
  testEventBus(new MyEventBus2());
}

void testEventBus(IEventBus eventBus) {
  ISubscriber<EventX> subscriber1 = (event) => print(event.toString());
  ISubscriber<EventX> subscriber2 = (event) => print(event.toString());
  eventBus.register(subscriber1);
  eventBus.register(subscriber2);
  eventBus.unregister(subscriber1);

  ISubscriber<EventY> subscriber3 = (event) => print(event.toString());
  ISubscriber<EventY> subscriber4 = (event) => print(event.toString());
  eventBus.register(subscriber3);
  eventBus.register(subscriber4);

  eventBus.post(new EventX());
  eventBus.post(new EventY());
}
复制代码

结果输出为以下,测试通过

Instance of 'EventX'
Instance of 'EventY'
Instance of 'EventY'
--------------------
Instance of 'EventX'
Instance of 'EventY'
Instance of 'EventY'
复制代码

使用自定义EventBus

需求效果图

点击每行的按钮,每行上的两个数字会增长1

[图片上传中…(image-a388cc-1588844159165-1)]

<figcaption></figcaption>

代码实现

MyEventBus1 _eventBus = new MyEventBus1();
//首先我们定义event。第一排用EventA,第二排用EventB
class BaseEvent { int count = 0;}
class EventA extends BaseEvent {}
class EventB extends BaseEvent {}

//点击按钮
class ButtonWidget<T extends BaseEvent> extends StatelessWidget {
  final T event;

  const ButtonWidget({Key key, this.event}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return RaisedButton(
      child: Text("increment"),
      onPressed: _increment,
    );
  }

  //点击处理
  void _increment() {
    if (event != null) {
      event.count++;
      _eventBus.post(event);
    }
  }
}

//显示数字
class TextWidget<T> extends StatefulWidget {
  @override
  _TextWidgetState<T> createState() {
    return _TextWidgetState<T>();
  }
}

class _TextWidgetState<T> extends State<TextWidget<T>> {
  int _count = 0;
  ISubscriber<T> _subscriber;

  @override
  void initState() {
    super.initState();
    //通过setState 刷新UI。

    _subscriber =
        (event) => setState(() => _count = (event as BaseEvent).count);
   //注册
    _eventBus.register<T>(_subscriber);
  }

  @override
  Widget build(BuildContext context) {
    return Text(
      "   $_count  ",
      style: TextStyle(fontSize: 18),
    );
  }

  @override
  void dispose() {
    super.dispose();
    //取消注册
    _eventBus.unregister<T>(_subscriber);
  }
}

//页面主体
class EventBusDemoWidget1 extends StatefulWidget {
  EventBusDemoWidget1({Key key}) : super(key: key);

  @override
  _EventBusDemoWidget1State createState() => _EventBusDemoWidget1State();
}

class _EventBusDemoWidget1State extends State<EventBusDemoWidget1> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(eventBusDemo1Title),
      ),
      body: Container(
        child: Column(
          children: <Widget>[
            Row(
              children: <Widget>[
                new ButtonWidget(event: new EventA()),
                new TextWidget<EventA>(),
                new TextWidget<EventA>()
              ],
            ),
            Divider(
              color: Colors.grey,
            ),
            Row(
              children: <Widget>[
                new ButtonWidget(event: new EventB()),
                new TextWidget<EventB>(),
                new TextWidget<EventB>()
              ],
            )
          ],
        ),
      ),
    );
  }
}
复制代码

自定义EventBus大功告成!再来看看别人家的EventBus。

pub上的EventBus

我们在pub.dev上找到了一个EventBus库 ,github地址

An Event Bus using Dart Streams for decoupling applications

使用EventBus

实际相对于自定义的EventBus,不同点在ButtonWidget和TextWidget中。

class ButtonWidget<T extends BaseEvent> extends StatelessWidget {
  final T event;

  const ButtonWidget({Key key, this.event}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return RaisedButton(
      child: Text("increment"),
      onPressed: _increment,
    );
  }

  void _increment() {
    if (event != null) {
      event.count++;
      print(event.count);
      _eventBus.fire(event);
//      _eventBus.post(event);
    }
  }
}

class TextWidget<T> extends StatefulWidget {
  @override
  _TextWidgetState<T> createState() {
    return _TextWidgetState<T>();
  }
}

class _TextWidgetState<T> extends State<TextWidget<T>> {
  int _count = 0;
//  ISubscriber<T> _subscriber;
  StreamSubscription<T> _subscriber;

  @override
  void initState() {
    super.initState();
//    _subscriber =
//        (event) => setState(() => _count = (event as BaseEvent).count);
//    _eventBus.register<T>(_subscriber);
    _subscriber = _eventBus
        .on<T>()
        .listen((event) => setState(() => _count = (event as BaseEvent).count));
  }

  @override
  Widget build(BuildContext context) {
    print(typeOf<T>());
    return Text(
      "   $_count  ",
      style: TextStyle(fontSize: 18),
    );
  }

  @override
  void dispose() {
    super.dispose();
    //    _eventBus.unregister<T>(_subscriber);
    _subscriber.cancel();
  }
}

复制代码

EventBus库源码

class EventBus {
  StreamController _streamController;

  StreamController get streamController => _streamController;

  EventBus({bool sync = false})
      : _streamController = StreamController.broadcast(sync: sync);

  EventBus.customController(StreamController controller)
      : _streamController = controller;

  Stream<T> on<T>() {
    if (T == dynamic) {
      return streamController.stream;
    } else {
      return streamController.stream.where((event) => event is T).cast<T>();
    }
  }

  void fire(event) {
    streamController.add(event);
  }

  void destroy() {
    _streamController.close();
  }
}
复制代码

EventBus库之Stream

不禁感慨,Dart自带Stream,处理异步事件。StreamDart:async库的核心API,对异步提供了非常好的支持。 这里简单贴图,体会下Stream。

****[图片上传中…(image-cd55ca-1588844159160-0)]

<figcaption></figcaption>**** 值得注意的是这句话

return streamController.stream.where((event) => event is T).cast<T>();
复制代码

返回的Stream包了两层 CastStream(_WhereStream(_BroadcastStream),通过这样的处理就可以监听泛型事件了。

_WhereStream先处理

class _WhereStream<T> extends _ForwardingStream<T, T> {
  final _Predicate<T> _test;

  _WhereStream(Stream<T> source, bool test(T value))
      : _test = test,
        super(source);

    //_test即为 (event) => event is T
  void _handleData(T inputEvent, _EventSink<T> sink) {
    bool  satisfies = _test(inputEvent);
    if (satisfies) {
      sink._add(inputEvent);
    }
  }
}
复制代码

CastStreamSubscription实际做的类型转换

class CastStreamSubscription<S, T> implements StreamSubscription<T> {
  final StreamSubscription<S> _source;

  /// User's data handler. May be null.
  void Function(T) _handleData;

  CastStreamSubscription(this._source) {
    _source.onData(_onData);
  }
  void _onData(S data) {
    if (_handleData == null) return;
    T targetData = data as T;
    _zone.runUnaryGuarded(_handleData, targetData);
  }
复制代码

后记

至此,我们在Provider库StreamProvider中遇到了Stream,我们在EventBus库中遇到了Stream Dart语言自带优秀的异步编程模型 Stream和Future。 Stream拥有良好的异步模型,滋养了一大波组件库 Provider/BLoC/flutter_redux/Rxdart/fish-redux。 接下来就有必要对Stream进行分析了。

WordPress中安装插件需要ftp怎么办?

在初次搭建wordpress成功后,老季想安装wordpress中有趣的插件时缺发现需要ftp服务,其实不用真的搭建了一个ftp服务器,然后发现并没有什么卵用,这只是wordpress。

全而留下的坑,那么,怎么绕过这道程序呢.其实很简单,我们只需在wordpress根目录找到一个

wp-config.php,添加以下代码:

define("FS_METHOD","direct");
define("FS_CHMOD_DIR", 0777);
define("FS_CHMOD_FILE", 0777);

搞定.这时又会提醒无法安装,理由是文件无法创建目录,这个好解决.给wordpress添加权限就好

chmod -R 777 wordpress的目录.