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

0x1 为什么要做Crash防护

在产品开发过程中Crash率是一个很重要的指标,也是一个团队中几乎所有的部门都应该关注或者去参与提升的一个指标,他不仅代表着整个产品的质量,也是一个团队整体技术能力的体现。更低的Crash率不但能让产品获得更好的用户口碑,在整个流程中也能让团队成员获得更多的成长,加深对iOS系统底层的理解,为今后的开发带了更大的帮助。

image

0x2 为什么要写这篇文章

起因也是因为自己的项目踩了FB的SDK的坑:2020.7.10,FB后台下发数据错误,导致大量使用FB SDK的App发生启动Crash,影响用户之多,范围之大,再加上当时包括我们的大部分App也缺乏相关的防护或者是容错处理,Crash率瞬间飙升,重新发版又要走发布流程,只能依赖FB后台的修复,当时束手无策十分被动,所以决定自己做一套较为完整的Crash防护体系,来避免这样的场景再次发生。第二个目的就是,发生问题后我也第一之间查阅了网上的一些资料和其他团队的做法,发现大家的方式各有千秋,方法不同,效果不同,所以我也决定把市面上能找到的好的思路和方法再结合自己的一些想法和经验记录下来。最后也是因为知识是要沉淀、积累和分享的,也算是巩固和加深自己的理解吧。

0x3 怎么做

其实当时Crash的场景很简单,本来一个Dictionary参数FB后台却下发了个String类型的数据,这样一来解析时候必然会Crash,解决的话其实只要做一层参数安全校验即可。

image
image

但是这么简单的问题,大部分App都没处理好,证明在流程上一定有大家注意不到的地方,暴露出来的只是冰山一角,我们机制一定存在着某种问题,或者存在可以优化的地方。

要想避免这种情况,就要先梳理出处理Crash的流程:

I:Crash处理流程

image

在iOS系统中基本可以总结出这四个步骤,

  • Crash防护 - 通过Hook等手段,对一些类似容器类进行入参校验等措施,来进来避免Crash的发生

  • Crash拦截 - 如果第一步防护失败,那么在Crash走到这一步就要进行拦截,要让我们发现异常

  • Crash上报 - 对防护的、捕获的Crash进行防护,生成有效的日志进行上报,尽可能的还原堆栈。

  • Crash后续流程 - Crash发生后如何做才能最大限度的保护用户体验,如何优雅的Crash

II:Crash防护

Crash防护方式主要分两种:针对非内存问题通常采用AOP方式,内存问题采用zombie对象的方式,

image

image

AOP:

iOS中AOP的相关知识网上线程的代码也很多,这里就不在赘述,但是在AOP这种频繁调用的场景中就需要注意的地方和坑点比较多。

  • AOP的影响范围问题:当时用了普通的方式对数组相关的方法进行了Hook,结果上线后发现大量的类似Crash。[UIKeyboardLayoutStar release]: message sent to deallocated instance UIKeyboardLayoutStar

    image

    在通过一些其他场景可以判断出是因为HookNSMutableArr的相关方法,导致系统类的调用受到了影响。

    通过Xcode调试发现,因为Hook的本质就是在原有的系统调用前插入一个用户自定义的函数进行方法交换,那么在某种极端情况下(比如多线程),传入该函数的变量被释放,这样一来再走到原本系统调用的时候正常释放时就会出现重复释放的情况。大概的流程为

    image

    该场景在测试过程中很难复现,但是一旦到了线上,用户量覆盖够大后该问题就会显现出来。解决方式很简单,Hook尽量在MRC下进行,使用autorelease pool进行包装。保证内部变量在当前的runloop结束时候进行释放。

  • AOP的性能问题:上面说了AOP的原理是会多一层方法调用,那么再结合iOS的方法转发流程可想而知,AOP必定会造成性能的损耗,而且在Crash防护场景下频繁调用,性能问题一定不能忽略。

    image

    通过上图看出,方法调用流程最终会返回出对应的IMP指针供外部调用,作为动态语言,OC无法确定开发者会再什么时候插入或者交换哪个函数,所以必须通过这一套流程进行类似校验的逻辑。

    使用过AOP的同学一定知道在AOP前会先做一层校验

    +(void)hookClass:(Class)classObject isClassMetohd:(BOOL)classMethod fromSelector:(SEL)fromSelector toSelector:(SEL)toSelector 
    {
        Class class = classObject;
        Method fromMethod = class_getInstanceMethod(class, fromSelector);
        Method toMethod = class_getInstanceMethod(class, toSelector);
        //  添加前进行检测
        if (classMethod) {
            class = object_getClass(classObject);
            fromMethod = class_getClassMethod(class, fromSelector);
            toMethod = class_getClassMethod(class, toSelector);
        }
        if(class_addMethod(class, fromSelector, method_getImplementation(toMethod), method_getTypeEncoding(toMethod))) {
            class_replaceMethod(class, toSelector, method_getImplementation(fromMethod), method_getTypeEncoding(fromMethod));
        } else {
            method_exchangeImplementations(fromMethod, toMethod);
        }
    }
    

    所以在方法我们在上面代码中的toSelector中 当我们需要调用回原方法时直接调用对应的函数指针即可

    image

    最终我对直接调用IMP的方法做了测试,分别是Demo中和App中的某一个场景,测试数据如下,对比结果还是较为明显。这也就是为什么Swift或者一些其他静态语言比OC快的原因。

    image

