iOS Crash防护你看这个就够了 - 下篇

0x1: Previously

上篇 中讲到了Crash处理流程分为四个环节,也分析了Crash防护的方法,本章来讲下其余三个环节。

0x2: Crash的拦截

所有的未被防护住的Crash最终会走到这里,在这里我们必须要保证拦截的 全面性稳定性尽可能多的拦截到所有类型的异常,同时拦截逻辑本身不能产生异常。那么我们需要通过以下几个方面去考虑。

[图片上传失败...(image-262507-1620274685799)]

I: Crash类型

和多数操作系统一样,iOS的异常也基本分为 用户层 系统底层 信号 这三个类别,接下来我们看下每种异常都做了哪些事情

image
  • Mach Exception

    Mach异常,分为两种情况,第一种是本身就是硬件层面或者系统层面的异常,这个大家比较好理解,因为mach是微内核,所以底层的内核级别的包括硬件的异常都是mach异常。另一种是iOS系统独有的逻辑或者说是苹果独有的。就是用户层面的异常也都首先下沉到mach层再发出来,也等于是另一种意义上的mach异常。苹果官方文档上写的是为了统一机制才做了这样的处理,但是没有说具体原因.他的触发流程大概为下图

    image

    然后我去看Runtime的源码进一步证明了这个说法,runloop中大量使用这种方式监听mach异常消息,一旦Crash随时准备打破循环,因为系统也需要监听crash,统一出口将对监听来说对系统将变得非常方便。

    image

    根据代码上下文可以判断出,苹果会监听统一的异常端口,在出现异常后进行相应的操作,也印证了我当时的推断。

  • Exception

    很常见的异常,触发流程大概为

    image

  • signal

    signal的产生流程大概分为几种情况

    • 由于 MachExcption 转换而成的signal

    • 由于Exception而发出的abort信号

    • 用户自定义的信号

    但是需要注意一点:<font color=red size=3 face="黑体">收到signal不一定会Crash,但是Crash一定会有Signal发出</font>

II: Crash传递流程

上面分析了每种Crash的类型,那么这三种类型的Crash是如何在App生命周期中传递的呢?他们又是如何相互转化以及相互之间有什么关系呢?

image

帮大家提取下上图中的几个关键信息

  • 1:Exception 最终会转化为Mach Exception

  • 2:通过Mach端口拦截的较为全面

  • 3:如果发生了exception那么就不会抛出对应的signal只能抛出abort();

  • 4:通过捕获signal是无法拦截到exception。

III: 拦截的选择

通过上面的分析大家一定会说通过Mach端口的拦截更加全面,毕竟苹果自己也在用。但是在实际使用中有一个问题,mach会拦截所有的异常以及信号量,也就是随便一个操作(比如发一个自定义signal等)可能都被mach捕获,那么如果在其捕获回调中再进行捕获就会很容易发生死锁,而且容易和系统的处理产生冲突。当时看了PLCrash的文档,也看到了开发者写的一句话:

image

这样说明了大家确实被坑过。

那接下来只剩signal 和 exception,其实细心的同学早已发现这两个的优缺点是一个互补的状态

  • singal能捕获除Exception之外的所有异常。

  • exception只能获取应用层的异常而对信号量无法处理

那么最终的方式采用 singnal + exception的方式进行捕获,最终的流程为:

image

IV: 坑点

上面的流程图可以看出在每一个CustomHandle之前都会有一个PreviousHandle,其实是因为在iOS系统中只能存在一个customHandel,如果你的项目中接入了或者准备接入多个 Crash 防护相关的SDK(虽然不建议这么做),那么多个Handle之间一定会产生冲突,导致堆栈不明确,或者丢失。所以在注册我们的handle前先将之前的handle指针保存下来,等我们的handle处理完后在通过函数指针调用回去,这样就能保证每一个handle都能被正常调用。

  • exception:通过NSGetUncaughtExceptionHandler获得之前handle指针,之后再通过NSSetUncaughtExceptionHandler(oldHandler);调用回去。

  • signal: 使用sigaction函数获得之前的handle指针。

0x3: 堆栈获取

