继续创作,加快生长!这是我参加「日新计划 10 月更文挑战」的第2天,点击检查活动详情

1. 前语: 如何把App的溃散率降到0.1%以下?

  • 溃散无疑是咱们在iOS开发工作中要面临的一个问题, 开发调试阶段的溃散往往能够经过断点排查处理; 线上的溃散往往让人手足无措, 需要结合Bugly等东西上传符号表, 抽丝剥茧的寻找原因同时处理.
  • 关于溃散率, 0.1%往往是很多公司的硬性要求合格线, 在到达0.1%溃散率的过程中, 咱们作为一线iOS开发者, 能够做些什么呢? 下面的思路和做法抛砖引玉, 欢迎大家在评论区交流探讨:)
  • 无痕植入的思路: AOP(面向切面编程)的思想. 根据OC的runtime运行时特性, 打点, 主动在App运行时实时捕获导致App溃散的因子, 然后经过针对性的的办法去应对因子, 做防崩处理.

2. 常见的8大溃散发生原因

  1. unrecognized selector形成的溃散: 没有找到对应的办法挑选器.
  2. KVO 形成的溃散: KVO的被调查者在dealloc时依然注册着KVO导致的溃散, 重复增加调查者或重复移除调查者.
  3. NSNotification 形成的溃散: 当一个方针增加了Notification之后, 在dealloc的时候, 依然持有Notification.
  4. NSTimer 形成的溃散: 需要在适宜的时机invalidate定时器, 不然就会由于定时器的timer强引用target导致target不被开释, 形成内存走漏.
  5. 容器类型越界形成的溃散: Array越界、Dictionary插入nil
  6. 非主线程改写UI形成的溃散: 在子线程改写UI会导致App溃散.
  7. 野指针形成的溃散: 访问了野指针, 方针已经被开释.
  8. 第三方协作时发生的溃散: 三方只供给了根据.a静态库的SDK文件, 三方更新后发生了溃散

3. 常见的8大溃散处理思路

3.1 unrecognized selector形成的溃散处理

  • 选用阻拦调用的办法, 在找不到调用的办法之后, App溃散之前, 咱们有时机经过重写NSObject的四个音讯转发办法来做防溃散处理.
+ (BOOL)resolveClassMethod:(SEL)sel; // 动态在办法决议机制, 决议类办法
+ (BOOL)resolveInstanceMethod:(SEL)sel; // 动态的方针办法决议, 决议方针办法
// 后两个办法需要转发到其他的类处理
- (id)forwardingTargetForSelector:(SEL)aSelector; // 转发给其它的一个方针去处理
- (void)forwardInvocation:(NSInvocation *)anInvocation; // 灵敏地将方针函数以其他方式执行
  • 阻拦调用的整个流程即OC的音讯转发机制. runtime供给了3种办法去补救:
  1. 调用resolveInstanceMethod给个时机让类增加这个函数完成.
    • 需要在类的自身动态地增加它不存在的办法, 这些办法关于该类是冗余的.
  1. 调用forwardingTargetForSelector让其他方针去执行这个函数.
    • 能够经过NSInvocation的方式将音讯转发给多个方针, 可是开支比较大,
    • 需要创建新的NSInvocation方针, 并且forwardInvocation的函数经常被使用者调用来做音讯的转发挑选机制, 不适合屡次重写.
  1. 调用forwardingInvocation(函数执行器)灵敏地将方针函数以其他方式执行.
    • 能够将音讯转发给一个同一方针, 开支较小, 并且被重写的概率较低, 推荐在这重写.
  • 假如都不可, 体系才会调用doesNotRecognizeSelector抛出异常.

  • 重写NSObjectforwardingTargetForSelector详细步骤:

  1. 为类动态地重建一个桩类.
  2. 动态为桩类增加对应的Selector, 用一个通用的返回0的函数来完成该SELIMP.
  3. 将音讯直接转发到这个桩类方针上.
- (id)jh_forwardingTargetForSelector:(SEL)aSelector {
    if (class_respondsToSelector([self class], @selector(forwardInvocation:))) {
        IMP impOfNSObject = class_getMethodImplementation([NSObject class], @selector(forwardInvocation:));
        IMP imp = class_getMethodImplementation([self class], @selector(forwardInvocation:));
        if (imp != impOfNSObject) {
            NSLog(@"class has implemented invocation");
            return nil;
        }
    }
    JHUnrecognizedSelectorSolveObject *solveObject = [JHUnrecoginzedSelectorSolveObject new];
    solveObject.objc = self;
    return solveObject;
}
  • ps: 假如方针的类自身重写了forwardInvocation办法的话, 就不应该对forwardingTargetForSelector进行重写了, 不然会影响到该类型的方针原本的音讯转发流程.

3.2 KVO 形成的溃散处理

  • 发生原因首要有2种
  1. KVO的被调查者dealloc时依然注册着KVO导致的溃散.
  2. 增加KVO重复增加调查者或重复移除调查者导致的溃散.

iOS老司机带你一起把App的崩溃率降到0.1%以下

  • 如上图所示: 一个被调查的方针有多个调查者, 每个调查者又有多个keyPath,

  • 假如调查者和keyPath的数量一多, 很简单不清楚被调查的方针整个KVO联系,

  • 导致被调查者在dealloc的时候, 依然残存着一些联系没有被注销,

  • 同时还会导致KVO注册者和移除调查者不匹配的状况发生,

  • 尤其是多线程环境下, 导致KVO重复增加调查者或者重复移除调查者的状况, 这种类似的状况比较难排查.

  • 能够这样管理混乱的KVO联系:

  • 让调查者方针持有一个KVO的delegate, 一切和KVO相关的操作均经过delegate来进行管理,

  • delegate经过树立一张Map表来维护KVO的整个联系, 如下图:

