Flutter的编译模式之Release以及Debug模式配置

背景

  1. 在开发阶段:我们希望调试尽可能方便、快速,尽可能多地提供错误上下文信息
  2. 在测试阶段:我们希望覆盖范围尽可能全面,能够具备不同配置切换能力,可以测试和验证还没有对外发布的新功能
  3. 在发布阶段:我们则希望能够去除一切测试代码,精简调试信息,使运行速度尽可能快,代码足够安全

这就要求开发者,不仅要在工程内提前准备多份配置环境,还要利用编译器提供的编译选项,打包出符合不同阶段优化需求的 App。所以理解Flutter 的编译模式非常重要。

Flutter 的编译模式

Flutter 支持 3 种运行模式,包括 Debug、Release 和 Profile。在编译时,这三种模式是完全独立的。首先,我们先来看看这 3 种模式的具体含义吧。

Debug 模式

对应 Dart 的 JIT 模式,可以在真机和模拟器上同时运行。该模式会打开所有的断言(assert),以及所有的调试信息、服务扩展和调试辅助(比如 Observatory)。此外,该模式为快速开发和运行做了优化,支持亚秒级有状态的 Hot reload(热重载),但并没有优化代码执行速度、二进制包大小和部署。flutter run --debug 命令,就是以这种模式运行的。

Release 模式

对应 Dart 的 AOT 模式,只能在真机上运行,不能在模拟器上运行,其编译目标为最终的线上发布,给最终的用户使用。该模式会关闭所有的断言,以及尽可能多的调试信息、服务扩展和调试辅助。此外,该模式优化了应用快速启动、代码快速执行,以及二级制包大小,因此编译时间较长。flutter run --release 命令,就是以这种模式运行的。

Profile 模式

基本与 Release 模式一致,只是多了对 Profile 模式的服务扩展的支持,包括支持跟踪,以及一些为了最低限度支持所需要的依赖(比如,可以连接 Observatory 到进程)。该模式用于分析真实设备实际运行性能。flutter run --profile 命令,就是以这种模式运行的。

由于 Profile 与 Release 在编译过程上几乎无差异,因此我们今天只讨论 Debug 和 Release 模式。

在开发应用时,在 Debug 模式下,我们会打印详细的日志,调用开发环境接口;而在 Release 模式下,我们会只记录极少的日志,调用生产环境接口。

在运行时识别应用的编译模式,有两种解决办法:

  1. 通过断言识别
  2. 通过 Dart VM 所提供的编译常数识别

断言这里我们不常用,这里我们主要谈编译常数

kReleaseMode

Dart 提供了一个布尔型的常量 kReleaseMode,用于反向指示当前 App 的编译模式。

if(kReleaseMode){
  //Do sth for release 
} else {
  //Do sth for debug
}

分离配置环境

通过 kReleaseMode 常量,我们能够识别出当前 App 的编译环境,从而可以在运行时对于代码进行行局部微调。而如果我们想在整个应用层面,为不同的运行环境提供更为统一的配置(比如,对于同一个接口调用行为,开发环境会使用 dev.example.com 域名,而生产环境会使用 api.example.com 域名),则需要在应用启动入口提供可配置的初始化方式,根据特定需求为应用注入配置环境。

在 Flutter 构建 App 时,为应用程序提供不同的配置环境,总体可以分为抽象配置、配置多入口、读配置和编译打包 4 个步骤:

  1. 抽象出应用程序的可配置部分,并使用 InheritedWidget 对其进行封装;
  2. 将不同的配置环境拆解为多个应用程序入口(比如,开发环境为 main-dev.dart、生产环境为 main.dart),把应用程序的可配置部分固化在各个入口处;
  3. 在运行期,通过 InheritedWidget 提供的数据共享机制,将配置部分应用到其子 Widget 对应的功能中;
  4. 使用 Flutter 提供的编译打包选项,构建出不同配置环境的安装包

