Skip to content

Instantly share code, notes, and snippets.

@kitasuke
Created May 31, 2020 02:29
Show Gist options
  • Save kitasuke/e1dbd47839072b7297af57a343064dc5 to your computer and use it in GitHub Desktop.
Save kitasuke/e1dbd47839072b7297af57a343064dc5 to your computer and use it in GitHub Desktop.
umbrella-framework-jp.md

マルチモジュールなiOSアプリの起動速度改善

こんにちは。ソフトウェアエンジニアの@kitasukeです。

今回は、マルチモジュール化されたアプリでも上手く Static Framework を利用してアプリ起動時間を 約30% 短縮した方法を紹介します。 起動時間はプロジェクトの構成や使用するフレームワークに依存するので、必ずしも同じ効果が得られるかは保証しません。

前提

ある程度大きなコードベースなアプリを想定していて、依存管理ツールはCocoaPodsを使用しています。 またマルチモジュール化というのは、1つのワークスペースに複数のXcodeプロジェクトを用いてマルチモジュール化している状態を想定しています。 ただ、1つのXcodeプロジェクトに複数のターゲットがある状態でも、今回のアプローチは十分適用できると思います。

概要

Dynamic Framework を多用すると、一般的にアプリ起動速度は遅くなります。 特に、モジュールが複数ありそれぞれのモジュールで同じフレームワークを使っている場合などは影響が大きいです。 その代わりにStatic Frameworkにすれば起動速度が向上しますが、モジュール間で同じフレームワークを使うとシンボルの重複が起こるので注意が必要です。

そこで、 Umbrella Framework を利用して上記の問題を解決します。 下記の記事の内容をかなり参考にして作りました。

https://medium.com/eureka-engineering/create-merged-framework-to-cut-appstartuptime-72ee67b2bbab

Umbrella Framework

Umbrella Frameworkとは、複数のフレームワークをまとめたフレームワークです。 Dynamic FrameworkであるUmbrella Frameworkに複数モジュールで使うStatic Frameworkをまとめて、Umbrella Framework自体を各モジュールにリンクすることでStatic Frameworkを最大限に活用しながらシンボルの重複を防ぎます。

依存関係は下記の画像のようになります。 今回のアプリは、RxSwiftを利用してリアクティブプログラミングを各モジュールで活用して、通信処理やモデル定義でProtocol Buffersを広く活用しているので、それらのフレームワークをUmbrella Frameworkにまとめて入れています。 それを AppCoreAppEntity にリンクして、それらをメインターゲットの App にリンクしています。

!!!画像!!!

注意すべき点として、純粋なUmbrella Framework、つまりEmbedded framework in embedded frameworkは下記のドキュメントの通りiOSでサポートされていません。 フレームワークパスの設定を適切にすればビルド自体はできますが、アプリのアーカイブ・アップロードでバリデーションエラーになります。 具体的にどう対応したかを次で解説します。

https://developer.apple.com/library/archive/technotes/tn2435/_index.html#//apple_ref/doc/uid/DTS40017543-CH1-PROJ_CONFIG-APPS_WITH_DEPENDENCIES_BETWEEN_FRAMEWORKS

アプローチ

フレームワークパスの設定やStatic Frameworkの生成は基本的にCocoaPodsに任せて、追加で必要な設定をマニュアルで行います。 それらの設定はxcconfigで行うのをお勧めしますが、今回はその説明を省きます。

アプリ構成

上記の依存関係の図のように、プロジェクト構成は以下のとおりです。

// UmbrellaApp.xcworkspace
- UmbrellaApp       // メインターゲット
- UmbrellaCore      // 通信処理などのコア機能
- UmbrellaEntity    // protobufで生成するモデル
- UmbrellaFramework
- Pods

Podfileは下記のとおりです。 UmbrellaCoreとUmbrellaEntityでは、UmbrellaFramework以外のフレームワークは不要なのでPodfileでやることは特にないです。

source "https://cdn.cocoapods.org/"
platform :ios, '13.0'
use_frameworks!

workspace 'UmbrellaApp'
target 'UmbrellaApp' do

  pod 'Firebase/Analytics'
  pod 'Firebase/Crashlytics'
  pod 'Firebase/Messaging'
end

target 'UmbrellaFramework' do
  project 'UmbrellaFramework/UmbrellaFramework'

  pod 'RxSwift'
  pod 'RxRelay'
  pod 'RxCocoa'
  pod 'SwiftGRPC'
end

Static Framework

Static Frameworkの生成はCocoaPodsプラグインの cocoapods-static-swift-framework で行います。 利用方法は、gem install cocoapods-static-swift-framework でインストールして、Podfileに plugin 'cocoapods-static-swift-framework' を追加するだけです。

