Flutter

Flutter

  Flutter是新兴的基于Dart语言的跨平台开发框架,本文将记录下日常Flutter开发时,积累的一些使用点滴.

  本文相关工具:

  • Flutter: stable channel
  • IDE: Android Studio
  • Cocoapods
  • Gradle

1. Flutter的开发模版

  Flutter的开发模版有4种,如下:

[app]                (default) Generate a Flutter application.
[module]             Generate a project to add a Flutter module to an existing Android or iOS application.
[package]            Generate a shareable Flutter project containing modular Dart code.
[plugin]             Generate a shareable Flutter project containing an API in Dart code with a platform-specific implementation for Android, for iOS code, or for both.

  在创建Flutter项目的时候,可以用flutter命令行直接创建或者用Android Studio菜单栏新建项目来创建.

  • app: 纯Flutter项目使用,用于直接开发新的Flutter项目
  • module: 混合开发项目使用,原理是将代码构建出适合Android与iOS集成的依赖库,集成进已有的Native项目
  • package: Flutter的SDK开发时使用,模块化Flutter的代码
  • plugin: Flutter与Native交互时使用,为双侧添加数据传递的能力

  本文将以module开发模版作为切入点记录混合开发中遇到的日常进行记录,另外会涉及到package与plugin开发模版,至于app开发模版大部分情况下是一致的,差异点会在遇到的时候指出.

2. 集成

  将Flutter集成到现有应用

2.1 构建

  将Flutter集成到现有App的方法在社区已经有,方法离不开iOS的Cocoapods和Android的Gradle,原理其实就是Flutter工具可以在module开发模版中生成相应平台的依赖库产物,便于多端集成接入.

  flutter build有很多可构建的产物选项,在module中,通常用到的是Android的aar和iOS的ios-framework,因为这样构建出来的产物可以直接通过Gradle和Cocoapods方式接入到已有App中.

aar             Build a repository containing an AAR and a POM file.
aot             Build an ahead-of-time compiled snapshot of your app's Dart code.
apk             Build an Android APK file from your app.
appbundle       Build an Android App Bundle file from your app.
bundle          Build the Flutter assets directory from your app.
ios             Build an iOS application bundle (Mac OS X host only).
ios-framework   Produces a .framework directory for a Flutter module and its plugins for integration into existing, plain Xcode projects.

  备注: 在新版本Flutter中,已经有直接创建Cocoapods依赖库的构建方式了,可以更加方便地进行构建,但原理都是一样的,新的方式只是更傻瓜更方便而已.

  同样的,这些构建也选择不同的参数,如下:

--[no-]debug         Build a debug version of the current project.
                        (defaults to on)
--[no-]profile       Build a version of the current project specialized for performance profiling.
                        (defaults to on)
--[no-]release       Build a release version of the current project.
                        (defaults to on)
--flavor             Build a custom app flavor as defined by platform-specific build setup.
                        Supports the use of product flavors in Android Gradle scripts, and the use of custom Xcode schemes.
--[no-]pub           Whether to run "flutter pub get" before executing this command.
                        (defaults to on)
--target-platform    The target platform for which the project is compiled.
                        [android-arm (default), android-arm64 (default), android-x86, android-x64 (default)]
--output-dir         The absolute path to the directory where the repository is generated.By default, this is '<current-directory>android/build'.

  在不添加额外参数进行构建时,默认会拉取pub.yaml内的相关依赖,并把debugprofilerelease这三种版本的产物依照顺序构建出来,这是很耗时的操作,在能明确构建的时候,尽量对构建产物作出挑选对提高开发效率能提供极大帮助.

  这里还需要提及一下这三种版本产物的区别:

  • debug: 用于debug,可调试、可热更新(JIT,Just In Time,方式编译)、可跟踪性能(但不准),支持模拟器与真机.
  • profile: 用于贴近于发布的监控,可跟踪性能,只支持真机
  • release: 用于发布,不可调试、不可跟踪性能(AOT, Ahead Of Time,方式编译),只支持真机.

2.2 接入

  对于现有的App,接入Flutter是个技术活,虽然Flutter官方已经提供了不少傻瓜式的工具和文档,Flutter也提供了跨端的开发能力,能使得一次开发,多端使用,但是接入姿势如果不对,一次完整开发的时间可能比直接双端原生开发时间还长.

  想要真正达到Flutter跨端开发减少开发时间,提升开发效率这个效果,接入的方式非常讲究.

  对于iOS侧开发来说,毕竟不像Android是亲儿子,在Flutter产物接入到iOS,需要注意的点如下:

  • 集成模式
  • 集成库安装时间
