Airing

Airing

哲学系学生 / 小学教师 / 程序员,个人网站: ursb.me
github
email
zhihu
medium
tg_channel
twitter_id

Flutter 産物分析と減包方案

混合開発シーンにおいて、Flutter のパッケージの増分がやや大きいことは、皆から批判されている点ですが、Google の公式は Flutter が動的化をサポートしないことを明言しており、現在 Flutter SDK の公式にはカスタマイズのためのソリューションが提供されていません。したがって、軽量化を望む場合は、自分で手を動かして工夫するしかありません。

いわゆる減パッケージの前提条件は、製品の内容が何であるかを知ることです。製品の中でどの部分を削減できるか?削減された部分をどのように戻すか?したがって、本記事では「製品分析」と「減パッケージ方案」という 2 つのテーマを中心に、iOS と Android の両方における Flutter の減パッケージ原理と方案について論じます。

では、まず iOS 側から始めましょう。

注:本記事のデータとコードスニペットは、Flutter 1.17.1 に基づく Flutter モジュールが Release(AOT アセンブリ)モードで構築された後の製品からのものであり、何の圧縮も行われていません。

1. iOS 編#

1.1 製品構成#

flutter build ios-frameworkを使用することで、Flutter モジュールを iOS ホストに統合するためのフレームワークに構築できます。この統合方法を製品統合と呼び、この「製品」は Flutter 製品であり、以下のいくつかの部分で構成されています:

  1. App.framework
    • App: これは Dart ビジネスコードの AOT 製品です
    • flutter_assets: Flutter の静的リソースファイル
  2. Flutter.framework
    • Flutter: Flutter エンジンのコンパイル製品
    • icudtl.dat: 国際化サポートデータファイル

製品を出力した後、ターミナルで各部分のサイズを表示できます。最終的に整理した iOS 側の Flutter 製品構造は以下の図のようになります:

image

注意が必要なのは、Mac Finder で表示されるサイズは大きく表示されることで、その換算倍率は 1000 であり、1024 ではありません。表示されたサイズをコマンドラインで取得した後、手動で計算して実際のサイズを得る必要があります。

さらに、エンジン製品のサイズは、プロファイルモード(arm64+arm32)のサイズを選択しています。Flutter 1.17.1 リリースにはバグが存在し、ビットコードが圧縮できないため、サイズが 351.47MB となり、分析に影響を与えています。具体的な理由については、Flutter app size is too big · Issue #45519を参照してください。

1.2 減パッケージ方案#

減パッケージの基本方法は 2 つあります:

  1. 製品削除:製品の中で不要な部分を直接削除します
  2. 製品移動:一時的に移動できる部分を遠隔で配信するように変更し、製品の読み込みロジックを変更して Flutter が遠隔で配信された部分の製品を動的に読み込むことをサポートします

前述の製品構造に基づいて、製品の減パッケージを実現します。まずは App.framework の App 部分からです。

1.2.1 App.framework/App#

方案について話す前に、App.framework の下の App がどのように構築されているかを見てみましょう。以下の図のようになります:

image

まず、frontend_server が Dart ソースコードを中間製品 dill にコンパイルします。以下のコマンドを実行することで、同様のコンパイル効果を得ることができます:

image

app.dill はバイナリバイトコードであり、string app.dill を通じて、実際には Dart コードが結合された後の製品であることがわかります:

image

Dart が開発モードで提供する Hot Reload は、実際には変更されたコードを frontend_server を通じてコンパイルし、新しいカーネル(app.dill.incremental.dill)を生成し、WS を介して Dart VM に送信した後、全体のツリーを再構築することによって Hot Reload を実現します。

image

その後、2 つのプラットフォーム側の gen_snapshot を通じてコンパイルし、IL 命令セットと最適化コードを得て、最終的にアセンブリ製品を出力します。アセンブリ製品は xcrun ツールを通じて単一アーキテクチャの App 製品を得て、最終的に lipo を通じて二重 ARM アーキテクチャの App 製品を得ます。したがって、ここで示す App.framework の下の App のサイズは二重アーキテクチャのものです。

ARMv7: iPhone 5s 以前の iOS デバイス。
ARM64: iPhone 5s 以降の iOS デバイス。

次に、製品削除と製品移動の 2 つの側面から、この製品のサイズを減らす方法を説明します。

製品削除#

image

この部分のサイズは Dart コードの AOT 後の製品であり、サイズが大きく、私たちの減パッケージプロセスで重点的に注目する対象です。

前述の減パッケージの基本方法に従い、まず「製品削除」を試みて、直接削除できるものがあるかを見てみましょう。Flutter が提供するサイズ分析ツールを使用すると、サイズのグラフを直接得ることができます:

image

実際に 2 つのライブラリがビジネスで使用されていないことがわかり、依存関係を直接削除できます。

さらに、コードサイズを減らすためのいくつかの最適化もあります:

  • 不合理な構文を禁止するために linter を設定する:例えば、明示的な型変換など、コンパイル前に大量の try-catch が追加され、コードサイズが大きくなります。
  • Dart コードを混乱させる: 0.75MB (2.5%) ↓

また、いくつかのシンボルを削除することで減パッケージ効果を得ることもできます。

  • スタックトレースシンボルを使用しない:1.8MB (6.2%) ↓
  • dSYM シンボルテーブル情報ファイルを削除:5.8MB (20%) ↓

注:dSYM は 16 進数の関数アドレスマッピング情報を保存する中間ファイルで、デバッグ用のシンボルを含み、クラッシュレポートファイルを分析して正しいエラー関数情報を解析するために使用されます。

製品移動#

次に、「製品移動」を実現する方法を見てみましょう。そのためには、App.framework/App の内容を具体的に分析する必要があります。前述の通り、これは Dart コードの AOT 後の製品であり、主に 4 つの AOT スナップショットライブラリ(スナップショット)で構成されています:

  • kDartIsolateSnapshotData: Isolate スナップショットデータ、これは Dart ヒープの初期状態であり、isolate 専用の情報を含みます。
  • kDartIsolateSnapshotInstructions: Isolate スナップショット命令、Dart isolate が実行する AOT 命令を含みます。
  • kDartVmSnapshotData: Dart VM スナップショットデータ、isolate 間で共有される Dart ヒープの初期状態。
  • kDartVmSnapshotInstructions: Dart VM スナップショット命令、VM 内のすべての Dart isolate 間で共有される一般的なルーチンの AOT 命令を含みます。

詳細は公式 Wiki の紹介を参照してください:https://github.com/flutter/flutter/wiki/Flutter-engine-operation-in-AOT-Mode

同じプロセス内に多くの Isolate を持つことができますが、2 つの Isolate のヒープは共有できません。Dart VM 開発チームは早くから相互作用の問題を考慮し、UI スレッド内で Isolate 間の相互作用の橋渡しを行う VM Isolate を設計しました。Dart VM 内の isolate 間の関係は以下の図のようになります:

image

したがって、isolate に対応する AOT スナップショットは kDartIsolateSnapshot であり、命令セクションとデータセクションに分かれます。VM Isolate に対応する AOT スナップショットは kDartVmSnapshot であり、こちらも命令セクションとデータセクションに分かれます。

以上の分析に基づき、App.framework の構造をさらに分解すると、以下の図のようになります:

image

私たちはApp Store の審査規則が動的に配信される実行可能なバイナリコードを許可しないことを知っています。したがって、上記の 4 つのスナップショットについては、データセクションの内容(kDartIsolateSnapshotData と kDartVmSnapshotData)を配信することしかできず、命令セクションの内容(kDartIsolateSnapshotInstructions と kDartVmSnapshotInstructions)は依然として製品内に残す必要があります。

では、このスナップショットライブラリをどこで分離すればよいのでしょうか?

Dart VM が起動する際のデータ読み込み段階で、以下の図のように、settings 内のスナップショットライブラリの読み込みパスを変更することで実現できます:

image

変更後の具体的な実装については、本記事では説明しませんが、《Q 音ライブ Flutter パッケージ裁剪方案 (iOS)》の記事に詳細なコード変更の紹介があります。

1.2.2 App.framework/flutter_assets#

image

flutter_assets は Flutter モジュールで使用されるローカル静的リソースであり、この部分は「削除」することはできず、「移動」するしかありません。私たちは 2 つの方案を用いて製品移動を実現します —— 通常の方案は、Dart VM の起動時のデータ読み込み段階で settings 内の flutter_assets パスを変更し、リモート読み込みを実現します。通常の場合、この方法を使用することで flutter_assets を削除できます。

