Flutter技术开发入门和一些心得

[TOC]

Flutter是什么?

Flutter 是 Google推出并开源的移动应用开发框架,主打跨平台、高保真、高性能。

  • 它开辟了全新的思路,提供了一整套从底层渲染逻辑到上层开发语言的完整解决方案:视图渲染完全闭环在其框架内部,不依赖于底层操作系统提供的任何组件,从根本上保证了视图渲染在 Android 和 iOS 上的高度一致性。
  • Flutter 的开发语言 Dart,是 Google 专门为(大)前端开发量身打造的专属语言,借助于先进的工具链和编译器,成为了少数同时支持 JIT 和 AOT 的语言之一,开发期调试效率高,发布期运行速度快、执行性能好,在代码执行效率上可以媲美原生 App。
  • Flutter提供了丰富的组件、接口,开发者可以很快地为 Flutter添加 native扩展。

Flutter的特性

跨平台自绘引擎

Flutter与用于构建移动应用程序的其它大多数框架不同,因为Flutter既不使用WebView,也不使用操作系统的原生控件。 相反,Flutter使用自己的高性能渲染引擎来绘制widget。这样不仅可以保证在Android和iOS上UI的一致性,而且也可以避免对原生控件依赖而带来的限制及高昂的维护成本。

Flutter使用Skia作为其2D渲染引擎,Skia是Google的一个2D图形处理函数库,包含字型、坐标转换,以及点阵图都有高效能且简洁的表现,Skia是跨平台的,并提供了非常友好的API,目前Google Chrome浏览器和Android均采用Skia作为其绘图引擎。

目前Flutter默认支持iOS、Android、Fuchsia(Google新的自研操作系统)三个移动平台。但Flutter亦可支持Web开发(Flutter for web)和PC开发。

高性能

Flutter高性能主要靠两点来保证,首先,Flutter APP采用Dart语言开发。Dart在 JIT(即时编译)模式下,速度与 JavaScript基本持平。但是 Dart支持 AOT(提前编译),当以 AOT模式运行时,JavaScript便远远追不上了。速度的提升对高帧率下的视图数据计算很有帮助。其次,Flutter使用自己的渲染引擎来绘制UI,布局数据等由Dart语言直接控制,所以在布局过程中不需要像RN那样要在JavaScript和Native之间通信,这在一些滑动和拖动的场景下具有明显优势,因为在滑动和拖动过程往往都会引起布局发生变化,所以JavaScript需要和Native之间不停的同步布局信息,这和在浏览器中要JavaScript频繁操作DOM所带来的问题是相同的,都会带来比较可观的性能开销。

开发效率高

Dart运行时和编译器支持Flutter的两个关键特性的组合:

基于JIT的快速开发周期:Flutter在开发阶段采用,采用JIT模式,这样就避免了每次改动都要进行编译,极大的节省了开发时间;

基于AOT的发布包: Flutter在发布时可以通过AOT生成高效的ARM代码以保证应用性能。而JavaScript则不具有这个能力。

高性能

Flutter旨在提供流畅、高保真的的UI体验。为了实现这一点,Flutter中需要能够在每个动画帧中运行大量的代码。这意味着需要一种既能提供高性能的语言,而不会出现会丢帧的周期性暂停,而Dart支持AOT,在这一点上可以做的比JavaScript更好。
Google作为一个轮子大厂,直接在两个平台上重写了各自的UIKit,对接到平台底层,减少UI层的多层转换,UI性能可以比肩原生,这个优势在滑动和播放动画时尤为明显。

高度一致性

这里的高度一致性不仅仅指各平台 UI 一致,更重要的是各个平台运行的是同一份代码。以前一份需求在 iOS 与 Android 上需要各实现一份,在迭代的时候就会带来额外的协商成本,对于迭代速度很快的我们来说,Flutter可以很好地抹平这个成本。
但是并不是说用了Flutter就不需要原生开发了,原来做业务实现的原生开发者可以更关注与本身系统相关的底层和性能实现。

