Try Catch 原理剖析

栏目: IOS · 发布时间: 6年前

内容简介:大部分语言的运行控制模型,都是基于栈的。在这种模型中,调用一个函数时,就会将这个函数的参数、返回地址、局部变量等信息入栈;这个函数返回时,对应信息再出栈。正常情况下,函数调用的进栈和出栈都是成对出现的,比如函数的调用顺序是:func1 —> func2 — > func3,那么一定是 func1 先进栈,然后是 func2,最后是 func3;而 func3 调用结束后一定是先返回到 func2 ,然后从 func2 返回到func3,而不能直接从 func3 返回到 func1。我们都知道,C 语言中的

一、非局部跳转

1、简介

大部分语言的运行控制模型,都是基于栈的。在这种模型中,调用一个函数时,就会将这个函数的参数、返回地址、局部变量等信息入栈;这个函数返回时,对应信息再出栈。正常情况下,函数调用的进栈和出栈都是成对出现的,比如函数的调用顺序是:func1 —> func2 — > func3,那么一定是 func1 先进栈,然后是 func2,最后是 func3;而 func3 调用结束后一定是先返回到 func2 ,然后从 func2 返回到func3,而不能直接从 func3 返回到 func1。

我们都知道,C 语言中的 goto 语句,可以实现在一个函数内部跳转;与此同时,C 语言还提供了一种能够在函数间跳转、被称为 非局部跳转 (no-local goto) 的机制,这种机制可以允许从一个多层嵌套的函数调用中直接返回。我们先通过下面的栗子来见证它的神奇之处:

#include <stdio.h>
#include <setjmp.h>

jmp_buf jump_buffer;

void func2(void)
{
printf("Before calling longjmp\n");
longjmp(jump_buffer, 1);
printf("After calling longjmp\n");
}

void func1(void)
{
printf("Before calling func2\n");
func2();
printf("After calling func2\n");
}

int main()
{
if (setjmp(jump_buffer) == 0){
printf("first calling set_jmp\n");
func1();
} else {
printf("second calling set_jmp\n");
}

return 0;
}

运行结果如下:

first calling set_jmp
Before calling func2
Before calling longjmp
second calling set_jmp

从日志可以看出,函数的执行过程跳过了 After calling func2After calling longjmp 两句日志所在的代码行,在 func2 中执行了 longjmp 方法后函数直接从 func2 跳转回了 main 函数中继续执行,而没经过 func1

2、实现机制

非局部跳转功能主要是通过位于 <setjmp.h> 中的 setjmplongjmp 两个函数实现。

  • setjmp
`int setjmp(jmp_buf env);`

可以把当前代码行的状态信息保存到 env 中,供以后 longjmp 恢复状态信息时使用。如果直接调用 setjmp() ,则返回值为 0;如果是由于调用了 longjmp 而调用到 setjmp ,则返回值为 longjmp 第二个参数所指定的值。

  • longjmp
`void longjmp(jmp_buf env, int val);`

用于将调用堆栈恢复成最近一次调用 setjmp 时所保存到 env 中的状态信息。也就是说,调用了 longjmp 后,不管当前调用堆栈在哪个方法中,都会回到有效范围内最近一次调用 setjmp 方法的地方,而 setjmp 方法的返回值就是这里设置的 val 的值,用于区分到底是从哪个 longjmp 返回到的 setjmp

jmp_buf<setjmp.h> 文件中定义的结构类型,用于保存系统状态信息。函数 setjmp 会将其所在的程序点的系统状态信息都保存到 jmp_buf 类型的结构变量 env 中,而调用 longjmp 会将 env 的系统状态信息恢复,以实现非局部跳转的功能。

3、注意事项

  • 执行顺序

setjmplongjmp 结合使用时,必须要有严格的先后执行顺序,即先调用 setjmp 函数,再调用 longjmp 函数。否则如果在 setjmp 之前调用 longjmp ,将导致程序的执行流变的不可预测,有可能导致程序崩溃。

  • 作用域

