iOS老司机带你一起把App的崩溃率降到0.1%以下
持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第2天,点击查看活动详情
1. 前言: 如何把App的崩溃率降到0.1%以下?
- 崩溃无疑是我们在iOS开发工作中要面对的一个问题, 开发调试阶段的崩溃往往可以通过断点排查处理; 线上的崩溃往往让人手足无措, 需要结合Bugly等工具上传符号表, 抽丝剥茧的寻找原因一并解决.
- 对于崩溃率, 0.1%往往是很多公司的硬性要求合格线, 在达到0.1%崩溃率的过程中, 我们作为一线iOS开发者, 可以做些什么呢? 下面的思路和做法抛砖引玉, 欢迎大家在评论区交流探讨:)
- 无痕植入的思路: AOP(面向切面编程)的思想. 基于OC的runtime运行时特性, 打点, 自动在App运行时实时捕获导致App崩溃的因子, 然后通过针对性的的方法去应对因子, 做防崩处理.
2. 常见的8大崩溃产生原因
unrecognized selector
造成的崩溃: 没有找到对应的方法选择器.- KVO 造成的崩溃: KVO的被观察者在
dealloc
时仍然注册着KVO导致的崩溃, 重复添加观察者或重复移除观察者. - NSNotification 造成的崩溃: 当一个对象添加了
Notification
之后, 在dealloc
的时候, 仍然持有Notification
. - NSTimer 造成的崩溃: 需要在合适的时机
invalidate
定时器, 否则就会由于定时器的timer
强引用target
导致target
不被释放, 造成内存泄漏. - 容器类型越界造成的崩溃:
Array越界、Dictionary插入nil
- 非主线程刷新UI造成的崩溃: 在子线程刷新UI会导致App崩溃.
- 野指针造成的崩溃: 访问了野指针, 对象已经被释放.
- 跟第三方合作时产生的崩溃: 三方只提供了基于.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给个机会让类添加这个函数实现.
- - 需要在类的本身动态地添加它不存在的方法, 这些方法对于该类是冗余的.
2. 调用
forwardingTargetForSelector让别的对象去执行这个函数.
- - 可以通过
NSInvocation的形式将消息转发给多个对象, 但是开销比较大,
- - 需要创建新的
NSInvocation对象, 并且
forwardInvocation的函数经常被使用者调用来做**消息的转发选择机制**, 不适合多次重写.
3. 调用
forwardingInvocation(函数执行器)灵活地将目标函数以其他形式执行.
- - 可以将消息转发给一个同一对象, 开销较小, 并且被重写的概率较低, **推荐在这重写.**
- 如果都不行, 系统才会调用
doesNotRecognizeSelector`抛出异常.
- 重写
NSObject
的forwardingTargetForSelector
具体步骤: - 为类动态地重建一个桩类.
- 动态为桩类添加对应的
Selector
, 用一个通用的返回0的函数来实现该SEL
的IMP
. - 将消息直接转发到这个桩类对象上. ```
-
(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种
- KVO的被观察者
dealloc
时仍然注册着KVO导致的崩溃. - 添加KVO重复添加观察者或重复移除观察者导致的崩溃.
- 如上图所示: 一个被观察的对象有多个观察者, 每个观察者又有多个
keyPath
, - 如果观察者和
keyPath
的数量一多, 很容易不清楚被观察的对象整个KVO关系, - 导致被观察者在
dealloc
的时候, 仍然残存着一些关系没有被注销, - 同时还会导致KVO注册者和移除观察者不匹配的情况发生,
-
尤其是多线程环境下, 导致KVO重复添加观察者或者重复移除观察者的情况, 这种类似的情况比较难排查.
-
可以这样管理混乱的KVO关系:
- 让观察者对象持有一个KVO的
delegate
, 所有和KVO相关的操作均通过delegate
来进行管理, delegate
通过建立一张Map表来维护KVO的整个关系, 如下图:
- 这样做的好处如下:
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 NSObject
的dealloc
方法, - 在对象真正
dealloc
之前先调用一下[[NSNotificationCenter defaultCenter] removeObserve:self]
.
3.4 NSTimer内存泄漏造成的崩溃处理
- 产生原因: Runloop -> NSTimer --> <- - 对象 <-VC
- 这就导致了内存泄漏
- 处理方法如下:
NSTimer
和对象间添加一个中间对象,NSTimer
强引用中间对象, 中间对象弱引用NSTimer
、对象
3.5 容器类型越界造成的崩溃处理
- 针对
NSArray、NSMutableArray、NSDictionary、NSMutableDictionary、NSCache
的一些常用的, 可能会导致崩溃的API进行基于runtime的method swizzling
, 然后在swizzle的新方法中针对Debug环境和Release加入一些判空处理操作, 从而让这些API变得更难崩溃.
3.6 子线程刷新UI造成的崩溃处理
- 采用基于runtime的
swizzle
UIView类的刷新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,坑位有限,备注“掘金网友”可被群管通过~
- iOS老司机聊聊实际项目开发中的<<人月神话>>
- iOS老司机可落地在中大型iOS项目中的5大接地气设计模式合集
- iOS老司机的跨端跨平台Hybrid开发Tips
- iOS老司机的2022年回顾, 聊聊寒冬下的实用<<谈判力>>
- iOS老司机可落地的中大型iOS项目中的设计模式优化Tips_桥接模式
- iOS老司机的多线程PThread学习分享
- iOS老司机整理, iOSer必会的经典算法_2
- iOS老司机的<<蓝海转型>>读书分享
- iOS老司机的<<程序员的自我修养:链接、装载与库>>读书分享
- iOS老司机的接地气算法Tips
- iOS老司机的RunLoop原理探究及实用Tips
- iOS老司机整理, iOSer必会的经典算法_1
- iOS老司机的App启动优化Tips, 让启动速度提升10%
- iOS老司机的网络相关Tips
- 恋上数据结构与算法
- iOS老司机带你一起把App的崩溃率降到0.1%以下
- 探究Swift的String底层实现
- iOS老司机万字整理, 可能是最全的Swift Tips
- iOS老司机可落地的中大型iOS项目中的设计模式优化Tips
- 聊一聊Swift中的闭包