上篇 中讲到了Crash处理流程分为四个环节,也分析了Crash防护的方法,本章来讲下其余三个环节:Crash的拦截、Crash的上报、Crash的后续。
所有的未被防护住的Crash最终会走到这一步,在这里我们必须要保证拦截的 全面性
、稳定性
尽可能多的拦截到所有类型的异常,同时拦截逻辑本身不能产生异常。那么我们需要通过以下几个方面去考虑。
和多数操作系统一样,iOS的异常也基本分为 用户层
系统底层
信号
这三个类别,接下来我们看下每种异常都做了哪些事情
然后我去看Runtime的源码进一步证明了这个说法,runloop中大量使用这种方式监听mach异常消息,一旦Crash随时准备打破循环,因为系统也需要监听crash,统一出口将对监听来说对系统将变得非常方便。
根据代码上下文可以判断出,苹果会监听统一的异常端口,在出现异常后进行相应的操作,也印证了我当时的推断。
signal的产生流程大概分为几种情况
MachExcption
转换而成的signal但是需要注意一点:收到signal不一定会Crash,但是Crash一定会有Signal发出
上面分析了每种Crash的类型,那么这三种类型的Crash是如何在App生命周期中传递的呢?他们又是如何相互转化以及相互之间有什么关系呢?
帮大家提取下上图中的几个关键信息
通过上面的分析大家一定会说通过Mach端口的拦截更加全面,毕竟苹果自己也在用。但是在实际使用中有一个问题,mach会拦截所有的异常以及信号量,也就是随便一个操作(比如发一个自定义signal等)可能都被mach捕获,那么如果在其捕获回调中再进行捕获就会很容易发生死锁,而且容易和系统的处理产生冲突。当时看了PLCrash的文档,也看到了开发者写的一句话:
这样说明了大家确实被坑过。
那接下来只剩signal 和 exception,其实细心的同学早已发现这两个的优缺点是一个互补的状态
那么最终的方式采用 singnal + exception的方式进行捕获,最终的流程为:
上面的流程图可以看出在每一个CustomHandle之前都会有一个PreviousHandle,其实是因为在iOS系统中只能存在一个customHandel,如果你的项目中接入了或者准备接入多个 Crash 防护相关的SDK(虽然不建议这么做),那么多个Handle之间一定会产生冲突,导致堆栈不明确,或者丢失。所以在注册我们的handle前先将之前的handle指针保存下来,等我们的handle处理完后在通过函数指针调用回去,这样就能保证每一个handle都能被正常调用。
NSGetUncaughtExceptionHandler
获得之前handle指针,之后再通过NSSetUncaughtExceptionHandler(oldHandler);
调用回去。sigaction
函数获得之前的handle指针。因为苹果使用了(Address Space Layout Randomization ) 地址空间配置随机加载技术,所以线上堆栈必须要通过符号表堆栈还原进行解读,不然的话就是内存地址。所以当我们使用NSThread的相关函数在Debug下虽然能看到可读性的堆栈,但是在线上包上并不可取,那我们要怎么获取堆栈呢?先来看下符号表的构造:
之前拿到这样的符号表,我们通常手动还原,找一个相同系统的真机,找到对应库的基地址按照符号表上函数的偏移量进行计算(通过LLDB的相关函数)
通过看Mach-o相关接口可以找到相关函数进行端内符号表还原,大致流程为:
最终的效果:
通常在AppCrash后会在handle中做些上报操作.
但是这样做有两个问题:
通过查看runloop源码可以看出,在Crash发生后当前runloop中断
注意:runloop本次循环还在继续,但是循环已经被打破,本次循环结束后app才退出 既下图的retVal被置为NO
iOS Crash发生后 runloop中的do-while循环的条件会被置为 NO,然后Handler函数走完之后当前循环后直接结束,不会在进行下一次循环了,此时我们只需要再handler中再重启runloop,便可以继续执行代码,通过观察runloop源码可以看出 这样的操作是在之前已经中断但是还没结束的runloop中开启一个新的runloop,他依然可以接受各种事件,比如交互事件等,前提是每个model都要开启,因为不同操作是发生在不同阶段的。但是之前runloop中的内容处于不可控状态,且之前的东西被永远的留在内存中,不可恢复,所以在做完相关操作后要立即结束App,避免其他异常情况,这种做法类似于一种安全模式,在安全模式中处理相关的东西。
函数调用:
void continueAfterCrash()
{
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFArrayRef allModes = CFRunLoopCopyAllModes(runLoop);
for (NSString *mode in (__bridge NSArray *)allModes)
{
CFRunLoopRunInMode((CFStringRef)mode, 1.0e10, false);
}
}
在新的runloop中我们做一些操作后再调用abort退出App,比如弹出友好提示之类的操作,告知用户app即将退出,但是该操作存在风险,需要注意以下情况
大概这就是所有Crash防护的流程,通过两篇文章讲解,希望大家对iOS系统的Crash流程能有些许的了解,并没有贴太多的源码,其实还是解耦度不够,思路有了代码就很简单了。
全民K歌国际版招聘Android/iOS客户端开发,点击左下方“查看原文”投递简历~
也可将简历发送至邮箱:tmezp@tencent.com