让断言无所遁形 - iOS断言弹窗

2,151 阅读3分钟

1.前言:

在平时开发中,使用NSAssert断言处理产生的崩溃和普通崩溃是一样的体现。这里就无法直观的区分出是断言还是崩溃(当然可以等崩溃平台解析数据),我们能不能把断言提早展示出来,便于区分。今天提供一个轻量级的断言提示工具。

2.直接上图:

3.使用到技术点

  1. NSAssert断言原理
  2. backtrace获取线程调用栈
  3. 本地符号化

4.NSAssert

4.1 断言的本质:NSAssertionHandler

可以阅读 nshipster.cn/nsassertion… 对NSAssert进行了解

Objective-C 用一个面向对象的途径混合了 C 语言风格的断言宏定义来注入和处理断言失败。即:NSAssertionHandler

每个线程拥有它自己的断言处理器,它是 NSAssertionHandler 类的实例对象。当被调用时,一个断言处理器打印一条包含方法和类名(或者函数名)的错误信息。然后它抛出一个 NSInternalInconsistencyException 异常。`

4.2 为什么正式环境不会报异常: NS_BLOCK_ASSERTIONS

从 Xcode 4.2 开始,发布构建默认关闭了断言,它是通过定义 NS_BLOCK_ASSERTIONS 宏实现的。也就是说,当编译发布版时,任何调用 NSAssert 等的地方都被有效的移除了。 在Build Settings菜单,找到Preprocessor Macros项,在Release中设置NS_BLOCK_ASSERTIONS,不进行断言检查。如下图所示。

4.3自己实现断言

#if !define(NS_BLOCK_ASSERTIONS)
#define HXAssert(condition,desc,...) \
do {\
__PRAGMA_PUSH_NO_EXTRA_ARG_WARNINGS \
if (__builtin_expect(!(condition),0)) {\
UIAlertView * av = [[UIAlertView alloc] initWithTitle:@"断言崩溃,将在5秒后退出,请及时截图" message:desc delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];\
[av show];\
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(50 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{\
NSString *__assert_file__ = [NSString stringWithUTF8String:__FILE__];\
__assert_file__ = __assert_file__ ?__assert_file__: @"<Unknow file>";\
[[NSAssertionHandler currentHandler] handleFailureInMethod:_cmd object:self file:__assert_file__ lineNumber:__LINE__ description:(desc),##__VA_ARGS__];\
});\
}\
__PRAGMA_POP_NO_EXTRA_ARG_WARNINGS\
} while(0) 

5.获取线程调用栈

在获取线程调用堆栈前,要对函数的调用过程有个大概的了解(寄存器,汇编的角度)。不了解的可以看下这些文章:

blog.cnbluebox.com/blog/2017/0… juejin.cn/post/684490… mp.weixin.qq.com/s/ty8Pt56EQ… blog.csdn.net/wxs0124/art…

在arm64的机器上,有几个重要的寄存器,R29,R30,R31寄存器,分别也叫fp,lr,sp。

注: lr 是link register中的值,它存的是方法_funcA的执行的最后一行指令的下一行。它的作用也很好理解:当_funcB执行完了之后要返回_funcA继续执行,但是计算机要如何知道返回到哪执行呢? 就是靠lr记录了返回的地址,方法才能得以正常返回。 @引用自《iOS开发同学的arm64汇编入门》

FP 寄存器:指向当前方法栈的底部

SP 即我们通常说的栈帧 SP(Stack Pointer)。指向当前方法栈的顶部

也就是说我们要用这几个特殊的寄存器获得我们整个线程的调用堆栈,但我们只有3个特殊寄存器,我们如何去找上上层或者上上层的调用方? 这里强烈推荐大家一片文章 mp.weixin.qq.com/s/ty8Pt56EQ… 清楚的描述了我们在调用一个方法的时候,寄存器是如何工作的。

在这个项目工程中,调用thread_get_state获得指定线程上下问信息_STRUCT_MCONTEXT_STRUCT_MCONTEXT结构存储当前线程栈顶指针(sp)和最顶部的栈帧指针(frame pointer),我们通过sp,fp不断得往前进行回溯,这里我们使用一个 vm_read_overwriteAPI从而获得整个线程的调用栈。在代码中我们用了一个链表的数据接口进行堆栈信息的存储。

6.本地符号化

通过上一节地址通过符号化得到我们能看得懂的方法。这里我们需要了解Mach-O的结构及符号表的知识。可以阅读下面的文章进行了解:

www.desgard.com/iOS-Source-…

具体符号流程如下:

7.项目工程 & 参考代码、文章

项目工程:github.com/lalalafq/FQ…

Mach-o / FishHook / NSAssert / MTHawkeye / KSCrash