摘要:

货拉拉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体系以及日志体系;

监控主面板如下:

三年磨一剑,货拉拉iOS用户端10万分位Crash率攻坚之战

本文就不过多介绍自建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…

三年磨一剑,货拉拉iOS用户端10万分位Crash率攻坚之战

遇到的常见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剖析:

  1. 剖析页面生命周期事情

经过大量的Crash数据剖析发现,溃散前的页面生命周期计算占比数据如下:

  • [ViewWillDisappear]WelfareVC *14(82%)
  • [ViewWillDisappear]QrcVC *2 (12%)
  • [ViewWillDisappear]SearchVC *1(6%)

也就是说,很或许是什么原因触发了WelfareVC 在被push或pop的时分,还有其他vc也正在被push或pop。接着剖析用户行为日志: 发生crash时,app的运用时长:

三年磨一剑,货拉拉iOS用户端10万分位Crash率攻坚之战

发现10s以内占比27%,也就是刚发动没多久;结合剖析用户日志,看到有用户点击越过开屏广告的几乎一起,app也翻开了福利中心的页面[WelfareVC]。

这时分有两个猜测:

  1. 用户点击越过的时分,点击事情穿透了广告页,直接翻开了福利中心的页面,导致福利中心页面push的时分,一起广告页在dismiss导致的。
  2. 用户点击越过广告的时分,一般是没有耐心的,很或许高频次重复点击,第一次点击触发了封闭广告页,第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得以完全办理.

三年磨一剑,货拉拉iOS用户端10万分位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,咱们是真的头疼,先上图:

三年磨一剑,货拉拉iOS用户端10万分位Crash率攻坚之战

三年磨一剑,货拉拉iOS用户端10万分位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导致的溃散。

三年磨一剑,货拉拉iOS用户端10万分位Crash率攻坚之战

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了,给咱们介绍经常运用的好办法:

  1. 可经过Crash监控后台,对版别维度、体系维度、手机类型维度,去准备当时Crash复现的场景。

  2. Crash复现思路

    1. 调查当时的Crash仓库,选取中心的Api调用。
    2. 经过日志剖析大约的页面路径,在项目对应版别跑起来。
    3. 经过Xcode 符号断点,触发时调查仓库相似度,判别项目代码中运用场景。
  3. XCode Organize渠道Crashes栏目上如有相似的Crash,可经过解析,直接用当时版别的项目代码翻开,更直观的调查当时一切的线程仓库信息。

三年磨一剑,货拉拉iOS用户端10万分位Crash率攻坚之战

  1. 依据KSCrash的Crash监控组件,上报的Crash寄存器可以保持最近的一些函数符号,可以依据当时一切寄存器的信息,尽或许的提供咱们解题思路。

三年磨一剑,货拉拉iOS用户端10万分位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/…

三年磨一剑,货拉拉iOS用户端10万分位Crash率攻坚之战

针关于咱们所认知的,当时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