在Java层,我们可以使用try/catch语句来捕获和处理异常。然而,在Android的Native层(C/C++代码),我们并没有内置的异常处理机制。这篇文章将介绍如何在Android Native层实现类似于try/catch的异常处理机制。
一、技术原理
在Native层实现异常处理的关键在于信号处理(Signal Handling)和非局部跳转(Non-Local Jumps)。当程序发生错误(如访问非法内存、除以零等)时,操作系统会向进程发送一个信号。我们可以设置一个信号处理函数(Signal Handler),在收到信号时执行特定的代码。
非局部跳转提供了一种在程序中跳转到另一个位置的方法,而不是按照正常的控制流程执行。在C语言中,我们可以使用setjmp
和longjmp
函数来实现非局部跳转。setjmp
函数保存当前的执行上下文(包括堆栈和寄存器状态等),并返回0。longjmp
函数恢复由setjmp
保存的上下文,并使setjmp
返回一个非零值。我们可以利用这个特性,在信号处理函数中调用longjmp
,跳转到setjmp
所在的位置,实现异常的捕获和处理。
二、代码实现
2.1 定义结构体保存线程的异常处理信息
首先,我们定义一个结构体native_code_handler_struct
,用于保存线程的异常处理信息。这个结构体包括一个sigjmp_buf
类型的变量ctx
,用于保存setjmp
的上下文;一个标志位ctx_is_set
,表示上下文是否已经被设置;以及其他与异常处理相关的信息。
2.2 实现try/catch语义
然后,我们定义了一系列的函数和宏,用于实现try/catch语义。COFFEE_TRY
宏检查当前是否已经在一个try块中(通过inside
函数),如果不在,则设置信号处理函数(通过setupSignalHandler
函数)并保存执行上下文(通过sigsetjmp
函数)。COFFEE_CATCH
宏和COFFEE_END
宏则用于标识catch块和try/catch块的结束。
2.3 检查当前线程的异常处理信息
inside
函数检查当前线程的异常处理信息,如果已经在一个try块中,则增加reenter
计数并返回1;否则返回0。
2.4 设置信号处理函数
setupSignalHandler
函数设置信号处理函数,并将reenter
计数加1,表示进入了一个新的try块。
handler_setup
设置崩溃处理器,包括全局和线程相关的资源。首先调用handler_setup_global(id)
初始化全局资源,然后为当前线程初始化本地资源。
handler_setup_global
初始化全局资源,包括分配内存、设置信号处理函数等。首先为native_code_g.sa_old
和native_code_g.id
分配内存,然后设置信号处理函数coffeecatch_signal_pass
,并将其设置到指定的信号上。
2.5 信号处理和非局部跳转
coffeecatch_signal_pass
和coffeecatch_try_jump_userland
两个函数用于信号处理和非局部跳转,以实现在Java层捕获Native层的异常。
2.5.1 信号处理函数实现
coffeecatch_signal_pass
是一个信号处理函数,用于在捕获到信号时执行。它首先调用原始的Java信号处理器,然后设置一个定时器以防止死锁。接着,它检查是否有可用的上下文,如果有,则将信号信息和上下文信息保存到native_code_handler_struct
结构体中,并尝试跳转到用户空间。如果没有可用的上下文,函数将调用abort()
终止程序。
2.5.2 跳转回用户空间
coffeecatch_try_jump_userland
尝试将程序的执行环境跳转回用户空间。它首先检查是否有有效的上下文,如果有,则恢复备用堆栈,并调用siglongjmp()
函数跳转回之前保存的执行环境。
需要注意的是,siglongjmp()
函数在信号处理中并不是异步信号安全的,因此在使用它时需要谨慎。
这段代码的主要作用是在捕获到信号时执行特定的操作,例如保存信号信息、恢复执行环境等。
2.6 清理异常处理的资源
cleanup
函数清理异常处理的资源,并将reenter
计数减1,表示退出了一个try块。
revert_alternate_stack()
用于恢复线程的堆栈。它通过 sigaltstack()
系统调用获取当前线程的堆栈信息,并将 SS_ONSTACK
标志位清除,表示不再使用备用堆栈。
handler_cleanup()
用于清理异常处理的全局资源:
- 释放当前线程的异常处理信息,并恢复线程的堆栈。
- 通过
pthread_mutex_lock()
和pthread_mutex_unlock()
函数加锁和解锁全局资源,以保证在多线程环境中的安全性。 - 遍历所有捕获的信号,并使用
sigaction()
函数将信号处理函数恢复为最早设置的旧信号处理函数。 - 释放所有分配的内存,并使用
pthread_key_delete()
函数删除线程局部存储的键。
三、使用示例
3.1 示例
上述实现允许我们从信号(如segv,sibus等)中恢复正常,就像一个Java异常一样。然而,它无法从allocator/mutexes
等问题中恢复正常,但至少大多数崩溃(如空指针解引用、整数除法、栈溢出等)应该可以处理。
我们需用使用-funwind-tables
编译所有的库,才可以在所有的二进制文件上获取正确的堆栈信息。在ARM上,也可以使用--no-merge-exidx-entries
链接器开关,来解决堆栈相关的问题。在Android上,可以在每个库的Android.mk文件中使用以下行来实现这一点: LOCAL_CFLAGS := -funwind-tables -Wl,--no-merge-exidx-entries
以下是一个简单的示例,演示如何在Android Native层使用上述代码实现的try/catch异常处理机制。
当异常发生时,程序会跳过try块中剩余的代码,直接进入catch块。这样,我们可以捕获和处理异常,避免程序崩溃。
通过上述代码,我们可以在Android Native层实现类似于Java的try/catch异常处理机制。这对于提高Native代码的稳定性和可维护性非常有帮助。需要注意的是,这种方法并不能捕获所有类型的异常,例如C++抛出的异常。在实际应用中,我们需要根据具体的需求和场景来选择最合适的异常处理策略。
3.2 如何在Native层获取更多的异常信息
我们还可以在catch块中获取和处理这些异常信息。例如,打印异常类型、出错地址、寄存器状态等。
这个函数的主要作用是在捕获到异常时获取异常的详细信息,以便在异常处理代码中使用。通过这个函数,我们可以在Android Native层实现更详细和准确的异常处理。
需要注意的是,在处理异常时,我们应该尽量避免执行可能触发新异常的操作,例如访问非法内存、调用不安全的函数等。在实际应用中,我们可以根据具体的需求和场景来选择最合适的异常处理策略。
3.3 限制
- 本文提供的异常处理机制不能捕获所有类型的异常。例如,不能捕获由于堆栈溢出导致的异常。对于这些情况,需要使用其他方法来进行处理和调试。
- 在某些架构和编译器下,
setjmp
和longjmp
函数的行为可能与本文描述的不完全相同。因此在使用本文提供的异常处理机制之前,请确保在目标平台上能够正常工作。 - 本文提供的异常处理机制可能会影响应用程序的性能。因为它需要在运行时设置信号处理函数,并在发生异常时执行非局部跳转。在性能敏感的场景中,请谨慎使用这种机制。
3.4 注意事项
- 在使用本文提供的异常处理机制时,请确保正确地设置和清理信号处理函数。在多线程环境中,需要为每个线程单独设置和清理信号处理函数。
- 在catch块中,尽量避免执行可能引发新异常的代码。因为在catch块中发生的异常可能无法被捕获和处理。
- 在catch块中,可以使用
COFFEE_EXCEPTION()
宏获取异常的详细信息,例如信号编号、错误地址等。这些信息对于调试和错误报告非常有用。 - 请注意,本文提供的异常处理机制并不能替代合理的错误处理和资源管理策略。在编写Native代码时,请始终确保正确地处理错误情况,并在适当的时候释放分配的资源。
四、如何在Native层捕获和处理C++抛出的异常
在前面的部分中,我们已经介绍了如何在Android Native层实现类似于Java的try/catch异常处理机制,并获取异常的详细信息。现在,我们将介绍如何在Native层捕获和处理C++抛出的异常。
在C++中,异常处理机制与C语言中的信号处理和非局部跳转不同。C++异常是通过throw
语句抛出的,可以被catch
语句捕获和处理。由于C++异常处理机制与C语言不兼容,我们需要使用C++特性来捕获和处理C++异常。
以下是一个简单的示例,演示如何在Android Native层捕获和处理C++抛出的异常:
在这个示例中,我们使用C++的try
和catch
语句捕获和处理异常。当发生异常时,程序会跳过try块中剩余的代码,直接进入catch块。这样,我们可以捕获和处理C++抛出的异常,避免程序崩溃。
需要注意的是,C++异常处理机制与前面介绍的C语言异常处理机制不兼容。在混合使用C和C++代码的项目中,我们需要分别处理C和C++的异常。在实际应用中,我们可以根据具体的需求和场景来选择最合适的异常处理策略。
五、总结
总结一下,在Android Native层实现异常处理机制,我们需要考虑以下几点:
- 使用信号处理和非局部跳转实现类似于Java的try/catch异常处理机制,捕获C语言中的异常(如非法内存访问、浮点异常等)。
- 在信号处理函数中获取异常的详细信息(如信号类型、出错地址、寄存器状态等),并在catch块中进行处理。
- 对于C++抛出的异常,使用C++的try/catch语句进行捕获和处理。
通过以上方法,我们可以在Android Native层实现更稳定和可维护的代码。在实际应用中,我们需要根据具体的需求和场景来选择最合适的异常处理策略。
最后
如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。
如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。