作者|阿里巴巴科技

谷歌的 Flutter 爲開發人員提供了一種構建 Android 和 iOS 原生用戶界面的方法,爲開發人員減少了很多相關的負擔,包括幫助他們節省時間。有些人可能想知道 Flutter 項目和原生 Android 或 iOS 項目有何不同,例如它們的渲染機制或事件傳遞機制,或者在出現問題時開發人員如何修復錯誤和實現項目。

爲了回答這些問題,這篇文章將以“hello_flutter”爲例,首先介紹 Flutter 的設計原則,然後討論定製和優化,併爲對 Flutter 感興趣的開發人員提供了一些可遵循的步驟。

Flutter 基礎

架構

Flutter 的架構包含三個不同的層:框架、引擎和嵌入器。

Flutter 的框架使用 Dart 實現,提供了 Material 風格的小部件、Cupertino 風格的小部件(用於 iOS)、文本 / 圖像 / 按鈕小部件、渲染、動畫、手勢等。該層的核心代碼包含了 flutter 代碼庫的包和 sky_engine 代碼庫的包(dart:ui 庫提供了 flutter 框架和引擎之間的接口),例如 io、async 和 ui 包。

Flutter 的引擎是用 C++ 實現的,幷包含了 Skia、Dart 和 Text。Skia 是一個開源的 2D 圖形庫,爲各種硬件和軟件平臺提供通用 API。它是谷歌 Chrome、Chrome OS、Android、Mozilla Firefox、Firefox OS 等產品的圖形引擎。支持的平臺包括 Windows7+、macOS 10.10.5+、iOS8+、Android4.1+、Ubuntu14.04+ 等。

引擎的 Dart 部分主要包括 Dart 運行時和垃圾回收(GC)。如果 Flutter 在調試模式下運行,則還包括 JIT(Just in Time)支持,而如果是在發佈模式下,則通過 AOT(Ahead of Time)將 Dart 代碼編譯爲原生的“arm”代碼,這個時候就沒有 JIT。Text 是指以下的文本渲染庫:libtxt 庫(用於字體選擇和分隔線),派生自 minikin 和 HartBuzz(用於字形和圖形)。Skia 充當渲染後端,在 Android 上使用 FreeType 渲染,在 iOS 上使用 Fuchsia 和 CoreGraphics 渲染。

嵌入器可將 Flutter 嵌入到各種平臺中。它的主要任務是渲染 Surface 設置、線程設置和插件。我們可以看到,Flutter 的平臺相關層是最小的,其中平臺(例如 iOS)只提供畫布,其餘與渲染相關的邏輯發生在 Flutter 內部,從而實現良好的跨平臺一致性。

工程結構

本文中使用的開發環境是 Flutter beta v0.3.1,對應的引擎提交標籤爲 09d05a389。

“hello_flutter”示例項目的工程結構如下:

在上面的例子中,“ios”是 iOS 的代碼,使用 CocoaPods 來管理依賴項,“android”是 Android 的代碼,使用 Gradle 來管理依賴項,“lib”是 Dart 代碼,使用 pub 來管理依賴項。pub 中與 Cocoapods 的 Podfile 和 Podfile.lock 相對應的分別是 pubspec.yaml 和 pubspec.lock。

模式

Flutter 支持常見的模式,包括調試、發佈和分析,但它們之間存在一些區別。

Flutter 的調試模式對應於 Dart 的 JIT 模式,也稱爲檢查模式或慢模式,並支持 iOS 和 Android 的設備和模擬器。在這個模式下,可以啓用斷言功能,包括所有調試信息、服務擴展和調試輔助工具,如“observatory”。這個模式針對快速開發進行了優化,但並沒有針對運行速度、程序包大小或部署進行過優化。在這種模式下,採用了基於 JIT 的編譯技術,支持流行的亞秒級有狀態熱重載。

Flutter 的發佈模式對應於 Dart 的 AOT 模式,該模式的目標是部署到最終用戶的設備上,支持真實設備而不是模擬器。在此模式下,所有斷言都被禁用,爲了儘可能多地刪除調試信息,還會禁用所有調試工具。這個模式針對快速啓動、快速運行和包大小進行了優化,同時禁止所有調試輔助和服務擴展。