因为苹果使用了(Address Space Layout Randomization ) 地址空间配置随机加载技术,所以线上堆栈必须要通过符号表堆栈还原进行解读,不然的话就是内存地址。所以当我们使用NSThread的相关函数在Debug下虽然能看到可读性行的堆栈,但是在线上包上并不可取,那我们要怎么获取堆栈呢?先来看下符号表的构造:

image

之前拿到这样的符号表,我们通常手动还原,找一个相同系统的真机,找到对应库的基地址按照符号表上函数的偏移量进行计算(通过LLDB的相关函数)

image

通过看Mach-o相关接口可以找到相关函数进行端内符号表还原,大致流程为:

  • 获取函数地址:

    • 遍历Mach-o中的所有image

    • 获取每个image的基地址

    • 通过堆栈偏移地址获取栈帧函数地址

  • 将函数地址翻译成函数名

    • 找到对应Image的symple table段的nlist_64结构体

    • 通过nlist_64.n_un.n_strx获取函数对应的字符串

最终的效果:

image

0x4: Crash后续

通常在AppCrash后会在handle中做些上报操作.

image

但是这样做有两个问题:

  • 苹果不推荐在Handle中做太多操作,而且数据上报等网络请求属于耗时操作,有可能没有完成App就被杀死。

  • App直接闪退,体验不好

通过查看runloop源码可以看出,在Crash发生后当前runloop中断

<font color='red'> 注意:runloop本次循环还在继续,但是循环已经被打破,本次循环结束后app才退出</font> 既下图的retVal被置为NO


image

iOS Crash发生后 runloop中的do-while循环的条件会被置为 NO,然后Handler函数走完之后当前循环后直接结束,不会在进行下一次循环了,此时我们只需要再handler中再重启runloop,便可以继续执行代码,通过观察runloop源码可以看出 这样的操作是在之前已经中断但是还没结束的runloop中开启一个新的runloop,他依然可以接受各种事件,比如交互事件等,前提是每个model都要开启,因为不同操作是发生在不同阶段的。 但是之前runloop中的内容处于不可控状态,且之前的东西被永远的留在内存中,不可恢复,所以在做完相关操作后要立即结束App,避免其他异常情况,这种做法类似于一种安全模式,在安全模式中处理相关的东西。

image

函数调用:

void continueAfterCrash()
{
    CFRunLoopRef runLoop = CFRunLoopGetCurrent();
    CFArrayRef allModes = CFRunLoopCopyAllModes(runLoop);

    for (NSString *mode in (__bridge NSArray *)allModes)
    {
        CFRunLoopRunInMode((CFStringRef)mode, 1.0e10, false);
    }
}
image

在新的runloop中我们做一些操作后再调用abort退出App,比如弹出友好提示之类的操作,告知用户app即将退出,但是该操作存在风险,需要注意以下情况

  • 新开runloop后之前的runloop内容便会永远的留在内存中变成不可控的状态如果一旦被访问可能会有异常,所以在做完我们必要的操作后要及时结束App。

  • 安全模式必须保证稳定,在新runloop中执行的上报、弹窗或者其他逻辑必须要使用系统原生的API,不能依赖任何第三方。

  • 尽量不要做太多的操作,及时结束。

0x5: 参考资料

0x6: 最后

大概这就是所有Crash防护的流程,通过两篇文章讲解,希望大家对iOS系统的Crash流程能有些许的了解,并没有贴太多的源码,其实还是解耦度不够,思路有了代码就很简单了。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 144,247评论 1 305
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 61,830评论 1 258
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 95,531评论 0 214
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 41,345评论 0 183
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 49,160评论 1 260
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 38,936评论 1 178
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 30,538评论 2 275
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,291评论 0 168
  • 想象着我的养父在大火中拼命挣扎,窒息,最后皮肤化为焦炭。我心中就已经是抑制不住地欢快,这就叫做以其人之道,还治其人...
    爱写小说的胖达阅读 29,162评论 6 237
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 32,654评论 0 214
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,401评论 2 217
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 30,747评论 1 232
  • 白月光回国,霸总把我这个替身辞退。还一脸阴沉的警告我。[不要出现在思思面前, 不然我有一百种方法让你生不如死。]我...
    爱写小说的胖达阅读 24,297评论 1 33
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,206评论 2 213
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 31,670评论 3 213
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,661评论 0 9
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,089评论 0 169
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 33,677评论 2 233
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 33,819评论 2 237

推荐阅读更多精彩内容