flutter的包管理#资源管理#调试Flutter应用#Flutter异常捕获
2.5 包管理
2.5.1 简介
在软件开发中,很多时候有一些公共的库或 SDK 可能会被很多项目用到,因此,将这些代码单独抽到一个独立模块,然后哪个项目需要使用时再直接集成这个模块,便可大大提高开发效率。很多编程语言或开发工具都支持这种“模块共享”机制,如 Java 语言中这种独立模块会被打成一个 jar 包,Android 中的 aar 包,Web开发中的 npm 包等。为了方便表述,我们将这种可共享的独立模块统一称为“包”( Package)。
一个 App 在实际开发中往往会依赖很多包,而这些包通常都有交叉依赖关系、版本依赖等,如果由开发者手动来管理应用中的依赖包将会非常麻烦。因此,各种开发生态或编程语言官方通常都会提供一些包管理工具,比如在 Android 提供了 Gradle 来管理依赖,iOS 用 Cocoapods 或 Carthage 来管理依赖,Node 中通过 npm 等。而在 Flutter 开发中也有自己的包管理工具。本节我们主要介绍一下 Flutter 如何使用配置文件pubspec.yaml
(位于项目根目录)来管理第三方依赖包。
YAML 是一种直观、可读性高并且容易被人类阅读的文件格式,和 xml 或 Json 相比它语法简单并非常容易解析,所以 YAML 常用于配置文件,Flutter 也是用 yaml 文件作为其配置文件。Flutter 项目默认的配置文件是pubspec.yaml
,我们看一个简单的示例:
name: flutter_in_action
description: First Flutter Application.version: 1.0.0+1dependencies:flutter:sdk: fluttercupertino_icons: ^0.1.2dev_dependencies:flutter_test:sdk: flutterflutter:uses-material-design: true
下面,我们逐一解释一下各个字段的意义:
name
:应用或包名称。description
: 应用或包的描述、简介。version
:应用或包的版本号。dependencies
:应用或包依赖的其他包或插件。dev_dependencies
:开发环境依赖的工具包(而不是flutter应用本身依赖的包)。flutter
:flutter相关的配置选项。
如果我们的Flutter应用本身依赖某个包,我们需要将所依赖的包添加到dependencies
下,接下来我们通过一个例子来演示一下如何添加、下载并使用第三方包。
#2.5.2 Pub仓库
Pub(https://pub.dev/ )是 Google 官方的 Dart Packages 仓库,类似于 node 中的 npm仓库、Android中的 jcenter。我们可以在 Pub 上面查找我们需要的包和插件,也可以向 Pub 发布我们的包和插件。我们将在后面的章节中介绍如何向 Pub 发布我们的包和插件。
#2.5.3 示例
接下来,我们实现一个显示随机字符串的 widget。有一个名为 “english_words” 的开源软件包,其中包含数千个常用的英文单词以及一些实用功能。我们首先在 pub 上找到 english_words 这个包(如图2-12所示),确定其最新的版本号和是否支持 Flutter。
我们看到“english_words”包最新的版本是4.0.0,并且支持flutter,接下来:
-
将“english_words” 添加到依赖项列表,如下:
dependencies:flutter:sdk: flutter# 新添加的依赖english_words: ^4.0.0
-
下载包。在Android Studio的编辑器视图中查看pubspec.yaml时(图2-13),单击右上角的 Pub get 。
这会将依赖包安装到您的项目。我们可以在控制台中看到以下内容:
flutter packages get Running "flutter packages get" in flutter_in_action... Process finished with exit code 0
我们也可以在控制台,定位到当前工程目录,然后手动运行
flutter packages get
命令来下载依赖包。另外,需要注意dependencies
和dev_dependencies
的区别,前者的依赖包将作为App的源码的一部分参与编译,生成最终的安装包。而后者的依赖包只是作为开发阶段的一些工具包,主要是用于帮助我们提高开发、测试效率,比如 flutter 的自动化测试包等。 -
引入
english_words
包。import 'package:english_words/english_words.dart';
在输入时,Android Studio会自动提供有关库导入的建议选项。导入后该行代码将会显示为灰色,表示导入的库尚未使用。
-
使用
english_words
包来生成随机字符串。class RandomWordsWidget extends StatelessWidget {@overrideWidget build(BuildContext context) {// 生成随机字符串final wordPair = WordPair.random();return Padding(padding: const EdgeInsets.all(8.0),child: Text(wordPair.toString()),);} }
我们将
RandomWordsWidget
添加到_MyHomePageState.build
的Column
的子widget中。Column(mainAxisAlignment: MainAxisAlignment.center,children: <Widget>[... //省略无关代码RandomWordsWidget(),], )
-
如果应用程序正在运行,请使用热重载按钮(⚡️图标) 更新正在运行的应用程序。每次单击热重载或保存项目时,都会在正在运行的应用程序中随机选择不同的单词对。 这是因为单词对是在
build
方法内部生成的。每次热更新时,build
方法都会被执行,运行效果如图2-14所示。
#2.5.4 其他依赖方式
上文所述的依赖方式是依赖Pub仓库的。但我们还可以依赖本地包和git仓库。
-
依赖本地包
如果我们正在本地开发一个包,包名为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.gitpath: packages/package1
上面介绍的这些依赖方式是Flutter开发中常用的,但还有一些其他依赖方式,完整的内容读者可以自行查看:https://www.dartlang.org/tools/pub/dependencies 。
2.6 资源管理
Flutter APP 安装包中会包含代码和 assets(资源)两部分。Assets 是会打包到程序安装包中的,可在运行时访问。常见类型的 assets 包括静态数据(例如JSON文件)、配置文件、图标和图片等。
#2.6.1 指定 assets
和包管理一样,Flutter 也使用pubspec.yaml (opens new window)文件来管理应用程序所需的资源,举个例子:
flutter:assets:- assets/my_icon.png- assets/background.png
assets
指定应包含在应用程序中的文件, 每个 asset 都通过相对于pubspec.yaml
文件所在的文件系统路径来标识自身的路径。asset 的声明顺序是无关紧要的,asset的实际目录可以是任意文件夹(在本示例中是assets 文件夹)。
在构建期间,Flutter 将 asset 放置到称为 asset bundle 的特殊存档中,应用程序可以在运行时读取它们(但不能修改)。
#2.6.2 Asset 变体(variant)
构建过程支持“asset变体”的概念:不同版本的 asset 可能会显示在不同的上下文中。 在pubspec.yaml
的assets 部分中指定 asset 路径时,构建过程中,会在相邻子目录中查找具有相同名称的任何文件。这些文件随后会与指定的 asset 一起被包含在 asset bundle 中。
例如,如果应用程序目录中有以下文件:
- …/pubspec.yaml
- …/graphics/my_icon.png
- …/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 变体(见下文)。
#2.6.3 加载 assets
您的应用可以通过AssetBundle (opens new window)对象访问其 asset 。有两种主要方法允许从 Asset bundle 中加载字符串或图片(二进制)文件。
#1. 加载文本assets
- 通过rootBundle (opens new window)对象加载:每个Flutter应用程序都有一个rootBundle (opens new window)对象, 通过它可以轻松访问主资源包,直接使用
package:flutter/services.dart
中全局静态的rootBundle
对象来加载asset即可。 - 通过 DefaultAssetBundle (opens new window)加载:建议使用 DefaultAssetBundle (opens new window)来获取当前 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. 加载图片
类似于原生开发,Flutter也可以为当前设备加载适合其分辨率的图像。
#1)声明分辨率相关的图片 assets
AssetImage (opens new window)可以将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乘72px,那么.../3.0x/my_icon.png
应该是216px乘216px; 但如果未指定宽度和高度,它们都将渲染为72像素×72像素(以逻辑像素为单位)。
pubspec.yaml
中asset部分中的每一项都应与实际文件相对应,但主资源项除外。当主资源缺少某个资源时,会按分辨率从低到高的顺序去选择 ,也就是说1x中没有的话会在2x中找,2x中还没有的话就在3x中找。
#2)加载图片
要加载图片,可以使用 AssetImage (opens new window)类。例如,我们可以从上面的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 (opens new window)或 ImageCache (opens new window)时你会注意到有与缩放相关的参数)
#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/
是隐含的,所以它不应该包含在资产路径中。
#3. 特定平台 assets
上面的资源都是flutter应用中的,这些资源只有在Flutter框架运行之后才能使用,如果要给我们的应用设置APP图标或者添加启动图,那我们必须使用特定平台的assets。
#1)设置APP图标
更新Flutter应用程序启动图标的方式与在本机Android或iOS应用程序中更新启动图标的方式相同。
-
Android
在 Flutter 项目的根目录中,导航到
.../android/app/src/main/res
目录,里面包含了各种资源文件夹(如mipmap-hdpi
已包含占位符图像 “ic_launcher.png”,见图2-15)。 只需按照Android开发人员指南 (opens new window)中的说明, 将其替换为所需的资源,并遵守每种屏幕密度(dpi)的建议图标大小标准。注意: 如果您重命名.png文件,则还必须在您
AndroidManifest.xml
的<application>
标签的android:icon
属性中更新名称。 -
iOS
在Flutter项目的根目录中,导航到
.../ios/Runner
。该目录中Assets.xcassets/AppIcon.appiconset
已经包含占位符图片(见图2-16), 只需将它们替换为适当大小的图片,保留原始文件名称。
#2)更新启动页
在 Flutter 框架加载时,Flutter 会使用本地平台机制绘制启动页。此启动页将持续到Flutter渲染应用程序的第一帧时。
注意: 这意味着如果您不在应用程序的
main()
方法中调用runApp (opens new window)函数 (或者更具体地说,如果您不调用window.render (opens new window)去响应window.onDrawFrame (opens new window))的话, 启动屏幕将永远持续显示。
- 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进行自定义,如图2-18所示。
#2.6.4 平台共享 assets
如果我们采用的是Flutter+原生的开发模式,那么可能会存Flutter和原生需要共享资源的情况,比如Flutter项目中已经有了一张图片A,如果原生代码中也要使用A,我们可以将A拷贝一份到原生项目的特定目录,这样的话虽然功能可以实现,但是最终的应用程序包会变大,因为包含了重复的资源,为了解决这个问题,Flutter 提供了一种Flutter和原生之间共享资源的方式,由于实现上需要涉及平台相关的原生代码,故本书不做展开,读者有需要可以自行查阅官方文档 (opens new window)。
2.7 调试Flutter应用
有各种各样的工具和功能来帮助调试Flutter应用程序。
#2.7.1 日志与断点
#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() (opens new window),它封装了 print,将一次输出的内容长度限制在一个级别(内容过多时会分批输出),避免被Android内核丢弃。
Flutter框架中的许多类都有toString
实现,按照惯例,输出信息通过包括对象的运行时类型 、类名以及关键字段等信息。 树中的一些类也具有toStringDeep
实现,从该点返回整个子树的多行描述。一些具有详细信息toString
的类会实现一个toStringShort
,它只返回对象的类型或其他非常简短的(一个或两个单词)描述。
#3. 调试模式断言
在Flutter应用调试过程中,Dart assert
语句被启用,并且 Flutter 框架使用它来执行许多运行时检查来验证是否违反一些不可变的规则。当一个某个规则被违反时,就会在控制台打印错误日志,并带上一些上下文信息来帮助追踪问题的根源。
要关闭调试模式并使用发布模式,请使用flutter run --release
运行我们的应用程序。 这也关闭了Observatory调试器。一个中间模式可以关闭除Observatory之外所有调试辅助工具的,称为“profile mode”,用--profile
替代--release
即可。
#4. 断点
开发过程中,断点是最实用的调试工具之一,我们以 Android Studio 为例,如图2-19:
我们在 93 行打了一个断点,一旦代码执行到这一行就会暂停,这时我们可以看到当前上下文所有变量的值,然后可以选择一步一步的执行代码。关于如何通过 IDE 来打断点,网上教程很多,读者可以自行搜索。
#2.7.2 调试应用程序层
Flutter框架的每一层都提供了将其当前状态或事件转储(dump)到控制台(使用debugPrint
)的功能。
#1. Widget 树
要转储Widgets树的状态,请调用debugDumpApp() (opens new window)。 只要应用程序已经构建了至少一次(即在调用build()
之后的任何时间),我们可以在应用程序未处于构建阶段(即,不在build()
方法内调用 )的任何时间调用此方法(在调用runApp()
之后)。
如, 这个应用程序:
import 'package:flutter/material.dart';void main() {runApp(MaterialApp(home: AppHome(),),);
}class AppHome extends StatelessWidget {@overrideWidget 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 (opens new window)是Material widget的一个实现细节 。
当按钮从被按下变为被释放时debugDumpApp()被调用,TextButton对象同时调用setState()
,并将自己标记为"dirty"。我们还可以查看已注册了哪些手势监听器; 在这种情况下,一个单一的GestureDetector被列出,并且监听“tap”手势(“tap”是TapGestureDetector
的toStringShort
函数输出的)。
如果我们编写自己的widget,则可以通过覆盖debugFillProperties() (opens new window)来添加信息。 将DiagnosticsProperty (opens new window)对象作为方法参数,并调用父类方法。 该函数是该toString
方法用来填充小部件描述信息的。
#2. 渲染树
如果我们尝试调试布局问题,那么Widget树可能不够详细。在这种情况下,我们可以通过调用debugDumpRenderTree()
转储渲染树。 正如debugDumpApp()
,除布局或绘制阶段外,我们可以随时调用此函数。作为一般规则,从frame 回调 (opens new window)或事件处理器中调用它是最佳解决方案。
要调用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() (opens new window)将信息添加到转储。 将DiagnosticsProperty (opens new window)对象作为方法的参数,并调用父类方法。
#3. Layer树
读者可以理解为渲染树是可以分层的,而最终绘制需要将不同的层合成起来,而Layer则是绘制时需要合成的层,如果我们尝试调试合成问题,则可以使用debugDumpLayerTree() (opens new window)。对于上面的例子,它会输出:
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() (opens new window)获取语义树(呈现给系统可访问性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 (opens new window)和debugPrintEndFrameBanner (opens new window)布尔值以将帧的开始和结束打印到控制台。
例如:
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 (opens new window)还可以用来打印导致当前帧被调度的调用堆栈。
#6. 可视化调试
我们也可以通过设置debugPaintSizeEnabled
为true
以可视方式调试布局问题。 这是来自rendering
库的布尔值。它可以在任何时候启用,并在为true时影响绘制。 设置它的最简单方法是在void main()
的顶部设置。
当它被启用时,所有的盒子都会得到一个明亮的深青色边框,padding(来自widget如Padding)显示为浅蓝色,子widget周围有一个深蓝色框, 对齐方式(来自widget如Center和Align)显示为黄色箭头. 空白(如没有任何子节点的Container)以灰色显示。
debugPaintBaselinesEnabled (opens new window)做了类似的事情,但对于具有基线的对象,文字基线以绿色显示,表意(ideographic)基线以橙色显示。
debugPaintPointersEnabled (opens new window)标志打开一个特殊模式,任何正在点击的对象都会以深青色突出显示。 这可以帮助我们确定某个对象是否以某种不正确的方式进行hit测试(Flutter检测点击的位置是否有能响应用户操作的widget),例如,如果它实际上超出了其父项的范围,首先不会考虑通过hit测试。
如果我们尝试调试合成图层,例如以确定是否以及在何处添加RepaintBoundary
widget,则可以使用debugPaintLayerBordersEnabled (opens new window)标志, 该标志用橙色或轮廓线标出每个层的边界,或者使用debugRepaintRainbowEnabled (opens new window)标志, 只要他们重绘时,这会使该层被一组旋转色所覆盖。
所有这些标志只能在调试模式下工作。通常,Flutter框架中以“debug...
” 开头的任何内容都只能在调试模式下工作。
#7. 调试动画
调试动画最简单的方法是减慢它们的速度。为此,请将timeDilation (opens new window)变量(在scheduler库中)设置为大于1.0的数字,例如50.0。 最好在应用程序启动时只设置一次。如果我们在运行中更改它,尤其是在动画运行时将其值改小,则在观察时可能会出现倒退,这可能会导致断言命中,并且这通常会干扰我们的开发工作。
#8. 调试性能问题
要了解我们的应用程序导致重新布局或重新绘制的原因,我们可以分别设置debugPrintMarkNeedsLayoutStacks (opens new window)和 debugPrintMarkNeedsPaintStacks (opens new window)标志。 每当渲染盒被要求重新布局和重新绘制时,这些都会将堆栈跟踪记录到控制台。如果这种方法对我们有用,我们可以使用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 (opens new window))。 使用dart:developer
的Timeline (opens new window)工具来包含你想测试的代码块,例如:
Timeline.startSync('interesting function');
// iWonderHowLongThisTakes();
Timeline.finishSync();
然后打开你应用程序的Observatory timeline页面,在“Recorded Streams”中选择‘Dart’复选框,并执行你想测量的功能。
刷新页面将在Chrome的跟踪工具 (opens new window)中显示应用按时间顺序排列的timeline记录。
请确保运行flutter run
时带有--profile
标志,以确保运行时性能特征与我们的最终产品差异最小。
2.7.3 DevTools
Flutter DevTools 是 Flutter 可视化调试工具,如图2-20。它将各种调试工具和能力集成在一起,并提供可视化调试界面,它的功能很强大,掌握它会对我们开发和优化 Flutter 应用有很大帮助。由于 Flutter DevTools 功能很多,短篇幅是讲不完的,本书不做专门介绍,Flutter 官网对 DevTools 有详细的介绍,读者可以去官网查看相关教程。
2.8 Flutter异常捕获
在介绍Flutter异常捕获之前必须先了解一下Dart单线程模型,只有了解了Dart的代码执行流程,我们才能知道该在什么地方去捕获异常。
#2.8.1 Dart单线程模型
在 Java 和 Objective-C(以下简称“OC”)中,如果程序发生异常且没有被捕获,那么程序将会终止,但是这在Dart或JavaScript中则不会!究其原因,这和它们的运行机制有关系。Java 和 OC 都是多线程模型的编程语言,任意一个线程触发异常且该异常未被捕获时,就会导致整个进程退出。但 Dart 和 JavaScript 不会,它们都是单线程模型,运行机制很相似(但有区别),下面我们通过Dart官方提供的一张图(2-21)来看看 Dart 大致运行原理:
Dart 在单线程中是以消息循环机制来运行的,其中包含两个任务队列,一个是“微任务队列” microtask queue,另一个叫做“事件队列” event queue。从图中可以发现,微任务队列的执行优先级高于事件队列。
现在我们来介绍一下Dart线程运行过程,如上图中所示,入口函数 main() 执行完后,消息循环机制便启动了。首先会按照先进先出的顺序逐个执行微任务队列中的任务,事件任务执行完毕后程序便会退出,但是,在事件任务执行的过程中也可以插入新的微任务和事件任务,在这种情况下,整个线程的执行过程便是一直在循环,不会退出,而Flutter中,主线程的执行过程正是如此,永不终止。
在Dart中,所有的外部事件任务都在事件队列中,如IO、计时器、点击、以及绘制事件等,而微任务通常来源于Dart内部,并且微任务非常少,之所以如此,是因为微任务队列优先级高,如果微任务太多,执行时间总和就越久,事件队列任务的延迟也就越久,对于GUI应用来说最直观的表现就是比较卡,所以必须得保证微任务队列不会太长。值得注意的是,我们可以通过Future.microtask(…)
方法向微任务队列插入一个任务。
在事件循环中,当某个任务发生异常并没有被捕获时,程序并不会退出,而直接导致的结果是当前任务的后续代码就不会被执行了,也就是说一个任务中的异常是不会影响其他任务执行的。
#2.8.2 Flutter异常捕获
Dart中可以通过try/catch/finally
来捕获代码块异常,这个和其他编程语言类似,如果读者不清楚,可以查看Dart语言文档,不再赘述,下面我们看看Flutter中的异常捕获。
#1. Flutter框架异常捕获
Flutter 框架为我们在很多关键的方法进行了异常捕获。这里举一个例子,当我们布局发生越界或不合规范时,Flutter就会自动弹出一个错误界面,这是因为Flutter已经在执行build方法时添加了异常捕获,最终的源码如下:
@override
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为我们捕获的异常了,接下来我们看看如何捕获其他异常。
#2. 其他异常捕获与日志收集
在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(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应用错误了并进行上报了!
#3. 最终的错误上报代码
我们最终的异常捕获和上报代码大致如下:
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); //调用默认的onErrorreportErrorAndLog(details); //上报};runZoned(() => runApp(MyApp()),zoneSpecification: ZoneSpecification(// 拦截printprint: (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');},),);
}