高可控制性

什么是高可控制性?Flutter 对宿主的依赖很低,宿主提供一个画布就可以自己运行起来,还有渲染流程和时间派发都是自行运作的。换句话说,无论是修改内部实现还是优化内部逻辑,我们都可以很轻松地做到,这点和过去的 Native 应用开发有很大区别,使用 Native 开发需要各种 Hook,API 还有较高的风险。

快速内存分配

Flutter框架使用函数式流,这使得它在很大程度上依赖于底层的内存分配器。因此,拥有一个能够有效地处理琐碎任务的内存分配器将显得十分重要,在缺乏此功能的语言中,Flutter将无法有效地工作。当然Chrome V8的JavaScript引擎在内存分配上也已经做的很好,事实上Dart开发团队的很多成员都是来自Chrome团队的,所以在内存分配上Dart并不能作为超越JavaScript的优势,而对于Flutter来说,它需要这样的特性,而Dart也正好满足而已。

类型安全

由于Dart是类型安全的语言,支持静态类型检测,所以可以在编译前发现一些类型的错误,并排除潜在问题,这一点对于前端开发者来说可能会更具有吸引力。与之不同的,JavaScript是一个弱类型语言,也因此前端社区出现了很多给JavaScript代码添加静态类型检测的扩展语言和工具,如:微软的TypeScript以及Facebook的Flow。相比之下,Dart本身就支持静态类型,这是它的一个重要优势。

Flutter与RN的优势

RN是通过JavaScript通过 bridge 传递到native完成原生绘制, bridge 的成本高,因为需要频繁的跨桥调用,导致卡顿等性能问题。

Flutter利用DVM(dart虚拟机) 减少了桥的交互,在运行时期直接执行这些编译后的原生代码,就和我们进行原生开发一样,不再需要Bridge来担任中介的角色

用chflutter工具包安装和更新Flutter环境

因为实现版本的统一等功能,我们开发了chflutter工具包,按下面步骤可以直接安装和配置好Flutter的SDK环境。

chflutter工具安装:

chflutter工具

1、打开终端下载flutter_tool_kit代码:

1
2
➜ cd /Code-Flutter/Codes
➜ git clone http://gitlab.108sq.org/flutter_app/flutter_tool_kit.git

2、切换到目录,然后根据终端提示初始化步骤输入

1
2
3
4
➜ cd framework_config.config
➜ python3 framework.py install
请输入flutter项目根目录:/Code-Flutter/Codes/ChangShuo/
请输入gitlab用户token:LNHmxYB1PCuuwJb-bRf5

3、其中用到的access token需要在Gitlab上面设置获取

4、 配置本地环境变量,在终端输入:

1
2
3
4
5
6
7
8
9
10
11
➜ vim ~/.bash_profile
.bash_profile文件中添加保存:
alias chflutter="python3 /Code-Flutter/Codes/flutter_tool_kit/framework_config.config/framework.py"
export PUB_HOSTED_URL=https://pub.flutter-io.cn
export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn
export FLUTTER_ROOT=/flutter
export PATH=/flutter/bin:$PATH
➜ source ~/.bash_profile
➜ chflutter -h

安装flutterSDK环境

设置flutter路径,安装SDK。这边定的地址是:/flutter

1
2
3
4
➜ chflutter -eh
➜ chflutter -ufd /flutter
flutter安装目录已更新
➜ chflutter -i

Dart语言简介

Dart 的特性

JIT 与 AOT

JIT 在运行时即时编译,在开发周期中使用,可以动态下发和执行代码,开发测试效率高,但运行速度和执行性能则会因为运行时即时编译受到影响。
AOT 即提前编译,可以生成被直接执行的二进制代码,运行速度快、执行性能表现好,但每次执行前都需要提前编译,开发测试效率低。

内存分配与垃圾回收

