路由管理
Flutter中的路由通俗的讲就是页面跳转。在Flutter中通过Navigator
组件管理路由导航。并提供了管理堆栈的方法。如:Navigator.push
和Navigator.pop
Flutter中给我们提供了两种配置路由跳转的方式:1、基本路由, 2、命名路由
普通路由使用
比如我们现在想从HomePage组件跳转到SearchPage组件。
1、需要在HomPage中引入SearchPage.dart
import '../SearchPage.dart';
2、在HomePage中通过下面方法跳转
Center(
child: ElevatedButton(
onPressed: () {
Navigator.push(context,
MaterialPageRoute(builder: (context) {
return const SearchPage();
}));
},
child: const Text("跳转到搜索页面"),
),
)
MaterialPageRoute
MaterialPageRoute
继承自PageRoute
类,PageRoute
类是一个抽象类,表示占有整个屏幕空间的一个模态路由页面,它还定义了路由构建及切换时过渡动画的相关接口及属性。MaterialPageRoute
是 Material组件库提供的组件,它可以针对不同平台,实现与平台页面切换动画风格一致的路由切换动画:
- 对于 Android,当打开新页面时,新的页面会从屏幕底部滑动到屏幕顶部;当关闭页面时,当前页面会从屏幕顶部滑动到屏幕底部后消失,同时上一个页面会显示到屏幕上。
- 对于 iOS,当打开页面时,新的页面会从屏幕右侧边缘一直滑动到屏幕左边,直到新页面全部显示到屏幕上,而上一个页面则会从当前屏幕滑动到屏幕左侧而消失;当关闭页面时,正好相反,当前页面会从屏幕右侧滑出,同时上一个页面会从屏幕左侧滑入。
下面我们介绍一下MaterialPageRoute 构造函数的各个参数的意义:
MaterialPageRoute({
WidgetBuilder builder,
RouteSettings settings,
bool maintainState = true,
bool fullscreenDialog = false,
})
builder
是一个WidgetBuilder
类型的回调函数,它的作用是构建路由页面的具体内容,返回值是一个widget。我们通常要实现此回调,返回新路由的实例。settings
包含路由的配置信息,如路由名称、是否初始路由(首页)。maintainState
:默认情况下,当入栈一个新路由时,原来的路由仍然会被保存在内存中,如果想在路由没用的时候释放其所占用的所有资源,可以设置maintainState
为false
。fullscreenDialog
表示新的路由页面是否是一个全屏的模态对话框,在 iOS 中,如果- fullscreenDialog为true,新页面将会从屏幕底部滑入(而不是水平方向)。
Navigator
Navigator
是一个路由管理的组件,它提供了打开和退出路由页方法。Navigator
通过一个栈来管理活动路由集合。通常当前屏幕显示的页面就是栈顶的路由。Navigator
提供了一系列方法来管理路由栈,在此我们只介绍其最常用的两个方法:
1. Future push(BuildContext context, Route route)
将给定的路由入栈(即打开新的页面),返回值是一个Future
对象,用以接收新路由出栈(即关闭)时的返回数据。
2. bool pop(BuildContext context, [ result ])
将栈顶路由出栈,result
为页面关闭时返回给上一个页面的数据。
Navigator
还有很多其他方法,如Navigator.replace
、Navigator.popUntil
等,详情请参考API文档或SDK 源码注释,在此不再赘述。
实例方法
Navigator
类中每个第一个参数为context
的静态方法都对应一个相同功能的实例方法, 比如Navigator.push(BuildContext context, Route route)
等价于Navigator.of(context).push(Route route)
。
普通路由跳转传值
路由跳转时,可以通过组件的构造函数直接传值,比如下面想从HomePage给SearchPage传参数
1、定义一个SearchPage接收传值
import 'package:flutter/material.dart';
class SearchPage extends StatefulWidget {
final String title;
const SearchPage({
super.key, this.title = "Search Page"
});
State < SearchPage > createState() => _SearchPageState();
}
class _SearchPageState extends State < SearchPage > {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
centerTitle: true,
),
body: const Center(
child: Text("组件居中"),
),
);
}
}
2、在跳转页面实现传值
Center(
child: ElevatedButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) {
return const SearchPage(title: "我是标题",);
})
);
},
child: const Text("跳转到搜索页面")
),
)
命名路由传值
官方文档:navigate-with-arguments
1、配置onGenerateRoute
import 'package:flutter/material.dart';
import './pages/tabs.dart';
import './pages/search.dart';
import './pages/form.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
MyApp({Key? key}) : super(key: key);
// 1、配置路由, 定义Map类型的routes, Key为String类型,value为Function类型
final Map<String, WidgetBuilder> routes = {
'/':(context)=>const Tabs(),
'/search':(context,{arguments})=> SearchPage(arguments:arguments),
'/login':(context)=>const LoginPage(),
};
// 2. 固定写法 统一处理
Route? onGenerateRoute(RouteSettings settings) {
final String? name = settings.name;
final Function? pageContentBuilder = routes[name];
if (pageContentBuilder != null) {
if (settings.arguments != null) {
return MaterialPageRoute(builder: (context) => pageContentBuilder(context, arguments: settings.arguments));
} else {
return MaterialPageRoute(builder: (context) => pageContentBuilder(context));
}
}
return null;
}
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
theme: ThemeData(primarySwatch: Colors.blue,),
initialRoute: '/',
//2、调用onGenerateRoute处理
onGenerateRoute: onGenerateRoute,
);
}
}
2、定义页面接收arguments传参
import 'package:flutter/material.dart';
class SearchPage extends StatefulWidget {
final Map arguments;
const SearchPage({super.key, required this.arguments}); // 构造函数接受参数
State<SearchPage> createState() => _SearchPageState();
}
class _SearchPageState extends State<SearchPage> {
void initState() {
super.initState();
print(widget.arguments); // 打印接受到的参数
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("我是搜索页面"),
),
);
}
}
3、在跳转页面实现传参
ElevatedButton(
onPressed: () {
Navigator.pushNamed(context, '/search', arguments: {
"title": "搜索页面",
});
},
child: const Text("打开搜索页面")
)
Navigator
除了pushNamed
方法,还有pushReplacementNamed
等其他管理命名路由的方法,可以自行查看API文档。
RouteSetting获取路由参数
也可以通过settings.arguments
获取路由参数,组件构造函数无需添加额外参数
class EchoRoute extends StatelessWidget {
Widget build(BuildContext context) {
//获取路由参数
var args=ModalRoute.of(context).settings.arguments;
//...省略无关代码
}
}
在打开路由时传递参数:
Navigator.of(context).pushNamed("new_page", arguments: "hi");
路由表
路由表的定义如下:
Map<String, WidgetBuilder> routes;
它是一个Map
,key
为路由的名字,是个字符串;value
是个builder
回调函数,用于生成相应的路由widget
。我们在通过路由名字打开新路由时,应用会根据路由名字在路由表中查找到对应的WidgetBuilder
回调函数,然后调用该回调函数生成路由widget
并返回。
注册路由表
直接看代码:
MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(primarySwatch: Colors.blue,),
// home:Tabs(),
initialRoute:"/", //名为"/"的路由作为应用的home(首页)
//注册路由表
routes:{
"new_page":(context) => NewRoute(),
"/":(context) => MyHomePage(title: 'Flutter Demo Home Page'), //注册首页路由
}
);
可以看到,如果想配置根路由页面,我们只需在路由表routes
中注册一下MyHomePage
路由,然后将其名字作为MaterialApp
的initialRoute
属性值即可,该属性决定应用的初始路由页是哪一个命名路由。这样就可以替代默认示例样板中的 home
参数来指定首页。
路由生成钩子
MaterialApp
有一个onGenerateRoute
属性,它在打开命名路由时可能会被调用,之所以说可能,是因为当调用Navigator.pushNamed(...)
打开命名路由时,如果指定的路由名在路由表中已注册,则会调用路由表中的builder
函数来生成路由组件;如果路由表中没有注册,才会调用onGenerateRoute
来生成路由。onGenerateRoute
回调签名如下:
Route<dynamic> Function(RouteSettings settings)
有了onGenerateRoute
回调,要实现上面控制页面权限的功能就非常容易:我们放弃使用路由表,取而代之的是提供一个onGenerateRoute
回调,然后在该回调中进行统一的权限控制,如:
MaterialApp(
... //省略无关代码
onGenerateRoute:(RouteSettings settings){
return MaterialPageRoute(builder: (context){
String routeName = settings.name;
// 如果访问的路由页需要登录,但当前未登录,则直接返回登录页路由,
// 引导用户登录;其他情况则正常打开路由。
}
);
}
);
这个函数可以用来做页面拦截器、用户权限判断等。
注意,onGenerateRoute
只会对命名路由生效。
在单独文件中统一配置路由表
我们可以把路由表和路由钩子函数统一配置到一个独立的dart文件中,方便管理和使用。
1、新建routers/routers.dart 配置路由
import 'package:flutter/material.dart';
// 1.配置路由
final Map<String, WidgetBuilder> routes = {
'/': (context) => const Tabs(),
'/form': (context) => const FormPage(),
'/product': (context) => const ProductPage(),
'/productinfo': (context, {arguments}) => ProductInfoPage(arguments: arguments),
'/search': (context, {arguments}) => SearchPage(arguments: arguments),
'/login': (context) => const LoginPage(),
'/registerFirst': (context) => const RegisterFirstPage(),
'/registerSecond': (context) => const RegisterSecondPage(),
'/registerThird': (context) => const RegisterThirdPage(),
};
// 2.onGenerateRoute
Route? onGenerateRoute(RouteSettings settings) {
// 统一处理
final String? name = settings.name;
final Function? pageContentBuilder = routes[name];
if (pageContentBuilder != null) {
if (settings.arguments != null) {
return MaterialPageRoute(builder: (context) => pageContentBuilder(context, arguments: settings.arguments));
} else {
return MaterialPageRoute(
builder: (context) => pageContentBuilder(context));
}
} else {
// 可以在这里添加全局跳转错误拦截处理页面
print("路由不存在");
return null;
}
}
然后使用的时候就可以这样:
import 'package:flutter/material.dart';
import 'routes/Routes.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return const MaterialApp(
initialRoute: '/', //初始化的时候加载的路由
onGenerateRoute: onGenerateRoute,
);
}
}
这是使用路由钩子的情况,如果不使用路由钩子,可以这样写:
MaterialApp(
// ...
initialRoute: "/",
routes: routes
);
路由返回
Navigator.of(context).pop();
路由返回传值给上一个页面
首先,在启动页面,主要使用await/async
来等待要打开的页面的返回结果,这是因为Navigator.pushNamed
返回的是一个Future
对象。
class RouterTestRoute extends StatelessWidget {
Widget build(BuildContext context) {
return Center(
child: ElevatedButton(
onPressed: () async {
// 打开`TipRoute`,并等待返回结果
var result = await Navigator.pushNamed(context, "tip_page", arguments: "初始参数");
var result = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return TipRoute(text: "我是提示xxxx"); // 路由参数
},
),
);
print("路由返回结果: $result");
},
child: Text("打开提示页"),
),
);
}
}
// MaterialApp 配置
MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(primarySwatch: Colors.blue,),
initialRoute:"/",
routes:{
"/":(context) => MyHomePage(title: 'Flutter Demo Home Page'),
"tip_page": (context) =>
TipRoute(title: '${ModalRoute.of(context)?.settings.arguments}'),
}
);
然后,在打开的路由页面中使用 Navigator.pop(context, result)
来返回值。
class TipRoute extends StatelessWidget {
final String title;
const TipRoute({Key? key, required this.title}) : super(key: key);
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("提示"),
),
body: Padding(
padding: const EdgeInsets.all(18),
child: Center(
child: Column(
children: <Widget>[
Text(title),
ElevatedButton(
onPressed: () => Navigator.pop(context, "我是返回值"),
child: const Text("返回"),
)
],
),
),
),
);
}
}
替换路由
比如我们从用户中心页面跳转到了registerFirst
页面,然后从registerFirst
页面通过
pushReplacementNamed
跳转到了registerSecond
页面。这个时候当我们点击registerSecond
的返回按钮的时候它会直接返回到用户中心。
Navigator.of(context).pushReplacementNamed('/registerSecond');
返回根路由
比如我们从用户中心跳转到registerFirst
页面,然后从registerFirst
页面跳转到registerSecond
页面,然后从registerSecond
跳转到了registerThird
页面。这个时候我们想的是registerThird
注册成功后返回到用户中心。 这个时候就用到了返回到根路由的方法。
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (BuildContext context) {
return const Tabs();
}), (route) => false);
Android 和Ios使用同样风格的路由跳转
Material组件库中提供了一个MaterialPageRoute组件,它可以使用和平台风格一致的路由切换动画,如在iOS上会左右滑动切换,而在Android上会上下滑动切换 , CupertinoPageRoute是Cupertino组件库提供的iOS风格的路由切换组件如果在Android上也想使用左右切换风格,可以使用CupertinoPageRoute。
1、routers.dart中引入cupertino.dart
import 'package:flutter/cupertino.dart';
2、MaterialPageRoute改为CupertinoPageRoute
import 'package:flutter/cupertino.dart';
import '../pages/tabs.dart';
import '../pages/shop.dart';
import '../pages/user/login.dart';
import '../pages/user/registerFirst.dart';
import '../pages/user/registerSecond.dart';
import '../pages/user/registerThird.dart';
//1、配置路由
Map routes = {
"/": (contxt) => const Tabs(),
"/login": (contxt) => const LoginPage(),
"/registerFirst": (contxt) => const RegisterFirstPage(),
"/registerSecond": (contxt) => const RegisterSecondPage(),
"/registerThird": (contxt) => const RegisterThirdPage(),
"/shop": (contxt, {arguments}) => ShopPage(arguments: arguments),
};
//2、配置onGenerateRoute 固定写法 这个方法也相当于一个中间件,这里可以做权限判断
var onGenerateRoute = (RouteSettings settings) {
final String? name = settings.name;
final Function? pageContentBuilder = routes[name];
Function = (contxt) { return const NewsPage()}
if (pageContentBuilder != null) {
if (settings.arguments != null) {
return CupertinoPageRoute(builder: (context) => pageContentBuilder(context, arguments: settings.arguments));
} else {
return CupertinoPageRoute(builder: (context) => pageContentBuilder(context));
}
}
return null;
};
路由观察器
路由观察器,可以监听所有路由跳转动作,首先创建一个类继承 NavigatorObserver
实现路由监听 :
class MyObserver extends NavigatorObserver {
void didPush(Route route, Route? previousRoute) {
super.didPush(route, previousRoute);
var currentName = route.settings.name;
var previousName =
previousRoute == null ? 'null' : previousRoute.settings.name;
if (kDebugMode) {
print('MyObserver-didPush-Current:$currentName Previous:$previousName');
}
}
void didPop(Route route, Route? previousRoute) {
super.didPop(route, previousRoute);
var currentName = route.settings.name;
var previousName =
previousRoute == null ? 'null' : previousRoute.settings.name;
if (kDebugMode) {
print('MyObserver-didPop--Current:$currentName Previous:$previousName');
}
}
}
然后在 MaterialApp
中配置 navigatorObservers
属性:
MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(primarySwatch: Colors.blue,),
initialRoute:"/",
routes:routes,
navigatorObservers: [MyObserver()], // 可以配多个
);
监听未注册的路由
MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(primarySwatch: Colors.blue,),
initialRoute:"/",
routes:routes,
// 在打开一个不存在的命名路由时会被调用, 调用顺序为onGenerateRoute ==> onUnknownRoute
onUnknownRoute: (RouteSettings settings){
String routeName = settings.name;
print('未注册的路由:$routeName');
},
);
包管理
除了常规的在 pub.dev上搜索库文件然后在pubspec.yaml
文件下添加外,还有以下几种方式:
- 依赖本地包:如果我们正在本地开发一个包,包名为pkg1,我们可以通过下面方式依赖
dependencies:
pkg1:
path: ../../code/pkg1
- 依赖Git:你也可以依赖存储在Git仓库中的包。如果软件包位于仓库的根目录中,请使用以下语法
dependencies:
pkg1:
git:
url: git://github.com/xxx/pkg1.git
上面假定包位于Git存储库的根目录中。如果不是这种情况,可以使用path参数指定相对位置,例如:
dependencies:
package1:
git:
url: git://github.com/flutter/packages.git
path: packages/package1
上面介绍的这些依赖方式是Flutter开发中常用的,但还有一些其他依赖方式,完整的内容读者可以自行查看:https://www.dartlang.org/tools/pub/dependencies 。
注意,不要添加错了位置,要添加到dependencies
后面,不要添加到了dev_dependencies
后面( 这个是配置开发环境依赖的工具包,而不是flutter应用本身依赖的包)。
配置完后执行界面上的 “Pub get” 提示,自动更新下载依赖包,或者在命令行手动执行 flutter packages get
也可以。
小技巧:
如果我们使用的插件,尤其是native插件,出现报错时(感谢Android混乱的SDK版本以及令人头晕AGP版本),最好的办法是到 pub.dev上搜索它的最新版本使用。
可是,当你找到插件库,打开页面一看:Published 24 months ago … 好家伙,已经超过2年没更新了。。。
这时就会很尴尬,该怎么办呢,有两种办法:
- 1)可以在 pub.dev 上搜索类似的插件,比如假设你之前使用的是
flutter_webview_plugin
,那么现在这个库作者还没有更新,我们可以搜索关键字webview
,这样就能找到类似的库,初步只要看两个指标:它的发布时间和POPULARITY
或LIKES
指数,发布越近、POPULARITY
指数越高越好,因为往往最新的库会修复以前的bug,并且喜欢指数越高的说明问题较少兼容性较好。 - 2)如果第一种方法没有解决你的问题,或者找到的库有新的兼容问题存在,那么你可以到原来出现问题的库的Github主页上面的
issues
中搜索,看回复评论较多的,有些国外大神们往往会留下自己暂时解决问题的fork
版本地址,你可以尝试使用它(通过上面的Git依赖的配置方式)。 - 3)如果找遍整个世界,都没有找到你想要的,但是假如你的团队中有专业的Android和iOS开发人员的话,那么可以把原始库的源码下载下来自己团队去修复和维护一个版本。
- 4)以上都没有解决,那我只能 deeply sorry for that,Leave it to God!
资源管理
可以在在pubspec.yaml
文件配置存放图片、字体等资源文件
assets:
- assets/
- images/ic_timg.jpg
- images/avatar.png
- images/bg.jpeg
fonts:
- family: myIcon #指定一个字体名
fonts:
- asset: fonts/iconfont.ttf
这里的目录是与pubspec.yaml
文件同级的,一般就是根目录。
工程里需要用到的图片比较多,有时不想一个一个添加,太麻烦,可使用下面方式批量添加:
assets: [images/]
asset 变体
构建过程支持“asset变体”的概念:不同版本的 asset
可能会显示在不同的上下文中。 在pubspec.yaml
的assets
部分中指定 asset
路径时,构建过程中,会在相邻子目录中查找具有相同名称的任何文件。这些文件随后会与指定的 asset
一起被包含在 asset bundle
中。
例如,如果应用程序目录中有以下文件:
…/pubspec.yaml
…/graphics/background.png
…/graphics/dark/background.png
…
然后pubspec.yaml
文件中只需包含:
flutter:
assets:
- graphics/background.png
那么这两个graphics/background.png
和graphics/dark/background.png
都将包含在您的 asset bundle
中。前者被认为是 main asset
(主资源),后者被认为是一种变体(variant
)。
在选择匹配当前设备分辨率的图片时,Flutter会使用到 asset
变体。
加载 assets
您的应用可以通过AssetBundle
对象访问其 asset
。有两种主要方法允许从 Asset bundle
中加载字符串或图片(二进制)文件。
1. 加载文本assets
- 通过
rootBundle
对象加载:每个Flutter应用程序都有一个rootBundle
对象, 通过它可以轻松访问主资源包,直接使用package:flutter/services.dart
中全局静态的rootBundle
对象来加载asset
即可。 - 通过
DefaultAssetBundle
加载:建议使用DefaultAssetBundle
来获取当前BuildContext
的AssetBundle
。 这种方法不是使用应用程序构建的默认asset bundle
,而是使父级widget
在运行时动态替换的不同的AssetBundle
,这对于本地化或测试场景很有用。
通常,可以使用DefaultAssetBundle.of()
在应用运行时来间接加载 asset
(例如JSON文件),而在 widget
上下文之外,或其他AssetBundle
句柄不可用时,可以使用rootBundle
直接加载这些 asset
,例如:
import 'dart:async' show Future;
import 'package:flutter/services.dart' show rootBundle;
Future<String> loadAsset() async {
return await rootBundle.loadString('assets/config.json');
}
2. 加载图片
1)声明分辨率相关的图片 assets
AssetImage
可以将asset
的请求逻辑映射到最接近当前设备像素比例(dpi)的asset
。为了使这种映射起作用,必须根据特定的目录结构来保存asset
:
…/image.png
…/Mx/image.png
…/Nx/image.png
…
其中 M
和 N
是数字标识符,对应于其中包含的图像的分辨率,也就是说,它们指定不同设备像素比例的图片。
主资源默认对应于1.0
倍的分辨率图片。看一个例子:
…/my_icon.png
…/2.0x/my_icon.png
…/3.0x/my_icon.png
在设备像素比率为1.8
的设备上,.../2.0x/my_icon.png
将被选择。对于2.7
的设备像素比率,.../3.0x/my_icon.png
将被选择。
如果未在Image
widget上指定渲染图像的宽度和高度,那么Image
widget将占用与主资源相同的屏幕空间大小。 也就是说,如果.../my_icon.png
是72px
x72px
,那么.../3.0x/my_icon.png
应该是216px
x216px
; 但如果未指定宽度和高度,它们都将渲染为72px
x72px
(以逻辑像素为单位)。
pubspec.yaml
中asset
部分中的每一项都应与实际文件相对应,但主资源项除外。当主资源缺少某个资源时,会按分辨率从低到高的顺序去选择 ,也就是说1x
中没有的话会在2x
中找,2x
中还没有的话就在3x
中找。
2)加载图片
要加载图片,可以使用 AssetImage
类。例如,我们可以从上面的asset
声明中加载背景图片:
Widget build(BuildContext context) {
return DecoratedBox(
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage('graphics/background.png'),
),
),
);
}
注意,AssetImage
并非是一个widget
, 它实际上是一个ImageProvider
,有些时候你可能期望直接得到一个显示图片的widget
,那么你可以使用Image.asset()
方法,如:
Widget build(BuildContext context) {
return Image.asset('graphics/background.png');
}
使用默认的 asset bundle
加载资源时,内部会自动处理分辨率等,这些处理对开发者来说是无感知的。 (如果使用一些更低级别的类,如 ImageStream
或 ImageCache
时你会注意到有与缩放相关的参数)
3)依赖包中的资源图片
要加载依赖包中的图像,必须给AssetImage
提供package
参数。
例如,假设您的应用程序依赖于一个名为“my_icons
”的包,它具有如下目录结构:
…/pubspec.yaml
…/icons/heart.png
…/icons/1.5x/heart.png
…/icons/2.0x/heart.png
…
然后加载图像,使用:
AssetImage('icons/heart.png', package: 'my_icons')
或者:
Image.asset('icons/heart.png', package: 'my_icons')
注意:包在使用本身的资源时也应该加上package参数来获取。
打包 assets
如果在pubspec.yaml
文件中声明了期望的资源,它将会打包到相应的package
中。特别是,包本身使用的资源必须在pubspec.yaml
中指定。
包也可以选择在其lib
文件夹中包含未在其pubspec.yaml
文件中声明的资源。在这种情况下,对于要打包的图片,应用程序必须在pubspec.yaml
中指定包含哪些图像。 例如,一个名为“fancy_backgrounds
”的包,可能包含以下文件:
…/lib/backgrounds/background1.png
…/lib/backgrounds/background2.png
…/lib/backgrounds/background3.png
要包含第一张图像,必须在pubspec.yaml
的assets
部分中声明它:
flutter:
assets:
- packages/fancy_backgrounds/backgrounds/background1.png
lib/
是隐含的,所以它不应该包含在资产路径中。
特定平台的 assets 使用
上面的资源都是flutter应用中的,这些资源只有在Flutter框架运行之后才能使用,如果要给我们的应用设置APP图标或者添加启动图,那我们必须使用特定平台的assets。
1)设置APP图标
更新Flutter应用程序启动图标的方式与在本机Android或iOS应用程序中更新启动图标的方式相同。
-
Android
在 Flutter 项目的根目录中,导航到
../android/app/src/main/res
目录,里面包含了各种资源文件夹(如mipmap-hdpi
已包含占位符图像 “ic_launcher.png
”,见下图)。 只需按照Android开发人员指南中的说明, 将其替换为所需的资源,并遵守每种屏幕密度(dpi)的建议图标大小标准。
注意: 如果您重命名.png
文件,则还必须在您AndroidManifest.xml
的<application>
标签的android:icon
属性中更新名称。 -
iOS
在Flutter项目的根目录中,导航到
../ios/Runner
。该目录中Assets.xcassets/AppIcon.appiconset
已经包含占位符图片(见下图), 只需将它们替换为适当大小的图片,保留原始文件名称。
2)更新启动页
在 Flutter 框架加载时,Flutter 会使用本地平台机制绘制启动页。此启动页将持续到Flutter渲染应用程序的第一帧时。
注意: 这意味着如果您不在应用程序的
main()
方法中调用runApp
函数 (或者更具体地说,如果您不调用window.render
去响应window.onDrawFrame
)的话, 启动屏幕将永远持续显示。
-
Android
要将启动屏幕(splash screen)添加到您的Flutter应用程序, 请导航至
../android/app/src/main
。在res/drawable/launch_background.xml
,通过自定义drawable
来实现自定义启动界面(你也可以直接换一张图片)。 -
iOS
要将图片添加到启动屏幕(splash screen)的中心,请导航至
../ios/Runner
。在Assets.xcassets/LaunchImage.imageset
, 拖入图片,并命名为LaunchImage.png、LaunchImage@2x.png、LaunchImage@3x.png
。 如果你使用不同的文件名,那您还必须更新同一目录中的Contents.json
文件,图片的具体尺寸可以查看苹果官方的标准。您也可以通过打开
Xcode
完全自定义storyboard
。在Project Navigator
中导航到Runner/Runner
然后通过打开Assets.xcassets
拖入图片,或者通过在LaunchScreen.storyboard
中使用Interface Builder
进行自定义,如图所示。
平台共享资源
官方文档:sharing-assets-with-the-underlying-platform
如果我们采用的是Flutter+原生的开发模式,那么可能会存Flutter和原生需要共享资源的情况,比如Flutter项目中已经有了一张图片A,如果原生代码中也要使用A,我们可以将A拷贝一份到原生项目的特定目录,这样的话虽然功能可以实现,但是最终的应用程序包会变大,因为包含了重复的资源,为了解决这个问题,Flutter 提供了一种Flutter和原生之间共享资源的方式。
1.在 Android 中加载 Flutter 资源文件
在 Android 平台上,assets
通过 AssetManager
API 读取。通过 PluginRegistry.Registrar
的 lookupKeyForAsset
方法,或者 FlutterView
的 getLookupKeyForAsset
方法来获取文件路径,然后 AssetManager
的 openFd
根据文件路径得到文件描述符。开发插件时可以使用 PluginRegistry.Registrar
,而开发应用程序使用平台视图时,FlutterView
是最好的选择。
举个例子,假设你在 pubspec.yaml
中这样指定:
flutter:
assets:
- icons/heart.png
在你的 Flutter 应用程序对应以下结构。
.../pubspec.yaml
.../icons/heart.png
...etc.
想要在 Java
插件中访问 icons/heart.png
:
AssetManager assetManager = registrar.context().getAssets();
String key = registrar.lookupKeyForAsset("icons/heart.png");
AssetFileDescriptor fd = assetManager.openFd(key);
2.在 iOS 中加载 Flutter 资源文件
在 iOS 平台上,assets
资源文件通过 mainBundle
读取。通过 pathForResource:ofType:
的 lookupKeyForAsset
或者 lookupKeyForAsset:fromPackage:
方法获取文件路径,同样,FlutterViewController
的 lookupKeyForAsset:
或者 lookupKeyForAsset:fromPackage:
方法也可以获取文件路径。开发插件时可以使用 FlutterPluginRegistrar
,而开发应用程序使用平台视图时, FlutterViewController
是最好的选择。
举个例子,假设你的 Flutter 配置和上面一样。
要在 Objective-C 插件中访问 icons/heart.png
:
NSString* key = [registrar lookupKeyForAsset:@"icons/heart.png"];
NSString* path = [[NSBundle mainBundle] pathForResource:key ofType:nil];
这有一个更完整的实例可以理解 Flutter 的应用:video_player plugin。
pub.dev 上的 ios_platform_images
plugin 将这些逻辑封装成方便的类别。它允许编写:
Objective-C:
[UIImage flutterImageWithName:@"icons/heart.png"];
Swift:
UIImage.flutterImageNamed("icons/heart.png")
使用第三方 Icons 图标资源
这里推荐两个比较好用的Icons图标资源网站,可能做前端开发的人已经比较熟悉了,它们是:
- iconfont:https://www.iconfont.cn/ 可以搜索自己想要的图标,然后下载下来(需要登录),里面会包含各种格式,导入Flutter中使用即可。
- fluttericon:https://fluttericon.com/ 这个是专门为Flutter设计的,提供了很多 Material Design 风格的图标,直接在页面中选择喜欢的图标后点击顶部的DOWNLOAD按钮下载即可,下载文件中会同时包含
Dart
使用示例代码,
注意,虽然这两个网站提供了其他格式的图标,但是尽量选择ttf字体图标,因为可以动态设置字体图标的颜色,不然当需要更改应用主题颜色时就会比较尴尬了。当然如果你选择使用普通png图片做图标的话,自己就别折腾了,还是让设计切图吧。
调试Flutter应用
常规调试手段总结:
- 巧用断点调试
- Debugger面板
- 善用变量Variables视窗与观察Watchers视窗
- 善用控制台(Console)进行log分析
- 使用 Dart DevTools
- 使用 Flutter Inspector 诊断布局问题(不清楚现有布局)
- 使用 Flutter Outline
日志与断点
1. debugger() 声明
当使用Dart Observatory
(或另一个Dart
调试器,例如IntelliJ IDE中的调试器)时,可以使用该debugger()
语句插入编程式断点。要使用这个,你必须添加import 'dart:developer';
到相关文件顶部。
debugger()
语句采用一个可选when
参数,我们可以指定该参数仅在特定条件为真时中断,如下所示:
void someFunction(double offset) {
debugger(when: offset > 30.0);
// ...
}
2. print、debugPrint、flutter logs
Dart print()
功能将输出到系统控制台,我们可以使用flutter logs来查看它(基本上是一个包装adb logcat)。
如果你一次输出太多,那么Android有时会丢弃一些日志行。为了避免这种情况,我们可以使用Flutter的foundation
库中的debugPrint()
(需要导入flutter/foundation包),它封装了 print,将一次输出的内容长度限制在一个级别(内容过多时会分批输出),避免被Android内核丢弃。
还可以根据kDebugMode
和kReleaseMode
来决定是否只在debug/release
模式下输出日志:
import 'package:flutter/foundation.dart';
if (kDebugMode) print("只在Debug模式下输出");
if (kReleaseMode) print("只在Release模式下输出");
debugPrint("AAA");
Flutter框架中的许多类都有toString
实现,按照惯例,输出信息通过包括对象的运行时类型 、类名以及关键字段等信息。 树中的一些类也具有toStringDeep
实现,从该点返回整个子树的多行描述。一些具有详细信息toString
的类会实现一个toStringShort
,它只返回对象的类型或其他非常简短的(一个或两个单词)描述。
3. 调试模式断言
在Flutter应用调试过程中,Dart assert
语句被启用,并且 Flutter 框架使用它来执行许多运行时检查来验证是否违反一些不可变的规则。当一个某个规则被违反时,就会在控制台打印错误日志,并带上一些上下文信息来帮助追踪问题的根源。
要关闭调试模式并使用发布模式,请使用flutter run --release
运行我们的应用程序。 这也关闭了Observatory
调试器。一个中间模式可以关闭除Observatory
之外所有调试辅助工具的,称为“profile mode
”,用--profile
替代--release
即可。
4. 断点
开发过程中,断点是最实用的调试工具之一,我们以 Android Studio 为例:
我们在 93 行打了一个断点,一旦代码执行到这一行就会暂停,这时我们可以看到当前上下文所有变量的值,然后可以选择一步一步的执行代码。关于如何通过 IDE 来打断点,可以自行搜索网上教程很多。
Flutter 调试中的后悔药:
- 通过Frames回退
- 通过Drop Frame回退
调试应用程序层
Flutter框架的每一层都提供了将其当前状态或事件转储(dump
)到控制台(使用debugPrint
)的功能。
1. Widget 树调试 debugDumpApp()
要转储Widgets
树的状态,请调用debugDumpApp()
。 只要应用程序已经构建了至少一次(即在调用build()
之后的任何时间),我们可以在应用程序未处于构建阶段(即不在build()
方法内调用 )的任何时间调用此方法(在调用runApp()
之后)。
如, 这个应用程序:
import 'package:flutter/material.dart';
void main() {
runApp(
MaterialApp(
home: AppHome(),
),
);
}
class AppHome extends StatelessWidget {
Widget build(BuildContext context) {
return Material(
child: Center(
child: TextButton(
onPressed: () {
debugDumpApp();
},
child: Text('Dump App'),
),
),
);
}
}
会输出这样的内容:
I/flutter ( 6559): WidgetsFlutterBinding - CHECKED MODE
I/flutter ( 6559): RenderObjectToWidgetAdapter<RenderBox>([GlobalObjectKey RenderView(497039273)]; renderObject: RenderView)
I/flutter ( 6559): └MaterialApp(state: _MaterialAppState(1009803148))
I/flutter ( 6559): └ScrollConfiguration()
I/flutter ( 6559): └AnimatedTheme(duration: 200ms; state: _AnimatedThemeState(543295893; ticker inactive; ThemeDataTween(ThemeData(Brightness.light Color(0xff2196f3) etc...) → null)))
I/flutter ( 6559): └Theme(ThemeData(Brightness.light Color(0xff2196f3) etc...))
I/flutter ( 6559): └WidgetsApp([GlobalObjectKey _MaterialAppState(1009803148)]; state: _WidgetsAppState(552902158))
I/flutter ( 6559): └CheckedModeBanner()
I/flutter ( 6559): └Banner()
I/flutter ( 6559): └CustomPaint(renderObject: RenderCustomPaint)
I/flutter ( 6559): └DefaultTextStyle(inherit: true; color: Color(0xd0ff0000); family: "monospace"; size: 48.0; weight: 900; decoration: double Color(0xffffff00) TextDecoration.underline)
I/flutter ( 6559): └MediaQuery(MediaQueryData(size: Size(411.4, 683.4), devicePixelRatio: 2.625, textScaleFactor: 1.0, padding: EdgeInsets(0.0, 24.0, 0.0, 0.0)))
I/flutter ( 6559): └LocaleQuery(null)
I/flutter ( 6559): └Title(color: Color(0xff2196f3))
... #省略剩余内容
这是一个“扁平化”的树,显示了通过各种构建函数投影的所有widget
(如果你在widget树的根中调用toStringDeepwidget
,这是你获得的树)。 你会看到很多在你的应用源代码中没有出现的widget
,因为它们是被框架中widget
的build()
函数插入的。例如,InkFeature
是Material widget
的一个实现细节 。
当按钮从被按下变为被释放时debugDumpApp()
被调用,TextButton
对象同时调用setState()
,并将自己标记为"dirty
"。我们还可以查看已注册了哪些手势监听器; 在这种情况下,一个单一的GestureDetector
被列出,并且监听“tap
”手势(“tap”是TapGestureDetector
的toStringShort
函数输出的)。
如果我们编写自己的widget
,则可以通过覆盖debugFillProperties()
来添加信息。 将DiagnosticsProperty
对象作为方法参数,并调用父类方法。 该函数是该toString
方法用来填充小部件描述信息的。
2. 渲染树调试 debugDumpRenderTree()
如果我们尝试调试布局问题,那么Widget树可能不够详细。在这种情况下,我们可以通过调用debugDumpRenderTree()
转储渲染树。 正如debugDumpApp()
,除布局或绘制阶段外,我们可以随时调用此函数。作为一般规则,从frame
回调或事件处理器中调用它是最佳解决方案。
要调用debugDumpRenderTree()
,我们需要添加import'package:flutter/rendering.dart';
到我们的源文件。
上面这个小例子的输出结果如下所示:
I/flutter ( 6559): RenderView
I/flutter ( 6559): │ debug mode enabled - android
I/flutter ( 6559): │ window size: Size(1080.0, 1794.0) (in physical pixels)
I/flutter ( 6559): │ device pixel ratio: 2.625 (physical pixels per logical pixel)
I/flutter ( 6559): │ configuration: Size(411.4, 683.4) at 2.625x (in logical pixels)
I/flutter ( 6559): │
I/flutter ( 6559): └─child: RenderCustomPaint
I/flutter ( 6559): │ creator: CustomPaint ← Banner ← CheckedModeBanner ←
I/flutter ( 6559): │ WidgetsApp-[GlobalObjectKey _MaterialAppState(1009803148)] ←
I/flutter ( 6559): │ Theme ← AnimatedTheme ← ScrollConfiguration ← MaterialApp ←
I/flutter ( 6559): │ [root]
I/flutter ( 6559): │ parentData: <none>
I/flutter ( 6559): │ constraints: BoxConstraints(w=411.4, h=683.4)
I/flutter ( 6559): │ size: Size(411.4, 683.4)
... # 省略
这是根RenderObject
对象的toStringDeep
函数的输出。
当调试布局问题时,关键要看的是size
和constraints
字段。约束沿着树向下传递,尺寸向上传递。
如果我们编写自己的渲染对象,则可以通过覆盖debugFillProperties()
将信息添加到转储。 将DiagnosticsProperty
对象作为方法的参数,并调用父类方法。
3. Layer树调试 debugDumpLayerTree()
渲染树是可以分层的,而最终绘制需要将不同的层合成起来,而Layer
则是绘制时需要合成的层,如果我们尝试调试合成问题,则可以使用debugDumpLayerTree()
。对于上面的例子,它会输出:
I/flutter : TransformLayer
I/flutter : │ creator: [root]
I/flutter : │ offset: Offset(0.0, 0.0)
I/flutter : │ transform:
I/flutter : │ [0] 3.5,0.0,0.0,0.0
I/flutter : │ [1] 0.0,3.5,0.0,0.0
I/flutter : │ [2] 0.0,0.0,1.0,0.0
I/flutter : │ [3] 0.0,0.0,0.0,1.0
I/flutter : │
I/flutter : ├─child 1: OffsetLayer
I/flutter : │ │ creator: RepaintBoundary ← _FocusScope ← Semantics ← Focus-[GlobalObjectKey MaterialPageRoute(560156430)] ← _ModalScope-[GlobalKey 328026813] ← _OverlayEntry-[GlobalKey 388965355] ← Stack ← Overlay-[GlobalKey 625702218] ← Navigator-[GlobalObjectKey _MaterialAppState(859106034)] ← Title ← ⋯
I/flutter : │ │ offset: Offset(0.0, 0.0)
I/flutter : │ │
I/flutter : │ └─child 1: PictureLayer
I/flutter : │
I/flutter : └─child 2: PictureLayer
这是根Layer
的toStringDeep
输出的。
根部的变换是应用设备像素比的变换; 在这种情况下,每个逻辑像素代表3.5个设备像素。
RepaintBoundary
widget在渲染树的层中创建了一个RenderRepaintBoundary
。这用于减少需要重绘的需求量。
4. 语义树调试 debugDumpSemanticsTree()
我们还可以调用debugDumpSemanticsTree()
获取语义树(呈现给系统可访问性API的树)的转储。 要使用此功能,必须首先启用辅助功能,例如启用系统辅助工具或SemanticsDebugger
。
对于上面的例子,它会输出:
I/flutter : SemanticsNode(0; Rect.fromLTRB(0.0, 0.0, 411.4, 683.4))
I/flutter : ├SemanticsNode(1; Rect.fromLTRB(0.0, 0.0, 411.4, 683.4))
I/flutter : │ └SemanticsNode(2; Rect.fromLTRB(0.0, 0.0, 411.4, 683.4); canBeTapped)
I/flutter : └SemanticsNode(3; Rect.fromLTRB(0.0, 0.0, 411.4, 683.4))
I/flutter : └SemanticsNode(4; Rect.fromLTRB(0.0, 0.0, 82.0, 36.0); canBeTapped; "Dump App")
5. 调度
要找出相对于帧的开始/结束事件发生的位置,可以切换debugPrintBeginFrameBanner
和debugPrintEndFrameBanner
布尔值以将帧的开始和结束打印到控制台。
例如:
I/flutter : ▄▄▄▄▄▄▄▄ Frame 12 30s 437.086ms ▄▄▄▄▄▄▄▄
I/flutter : Debug print: Am I performing this work more than once per frame?
I/flutter : Debug print: Am I performing this work more than once per frame?
I/flutter : ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
debugPrintScheduleFrameStacks
还可以用来打印导致当前帧被调度的调用堆栈。
6. 可视化调试
我们也可以通过设置debugPaintSizeEnabled
为true
以可视方式调试布局问题。 这是来自rendering
库的布尔值。它可以在任何时候启用,并在为true
时影响绘制。 设置它的最简单方法是在void main()
的顶部设置。
当它被启用时,所有的盒子都会得到一个明亮的深青色边框,padding
(来自widget
如Padding
)显示为浅蓝色,子widget
周围有一个深蓝色框, 对齐方式(来自widget
如Center
和Align
)显示为黄色箭头. 空白(如没有任何子节点的Container
)以灰色显示。
debugPaintBaselinesEnabled
做了类似的事情,但对于具有基线的对象,文字基线以绿色显示,表意(ideographic
)基线以橙色显示。
debugPaintPointersEnabled
标志打开一个特殊模式,任何正在点击的对象都会以深青色突出显示。 这可以帮助我们确定某个对象是否以某种不正确的方式进行hit
测试(Flutter检测点击的位置是否有能响应用户操作的widget
),例如,如果它实际上超出了其父项的范围,首先不会考虑通过hit
测试。
如果我们尝试调试合成图层,例如以确定是否以及在何处添加RepaintBoundary
widget,则可以使用debugPaintLayerBordersEnabled
标志, 该标志用橙色或轮廓线标出每个层的边界,或者使用debugRepaintRainbowEnabled
标志, 只要他们重绘时,这会使该层被一组旋转色所覆盖。
所有这些标志只能在调试模式下工作。通常,Flutter框架中以“debug...
” 开头的任何内容都只能在调试模式下工作。
7. 调试动画
调试动画最简单的方法是减慢它们的速度。为此,请将timeDilation
变量(在scheduler
库中)设置为大于1.0
的数字,例如50.0
。 最好在应用程序启动时只设置一次。如果我们在运行中更改它,尤其是在动画运行时将其值改小,则在观察时可能会出现倒退,这可能会导致断言命中,并且这通常会干扰我们的开发工作。
8. 调试性能问题
要了解我们的应用程序导致重新布局或重新绘制的原因,我们可以分别设置debugPrintMarkNeedsLayoutStacks
和 debugPrintMarkNeedsPaintStacks
标志。 每当渲染盒被要求重新布局和重新绘制时,这些都会将堆栈跟踪记录到控制台。如果这种方法对我们有用,我们可以使用services
库中的debugPrintStack()
方法按需打印堆栈痕迹。
9. 统计应用启动时间
要收集有关Flutter应用程序启动所需时间的详细信息,可以在运行flutter run
时使用trace-startup
和profile
选项。
$ flutter run --trace-startup --profile
跟踪输出保存为start_up_info.json
,在Flutter工程目录在build
目录下。输出列出了从应用程序启动到这些跟踪事件(以微秒捕获)所用的时间:
- 进入Flutter引擎时.
- 展示应用第一帧时.
- 初始化Flutter框架时.
- 完成Flutter框架初始化时.
如 :
{
"engineEnterTimestampMicros": 96025565262,
"timeToFirstFrameMicros": 2171978,
"timeToFrameworkInitMicros": 514585,
"timeAfterFrameworkInitMicros": 1657393
}
10. 跟踪Dart代码性能
要执行自定义性能跟踪和测量Dart
任意代码段的wall/CPU
时间(类似于在Android
上使用systrace
)。 使用dart:developer
的Timeline
工具来包含你想测试的代码块,例如:
Timeline.startSync('interesting function');
// iWonderHowLongThisTakes();
Timeline.finishSync();
然后打开你应用程序的Observatory timeline
页面,在“Recorded Streams
”中选择‘Dart
’复选框,并执行你想测量的功能。
刷新页面将在Chrome的跟踪工具中显示应用按时间顺序排列的timeline
记录。
请确保运行flutter run
时带有--profile
标志,以确保运行时性能特征与我们的最终产品差异最小。
Dart DevTools 使用
Tools->Flutter->Open Dart DevTools 首次运行会先自动安装,点击debug
运行后在控制台中有个 Open DevTools 的蓝色小图标,点击会在浏览器打开Dart DevTools。
- 选中
select widget mode
后,点击手机上的控件可以进入选中的控件的调试页面 - 当选中某个控件后点击
layout explorer
之后可以调试 - 如果是
flex
布局如Row
Column
等,在layout explorer
可点击主轴和交叉轴的对齐方式,手机界面会实时的根据选中结果变化效果 - 如果报错可能是Widget组件没有指定
textDirection: TextDirection.ltr
导致
Flutter 在 Android Studio中如何运行除了main.dart
以外的dart
文件(包含main
函数),执行命令:
flutter run lib/animated_list.dart
Flutter 运行快捷键命令:
- r:热重新加载。
- R:热重启。
- h:重复此帮助信息。
- d:分离(终止“颤动运行”,但保持应用程序运行)。
- c:清除屏幕。
- q:退出(终止设备上的应用程序)。
一般最常用的就是热加载输入:r
Flutter 创建应用:
- 在终端中运行:
flutter create xxx
快速创建一个flutter应用模板
Flutter Outline面板顶部有一排按钮可以使点击的控件被快速包裹进某个常用布局容器中 或者选中控件右键。
DevTools提供了很多很全面的功能,更多内容可以参考其官网:DevTools (点击该页面左侧的目录了解更多)
Flutter的四种运行模式
Flutter有四种运行模式:Debug、Release、Profile和Test,这四种模式在build的时候是完全独立的。
-
Debug:Debug模式可以在真机和模拟器上同时运行:会打开所有的断言,包括debugging信息、debugger aids(比如observatory)和服务扩展。优化了快速develop/run循环,但是没有优化执行速度、二进制大小和部署。运行命令:
flutter run
,通过sky/tools/gn --android
或者sky/tools/gn --ios
来build
。有时候也被叫做“checked
模式”或者“slow
模式”。 -
Release:Release模式只能在真机上运行,不能在模拟器上运行:会关闭所有断言和debugging信息,关闭所有debugger工具。优化了快速启动、快速执行和减小包体积。禁用所有的debugging aids和服务扩展。这个模式是为了部署给最终的用户使用。运行命令:
flutter run --release
,通过sky/tools/gn --android --runtime-mode=release
或者sky/tools/gn --ios --runtime-mode=release
来build
。 -
Profile:Profile模式只能在真机上运行,不能在模拟器上运行:基本和Release模式一致,除了启用了服务扩展和tracing,以及一些为了最低限度支持tracing运行的东西(比如可以连接observatory到进程)。命令flutter run --profile就是以这种模式运行的,通过sky/tools/gn --android --runtime-mode=profile或者sky/tools/gn --ios --runtime-mode=profile```来build。因为模拟器不能代表真实场景,所以不能在模拟器上运行。
-
Test:headless test模式只能在桌面上运行:基本和Debug模式一致,除了是headless的而且你能在桌面运行。命令
flutter test
就是以这种模式运行的,通过sky/tools/gn
来build
。
在我们实际开发中,应该用到上面所说的四种模式又各自分为两种:一种是未优化的模式,供开发人员调试使用;一种是优化过的模式,供最终的开发人员使用。默认情况下是未优化模式,如果要开启优化模式,build的时候在命令行后面添加
--unoptimized
参数。
注意,release
模式Android有可能要手动添加Androidmanifest.xml
中的INTERNET
权限(不然你可能会发现不能使用网络)
Flutter 热键 / 快捷代码生成
Android Studio可以通过Settings->Editor->Live Templates中配置热键输入关键字快速生成Flutter的有状态和无状态组件:
这样当我们在dart
文件中输入stless
时就会快速生成StatelessWidget
模板,输入stful
时就会快速生成StatefulWidget
模板。
其他任何你想要避免重复输入的代码片段,都可以如法炮制。
当然,如果你比较懒,想用别人配好的,直接在Android Studio中的Settings->Plugins搜索Flutter Snippets 插件安装使用:
安装完成后,可以打开Settings->Editor->Live Templates找到Flutter分组查看它有哪些快捷热键,另外你也可以直接查看其官方文档的说明:Flutter Snippets
对于使用 Visual Studio Code 的用户,同样可以在它的插件市场中搜索到类似的插件。
Flutter异常捕获
Dart 单线程模型
在 Java
和 Objective-C
(以下简称“OC”)中,如果程序发生异常且没有被捕获,那么程序将会终止,但是这在Dart
或JavaScript
中则不会!究其原因,这和它们的运行机制有关系。Java
和 OC
都是多线程模型的编程语言,任意一个线程触发异常且该异常未被捕获时,就会导致整个进程退出。但 Dart
和 JavaScript
不会,它们都是单线程模型,运行机制很相似(但有区别),下面我们通过Dart官方提供的一张图来看看 Dart
大致运行原理:
Dart 在单线程中是以消息循环机制来运行的,其中包含两个任务队列,一个是“微任务队列” microtask queue,另一个叫做“事件队列” event queue。从图中可以发现,微任务队列的执行优先级高于事件队列。
现在我们来介绍一下Dart线程运行过程,如上图中所示,入口函数 main()
执行完后,消息循环机制便启动了。首先会按照先进先出的顺序逐个执行微任务队列中的任务,事件任务执行完毕后程序便会退出,但是,在事件任务执行的过程中也可以插入新的微任务和事件任务,在这种情况下,整个线程的执行过程便是一直在循环,不会退出,而Flutter中,主线程的执行过程正是如此,永不终止。
在Dart中,所有的外部事件任务都在事件队列中,如 IO、计时器、点击、以及绘制事件等,而微任务通常来源于Dart内部,并且微任务非常少,之所以如此,是因为微任务队列优先级高,如果微任务太多,执行时间总和就越久,事件队列任务的延迟也就越久,对于GUI应用来说最直观的表现就是比较卡,所以必须得保证微任务队列不会太长。值得注意的是,我们可以通过Future.microtask(…)
方法向微任务队列插入一个任务。
在事件循环中,当某个任务发生异常并没有被捕获时,程序并不会退出,而直接导致的结果是当前任务的后续代码就不会被执行了,也就是说一个任务中的异常是不会影响其他任务执行的。
Flutter 框架的异常捕获
Dart 中可以通过try/catch/finally
来捕获代码块异常,这个和其他编程语言类似。
Flutter 框架为我们在很多关键的方法进行了异常捕获。这里举一个例子,当我们布局发生越界或不合规范时,Flutter就会自动弹出一个错误界面,这是因为Flutter已经在执行build
方法时添加了异常捕获,最终的源码如下:
void performRebuild() {
...
try {
//执行build方法
built = build();
} catch (e, stack) {
// 有异常时则弹出错误提示
built = ErrorWidget.builder(_debugReportException('building $this', e, stack));
}
...
}
可以看到,在发生异常时,Flutter默认的处理方式是弹一个ErrorWidget
,但如果我们想自己捕获异常并上报到报警平台的话应该怎么做?我们进入_debugReportException()
方法看看:
FlutterErrorDetails _debugReportException(
String context,
dynamic exception,
StackTrace stack, {
InformationCollector informationCollector
}) {
//构建错误详情对象
final FlutterErrorDetails details = FlutterErrorDetails(
exception: exception,
stack: stack,
library: 'widgets library',
context: context,
informationCollector: informationCollector,
);
//报告错误
FlutterError.reportError(details);
return details;
}
我们发现,错误是通过FlutterError.reportError
方法上报的,继续跟踪:
static void reportError(FlutterErrorDetails details) {
...
if (onError != null)
onError(details); //调用了onError回调
}
我们发现onError
是FlutterError
的一个静态属性,它有一个默认的处理方法 dumpErrorToConsole
,到这里就清晰了,如果我们想自己上报异常,只需要提供一个自定义的错误处理回调即可,如:
void main() {
FlutterError.onError = (FlutterErrorDetails details) {
reportError(details);
};
...
}
这样我们就可以处理那些Flutter为我们捕获的异常了。
其他异常捕获与日志收集
在Flutter中,还有一些Flutter没有为我们捕获的异常,如调用空对象方法异常、Future中的异常。在Dart中,异常分两类:同步异常和异步异常,同步异常可以通过try/catch
捕获,而异步异常则比较麻烦,如下面的代码是捕获不了Future的异常的:
try{
Future.delayed(Duration(seconds: 1)).then((e) => Future.error("xxx"));
}catch (e){
print(e)
}
Dart中有一个runZoned(...)
方法,可以给执行对象指定一个Zone
。Zone
表示一个代码执行的环境范围,为了方便理解,读者可以将Zone
类比为一个代码执行沙箱,不同沙箱的之间是隔离的,沙箱可以捕获、拦截或修改一些代码行为,如Zone
中可以捕获日志输出、Timer创建、微任务调度的行为,同时Zone
也可以捕获所有未处理的异常。下面我们看看runZoned(...)
方法定义:
R runZoned<R>(R body(), {
Map zoneValues,
ZoneSpecification zoneSpecification,
})
-
zoneValues
: Zone 的私有数据,可以通过实例zone[key]
获取,可以理解为每个“沙箱”的私有数据。 -
zoneSpecification
:Zone的一些配置,可以自定义一些代码行为,比如拦截日志输出和错误等。
举个例子:
runZoned(() => runApp(const MyApp()),
zoneSpecification: ZoneSpecification(
// 拦截print
print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
parent.print(zone, "Interceptor: $line");
},
// 拦截未处理的异步错误
handleUncaughtError: (Zone self, ZoneDelegate parent, Zone zone,
Object error, StackTrace stackTrace) {
parent.print(zone, '${error.toString()} $stackTrace');
},
),
);
这样一来,我们 APP 中所有调用print
方法输出日志的行为都会被拦截,通过这种方式,我们也可以在应用中记录日志,等到应用触发未捕获的异常时,将异常信息和日志统一上报。
另外我们还拦截了未被捕获的异步错误,这样一来,结合上面的 FlutterError.onError
我们就可以捕获我们Flutter应用错误了并进行上报了!
最终的错误上报代码大致如下:
void collectLog(String line){
... //收集日志
}
void reportErrorAndLog(FlutterErrorDetails details){
... //上报错误和日志逻辑
}
FlutterErrorDetails makeDetails(Object obj, StackTrace stack){
...// 构建错误信息
}
void main() {
var onError = FlutterError.onError; // 先将 onerror 保存起来
FlutterError.onError = (FlutterErrorDetails details) {
onError?.call(details); // 调用默认的onError
reportErrorAndLog(details); // 上报
};
runZoned(() => runApp(MyApp()),
zoneSpecification: ZoneSpecification(
// 拦截print
print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
collectLog(line); // 收集日志
parent.print(zone, "Interceptor: $line");
},
// 拦截未处理的异步错误
handleUncaughtError: (Zone self, ZoneDelegate parent, Zone zone,
Object error, StackTrace stackTrace) {
reportErrorAndLog(details); // 上报
parent.print(zone, '${error.toString()} $stackTrace');
}, ),
);
}
Flutter 中的消息循环原理分析
前面在 Dart 单线程模型中提到它有两个任务队列,都是在消息循环机制下运行的,消息循环(MessageLoop)是所有UI框架的基石,类似 Android 的 Looper 机制,Flutter 的底层也是基于消息循环驱动的,下面开始详细分析。
首先分析Flutter中消息循环的关键类及其关系,如图所示:
上图中,MessageLoop
类是Flutter创建消息循环能力的入口,而TaskRunner
则是Flutter使用消息循环能力的入口,大部分线程任务的注册都是通过TaskRunner
的PostTask
方法实现的。
-
MessageLoopImpl
实现了消息循环的通用逻辑以及持有并管理消息队列(通过MessageLoopTaskQueues
)。对Android平台而言,MessageLoopAndroid
继承了MessageLoopImpl
,并提供消息循环的底层实现:ALooper
和timerfd_create
所创建的文件描述符。 -
MessageLoopTaskQueues
将管理所有线程的消息队列(也称任务队列),每个线程的消息循环(MessageLoopImpl
对象)都将持有一个TaskQueueId
实例,用于向MessageLoopTaskQueues
的queue_entries_
字段查询当前消息循环所对应的消息队列——TaskQueueEntry
。 -
TaskQueueEntry
的delayed_tasks
字段持有一个DelayedTaskQueue
的实例,DelayedTaskQueue
是一个优先队列(std::priority_queue
),将根据时间和优先级对等待执行的消息进行排序。此外TaskQueueEntry
还会通过Wakeable
接口持有消息循环(MessageLoopImpl
)的引用,用以执行DelayedTaskQueue
中的任务。
以上便是消息循环的总体架构,核心在于TaskRunner
到TaskQueueEntry
的调用路径。
消息循环启动
Flutter通过系统API完成新线程的创建后,会基于该线程启动了消息循环,接下来基于EnsureInitializedForCurrentThread
方法的逻辑继续分析消息循环的启动,如代码清单10-1所示。
// 代码清单10-1 engine/fml/message_loop.cc
FML_THREAD_LOCAL ThreadLocalUniquePtr<MessageLoop> tls_message_loop; // 线程独有
void MessageLoop::EnsureInitializedForCurrentThread() {
if (tls_message_loop.get() != nullptr) { return; } // 已经完成初始化
tls_message_loop.reset(new MessageLoop()); // 初始化
} // 由于每个线程持有自己的MessageLoop,因此无须加锁
MessageLoop::MessageLoop()
: loop_(MessageLoopImpl::Create()), // 创建消息循环实例,见代码清单10-2
task_runner_(fml::MakeRefCounted<fml::TaskRunner>(loop_)) { } // 创建TaskRunner
MessageLoop& MessageLoop::GetCurrent() {
auto* loop = tls_message_loop.get();
return *loop;
}
以上逻辑将触发MessageLoop
的构造函数,完成loop_
、task_runner_
字段的初始化。首先分析MessageLoopImpl::Create
方法的逻辑,如代码清单10-2所示。
// 代码清单10-2 engine/fml/message_loop_impl.cc
fml::RefPtr<MessageLoopImpl> MessageLoopImpl::Create() {
#if OS_MACOSX // 在编译期确定
return fml::MakeRefCounted<MessageLoopDarwin>();
#elif OS_ANDROID
return fml::MakeRefCounted<MessageLoopAndroid>();
// SKIP OS_FUCHSIA / OS_LINUX / OS_WIN
#else
return nullptr;
#endif
}
以上逻辑将触发MessageLoopAndroid
的构造函数,其逻辑将在代码清单10-8中详细分析。首先分析MessageLoopAndroid
的父类MessageLoopImpl
的构造函数(注意C++中父类的构造函数是隐式触发的),如代码清单10-3所示。
// 代码清单10-3 engine/fml/message_loop_impl.cc
MessageLoopImpl::MessageLoopImpl()
: task_queue_(MessageLoopTaskQueues::GetInstance()), // 见代码清单10-4
queue_id_(task_queue_->CreateTaskQueue()), // 见代码清单10-5
terminated_(false) { // 当前消息循环是否停止
task_queue_->SetWakeable(queue_id_, this); // 见代码清单10-5
}
以上逻辑仍然是类成员字段的初始化。首先分析task_queue_
字段的初始化,如代码清单10-4所示。
// 代码清单10-4 engine/fml/message_loop_task_queues.cc
fml::RefPtr<MessageLoopTaskQueues> MessageLoopTaskQueues::instance_;
fml::RefPtr<MessageLoopTaskQueues> MessageLoopTaskQueues::GetInstance() {
std::scoped_lock creation(creation_mutex_);
if (!instance_) {
instance_ = fml::MakeRefCounted<MessageLoopTaskQueues>();
}
return instance_;
}
以上逻辑是一个典型的单例实现。接下来分析queue_id_
字段的初始化,如代码清单10-5 所示。
// 代码清单10-5 engine/fml/message_loop_task_queues.cc
TaskQueueId MessageLoopTaskQueues::CreateTaskQueue() {
std::lock_guard guard(queue_mutex_);
TaskQueueId loop_id = TaskQueueId(task_queue_id_counter_);
++task_queue_id_counter_; // TaskQueue的计数id
queue_entries_[loop_id] = std::make_unique<TaskQueueEntry>();
return loop_id;
}
void MessageLoopTaskQueues::SetWakeable(TaskQueueId queue_id,
fml::Wakeable* wakeable) {
std::lock_guard guard(queue_mutex_);
queue_entries_.at(queue_id)->wakeable = wakeable;
}
以上逻辑中,考虑到queue_entries_
将存储不同线程创建的TaskQueueId
,因此每次使用都需要加锁。SetWakeable
使得task_queue_
反向持有MessageLoopImpl
实例的引用,其将在后续逻辑中用到。
以上逻辑中,TaskQueueId
和TaskQueueEntry
一一对应,而TaskQueueEntry
又会通过wakeable
持有当前消息循环实例的引用(在代码清单10-3中设置)。接下来继续分析TaskQueueEntry
的构造函数,如代码清单10-6所示。
// 代码清单10-6 engine/fml/message_loop_task_queues.cc
const size_t TaskQueueId::kUnmerged = ULONG_MAX;
TaskQueueEntry::TaskQueueEntry()
: owner_of(_kUnmerged), subsumed_by(_kUnmerged) {
wakeable = NULL; // 消息循环的引用
task_observers = TaskObservers();
delayed_tasks = DelayedTaskQueue(); // 等待中的任务队列,详见10.1.2节
}
以上逻辑主要是TaskQueueEntry
的相关字段的初始化,owner_of
和subsumed_by
字段将用于后面内容介绍的动态线程合并,它们在正常情况下为常量_kUnmerged
,表示当前任务队列不被其他任务队列持有,也不持有其他任务队列。
下面,继续分析TaskQueueEntry(Value)
所对应的Key
,即TaskQueueId
类,如代码清单10-7所示。
// 代码清单10-7 engine/fml/message_loop_task_queues.h
class TaskQueueId {
public:
static const size_t kUnmerged; // ULONG_MAX
explicit TaskQueueId(size_t value) : value_(value) {}
operator int() const { return value_; }
private:
size_t value_ = kUnmerged; // 默认值
};
TaskQueueId
顾名思义是一个任务队列的id
,由以上逻辑可知,其本质就是一个int
类型的整数(value_
)。
以上便是MessageLoopImpl
的构造函数所引发的逻辑,是Flutter中消息循环相关的通用逻辑的初始化。接下来,不同平台将基于各自的系统API开始消息循环中与平台相关的初始化逻辑,接下来以Android为例进行介绍,如代码清单10-8所示。
// 代码清单10-8 engine/fml/platform/android/message_loop_android.cc
MessageLoopAndroid::MessageLoopAndroid()
: looper_(AcquireLooperForThread()), // 见代码清单10-9
// timerfd_create函数创建一个定时器对象,同时返回一个与之关联的文件描述符
timer_fd_(::timerfd_create(kClockType, TFD_NONBLOCK | TFD_CLOEXEC)),
// 第1步,创建定时器对象
running_(false) { // 判断当前消息循环是否正在运行,初始化时为false
static const int kWakeEvents = ALOOPER_EVENT_INPUT; // 第2步,构造响应回调
ALooper_callbackFunc read_event_fd = [](int, int events, void* data) -> int {
if (events & kWakeEvents) { // 轮询到数据,触发本回调
reinterpret_cast<MessageLoopAndroid*>(data)->OnEventFired(); // 见代码清单10-16
}
return 1; // continue receiving callbacks
}; // 第3步,为Looper添加一个用于轮询的文件描述符
int add_result = ::ALooper_addFd(looper_.get(), // 目标Looper
timer_fd_.get(), // 添加提供给Looper轮询的文件描述符
ALOOPER_POLL_CALLBACK, // 表明轮询到数据时将触发回调
kWakeEvents, // 用于唤醒Looper的事件类型
read_event_fd, // 将被触发的回调
this); // 回调的持有者的引用
FML_CHECK(add_result == 1);
}
以上逻辑主要分为3
步,相关细节在代码中均已注明。其中,
-
第
1
步的timerfd_create
方法是一个Linux系统API,将产生一个文件描述符用于后续Looper
的轮询。timerfd_create
方法的第1
个参数clockid
的值为kClockType
,本质是CLOCK_MONOTONIC
,表示从系统启动这一刻开始计时,不受系统时间被用户改变的影响;第2
个参数flags
包括TFD_NONBLOCK
和TFD_CLOEXEC
,均是为了保证当前面内容件描述符的正常使用。具体来说,TFD_NONBLOCK
表明当前是非阻塞模式,TFD_CLOEXEC
表示当程序执行exec
函数时,当前文件描述符将被系统自动关闭而不继续传递。 -
第
2
步,构造一个回调,当目标文件描述符存在数据时Looper
将触发这个回调,详见代码清单10-16。 -
第
3
步,通过ALooper_addFd
这个系统调用完成Looper
和文件描述符timer_fd_
的绑定。
以上逻辑中,Looper
的初始化逻辑在AcquireLooperForThread
方法中,如代码清单10-9所示。
// 代码清单10-9 engine/fml/platform/android/message_loop_android.cc
static ALooper* AcquireLooperForThread() {
ALooper* looper = ALooper_forThread(); // 返回与调用线程相关联的Looper
if (looper == nullptr) { // 当前线程没有关联Looper
looper = ALooper_prepare(0); // 初始化并返回一个与当前线程相关联的Looper
}
ALooper_acquire(looper);
return looper;
}
对Platform
线程(主线程)来说,ALooper_forThread
即可获得Looper
(即Android主线程的消息循环),而UI
线程、Raster
线程和I/O
线程则需要通过ALooper_prepare
新建一个Looper
。
完成以上逻辑后,可以正式启动Looper
了,如代码清单10-10所示。
// 代码清单10-10 engine/fml/platform/android/message_loop_android.cc
void MessageLoopAndroid::Run() {
FML_DCHECK(looper_.get() == ALooper_forThread()); // 确保Looper一致
running_ = true;
while (running_) {
int result = ::ALooper_pollOnce(-1, // 超时时间,-1表示无限轮询
nullptr, nullptr, nullptr);
if (result == ALOOPER_POLL_TIMEOUT || // 异常情况
result == ALOOPER_POLL_ERROR) {
running_ = false;
}
} // while
}
以上逻辑中,ALooper_pollOnce
涉及Linux
的pipe/epoll
机制,即调用该方法后便会释放CPU资源并等待Looper轮询的文件描述符传来的数据,既不会像常规的同步方法那样阻塞,也不会像常规的异步方法那样直接进入while
的无限循环。
接下来分析如何向消息循环注册或提交任务。
任务注册
在前面曾多次出现的PostTask
方法,其逻辑如代码清单10-11所示。
// 代码清单10-11 engine/fml/task_runner.cc
void TaskRunner::PostTask(const fml::closure& task) {
loop_->PostTask(task, fml::TimePoint::Now()); // 立即执行
}
void TaskRunner::PostTaskForTime(const fml::closure& task, fml::TimePoint target_time)
{
loop_->PostTask(task, target_time); // 指定目标时间
}
void TaskRunner::PostDelayedTask(const fml::closure& task, fml::TimeDelta delay) {
loop_->PostTask(task, fml::TimePoint::Now() + delay); // 指定时间间隔
}
以上逻辑中,无论何种方式,最终都将调用loop_
字段的PostTask
方法,如代码清单10-12所示。
// 代码清单10-12 engine/fml/message_loop_impl.cc
void MessageLoopImpl::PostTask(const fml::closure& task, // 目标任务
fml::TimePoint target_time) { // 目标执行时间
if (terminated_) { return; } // 消息循环已经停止
task_queue_->RegisterTask(queue_id_, task, target_time); // 见代码清单10-13
}
以上逻辑主要是通过RegisterTask
方法向当前线程的消息循环所对应的任务队列注册任务并设置唤醒时间,如代码清单10-13所示。
// 代码清单10-13 engine/fml/message_loop_task_queues.cc
void MessageLoopTaskQueues::RegisterTask(TaskQueueId queue_id, // 目标任务队列
const fml::closure& task, fml::TimePoint target_time) { // 任务和触发时间
std::lock_guard guard(queue_mutex_);
size_t order = order_++;
const auto& queue_entry = queue_entries_.at(queue_id);
queue_entry->delayed_tasks.push({order, task, target_time}); // 加入任务队列
TaskQueueId loop_to_wake = queue_id;
if (queue_entry->subsumed_by != _kUnmerged) { // 详见10.2节
loop_to_wake = queue_entry->subsumed_by;
}
WakeUpUnlocked(loop_to_wake, GetNextWakeTimeUnlocked(loop_to_wake));
}
void MessageLoopTaskQueues::WakeUpUnlocked(TaskQueueId queue_id,
fml::TimePoint time) const {
if (queue_entries_.at(queue_id)->wakeable) { // 存在对应的消息循环实现
queue_entries_.at(queue_id)->wakeable->WakeUp(time); // 设置唤醒时间
}
}
以上逻辑首先通过delayed_tasks
字段完成任务的注册,然后通过WakeUp
方法告知消息循环在指定时间触发任务执行,如代码清单10-14所示。
// 代码清单10-14 engine/fml/platform/android/message_loop_android.cc
void MessageLoopAndroid::WakeUp(fml::TimePoint time_point) {
bool result = TimerRearm(timer_fd_.get(), time_point); // 见代码清单10-15
}
以上逻辑将通过代码清单10-8中创建的文件描述符来实现硬件层的定时器逻辑,如代码清单10-15所示。
// 代码清单10-15 engine/fml/platform/linux/timerfd.cc
bool TimerRearm(int fd, fml::TimePoint time_point) {
uint64_t nano_secs = time_point.ToEpochDelta().ToNanoseconds(); // 转换为纳秒
if (nano_secs < 1) { nano_secs = 1; }
struct itimerspec spec = {};
// it_value是首次超时时间,it_interval是后续周期性超时时间
spec.it_value.tv_sec = (time_t)(nano_secs / NSEC_PER_SEC); // 超过的部分转换为秒
spec.it_value.tv_nsec = nano_secs % NSEC_PER_SEC; // 小于1s的部分仍用纳秒表示
spec.it_interval = spec.it_value;
int result = ::timerfd_settime( // 系统调用
fd, // 目标文件描述符,即代码清单10-8中的timer_fd_
TFD_TIMER_ABSTIME, // 绝对定时器
&spec, // 超时时间设置
nullptr);
return result == 0;
}
以上逻辑主要借助系统调用timerfd_settime
方法完成定时器的设置,在此不再赘述。
任务执行
当到达指定时间时,timer_fd_
将触发完成一次轮询以及代码清单10-8中的回调,OnEventFired
的逻辑如代码清单10-16所示。
// 代码清单10-16 engine/fml/platform/android/message_loop_android.cc
void MessageLoopAndroid::OnEventFired() {
if (TimerDrain(timer_fd_.get())) { // 见代码清单10-17
RunExpiredTasksNow(); // 父类MessageLoopImpl的方法
}
}
// engine/fml/message_loop_impl.cc
void MessageLoopImpl::RunExpiredTasksNow() {
FlushTasks(FlushType::kAll); // 见代码清单10-18
}
以上逻辑首先调用TimerDrain
方法进行检查,如代码清单10-17所示。
// 代码清单10-17 engine/fml/platform/linux/timerfd.cc
bool TimerDrain(int fd) {
uint64_t fire_count = 0;
ssize_t size = FML_HANDLE_EINTR(::read(fd, &fire_count, sizeof(uint64_t)));
if (size != sizeof(uint64_t)) {
return false;
}
return fire_count > 0;
}
检查通过后将触发FlushTasks
方法,处理代码清单10-13中注册的任务,具体逻辑如代码清单10-18所示。
// 代码清单10-18 engine/fml/message_loop_impl.cc
void MessageLoopImpl::FlushTasks(FlushType type) {
TRACE_EVENT0("fml", "MessageLoop::FlushTasks");
const auto now = fml::TimePoint::Now();
fml::closure invocation;
do {
invocation = task_queue_->GetNextTaskToRun(queue_id_, now); // 见代码清单10-28
if (!invocation) { break; } // 如果是非法任务,直接退出
invocation(); // 执行任务,即代码清单10-11中传入的task参数
std::vector<fml::closure> observers = task_queue_->GetObserversToNotify(queue_id_);
for (const auto& observer : observers) {
observer(); // 通知已注册当前任务队列监听的观察者
}
if (type == FlushType::kSingle) { break; } // 只执行一个任务
} while (invocation);
}
以上逻辑主要是从当前消息循环所持有的任务队列task_queue_
字段中取出一个任务并执行,并通知已注册的观察者,GetNextTaskToRun
方法的逻辑将在10.2节详细介绍。
至此,Flutter Engine的消息循环及其底层机制分析完毕。
总结:
在Flutter端通过MessageLoopImpl
实现了消息循环的通用逻辑以及持有并管理消息队列,到了Android平台端则通过MessageLoopAndroid
具体化在Android平台消息循环的实现。
- 对于Android,消息循环底层离不开Linux的
epoll
机制,该机制是保证系统UI架构中的任务队列既不占用CPU,又可以随时被唤醒的核心。 - 当然消息队列中的任务,需要按优先级排队,或延时执行,而每个通过
RegisterTask
注册的任务都设置了唤醒时间,这里的关键就是关于执行时间的安排,由于Android基于Linux内核,解决方式自然离不开Linux的文件描述符(Linux万物皆文件),要点就是使用timer_fd_
这个文件描述符通过系统调用来设置硬件定时时间,定时时间到,触发回调,执行任务。
动态线程合并技术
Flutter Engine中的动态线程合并技术,虽然本身是一套独立的逻辑,但是却分散在其他诸多逻辑中,如果不单独加以分析,那么在阅读其他代码时会因这个奇怪而突兀的逻辑而难以理解透彻。比如代码清单10-18中的GetNextTaskToRun
方法,如果没有动态线程合并,那么可以简单地处理为取出当前消息循环所持有的task_queue_
字段的最高优先级任务,但是由于存在动态线程合并,其逻辑将复杂好几倍。
动态线程合并主要是为了Flutter UI
能与原生View
同帧渲染(因为Platform
线程中的FlutterImageView
的Canvas
变成Flutter UI
的渲染输出),结合消息循环机制,可以猜测:所谓动态线程合并,并不是将Platform
线程和Raster
线程在系统层面做了合并,而是让Platform
线程的消息循环可以接管并处理原Raster
线程的消息循环所持有的任务队列。
此外,动态线程合并还涉及一些现实问题,比如何时启动线程合并、何时关闭线程合并,因为Platform View
消失之后,自然不需要动态线程合并,此时为了提高性能,应该恢复原来正常的处理关系。
合并、维持与消解
首先分析线程(更准确地说是消息循环,下同)的合并逻辑,它们是后续分析的基础。由于Platform View
的存在,会触发MergeWithLease
方法,其逻辑如代码清单10-19所示。
// 代码清单10-19 engine/fml/raster_thread_merger.cc
void RasterThreadMerger::MergeWithLease(size_t lease_term) { // 合并处于维持状态的帧数
std::scoped_lock lock(lease_term_mutex_);
if (TaskQueuesAreSame()) { return; } // 见代码清单10-20
if (!IsEnabledUnSafe()) { return; } // 见代码清单10-20
FML_DCHECK(lease_term > 0) << "lease_term should be positive.";
if (IsMergedUnSafe()) { // 见代码清单10-20
merged_condition_.notify_one();
return;
} // 检查工作完成,开始合并,见代码清单10-21
bool success = task_queues_->Merge(platform_queue_id_, gpu_queue_id_);
if (success && merge_unmerge_callback_ != nullptr) {
merge_unmerge_callback_(); // 通知
}
FML_CHECK(success) << "Unable to merge the raster and platform threads.";
lease_term_ = lease_term; // 线程合并处于维持状态的帧数,默认为10帧
// 唤醒某个等待(Wait)的线程,如果当前没有等待线程,则该函数什么也不做
merged_condition_.notify_one(); // For WaitUntilMerged方法
}
以上逻辑首先检查是否有必要开始动态线程合并,相关逻辑如代码清单10-20所示。检查完成后将正式开始合并,并更新lease_term_
字段,该字段用于判断当前是否有必要维持线程合并状态,具体作用将在后面内容分析。
// 代码清单10-20 engine/fml/raster_thread_merger.cc
bool RasterThreadMerger::IsEnabledUnSafe() const {
return enabled_; // 检查是否允许动态线程合并
}
bool RasterThreadMerger::IsMergedUnSafe() const {
return lease_term_ > 0 || TaskQueuesAreSame(); // 检查是否已处于合并状态
}
bool RasterThreadMerger::TaskQueuesAreSame() const {
return platform_queue_id_ == gpu_queue_id_; // 检查两个任务队列本身是否相同
}
下面分析动态线程合并的具体逻辑,如代码清单10-21所示。
// 代码清单10-21 engine/fml/message_loop_task_queues.cc
// owner: 合并后任务队列的所有者通常为Platform线程
// subsumed: 被合并的任务队列通常为Raster线程
bool MessageLoopTaskQueues::Merge(TaskQueueId owner, TaskQueueId subsumed) {
if (owner == subsumed) { return true; } // 合并自身,异常参数
std::lock_guard guard(queue_mutex_);
auto& owner_entry = queue_entries_.at(owner); // 合并方的任务队列入口
auto& subsumed_entry = queue_entries_.at(subsumed); // 被合并方的任务队列入口
if (owner_entry->owner_of == subsumed) {
return true; // 合并方的任务队列已经是被合并方的持有者(owner_of)
} // 下面开始真正合并
std::vector<TaskQueueId> owner_subsumed_keys = {
// 检查合并方当前是否持有任务队列或被其他任务队列持有
owner_entry->owner_of, owner_entry->subsumed_by,
// 检查被合并方当前是否持有任务队列或被其他任务队列持有
subsumed_entry->owner_of,subsumed_entry->subsumed_by};
for (auto key : owner_subsumed_keys) {
if (key != _kUnmerged) { return false; } // 通过检查以上4个关键字段是否为_kUnmerged
} // 判断owner和subsumed对应的任务队列当前是否处于动态线程合并状态,若已处于则返回
owner_entry->owner_of = subsumed; // 标记owner_entry持有被合并方(subsumed)
subsumed_entry->subsumed_by = owner; // 标记被合并方被owner持有
if (HasPendingTasksUnlocked(owner)) { // 如果有未处理的任务,则见代码清单10-22
WakeUpUnlocked(owner, GetNextWakeTimeUnlocked(owner)); // 见代码清单10-13
}
return true;
}
由于Android中原生UI必须主线程渲染,因此以上逻辑中owner
为Platform
线程的任务队列,subsumed
为Raster
线程的任务队列。以上逻辑主要是将owner
任务队列的owner_of
字段设置为Raster
线程的任务队列,将subsumed
任务队列的subsumed_by
字段设置为Platform
线程的任务队列。这样,处理owner
任务队列时就会一并处理调用owner_of
所对应的任务队列,而如果一个任务队列的subsumed_by
字段不为_kUnmerged
,则说明它将由其他任务队列连带处理,因此直接退出即可,这部分内容后面将详细分析。
以上逻辑将在合并完成后通过HasPendingTasksUnlocked
方法检查是否有未处理的任务,如代码清单10-22所示。
// 代码清单10-22 engine/fml/message_loop_task_queues.cc
bool MessageLoopTaskQueues::HasPendingTasksUnlocked(TaskQueueId queue_id) const {
const auto& entry = queue_entries_.at(queue_id);
bool is_subsumed = entry->subsumed_by != _kUnmerged;
if (is_subsumed) {
return false; // 当前任务队列已被合并进其他任务队列,无须在此处理
}
if (!entry->delayed_tasks.empty()) {
return true; // 当前任务队列存在待处理任务
} // 当前任务队列不存在待处理任务,开始检查是否有被当前消息循环合并的任务队列
const TaskQueueId subsumed = entry->owner_of;
if (subsumed == _kUnmerged) {
return false; // 如果不存在被合并的任务队列,则认为确实不存在排队任务
} else { // 根据被合并的任务队列是否有排队任务返回结果
return !queue_entries_.at(subsumed)->delayed_tasks.empty();
}
}
在理解owner
和subsumed
的含义后,以上逻辑变得十分清晰。接下来分析动态线程合并状态的维持。在代码清单9-42中,如果已经处于线程合并状态,而当前又正好在绘制包含Platform View
的帧,则会调用ExtendLeaseTo
方法以延长动态线程合并维持的时间,如代码清单10-23所示。
// 代码清单10-23 engine/fml/raster_thread_merger.cc
void RasterThreadMerger::ExtendLeaseTo(size_t lease_term) { // 动态线程合并维持的帧数
if (TaskQueuesAreSame()) { return; }
std::scoped_lock lock(lease_term_mutex_);
FML_DCHECK(IsMergedUnSafe()) << "lease_term should be positive.";
if (lease_term_ != kLeaseNotSet && // 不要延长一个未设置的值
static_cast<int>(lease_term) > lease_term_) { // 最大不超过原来的值
lease_term_ = lease_term;
}
}
以上逻辑中,ExtendLeaseTo
方法的传入参数lease_term
的值一般是10
,由if逻辑可知,如果Platform View
一直在渲染,lease_term_
会被始终更新成10
,而不是每次累加10
,即每次调用该方法,都将让动态线程合并状态继续维持lease_term
帧。动态线程合并状态的维持,本质是lease_term_
字段的更新。接下来分析动态线程合并状态的消解,以及在此过程中lease_term_
字段所产生的作用。
在代码清单5-100中,完成一帧的渲染后,会触发DecrementLease
方法,如代码清单10-24所示。
// 代码清单10-24 engine/fml/raster_thread_merger.cc
RasterThreadStatus RasterThreadMerger::DecrementLease() {
if (TaskQueuesAreSame()) { // 见代码清单10-20
return RasterThreadStatus::kRemainsMerged;
}
std::unique_lock<std::mutex> lock(lease_term_mutex_);
if (!IsMergedUnSafe()) { // 已经解除合并
return RasterThreadStatus::kRemainsUnmerged;
}
if (!IsEnabledUnSafe()) { // 不允许执行相关操作
return RasterThreadStatus::kRemainsMerged;
} // 调用本方法时lease_term_必须大于0,即线程处于合并状态
FML_DCHECK(lease_term_ > 0)
<< "lease_term should always be positive when merged.";
lease_term_--; // -1,为0时表示动态线程合并状态结束
if (lease_term_ == 0) {
lock.unlock();
UnMergeNow(); // 开始消解两个任务队列的关系,见代码清单10-25
return RasterThreadStatus::kUnmergedNow;
}
return RasterThreadStatus::kRemainsMerged;
}
以上逻辑的主要工作是在条件允许时将lease_term_
字段的计数减1
。当lease_term_
字段的值为0
时,即可开始动态线程合并的消解,解绑任务队列,如代码清单10-25所示。
// 代码清单10-25 engine/fml/raster_thread_merger.cc
void RasterThreadMerger::UnMergeNow() {
std::scoped_lock lock(lease_term_mutex_);
if (TaskQueuesAreSame()) { return; }
if (!IsEnabledUnSafe()) { return; }
lease_term_ = 0; // 重置
bool success = task_queues_->Unmerge(platform_queue_id_); // 见代码清单10-26
if (success && merge_unmerge_callback_ != nullptr) {
merge_unmerge_callback_(); // 告知监听者
}
}
以上逻辑主要是调用task_queues_
对象的Unmerge
方法,并触发解除绑定的回调。Unmerge
方法的逻辑如代码清单10-26所示。
// 代码清单10-26 engine/fml/message_loop_task_queues.cc
bool MessageLoopTaskQueues::Unmerge(TaskQueueId owner) {
std::lock_guard guard(queue_mutex_);
const auto& owner_entry = queue_entries_.at(owner);
const TaskQueueId subsumed = owner_entry->owner_of;
if (subsumed == _kUnmerged) { return false; } // 无须解除绑定
queue_entries_.at(subsumed)->subsumed_by = _kUnmerged;
owner_entry->owner_of = _kUnmerged; // 重置相关字段
if (HasPendingTasksUnlocked(owner)) { // 见代码清单10-22
WakeUpUnlocked(owner, GetNextWakeTimeUnlocked(owner)); // 见代码清单10-13
} // 分别检查两个任务队列是否有排队任务,不同于代码清单10-21,此时需要分别处理
if (HasPendingTasksUnlocked(subsumed)) { // 因为subsumed已经从owener中释放
WakeUpUnlocked(subsumed, GetNextWakeTimeUnlocked(subsumed));
}
return true; // 消解成功
}
以上逻辑已在代码中注明,在此不再赘述。此外,DecrementLease
方法并非UnMergeNow
方法的唯一触发点,当Embedder中调用nativeSurfaceDestroyed
方法时,将会触发Shell的OnPlatformViewDestroyed
方法,该方法又将触发Rasterizer
的Teardown
方法,如代码清单10-27所示。
// 代码清单10-27 engine/shell/common/rasterizer.cc
void Rasterizer::Teardown() { // 渲染相关资源的清理、重置
compositor_context_->OnGrContextDestroyed();
surface_.reset();
last_layer_tree_.reset();
if (raster_thread_merger_.get() != nullptr && raster_thread_merger_.get()->
IsMerged()) {
FML_DCHECK(raster_thread_merger_->IsEnabled());
raster_thread_merger_->UnMergeNow(); // 见代码清单10-25
raster_thread_merger_->SetMergeUnmergeCallback(nullptr);
}
}
以上逻辑中,如有必要,也会触发已经动态合并线程的消解(即任务队列绑定的解除)。到目前为止,只介绍了owner_of
、subsumed_by
、lease_term_
等几个字段的赋值与重置,这些字段产生的影响尚未触及,下面开始分析。
合并状态下的任务执行
代码清单10-18中,GetNextTaskToRun
方法用于获取下一个被执行的任务,如代码清单10-28所示。
// 代码清单10-28 engine/fml/message_loop_task_queues.cc
fml::closure MessageLoopTaskQueues::GetNextTaskToRun( TaskQueueId queue_id,
fml::TimePoint from_time) {
std::lock_guard guard(queue_mutex_);
if (!HasPendingTasksUnlocked(queue_id)) { // 见代码清单10-22
return nullptr; // 如果没有排队任务,则直接返回
}
TaskQueueId top_queue = _kUnmerged;
const auto& top = PeekNextTaskUnlocked(queue_id, top_queue); // 见代码清单10-29
if (!HasPendingTasksUnlocked(queue_id)) {
WakeUpUnlocked(queue_id, fml::TimePoint::Max());
} else { // 存在排队任务,在下一个任务的预期执行时间触发
WakeUpUnlocked(queue_id, GetNextWakeTimeUnlocked(queue_id));
} // 如果尚未到任务的预期执行时间,则直接返回
if (top.GetTargetTime() > from_time) { return nullptr; }
fml::closure invocation = top.GetTask(); // 读取任务,并移出队列
queue_entries_.at(top_queue)->delayed_tasks.pop(); // 确定invocation满足条件后再移除
return invocation;
}
以上逻辑的核心在于通过PeekNextTaskUnlocked
获取优先级最高的队列,具体逻辑如代码清单10-29所示。
// 代码清单10-29 engine/fml/message_loop_task_queues.cc
const DelayedTask& MessageLoopTaskQueues::PeekNextTaskUnlocked(
TaskQueueId owner, // 目标任务队列id
TaskQueueId& top_queue_id) const { // 一般将_kUnmerged作为默认值
FML_DCHECK(HasPendingTasksUnlocked(owner));
const auto& entry = queue_entries_.at(owner); // 目标任务队列
const TaskQueueId subsumed = entry->owner_of; // 被合并的任务队列id
if (subsumed == _kUnmerged) { // 自身没有合并其他任务队列
top_queue_id = owner;
return entry->delayed_tasks.top(); // 取任务队列第1个任务
} // 以下是存在被合并任务队列的情况
const auto& owner_tasks = entry->delayed_tasks;
const auto& subsumed_tasks = queue_entries_.at(subsumed)->delayed_tasks;
const bool subsumed_has_task = !subsumed_tasks.empty();
const bool owner_has_task = !owner_tasks.empty();
if (owner_has_task && subsumed_has_task) { // 两个队列均有任务
const auto owner_task = owner_tasks.top();
const auto subsumed_task = subsumed_tasks.top();
if (owner_task > subsumed_task) { // 取优先级较高者,见代码清单10-30
top_queue_id = subsumed;
} else {
top_queue_id = owner;
}
} else if (owner_has_task) { // 仅owner任务队列有任务
top_queue_id = owner;
} else { // 仅subsumed任务队列有任务
top_queue_id = subsumed;
}
return queue_entries_.at(top_queue_id)->delayed_tasks.top(); // 取第1个任务
}
以上逻辑的解释均已在代码中注明,其中owner_task
的大小比较规则如代码清单10-30所示,需要注意的是,DelayedTask
是递增排列的,其取值越小,排序越靠前,优先级越高。
// 代码清单10-30 engine/fml/delayed_task.cc
bool DelayedTask::operator>(const DelayedTask& other) const {
if (target_time_ == other.target_time_) { // 预期执行时间相同
return order_ > other.order_; // order_值越小,优先级越高
}
return target_time_ > other.target_time_; // 预期执行时间越小,优先级越高
}
至此,我们已经完成动态线程合并技术的分析。
总结:动态线程合并技术的本质就是在Platform
线程执行Raster
线程所持有的任务队列。
参考:
- 《Flutter实战·第二版》
- 《Flutter内核源码剖析》