Flutter 的分析模式與發佈模式類似,只是添加了對服務擴展和跟蹤的支持,並最小化使用跟蹤信息所需的依賴性。例如,“observatory”可以連到進程上。分析模式不支持模擬器,因爲模擬器上的診斷不能代表實際性能。

由於 Flutter 的分析模式和發佈模式在編譯方面沒有差異,因此本文僅討論調試模式和發佈模式。

實際上,使用 Flutter 開發的 iOS 或 Android 項目仍然是標準的 iOS 或 Android 項目。Flutter 通過在 BuildPhase 中添加 shell 來生成並嵌入 App.framework 和 Flutter.framework(iOS),並通過 Gradle 添加 flutter.jar 和 vm/isolate_snapshot_data/instr(Android)來編譯相關代碼並將其嵌入到原生應用程序中。因此,本文主要討論 Flutter 引入的構建和運行原則。儘管編譯目標包括 arm、x64、x86 和 arm64,但它們的原理都很類似,所以本文僅討論與 arm 相關的原則。(如果沒有特殊描述,Android 默認爲 armv7。)

iOS 的代碼編譯和執行

在發佈模式下編譯

在發佈模式下,iOS 項目的 Dart 代碼構建過程如下:

在圖中,gen_snapshot 是 Dart 編譯器,它使用搖樹優化技術(類似於可生成最小包的依賴樹邏輯,因此在 Flutter 中禁用 Dart 支持的反射)生成彙編形式的機器碼,然後通過編譯工具鏈(如 xcrun)生成最終 App.framework。換句話說,對於所有的 Dart 代碼,包括業務邏輯代碼和第三方軟件包代碼,它們所依賴的 Flutter 框架(Dart)代碼最終會變成 App.framework。

搖樹優化功能位於 gen_snapshot 中。要查看相應的邏輯,可以訪問:

engine/src/third_party/dart/runtime/vm/compiler/aot/precompiler.cc

Dart 代碼最終對應於 App.framework 的符號如下:

事實上,與 Android 類似,App.framework 也包括了四個部分:kDartVmSnapshotData、kDartVmSnapshotInstructions、kDartIsolateSnapshotData 和 kDartIsolateSnapshotInstructions。爲什麼 iOS 使用 App.framework 而不是像 Android 那樣的四個文件?由於 iOS 系統的限制,Flutter 引擎無法在運行時將內存頁標記爲可執行,而在 Android 下則可以。

Flutter.framework 對應於 Flutter 架構中的引擎和嵌入器。實際上,Flutter.framework 位於 flutter 代碼庫的 /bin/cache/artifacts/engine/ios * 中,默認是從谷歌的代碼庫中提取的。當需要自定義變更時,可以使用 Ninja 系統下載和構建與引擎相關的代碼。

Flutter 相關代碼的最終產物是 App.framework(由 Dart 代碼生成)和 Flutter.framework(引擎)。在 Xcode 項目中,Generated.xcconfig 描述了與 Flutter 相關的環境配置信息,在 Runner 項目設置中新增的 xcode_backend.sh 實現了一個副本(從 flutter 框架代碼庫到 Runner 項目根目錄下的 Flutter 目錄)和 Flutter.framework 的嵌入以及 App.framework 的編譯和嵌入。

最終生成的 Runner.app 中與 Flutter 相關的文件如下:

flutter_assets 是 Flutter 資源,App.framework 和 Flutter.framework 是代碼,位於 Frameworks 目錄中。

在發佈模式下運行

與 Flutter 相關的渲染、事件和通信處理的邏輯如下:

Dart 主函數的調用棧如下:

在調試模式下編譯

在調試模式下,Flutter 的編譯結構與發佈模式中的編譯結構類似。差異主要表現在兩點:

1.Flutter.framework

在調試模式下,框架包含 JIT 支持,而在發佈模式下沒有 JIT 支持。

2.App.framework

與 Flutter.framework 不同,App.framework 是原生機器碼,與 AOT 模式中的 Dart 代碼對應,而在 JIT 模式下,App.framework 只有幾個簡單的 API,Dart 代碼存在於 snapshot_blob.bin 文件中。這部分代碼的快照是帶有簡單標記的源代碼的腳本快照。所有的註釋和空格字符都被移除,常量被規格化,不存在機器代碼、搖樹優化或代碼混淆。