Dart VM 的内存分配策略比较简单,创建对象时只需要在堆上移动指针,内存增长始终是线性的,省去了查找可用内存的过程。
在 Dart 中,并发是通过 Isolate 实现的。Isolate 是类似于线程但不共享内存,独立运行的 worker。这样的机制,就可以让 Dart 实现无锁的快速分配。
Dart 的垃圾回收,则是采用了多生代算法。新生代在回收内存时采用“半空间”机制,触发垃圾回收时,Dart 会将当前半空间中的“活跃”对象拷贝到备用空间,然后整体释放当前空间的所有内存。回收过程中,Dart 只需要操作少量的“活跃”对象,没有引用的大量“死亡”对象则被忽略,这样的回收机制很适合 Flutter 框架中大量 Widget 销毁重建的场景。

单线程模型

Dart 是单线程的。那单线程意味着什么呢?这意味着 Dart 代码是有序的,按照在 main 函数出现的次序一个接一个地执行,不会被其他代码中断。
另外,Dart 当然也支持异步。需要注意的是,单线程和异步并不冲突。
单线程模型可以在等待的过程中做别的事情,等真正需要响应结果了,再去做对应的处理。因为等待过程并不是阻塞的,所以给我们的感觉就像是同时在做多件事情一样。但其实始终只有一个线程在处理你的事情。等待这个行为是通过 Event Loop 驱动的。

在 Dart 中,实际上有两个队列,一个事件队列(Event Queue),另一个则是微任务队列(Microtask Queue)。在每一次事件循环中,Dart 总是先去第一个微任务队列中查询是否有可执行的任务,如果没有,才会处理后续的事件队列的流程。
异步任务我们用的最多的还是优先级更低的 Event Queue。比如,I/O、绘制、定时器这些异步事件,都是通过事件队列驱动主线程执行的。
Dart 为 Event Queue 的任务建立提供了一层封装,叫作 Future。

差异性

1. 变量声明

1.1 var

类似于JavaScript中的var,它可以接收任何类型的变量,但最大的不同是Dart中var变量一旦赋值,类型便会确定,则不能再改变其类型,如:

1
2
3
4
5
var t;
t = "hi world";
// 下面代码在dart中会报错,因为变量t的类型已经确定为String,
// 类型一旦确定后则不能再更改其类型。
t = 1000;

上面的代码在JavaScript是没有问题的,前端开发者需要注意一下,之所以有此差异是因为Dart本身是一个强类型语言,任何变量都是有确定类型的,在Dart中,当用var声明一个变量后,Dart在编译时会根据第一次赋值数据的类型来推断其类型,编译结束后其类型就已经被确定,而JavaScript是纯粹的弱类型脚本语言,var只是变量的声明方式而已。

1.2 dynamic和Object

Object 是Dart所有对象的根基类,也就是说所有类型都是Object的子类(包括Function和Null),所以任何类型的数据都可以赋值给Object声明的对象.
dynamic与var一样都是关键词,声明的变量可以赋值任意对象。
dynamic与Object相同之处在于,他们声明的变量可以在后期改变赋值类型。

1
2
3
4
5
6
7
dynamic t;
Object x;
t = "hi world";
x = 'Hello Object';
//下面代码没有问题
t = 1000;
x = 1000;

dynamic与Object不同的是,dynamic声明的对象编译器会提供所有可能的组合, 而Object声明的对象只能使用Object的属性与方法, 否则编译器会报错。如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
dynamic a;
Object b;
main() {
a = "";
b = "";
printLengths();
}
printLengths() {
// no warning
print(a.length);
// warning:
// The getter 'length' is not defined for the class 'Object'
print(b.length);
}

变量a不会报错, 变量b编译器会报错
dynamic的这个特性与Objective-C中的id作用很像. dynamic的这个特点使得我们在使用它时需要格外注意,这很容易引入一个运行时错误.

1.3 final和const

如果您从未打算更改一个变量,那么使用 final 或 const,不是var,也不是一个类型。 一个 final 变量只能被设置一次,两者区别在于:const 变量是一个编译时常量,final变量在第一次使用时被初始化。被final或者const修饰的变量,变量类型可以省略,如:

1
2
3
4
5
//可以省略String这个类型声明
final str = "hi world";
//final String str = "hi world";
const str1 = "hi world";
//const String str1 = "hi world";

2. 函数

2.1 对于只包含一个表达式的函数,可以使用简写语法

1
2
3
4
5
bool isNoble (int atomicNumber)=> _nobleGases [ atomicNumber ] != null ;
等价于
bool isNoble (int atomicNumber) {
return _nobleGases [ atomicNumber ] != null ;
}

2.2 可选的位置参数

包装一组函数参数,用[]标记为可选的位置参数,并放在参数列表的最后面:

1
2
3
4
5
6
7
String say(String from, String msg, [String device]) {
var result = '$from says $msg';
if (device != null) {
result = '$result with a $device';
}
return result;
}

下面是一个不带可选参数调用这个函数的例子:

1
say('Bob', 'Howdy'); //结果是: Bob says Howdy

下面是用第三个参数调用这个函数的例子:

1
say('Bob', 'Howdy', 'smoke signal'); //结果是:Bob says Howdy with a smoke signal

2.3 可选的命名参数

定义函数时,使用{param1, param2, …},放在参数列表的最后面,用于指定命名参数。例如:

1
2
3
4
//设置[bold]和[hidden]标志
void enableFlags({bool bold, bool hidden}) {
// ...
}

调用函数时,可以使用指定命名参数。例如:paramName: value

1
enableFlags(bold: true, hidden: false);

可选命名参数在Flutter中使用非常多。

注意,不能同时使用可选的位置参数和可选的命名参数

3. 异步支持

3.1 Async/await

async和await关键词支持了异步编程,允许您写出和同步代码很像的异步代码。

3.2 Future

Future与JavaScript中的Promise非常相似,表示一个异步操作的最终完成(或失败)及其结果值的表示。简单来说,它就是用于处理异步操作的,异步处理成功了就执行成功的操作,异步处理失败了就捕获错误或者停止后续操作。一个Future只会对应一个结果,要么成功,要么失败。

1
2
3
4
5
6
7
8
9
10
11
12
Future.delayed(new Duration(seconds: 2),(){
//return "hi world!";
throw AssertionError("Error");
}).then((data){
//执行成功会走到这里
print(data);
}).catchError((e){
//执行失败会走到这里
print(e);
}).whenComplete((){
//无论成功或失败都会走到这里
});

3.3 回调地狱(Callback Hell)

如果代码中有大量异步逻辑,并且出现大量异步任务依赖其它异步任务的结果时,必然会出现Future.then回调中套回调情况。
举个例子,比如现在有个需求场景是用户先登录,登录成功后会获得用户ID,然后通过用户ID,再去请求用户个人信息,获取到用户个人信息后,为了使用方便,我们需要将其缓存在本地文件系统,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//用户登录
Future<String> login(String userName, String pwd){
...
//返回用户ID
};
//获取用户信息
Future<String> getUserInfo(String id){
...
//返回用户个人信息
};
// 保存用户信息
Future saveUserInfo(String userInfo){
...
};

接下来,执行整个任务流:

1
2
3
4
5
6
7
8
9
10
login("alice","******").then((id){
//登录成功后通过,id获取用户信息
getUserInfo(id).then((userInfo){
//获取用户信息后保存
saveUserInfo(userInfo).then((){
//保存用户信息,接下来执行其它操作
...
});
});
})

使用Future消除Callback Hell

1
2
3
4
5
6
7
8
9
10
login("alice","******").then((id){
return getUserInfo(id);
}).then((userInfo){
return saveUserInfo(userInfo);
}).then((e){
//执行接下来的操作
}).catchError((e){
//错误处理
print(e);
});

使用async/await消除callback hell

