NSException

  • @throw

一旦程序检测到异常,就必须得将它传递到处理它的代码,这段代码被称为异常处理程序。而异常传递的整个过程被称为抛出异常(throwing an exception),或者叫引发异常(raising an exception)。

示例代码:

NSException *exception = [NSException exceptionWithName:@"FileNotFoundException" reason:@"File Not Found on System" userInfo:nil];
@throw exception;
// [exception raise]; // 等价于@throw exception
1
2
3

然而@throwraise并不是完全等价的,一个重要的区别就是raise方法的调用者只能是NSException对象,而@throw的参数不仅可以是NSException对象,还可以是其他类型的对象(比如字符串对象),但是在Cocoa 应用程序中,应该只使用NSException对象。

  • @try-@catch-@finally

既然有异常,那么我们就需要捕获并处理它。通过@try-@catch来捕获异常,可以避免应用程序Crash。

@try {
  // 这里是可能会引发异常的代码,比如:数组越界、往数组中插入nil、使用KVC等
} @catch {
  // 这里是处理@try中抛出的异常的代码
} @finally {
  // 无论是否抛出异常,这里都会执行
}
1
2
3
4
5
6
7

异常处理程序也可以进行嵌套,最里层抛出的异常由它的外层进行catch,而最外层抛出的异常由于没有handler进行catch,则会进入到顶层的错误处理函数,以便在App闪退之前可以做些日志记录等相关操作。

一图以蔽之:


图取自Apple文档

demo代码如下:

void myUncaughtExceptionHandler(NSException *exception) {
    NSLog(@"uncaught exception is %@", exception); // 未捕获的异常
}

- (void)viewDidLoad {
    [super viewDidLoad];
    NSSetUncaughtExceptionHandler(myUncaughtExceptionHandler);
    [self doMethod1];
}

- (void)doMethod1 {
    @try {
        NSLog(@"-->> try1");
        [self doMethod2];
    } @catch (NSException *exception) {
        NSLog(@"-->> catch1");
        @throw;
    } @finally {
        NSLog(@"-->> finally1");
    }
}

- (void)doMethod2 {
    @try {
        NSLog(@"-->> try2");
        [self doMethod3];
    } @catch (NSException *exception) {
        NSLog(@"-->> catch2");
        @throw;
    } @finally {
        NSLog(@"-->> finally2");
    }
}

- (void)doMethod3 {
    @try {
        NSLog(@"-->> try3");
        @throw [NSException exceptionWithName:@"TestExceptionName" reason:@"throw an exception" userInfo:nil];
    } @catch (NSException *exception) {
        NSLog(@"-->> catch3");
        @throw;
    } @finally {
        NSLog(@"-->> finally3");
    }
}
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

虽然@try-@catch能捕获异常避免crash,但是在日常开发中却很少使用。一方面,源于Apple文档所说的内存管理方面的问题;另一方面,个人认为@try-@catch能够捕获的异常基本上都是我们应用层能够预料得到的或者说能够避免的。拿数组越界来举例,苹果会在底层实现判断是否会越界,如果没有越界就正常取值,反之生成一个exception对象并向外抛出。 通过汇编我们能看到一些蛛丝马迹:

CoreFoundation`-[__NSSingleObjectArrayI objectAtIndex:]:
->  0x1b47ebef0 <+0>:   cbnz   x2, 0x1b47ebf04   ;通过比较x2中的值是否为0来判断是否越界
    0x1b47ebef4 <+4>:   adrp   x8, 229448
    0x1b47ebef8 <+8>:   ldrsw  x8, [x8, #0x154]
    0x1b47ebefc <+12>:  ldr    x0, [x0, x8]
    0x1b47ebf00 <+16>:  ret    

    0x1b47ebf04 <+20>:  pacibsp                  ;如果越界,会执行到这里
    0x1b47ebf08 <+24>:  sub    sp, sp, #0x20             ; =0x20 
    0x1b47ebf0c <+28>:  stp    x29, x30, [sp, #0x10]
    0x1b47ebf10 <+32>:  add    x29, sp, #0x10            ; =0x10 
    .......                                      ;省略一些无用指令
    0x1b47ebf68 <+120>: mov    x4, #0x0
    0x1b47ebf6c <+124>: bl     0x1b3afb6a0               ; objc_msgSend
    0x1b47ebf70 <+128>: bl     0x1b3ae3cc4               ; objc_exception_throw ;最终执行到这,该函数就是异常的入口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

关于函数objc_exception_throw实现,可在Apple源码中找到,这里暂不深究。

最后更新: 2/1/2021, 5:39:58 PM