2.2.1 集成模式

  Flutter的集成模式对应产物的构建版本.

  App发板,使用的是release的构建产物,因为使用的是AOT编译方式,产物经过处理,在iOS端,直接通过Cocoapods集成所有frameworks文件即可,与其他的framework没区别,集成到系统后就是一个framework库,如果原有应用的Cocoapods还不是使用use_frameworks!的话,就还需要对podspec文件配置一下支持静态库的集成.这种方式对于不开发Flutter业务的开发者是无感知的,接入Flutter开发前后,都没有什么区别.

  在App开发Flutter相关功能时,使用release的构建产物就不合适了.开发过程中,涉及断点调试,性能监控,数据传递,还想享受Flutter方便的热更新开发,就必须了解一下官方是怎么做的了.

  在创建module开发模版后,会一起创建一个附带main.dart文件能单独启动的Example项目,这个项目能让module运行起来查看Flutter测的开发效果.

  在Android Studio做好运行配置后,可以发现,debug模式是可以直接在真机上运行,且支持热更新的,那么它在运行前,到底做了什么?

以module开发模版运行到iOS真机为例:

  • 自检Fluter、Xcode等运行环境
  • 查找当前目录的pub.yaml及其依赖并构建
  • 生成临时的iOS项目.ios/并进行项目配置
  • 查找用于安装App到真机的签名证书(可选)
  • 使用pod install为临时iOS项目安装依赖(即module开发模版的构建产物)
  • 构建临时iOS项目
  • 启动电脑端的虚拟机作为调试准备
  • 安装到真机上并启动App
  • 连接电脑上的虚拟机并进行监听
  • 同步电脑上的代码文件到真机中

  一次完整的Flutter运行,Flutter做的事情可不少,同时可以看出为了做到热更新调试代码,Flutter在好几个步骤上做了处理.

  首先,对临时项目进行配置时,Flutter对创建的iOS临时项目模版做了调整,增加了可以执行ruby脚本(位于.ios/Flutter/podhelper.rb)的操作.

flutter_application_path = 'module_path'
load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')
#... 中间省略 ...
install_all_flutter_pods(flutter_application_path)

  这些操作目的是导入Flutter构建的产物依赖,并且对项目注入用于在电脑端启动VM的脚本xcode_backend.sh,当执行pod install时,这些操作被执行.

  在iOS临时项目被构建完毕后,Xcode执行打开VM的脚本等待接入,而App在安装到真机时,接入VM监听等待,VM在此时就能对iOS项目实现调试、监控、热更新代码等操作了.

  在了解了原理后,想要在已有项目中也能想用这些功能,也就是复制一下Podfile中的这些集成语句和使用debug构建运行的功夫.此后,在Native侧与Flutter侧开发,都能提高效率了.

2.2.2 集成库安装时间

  Flutter在开发过程中长生的产物体积很大,经常会导致团队中不需要开发Flutter的同学在更新依赖时花很多时间,降低了开发效率.

  为了避免这种情况,在团队中提交Flutter更新依赖需要以release构建的产物为准,产物也尽量以纯产物为合适,构建过程产生的一系列中间产物对不开发Flutter的同学完全无用.剔除后可以省掉接近95%的拉取依赖的时间.

  所以在已有项目中接入Flutter框架开发的时候,为了避免团队的开发效率明升实降,这些规则是不能缺少的.

3. 调试

  Flutter的调试方式从原理上来看,跟其他的开发的区别是不大的,但对于iOS来说,就需要麻烦一点点。

  Flutter与原生的差异导致开发Flutter的时候,需要选择另外一个代码编辑器开发,例如Android Studio,但是Android又不能很好地让Xcode协助调试,于是在调试Flutter时,基本步骤如下:

  • Xcode启动App
  • App中打开由Flutter开发的页面(使Isolates启动起来)

    1. Xcode控制台查看flutter打印,找到Dart VM Observatory地址
       flutter: Observatory listening on http://127.0.0.1:56371/wnqAowD4ZQM=/
       # 如果Xcode控制台找得麻烦,可以直接在终端以如下方式寻找,更方便
       # idevicesyslog | grep "flutter: Observatory listening on"
      
    2. 利用iproxy映射通过USB连接的真机
       iproxy 8888 56371
      
  • 在Android Stuido中连接App,如下:
  1. 右上角配置入口进入,添加新的调试配置.