1
2
3
4
5
6
7
8
9
10
11
task() async {
try{
String id = await login("alice","******");
String userInfo = await getUserInfo(id);
await saveUserInfo(userInfo);
//执行接下来的操作
} catch(e){
//错误处理
print(e);
}
}

4. 字符串拼接的方式

变量取值 $name
函数取值 ${code??” 没有 “}
三引号:’’’ ‘’’

1
2
3
4
5
6
7
8
9
getInfo() => '''
购物车信息:
-----------------------------
用户名: $name
优惠码: ${code??" 没有 "}
总价: $price
Date: $date
-----------------------------
''';

一些Flutter开发中的心得:

1. Dart/Flutter - 扩展方法(ExtensionMethod)

目前Dart-SDK-2.6.0及以上是可以正常使用的,但是在Flutter的Stable分支中是属于实验性的支持,因此我们需要做一下几个步骤的操作:

1.Flutter项目根目录创建一个analysis_options.yaml文件,然后添加以下内容到文件中。

1
2
3
analyzer:
enable-experiment:
- extension-methods

2.语法是怎么样的呢,其实很简单。

1
2
3
4
5
6
7
8
/// 字符串扩展方法
extension StringExtension on String{
/// 是否是电话号码
bool get isMobileNumber {
if(this?.isNotEmpty != true) return false;
return RegExp(r'^((13[0-9])|(14[5,7,9])|(15[^4])|(18[0-9])|(17[0,1,3,5,6,7,8])|(19)[0-9])\d{8}$').hasMatch(this);
}
}

3.怎么使用呢?那就更简单了,看实例!

1
2
3
void test(){
bool isMobileNumber= "电话号码".isMobileNumber;
}

2. Flutter渲染、State原理和优化

渲染方面基本的理解

优化之前我们先来介绍下Flutter的渲染原理,通过这部分基础了解渲染流程以及主要耗时花费。

Flutter视图树包含了三颗树:Widget、Element、RenderObject

Widget: 存放渲染内容、它只是一个配置数据结构,创建是非常轻量的,在页面刷新的过程中随时会重建

Element: 同时持有Widget和RenderObject,存放上下文信息,通过它来遍历视图树,支撑UI结构

RenderObject: 根据Widget的布局属性进行layout,paint ,负责真正的渲染

从创建到渲染的大体流程是:根据Widget生成Element,然后创建相应的RenderObject并关联到Element.renderObject属性上,最后再通过RenderObject来完成布局排列和绘制。

例如下面这段布局代码

1
2
3
4
5
6
7
8
9
Container(
color: Colors.blue,
child: Row(
children: <Widget>[
Image.asset('image'),
Text('text'),
],
),
);

对应三棵树的结构如下图

了解了这三棵树,我们再来看下页面刷新的时候具体做了哪些操作

当需要更新UI的时候,Framework通知Engine,Engine会等到下个Vsync信号到达的时候,会通知Framework进行animate, build,layout,paint,最后生成layer提交给Engine。Engine会把layer进行组合,生成纹理,最后通过Open Gl接口提交数据给GPU, GPU经过处理后在显示器上面显示,如下图:

结合前面的例子,如果text文本或者image内容发生变化会触发哪些操作呢?

Widget是不可改变,需要重新创建一颗新树,build开始,然后对上一帧的element树做遍历,调用他的updateChild,看子节点类型跟之前是不是一样,不一样的话就把子节点扔掉,创造一个新的,一样的话就做内容更新,对renderObject做updateRenderObject操作,updateRenderObject内部实现会判断现在的节点跟上一帧是不是有改动,有改动才会别标记dirty,重新layout、paint,再生成新的layer交给GPU,流程如下图:

Widget中的State到底是什么?

Widget 有 StatelessWidget 和 StatefulWidget 两种类型。StatefulWidget 应对有交互、需要动态变化视觉效果的场景,而 StatelessWidget 则用于处理静态的、无状态的视图展示。StatefulWidget 的场景已经完全覆盖了 StatelessWidget,因此我们在构建界面时,往往会大量使用 StatefulWidget 来处理静态的视图展示需求,看起来似乎也没什么问题。

