摘要:
货拉拉iOS用户端阅历了多年的迭代,作为近百万日活的App,Crash率阅历了从千分位到万分位,再到十万分位的降率进程。本篇文章将深入谈论iOS渠道下Crash办理的布景、收益,介绍Crash监控方案的优缺点,共享Crash办理的思路和阅历,剖析常见的Crash类型及其处理方案,并谈论iOS渠道下常用的Crash防护办法,终究总结长期Crash办理的阅历,旨在为前进中的开发者提供名贵的技能方案和阅历。
作者: Sherwin.Chen
一、布景
货拉拉iOS用户端作为一个拥有庞大用户群体的运用,日活量挨近百万,在Crash办理方面的投入和努力直接影响到用户满意度和事务安稳性。其App的Crash阅历了从千分位到万分位,再到十万分位的Crash率下降进程。在这期间,咱们上线了过flutter、mPaaS混合开发方案,也阅历过App主页大重构,遇到BaiduMapSDK大规模的溃散。经过3年左右时刻,咱们终于达到Crash率10万分位方针,这个进程中咱们积累了丰富的Crash办理阅历和技能方案。
Crash办理收益可包含:
- 进步用户体会:削减Crash可以下降用户的不满和丢失,进步用户满意度。削减引发的卸载行为。
- 维护品牌声誉:安稳的运用会增强品牌的诺言,吸引更多用户。
- 下降维护本钱:削减溃散导致的客服投诉和bug修正时刻。
- 进步开发功率:削减Crash可以削减开发人员的深重维护作业,使他们可以更专心于新功能和性能优化。
下表列举了咱们办理进程中几个重要的里程碑事情:
货拉拉iOS用户端App | Crash率 | App版别 | 时刻 |
---|---|---|---|
节点一(千分位): | 0.270% | 6.4.88 | 2020/11 |
节点二(万分位): | 0.030% | 6.5.77 | 2021/10 |
节点三(十万位分): | 0.008% | 6.8.8 | 2023/08 |
二、Crash监控方案
iOS Crash监控渠道是开发中非常重要的东西,它可以协助咱们及时发现和处理运用程序的溃散问题。在iOS渠道下,Crash监控方案有多种挑选,如KSCrash、PLCrashReporter、Firebase Crashlytics、友盟、Bugly等。在技能层面本文就不展开解释,如有爱好可阅读开源项目KSCrash:github.com/kstenerud/K…
在挑选监控方案时会考虑以下因素:
- 实时性:监控能否及时发现Crash。
- 安稳性:监控东西本身不该成为引发Crash的因素。
- 数据剖析:提供具体的Crash报告,有助于问题定位。
- 集成难度:方案是否简单集成到现有开发流程中。
- 本钱:方案是否符合预算。
咱们关于Crash监控方案,挑选阅历了友盟、Bugly以及现在所运用的自建HadesCrash。之所以挑选自建,有以下几个方面的思考:
1. 信息安全: 现在App的UV、PV、溃散等数据暴露给第三方Crash监控渠道,存在数据安全风险;
2. 数据才能: 打破数据壁垒,结合公司体系完善溃散办理流程,增加日报、周报、告警等才能;
3. 进步人效: 溃散自动分配,符号表自动上传,发布自动回滚,定位问题提效;
4. 生态闭环: 打通飞书账号、CI发布、用户反馈、qamp体系以及日志体系;
监控主面板如下:
本文就不过多介绍自建Crash渠道的相关技能,有爱好的可以了解:www.xuyanlan.com/2019/01/14/…
三、 Crash办理思路
在Crash办理进程中,关键是继续改善和优化。以下是一些阅历共享:
- 分级办理:将Crash依照严峻性分级,优先处理严峻溃散,逐步下降Crash率。
- 版别追寻:监控Crash与运用版别相关,保证新版别没有引进新的Crash。
- 定时剖析:定时剖析Crash数据,了解发生频率最高的Crash类型,以便有针对性地处理。
- 团队协作:Crash办理需求跨部门协作,开发、测验、运维等团队需求密切协作。
依据这几年的阅历和实践,总结一个有用的方案,中心是”共建共治”:
- 树立Crash攻坚专项小组(虚拟团队),拉上相关人:本组成员、其它事务成员、二方组件维护者
- 每个迭代版别定时搜集Crash数据,记录在使命办理文档中,可包含: 使命称号、Crash链接、累计发生次数、分派人、事务分类、进展….etc
- 每两周在攻坚专项小组花30分钟(Crash双周会),过一下近期Crash使命进展和办理状况
- 每周周会上可共享Crash办理阅历和进程,让更多搭档了解此问题。以及编写代码时防止入坑,写出更优异的代码。
- 每个季度总结当时Crash率数据与作用,总结阅历并复盘整体收益,让一切参与者有使命感、责任心。
四、常见Crash类型与处理方案
在iOS渠道上,关于OC项目许多都是野指针问题导致,关于Swift项目许多都是强解包导致。
常见的信号量类型本文就展开聊了,有爱好的可参阅: /post/700101…
遇到的常见Crash类型主要有以下几种:
1. 空指针引证(NULL Pointer Dereference)
问题描述: 当测验拜访或操作一个空指针时,会导致空指针引证Crash。
处理方案/思路:
- 运用条件判别(if语句)在拜访指针之前检查其是否为nil。
- 在运用可选类型时,运用可选绑定(optional binding)或空合并运算符(??)来安全地处理或许为空的值。事例剖析:
var someObject: SomeClass? = nil
someObject!.someMethod() // 会导致空指针引证Crash
// 处理方案
if let object = someObject {
object.someMethod() // 只要当 someObject 不为 nil 时才调用办法
}
2.野指针拜访(Dangling Pointer Access)
问题描述: 当测验拜访现已被开释或无效的内存地址时,会导致野指针拜访Crash。
处理方案/思路:
- 在开释方针后,将指针设置为nil,以防止拜访已开释的方针。
- Xcode开发时,在Debug阶段敞开僵尸模式,Release时封闭僵尸模式
- 运用弱引证(weak references)来防止强引证环(retain cycle)导致的野指针问题。事例剖析:
- 更具体的方案可参阅: [iOS 野指针定位:野指针嗅探器] www.jianshu.com/p/9fd4dc046…
var strongReference: SomeClass? = SomeClass()
var weakReference: SomeClass? = strongReference
strongReference = nil // 此刻 weakReference 变成了野指针
// 处理方案
weakReference = nil // 在开释 strongReference 后,将 weakReference 设置为 nil
3.内存走漏(Memory Leaks)
问题描述: 当运用中的方针没有被正确开释或开释机遇不妥,会导致内存走漏,终究导致运用溃散或占用过多内存。
处理方案/思路:
- 运用ARC(自动引证计数)来办理内存,防止手动开释方针。
- 运用东西如Instruments来检测和剖析内存走漏问题。
- 在恰当的时分,运用弱引证或无主引证(unowned references)来防止循环引证。
class ViewController: UIViewController {
// ...
@IBAction func showModal() {
let modalVC = ModalViewController()
modalVC.onClose = {
// 在这里更新UI
}
present(modalVC, animated: true, completion: nil)
}
}
class ModalViewController: UIViewController {
var onClose: (() -> Void)?
// ...
@IBAction func close() {
// 履行网络恳求等操作
//......
// 封闭模态视图控制器
dismiss(animated: true) {
// 在这里履行闭包
self.onClose?()
}
}
}
//这个事例中,ModalViewController包含一个闭包特点onClose,当模态视图控制器被封闭时,它会履行这个闭包。在ViewController中,咱们在按钮点击事情中设置onClose闭包,以便在模态视图控制器封闭时更新UI。
//然而,这个代码存在潜在的内存走漏问题。当ModalViewController被弹出时,它会保存对ViewController的引证,因为onClose闭包捕获了self。
//假如用户在ModalViewController显现期间旋转设备或者履行其他操作,或许会导致ViewController无法被开释,从而形成内存走漏。
//为了防止这种内存走漏,你可以运用Swift的弱引证来处理问题。修正ModalViewController的onClose闭包如下:
class ModalViewController: UIViewController {
weak var onClose: (() -> Void)?
// ...
}
4.主线程堵塞(Main Thread Blocking)
问题描述: 当主线程被长时刻堵塞(例如,耗时操作在主线程上履行)时,会导致运用无呼应或溃散。
处理方案/思路:
- 将耗时操作移到后台线程以防止主线程堵塞。
- 运用GCD(Grand Central Dispatch)或操作行列来办理并发使命。
- 运用异步操作来履行网络恳求、文件读写等或许耗时的操作。事例剖析:
// 过错示例:在主线程上履行耗时操作
DispatchQueue.main.async {
for _ in 0..<1_000_000 {
// 长时刻的核算
}
}
// 处理方案
DispatchQueue.global().async {
for _ in 0..<1_000_000 {
// 在后台线程上履行核算
}
}
5.数组越界(Array Out of Bounds)
问题描述: 当测验拜访数组的索引超出有用规模时,会导致数组越界Crash。
处理方案/思路:
- 运用合适的鸿沟检查来防止数组越界,例如运用count特点来判别数组的大小。
- 在拜访数组元素之前,保证索引值在有用规模内。事例剖析:
let numbers = [1, 2, 3]
let index = 5
let value = numbers[index] // 会导致数组越界Crash
// 处理方案
if index < numbers.count {
let value = numbers[index] // 只要在索引有用时才拜访数组元素
}
- 如上面所列的问题,都是初级的Crash,可以在Crash监控渠道看到相应的事务函数调用仓库。
- 关于iOS开发老手来说,千分位的Crash所展示或看到的,都是能逐一处理的。困扰着咱们开发的是那些只要体系仓库的Crash,只能经过个人阅历、以及日志埋点去验证性的测验修正.
疑难杂症之Crash
当时货运iOS用户端也遇到一些项目实际中比较难解的Crash问题,我搜罗整理了一下比较经典的,为各位读者提供思路或处理方案。
1. Can’t add self as subview
Exception Type: EXC_CRASH (SIGABRT)
Exception Codes: 0x00000000 at 0x0000000000000000
Crashed Thread: 0
Application Specific Information:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Can't add self as subview
UserInfo:(null)'
// 溃散线程
Thread 0 Crashed:
0 CoreFoundation ___exceptionPreprocess
1 libobjc.A.dylib _objc_exception_throw
2 CoreFoundation ___CFDictionaryCreateGeneric
3 UIKitCore -[UIView(Internal) _addSubview:positioned:relativeTo:]
4 UIKitCore ___53-[_UINavigationParallaxTransition animateTransition:]_block_invoke_2
5 UIKitCore +[UIView(Animation) performWithoutAnimation:]
6 UIKitCore ___53-[_UINavigationParallaxTransition animateTransition:]_block_invoke
7 UIKitCore +[UIView _performBlockDelayingTriggeringResponderEvents:forScene:]
8 UIKitCore -[_UINavigationParallaxTransition animateTransition:]
9 UIKitCore -[UIPercentDrivenInteractiveTransition startInteractiveTransition:]
10 UIKitCore -[_UINavigationInteractiveTransitionBase startInteractiveTransition:]
11 UIKitCore ____UIViewControllerTransitioningRunCustomTransition_block_invoke_3
12 UIKitCore +[UIKeyboardSceneDelegate _pinInputViewsForKeyboardSceneDelegate:onBehalfOfResponder:duringBlock:]
13 UIKitCore ____UIViewControllerTransitioningRunCustomTransition_block_invoke_2
14 UIKitCore +[UIView(Animation) _setAlongsideAnimations:toRunByEndOfBlock:]
15 UIKitCore __UIViewControllerTransitioningRunCustomTransition
16 UIKitCore -[UINavigationController _startCustomTransition:]
17 UIKitCore -[UINavigationController _startDeferredTransitionIfNeeded:]
18 UIKitCore -[UINavigationController __viewWillLayoutSubviews]
19 UIKitCore -[UILayoutContainerView layoutSubviews]
20 UIKitCore -[UIView(CALayerDelegate) layoutSublayersOfLayer:]
...........
38 Huolala main main.m:14
39 (null) 0x0 + 7672682824
Crash原因:
Push和Pop时假如Animated参数是YES,那么Push和Pop不是立马完结的。假如在Push、Pop动画完结前又有新的Push、Pop,此刻会发生SIGABRT信号反常。
Crash剖析:
- 剖析页面生命周期事情
经过大量的Crash数据剖析发现,溃散前的页面生命周期计算占比数据如下:
- [ViewWillDisappear]WelfareVC *14(82%)
- [ViewWillDisappear]QrcVC *2 (12%)
- [ViewWillDisappear]SearchVC *1(6%)
也就是说,很或许是什么原因触发了WelfareVC 在被push或pop的时分,还有其他vc也正在被push或pop。接着剖析用户行为日志: 发生crash时,app的运用时长:
发现10s以内占比27%,也就是刚发动没多久;结合剖析用户日志,看到有用户点击越过开屏广告的几乎一起,app也翻开了福利中心的页面[WelfareVC]。
这时分有两个猜测:
- 用户点击越过的时分,点击事情穿透了广告页,直接翻开了福利中心的页面,导致福利中心页面push的时分,一起广告页在dismiss导致的。
- 用户点击越过广告的时分,一般是没有耐心的,很或许高频次重复点击,第一次点击触发了封闭广告页,第2次点击触发了翻开福利中心页(福利中心页面的入口跟越过按钮在屏幕上的坐标挨近);后续的点击触发了重复push福利中心页(正好咱们查到代码中,福利中心的入口按钮是没有做防抖处理的)。
接下来是验证这两个猜测:
- 仿照用户的行为,发动时在开屏广告页面,高频重复多次点击「越过按钮」,看是否能复现。
- 假如不好复现,可以在代码中,将点击事情推迟1秒呼应,然后重复点击。
处理方案
1. 暂时方案
查阅当时Crash数据,WelfareVC、QrcVC 这两个页面占比最多,结合重复点击福利中心、扫码下单按钮可复现crash,所以第一期修正是对这两个按钮做了1s的防抖处理;上线后,新版别相关crash数量下降显着WelfareVC、QrcVC 相关已消失,但还剩零星的其他页面有crash上报。此次试验证明重复push的确是crash的根因。
2. 彻底处理方案
在 UINavigationController 基类中增加正在push的标记位,在动画结束之后,再重置这个标志位,然后,用这个标志位判别push和pop操作是否可以履行。
-(void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated
{
if (animated) {
if (self.isSwitching) {
#if defined(DEBUG) && DEBUG
NSArray *array = self.viewControllers;
[SHDialogManager showDialogWithMessage:[NSString stringWithFormat:@"重复跳转, %@",array] confirmTitle:@"确认"];
#else
#endif
return; // 1. 假如是动画,而且正在切换,就不履行当次转场动画,防止重复push引起的crash
}
self.isSwitching = YES; // 2. 不然修正状况
}
[super pushViewController:viewController animated:animated];
}
当导航控制器经过视图控制器仓库的推入、弹出或设置显现新的顶部视图控制器时重置标记位:
- (nullable NSArray<__kindof UIViewController *> *)popToRootViewControllerAnimated:(BOOL)animated{
self.isSwitching = NO;
return [super popToRootViewControllerAnimated:animated];
}
此次处理在版别 6.7.32上线后,此Crash得以完全办理.
参阅资料:
github.com/Instagram/I… stackoverflow.com/questions/1… lengmolehongyan.github.io/blog/2015/1…
2. NSLayoutConstraint for xxView: Location attributes must be specified in pairs.
Exception Type: EXC_CRASH (SIGABRT)
Exception Codes: 0x00000000 at 0x0000000000000000
Crashed Thread: 0
Application Specific Information:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'NSLayoutConstraint for xxView: A multiplier of 0 or a nil second item together with a location for the first attribute cre
// 溃散线程
Thread 0 name: Tmcom-MapRender
Thread 0 Crashed:
0 CoreFoundation ___exceptionPreprocess
1 libobjc.A.dylib _objc_exception_throw
2 CoreAutoLayout _ResolveConstraintArguments
3 CoreAutoLayout +[NSLayoutConstraint constraintWithItem:attribute:relatedBy:toItem:attribute:multiplier:constant:]
4 Masonry -[MASViewConstraint install] MASViewConstraint.m:328
5 Masonry -[MASCompositeConstraint install] MASCompositeConstraint.m:174
6 Masonry -[MASConstraintMaker install] MASConstraintMaker.m:46
7 Masonry -[UIView(MASAdditions) mas_remakeConstraints:] View+MASAdditions.m:34
....
22 Huolala main main.m:14
23 libdyld.dylib _start
Crash原因:
NSLayoutConstraint针对xxView:乘数为0或第二项为nil,以及第一特点的方位,将创建一个非法的束缚,该方位等于一个常数。方位特点必须成对指定。在具体的事务里,依据束缚的UI布局中,做动画的时分运用了Masonry的mas_remakeConstraints函数,来重置UI视图的束缚,而此刻视图的superview现已被开释,导致束缚增加时没有父视图,形成crash发生. 而superview 被开释原因是因为网络数据回来后会履行0.2s的折叠动画,而此刻疏忽了用户或许刚进来,就立即就点击了回来,将当时页面Pop出去了。此刻在履行动画的时分,vc的内存刚好被收回,束缚就会找不到superview而发生crash。
[UIView animateWithDuration:0.3
animations:^{
self.detailView.alpha = 0;
self.alpha = 1;
[self.detailView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.left.right.equalTo(self.superview);
make.top.equalTo(self.superview).offset(6);
make.bottom.equalTo(self.superview).offset(-6);
make.height.mas_equalTo([self smallStyleHeight]);
}];
[parentView layoutIfNeeded];
}];
处理方案:
动画进行时,判别一下要履行视图以及视图父类是否为nil。假如为nil,直接回来,不做动作处理.
[UIView animateWithDuration:0.3
animations:^{
if (self.detailView == nil || self.detailView.superview == nil) {
return;
}
.....
}];
Crash预防
- 警觉特别场景
相似场景在处理动画和和UI束缚的时分一定要警觉,特别是一些需求在某个机遇自动触发的,要考虑到代码履行到那个机遇时,上下文的一些变量是否发生变化。
- 善用mas_remakeConstraints 办法
因为这个办法是会把之前的束缚给清理掉,而从头增加束缚,一般不到万不得已可以不必该办法,而且该办法移除束缚的时分,关于一些杂乱页面或许会形成其他束缚抵触。 比如:某个页面在迭代的进程中,新增了一个A控件,假如A控件在某个当地设置束缚equalTo了B控件,那么B控件假如调用mas_remakeConstraints办法,此刻假如没考虑到A控件,就会形成A控件束缚报错。
3. MMKV clearAll
EXC_BAD_ACCESS (SIGSEGV) MMKVCore mmkv::MMKV::oldStyleWriteActualSize
Error stack:
1 MMKVCore | mmkv::MMKV::oldStyleWriteActualSize(unsigned long) + 72
2 MMKVCore | mmkv::MMKV::oldStyleWriteActualSize(unsigned long) + 56
3 MMKVCore | mmkv::MMKV::writeActualSize(unsigned long, unsigned int, void const*, bool) + 48
4 MMKVCore | mmkv::MMKV::clearAll() + 256
5 MMKV | [mmkvObj clearAll]
6 xxxx | [self.mProductValueStore clearAll]
Crash原因:
在咱们项目里,有一些在线装备需求动态下发,因而运用了装备下发服务。其底层存储办理运用到了MMKV三方库。在App发动时,会自动获取最新的数据,并更新本地已有的存储数据。而此刻会有一定概率会触发溃散问题。
去MMKV项目的issues,也可看到有其它人遇到:github.com/Tencent/MMK…,相关运用MMKV代码如下:
MMKV *mProductValueStore = [MMKV mmkvWithID:kVisEventRelationIndexStore
rootPath:[HLLActDataTool getForeverPathWithName:kVisEventRelationIndexStore]];
[self.mProductValueStore clearAll];
Crash剖析:
仓库终究一个办法为 MMKV::oldStyleWriteActualSize,咱们直接检查源码如下:
void MMKV::oldStyleWriteActualSize(size_t actualSize) {
MMKV_ASSERT(m_file->getMemory());
m_actualSize = actualSize;
#ifdef MMKV_IOS
auto ret = guardForBackgroundWriting(m_file->getMemory(), Fixed32Size);
if (!ret.first) {
return;
}
#endif
memcpy(m_file->getMemory(), &actualSize, Fixed32Size);
}
经过Crash仓库,咱们将报错的代码定位在了 memcpy(m_file->getMemory(), &actualSize, Fixed32Size); 这一行,对memcpy(void *__dst, const void *__src, size_t __n)三个入参进行预判发现,m_file->getMemory()是有或许为空的,再深入源码:
void *getMemory() { return m_ptr; }
会发现 m_ptr 是指向了内存影射方针mmap,是否这块的值初使化失利了,导致指针是空引起的问题 ,还需求继续排查。思路有了,经过具体的排对、对比各类Crash的实时日志发现:
- Crash的场景在App发动时触发
- 内存剩余值: 20Mb~100Mb
- 磁盘剩余值: 无特别规律
此刻发现内存可用值特别的不健康,mmap是否会在这场景出现反常,不得而知。因对这块的技能深入缺乏,也无法在本地机器上复现这样场景。只能另辟蹊径,那咱们是否可以削减此MMKV::oldStyleWriteActualSize()办法的调用,就能躲避这个问题呢?
可以想到的方案:
- 获取mmkv方针一切的key,一个个删去去。不触发 clearAll办法调用 oldStyleWriteActualSize 的路径。且多线程改为单线程。
- 需求清除数据时,清掉此方针,删去对应的文件,再从头初使化,再装载对应的数据,不触发 clearAll办法调用。
处理方案:
报着测验的心态,每次发动时数据要更新,直接删去mmkv文件,再从头生成对应的数据。代码变成如下:
方案一:
//如需求更新,先将原本地对应的磁盘数据删去
if ([[NSFileManager defaultManager] fileExistsAtPath:pvs_fileName]) {
[[NSFileManager defaultManager] removeItemAtPath:pvs_fileName error:nil];
}
//再生成新的MMKV方针
self.mProductValueStore = [MMKV mmkvWithID:kProductValueStore
rootPath:pvs_fileName];
上线后验证,App放量后,60万的设备,没有一个相关的上报,也没有引发其它事务问题。于是这个Crash问题得处理。后面在做性能耗时优化时发现方案一会存在发动耗时的问题,于是测验:
方案二:
//自动清空mmkv方针内一切数据
self.mProductValueStore = [MMKV mmkvWithID:kProductValueStore
rootPath:pvs_fileName];
NSArray *allKeys = [self.mProductValueStore allKeys];
if(allKeys.count > 0){
[self.mProductValueStore removeValuesForKeys:allKeys];
}
经过验证,线上未有相关的Crash上报,一起在性能上也有新的进步。但底层次的原因还是无法解释,如有读者了解具体原因,欢迎留言指教!
4. libnetwork.dylib _nw_endpoint_flow_copy_path
先来看Crash仓库信息:
Exception Type: EXC_BAD_ACCESS (SIGSEGV)
Exception Codes: KERN_INVALID_ADDRESS at 0x0000000000000010
// 溃散线程
Thread 36 Crashed:
0 libnetwork.dylib _nw_endpoint_flow_copy_path
1 libnetwork.dylib _nw_endpoint_flow_copy_path
2 libnetwork.dylib _nw_endpoint_flow_connected
3 libnetwork.dylib _nw_flow_connected
4 libnetwork.dylib _nw_socket_connect
5 libnetwork.dylib _nw_endpoint_flow_connect
6 libnetwork.dylib _nw_endpoint_flow_setup_protocols
7 libnetwork.dylib -[NWConcrete_nw_endpoint_flow startWithHandler:]
8 libnetwork.dylib _nw_endpoint_handler_path_change
9 libnetwork.dylib _nw_endpoint_handler_start
10 libnetwork.dylib ___nw_connection_start_block_invoke
11 libdispatch.dylib __dispatch_call_block_and_release
12 libdispatch.dylib __dispatch_client_callout
13 libdispatch.dylib __dispatch_lane_serial_drain
14 libdispatch.dylib __dispatch_lane_invoke
15 libdispatch.dylib __dispatch_workloop_invoke
16 libdispatch.dylib __dispatch_workloop_worker_thread
17 libsystem_pthread.dylib __pthread_wqthread
说到这个Crash,咱们是真的头疼,先上图:
- 上图为历史溃散数据,一共发生7.2万次,影响到2.9万用户设备,其间发生Crash设备的体系会集在了iOS-14.6。
- Crash列表中占到了Top1,是Crash办理专项的大头,此刻咱们App的溃散率还处于千分位等级。
- 一起也了解到在苹果论坛关于该问题的谈论:developer.apple.com/forums/thre…
Crash原因:
这是一个很显着的野指针溃散,具体原因因为iOS 在14.5、14.6体系动态库libnetwork.dylib 内部API Bug导致的,而外部诱因是因为GCDAsyncSocket运用到了CFSocketStream相关的API,刚好触发了体系内部的Bug导致的溃散。
Crash剖析:
- 1.经过大量Crash的日志排查,发现App在发动、进入后台时,溃散量占比特别高。
- 2.经过对一切代码、二进制库搜过排查,发现只要 个推SDK包含 “xxxAsyncSocket” 字符串,与个推技能开发确认,他们SDK的确运用了CFSocketStream相关的API。
- 3.剖析设备类型、体系类型,发现Crash会集在了iOS14.x体系下发生,其它版别号的体系无此问题。
处理方案:
首要确认整体的办理思路:
- 推动个推停止运用GCDAsyncSocket,转变为运用Network.framework库,完结问题的修正。
- 剖析溃散的场景,削减libnetwork.dylib 溃散的机率。
为此咱们做了如下优化动作:
- 针对后台运转的溃散,咱们自动禁止掉了后台改写才能。
- 针对App发动时溃散,咱们将个推SDK初使化推迟到主页加载完后。
完结上述两项目优化办法后,咱们每日的Crash由本来的 300个降到每日50个。
为了完结10万分位的方针,咱们又做了如下动作:
- 跟进个推SDK晋级状况,树立技能沟通桥梁。优先处理我方公司提出的问题,定制化开发问题修正版别。
- 自研音讯推送方案,对个推SDK进行相应的替换,以达到完全控制问题的发生源。
自研音讯推送因人力本钱问题,还在试验阶段,未在运用侧上线。但个推SDK晋级多个版别后,终于在v2.7.4定制化版别终结了此问题,处理的方案也很简单,运用苹果推荐的Network.framework库,做socket通信。
到此咱们的Crash率进入了10 万 分位段位了。
排查好办法
在此,就不花过多的篇幅介绍其它的Crash了,给咱们介绍经常运用的好办法:
-
可经过Crash监控后台,对版别维度、体系维度、手机类型维度,去准备当时Crash复现的场景。
-
Crash复现思路
- 调查当时的Crash仓库,选取中心的Api调用。
- 经过日志剖析大约的页面路径,在项目对应版别跑起来。
- 经过Xcode 符号断点,触发时调查仓库相似度,判别项目代码中运用场景。
-
XCode Organize渠道Crashes栏目上如有相似的Crash,可经过解析,直接用当时版别的项目代码翻开,更直观的调查当时一切的线程仓库信息。
- 依据KSCrash的Crash监控组件,上报的Crash寄存器可以保持最近的一些函数符号,可以依据当时一切寄存器的信息,尽或许的提供咱们解题思路。
五、Crash防护技能方案
快速的事务的迭代(一周一版)总防止不了出现一些无可预料的问题,特别是Crash率已处理万分位或者十万分位,每天几个新增的Crash,就能让当日的Crash率像A股一样奇幻。为此,咱们想着是否可以做一些安全防护办法,在发生Crash时,可以兜底住。
在Crash防护方面,咱们主要采用了了以下几种技能方案:
- 首要,咱们会运用静态剖析东西,如Clang Static Analyzer,进行代码检查,以便及时发现潜在的问题;
- 其次,咱们会运用单元测验和UI测验,保证代码的质量和安稳性;
- 终究,针关于依据Runtime的东西类或容器类,咱们可以做些AOP层面的反常处理,将这样一个处理组件称之为:安全气囊, 咱们开发并运用到咱们项目中。
安全气囊
经过 Runtime 机制可以防止的常见 Crash :
- unrecognized selector sent to class/instance(找不到类/方针办法的实现)
- KVO Crash
- NSNotification Crash
- NSTimer Crash
- Container Crash(调集类操作形成的溃散,例如数组越界,插入 nil 等)
- NSString Crash (字符串类操作形成的溃散)
大约的功能架构如下,具体请参阅网易大神方案: neyoufan.github.io/2017/01/13/…
针关于咱们所认知的,当时Crash虽然被防护住 了,是不是会给事务流程带来更致命的过错。站在事务产品的角度来说,这的确是需求考虑的,为此咱们参加“安全防护提示弹窗”。
目的: 中心页面发生了安全防护动作时,给予用户提示,引导用户操作。
- 引导用户从头进入当时页面,可处理一些偶现的Crash。
- 依据当时页面的crash的次数阈值,测验引导用户重启APP,躲避此次crash。
- 假如当时页面的crash的次数严峻超支,引导用户截图当时的提示信息,经过用户反馈流程进行上报客服后台。
关于越老练的团队,防护方案带来的作用会越小。因为老练团队的代码质量相对更高,一些初级过错出现的概率极小。但关于小团队,或者历史比较久的项目而言,这套方案带来的协助会比较大,究竟坑总是防不胜防的。
六、 长期办理的总结和复盘
在不断追求下降Crash率的进程中,总结和复盘可以协助咱们团队更好地了解问题的本质、发现潜在的改善点,并拟定未来的战略。我总结了一些阅历,具体进程如下,供咱们参阅:
1. 搜集和剖析Crash数据
- 运用Crash监控东西/渠道(如KSCrash、Bugly等)继续搜集Crash数据。
- 剖析Crash数据,了解Crash的类型、频率、发生场景等信息。
- 保证Crash数据的及时性和准确性。
2. 确认优先级
- 辨认并优先处理高频率的Crash,特别是那些影响用户体会的Crash。
- 依据Crash的影响规模(是否影响中心功能)、Crash的危害性(是否导致数据丢失或安全问题)以及修正的杂乱性来确认优先级。
3. 拟定办理方案
- 为每个高优先级的Crash拟定办理方案,明确责任人和截止日期。
- 保证办理方案是可履行的,包含必要的资源和东西支撑。
- 考虑运用灵敏开发办法,将Crash修正纳入每个迭代周期。
4. 进行Crash修正
- 运用阅历丰富的开发人员负责Crash修正作业,保证修正方案是牢靠和安稳的。
- 在修正Crash时,不只要处理当时的Crash问题,还要防止相似问题再次发生。进行根本性的问题排查。
- 在修正Crash后,进行单元测验和集成测验,以保证修正没有引进新问题。
- 兜地方案,假如能接入装备下发办理渠道,可经过开关控制,线上运转出现反常时可下发封闭。
5. 验证和监控
- 部署修正后,继续监控Crash率,保证修正有用。
- 假如Crash率下降,保证不会引进性能问题或其他不良影响。
6. 复盘和总结
- 在完结Crash修正后,安排一个复盘会议,谈论整个办理进程,包含问题的根本原因、处理方案和修正作用。
- 确认哪些操作是成功的,哪些是不成功的,以及如何改善。
- 拟定下一步的方案,包含如何预防相似Crash问题的再次发生,以及如何进步团队对Crash办理的敏感性。
7. 继续改善
- 在Crash办理的基础上,树立一个继续改善的机制,保证不断改善运用的安稳性。
- 定时审查Crash数据,检测新的Crash问题,并重复上述进程来处理它们。
- 保持团队的学习和更新,以跟进最新的iOS开发技能和东西。
总结和复盘是一个迭代的进程,可以协助团队不断改善Crash办理战略,进步运用的安稳性和用户体会。长期的办理和继续改善将有助于削减Crash率,增强运用的牢靠性,进步用户满意度,一起下降维护本钱。
七、总结
在长期Crash办理进程中,需定时进行复盘,总结阅历教训,不断改善。回顾过去3年,咱们成功将Crash率从万分位降至十万分位。这个进程中,咱们不只进步了用户体会,也积累了名贵的技能阅历。
在Crash办理的道路上,不断学习、不断进步,与其他开发者共享阅历,将有助于整个iOS生态体系的质量进步。期望这些阅历和技能方案关于霸占Crash问题的开发者们有所协助。愿咱们的运用可以继续安稳运转,用户满意度不断进步。
因作者水平有限,本文有过错之处或者技能上谈论沟通的,欢迎谈论区留言。
八、货运iOS用户组成员:
Shirly、Elina、Stephen、Jeff、Jesse、Connor、Sherwin、Jun、Carl