🔧

最近のXcodeでiOSのスタックサイズを変えたい

に公開

iOSのスタックサイズ制限について

iOSアプリのビルドで「スタックオーバーフローで落ちる…」と困ったことはありませんか?デフォルトの8MBのままだと、複雑なデータ構造や大量の履歴を扱う場合にスタックが足りなくなることがあります。特に開発中やデバッグビルドで問題が表面化しやすいです。

実はこのスタックサイズ、アプリの実行ファイル(Mach-Oバイナリ)の中に「このバイナリはどれだけスタックを使っていいか」という情報として埋め込まれています。具体的には、Mach-Oのロードコマンド(load command)のひとつでスタックサイズが指定されており、OSはこの値を見てメインスレッドのスタック領域を確保します。

Xcodeのビルド時に-stack_sizeフラグを指定すると、このMach-Oのスタックサイズ情報を書き換えることができます。たとえば-Xlinker -stack_size -Xlinker 0x10000000と渡すことで、デフォルト(8MB)よりも大きなスタック領域を確保できるようになります。

ただし、スタックサイズは「メイン実行ファイル」にしか設定できません。dylibには適用できず、ldの-stack_sizeフラグもメイン実行ファイル専用です。スタックサイズを変えたい場合は、この点を押さえておく必要があります。

ENABLE_DEBUG_DYLIBとは

最近のXcodeにはENABLE_DEBUG_DYLIBというビルド設定があります。これを有効にすると、デバッグビルド時にアプリ本体のコードがapp.debug.dylibに分離され、スタブ実行器がdylibを読み込む形になります。

この設定は、SwiftUI Previewや新しい開発機能を使いたいときには必須です。ただし、ターゲットによっては互換性がなかったり、必要ない場合もあるので、用途に応じてON/OFFを切り替えるのが良いでしょう。

設定方法

Xcodeのビルド設定でENABLE_DEBUG_DYLIBYESにするだけです。

スタックサイズを変えたいときのポイント

「じゃあスタックサイズを増やしたいときはどうする?」という話ですが、ここでENABLE_DEBUG_DYLIBが有効だと困ったことが起きます。dylibが生成されてしまうため、-stack_sizeフラグを渡しても「main executableじゃないからダメ」とエラーになってしまいます。

この場合は、以下のように設定するのがおすすめです:

  1. ENABLE_DEBUG_DYLIBNOにする
  2. OTHER_LDFLAGS-Xlinker -stack_size -Xlinker 0x10000000(例:256MB)を追加

こうすることで、メイン実行ファイルに直接スタックサイズの設定が効くようになります。

iOSのスタックサイズについて

スタックサイズのデフォルトは8MB(0x800000)ですが、必要に応じて増やすことができます。特に、複雑なデータ構造や大量の履歴情報などを扱う場合は、余裕を持った設定をしておくと安心です。

設定方法

OTHER_LDFLAGSに以下のように追加します:

-Xlinker -stack_size -Xlinker 0x10000000

この例では256MBに設定していますが、実際に必要なサイズはアプリの用途やデータ量に応じて調整してください。

使用例

たとえば、ユーザー情報や履歴をたくさん持つようなアプリの場合、デフォルトのままだと処理中にスタックオーバーフローが起きることがあります。下記のような構造体を大量に扱う場合は、スタックサイズを増やしておくと安心です。

struct User {
    var id: UUID
    var name: String
    var profile: Profile
    var settings: UserSettings
    var history: [UserHistory]
}

struct Profile {
    var bio: String
    var avatar: Data
    var stats: UserStats
    var preferences: UserPreferences
}

struct UserHistory {
    var timestamp: Date
    var action: UserAction
    var metadata: [String: Any]
}

func processUsers(_ users: [User]) {
    for user in users {
        for history in user.history {
            processHistory(history)
        }
    }
}

一方で、一般的なアプリやリリースビルドでは、デフォルトのスタックサイズで十分なケースが多いです。必要なときだけ設定を変更する運用をおすすめします。

注意点

  • スタックサイズは16KB(0x4000)の倍数で指定すると良いでしょう
  • スタックはスレッドごとに確保されるため、極端に大きな値を指定するとメモリ不足や予期せぬ挙動につながることがあります
  • メインスレッド(main thread)とそれ以外のスレッド(worker thread等)ではデフォルトのスタックサイズが異なる場合があります。main threadだけでなく、他のスレッドの動作も考慮して設定すると安心です
  • メモリ使用量が増えるので、必要以上に大きくしすぎないのがおすすめです
  • デバッグビルド時だけ調整し、リリースビルドではデフォルト値を使うのが一般的です
  • ENABLE_DEBUG_DYLIBNOにするとSwiftUI Previewなどが使えなくなるので、Previewを活用したい場合はSwift Package Managerでパッケージを分離する方法も検討できます

SwiftUI Previewの活用方法

ENABLE_DEBUG_DYLIBNOにするとSwiftUI Previewが使えなくなるデメリットがありますが、Swift Package Manager(SPM)でビューをパッケージ化しておくと、パッケージ内のPreviewは影響を受けません。

  • ビュー定義をSwift Packageとして分離
  • パッケージ内でのPreviewはENABLE_DEBUG_DYLIBの影響を受けない
  • メインアプリケーション側ではスタックサイズを適切に設定

このように、用途や開発スタイルに合わせて設定を工夫すると、Xcodeの制約とうまく付き合いながら開発を進めることができます。

実例:The Composable Architectureでのケース

実際の現場でも、たとえばThe Composable Architecture(TCA)を使っていると、巨大なstate structを確保するタイミングでスタックオーバーフローが発生することがあります。TCAではアプリ全体の状態をひとつの大きなstructで管理することが多く、初期化やdeep copyのタイミングでスタックを多く消費する場合があります。

このような場合も、今回紹介したスタックサイズの調整方法が有効です。TCAのようなアーキテクチャを採用している場合は、特にスタックサイズに注意しておくと安心です。

実際に問題になっているディスカッションもあります。

https://github.com/pointfreeco/swift-composable-architecture/discussions/3162

Discussion