那么,StatelessWidget 存在的必要性在哪里?StatefulWidget 是否是 Flutter 中的万金油?

UI 编程范式

如果你有过原生系统(Android、iOS)或原生 JavaScript 开发经验的话,应该知道视图开发是命令式的,需要精确地告诉操作系统或浏览器用何种方式去做事情。比如,如果我们想要变更界面的某个文案,则需要找到具体的文本控件并调用它的控件方法命令,才能完成文字变更。
Flutter 的视图开发是声明式的,其核心设计思想就是将视图和数据分离
对我们来说,如果要实现同样的需求,则要稍微麻烦点:除了设计好 Widget 布局方案之外,还需要提前维护一套文案数据集,并为需要变化的 Widget 绑定数据集中的数据,使 Widget 根据这个数据集完成渲染。
但是,当需要变更界面的文案时,我们只要改变数据集中的文案数据,并通知 Flutter 框架触发 Widget 的重新渲染即可。这样一来,开发者将无需再精确关注 UI 编程中的各个过程细节,只要维护好数据集即可。比起命令式的视图开发方式需要挨个设置不同组件(Widget)的视觉属性,这种方式要便捷得多。
总结来说,命令式编程强调精确控制过程细节;而声明式编程强调通过意图输出结果整体。对应到 Flutter 中,意图是绑定了组件状态的 State,结果则是重新渲染后的组件。在 Widget 的生命周期内,应用到 State 中的任何更改都将强制 Widget 重新构建。
其中,对于组件完成创建后就无需变更的场景,状态的绑定是可选项。这里“可选”就区分出了 Widget 的两种类型,即:StatelessWidget 不带绑定状态,而 StatefulWidget 带绑定状态。当你所要构建的用户界面不随任何状态信息的变化而变化时,需要选择使用 StatelessWidget,反之则选用 StatefulWidget。前者一般用于静态内容的展示,而后者则用于存在交互反馈的内容呈现中。

StatefulWidget 不是万金油,要慎用
对于 UI 框架而言,同样的展示效果一般可以通过多种控件实现。从定义来看,StatefulWidget 仿佛是万能的,替代 StatelessWidget 看起来合情合理。于是 StatefulWidget 的滥用,也容易因此变得顺理成章,难以避免。
但事实是,StatefulWidget 的滥用会直接影响 Flutter 应用的渲染性能。
如果我们的根布局是一个 StatefulWidget,在其 State 中每调用一次更新 UI,都将是一整个页面所有 Widget 的销毁和重建。
在上面渲染方面有基本的理解,我们了解到,虽然 Flutter 内部通过 Element 层可以最大程度地降低对真实渲染视图的修改,提高渲染效率,而不是销毁整个 RenderObject 树重建。但,大量 Widget 对象的销毁重建是无法避免的。如果某个子 Widget 的重建涉及到一些耗时操作,那页面的渲染性能将会急剧下降
因此,正确评估你的视图展示需求,避免无谓的 StatefulWidget 使用,是提高 Flutter 应用渲染性能最简单也是最直接的手段。

优化措施:

  • 提高build效率,setState刷新数据尽量下发到底层节点,减少build层级深度。
  • 提高paint效率,RepaintBoundry创建单独layer减少重绘区域。
  • StatefulWidget使用在实际有改变的Widget。
  • 减少build中逻辑处理,因为widget在页面刷新的过程中随时会通过build重建,build调用频繁,我们应该只处理跟UI相关的逻辑
  • 减少saveLayer(ShaderMask、ColorFilter、Text Overflow)、clipPath的使用,saveLayer会在GPU中分配一块新的绘图缓冲区,切换绘图目标,这个操作是在GPU中非常耗时的,clipPath会影响每个绘图指令,做相交操作,之外的部分剔除掉,所以这也是个耗时操作
  • 减少Opacity Widget 使用,尤其是在动画中,因为他会导致widget每一帧都会被重建,可以用 AnimatedOpacity 或 FadeInImage 进行代替

