😇

Widget Extensionで起きたバイナリサイズの肥大化

2024/11/16に公開

※この記事は LuupのCM放映に合わせた「少し早いAdvent Calendar」 の16日目の記事です。

こんにちは。iOSエンジニアの山手です。普段は公共交通関連のiOSアプリ開発に携わり、Luupでは業務委託としてiOSアプリの機能改修や品質改善のお手伝いをさせていただいています。

今回は、LUUP iOSアプリに発生したバイナリサイズの肥大化問題についてご紹介いたします。

LUUP iOSアプリのマルチモジュール構成について

LUUP iOS版アプリでは、Swift Package Managerを用いたマルチモジュール構成を採用しています。
マルチモジュール構成への移行は、ビルド時間の短縮やレイヤー間の依存関係を明確にすることを目的としています。

Swift Package Managerの導入や現在のアーキテクチャについては、以下の記事に詳しく記載されていますので、ぜひご覧ください。

LUUP iOSアプリのプロジェクト構成
「機能開発を進める」「SwiftPMへ移行する」「両方」やらなくっちゃあならないってのが

現在のLUUP iOSアプリは39個のモジュールで構成されています。
マルチモジュール構成を運用する際、機能ごとの縦割り構造とレイヤーごとの横割り構造がモジュール分割の目安になりますが、LUUPではレイヤーごとにモジュールを分割する横割り構造を採用し、UIレイヤーのみ機能ごとの縦割り構造で画面ごとに細かくモジュールを区切っています。

.
└── Sources/
    ├── Common
    ├── DesignSystems
    ├── Entities
    ├── Libraries
    ├── Resources
    ├── Services
    ├── Store
    ├── UI
    ├── LiveActivity
    └── Widget

一部を割愛していますが、各モジュールは以下のような責務を持ち、追加の実装が発生した際には適切なモジュールに対して追加しています。

  • Common
    • アプリケーション上でグローバルに使われるビジネスロジックを持たない定数、環境変数を持つ
  • DesignSystems
    • DesignSystemに則ったカラー定義や共通コンポーネントを持つ
  • Entities
    • ドメインモデル、データモデルを持つ
  • Libraries
    • Exntesion,Protocol,Utilメソッドなど汎用的なコードを持つ
  • Resources
    • DesignSystemの定義外で、アプリケーション上で扱う画像や文字列を持つ
  • Services
    • データベースやAPIなどのロジックを持つ
  • Store
    • アプリケーション上でグローバルに扱う状態
  • UI
    • 各画面のUIの実装を持つ
    • 細かく画面ごとにモジュールが分割されている
  • LiveActivity
    • LiveActivity関連のコードを持つ
  • Widget
    • Widget関連のコードを持つ

Widget Extensionで起きたバイナリサイズの肥大化

LUUP iOSアプリケーションは、近くのポートから車両を探し自動設定ができる「クイックライドウィジェット」と、利用ステータスをLive Activityで表示する機能を提供しています。
利用ステータスをLive Activityで表示する機能に関しては、下記の記事に詳しく記載しておりますので、併せてご覧ください。
Live Activity をプロダクトに導入した話

WidgetとLive Activity機能のリリース後、LUUPのストアページを確認していたところストアページ記載のアプリサイズ容量が肥大化していることに気づきました。

肥大化に気づいた発端

原因を辿っていくと、WidgetとLive Activity向けのappexファイルの容量がアプリ本体並みに肥大化する事象が発生していました。

原因は、CommonモジュールやLibrariesモジュールの一部汎用ロジックを利用したため、WidgetやLive Activityのモジュールに依存関係を持たせたことにあります。
CommonモジュールやLibrariesモジュールにはユーザー認証を含むためFirebaseなどサードパーティーFrameworkへの依存がありますが、これによりWidget向けのApp ExtensionにFirebase等のサードパーティーFrameworkが含まれてしまい、バイナリサイズが大幅に増加していました。

Widget Extension
├── DesignSystems
│   ├── Entities
│   ├── Libraries // 以下の依存によってバイナリサイズが肥大化してしまった
│   │   ├── FirebaseAuth
│   │   ├── FirebaseFirestore
│   │   └── その他のサードパーティーFrameworkなど
│   └── Resources
└── Resources

これでは、ダウンロードサイズが大きくなり、ユーザーの負担も増加してしまいます。
特に出先でアプリを試してみたい時、ダウンロードサイズが大きいとダウンロードが躊躇われてしまう可能性があるため、アプリのバイナリサイズは極力最小限に抑える必要があります。

対策

対策として、モジュール間の依存関係を見直し、Widget Extensionの依存モジュールを最小限にしました。
外部Frameworkへの依存がないはずのモジュールも、運用の過程でサードパーティーFrameworkへの依存が生じたため、ロジックやリソース定義のみ残し、他Framework依存が必要なロジックは新規の共通モジュールに実装し、依存関係の整理しました。
これにより、Widget Extension側にサードパーティーFrameworkが含まれず、バイナリサイズも最小限に抑えられました。AppStoreConnect上のファイルサイズは、対応前と比較して22MBの削減が実現できました。

削減前 (1.89.0) 削減後 (1.90.0)
1.89.0_バイナリーサイズ 1.90.0_バイナリーサイズ

最後に

国内のiOSアプリ開発界隈でも、マルチモジュール構成を採用するプロダクトが増えています。
マルチモジュール構成によりビルド時間短縮や依存関係の明確化が可能ですが、追加実装の重ねる中で無意識に依存関係が複雑化し、モジュールが肥大化してしまいます。
今回のような事象を防ぐため、各モジュールの責務や依存関係を定期的に見直し、チーム内で目線を揃えることが重要だと感じました。

Luup Developers Blog

Discussion