では、Flutter エンジンのコードを変更せずに flutter_assets を削除する方法はあるのでしょうか?あります。CDN 画像 + ディスクキャッシュ + プリロードの組み合わせ方案を使用して同様の効果を実現できます。手順は以下の通りです:

  1. 画像コンポーネントをラップし、コンパイルモードに応じてローカル画像またはネットワーク画像を使用します。開発環境ではローカル画像を使用して迅速に開発し、製品環境では CDN 画像を使用します。
  2. CI を改造し、継続的インテグレーション時に flutter_assets を削除し、パッケージ内の画像を CDN に公開します。
  3. 画像コンポーネントの能力を拡張し、cached_network_image を導入してディスクキャッシュをサポートします。
  4. Flutter モジュールが読み込まれる際に、precacheImageメソッドを使用して CDN 画像をプリロードします。

この方案はやや面倒であり、環境を区別する必要があるため、やはり Flutter エンジンを変更して flutter_assets をリモートで読み込むことをお勧めします。

1.2.3 Flutter.framework/icudtl.dat#

image

icudtl.dat は国際化サポートデータファイルであり、直接削除することはお勧めできません。前述の製品移動の方案と同様に、Dart VM の起動時のデータ読み込み段階で settings 内の icudtl.dat パス(icu_data_path)を変更してリモート読み込みを実現します:

image

1.2.4 Flutter.framework/Flutter#

image

エンジン変更#

この部分は Flutter エンジン(C++)のコンパイル後のバイナリ製品であり、製品内で最も大きなサイズを占めています。現在、字節跳動の共有を参考にして《Flutter パッケージのサイズを 50% 近く削減する方法》において、最適化可能な部分は以下の 2 点です:

  1. コンパイル最適化
  2. エンジン裁剪

Flutter エンジンは LLVM を使用してコンパイルされており、リンク時最適化(LTO)には Clang 最適化レベルのコンパイルパラメータがあります。以下の図のように(buildroot 内):

image

ここで iOS プラットフォームのエンジンコンパイルパラメータを **-Os パラメータから - Oz パラメータに変更することで、最終的に700KB** 程度のサイズを削減できます。

image

また、エンジン裁剪には 2 つの部分があり、裁剪できます:

  1. Skia: 一部のパラメータを削除することで、性能に影響を与えずに200KBのサイズを削減できます。
  2. BoringSSL: クライアントプロキシリクエストを使用する場合、Dart HttpClient モジュールは不要であり、この部分は完全に削除できます。また、プロキシリクエストは HttpClient よりも性能が良いため、この部分は500KBのサイズを削減できます。

付録: https://github.com/flutter/flutter/issues/40345では、別の観点からのコンパイル最適化、すなわち関数コンパイル最適化について言及されています。同じ加算関数において、Dart の実装はコンパイル後に 36 の命令があり、Objective-C は 11 の命令しかありません。36 の命令の中には、先頭の 8 つと末尾の 6 つの整列命令があり、削除可能です。また、中間には 5 つのスタックオーバーフロー検査も削除可能です。** つまり、Dart コンパイル後の 36 の命令は 13 の命令に最適化できます。** ここは Google 公式の最適化を待つ必要があります。

エンジンコンパイル#

変更が完了したら、エンジンをコンパイルする必要があります。まず、Flutter エンジンのコンパイルに必要なツールを紹介します:

  • gclient:ソースコードリポジトリ管理ツールで、元々は chromium で使用されており、ソースコードと対応する依存関係を管理できます。gclient を使用して、コンパイルに必要なすべてのソースコードと依存関係を取得します。
  • gn:ninja コンパイルに必要なビルドファイルを生成します。特に Flutter のように多くのオペレーティングシステムプラットフォームと多くの CPU アーキテクチャを跨ぐ場合、gn を使用して多くの異なる ninja ビルドファイルを生成する必要があります。
  • ninja:コンパイルツールで、最終的なコンパイル作業を担当します。

コンパイルツールの詳細は Flutter 公式 Wiki を参照してください:Setting up the Engine development environment - Flutter wiki

具体的なコンパイルは 3 つのステップに分かれます。まず、.gclient ファイルを作成し、ソースコードとすべての対応する依存関係を取得します。以下の図のようになります:

image

次に、gclient syncを実行して依存関係をダウンロードします。

これらの変更はすべて依存関係(buildroot、skia など)であり、ソースコードではないため、私たちは Flutter エンジンをフォークし、依存関係を変更した後、対応する依存関係のコミット番号を取得してエンジンの DEPS ファイルに記入し、コードを提出した後にエンジンリポジトリの最新のコミット番号を取得して.gclient ファイルに記入する必要があります。

