美文网首页跨平台
Flutter了解之入门篇1-2(调试、异常捕获、打包)

Flutter了解之入门篇1-2(调试、异常捕获、打包)

作者: 平安喜乐698 | 来源:发表于2022-09-21 22:44 被阅读0次
目录
  1. 调试
  2. Flutter异常捕获
  3. 打包

1. 调试

  1. 日志与断点

debugger()

需要导入import 'dart:developer';
当使用Dart Observatory(或另一个Dart调试器,例如IntelliJ IDE中的调试器)时,可以使用该debugger()语句插入编程式断点。

例:
// debugger()语句采用一个可选when参数,可以指定该参数仅在特定条件为真时中断。
void someFunction(double offset) {
  debugger(when: offset > 30.0);
}

print、debugPrint、flutter logs

print() 将输出到系统控制台(可以使用flutter logs命令来查看)。
如果一次输出太多,Android有时会丢弃一些日志行。可以使用Flutter的foundation库中的debugPrint()来避免被Android内核丢弃。

assert

Flutter框架使用它来执行许多运行时检查来验证是否违反一些不可变的规则。当一个某个规则被违反时,就会在控制台打印错误日志,并带上一些上下文信息来帮助追踪问题的根源。

flutter run --release
  关闭调试模式(也关闭了Observatory调试器)并使用发布模式。
flutter run --profile
  关闭除Observatory之外所有调试辅助工具。

断点

打了一个断点,一旦代码执行到这一行就会暂停,这时可以看到当前上下文所有变量的值,然后可以选择一步一步的执行代码。
  1. 调试应用程序层

Flutter框架的每一层都提供了将其当前状态或事件转储到控制台(使用debugPrint)的功能。

  1. Widget树
要转储Widgets树的状态,请调用debugDumpApp()。
只要应用程序已经构建了至少一次(即在调用build()之后的任何时间),可以在应用程序未处于构建阶段(即不在build()方法内调用 )的任何时间调用此方法(在调用runApp()之后)。

示例
import 'package:flutter/material.dart';
void main() {
  runApp(
    MaterialApp(
      home: AppHome(),
    ),
  );
}
class AppHome extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Material(
      child: Center(
        child: TextButton(
          onPressed: () {
            debugDumpApp();
          },
          child: Text('Dump App'),
        ),
      ),
    );
  }
}
/*
会输出:
在widget树的根中调用toStringDeepwidget的输出。

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中可以通过覆盖debugFillProperties()来添加信息,将DiagnosticsPropertie对象作为方法参数调用父类方法。
*/
  1. 渲染树
需要导入import'package:flutter/rendering.dart';

调试布局问题时,Widget树可能不够详细。
可以通过调用debugDumpRenderTree()转储渲染树,和debugDumpApp(),一样,除了布局或绘制阶段外可以随时调用此函数。

/*
上面的示例中修改为debugDumpRenderTree方法,输出如下:
根RenderObject对象的toStringDeep函数的输出

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)
...

当调试布局问题时,关键要看的是size和constraints字段。约束沿着树向下传递,尺寸向上传递。

自定义widget中可以通过覆盖debugFillProperties()来添加信息,将DiagnosticsPropertie对象作为方法参数调用父类方法。
*/
  1. Layer树
渲染树是可以分层的,而最终绘制需要将不同的层合成起来,而Layer则是绘制时需要合成的层。

如果尝试调试合成问题,则可以使用debugDumpLayerTree方法。
/*
根Layer的toStringDeep输出

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

RepaintBoundary widget在渲染树的层中创建了一个RenderRepaintBoundary 用于减少需要重绘的需求量
*/
  1. 语义树
还可以调用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")
*/
  1. 调度
要找出相对于帧的开始/结束事件发生的位置,可以切换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还可以用来打印导致当前帧被调度的调用堆栈。
  1. 可视化调试