Zombie:

使用僵尸对象来解决内存问题一直是苹果主推的方式,Xcode也有相关设置,在Debug下打开相应开关,但是一旦把该功能放到线上做防护或监控就要考虑很多的问题。

  • zombie入口问题:换句话说就是在哪个地方生成zombie对象,看了一些相关的SDK都是采用Dealloc作为入口函数,不是不行,只是不是最优。原因有两点:

    • 1:苹果已经不建议在ARC下主动调用dealloc,目前只能采用performSelector或者其他动态调用的方式。

    • 2:容易漏掉 Objc_destructInstance,所有的成员变量、属性都会在这个函数中释放,如果漏掉这个函数就会生成一个并不干净的僵尸对象,内存占用过高,白白浪费内存空间。

    综上两点我最终选择在Free函数中生成僵尸对象

    image
  • zombie内存阈值问题:僵尸对象会占用内存空间,然而在线上环境操作内存一定要小心且一定要有一套完整的逻辑,当超过某一个内存阈值后需要及时清空僵尸对象。内存阈值的确定便成了关键,这里会遇到两个问题:

    • <font color='red'> 1:内存问题一定会和机型强相关,如何根据不同的机型调整不同的阈值? </font>

    • <font color='red'> 2:如何做到根据线上情况灵活动态调整?</font>

    我们的底线是在加入zombie后不能触发memorywarning,所以我先对大部分机型做了memorywarning阈值测试:

    image

    从上图可以看出当App占用内存达到总内存的 57%~69%时候会触发内存警告,而且由于iphone中有一部分内存是系统保留内存并不会给到开发者,所以我们可用的也就50%左右,我总结出如下公式:

    公式1:不能触发内存警告 Y = 0.5 * deviceMem – currentAppMem

    公式2: 僵尸对象的内存占用再大也不会超过App本身的内存 Y = min ( ( 0.5 * deviceMem – currentAppMem ) , currentAppMem)

    上面两个公式看似完美,但是还是有优化的地方,因为并不是APP中所有的变量都有可能成为僵尸对象,可能只是其中的某一部分需要被监控, 所以得到最终的内存阈值计算公式:

    <font color=red size=5 face="黑体">Y = min ( ( 0.5 * deviceMem – currentAppMem ) , currentAppMem / N )</font>

    因为app占用内存随时在变,所以可以加一个定时器每隔一定时间去更新该值。

    上面公式的 N 还有一个好处就是我们可以后台动态下发,根据线上内存引起Crash量,如果Crash量大,那可能就需要更大的内存阈值去保存僵尸对象,就可以把N调小,反正调大,这样就可以无视机型的差异根据Crash的情况进行远程配置。

    image

    通过如图的线上数据可以看出 随着N的减小,zombie的内存阈值在增加,但是并不会超过内存警告阈值,确保了内存健康。

    下图表示了不同的N值对应不同的捕获野指针问题的数量,各自App可以根据自己的业务情况进行调整。

    image
  • zombie更新策略问题:目前大家的做法都是在加入新的zombie对象时候检查是否超过阈值,达到阈值后删掉之前的zombie对象再加入新的对象,这样的清理逻辑是依赖于新zombie对象的加入,如果没有新对象的加入那么缓存空间也不会有变化,zombie空间一旦生成就无法删掉,无法做到缓存的自清理,等于App无故增大了内存占用。

    image

    同样借鉴LRU最近最久未使用的逻辑,每隔30s会检测下缓存情况,超过30s还未被使用的zombie对象将被删除,30s是一个经验值,通过大量测试发现,内存问题一般会发生在对象被销毁的30s内,超过30s再出现的概率及小。这样可以做到缓存自清理的逻辑。

    image

    通过Instrument测试发现该zombie逻辑并不会对App本身的内存造成太大的影响。

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

推荐阅读更多精彩内容