iOS崩溃保护方案

前言

软件在运行时遇到不能理解的异常时会中断执行,iOS系统下给出的解决方案是强制结束应用并回到桌面,该方案不仅丢失内存信息,还会阻断用户操作流程,对业务影响极其严重,所以线上应用当极力避免此类情况,开发过程中应当对iOS系统下会引发崩溃的所有点牢记在心。对于无法预料的编码bug,可通过多种方式接管系统异常,以更温和的、低成本的弹窗或提醒等方案保证用户的其它正常操作。等后续通过排查线上日志定位异常后,再进行修改。

系统崩溃事件

  • 方法未找到(类或者实例执行方法出错)
  • 空值nil (数组插入nil,setObject值为nil)
  • 下标越界(数组操作,字符串操作)
  • 野指针 (assign修饰非基本数据,和CF框架交互释放错误)
  • KVO观察者未释放 (持有和释放未一一对应)
  • NAN错误
  • 线程异常 (子线程操作UI)
  • 内存暴涨,超出系统阈值 (内存泄漏,递归调用等)

对应接管方案

1. iOS系统的方法查找流程

消息转发流程图.png

可以看到消息转发阶段有三个接管时机,具体该在哪个时机接管,各有优势,不再赘述。
此类异常的接管可通过重写消息转发方法来实现:

  • NSObject下增加分类,用于全局替换
  • 新增方法 swizze_xxx 与系统转发方法 xxx 方法交换 (xxx 为消息转发三个时机中的方法)
  • 在新方法中跳过崩溃后,上报异常到服务器。

2.空值&下标越界

空值

先补充下各种空值的意义:

  • nil
    nil具体指的是oc下实例的空指针,nil 调用方法并不会发生异常,但因为nil在容器类中有特殊含义(NSArray,NSDictionary中nil代表结束位),所以使用 addObject 或setObject时 参数为nil时会报被系统当做严重异常。
  • Nil
    类对象的空指针,类对象在OC中是以单例存在的,所以基本不会遇到,无需考虑。
  • Null
    C类型的空指针,暂不考虑
  • NSNull
    OC下的标准空值类型,常见的场景就是与服务器数据交互中,模型中对象类型的属性如果遇到空值就会被重置成NSNull类型,此时如果不注意会出现很多 unrecognized selector 的崩溃,因为对象类型已经变了。

空值涉及的类有很多,主要包含各种可变容器的空值插入,解决方案为将所有的插入空值会引起崩溃的方法全部替换为新方法。

//NSArray
- (void) addObject:(id)anObject;
- (void) insertObject:(id)anObject atIndex:(NSUInteger)index;
- (void) replaceObjectAtIndex:(NSUInteger)index withObject:(id)anObject;
- (void) setObject:(id)object atIndexedSubscript:(NSUInteger)index;
//NSDictionary
+ (instancetype) dictionaryWithObject:(id)object forKey:(id)key;
- (void) setObject:(id)anObject forKey:(id)aKey;
- (void) setObject:(id)object forKeyedSubscript:(id<NSCopying>)key;
//NSSet
+ (instancetype)setWithObject:(id)object;
- (void) addObject:(id)object;
- (void) removeObject:(id)object;
  • 新建类并重写 + load 方法。 (+load 方法优先于main函数且只执行一次),或者可以保证整个App生命周期只调用一次即可。
  • 新增对应hook方法。
  • 用runtime 的method swizzing API 进行方法交换。
    例如以下:
swizzleInstanceMethod([NSArray class], @selector(addObject:), @selector(hookAddObject:));

- (void) hookAddObject:(id)objc {
    if (objc) {
        [self hookAddObject];
    }
    handleCrashException(@"hookAddObject object is nil");
}