也可以通过设置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...`” 开头的任何内容都只能在调试模式下工作。
  1. 调试动画
调试动画最简单的方法是减慢它们的速度。将timeDilation变量(在scheduler库中)设置为大于1.0的数字,例如50.0。 最好在应用程序启动时只设置一次。
如果我们在运行中更改它,尤其是在动画运行时将其值改小,则在观察时可能会出现倒退,这可能会导致断言命中,并且这通常会干扰我们的开发工作。
  1. 调试性能问题
想了解应用程序导致重新布局或重新绘制的原因,可以分别设置debugPrintMarkNeedsLayoutStacks和debugPrintMarkNeedsPaintStacks标志。每当渲染盒被要求重新布局和重新绘制时,这些都会将堆栈跟踪记录到控制台。如果这种方法对我们有用,我们可以使用`services`库中的`debugPrintStack()`方法按需打印堆栈痕迹。
  1. 统计应用启动时间
// 收集有关Flutter应用程序启动所需时间的详细信息
flutter run --trace-startup --profile

跟踪输出保存为start_up_info.json,在Flutter工程目录在build目录下。
输出列出了从应用程序启动到这些跟踪事件(以微秒捕获)所用的时间:
    进入Flutter引擎时.
    展示应用第一帧时.
    初始化Flutter框架时.
    完成Flutter框架初始化时.
/*
例

{
  "engineEnterTimestampMicros": 96025565262,
  "timeToFirstFrameMicros": 2171978,
  "timeToFrameworkInitMicros": 514585,
  "timeAfterFrameworkInitMicros": 1657393
}
*/
  1. 跟踪Dart代码性能
要执行自定义性能跟踪和测量Dart任意代码段的wall/CPU时间(类似于在Android上使用systrace)。

使用dart:developer的Timeline工具来包含想测试的代码块。
/*
Timeline.startSync('interesting function');
...
Timeline.finishSync();
*/
打开应用程序的Observatory timeline页面,在“Recorded Streams”中选择‘Dart’复选框,并执行想测量的功能。


确保运行flutter run时带有--profile标志,以确保运行时性能特征与最终产品差异最小。
  1. DevTools(Flutter可视化调试工具)
DevTools

2. Flutter异常捕获

  1. 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) {
  ...
  // onError是FlutterError的一个静态属性,它有一个默认的处理方法 dumpErrorToConsole
  if (onError != null)
    onError(details); // 调用了onError回调
}

如果想自己上报异常,只需要提供一个自定义的错误处理回调即可,如:
void main() {
  FlutterError.onError = (FlutterErrorDetails details) {
    // 自定义方法,处理那些Flutter为我们捕获的异常
    reportError(details);
  };
 ...
}
  1. 在Flutter中,还有一些Flutter没有为我们捕获的异常,如调用空对象方法异常、Future中的异常。
在Dart中,异常分两类:
  1. 同步异常
     可以通过try/catch捕获
  2. 异步异常
      比较麻烦

下面的代码是捕获不了Future的异常的:
try{
    Future.delayed(Duration(seconds: 1)).then((e) => Future.error("xxx"));
}catch (e){
    print(e)
}
Dart中有一个runZoned(...) 方法

// 可以给执行对象指定一个Zone。Zone表示一个代码执行的环境范围(一个代码执行沙箱),不同沙箱的之间是隔离的,沙箱可以捕获、拦截或修改一些代码行为,如Zone中可以捕获日志输出、Timer创建、微任务调度的行为,同时Zone也可以捕获所有未处理的异常。
R runZoned<R>(R body(), {
    // Zone 的私有数据,可以通过实例zone[key]获取,可以理解为每个“沙箱”的私有数据
    Map zoneValues, 
    // Zone的一些配置,可以自定义一些代码行为,比如拦截日志输出和错误
    ZoneSpecification zoneSpecification,
}) 

可以结合FlutterError.onError来实现日志和未捕获异常的上报

示例

// 自定义方法
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,APP中所有调用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');
    },
  ),
 );
}

3. 打包

  1. iOS

和普通iOS原生项目打包过程一样。大致过程如下:

1. 配置项目(双击Runner.xcworkspace打开项目)
应用名、bundleId、支持的最低版本、证书签名
icon图标、启动图、白名单

2. iTuneStore中创建应用,填写相关信息

3. Archive打包、上传

4. TestFlight内部测试

5. 发布
  1. Android
  1. 配置AndroidManifest.xml(android/app/src/main/目录下)
设置包名
  manifest的package属性,例"com.sst.cx.demo"

设置应用名称
  application的android:label属性

设置应用图标
  application的android:icon属性

设置权限(网络、相册、摄像头、蓝牙、通讯录)
    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
    <uses-permission android:name="android.permission.READ_PHONE_STATE"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

注意:
  1. meta-data 不可更改,不可更改,用于Flutter生成安卓插件。
  2. 开发模式是修改在dubug目录下的AndroidManifest.xml。
  1. 配置build.gadle(android/app/目录下)
设置应用ID
  applicationId

设置版本号
  versionCode
设置版本字符串
  versionName

设置支持的最低的Android API
  minSdkVersion
设置指定程序设计运行的Android API
  targetSdkVersion

例:
    defaultConfig {
        applicationId "com.example.demo"
        minSdkVersion 16
        targetSdkVersion 28
        versionCode flutterVersionCode.toInteger()
        versionName flutterVersionName
    }
  1. 配置图标(android/app/src/main/res目录下)
方式1: 使用flutter_launcher_icons三方库自动生成

修改pubspec.yaml文件:
dev_dependencies:
  flutter_launcher_icons: #lastversion

flutter_icons:
  android: "launcher_icon"
  ios: true
  image_path: "assets/icon/icon.png"

终端执行
flutter pub get
flutter pub run flutter_launcher_icons:main -f <your config file name here>
方式2: 手动生成图标拷贝到目录下

可以使用在线工具MakeAppIcon来生成
/*
启动图标

尺寸  图标大小  屏幕尺寸
mipmap-mdpi  48x48  320×480
mipmap-hdpi  72x72  480×800,480×854
mipmap-xhdpi  96x96  1280*720,720p
mipmap-xxhdpi  144x144  1920*1080,1080p
mipmap-xxxhdpi  192x192  3840×2160,4k
*/

启动页
修改android/app/src/main/res/drawable下的launch_background.xml配置文件
默认是一个白色底。
/*
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <!--部分机型的尺寸未必和启动页图片一致,因此可以设置启动页的背景色与启动页图片边缘一致-->
    <item android:drawable="@android:color/white" /> 

    <!-- 设置启动图 -->
    <item>
        <bitmap
            android:gravity="center"
            android:src="@mipmap/launch_image" />
    </item>
</layer-list>
*/
  1. 签名
1. 创建keystore(在flutter项目根目录下 终端执行)
keytool -genkey -v -keystore ~/key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias key

2. 创建key.properties文件(android目录下)
storePassword=oneplus
keyPassword=oneplus
keyAlias=key
storeFile=/Users/cx/key.jks

3. 修改android/app/build.gradle文件:
def keystorePropertiesFile = rootProject.file("key.properties")
def keystoreProperties = new Properties()
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
android{
  ...
  signingConfigs {
        release {
            keyAlias keystoreProperties['keyAlias']
            keyPassword keystoreProperties['keyPassword']
            storeFile = file(keystoreProperties['storeFile'])
            storePassword = keystoreProperties['storePassword']
        }
    }
    buildTypes {
        release {
            signingConfig signingConfigs.release
        }
    }
}
  1. 构建
在项目根目录下执行
  fluter build apk

// 会在项目根目录下生成build目录
生成的apk路径:build/app/outputs/apk/release/app-release.appk
/*
app bundle
  flutter build appbundle --target-platform android-arm,android-arm64,android-x64
拆分
  flutter build apk --target-platform android-arm,android-arm64,android-x64 --split-per-abi
*/
注意:
  1. 修改AndroidManifest.xml文件后,打包可能会存在缓存,需要使用flutter clean清除缓存。
  1. 发布
上传到应用市场

4. 常用小功能

  1. 格式化代码
右键 | Reformat Code with dartfmt

1. 需要安装Dart插件
2. 始终添加尾随逗号:在函数、方法和构造函数的参数列表末尾, 有助于自动格式化程序为Flutter样式代码插入适当的换行符。
格式化代码
  1. 修改文件名
修改文件名
移动文件位置
  1. 调试
1. Data Analysls静态分析(追踪问题的最快的方式)
  Dart分析器大量使用了代码中的类型注释来帮助追踪问题。所以要尽量避免var、无类型的参数、无类型的列表文字等。

2. Flutter Inspector可视化浏览Widget树(运行后查看)
  解决:布局问题

3. Dart Observatory (语句级的单步调试和分析器)
  运行后,打开Observatory URL的Web页面
Data Analysls静态分析
Flutter Inspector的Select widget。在AndroidStudio上选中组件,会选中手机上的对应组件,反之亦然
Flutter Inspector的render tree。调试布局
AndroidStudio上使用终端
  1. 编写README
1. 功能模块介绍
2. bug处理
3. 关键代码
4. 未来要更新的功能
  1. 切换开发/生产环境

方式1(需要改动代码来切换)

class ProjectConfig{
  static bool get isRelease => bool.fromEnvironment("dart.vm.product"); // 是否为release环境
}

方式2(切换入口文件)

Flutter在调试运行或打包时,可以指定入口文件。
  //
  flutter run -t lib/main.dart
  flutter run -t lib/hello.dart
  //
  flutter build apk
  flutter build apk -t lib/hello.dart
针对该特性可以使用不同的main文件来加载对应环境的参数。
首先建立一个环境变量配置类,分布对应生产(Product)、测试环境(Development)和本地(Local),根据不同的环境枚举返回不同的接口地址。
enum Env {
  Product,
  Development,
  Local
}
class EnvConfig {
  static Env env;
  static String getBaseUrl(Env env) {
    switch (env) {
      case Env.Product:
        return "https://product-api.host.com/";
      case Env.Development:
        return "http://dev-api.host.com/";
      case Env.Local:
        return "http://localhost/api";
      default:
        return "http://localhost/api";
    }
  }
}

然后根据不同的环境定义不同的main入口文件,在该文件的main方法中配置不同的环境参数
// main_dev.dart
void main() {
  runApp(MyApp());
  CookieManager.instance.initCookie();
  HttpUtil.setBaseUrl(EnvConfig.getBaseUrl(Env.Development));
}
// main.dart
void main() {
  runApp(MyApp());
  CookieManager.instance.initCookie();
  HttpUtil.setBaseUrl(EnvConfig.getBaseUrl(Env.Product));
}

VSCode的调试配置文件launch.json的configurations是一个列表,对应了不同的环境。可以在配置文件的program参数指定main入口文件。
"version": "0.2.0",
"configurations": [
    {
        "name": "Dev",
        "type": "dart",
        "request": "launch",
        "program": "lib/main_dev.dart"
    },
    {
        "name": "Prod",
        "type": "dart",
        "request": "launch",
        "program": "lib/main.dart"
    },
]

Dart编译器 只支持少量核心库

相关文章

网友评论

    本文标题:Flutter了解之入门篇1-2(调试、异常捕获、打包)

    本文链接:https://www.haomeiwen.com/subject/cnlvortx.html