App.framework 中的符號表如下:

針對 Runner.app/flutter_assets/snapshot_blob.bin 上運行 strings 命令可以查看以下內容:

調試模式下主入口的調用棧如下:

Android 的代碼編譯和執行

除了一些與平臺相關的功能之外,Android 其他邏輯(例如對應於 AOT 的發佈模式和對應於 JIT 的調試模式)與 iOS 非常相似,只需注意兩個關鍵差異。

在發佈模式下編譯

在發佈模式下,Android Flutter 項目中的 Dart 代碼結構如下:

vm/isolate_snapshot_data/instr 是 arm 指令,引擎會在運行時加載它們並將其標記爲可執行。vm_snapshot_data/instr 用於初始化 DartVM,調用入口爲 Dart_Initialize(Dart_api.h)。isolate_snapshot_data/instr 對應於創建新隔離的 App 代碼,調用入口爲 Dart_CreateIsolate(Dart_api.h)。

Flutter.jar 類似於 iOS 的 Flutter.framework,包括引擎代碼(Flutter.jar 中的 libflutter.so)以及一組將 Flutter 嵌入到 Android 中的類和接口(FlutterMain、FlutterView、FlutterNativeView 等)。事實上,flutter.jar 位於 Flutter 代碼庫的 /bin/cache/artifacts/engine/android* 中,默認從谷歌代碼庫中拉取。當需要自定義更改時,可以使用 Ninja 系統下載引擎源代碼來生成 flutter.jar。

以 isolate_snapshot_data/instr 爲例,運行 disarm 命令的結果如下:

其 APK 結構如下:

在全新安裝 APK 之後,使用時間戳(結合 versionCode 與 packageinfo 的 lastUpdateTime)來決定是否將 Flutter 相關文件複製到本地 app 數據目錄。複製的內容如下:

isolate/vm_snapshot_data/instr 最終位於 app 本地數據目錄中,該目錄是可寫的。因此,可以通過下載和替換這些快照就可以完成 app 的替換和更新。

在發佈模式下執行

下圖顯示了發佈模式下的執行流程:

在調試模式下編譯

與 iOS 的情況一樣,Android 中的調試模式和發佈模式之間的區別主要在於以下兩個組件:

1.flutter.jar

這裏的區別與之前針對 iOS 所描述的完全相同。

2.app 代碼

app 代碼位於 flutter_assets 下的 snapshot_blob.bin 中,就像 iOS 一樣。

在介紹完 Flutter 有關 iOS 和 Android 的編譯原理後,我們將重點介紹如何配置 Flutter 及其引擎,以進行自定義和優化。因爲 Flutter 使用了敏捷開發模式,所以當前出現的問題在未來可能就不是問題。因此,以下部分不着重於如何解決問題,而是着重於不同類型的場景,這些場景體現了 Flutter 自定義和優化方面的原則。

在 Flutter 中進行定製和優化開發

Flutter 是一個複雜的系統。除了上面提到的三層架構,它還包括 Flutter Android Studio(Intellij)插件、pub 代碼庫管理和其他各種組件。不過,定製和優化通常與 Flutter 的工具鏈有關,代碼位於 Flutter 代碼庫的 flutter_tools 包中。我們現在將分別介紹如何針對 Android 和 iOS 進行自定義。

自定義 Android

自定義 Android 涉及 flutter.jar、libflutter.so(在 flutter.jar 中)、gen_snapshot、flutter.gradle 和 flutter_tools。在自定義 Flutter 時,需要注意以下事項:

1. 將 Android 中的目標設置爲 armeabi

這是構建過程的一部分,邏輯是在 flutter.gradle 中定義的。如果要應用程序通過 armeabi 支持 armv7/arm64,必須修改 Flutter 的默認邏輯,如下所示:

由於 Gradle 本身的特性,這部分可以在修改後生效。

2. 將 Android 設置爲在啓動時默認使用第一個可啓動的 Activity

這部分與 flutter_tools 有關,修改如下:

這裏的要點不是如何修改它,而是如何讓更改生效。原則上,使用“flutter run/build/analyze/test/upgrade”這樣的命令實際上運行的是 Flutter 腳本(flutter_repo_dir/bin/flutter),然後再通過這個腳本運行 flutter_tools.snapshot(由 packages/flutter_tools 生成)。邏輯如下:

if [[ ! -f "SNAPSHOT_PATH" ]] || [[ ! -s "STAMP_PATH" ]] || [[ "(cat "STAMP_PATH")" != "revision" ]] || [[ "FLUTTER_TOOLS_DIR/pubspec.yaml" -nt "$FLUTTER_TOOLS_DIR/pubspec.lock" ]]; then

rm -f "$FLUTTER_ROOT/version"

touch "$FLUTTER_ROOT/bin/cache/.dartignore"

"$FLUTTER_ROOT/bin/internal/update_dart_sdk.sh"

echo Building flutter tool...

if [[ "$TRAVIS" == "true" ]] || [[ "$BOT" == "true" ]] || [[ "$CONTINUOUS_INTEGRATION" == "true" ]] || [[ "$CHROME_HEADLESS" == "1" ]] || [[ "$APPVEYOR" == "true" ]] || [[ "$CI" == "true" ]]; then

PUB_ENVIRONMENT="$PUB_ENVIRONMENT:flutter_bot"

fi

export PUB_ENVIRONMENT="$PUB_ENVIRONMENT:flutter_install"

if [[ -d "$FLUTTER_ROOT/.pub-cache" ]]; then

export PUB_CACHE="${PUB_CACHE:-"$FLUTTER_ROOT/.pub-cache"}"

fi

while : ; do

cd "$FLUTTER_TOOLS_DIR"

"$PUB" upgrade --verbosity=error --no-packages-dir && break

echo Error: Unable to 'pub upgrade' flutter tool. Retrying in five seconds...

sleep 5

done

"$DART" --snapshot="$SNAPSHOT_PATH" --packages="$FLUTTER_TOOLS_DIR/.packages" "$SCRIPT_PATH"

echo "$revision" > "$STAMP_PATH"

fi

很明顯,如果要重建 flutter_tools,可以刪除 flutter_repo_dir/bin/cache/flutter_tools.stamp(以便重新生成它),或者註釋掉 if/fi(每次都重新生成)。

3. 在調試模式下發布 Flutter

如果你發現 Flutter 在開發中出現延遲,並且猜測這可能是由邏輯或調試模式引起的,那麼可以在發佈模式下構建 APK,或者將 Flutter 強制改爲爲發佈模式,如下所示:

自定義 iOS

與自定義 iOS 相關的內容包括 Flutter.framework、gen_snapshot、xcode_backend.sh 和 flutter_tools。在自定義 Flutter 時,需要注意以下事項:

1. 在優化期間重複替換 Flutter.framework 導致的重新編譯

這部分的邏輯與構建有關,位於 xcode_backend.sh 中。爲了確保每次都能獲得正確的 Flutter.framework,Flutter 每次都會根據配置查找並替換 Flutter.framework(請參閱 Generated.xcconfig 配置)。不過,這會導致重新編譯依賴於這個框架的項目代碼。必要的修改如下:

2. 在調試模式下發布 Flutter

要進行這個自定義,請將 Generated.xcconfig 中的 FLUTTER_BUILD_MODE 更改爲“Release”,將 FLUTTER_FRAMEWORK_DIR 更改爲與“Release”對應的路徑。

3. 設置對 armv7 的支持

有關此方案的原始文檔,請參閱 https://github.com/flutter/engine/wiki/iOS-Builds-Supporting-ARMv7。

事實上,Flutter 本身在 iOS 中支持 armv7,但目前官方還沒有提供支持,因此必須修改相關邏輯,如下所示:

a. 生成默認邏輯:

Flutter.framework(arm64)

b. 修改 Flutter,以便每次都可以重建 flutter_tools。修改 build_aot.Dart 和 mac.Dart,將 iOS 的相關 arm64 更改爲 armv7,並將 gen_snapshot 更改爲 i386。

可以通過以下命令生成 i386 的 gen_snapshot:

./flutter/tools/gn --runtime-mode=debug --ios --ios-cpu=arm

ninja -C out/ios_debug_arm

這裏有一種隱含的邏輯:

構造 gen_snapshot 的預定義宏(x86_64/__ i386 等)。目標 gen_snapshot 的結構和最終的 App.framework 結構必須保持一致。也就是說,使用 x86_64->x86_64->arm64 或 i386->i386->armv7。

c. 在 iPhone4S 上,當 gen_snapshot 生成不受支持的 SDIV 命令時會發生 EXC_BAD_INSTRUCTION(EXC_ARM_UNDEFINED)錯誤,這可以通過向 gen_snapshot(位於 build_aot.Dart 中)添加參數“——no-use-integer-division”來解決。其背後的邏輯如下:

d.“lipo -create”在 a 步驟和 b 步驟中生成的 Flutter.framework,以便生成支持 armv7 和 arm64 的 Flutter.framework。

e. 修改 Flutter.framework 中的 Info.plist,並刪除:

UIRequiredDeviceCapabilities

arm64

同樣,你必須在 App.framework 上執行相同的操作,以避免受到 AppStore 中應用程序細化的影響。

調試 Flutter 工具

在調試模式下構建 APK 時,如果你想知道 Flutter 的特定執行邏輯,可以採用以下方法:

a. 瞭解 flutter_tools 命令的參數。

b. 將 packages/flutter_tools 作爲 Dart 項目打開,並添加新的“Dart Command Line App”配置。將 Dart 文件設置爲“flutter_tools.Dart”,將工作目錄設置爲 Flutter 項目的路徑,並將 Program 參數設置爲先前獲得的參數。

自定義和調試引擎

請考慮以下情形。假設我們基於 Flutter beta v0.3.1 定製和開發服務,爲了確保穩定性,SDK 在某段時間內不會升級。同時,Flutter v0.3.1 的 master 分支上修改了一個 bug,標記爲 fix_bug_commit。你將如何應對這種情況?

1.Flutter beta v0.3.1 將其相應的引擎代碼提交指定爲 09d05a389。

請參閱:flutter/bin/internal/engine.version。

2. 獲取引擎代碼。

3. 由於 master 代碼是在第二步中獲得的,我們需要的是與特定提交相對應的代碼(09d05a389),所以需要從這次提交拉取一個新分支:custom_beta_v0.3.1。

4. 在 custom_beta_v0.3.1(commit:09d05a389)上運行“gclient sync”,獲取與 flutter beta v0.3.1 相對應的所有引擎代碼。

5. 使用“git cherry-pick fix_bug_commit”將 master 的變更同步到 custom_beta_v0.3.1。如果變更依賴了最新依賴項,則可能會發生編譯錯誤。

6. 運行以下命令應用與 iOS 相關的變更:

./flutter/tools/gn --runtime-mode=debug --ios --ios-cpu=arm

ninja -C out/ios_debug_arm

./flutter/tools/gn --runtime-mode=release --ios --ios-cpu=arm

ninja -C out/ios_release_arm

./flutter/tools/gn --runtime-mode=profile --ios --ios-cpu=arm

ninja -C out/ios_profile_arm

./flutter/tools/gn --runtime-mode=debug --ios --ios-cpu=arm64

ninja -C out/ios_debug

./flutter/tools/gn --runtime-mode=release --ios --ios-cpu=arm64

ninja -C out/ios_release

./flutter/tools/gn --runtime-mode=profile --ios --ios-cpu=arm64

ninja -C out/ios_profile

要調試 Flutter.framework 源代碼,請使用以下命令:

./flutter/tools/gn --runtime-mode=debug --unoptimized --ios --ios-cpu=arm64

ninja -C out/ios_debug_unopt

用生成的文件替換 Flutter 中的 Flutter.framework 和 gen_snapshot,這樣就可以調試引擎源代碼。

7. 最後,運行以下命令應用與 Android 相關的變更:

./flutter/tools/gn --runtime-mode=debug --android --android-cpu=arm

ninja -C out/android_debug

./flutter/tools/gn --runtime-mode=release --android --android-cpu=arm

ninja -C out/android_release

./flutter/tools/gn --runtime-mode=profile --android --android-cpu=arm

ninja -C out/android_profile

你可以使用生成的文件替換 flutter/bin/cache/artifacts/engine/android* 下的 gen_snapshot 和 flutter.jar,以便生成 Android 的 arm 和 debug/release/profile 文件包。

相關文章