再帰処理を書いたら「EXC_BAD_ACCESS (SIGSEGV)」が発生してアプリがクラッシュした。
warning: could not load any Objective-C class information. This will significantly reduce the quality of type information available.
どうやらXcodeのデバッグ環境では3000回以上の再帰を繰り返すと、無限ループとみなされてしまうらしい。デバッグビルドとの関係からStack overflowが発生しやすくなっているのだろうか。
対処方法
ケースによって対応方法が異なる。C言語やC++の場合は次に説明する# 再帰処理の場合や# 最適化を意識するを参考にすると良い。
無限ループの場合
super
指定で呼ぶべきメソッドをself
で呼び出していたりすると無限ループの原因になる。また独自定義のプロパティは直接インスタンス変数を更新しなければならない。
- (void)setName:(NSString *)name {
// 無限ループの原因になる
self.name = name; // NG
// 親クラスのプロパティを更新する場合
super.name = name; // OK
// 独自に宣言したプロパティの場合
_name = name; // OK
}
Swiftの場合も同じ。
override var name: String {
set {
self.name = newValue // NG
super.name = newValue // OK
}
}
再帰処理の場合
意図的に再帰処理を書いている場合は少し厄介。
とりあえず、リリースビルドであれば強制的にクラッシュすることは少ないので、どうしても巨大データによる再帰処理をテストしたいような場合には、一時的に以下の手順でリリースビルドに切り替えると良い。
本番環境の誤運用には注意すること
Xcodeメニューバー → Product → Scheme → Edit Scheme...
Run → Info → Bundle Configuration → [Release]
根本的な問題への対処
たとえ3000回以上の再帰が可能になったとしても、限界はある。
Objective-Cの再帰的なメソッド呼び出しは最適化が効かないため、いずれどこかでスタックオーバーフローの問題と遭遇することになる。
対応策1
この問題に対処するためには、再帰呼び出しによる処理を廃止し、for文やwhileによる単純なループ処理に置き換えると良い。
対応策2
または以下のコードのように、C言語関数をラップする形をとる方法もある。
static long Function(long v) {
if (v <= 0) return 0;
return v + Function(v - 1);
}
- (long)method:(long)v {
return Function(v);
}
最適化ビルド時にはFunction
関数の再帰処理が単純なループ処理に置き換わる可能性が生まれるため、場合によってはスタックオーバーフローの問題を回避出来る。
最適化を意識する
以下のように関数の再帰呼出しが処理の最終ステップで行われていると、最適化の恩恵を受けられる可能性がある。
static long fn(long v) {
if (v <= 0) return 0;
return v + fn(v - 1); // 再帰呼び出し
}
以下のように、再帰呼び出し後に余計な処理が行われていると、最適化の対象にならず、先程のスタックオーバーフローの問題にも繋がる。
static long fn(long v) {
if (v <= 0) return 0;
long r = v + fn(v - 1); // 再帰呼び出し
puts("hello"); // 最適化を阻害
return r;
}
Objective-C「ちょっと勘弁してください」
俺「大丈夫、君なら出来る」
Objective-C「いや現に落ちてます」