iOSの EXC_BAD_ACCESS とは?原因・よくある例・調査方法・対策まとめ
iOSアプリを開発していると、クラッシュログでよく見るのが EXC_BAD_ACCESS です。
特に、画面遷移・非同期処理・SDK連携が多いアプリでは遭遇しやすいエラーです。
この記事では、EXC_BAD_ACCESS とは何か、どんな時に起きるのか、どうやって調査すればよいのかを整理します。
EXC_BAD_ACCESS とは?
EXC_BAD_ACCESS は、不正なメモリアクセス が発生したときのクラッシュです。
たとえば、次のような状態で起こります。
- すでに解放されたオブジェクトにアクセスした
- 存在しないメモリアドレスを参照した
- ポインタやメモリ管理を誤った
- マルチスレッドでオブジェクトの生存期間が崩れた
クラッシュログでは、次のように表示されることが多いです。
EXC_BAD_ACCESS (SIGSEGV)
EXC_BAD_ACCESS (KERN_INVALID_ADDRESS)
ざっくり言うと、
触ってはいけないメモリを触ってしまった
というエラーです。
よく見る表記の意味
EXC_BAD_ACCESS
不正なメモリアクセスです。
KERN_INVALID_ADDRESS
存在しないアドレスにアクセスしたことを示します。
SIGSEGV
セグメンテーション違反です。
OSに保護されている領域や無効な領域にアクセスした場合に発生します。
なぜ怖いのか
EXC_BAD_ACCESS が厄介なのは、見えている行が真犯人とは限らない ことです。
たとえば、クラッシュしたのは label.text = "..." の行でも、
本当の原因はその前にオブジェクトが解放されていたこと、というケースがあります。
つまり、クラッシュ地点と原因地点がずれている ことが多いです。
よくある原因
1. 解放済みオブジェクトへのアクセス
最も典型的なパターンです。
final class SampleViewController: UIViewController {
var worker: Worker?
func start() {
worker = Worker()
worker = nil
worker!.execute()
}
}
このコードでは worker を nil にした後に強制アンラップしているため、問題になります。
Swiftでは nil アクセスは Unexpectedly found nil while unwrapping an Optional value になることもありますが、
Objective-C オブジェクトや低レイヤーの処理、ブリッジ周りでは EXC_BAD_ACCESS になることがあります。
2. weak 参照が解放された後に使われる
非同期処理や delegate でよくあります。
final class SampleViewController: UIViewController {
weak var delegate: SomeDelegate?
func finish() {
delegate?.didFinish()
}
}
weak は参照先が解放されると自動で nil になります。
delegate? なら安全ですが、強制アンラップしていると危険です。
delegate!.didFinish()
3. 非同期処理中に self が解放される
画面を閉じたあとに、非同期コールバックが戻ってきてクラッシュするパターンです。
final class ProfileViewController: UIViewController {
@IBOutlet private weak var nameLabel: UILabel!
func loadData() {
DispatchQueue.global().async {
let name = "Taro"
DispatchQueue.main.async {
self.nameLabel.text = name
}
}
}
}
このコードは、一見問題なさそうに見えます。
ただし、通信中や重い処理の間に画面が閉じられて self が解放される可能性があります。
その場合、戻ってきたタイミングで不正アクセスになることがあります。
対策
final class ProfileViewController: UIViewController {
@IBOutlet private weak var nameLabel: UILabel!
func loadData() {
DispatchQueue.global().async { [weak self] in
let name = "Taro"
DispatchQueue.main.async {
guard let self else { return }
self.nameLabel.text = name
}
}
}
}
4. Objective-C / C / C++ / SDK 連携でのメモリ問題
Swiftだけで開発していても、実際には以下とつながっていることがあります。
- Objective-C
- C / C++
- サードパーティSDK
- 動画・配信・音声SDK
- 画像変換ライブラリ
このとき、Swift側では安全に見えても、内部でメモリ管理ミスが起きていると EXC_BAD_ACCESS になります。
特に次のようなケースは注意です。
- SDKのdelegate解除漏れ
- コールバック先が解放済み
-
UnsafePointerの誤使用 - C API の
malloc/freeミス
5. 配列やポインタの範囲外アクセス
Swiftの配列であれば、通常は Fatal error: Index out of range になりやすいです。
ただし、低レイヤーやUnsafe系を使っている場合は EXC_BAD_ACCESS になることがあります。
let pointer = UnsafeMutablePointer<Int>.allocate(capacity: 1)
pointer.pointee = 10
pointer.deallocate()
print(pointer.pointee)
これはかなり危険なコードです。
解放済みメモリにアクセスしているため、クラッシュの原因になります。
6. スレッド競合
複数スレッドで同じオブジェクトを触っている場合、片方で解放・変更されたタイミングで、もう片方がアクセスすると落ちることがあります。
たとえば次のような状況です。
- スレッドAがオブジェクトを破棄
- スレッドBがそのオブジェクトを参照
- タイミング次第で
EXC_BAD_ACCESS
特に、以下のような処理は要注意です。
- キャッシュ更新
- 配列や辞書の同時書き換え
- 配信状態の管理
- 非同期コールバックの多重発火
よくある実例
実例1: 画面を閉じた後のUI更新
final class DetailViewController: UIViewController {
@IBOutlet private weak var statusLabel: UILabel!
func fetch() {
apiClient.getDetail { [weak self] result in
guard let self else { return }
switch result {
case .success(let detail):
self.statusLabel.text = detail.status
case .failure:
self.statusLabel.text = "error"
}
}
}
}
これは比較的安全な書き方です。
逆に [weak self] がなく、画面破棄後にもUIを更新しようとすると危険です。
実例2: NotificationCenter の解除漏れ
final class SampleViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(
self,
selector: #selector(handleNotification),
name: .sampleNotification,
object: nil
)
}
@objc private func handleNotification() {
print("received")
}
deinit {
NotificationCenter.default.removeObserver(self)
}
}
現在は以前より安全になっている部分もありますが、
設計次第では解除漏れや通知タイミングにより問題が起きることがあります。
実例3: Timer の循環参照や解放タイミング
final class SampleViewController: UIViewController {
private var timer: Timer?
override func viewDidLoad() {
super.viewDidLoad()
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
print("tick")
}
}
deinit {
timer?.invalidate()
}
}
Timer は解放タイミングや参照の持ち方を誤ると、思わぬクラッシュやリークの原因になります。
EXC_BAD_ACCESS の調査方法
1. Zombie Objects を有効にする
もっとも有名な調査方法です。
解放済みオブジェクトにメッセージ送信した場合に、原因を見つけやすくなります。
設定方法
- Xcode
- Product
- Scheme
- Edit Scheme
- Diagnostics
-
Enable Zombie ObjectsをON
これで、通常は EXC_BAD_ACCESS で落ちる箇所が、
より分かりやすいメッセージになることがあります。
例:
message sent to deallocated instance
これはかなり有力なヒントです。
2. Address Sanitizer を使う
メモリアクセス違反の検出に役立ちます。
設定方法
- Xcode
- Product
- Scheme
- Edit Scheme
- Diagnostics
-
Address SanitizerをON
検出できるもの:
- use-after-free
- heap overflow
- stack overflow の一部
- 不正メモリアクセス
低レイヤーの問題やSDK連携時の問題を追うときに有効です。
3. Thread Sanitizer を使う
競合状態が怪しいときに使います。
検出しやすいもの
- データ競合
- マルチスレッドによる不整合
- 同時読み書きによる破壊
EXC_BAD_ACCESS の直接原因がスレッド競合のこともあるため、かなり重要です。
4. クラッシュログのスタックトレースを見る
クラッシュログでは、まず以下を確認します。
- どのスレッドで落ちたか
- どのメソッドで落ちたか
- アプリ側コードか、SDK内部か
- メインスレッドか、バックグラウンドか
例:
Thread 0 Crashed:
0 MyApp 0x0000000101234567 DetailViewController.updateUI() + 120
1 MyApp 0x0000000101234500 closure #1 in DetailViewController.fetch() + 80
この場合、updateUI() が直接のクラッシュ地点です。
ただし、原因はその前段で self や内部オブジェクトが壊れていた可能性があります。
調査するときの考え方
EXC_BAD_ACCESS を見たときは、次の観点で整理すると追いやすいです。
1. そのオブジェクトはまだ生きているか
- 画面は閉じていないか
- delegate は残っているか
- timer / notification / callback は解除済みか
2. 非同期処理が絡んでいないか
- API通信
- DispatchQueue
- Task
- Combine
- RxSwift
- SDK callback
3. SDKやUnsafe系を使っていないか
- 動画SDK
- 音声SDK
- C API
- Objective-C bridge
UnsafePointer
4. 競合していないか
- 複数スレッドから同じデータを触っていないか
- キャッシュ更新と参照が同時に走っていないか
対策まとめ
1. 強制アンラップを減らす
user!.name
よりも、
user?.name
や、
guard let user else { return }
を優先します。
2. 非同期クロージャでは [weak self] を検討する
特に UIViewController や UIView では重要です。
apiClient.fetch { [weak self] result in
guard let self else { return }
self.render(result)
}
3. ライフサイクルに応じて解除する
- NotificationCenter
- Timer
- KVO
- delegate
- observer
- SDK listener
これらは、登録したら解除する を徹底したほうが安全です。
4. UI更新はメインスレッドで行う
スレッド違反そのものは別エラーになりやすいですが、
状態不整合からクラッシュを誘発することがあります。
DispatchQueue.main.async {
self.label.text = "updated"
}
5. Unsafe系はできるだけ閉じ込める
UnsafePointer や低レイヤーのC APIを使うなら、利用箇所を最小限にし、
アプリ全体に広げないことが大切です。
6. 所有者を明確にする
「誰がそのオブジェクトを保持しているのか」が曖昧だと、解放タイミングの事故が起きやすくなります。
たとえば、
- ViewControllerが持つのか
- ViewModelが持つのか
- Managerのシングルトンが持つのか
- SDKが内部で保持するのか
この整理はかなり重要です。
現場でよくあるチェックポイント
実務では、次の観点で確認するとかなり絞れます。
画面まわり
- 画面を閉じた後に callback が返ってきていないか
-
IBOutletに対して遅延更新していないか -
deinitが想定通り呼ばれているか
非同期まわり
-
Taskがキャンセルされているか - 多重通信になっていないか
- completion が複数回呼ばれていないか
SDKまわり
- delegate 解放漏れがないか
- listener 解除漏れがないか
- SDKコールバックが別スレッドから来ていないか
データまわり
- 共有配列・辞書を複数スレッドで触っていないか
- キャッシュ破棄後に参照していないか
まとめ
EXC_BAD_ACCESS は、iOS開発で非常に遭遇しやすいクラッシュです。
意味としてはシンプルで、不正なメモリアクセス です。
ただし実際の調査では、
- 解放済みオブジェクト
- 非同期処理
- weak参照
- SDK内部
- マルチスレッド競合
など、複数の観点から追う必要があります。
まずは次の3つを徹底すると、かなり調査しやすくなります。
- Zombie Objects を有効にする
- Address Sanitizer / Thread Sanitizer を使う
- オブジェクトの生存期間を疑う
EXC_BAD_ACCESS は最初は難しく見えますが、
「誰が保持していて、いつ解放され、どこからアクセスしたか」を丁寧に追うと、かなり整理できます。
おわりに
特に、配信アプリや動画SDKを使うアプリでは、EXC_BAD_ACCESS は出やすい印象があります。
画面遷移、非同期通信、SDK callback が重なるためです。
クラッシュログを見たら、まずは「そのオブジェクトは本当にまだ生きているか?」を疑うのがおすすめです。