今天,我們來聊聊 Flutter 的編譯模式吧。
在開發移動應用程序時,一個 App 的完整生命周期包括開發、測試和上線 3 個階段。在每個階段,開發者的關注點都不一樣。
比如,在開發階段,我們希望調試盡可能方便、快速,盡可能多地提供錯誤上下文信息;在測試階段,我們希望覆蓋范圍盡可能全面,能夠具備不同配置切換能力,可以測試和驗證還沒有對外發布的新功能;而在發布階段,我們則希望能夠去除一切測試代碼,精簡調試信息,使運行速度盡可能快,代碼足夠安全。
這就要求開發者在構建移動應用時,不僅要在工程內提前準備多份配置環境,還要利用編譯器提供的編譯選項,打包出符合不同階段優化需求的 App。
對于 Flutter 來說,它既支持常見的 Debug、Release 等工程物理層面的編譯模式,也支持在工程內提供多種配置環境入口。今天,我們就來學習一下 Flutter 提供的編譯模式,以及如何在 App 中引用開發環境和生產環境,使得我們在不破壞任何生產環境代碼的情況下,能夠測試處于開發期的新功能。
## 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 模式下,我們會只記錄極少的日志,調用生產環境接口。
在運行時識別應用的編譯模式,有兩種解決辦法:
* 通過斷言識別;
* 通過 Dart VM 所提供的編譯常數識別。
我們先來看看**如何通過斷言識別應用的編譯模式**。
通過 Debug 與 Release 模式的介紹,我們可以得出,Release 與 Debug 模式的一個重要區別就是,Release 模式關閉了所有的斷言。因此,我們可以借助于斷言,寫出只在 Debug 模式下生效的代碼。
如下所示,我們在斷言里傳入了一個始終返回 true 的匿名函數執行結果,這個匿名函數的函數體只會在 Debug 模式下生效:
~~~
assert(() {
//Do sth for debug
return true;
}());
~~~
需要注意的是,匿名函數聲明調用結束時追加了小括號()。 這是因為斷言只能檢查布爾值,所以我們必須使用括號強制執行這個始終返回 true 的匿名函數,以確保匿名函數體的代碼可以執行。
接下來,我們再看看**如何通過編譯常數識別應用的編譯模式**。
如果說通過斷言只能寫出在 Debug 模式下運行的代碼,而通過 Dart 提供的編譯常數,我們還可以寫出只在 Release 模式下生效的代碼。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 提供的編譯選項,來**構建出不同配置的安裝包**了。
如果想要在模擬器或真機上運行這段代碼,我們可以在 flutter run 命令后面,追加–target 或 -t 參數,來指定應用程序初始化入口:
~~~
// 運行開發環境應用程序
flutter run -t lib/main_dev.dart
// 運行生產環境應用程序
flutter run -t lib/main.dart
~~~
如果我們想在 Android Studio 上為應用程序創建不同的啟動配置,則可以**通過 Flutter 插件為 main\_dev.dart 增加啟動入口**。
首先,點擊工具欄上的 Config Selector,選擇 Edit Configurations 進入編輯應用程序啟動選項:
:-: 
圖 1 Config Selector 新增入口
然后,點擊位于工具欄面板左側頂部的“+”按鈕,在彈出的菜單中選擇 Flutter 選項,為應用程序新增一項啟動入口:
:-: 
圖 2 選擇新增類型
最后,在入口的編輯面板中,為 main\_dev 選擇程序的 Dart 入口,點擊 OK 后,就完成了入口的新增工作:
:-: 
圖 3 編輯啟動入口
接下來,我們就可以**在 Config Selector 中切換不同的啟動入口,從而直接在 Android Studio 中注入不同的配置環境了**:
:-: 
圖 4 Config Selector 切換啟動入口
我們試著在不同的入口中進行切換和運行,可以看到,App 已經可以識別出不同的配置環境了:

圖 5 開發環境運行示例
:-: 
圖 6 生產環境運行示例
而如果我們想要打包構建出適用于 Android 的 APK,或是 iOS 的 IPA 安裝包,則可以在 flutter build 命令后面,同樣追加–target 或 -t 參數,指定應用程序初始化入口:
~~~
// 打包開發環境應用程序
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 支持 Debug 與 Release 的編譯模式,并且這兩種模式在構建時是完全獨立的。Debug 模式下會打開所有的斷言和調試信息,而 Release 模式下則會關閉這些信息,因此我們可以通過斷言,寫出只在 Debug 模式下生效的代碼。而如果我們想更精準地識別出當前的編譯模式,則可以利用 Dart 所提供的編譯常數 kReleaseMode,寫出只在 Release 模式下生效的代碼。
除此之外,Flutter 對于常見的分環境配置能力也提供了支持,我們可以使用 InheritedWidget 為應用中可配置部分進行封裝抽象,通過配置多入口的方式為應用的啟動注入配置環境。
需要注意的是,雖然斷言和 kReleaseMode 都能夠識別出 Debug 編譯模式,但它們對二進制包的打包構建影響是不同的。
采用斷言的方式,其相關代碼會在 Release 構建中被完全剔除;而如果使用 kReleaseMode 常量來識別 Debug 環境,雖然這段代碼永遠不會在 Release 環境中執行,但卻會被打入到二進制包中,增大包體積。因此,如果沒有特殊需求的話,一定要使用斷言來實現 Debug 特有的邏輯,或是在發布期前將使用 kReleaseMode 判斷的 Debug 邏輯完全刪除。
我把今天分享所涉及到的知識點打包到了[GitHub](https://github.com/cyndibaby905/34_multi_env)中,你可以下載下來,反復運行幾次,加深理解與記憶。
## 思考題
最后,我給你留一道思考題吧。
在保持生產環境代碼不變的情況下,如果想在開發環境中支持不同配置的切換,我們應該如何實現?
- 前言
- 開篇詞
- 預習篇
- 01丨預習篇 · 從0開始搭建Flutter工程環境
- 02丨預習篇 · Dart語言概覽
- Flutter開發起步
- 03丨深入理解跨平臺方案的歷史發展邏輯
- 04丨Flutter區別于其他方案的關鍵技術是什么?
- 05丨從標準模板入手,體會Flutter代碼是如何運行在原生系統上的
- Dart語言基礎
- 06丨基礎語法與類型變量:Dart是如何表示信息的?
- 07丨函數、類與運算符:Dart是如何處理信息的?
- 08丨綜合案例:掌握Dart核心特性
- Flutter基礎
- 09丨Widget,構建Flutter界面的基石
- 10丨Widget中的State到底是什么?
- 11丨提到生命周期,我們是在說什么?
- 12丨經典控件(一):文本、圖片和按鈕在Flutter中怎么用?
- 13丨ListView在Flutter中是什么?
- 14 丨 經典布局:如何定義子控件在父容器中排版位置?
- 15 丨 組合與自繪,我該選用何種方式自定義Widget?
- 16 丨 從夜間模式說起,如何定制不同風格的App主題?
- 17丨依賴管理(一):圖片、配置和字體在Flutter中怎么用?
- 18丨依賴管理(二):第三方組件庫在Flutter中要如何管理?
- 19丨用戶交互事件該如何響應?
- 20丨關于跨組件傳遞數據,你只需要記住這三招
- 21丨路由與導航,Flutter是這樣實現頁面切換的
- Flutter進階
- 22丨如何構造炫酷的動畫效果?
- 23丨單線程模型怎么保證UI運行流暢?
- 24丨HTTP網絡編程與JSON解析
- 25丨本地存儲與數據庫的使用和優化
- 26丨如何在Dart層兼容Android-iOS平臺特定實現?(一)
- 27丨如何在Dart層兼容Android-iOS平臺特定實現?(二)
- 28丨如何在原生應用中混編Flutter工程?
- 29丨混合開發,該用何種方案管理導航棧?
- 30丨為什么需要做狀態管理,怎么做?
- 31丨如何實現原生推送能力?
- 32丨適配國際化,除了多語言我們還需要注意什么
- 33丨如何適配不同分辨率的手機屏幕?
- 34丨如何理解Flutter的編譯模式?
- 35丨HotReload是怎么做到的?
- 36丨如何通過工具鏈優化開發調試效率?
- 37丨如何檢測并優化FlutterApp的整體性能表現?
- 38丨如何通過自動化測試提高交付質量?
- Flutter綜合應用
- 39丨線上出現問題,該如何做好異常捕獲與信息采集?
- 40丨衡量FlutterApp線上質量,我們需要關注這三個指標
- 41丨組件化和平臺化,該如何組織合理穩定的Flutter工程結構?
- 42丨如何構建高效的FlutterApp打包發布環境?
- 43丨如何構建自己的Flutter混合開發框架(一)?
- 44丨如何構建自己的Flutter混合開發框架(二)?
- 結束語
- 結束語丨勿畏難,勿輕略