https://github.com/leavez/cocoapods-static-swift-framework

UmbrellaFramework

フレームワークパスの設定はCocoaPodsが面倒を見てくれますが、それだけでは不十分です。

まずは export.swift ファイルを追加して、各ライブラリのシンボルを外部モジュールに公開します。

@_exported import RxCocoa
@_exported import RxRelay
@_exported import RxSwift
@_exported import SwiftGRPC
@_exported import SwiftProtobuf

@_exported import xxx は、他のモジュールから参照する際に、あたかも自身のモジュールから参照するのと同等にシンボルを公開できる機能です。

https://forums.swift.org/t/exported-and-fixing-import-visibility/9415

次に、Static FrameworkのRxSwiftを利用するので、 Other Linker Flags-all_load を追加します。 このオプションで全オブジェクトファイルをロードを明示的に行えます。 これがないと下記のようなランタイムエラーが発生するはずです。

ld: warning: Could not find or use auto-linked library 'RxSwift'
ld: warning: Could not find or use auto-linked library 'RxCocoa'
ld: warning: Could not find or use auto-linked library 'RxRelay'

RxSwiftでなくても同じ現象が発生する可能性があるので、どちらにせよ追加するのも良いかもしれません。

ReactiveX/RxSwift#1799

UmbrellaCore, UmbrellaEntity

次に、UmbrellaFrameworkのリンク先での設定です。

上記で説明した通り、メインターゲットに追加するEmbedded Frameworkに対してEmbedded Frameworkは追加できません。 したがって、 Do Not Embed にしてUmbrella Frameworkを両モジュールに追加しましょう。

!!!画像!!!

Do Not Embedにする場合は自身でフレームワークパスの設定が必要です。 Framework Search Paths${BUILT_PRODUCTS_DIR}/UmbrellaFramework.framework を、 Header Search Paths${BUILT_PRODUCTS_DIR}/UmbrellaFramework.framework/Headers を追加します。

ここでも同様の目的で export.swift を追加して、UmbrellaFrameworkを外部モジュールに公開します。

@_exported import UmbrellaFramework

今回のアプリはUmbrellaCore、UmbrellaEntity共にメインターゲットにしかリンクしないので、 Mach-O TypeStatic Library に変更してStatic Frameworkにしました。

UmbrellaApp

上記の通りEmbedded framework in embedded frameworkはサポートされていないので、代わりにメインターゲットであるUmbrellaAppにUmbrellaFrameworkをEmbedします。 Embedded frameworkとして追加する場合はフレームワークパス設定もすべて面倒を見てくれるので、特にマニュアルで設定することはないです。

!!!画像!!!

UmbrellaTestsFramework

RxTestなど、UmbrellaFrameworkにまとめたフレームワークに依存を持つテストフレームワークを使う場合に注意する点があります。 それは両方のビルドを同じXcodeプロジェクト内で行わないと、下記のようにフレームワークを使用する側でランタイムのデマングルエラーが発生することです。

failed to demangle superclass of xxx from mangled name '�xxx'

それを回避するための、下記のようにUmbrellaFrameworkを継承するUmbrellaTestsFrameworkを作成して、そこにRxTestを追加します。

target 'UmbrellaFramework' do
  project 'UmbrellaFramework/UmbrellaFramework'

  pod 'RxSwift'
  pod 'RxRelay'
  pod 'RxCocoa'
  pod 'SwiftGRPC'

  target 'UmbrellaTestsFramework' do
    inherit! :search_paths
    pod 'RxTest'
  end
end

UmbrellaTestsFrameworkを直接メインターゲットのUmbrellaAppにリンクするとシンボルの重複でコンパイルエラーになります。 そこで、RxTestだけをUmbrellaAppのテストターゲットであるUmbrellaAppTestsの Link Binaries With Libraries に追加します。 Framework Search Paths${PODS_CONFIGURATION_BUILD_DIR}/RxTest を、 Header Search Paths${PODS_CONFIGURATION_BUILD_DIR}/RxTest/Headers を追加してフレームワークパスを設定します。 これが正攻法かどうかは不明ですが、これ以外にやり方が思いつきませんでした。

起動時間の計測方法

DYLD_PRINT_STATISTICS という環境変数を利用すれば、アプリ起動時の処理時間の計測が可能です。 下記のように、 Environment Variables のNameとValueに追加してアプリを起動すればコンソールに結果が表示されます。

!!!画像!!!

まとめ

使うフレームワークが多くなればなるほどアプリ起動速度が気になると思います。 その問題に対して、今回のアプローチで完全に解決できるわけではなく、また効果もアプリ次第です。 ただそれなりの効果は期待できるので、試して見る価値はあると思います。 若干ハックのようなことをやっているので、その辺に関して質問あればいつでもどうぞ。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment