继续创作,加快生长!这是我参加「日新计划 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种办法去补救:
- 调用
resolveInstanceMethod
给个时机让类增加这个函数完成.
-
- 需要在类的自身动态地增加它不存在的办法, 这些办法关于该类是冗余的.
- 调用
forwardingTargetForSelector
让其他方针去执行这个函数.
-
- 能够经过
NSInvocation
的方式将音讯转发给多个方针, 可是开支比较大,
- 能够经过
-
- 需要创建新的
NSInvocation
方针, 并且forwardInvocation
的函数经常被使用者调用来做音讯的转发挑选机制, 不适合屡次重写.
- 需要创建新的
- 调用
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的整个联系, 如下图:
- 这样做的优点如下:
- 假如出现KVO重复增加或移除调查者(KVO注册者不匹配)的状况,
delegate
能够直接阻挠这些异常操作. - 被调查方针
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,坑位有限,备注“网友”可被群管经过~