longjmp 必须在正确的 setjmp 的作用域范围内。具体来说,在一个函数中调用了 setjmp ,只要该函数没有返回,那么在任何其它地方都可以通过 longjmp 调用来跳转到 setjmp 的下一条语句执行。

二、try/catch 异常处理机制

在类 C 语言中,非局部跳转的一个重要应用场景就是 异常处理机制 。Objective-C 使用 try/catch/finally 来捕获并处理异常,比如下面的代码:

#import <Foundation/Foundation.h>

int main (int argc, const char * argv[])
{
@autoreleasepool
{
@try {
NSException *e = [NSException
exceptionWithName:@"FileNotFoundException"
reason:@"File Not Found on System"
userInfo:nil];
@throw e;
}
@catch (NSException *exception) {
if ([[exception name] isEqualToString:NSInvalidArgumentException]) {
NSLog(@"%@", exception);
} else {
@throw exception;
}
}
@finally {
NSLog(@"finally");
}
}
return 0;
}

通过 Clang 生成的 C 中间代码,可以看出 try/catch 的原理,上述代码保存成 main.m 文件后通过命令:

clang -rewrite-objc main.m

剔除无用信息后,可以得到下述代码:

#include <Foundation/Foundation.h>

int main (int argc, const char * argv[])
{
@autoreleasepool
{
/**
* try/catch的作用域从这里开始
*/
/* @try scope begin */
{
/**
* 首先定义一个_objc_exception_data类型的结构体,用来保存异常现场的数据。
*/
struct _objc_exception_data
{
/**
* buf变量就是 c语言 中的jmp_buf
* jmp_buf的定义可在setjmp.h文件中找到:
*
*      #define _JBLEN        (10 + 16 + 2)
*      #define _JBLEN_MAX    _JBLEN
*
*      typedef int jmp_buf[_JBLEN];
*/
int buf[18/*32-bit i386*/];

/**
* pointers[0]用来存储通过@throw抛出的异常对象,
* pointers[1]存储下一个_stack数据。
*/
char *pointers[4];
} _stack;

/**
* _rethrow保存可能在@catch中再次抛出的异常对象。
*/
id volatile _rethrow = 0;

/**
* 因为异常处理支持嵌套,_stack会被存储在一个全局的栈中,这个栈用单链表的存储结构表示。
* objc_exception_try_enter函数将_stack压栈。
*/
objc_exception_try_enter(&_stack);

/**
* _setjmp是C的函数,用于保存当前程序现场。
* _setjmp需要传入一个jmp_buf参数,保存当前需要用到的寄存器的值。
* _setjmp()它能返回两次,第一次是初始化时,返回0,第二次遇到_longjmp()函数调用会返回,返回值由_longjmp的第二个参数决定。
* 如果对_setjmp()和_longjmp()概念不太了解的,请参考C语言的异常处理机制。
*
* 下面_setjmp()初始化返回0,然后执行if{}中也就是@try{}中的代码。
*/
if (!_setjmp(_stack.buf)) /* @try block continue */
{
/**
* 创建一个NSException对象,对应代码:
*
*             NSException *e = [NSException
*                               exceptionWithName:@"FileNotFoundException"
*                               reason:@"File Not Found on System"
*                               userInfo:nil];
*/
NSException *e = ((NSException *(*)(id, SEL, NSString *, NSString *, NSDictionary *))(void *)objc_msgSend)(objc_getClass("NSException"), sel_registerName("exceptionWithName:reason:userInfo:"), (NSString *)&__NSConstantStringImpl_main_m_0, (NSString *)&__NSConstantStringImpl_main_m_1, (NSDictionary *)((void *)0));

/**
* 抛出异常对象,对应代码:@throw e;
*
* objc_exception_throw函数实现步骤如下:
* 1. 把e对象保存到_stack->pointers[0]中使其在@catch{}中能被捕获。
* 2. 将_stack从全局栈中弹出。
* 3. 调用_longjmp()跳转到前面if语句中的_setjmp()位置。_longjmp()使得_setjmp()函数第二次返回,
* 返回值为1,所以会执行else{}中也就是@catch{}中的代码。
*/
objc_exception_throw(e);

} /* @catch begin */ else {

/**
* objc_exception_extract函数从_stack->pointers[0]中取得上面抛出的异常对象。
*/
id _caught = objc_exception_extract(&_stack);

/**
* 这里为何再次调用objc_exception_try_enter对_stack压栈?先保留这个疑问,继续看下面的代码。
*/
objc_exception_try_enter (&_stack);

/**
* 在@catch中设置一个跳转位置
*/
if (_setjmp(_stack.buf))

/**
* 如果@catch{}中再次抛出异常,在这里捕获。
*/
_rethrow = objc_exception_extract(&_stack);

else { /* @catch continue */

/**
* objc_exception_match函数判断_caught对象是否是需要捕获的目标对象。对应代码:
*
* @catch (NSException *exception) {
*/
if (objc_exception_match((struct objc_class *)objc_getClass("NSException"), (struct objc_object *)_caught)) {
NSException *exception = _caught;

/**
* 比较捕获的异常是不是NSInvalidArgumentException类型。对应代码:
*
* if ([[exception name] isEqualToString:NSInvalidArgumentException]) {
*      NSLog(@"%@", exception);
*
*/
if (((BOOL (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)((NSString *(*)(id, SEL))(void *)objc_msgSend)((id)exception, sel_registerName("name")), sel_registerName("isEqualToString:"), (NSString *)NSInvalidArgumentException)) {

NSLog((NSString *)&__NSConstantStringImpl_main_m_2, exception);
} else {

/**
* 抛出异常对象,然后跳转到前面@catch中的if语句中的_setjmp()位置。
* 这就解释了前面为什么要在@catch中再次将_stack压栈和调用_setjmp()的原因。
* 在当前@catch中,如果不设置一个跳转点来捕获@catch中抛出的异常,那么程序就直接跳转到全局栈的下一个@catch中,而下面的@finally{}代码就无法执行。
* 在@catch中设置跳转点就是为了最后总能执行@finally中的代码。
*/
objc_exception_throw( exception);
}

} /* last catch end */ else {

/**
* 如果异常对象没被处理,先将其保存到_rethrow变量。
* objc_exception_try_exit函数将_stack从全局栈中弹出。
*/
_rethrow = _caught;
objc_exception_try_exit(&_stack);
}
} /* @catch end */
}
/* @finally */
{
if (!_rethrow) objc_exception_try_exit(&_stack);

NSLog((NSString *)&__NSConstantStringImpl_main_m_3);

/**
* _rethrow是前面@catch中没有被处理的或被捕获的异常对象,
* 最后,_rethrow异常对象被抛到全局栈的下一个@catch中。
*/
if (_rethrow) objc_exception_throw(_rethrow);
}

} /* @try scope end */

}
return 0;
}

以上代码还涉及了 objc_exception_try_enter、 objc_exception_extract、 objc_exception_throw、 objc_exception_try_exit 等函数,都可以在苹果开源 objc4 的 objc-exception.mm 文件中找到。

参考文档


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

大学的终结

大学的终结

[美] 凯文·凯里(Kevin Carey) / 朱志勇、韩倩 / 人民邮电出版社 / 2017-2-28 / 59.00

你了解目前全球高等教育的现状吗?你知道高等教育的未来是什么样的吗?你听说过泛在大学吗?翻开本书,了解大学的过去、现在与未来。 《大学的终结:泛在大学与高等教育革命》一书由美国著名教育作家凯文? 凯里倾情打造。作者在书中详细论述了美国大学的历史变迁、大学的本质、大学的未来、信息技术与教育的关系、泛在大学的定义、传统大学在大趋势下的挣扎,以及未来高等教育的学历认证与呈现形式。本书作者用缜密的逻辑......一起来看看 《大学的终结》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具