3. 对于类中的属性和方法的定义规范的建议:

  • 不引用其他属性的成员,定义为属性
  • 引用其他属性,且不接收参数的成员,定义为getter bool get isEmptyString
  • 引用其他属性,且接受参数的成员,定义为function

iOS接入flutter问题

  1. flutter以module形式接入到iOS主工程的时候(project-setup),官网默认*.xcworkspace*.xcodeproj是同一目录下,changshuo的ios工程由于历史原因ChangShuo.xcworkspaceChangShuo.xcodeproj不在同一目录下,因此需要调整.iOS下的podhelp.rb脚本

    1
    flutter_export_environment_path = File.join(current_directory_pathname, 'flutter_export_environment.sh');
  2. flutter的plugin是依赖Cocoapods建立的,当plugin依赖自定义的私有pod库的时候,需要手工在podfile中添加对于私有库的依赖,例如需要对changshuo_logic_account进行调试就需要在changshuo_logic_account/example/Podfile中添加pod 'changshuo_ios_initialize'

  3. flutter在需要使用到原生的UIKit控件的时(由于功能还在测试中),需要在info.plist中添加io.flutter.embedded_views_preview为true

  4. flutter引擎官方在Appdelegate的didFinishLaunchingWithOptions中初始化,但是运用到实际项目中,这样并不准确,引擎接入到历史工程中的时候,应该在初始化在FlutterController的前一个页面来同时保证内存资源和加载效率。

  5. 使用 DecorationImage.ExactAssetImage加载图片的时候,并不会通过assets目录下的2x,3x加载,只会加载1x的图片

  6. dart里所有可定义的变量都是对象,包括int,double,enum这些简单的变量,这和Objective-C是有本质的区别,最直观的例子在OC中定义BOOL aBool;和dart定义bool aBool在OC中aBool未NO,在dart中为null

  7. dart中的泛型是编译期的真泛型,例如Map和Map是不同的类型,这2个类型做相互赋值的时候会抛异常

  8. flutter中的BuildContext context是一个由上到下的环境变量记录,并且不同的Navigator对应不同的context,所以InheritWidget是使用同一个页面的子child中,而不是用于2个页面的传值。

  9. dart语言是没有析构函数,只有在StatefulWidget的State中才存在dispose()(严格意义上也不是析构函数),所有再使用StreamController做释放操作的时候,只能依赖StatefulWidget。

  10. 遇到 Waiting for another flutter command to release the startup lock...的时候,表示非正常结束了上一个flutter应用,这个时候需要手工删除{flutter}/bin/cache/lockfile文件

Android进阶实践

Flutter环境配置

  • 安装flutter过程中可能会遇到从github上clone过程非常慢的现象[镜像包问题];团队多人开发为解决这种现象,我们可以先把github flutter 项目同步到gitee(码云)上(这一过程大概只需要20秒左右)再次gitee上同步下来就非常快了;
  • 另外安装完后在环境变量中务必配置一下国内镜像,要不然在更新flutter upgrade将会非常慢有时还会下载不下来:

    1
    2
    export PUB_HOSTED_URL=https://pub.flutter-io.cn
    export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn

创建Flutter工程与AndroidX迁移

  • 1.在创建flutter项目或模块时,务必将project name和package name填写好不要使用默认.往往在创建项目什么都没看很容易就行一直next,然后结束.当然一定要这样做也没什么问题,不足的之处就是回头再改一个比较麻烦另外很容易改不全;
    2.在finished之后最好勾选Use androidx.* artifacts;
  • 因flutter sdk相关引用包使用的是androidx模块,因此如果要对接flutter项目就要把项目迁移到androidx.官网提供的迁移步骤:https://www.bookstack.cn/read/flutter-1.2-zh/40336d41b348da21.md

