最近の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_DYLIB
をYES
にするだけです。
スタックサイズを変えたいときのポイント
「じゃあスタックサイズを増やしたいときはどうする?」という話ですが、ここでENABLE_DEBUG_DYLIB
が有効だと困ったことが起きます。dylibが生成されてしまうため、-stack_size
フラグを渡しても「main executableじゃないからダメ」とエラーになってしまいます。
この場合は、以下のように設定するのがおすすめです:
-
ENABLE_DEBUG_DYLIB
をNO
にする -
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_DYLIB
をNO
にするとSwiftUI Previewなどが使えなくなるので、Previewを活用したい場合はSwift Package Managerでパッケージを分離する方法も検討できます
SwiftUI Previewの活用方法
ENABLE_DEBUG_DYLIB
をNO
にすると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のようなアーキテクチャを採用している場合は、特にスタックサイズに注意しておくと安心です。
実際に問題になっているディスカッションもあります。
Discussion