Open8

iOS開発におけるライブラリ、SDK、フレームワークなど

kamimikamimi

XCFramework

ユースケース:ソースコードを公開したくない場合に、使う

WWDC動画

https://developer.apple.com/videos/play/wwdc2019/416/

バイナリ互換性が重要な理由

クライアントが誰になるか、フレームワーク開発者側にはわからないから。
例えばクライアント自身もバイナリフレームワークである場合、お互いにバージョンがロックされている状況は避けたい(つまり、このバージョンでなければ動かないという状況を避けたい?)

フレームワークのバージョンは、セマンティックバージョニングにすること。

ライブラリとフレームワークの違い

https://medium.com/@zippicoder/libraries-frameworks-swift-packages-whats-the-difference-764f371444cd

XCFrameworkの作り方

https://www.appcoda.com/xcframework/#Creating_The_Two_Binary_Frameworks

Swift Package から XCFramework を使う

https://developer.apple.com/videos/play/wwdc2020/10147/

kamimikamimi

ライブラリのインポートとリンク

https://youtu.be/FZoYyAEPJ8w

ライブラリの種類

  • フレームワーク
    • UIKit.frameworkRxSwift.framework
    • 2種類ある
      • ダイナミック・ライブラリ
      • スタティック・ライブラリ
  • ダイナミック・ライブラリ
    • libsqlite3.dyliblibz.dylib
  • スタティック・ライブラリ
    • libssl.alibcrypto.a

トラブルシューティング

色々な組み合わせで問題が発生するが、一度に全てを見る必要はなく、縦の可能性を探りながら、一個ずつ見ていけば良い

フレームワークとライブラリの違い

  • フレームワーク
    • バンドルを持つ
  • ライブラリ
    • バンドルを持たない(持つことができない)

バンドルとは

  • バンドルとは、開発を便利にするために一定のルールに従ったディレクトリ構造を持つもの。
  • Appleのプラットフォームの上では、特定の拡張子のディレクトリはバンドルを呼ばれる。
  • ディレクトリだけど、ファイルのように扱うことができる
    • アプリケーション
    • xcodeprj

フレームワークのバンドルのディレクトリ構造

コード以外のもの(resourceなど)を含めることができるのが、便利!

スタティックとダイナミック

  • スタティック
    • 結合したいものと、一つになる
    • ビルド時に、シンボルと呼ばれる、変数やクラスなどは全て解決される
  • ダイナミック
    • ビルド時は、参照は設定されるが、シンボルの解決はされず、実行した時に行われる(遅延される)

スタティックリンクはリンクさえできればその後に問題は発生しないが、ダイナミックリンクはリンクするときはもちろん実行するときもリンクがちゃんとできている必要がある。

便利さでいうと、フレームワークの方がライブラリよりは便利。スタティックリンクよりダイナミックリンクの方が便利(後者は複数のターゲットから一つのライブラリにリンクできるのが便利)

スタティックかダイナミックかを調べる

file Logger.framework

インポートとリンク

言語機能なので、JSやRubyのインポートとは全く違う。JSやRubyなどは動的に行われるので、if文で分岐できたりするが、Swiftはできずビルド時に全て解決されている必要がある

インポートが終わり、ビルドが終わって コンパイルの後に 、リンクが行われる

リンクせずにEmbedする、は基本あり得ない。
リンクするけどEmbedしないことはない→スタティックフレームワークの場合

swiftmodule はインターフェースを定義しており、最低これだけあれば、importはできる

kamimikamimi

Swift Package Manager

略称は SwiftPM

https://developer.apple.com/videos/play/wwdc2018/411/

https://developer.apple.com/videos/play/wwdc2020/10147/

Swift Package plugins

  • Swift Package Plugin:Swift Package またはXcodeプロジェクト上で振る舞う、SwiftのScript
    • アプリでいうRun Script。それに該当するものが、つまりビルド前やビルド中に実行する処理を書くという機能がSwiftPMには存在していなかった

https://developer.apple.com/videos/play/wwdc2022/110359

https://qiita.com/maiyama18/items/3d0f7b288c6858b751ce

2種類ある

  • コマンドプラグイン
  • ビルドツールプラグイン

https://qiita.com/maiyama18/items/3d0f7b288c6858b751ce

https://github.com/apple/swift-evolution/blob/main/proposals/0303-swiftpm-extensible-build-tools.md

https://github.com/apple/swift-evolution/blob/main/proposals/0332-swiftpm-command-plugins.md

kamimikamimi

現時点のSDKの使用方法

  • フレームワーク
    • UIKit.framework
    • バイナリフレームワーク(これはフレームワークの一部だと思われる)
      • RealmSwift.xcframework
  • ダイナミック・ライブラリ
    • libsqlite3.dyliblibz.dylib
  • スタティック・ライブラリ
    • libssl.alibcrypto.a

SDKの使用方法

  • CocoaPods
  • Carthage
  • Swift Package Manager(今はこれがメジャー)
kamimikamimi

iOSDC Japan 2021: Swift Package中心のプロジェクト構成とその実践 / Daiki Matsudate

https://youtu.be/e_T1-XrYf4A

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

https://qiita.com/shtnkgm/items/0e2c6ecb0af97800e778

リソースをSwiftPMで扱う

https://developer.apple.com/videos/play/wwdc2020/10169

マルチモジュール

  • コンパイル時間の短縮
    • モジュールごとにコンパイルするので、モジュールに変更がなければ、コンパイルしない

