异常的处理其实很重要,一开始我都是在使用模拟器运行 App,一旦发现错误就可以在 Xcode 中查看发生了什么异常。但是如果安装了测试包给测试人员,一旦程序有 bug 崩溃了,而且是偶现的,这个 bug 就比较难重现,这个时候,就应该在崩溃的时候把崩溃日志写到手机的沙盒上,下次运行的时候,把崩溃日志上传到服务器,然后开发人员根据崩溃日志把 bug 修复。在开发调试阶段,收集崩溃日志有助于开发者快速定位 bug 并修复,是在已上线阶段,收集崩溃日志能够把一些潜在的 bug 修复,提升用户体验。

Try-Catch-Finally Block

捕获代码块的异常

如果我们希望捕获某一段有可能报异常的代码,那可以用 Try-Catch-Finally Block 来捕获,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@try {
// Code that can potentially throw an exception
NSArray *array = @[@"foo", @"bar"];
NSLog(@"%@", array[2]);
} @catch (NSException *exception) {
// Handle an exception thrown in the @try block
NSLog(@"exception -> %@", exception);
} @finally {
// Code that gets executed whether or not an exception is thrown
NSLog(@"finally.");
}
// 打印
exception -> *** -[__NSArrayI objectAtIndex:]: index 2 beyond bounds [0 .. 1]
finally.

捕获整个程序的异常

同样的道理,我们也可以在 main.m 文件中启动应用程序时加入 Try-Catch-Finally Block,只要程序任意位置一旦报异常,便能捕获得到:

1
2
3
4
5
6
7
8
9
10
11
12
int main(int argc, char * argv[]) {
@try {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
} @catch (NSException *exception) {
// 在这里可以把 exception 信息写到沙盒里,下次打开应用程序就把 exception 信息提交到服务器
NSLog(@"exception -> %@", exception);
} @finally {
}
}

这时,无论在哪个地方报异常,都会被捕获到,到时候我们可以把 NSException 的属性值取出来,放到一个 plist 文件即可:

1
2
3
4
5
6
@property (readonly, copy) NSExceptionName name;
@property (nullable, readonly, copy) NSString *reason;
@property (nullable, readonly, copy) NSDictionary *userInfo;
@property (readonly, copy) NSArray<NSNumber *> *callStackReturnAddresses NS_AVAILABLE(10_5, 2_0);
@property (readonly, copy) NSArray<NSString *> *callStackSymbols NS_AVAILABLE(10_6, 4_0);

不过注意,不能够在 catch 到异常的时候就把异常信息提交到服务器,因为网络请求是比较耗时的,所以最好就是先保存到沙盒,下次进入应用程序时再提交到服务器。

比较好的做法

虽然说可以在 main.m 里可以先保存异常信息到沙盒,下次打开程序再提交到服务器。还有一种比较主流的方法就是:先告诉你有异常即将来袭了,然后它再去打印异常信息。在 AppDelegate.m 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 在这里拦截异常
void handleException(NSException *exception) {
NSLog(@"exception -> %@", exception.name);
NSMutableDictionary *exceptionInfo = [NSMutableDictionary dictionary];
exceptionInfo[@"name"] = exception.name; // 异常名字
exceptionInfo[@"callStackSymbols"] = exception.callStackSymbols; // 调用栈信息(异常来自哪个方法)
exceptionInfo[@"reason"] = exception.reason; // 异常描述
NSString *filePath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"exceptionInfo.plist"];
[exceptionInfo writeToFile:filePath atomically:YES];
}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// 下次进来程序就把异常信息上传到服务器
// 设置捕捉异常到回调,只需要设置一次
NSSetUncaughtExceptionHandler(handleException);
return YES;
}
// 打印,如果在真机中,是直接崩掉的
exception -> NSRangeException // 这里是我们写的,下面是系统抛出来的
*** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArray0 objectAtIndex:]: index 0 beyond bounds for empty NSArray'
*** First throw call stack:
(
…………
)
libc++abi.dylib: terminating with uncaught exception of type NSException

小 Tips:

  • 可以在捕捉到异常到时候弹出一个对话框,告知用户程序崩溃了,此时为了能够让用户点击『确定』按钮,需要在当前线程重新启动 RunLoop,然后在『确定』按钮到回调中直接调用 exit(0); 即可。
  • 在写异常信息到沙盒的时候,可以把每一次的异常信息按照时间来写到一个文件中,这样就不会把上次到异常信息给覆盖掉了。