#pragma mark - 方法交换
- (void)swizzleMethod:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector{
    Class class = [self class];
    
    Method originalMethod = class_getInstanceMethod(class, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
    
    BOOL didAddMethod = class_addMethod(class,
                                        originalSelector,
                                        method_getImplementation(swizzledMethod),
                                        method_getTypeEncoding(swizzledMethod));
    
    if (didAddMethod) {
        class_replaceMethod(class,
                            swizzledSelector,
                            method_getImplementation(originalMethod),
                            method_getTypeEncoding(originalMethod));
    } else {
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}

下标越界

下标越界涉及众多类和API,涉及数组操作,字符串操作,UITableViewCell的添加删除等等。
好在原理一致,替换对应的方法后在内部做判断即可。

3.野指针、僵尸对象

野指针是对象释放过程出现了错误,导致对象失去了控制权,此时的对象就成了僵尸对象,其对应的指针便称作野指针,当继续操作该指针时,其所指向的内存地址可能已经被分配给了其它对象,所以在此时时会产生很多意想不到的错误。

一个典型的引发野指针的场景就是: 非基本类型的对象用 assign修饰 (常见为delegate属性修饰),因为assign属性的对象不会随着对象释放而释放。常见的判断野指针的方式为:查看日志中是否频繁出现某个类的实例调用了一堆不相关的方法。


野指针可能出现的问题

解决方案:
在ARC模式下,野指针出现的场景已经很少了,非基本类型的对象用assign修饰在编译器就会报错,所以基本只要注意 id类型的属性不要用 assign修饰即可。
野指针定位很困难,因为僵尸对象何时被分配怎样被分配是很随机的,导致野指针的崩溃完全成了人品问题。
网上比较高阶的解决方案主要是在开发阶段将野指针暴露出来,尽力提高野指针复现场景,其原理为:hook dealloc 方法,阻止对象正常释放(内存使用会飙升),同时将对应内存地址重写为0x55(不可操作),当野指针再次操作僵尸对象时,因为对应内存已不可操作就会发生崩溃,再通过一些更底层的函数,获取崩溃时调用的堆栈信息,定位到野指针调用的类名和方法,继而通过改代码解决问题。
过程很复杂,探究的精神很是佩服,但即便这样仍旧不能完全解决问题,首先工作方式会有一些限制,比如内存飙升的问题,解决方式为当占用快满时主动释放掉一些内存,然后让系统重新分配,但属于该片内存的野指针就会被漏诊,所以内存越大的设备加操作更可能出现的业务场景,才能有更多的几率找到野指针。

4.KVO、NSNotificationCenter

  1. KVO原理

2.如何引发?为什么会系统被认作是严重错误?
KVO、通知,造成崩溃的一个共性原因就是持有对象未释放,但看起来这就是普通的内存泄漏问题,为什么会被系统认为是严重错误,为什么循环引用就不认为是严重错误呢?
3.如何解决
先明确几个概念,用以下代码举例:

Person *per = [[Person alloc] init];
[per addObserver:self
       forKeyPath:@"name"
          options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
          context:nil];
per.name = @"zhangsan";

1.观察者observer,也就是其中的self,当变化发生时负责接收回调事件。
2.被监听者per,调用addObserver后自身遍和观察者产生了关联,当观察者dealloc时,如果 per 没有调用过 removeObserver ,就会发生崩溃。
3.键值keypath,此处为number属性,监听后再修改per.name就会通知到观察者self,当keypath已经被添加过,或者removeObserverFromKeypath时移除了多次都会发生崩溃。

这里吐槽下KVO的设计,要是局部变量想监听下属性变化必须得改成全局变量,要不然都没法在dealloc中写移除观察者的代码,这不是很坑嘛,其次一个键值被监听后再次添加观察者会崩溃,移除次数不对也会崩溃。那你好歹告知下这个键有没有被监听才是啊,提供个相关属性多好(例如isObserved?),现在好了,搞几个大坑出来,要想正常使用就必须封装下搞两个容器看对象的某个keypath是不是已经被监听过,观察者释放还后要看被监听者是不是被移除了....

推荐个靠谱封装 https://github.com/facebook/KVOController

主要原理为:

  • 建立两个容器,一个放置keypath对象,用于判断其添加和移除次数是不是一一对应。另外一个放置被监听对象,用于观察者对象释放时,移除对应的监听对象。
  • 新建类xxx创建实例作为观察者的运行时的associated对象,重写其dealloc方法,这样当观察者对象时,就会执行dealloc,监听对象的移除操作放到此处即可。

完美。

通知崩溃在iOS9以上已经没有了,也不过多讨论,再说通知本就已经是单例在全局操作观察者,比KVO更好处理崩溃问题,为毛还要开发者手动移除呢,可能我们作为开发者尤其重视崩溃而苹果作为语言设计者根本就不在乎吧。

JJException框架内部对于KVO保护的原理颇为复杂,为了实现无感知保护,hook了系统KVO的三个方法

+ (void)jj_swizzleKVOCrash{
    swizzleInstanceMethod([self class], @selector(addObserver:forKeyPath:options:context:), @selector(hookAddObserver:forKeyPath:options:context:));
    swizzleInstanceMethod([self class], @selector(removeObserver:forKeyPath:), @selector(hookRemoveObserver:forKeyPath:));
    swizzleInstanceMethod([self class], @selector(removeObserver:forKeyPath:context:), @selector(hookRemoveObserver:forKeyPath:context:));
}

内部原理和KVOController框架实现基本一致,但有个弊端是对某些使用系统KVO的第三方有影响,因为内部hook了dealloc方法主动执行了removeObserver操作,无需外部再调用,但第三方的remove操作无法修改,App运行时总是停在@try内部,日常调试很受影响。


截屏2020-02-12下午4.00.42.png

5.NaN错误

这个没有一劳永逸的方案,特别注意算数计算中 除数不能为0
另外可以通过isnan(x)函数来判断。

6.线程异常

  • 子线程操作UI

  • 线程锁问题

7.内存泄漏

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

推荐阅读更多精彩内容

  • 凡事预则立,不预则废 ----郑开首马感悟 “凡事预则立,不预则废”,中学就学过的道理,长大后就彻底还给...
    光卿阅读 835评论 0 3
  • 情商课之(一00七) 最好的财富! 良好的习惯是一笔财富,这样的财富更有价值。 女...
    开心佛阅读 748评论 0 0
  • 作为一个奋斗着的产品er,“从小到大”都被“去听用户的声音”的观点浸淫着,深以为然。 所以在周一,带着对自己做的数...
    wind迪阅读 321评论 0 1