kamimikamimi

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))を使っている場合
    • 選択的ローディングをするので、リンク時に必要となる何らかのシンボルを定義していないと、リンカにロードされない
  • dead strippingがスタティックライブラリの問題を隠すことがある
    • 通常はリンクできないとエラーになるが、到達できないコードのシンボルがあるとエラーを出さずにエラーを抑止する
    • シンボルが重複している場合、リンカは最初のものを選ぶのでエラーにならない
  • 1つのスタティックライブラリが複数のフレームワークに組み込まれている場合、アプリが両方のフレームワークを使用すると複数の定義のためにランタイムに奇妙な問題が発生する
    • ランタイム時に、シンボルが重複しているという警告

dead stripingについて↓

https://ja.wikipedia.org/wiki/デッドコード削除

ダイナミックリンキングについて

  • スタティックリンキングではソースコードをプログラムにコピーするが、ダイナミックリンキングは違う。以下を記録する
    • ダイナミックライブラリから使用されるシンボル名
    • 実行時にそのライブラリのパスがどうなるか
  • ↑のメリット
    • プログラムのファイルサイズを自分でコントロールできる
    • 複数のプロセスで同じダイナミックライブラリが使用されている場合、仮想記憶システムはそのdylibを使用する全てのプロセスで、同じRAMの物理ページを再利用する

  • メリット

    • ビルド時間が短縮できる
  • デメリット

    • アプリの起動が遅くなる
      • 起動が単純に一つのプログラムファイルを読み込むだけではないため
    • ダイナミックライブラリベースのプログラムでは、ダーティページが多くなる
      • スタティックライブラリでは、全グローバルをメイン実行ファイル内の同じDATAページに配置する
      • ダイナミックライブラリでは、各ライブラリにDATAページが用意されている
    • ランタイムリンカー(dyld)が必要になること
  • 実行バイナリは以下のセグメントで構成されている

    • __TEXT
    • __DATA
    • __LINKEDIT
  • 各セグメントごとに、権限がある

  • Page-in リンキング(2022登場)によって、ダーティページの削減や起動時間の短縮を行うことができた

ダイナミックリンクのベストプラクティス

  • なるべく少ないdylibの数を使う
  • staticのinitializerを最適化すること
    • ここで数ミリ秒以上かかる作業(I/Oやネットワーキングなど)を行わないこと

便利なツール

dyld_usage

  • macOS 13 から使えるコマンドラインツール

dyld_info

  • macOS12 から使えるコマンドラインツール
  • バイナリの検索に使える
  • nmotoolに似ている

kamimikamimi

Xcode の Frameworks, Libraries, and Embedded Content

https://holyswift.app/frameworks-embed-or-not-embed-thats-the-question/

https://stackoverflow.com/a/58860158/18519539

Embed するかどうか

  • スタティックフレームワークやライブラリは埋め込まず(リンクはビルド時に行われる)、共有されたものだけを埋め込む→ Do not embed
  • ダイナミックリンクは実行時に行われるため、バンドルに含める必要がある→ Embed

スタティックかダイナミックの確認の仕方

$ file frameworkToLink.framework/frameworkToLink

# 結果が以下なら、スタティックリンク
current ar archive

# 結果が以下なら、ダイナミックリンク
Mach-O dynamically linked

Signing するかどうか(共有/embedded の場合のみ)

  • すでに適切な署名がある場合は不要(adhoc は含まない→コメントによると署名が必要らしい)
    • らしいのだが、サードパーティから配布されているXCFrameworkの場合、サードパーティによる署名があってもcannot install xxx appというエラーでインストールができなかったので、必要な場合もあるらしい
$ codesign -dv frameworkToLink.framework

# 結果が以下なら、Embed and sign、それ以外なら、Embed without Signing
code object is not signed at all or adhoc
kamimikamimi

Briding-Header と Module Map

Briding-Header とは(以下の記事より引用)

  • Bridging Header は Objective-C で書かれたコードを Swift で利用する仕組み
  • それぞれの Objective-C 製ライブラリのヘッダーファイル(**.h) を、ファイルに import することで Swift 側から公開されているシンボルにアクセスできるようになります
  • Bridging Header はアプリケーションターゲットとしてのみ作用するのでフレームワークの開発などでは使用することができません
  • これらはグローバルに作用するので、特定のファイルのみに Import するということは基本的にはできません

https://yamatooo.blog/entry/2020/10/06/083000

Briding-Header の使い方

アプリターゲット内でコードをimportしたいとき

  1. Briding-Header.hをサフィックスに設定したファイルを作成する
  2. Xcode の Build Settings の Objective-C Bridging Headerで、Briding -Header の相対パスを指定する(大抵の場合はここの設定を変更する必要はないらしい)

フレームワークターゲットでコードをimportしたいとき

  1. Xcode の Build Settings の Defines Moduleで、YESに設定
  2. umbrellaヘッダーで、Swiftに公開したいすべてのObjective-Cヘッダーをインポートする
  • Swiftは、あなたがアンブレラヘッダーで公開するすべてのヘッダーを見る。
  • そのフレームワーク内のObjective-Cファイルの内容は、import文なしで、そのフレームワークターゲット内の任意のSwiftファイルから自動的に利用可能です。
  • システムクラスに使用するのと同じSwift構文で、Objective-Cコードからクラスや他の宣言を使用します。

https://developer.apple.com/documentation/swift/importing-objective-c-into-swift

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に文句を言うように要求できます。

https://clang.llvm.org/docs/Modules.html#umbrella-directory-declaration

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).

https://clang.llvm.org/docs/Modules.html#export-declaration

module * {export *}について

推論サブモジュール宣言は、モジュールの一部でありながらヘッダー宣言で明示的に記述されていないヘッダーに対応するサブモジュールの集合を記述します。

https://clang.llvm.org/docs/Modules.html#submodule-declaration


https://forums.swift.org/t/spm-library-with-bridging-header/4945/2