Android Flutter引擎升级带来的问题

  • Flutter升级1.12后做了比较大的发动,相关参考链接:
    https://flutter.cn/docs/development/tools/sdk/release-notes/release-notes-1.12.13

    https://baijiahao.baidu.com/s?id=1657245790948868968&wfr=spider&for=pc
  • 升级之后原io.flutter.app包下的有些类已经被弃用改用io.flutter.embedding.android(如FlutterActivity)
  • 升级之后的项目需要在manimafest文件中添加配置:

    1
    2
    3
    <meta-data
    android:name="flutterEmbedding"
    android:value="2" />
  • FlutterActivity 设置透明问题

    1
    2
    3
    4
    5
    6
    7
    8
    9
    官方api有问题,BackgroundMode枚举并没有暴露出来
    FlutterActivity
    .withNewEngine()
    .backgroundMode(FlutterActivity.BackgroundMode.transparent).build(context)
    暂时可以通过下面代码来设置
    Intent intent = FlutterActivity.withNewEngine().initialRoute('route').build(activity);
    intent.putExtra("background_mode","transparent");
    activity.startActivity(intent);

Flutter包体积压缩

安装包体积决定了用户等待下载的时间和可能会耗费的流量,如何控制安装包体积,减小flutter产物的大小成为当务之急,那么如何减少包大小?
1.打包时通过增加参数:

1
./gradlew clean assembleRelease -Pextra-gen-snapshot-options="--dwarf_stack_traces,--print-snapshot-sizes,--obfuscate"

查看缩减方式:
1.执行以下命令导出arm架构和包的组成结构:
flutter –suppress-analytics build aot –output-dir=build/aot –target-platform=android –target=lib/main.dart –release –android-arch=arm64 –extra-gen-snapshot-options=”–dwarf_stack_traces,–print-snapshot-sizes,–print_instructions_sizes_to=build/aot.json”
2.使用dart命令将aot.json转为可视化网页:
dart ./bin/run_binary_size_analysis.dart build/aot.json path_to_webpage_dir

2.通过对flutter/native资源的压缩和共享
https://www.devio.org/2019/04/11/flutter-image-widget/
3.过滤掉不需要的cpu架构

1
./gradlew clean assembleRelease -Ptarget-platform="android-arm"

Flutter集成

  • flutter模块集成到工程项目中时最好在gradle添加source方式,如果按ant和脚本打包出来的产物进行集成可能会出现版本兼容或包的冲突问题:

    1
    2
    3
    flutter {
    source "flutter模块地址"
    }
  • 通过封装中间产物降低原项目代码的侵入性或耦合度.(可查看changshuo_android_initial文档)

Flutter引擎的启动速度

Android项目在启动flutter引擎时或首次打开flutter页面都会出现短暂的黑屏情况,这种情况我们要尽可能的去避免;

1
2
3
4
5
6
对于加载flutter页面方式:
1.manifest或xml layout注册方式;
2.FlutterActivity.createDefaultIntent(this);
3.FlutterActivity.withNewEngine();
4.flutterView.attachToFlutterEngine(new FlutterEngine(this));
5.FlutterActivity.withCachedEngine("engine id");

对于不同的沉浸方式建议withCachedEngine;flutter engin与页面状态是完全分离的,因此可以应用启动时即可在后面默默的启动并缓存好; 【这里需要做一定的封装与判断,直接使用可能导致资源释放不了问题,sdk内部对引擎缓存做了强引用处理.】

对技术前瞻性多些思考

希望大家对于我们的技术前瞻性多些思考。空余时间多学一些。

无论是 Weex、RN、小程序,还是 Flutter、SwiftUI,他们解决的问题是不一样的,本质更不同。到底哪个更优其实没有标准答案,不同的业务场景和业务所处的时间点,造成选择会不一样。“我们是工程师,并不是 Swift 或者 Flutter 工程师,我们选择的技术最终要为我们的业务和商业服务。”

“拥有一把锤子可以敲一个钉子,拥有一个工具箱可以造一艘航母”。

------本文结束感谢阅读------