接下来,我将依次为你介绍具体的实现步骤。

在下面的示例中,我会把应用程序调用的接口和标题进行区分实现,即开发环境使用 dev.example.com 域名,应用主页标题为 dev;而生产环境使用 api.example.com 域名,主页标题为 example。

配置抽象

根据需求可以看出,应用程序中有两个需要配置的部分,即接口 apiBaseUrl 和标题 appName,因此我定义了一个继承自 InheritedWidget 的类 AppConfig,对这两个配置进行封装:


class AppConfig extends InheritedWidget {
  AppConfig({
    @required this.appName,
    @required this.apiBaseUrl,
    @required Widget child,
  }) : super(child: child);

  final String appName;//主页标题
  final String apiBaseUrl;//接口域名

  //方便其子Widget在Widget树中找到它
  static AppConfig of(BuildContext context) {
    return context.inheritFromWidgetOfExactType(AppConfig);
  }

  //判断是否需要子Widget更新。由于是应用入口,无需更新
  @override
  bool updateShouldNotify(InheritedWidget oldWidget) => false;
}

不同的环境创建不同的应用入口

在这个例子中,由于只有两个环境,即开发环境与生产环境,因此我们将文件分别命名为 main_dev.dart 和 main.dart。在这两个文件中,我们会使用不同的配置数据来对 AppConfig 进行初始化,同时把应用程序实例 MyApp 作为其子 Widget,这样整个应用内都可以获取到配置数据:


//main_dev.dart
void main() {
  var configuredApp = AppConfig(
    appName: 'dev',//主页标题
    apiBaseUrl: 'http://dev.example.com/',//接口域名
    child: MyApp(),
  );
  runApp(configuredApp);//启动应用入口
}

//main.dart
void main() {
  var configuredApp = AppConfig(
    appName: 'example',//主页标题
    apiBaseUrl: 'http://api.example.com/',//接口域名
    child: MyApp(),
  );
  runApp(configuredApp);//启动应用入口
}

在应用内获取配置数据

由于 AppConfig 是整个应用程序的根节点,因此我可以通过调用 AppConfig.of 方法,来获取到相关的数据配置。


class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var config = AppConfig.of(context);//获取应用配置
    return MaterialApp(
      title: config.appName,//应用主页标题
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var config = AppConfig.of(context);//获取应用配置
    return Scaffold(
      appBar: AppBar(
        title: Text(config.appName),//应用主页标题
      ),
      body:  Center(
        child: Text('API host: ${config.apiBaseUrl}'),//接口域名
      ),
    );
  }
}

构建出不同配置的安装包


//运行开发环境应用程序
flutter run -t lib/main_dev.dart 

//运行生产环境应用程序
flutter run -t lib/main.dart

Android Studio 创建不同的启动配置

通过 Flutter 插件为 main_dev.dart 增加启动入口。首先,点击工具栏上的 Config Selector,选择 Edit Configurations 进入编辑应用程序启动选项:

image.png

然后,点击位于工具栏面板左侧顶部的“+”按钮,在弹出的菜单中选择 Flutter 选项,为应用程序新增一项启动入口:

image.png

最后,在入口的编辑面板中,为 main_dev 选择程序的 Dart 入口,点击 OK 后,就完成了入口的新增工作:


image.png

接下来,我们就可以在 Config Selector 中切换不同的启动入口,从而直接在 Android Studio 中注入不同的配置环境了:


image.png

打包


//打包开发环境应用程序
flutter build apk -t lib/main_dev.dart 
flutter build ios -t lib/main_dev.dart

//打包生产环境应用程序
flutter build apk -t lib/main.dart
flutter build ios -t lib/main.dart

参考

Flutter 编译模式

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,045评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,114评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,120评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,902评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,828评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,132评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,590评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,258评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,408评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,335评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,385评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,068评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,660评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,747评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,967评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,406评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,970评论 2 341

推荐阅读更多精彩内容