Configurations

  1. 在连接前填写映射过端口的Dart VM Observatory地址

Dart Remote Debug

  1. 连接成功会如途中1所示,标记2的按钮可以点击打开网页版的Dart VM Observatory.

Dart Remote Debug connected

  1. 现在可以通过flutter attach按钮进入调试Root Isolate的阶段.

flutter attach

  1. flutter attach连接成功后会如标记1所示,VM与真机数据互通,这时候就可以实现断点调试(标记1)、热重载(标记2)、热重启(标记3)等的开发方式了.

Dart Remote Debug

  正常的情况下,这样子就能进行调试了,这个步骤比Native侧调试的确多了几步,但是Flutter侧的开发是可以hot reloadhot restart的,大大减少了编译App、安装App等平常耗时的步骤,反而提升了开发效率。

4. 性能监控

  Flutter在诞生的时候就有一个目的,就是为App提供丝滑的用户体验,所以流畅的60FPS体验是Flutter开发过程中追求的一个隐藏目标。当然还有很重要的一点是,想要安利别人用上好东西,总要让人有数据证据证明这是好东西(笑);

  性能监控能为你提供这一点证据。

  我们知道人的肉眼通常是24FPS的辨别能力,30FPS是业内通用让肉眼不觉得卡顿的刷新速度的最低标准,而60FPS会让人感觉到丝滑。

  60FPS意味着每一帧的渲染时间约为16.67ms,这就是Flutter性能监控会看到的一个数据标准。

  在基于上述调试的基础上(即Flutter attach后),打开Android Studio的Performance,顶部可以看到渲染帧的时序图,底部可以看到渲染树的刷新次数。

Dart Remote Debug

备注:只有Debug构建才能看到刷新次数,而Performance只能在非Release模式下使用,并且Debug模式看到的渲染时序图数据不能代表最终的性能表现。

  途中标记的1234,是一些能在App上展现出来的辅助工具,可以直接从页面上看到性能监控的情况。

  标记567是监控时序图,标记6是16ms上的一条线,这就是断定渲染当前帧是否满足60FPS的一个标准。

  至于另外的一些内存与CPU等的一些监控,可以直接打开VM提供的入口查看,或者在Android Studio中点dev tool打开查看。

  标记8是跟踪Widget的rebuild次数,这是个很有用的功能,如果发现页面有卡顿,可以先从这部分看起.如图中的例子所示,就是一个没有优化过的Widget树稍微操作了一下看到的rebuild次数,次数并不少.

5. 解决方案

5.1 数据刷新

  Flutter是一个亲UI的框架,万物皆Widget让很多人觉得数据的传递很麻烦。在写Widget的时候,一不小心就会面向过程编程,一个接着一个的嵌套,在debug的时候也容易让人抓狂。对于此,社区是有不错的解决方案的,那就是provider

  provider的使用,会让开发者不自觉地面向对象编程。为了尽可能地减少UI树的刷新,每一个数据的传递都会做到尽量靠近末端,这样子UI树的其他部分就不会触发rebuild,渲染刷新的时间就能尽可能地维持在16.67ms。

  以下来看一个例子,这是个创建Flutter App时自带的一个例子稍作修改的代码:

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    print("MyApp");

    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
  final String title;

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

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
  int _nagetiveCounter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  void _decrementCounter() {
    setState(() {
      _nagetiveCounter--;
    });
  }

  Widget body(BuildContext context) {
    print("_MyHomePageState");
    return Scaffold(
        appBar: AppBar(
          title: Text(widget.title),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Text(
                'You have pushed the button this many times:',
              ),
              Text(
                '${_counter}',
                style: Theme.of(context).textTheme.display1,
              ),
              Text(
                '${_nagetiveCounter}',
                style: Theme.of(context).textTheme.display1,
              )
            ],
          ),
        ),
        floatingActionButton: Column(
          mainAxisAlignment: MainAxisAlignment.end,
          children: <Widget>[
            FloatingActionButton(
              onPressed: () {
                _incrementCounter();
              },
              tooltip: 'Increment',
              child: Icon(Icons.add),
            ),
            FloatingActionButton(
              onPressed: () {
                _decrementCounter();
              },
              tooltip: 'decrement',
              child: Icon(Icons.delete),
            ),
            FloatingActionButton(
              onPressed: () {
                _incrementCounter();
                _decrementCounter();
              },
              tooltip: 'PM',
              child: Icon(Icons.refresh),
            ),
          ],
        ));
  }

  @override
  Widget build(BuildContext context) {
    return body(context);
  }
}

  在点击多次后,rebuild表现是这样的:

without-provider

  每一次的点击,都使UI树rebuild了一次,当页面UI树变得复杂时,这个情况会越来越糟糕,手机发热、卡顿会慢慢显现.

  同样的改动,在使用provider时的例子如下:

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

// =============================================================================
// Provider model
class NumberModel extends ChangeNotifier {
  int _number = 0;
  get number => _number;

  void increment() {
    _number++;
    notifyListeners();
  }

  void decrement() {
    _number--;
    notifyListeners();
  }
}

class TapNumber extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    print("TapNumber");
    return Consumer<NumberModel>(builder: (context, value, child) {
      print("TapNumber Consumer");
      return Text(
        '${value.number}',
        style: Theme.of(context).textTheme.display1,
      );
    });
  }
}

class NagativeTapNumber extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    print("TapNumber");
    return Consumer<NagativeNumberModel>(builder: (context, value, child) {
      print("NagativeTapNumber Consumer");
      return Text(
        '${value.number}',
        style: Theme.of(context).textTheme.display1,
      );
    });
  }
}

class NagativeNumberModel extends ChangeNotifier {
  int _number = 0;
  get number => _number;

  void increment() {
    _number++;
    notifyListeners();
  }

  void decrement() {
    _number--;
    notifyListeners();
  }
}

class TapNumberButton extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    print("TapNumberButton");
    return Column(
      mainAxisAlignment: MainAxisAlignment.end,
      children: <Widget>[
        FloatingActionButton(
          onPressed: () {
            Provider.of<NumberModel>(context, listen: false).increment();
          },
          tooltip: 'Increment',
          child: Icon(Icons.add),
        ), 
        FloatingActionButton(
          onPressed: () {
            Provider.of<NagativeNumberModel>(context, listen: false)
                .decrement();
          },
          tooltip: 'Increment',
          child: Icon(Icons.add),
        ), 
        FloatingActionButton(
          onPressed: () {
            Provider.of<NumberModel>(context, listen: false).increment();
            Provider.of<NagativeNumberModel>(context, listen: false)
                .decrement();
          },
          tooltip: 'Increment',
          child: Icon(Icons.add),
        ), 
      ],
    );
  }
}

// ==================================================================================================

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    print("MyApp");

    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
  final String title;

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

class _MyHomePageState extends State<MyHomePage> {
  Widget body(BuildContext context) {
    print("_MyHomePageState");
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            TapNumber(),
            NagativeTapNumber(),
          ],
        ),
      ),
      floatingActionButton: TapNumberButton(),
    );
  }

  @override
  Widget build(BuildContext context) {
    return MultiProvider(providers: [
      ChangeNotifierProvider(create: (context) => NumberModel()),
      ChangeNotifierProvider(create: (context) => NagativeNumberModel()),
    ], child: body(context));
  }
}

  这次它的表现如下:

without-provider

  互相对比,可以发现与数据无关的Widget,不需要也不会rebuild了,极大减少了设备的花销,页面越复杂,效果越明显.

6. dSYMs 文件获取

集成Flutter的项目遇到框架自身的崩溃异常上报后, 需要获取dSYMs文件恢复符号信息查看问题, 但是其本身构建的产物是不会携带这些文件的, 所以需要按照官方提供的方式获取这些文件.

获取流程:

  1. 获取项目正在使用的版本好, 例如: 2.10.4
  2. FLutter的仓库中, 按照tag选择正在使用的Flutter版本
  3. 打开这个版本的 bin/internal/engine.version 文件, 查看内部记录的版本号
  4. Google云盘 的flutter目录内搜索从上一步获取的版本号值
  5. 下载 ios_release 内的 dSYMs 文件用语恢复崩溃日志的符号

关于Flutter的积累可能会后面慢慢记下来,未完待续。。。

参考资料

《Flutter in action》

《Flutter 技术解析与实战》

《AliFlutter 体系化建设和实践》

聊一聊Flutter线程管理与Dart Isolate机制

Debugging your add-to-app module

Flutter 性能检测


lZackx © 2022. All rights reserved.

Powered by Hydejack v9.1.6