3 つ目のステップでは、ninja を使用して gn が生成した設定ファイルを使用してエンジンをコンパイルします。どのプラットフォームアーキテクチャのエンジンをコンパイルするかに応じて、gn を使用して設定を生成し、その後 ninja を実行してコンパイルします。以下の図のようになります:

image

最終的に、私たちはいくつかの(異なるプラットフォームアーキテクチャの)カスタマイズされたエンジンを得ることができ、それらを使用するのも簡単で、ローカルの Flutter SDK 内のエンジンを直接置き換えるだけです。

以上のように、各製品の内容に対する減パッケージ処理を行った結果、最終的な製品構造は以下の図のようになります:

image

1.3 減パッケージの効果#

iOS アプリのサイズを確認する方法は以下のようにいくつかあり、得られるサイズは異なります:

最初の方法は、ローカルで ipa を構築した後の分析レポートを確認することで、分析レポートには 2 つのサイズが提供されますが、注意が必要なのは、これらは未暗号化のものです:

  1. インストールパッケージサイズ:未暗号化のダウンロードサイズ
  2. 解凍後のサイズ:未暗号化の占有サイズ

しかし、App Store にアップロードした後はすべて暗号化されるため、ユーザーが最終的に見るサイズを知りたい場合は、App Store にアップロードしてレポートを確認する必要があります。ここでもレポートには 2 つのサイズが提供されます。以下の図のようになります:

image

それぞれは:

  1. ダウンロードサイズ
  2. インストールサイズ

ユーザーが最終的に App Store で見るのはインストールサイズです。

注:ただし、例外的な状況があり、Web ブラウザを使用して App Store にログインしてアプリのサイズを確認する場合、その時に表示されるサイズはダウンロードサイズです。Apple は、その時点で関心があるのはインストール占有サイズではないと考えています。

空白プロジェクトをホストエンジンとして使用して App Store にアップロードしたところ、アプリのサイズは18.7MB から 11.8MB に減少しました

2. Android 編#

Android 側の減パッケージ方案は比較的簡単で、App Store の審査規則の制限がないため、すべての製品を粗暴に移動し、動的に配信することができます。私たちは依然として製品構成、減パッケージ方案、減パッケージの効果を見て、Android 側の Flutter 減パッケージを確認します。

2.1 製品構成#

まず、Android 側の Flutter モジュール製品(Release)のコンパイルプロセスを見てみましょう。iOS と同様に、Dart ソースコードとエンジンの 2 つの部分で構成されています:

image

最終的な製品 flutter.gradle には以下が含まれます:

  1. libapp.so
  2. flutter.jar

その中で、flutter.jar は libflutter.so、icudtl.dat、およびいくつかの Java ファイルを含み、libflutter.so はエンジン製品であり、icudtl.dat は依然として国際化サポートファイルであり、最後のいくつかの Java はビジネス側が Flutter を呼び出すためのインターフェースを公開しています。

したがって、重要な製品構成は以下の表のようになります:

image

2.2 減パッケージ方案#

libflutter.so はエンジン製品であり、私たちは依然として裁剪カスタマイズを行うことができますが、必要性はあまり高くありません。なぜなら、Flutter 製品は Android 側で完全に動的に配信できるからです。手順は以下の通りです:

  1. libapp.so、libflutter.so、flutter_assets などのファイルを移動し、クラウドに公開します。
  2. flutter.jar 内の FlutterLoader.java のロジックをカスタマイズして、カスタム位置のライブラリパスを読み込むことで、動的読み込みを実現します。

具体的なコードはここでは示しません。

2.3 減パッケージの効果#

空白プロジェクトをホストとして使用し、減パッケージ前後の APK のサイズを測定したところ、6.2MB の Flutter 製品サイズを完全に削減できることがわかりました。

image

以上が両端の Flutter 減パッケージ方案であり、内容は比較的簡単で、先人の足跡を参考にしながら一歩一歩実践して得られた効果です。したがって、読者には末尾の 2 つの記事を延伸して読むことを強くお勧めします。さらなる学習を通じて理解を深めるためです。

参考記事:

  1. 《Q 音ライブ Flutter パッケージ裁剪方案 (iOS)》
  2. 《Flutter パッケージのサイズを 50% 近く削減する方法》
読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。