iOS開発におけるライブラリ、SDK、フレームワークなど
XCFramework
ユースケース:ソースコードを公開したくない場合に、使う
WWDC動画
バイナリ互換性が重要な理由
クライアントが誰になるか、フレームワーク開発者側にはわからないから。
例えばクライアント自身もバイナリフレームワークである場合、お互いにバージョンがロックされている状況は避けたい(つまり、このバージョンでなければ動かないという状況を避けたい?)
フレームワークのバージョンは、セマンティックバージョニングにすること。
ライブラリとフレームワークの違い
XCFrameworkの作り方
Swift Package から XCFramework を使う
ライブラリのインポートとリンク
ライブラリの種類
- フレームワーク
-
UIKit.framework
やRxSwift.framework
- 2種類ある
- ダイナミック・ライブラリ
- スタティック・ライブラリ
-
- ダイナミック・ライブラリ
-
libsqlite3.dylib
やlibz.dylib
-
- スタティック・ライブラリ
-
libssl.a
やlibcrypto.a
-
トラブルシューティング
色々な組み合わせで問題が発生するが、一度に全てを見る必要はなく、縦の可能性を探りながら、一個ずつ見ていけば良い
フレームワークとライブラリの違い
- フレームワーク
- バンドルを持つ
- ライブラリ
- バンドルを持たない(持つことができない)
バンドルとは
- バンドルとは、開発を便利にするために一定のルールに従ったディレクトリ構造を持つもの。
- Appleのプラットフォームの上では、特定の拡張子のディレクトリはバンドルを呼ばれる。
- ディレクトリだけど、ファイルのように扱うことができる
- アプリケーション
- xcodeprj
フレームワークのバンドルのディレクトリ構造
コード以外のもの(resourceなど)を含めることができるのが、便利!
スタティックとダイナミック
- スタティック
- 結合したいものと、一つになる
- ビルド時に、シンボルと呼ばれる、変数やクラスなどは全て解決される
- ダイナミック
- ビルド時は、参照は設定されるが、シンボルの解決はされず、実行した時に行われる(遅延される)
スタティックリンクはリンクさえできればその後に問題は発生しないが、ダイナミックリンクはリンクするときはもちろん実行するときもリンクがちゃんとできている必要がある。
便利さでいうと、フレームワークの方がライブラリよりは便利。スタティックリンクよりダイナミックリンクの方が便利(後者は複数のターゲットから一つのライブラリにリンクできるのが便利)
スタティックかダイナミックかを調べる
file Logger.framework
インポートとリンク
言語機能なので、JSやRubyのインポートとは全く違う。JSやRubyなどは動的に行われるので、if文で分岐できたりするが、Swiftはできずビルド時に全て解決されている必要がある
インポートが終わり、ビルドが終わって コンパイルの後に 、リンクが行われる
リンクせずにEmbedする、は基本あり得ない。
リンクするけどEmbedしないことはない→スタティックフレームワークの場合
swiftmodule はインターフェースを定義しており、最低これだけあれば、importはできる
Swift Package Manager
略称は SwiftPM
Swift Package plugins
- Swift Package Plugin:Swift Package またはXcodeプロジェクト上で振る舞う、SwiftのScript
- アプリでいうRun Script。それに該当するものが、つまりビルド前やビルド中に実行する処理を書くという機能がSwiftPMには存在していなかった
2種類ある
- コマンドプラグイン
- ビルドツールプラグイン
現時点のSDKの使用方法
- フレームワーク
UIKit.framework
- バイナリフレームワーク(これはフレームワークの一部だと思われる)
RealmSwift.xcframework
- ダイナミック・ライブラリ
-
libsqlite3.dylib
やlibz.dylib
-
- スタティック・ライブラリ
-
libssl.a
やlibcrypto.a
-
SDKの使用方法
- CocoaPods
- Carthage
- Swift Package Manager(今はこれがメジャー)
iOSDC Japan 2021: Swift Package中心のプロジェクト構成とその実践 / Daiki Matsudate
Buildとは
- コンパイル
- 1ファイルずつ翻訳していく作業
- LLVMがやっている
- リンク
- 1ファイルだけを翻訳してもしょうがないので、それをリンクする
オブジェクトコードは、DerivedDataに入っている。
Build Targetとは
- ビルドに使うインプットとアウトプットを定義
- インプット:ソースコード、リソースファイルなど
Build Configuration
Build Scheme
- Target と Configuration の組み合わせ
DebugとReleaseビルドは組み合わせに
Module
- importできる単位
Framework
- Build Targetとリンクする実行可能なバイナリ
XCFrameworkとFrameworkの違い
- XCFramework:いろんなFrameworkが含まれる
- ビルドアーキテクチャ
- プラットフォーム:実機?シミュレータ?
SwiftPMでプロジェクトファイルダイエット
@_exported
:
リソースをSwiftPMで扱う
マルチモジュール
- コンパイル時間の短縮
- モジュールごとにコンパイルするので、モジュールに変更がなければ、コンパイルしない
WWDC のセッション Link fast: Improve build and launch times
2種類のリンク方法
- スタティックリンキング
- ダイナミックリンキング
スタティックリンキングについて
- リンカは、未定義のシンボル(未定義の関数など)がないかを、コマンドライン順に探していく
- 全ての関数とデータを出力ファイルにコピーする
- 速さが2倍になった
- 複数コアを使って、並行処理を行うようになったため(以下)
- コンテンツのコピー
- LINKEDITの各部分の並列ビルド
- ハッシュの計算
- ビルドのアルゴリズムの最適化
- 複数コアを使って、並行処理を行うようになったため(以下)
スタティックリンキングのベストプラクティス
いつスタティックライブラリを使うべきか?
- 変更が少ないコード
- スタティックライブラリは変更のたびに再ビルドする必要があるため
-all_load
- 選択的ロードによるリンキングには時間がかかるため、
-all_load
オプションを使用することで改善できる- 全てのスタティックライブラリから、.oファイルを読み込む
- コンテンツの大部分を読み込む必要がある場合に有利
-all_load
の欠点
- アプリが同じシンボルを実装した複数のスタティックライブラリを持っていて、どの実装を使うかをスタティックライブラリのコマンドラインの順序に依存されている場合、シンボルが重複しているというエラーが発生する可能性がある
- 使われていないコードが追加されるので、プログラムが大きくなる
- 対策:
-dead_strip
- リンカが到達不可能なコードとデータを削除する
- 対策:
-no_exported_symbols
-
-no_exported_symbols
- メインのアプリのバイナリは通常、エクスポートされたシンボルは不要
- なのでこのオプションを使うことで、LINKEDITでのトライデータ構造の作成をスキップできる
- 出力のトライが大きい場合に使うと良い
-
dyld_info -exports /path/to/binary | wc -l
:エクスポートされたシンボルの数を数える
-
-no_exported_symbols
の欠点
- アプリがメイン実行ファイルにリンクするプラグインをロードする場合や、アプリでxctestをホスト環境として使い、xctestバンドルを実行する場合、アプリは全てのエクスポートを持つ必要があるのでこのオプションは使えない
-no_deduplicate
(重複排除の最適化)
- 同じ命令で異なる名前を持つ関数をマージするようにしてある。主にC++のコードを組み合わせるときに多い。
- だがこのアルゴリズムはとても負荷が高い(重複を探すために、全ての関数の命令を再帰的にハッシュ化する必要がある)
- Debugモードでは、デフォルトで有効なオプション
- Xcodeの非標準な設定を使っていたり、他のビルドシステムを使っている場合は、デバッグビルドでこのオプションを追加すべき
スタティックライブラリについての驚き
- アプリがリンクしているスタティックライブラリにビルドするソースコードが、最終的なアプリに含まれていない
- Objective-Cを使用している場合や、
__attribute__((used))
を使っている場合 - 選択的ローディングをするので、リンク時に必要となる何らかのシンボルを定義していないと、リンカにロードされない
- Objective-Cを使用している場合や、
- dead strippingがスタティックライブラリの問題を隠すことがある
- 通常はリンクできないとエラーになるが、到達できないコードのシンボルがあるとエラーを出さずにエラーを抑止する
- シンボルが重複している場合、リンカは最初のものを選ぶのでエラーにならない
- 1つのスタティックライブラリが複数のフレームワークに組み込まれている場合、アプリが両方のフレームワークを使用すると複数の定義のためにランタイムに奇妙な問題が発生する
- ランタイム時に、シンボルが重複しているという警告
dead stripingについて↓
ダイナミックリンキングについて
- スタティックリンキングではソースコードをプログラムにコピーするが、ダイナミックリンキングは違う。以下を記録する
- ダイナミックライブラリから使用されるシンボル名
- 実行時にそのライブラリのパスがどうなるか
- ↑のメリット
- プログラムのファイルサイズを自分でコントロールできる
- 複数のプロセスで同じダイナミックライブラリが使用されている場合、仮想記憶システムはそのdylibを使用する全てのプロセスで、同じRAMの物理ページを再利用する
-
メリット
- ビルド時間が短縮できる
-
デメリット
- アプリの起動が遅くなる
- 起動が単純に一つのプログラムファイルを読み込むだけではないため
- ダイナミックライブラリベースのプログラムでは、ダーティページが多くなる
- スタティックライブラリでは、全グローバルをメイン実行ファイル内の同じDATAページに配置する
- ダイナミックライブラリでは、各ライブラリにDATAページが用意されている
- ランタイムリンカー(dyld)が必要になること
- アプリの起動が遅くなる
-
実行バイナリは以下のセグメントで構成されている
__TEXT
__DATA
__LINKEDIT
-
各セグメントごとに、権限がある
-
Page-in リンキング(2022登場)によって、ダーティページの削減や起動時間の短縮を行うことができた
ダイナミックリンクのベストプラクティス
- なるべく少ないdylibの数を使う
- staticのinitializerを最適化すること
- ここで数ミリ秒以上かかる作業(I/Oやネットワーキングなど)を行わないこと
便利なツール
dyld_usage
- macOS 13 から使えるコマンドラインツール
dyld_info
- macOS12 から使えるコマンドラインツール
- バイナリの検索に使える
-
nm
やotool
に似ている
Xcode の Frameworks, Libraries, and Embedded Content
Embed するかどうか
- スタティックフレームワークやライブラリは埋め込まず(リンクはビルド時に行われる)、共有されたものだけを埋め込む→
Do not embed
- ダイナミックリンクは実行時に行われるため、バンドルに含める必要がある→
Embed
スタティックかダイナミックの確認の仕方
$ file frameworkToLink.framework/frameworkToLink
# 結果が以下なら、スタティックリンク
current ar archive
# 結果が以下なら、ダイナミックリンク
Mach-O dynamically linked
Signing するかどうか(共有/embedded の場合のみ)
- すでに適切な署名がある場合は不要(adhoc は含まない→コメントによると署名が必要らしい)
- らしいのだが、サードパーティから配布されているXCFrameworkの場合、サードパーティによる署名があっても
cannot install xxx app
というエラーでインストールができなかったので、必要な場合もあるらしい
- らしいのだが、サードパーティから配布されているXCFrameworkの場合、サードパーティによる署名があっても
$ codesign -dv frameworkToLink.framework
# 結果が以下なら、Embed and sign、それ以外なら、Embed without Signing
code object is not signed at all or adhoc
Briding-Header と Module Map
Briding-Header とは(以下の記事より引用)
- Bridging Header は Objective-C で書かれたコードを Swift で利用する仕組み
- それぞれの Objective-C 製ライブラリのヘッダーファイル(**.h) を、ファイルに import することで Swift 側から公開されているシンボルにアクセスできるようになります
- Bridging Header はアプリケーションターゲットとしてのみ作用するのでフレームワークの開発などでは使用することができません
- これらはグローバルに作用するので、特定のファイルのみに Import するということは基本的にはできません
Briding-Header の使い方
アプリターゲット内でコードをimportしたいとき
-
Briding-Header.h
をサフィックスに設定したファイルを作成する - Xcode の Build Settings の
Objective-C Bridging Header
で、Briding -Header の相対パスを指定する(大抵の場合はここの設定を変更する必要はないらしい)
フレームワークターゲットでコードをimportしたいとき
- Xcode の Build Settings の
Defines Module
で、YES
に設定 - umbrellaヘッダーで、Swiftに公開したいすべてのObjective-Cヘッダーをインポートする
- Swiftは、あなたがアンブレラヘッダーで公開するすべてのヘッダーを見る。
- そのフレームワーク内のObjective-Cファイルの内容は、import文なしで、そのフレームワークターゲット内の任意のSwiftファイルから自動的に利用可能です。
- システムクラスに使用するのと同じSwift構文で、Objective-Cコードからクラスや他の宣言を使用します。
module.modulemap とは(上記の記事より引用)
- Module Map は Bridging Header の上位互換で、Objective-C または C で書かれたライブラリの場合は、Modules ディレクトリの中に module.modulemap というファイルを設定することで、Swift 側からシンボルにアクセスできるようにします。
- Swift が登場して移行の Xcode で Objective-C 製のライブラリなどをビルドした場合は自動的に module.modulemap ファイルが生成されます。
module.modulemap の書き方
framework module Communication {
umbrella header "Communication.h" // どのヘッダーを公開するか
export * // * はワイルドカードの意味で、全てのモジュールを再エクスポートする
module * { export * }
}
umbrella
について
アンブレラディレクトリ宣言は、指定されたディレクトリにあるすべてのヘッダーをモジュールに含めることを指定する。
アンブレラディレクトリは、多数のヘッダを持つがアンブレラヘッダを持たないライブラリに便利である。
アンブレラヘッダに含まれていないヘッダは明示的なヘッダ宣言が必要です。Wincomplete-umbrella警告オプションを使うと、umbrellaヘッダやモジュールマップでカバーされていないヘッダについてClangに文句を言うように要求できます。
export *
について
The wildcard export syntax export * re-exports all of the modules that were imported in the actual header file. Because #include directives are automatically mapped to module imports, export * provides the same transitive-inclusion behavior provided by the C preprocessor, e.g., importing a given module implicitly imports all of the modules on which it depends. Therefore, liberal use of export * provides excellent backward compatibility for programs that rely on transitive inclusion (i.e., all of them).
module * {export *}
について
推論サブモジュール宣言は、モジュールの一部でありながらヘッダー宣言で明示的に記述されていないヘッダーに対応するサブモジュールの集合を記述します。