2013/01/22更新

[XCODE] iPhoneアプリが予期せぬエラーで異常終了する際に、そのログを取得しておく方法

このエントリーをはてなブックマークに追加      

こんにちは、@yoheiMuneです。
本日は、Evernoteアプリなどにある機能で、アプリが異常終了した際のログを保存しておき、 次の起動時に異常終了したことを検知したり、そのログを取得する方法をブログに書きたいと思います。

画像



iPhoneアプリで異常終了した際のログを取得するメリット

iPhoneアプリの開発経験があれば、誰しも異常終了した際のデバッグに困ったことが多いのではないでしょうか? 特にリリース後にユーザーから指摘された異常終了を再現するとなると、ちょいと大変。

iPhone実機上で動いている場合にも、異常終了した場合の詳細なログを取得しておけるとデバッグがすごく楽になります。 今回はそのための方法を書きたいと思います。



異常終了時にエラーログを取得して保存しておく実装方法

具体的な実装方法は以下の通りになります。
AppDelegateにエラーキャッチの仕組みと、エラーをキャッチした際のログの取得/保存する仕組みを導入します。

(AppDelegate.mでの実装)
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {    
    
	〜 途中省略 〜
    
    // エラー追跡用の機能を追加する。
    NSSetUncaughtExceptionHandler(&exceptionHandler);
    
    return YES;
}

// 異常終了を検知した場合に呼び出されるメソッド
void exceptionHandler(NSException *exception) {
    // ここで、例外発生時の情報を出力します。
    // NSLog関数でcallStackSymbolsを出力することで、
    // XCODE上で開発している際にも、役立つスタックトレースを取得できるようになります。
    NSLog(@"%@", exception.name);
    NSLog(@"%@", exception.reason);
    NSLog(@"%@", exception.callStackSymbols);
    
    // ログをUserDefaultsに保存しておく。
    // 次の起動の際に存在チェックすれば、前の起動時に異常終了したことを検知できます。
    NSString *log = [NSString stringWithFormat:@"%@, %@, %@", exception.name, exception.reason, exception.callStackSymbols];
    [[NSUserDefaults standardUserDefaults] setValue:log forKey:@"failLog"];
}

こんな簡単な実装で、異常終了時のログを採取することが可能となります。


例えば、以下のような悪いコードを書いた場合には、
// わざとエラーが発生するように、実装する。
NSArray *array = [NSArray array];
[array objectAtIndex:0];
以下のような、エラーログが表示されます。
2013-01-20 22:10:31.090 iPhoneStudy[1366:c07] NSRangeException
2013-01-20 22:10:31.091 iPhoneStudy[1366:c07] *** -[__NSArrayI objectAtIndex:]: index 0 beyond bounds for empty array
2013-01-20 22:10:31.094 iPhoneStudy[1366:c07] (
0   CoreFoundation                      0x021bc02e __exceptionPreprocess + 206
1   libobjc.A.dylib                     0x01ec9e7e objc_exception_throw + 44
2   CoreFoundation                      0x02171b44 -[__NSArrayI objectAtIndex:] + 196
3   iPhoneStudy                         0x0002292e -[ShowErrorViewController viewDidLoad] + 142
4   UIKit                               0x0113d817 -[UIViewController loadViewIfRequired] + 536
5   UIKit                               0x0113d882 -[UIViewController view] + 33
6   UIKit                               0x0113db2a -[UIViewController contentScrollView] + 36
7   UIKit                               0x01154ef5 -[UINavigationController _computeAndApplyScrollContentInsetDeltaForViewController:] + 36
8   UIKit                               0x01154fdb -[UINavigationController _layoutViewController:] + 43
9   UIKit                               0x01155286 -[UINavigationController _updateScrollViewFromViewController:toViewController:] + 254
10  UIKit                               0x01155381 -[UINavigationController _startTransition:fromViewController:toViewController:] + 72
11  UIKit                               0x01155eab -[UINavigationController _startDeferredTransitionIfNeeded:] + 386
12  UIKit                               0x011564a3 -[UINavigationController pushViewController:transition:forceImmediate:] + 1030
13  UIKit                               0x01156098 -[UINavigationController pushViewController:animated:] + 62
14  iPhoneStudy                         0x00003b14 -[RootViewController tableView:didSelectRowAtIndexPath:] + 388
15  UIKit                               0x0110b8d5 -[UITableView _selectRowAtIndexPath:animated:scrollPosition:notifyDelegate:] + 1194
16  UIKit                               0x0110bb3d -[UITableView _userSelectRowAtPendingSelectionIndexPath:] + 201
17  Foundation                          0x00c7de83 __NSFireDelayedPerform + 380
18  CoreFoundation                      0x0217b376 __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__ + 22
19  CoreFoundation                      0x0217ae06 __CFRunLoopDoTimer + 534
20  CoreFoundation                      0x02162a82 __CFRunLoopRun + 1810
21  CoreFoundation                      0x02161f44 CFRunLoopRunSpecific + 276
22  CoreFoundation                      0x02161e1b CFRunLoopRunInMode + 123
23  GraphicsServices                    0x02f977e3 GSEventRunModal + 88
24  GraphicsServices                    0x02f97668 GSEventRun + 104
25  UIKit                               0x0105c65c UIApplicationMain + 1211
26  iPhoneStudy                         0x00001f63 main + 67
27  iPhoneStudy                         0x00001ed5 start + 53
)
2013-01-20 22:10:31.095 iPhoneStudy[1366:c07] *** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArrayI objectAtIndex:]: index 0 beyond bounds for empty array'
*** First throw call stack:
(0x21bc012 0x1ec9e7e 0x2171b44 0x2292e 0x113d817 0x113d882 0x113db2a 0x1154ef5 0x1154fdb 0x1155286 0x1155381 0x1155eab 0x11564a3 0x1156098 0x3b14 0x110b8d5 0x110bb3d 0xc7de83 0x217b376 0x217ae06 0x2162a82 0x2161f44 0x2161e1b 0x2f977e3 0x2f97668 0x105c65c 0x1f63 0x1ed5)
libc++abi.dylib: terminate called throwing an exception
(lldb) 

上記ログを見て、自身のクラス(今回の例では、iPhoneStudyプロジェクトのShowErrorViewController)で、NSArrayのindex不正で落ちたことがわかります。
実際の実装では異常終了したNSArrayが何に使われているものでどのような情報をつめているはずかを事前に把握していれば、よりデバッグしやすいかもしれません。



最後に

iPhoneアプリを開発する上で、異常終了に対するデバッグ処理の効率化は大変重要かと思います。
上記のNSArrayのindex不正もそうですが、ネットワーク処理やマルチスレッドなどの実装を行うと意味不明に落ちる場合もあり、再現させるまでが大変です。

今後もより開発しやすい環境を目指しいろいろと調べて、ブログに書きたいと思います。
最後までご覧頂きましてありがとうございました。





こんな記事もいかがですか?

RSS画像

もしご興味をお持ち頂けましたら、ぜひRSSへの登録をお願い致します。