iOS老司机带你一起把App的崩溃率降到0.1%以下

  • 这样做的优点如下:
  1. 假如出现KVO重复增加或移除调查者(KVO注册者不匹配)的状况, delegate能够直接阻挠这些异常操作.
  2. 被调查方针dealloc之前, 能够经过delegate主动将与自己有关的KVO联系都注销掉, 避免了KVO的被调查者dealloc时依然注册着KVO导致的溃散.

3.3 NSNotification形成的溃散处理

  • iOS9之前, 当一个方针增加了Notification之后, 假如dealloc的时候, 依然持有Notification, 就会出现NSNotification类型的溃散.
  • iOS9之后苹果专门针对这种状况做了处理, 所以在iOS9之后, 即使开发者没有移除Observer, Notification溃散也不会再发生了.
  • 针对iOS9之前的用户, 防止NSNotification溃散的思路是:
  • 使用method swizzling hook NSObjectdealloc办法,
  • 在方针真正dealloc之前先调用一下[[NSNotificationCenter defaultCenter] removeObserve:self].

3.4 NSTimer内存走漏形成的溃散处理

  • 发生原因: Runloop -> NSTimer –> <- – 方针 <-VC
  • 这就导致了内存走漏
  • 处理办法如下:
  • NSTimer和方针间增加一个中心方针, NSTimer强引用中心方针, 中心方针弱引用NSTimer、方针
    iOS老司机带你一起把App的崩溃率降到0.1%以下

3.5 容器类型越界形成的溃散处理

  • 针对NSArray、NSMutableArray、NSDictionary、NSMutableDictionary、NSCache的一些常用的, 可能会导致溃散的API进行根据runtime的method swizzling, 然后在swizzle的新办法中针对Debug环境和Release参加一些判空处理操作, 然后让这些API变得更难溃散.

3.6 子线程改写UI形成的溃散处理

  • 选用根据runtime的swizzleUIView类的改写UI办法
- (void)setNeedsLayout;
- (void)setNeedsDisplay;
- (void)setNeedsDisplayInRect:(CGRect)rect;
  • 在自定义的交流办法里, 调用上面几个办法时, 判别一下当前的线程, 假如不是主线程, 直接调用dispatch_async(dispatch_get_main_queue(),^{// 原代码});, 来将对应的改写UI操作转移到主线程来做, 也可计算错误信息Debug形式下给到提示.

3.7 野指针形成的溃散处理

  • 当Bugly计算到Exception Type:SIGSEGV, Exception Codes:SEGV_ACCERR时, 就代表发生了野指针访问.
  • 然而处理野指针形成的溃散是一件比较棘手的事, 首要是因为溃散信息很难供给精准的定位, 这就导致野指针溃散的场景不一定好复现.
  • XCode为了开发阶段调试时就发现野指针问题, 供给了Zombie机制, 能够在发生野指针时提示出现野指针的类, 然后处理了开发阶段出现野指针的问题.
  • 可是线上环境发生的野指针问题, 依旧很难定位到详细的发生野指针的代码. 所以专门针对野指针做一层防崩办法, 在生产环境中就显得很有必要. 常见的一个思路:
  • 在类init初始化的时候做一个符号, 在该类dealloc时再做一个符号. 经过2次的符号来判别是否存在野指针. 可是关于UIVIew、UIImageView这些常用的类来说, 屡次分配开释内存的CPU开支仍是很大的, 这仅仅一个思路.
  • 更推荐腾讯的MLeaksFinder.
  • MLeaksFinder的思路:
MLeaksFinder一开始从UIViewController下手,
当一个UIViewController被pop或dismiss后, 该UIViewController包括他的view及subviews将很亏被开释. 
所以, 咱们只需要在一个UIViewController被pop或dismiss一小段时刻后, 
看看这个UIViewController及它的view、subviews等是否还存在.
MLeaksFinder详细的办法是为积累NSObject增加一个办法 -(void)willDealloc, 该办法的作用是:
先用一个弱指针指向self, 并在一小段时刻后, 
经过这个弱指针调用 -(void)assertNotDealloc, 而 assertNotDealloc首要作用是直接调用中断语.
若果它没被开释(即发生了内存走漏), assertNotDealloc就会被调用中断语.
这样一来, 当一个UIViewController被pop或dismiss时, 咱们遍历UIViewController上一切的view, 顺次调用 willDealloc, 若一小段时刻(如2s)之后还没开释, 那么指向它的weak指针仍是存在的,
所以能够调用其tuntime绑定的办法 willDealloc 来提示野指针内存走漏.

3.8 跟第三方协作时发生的溃散处理

  • 当公司跟第三方公司协作时, 第三方公司只供给了一个.a的SDK,
  • 之前的版别能够安稳运行, 更新了第三方的SDK相关文件后却发生了线上的溃散.
  • 这种状况一般来说一旦出现就会十分紧迫.
  • 一般的处理思路是直接跟第三方联系, 让他们再跑一下测验流程, 定位问题.
  • 自己公司能够经过Bugly上收集到的溃散信息, 上传符号表, 定位到溃散的堆栈调用信息.
  • 联合排查, 假如线上版别已经发布, 溃散又比较紧迫, 短时刻内三方也排查不出问题.
  • 这时能够经过Git分支的Tag, 回退到安稳版别, 紧迫更新一个版别, 避免线上溃散.
  • 待三方公司排查出问题后, 更新三方SDK相关文件, 再发一个bugFix版别.

发文不易, 喜爱点赞的人更有好运气 :), 定期更新+关注不迷路~

ps:欢迎参加笔者18年树立的研究iOS审阅及前沿技术的三千人扣群:662339934,坑位有限,备注“网友”可被群管经过~