查看原文
其他

京东金融App崩溃治理实践

吴欣宇 京东科技技术说 2022-06-25


作者:吴欣宇京东科技业务中台-移动研发团队原创


一、前

在2020年初,京东金融App的用户规模已远超几年前,日活也是成倍的增长,同时我们也注意到App崩溃率随着版本迭代一直在小幅度升高。当我们意识到App的崩溃已经伤害到用户日常的使用体验时,崩溃率已达到千分之几。崩溃率是衡量App质量的重要指标,不仅影响App的稳定性,还会直接影响用户体验和业务增长。如果启动时发生崩溃可能导致App直接被卸载,进一步造成口碑变差、品牌价值下降等影响。所以金融App在业务高速发展的同时也更加的注重质量建设。

京东金融App的崩溃率上升与App业务的高速发展是分不开的。越来越复杂的业务场景、多个业务之间的逻辑耦合以及App功能的扩展都使程序更容易出现错误。一些古董代码在多次业务迭代之后慢慢受到了影响,在某些特殊场景下出现的错误需要很长时间并且是大量的用户使用时才会出现,这些都使得错误的修复变得不那么及时。灰度中出现的崩溃问题在查找无果后就变成了待观察状态,在上线后用户量大增的情况下就凸显出来。崩溃的慢慢累积,使得崩溃率在某个版本变成了很刺眼的数字。基于此情况,团队内部决定对当时的情况做彻底的治理、找到维护的方式方法。

京东金融App的崩溃治理持续了几个版本,崩溃数量前二十的问题基本修复完成。然而崩溃的修复并非是一帆风顺的,一些难以复现的问题经过修复观察再修复最终将问题解决。在修复原有问题期间App业务也在持续更新并带来了一些新的问题,对于新出现的问题研发团队格外关注,利用灰度发布阶段将问题消灭在萌芽之中。最终金融App将崩溃率稳定在万分之一以下。

根据2020年度移动行业性能体验报告,App行业平均崩溃在0.29%,Android端行业平均崩溃率在0.32%,而iOS端应用行业平均崩溃率在0.10%。


京东金融App经过高质量的持续修复,崩溃率远低于行业平均水平两个数量级,长期稳定在0.007%水平。


用户崩溃率数据来源于APM性能监控系统。


京东金融App的崩溃率远优于行业水平与研发团队的深入技术探索是分不开的,崩溃基础知识是技术探索的前提条件。本篇文章将由浅入深的讲解崩溃基本知识并分享典型崩溃案例的解决过程。


一、崩溃的定义

1、崩溃发生的原因

崩溃是CPU对发生异常的一种显式反应,CPU的异常处理是基于中断来完成的。中断是CPU暂停正在执行的程序,保留现场后转去执行相应的处理程序,处理完该事件后再返回断点继续执行被“打断”的程序。


在操作系统相关资料中介绍:中断(interrupt)和异常(exception)在不同的CPU架构里有不同的含义。
  • 比如在Intel架构中,中断处理的入口由操作系统内核中的中断分配表定义(interrupt dispatch table, IDT),IDT中有255个中断向量,其中前20个定义为异常(exception)的处理入口,即中断包含异常。
  • 而在ARM架构中,中断处理的入口则是在异常向量(exception vector)中,8个异常向量里边有3个是中断相关的,即异常包含中断。

不管如何界定中断和异常,CPU发生异常时,都会将控制权从异常前的程序交给异常处理程序,而且CPU将获得不会更低的执行权利,比如执行用户态的应用程序发生异常,CPU将切换到内核态,并执行对应的异常处理程序。经典的CPU五级流水线中一条指令的生命周期为[取指、译码、执行、访存、写回],每个阶段都可能出现CPU异常,比如在ARM架构下:
  • 在“执行”阶段产生的“数据中止”异常:若处理器数据访问指令的地址不存在,或该地址不允许当前指令访问时,产生数据中止异常。
  • 在“取指”阶段产生的”预取中止“异常:若处理器预取指令的地址不存在,或该地址不允许当前指令访问,存储器会向处理器发出中止信号,但当预取的指令被执行时,才会产生指令预取中止异常。

两种异常对应的处理程序会直接或者间接调用 Mach 内核的exception_triage() 函数,并将EXC_BAD_ACCESS作为入参传进去,exception_triage() 将会利用Mach消息传递机制投递异常。

2、在iOS系统中崩溃是如何发生的

在iOS系统内核(Mach)中,异常是通过内核中的基础设置「消息传递机制」处理的,异常并不比一条消息复杂,异常由出错的线程和任务通过msg_send()抛出,然后由一个处理程序通过msg_recv()捕捉。处理程序可以处理异常,也可以清除异常,还可以决定终止应用程序。

对于App来说,当App试图做一些不被允许的事情,比如CPU无法执行某些代码(访问无效内存、修改只读存储区等),或者触发了操作系统的某些策略(内存占用高、App启动时间过长等),操作系统将通过终止你的App来保护用户体验。

在一些开发语言中,一些编程对象遇到错误也会停止程序运行而发生崩溃。比如Object-C/Swift中越界访问数组,NSArray/Array会触发崩溃并停止程序运行。


二、常见崩溃的几种类型

1、野指针
野指针即指向一个不确定的内存地址,通过野指针访问这个内存地址时可能会发生各种不确定的情况。如果这块内存地址未被覆盖并不一定会出现问题,如果已经被覆盖或者分配成不可访问空间则程序直接崩溃。如果判断是野指针造成的崩溃,那么当前的崩溃代码很大可能并不是导致崩溃的原因,需要通过分析调用关系找到真正的崩溃原因。

在C语言中野指针经常发生在声明变量后未赋初值(随机地址)或指针释放后未置空,Object-C中的野指针多发生在多线程,当前线程访问的变量在另一个线程被释放。Object-C中指针默认值是nil,同C语言中的NULL,表示指针不指向任何内存空间。野指针错误结果通常都是变量或内存访问异常,常见的崩溃类型是EXC_BAD_ACCESS内存错误。
2、死锁
在iOS系统中,使用dispatch_sync在主线程执行同步任务就会产生死锁。如果因为某些复杂业务逻辑场景导致任务运行在主线程(下面示例代码),则会导致应用crash。
-(void)sceneAnalysis { dispatch_sync(dispatch_get_main_queue(), ^{ NSLog(@"Sync Task Result"); }); NSLog(@"Do Other Tasks"); }

程序会卡死在函数体的第一行,错误信息如下:Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)


以上示例代码是最典型的主线程死锁,主队列中添加了一个同步任务「NSLog(@"Sync Task Result")」,所以主线程会暂停当前代码转去执行block代码块,并等待dispatch_sync函数返回后继续执行。但主队列(main queu)是串行队列遵循先入先出的原则,当前主线程正在执行sceneAnalysis函数,dispatch_sync需要等待sceneAnalysis函数执行完毕。sceneAnalysis和dispatch_sync互相等待,这样就造成了死锁。

3、watchdog

如果App在执行某个任务(启动、终止或响应系统事件)时花费时间较长,操作系统会终止当前进程。看门狗机制触发的崩溃最明显的标识就是错误码:0x8badf00d 。通常崩溃日志会如下图所示:

当App的启动时间超过最大允许值(一般为20s),在iOS系统中就会触发看门狗机制立即终止进程。值得注意的是:由看门狗触发的崩溃并不会收集在自己开发的错误监控里,可以在崩溃设备中获取崩溃日志。苹果在使用模拟器时关闭了看门狗机制,并且在Debug模式下也不会触发看门狗机制。所以在开发中对App启动流程务必做到简洁、按需加载。


三、金融App的崩溃治理实战案例

1、多线程导致的野指针问题

在金融APP中使用长连接技术更新大盘指数信息。如下图例:



功能灰度时一直持续关注,期间并未发现崩溃问题,但随着新版本开放更新及活跃用户量的持续增长,APM性能监控平台开始发现偶发崩溃。从崩溃日志上看,崩溃出现在MQTTClient开源库里。通过沟通发现其他业务部门使用MQTTClient开源库的场景也出现过同样问题。



由于属线上崩溃,在APM性能监控平台上通过崩溃次数和堆栈信息评估风险确定这是非必现偶发问题,于是研发团队开始着手对问题进行定位并查找原因。通过在APM性能监控平台对崩溃进行持续跟踪,收集数据样本后,分析出App崩溃前用户的共通操作 —— 应用切换前后台。这是非常可疑的操作路径信息。



京东金融App中长链接使用开源的MQTT协议,研发团队在项目开源社区中查询相关issues和解决方案,社区中虽有相似问题,但由于该库超过2年未进行更新维护,未能找到解决方案。


于是,我们将注意力重新转回业务使用场景和MQTTClient源码当中。源码中引起崩溃的函数如下,此处为MQTT发送消息的NSStream流对象回调时机。

- (void)stream:(NSStream *)sender handleEvent:(NSStreamEvent)eventCode


在实际业务场景中,MQTTClient采用前台运行时连接,退入后台主动断开的方式进行工作。于是研发团队想要通过前后台切换复现此问题,通过代码高频模拟App进入前后台场景,终于在debug模式下复现了此问题。



崩溃发生在MQTT的内部线程,金融App在切换后台时断开连接,MQTTCFSockeEncoder对象在外部线程(对象创建的线程)被释放,于MQTTCFSockeEncoder对象的释放与stream处理现队列不统一,当前线程未能同步状态,依然访问该地址造成“野指针”崩溃。


修复方案:通过对self对象进行“保留”操作,使对象所属的堆内存引用计数增加,防止在执行回调函数过程中,堆内存被系统回收。之后再次通过高频调用对场景进行模拟,未再复现。问题因此得以解决。 

- (void)stream:(NSStream *)sender handleEvent:(NSStreamEvent)eventCode { MQTTCFSocketDecoder *strongDecoder = self; (void)strongDecoder; //其他代码。。。}


解决问题后,在团队内部通过托管更新方式,为金融app及其他业务团队进行统一更新。同时在社区中提交了PR。未来在长连接的使用过程中,研发团队还会持续对发现的问题进行关注。

 

开发中还一个有关于野指针比较明显的例子就是通知中心NSNotificationCenter。注册通知时,通知中心会保存接收对象的内存地址,但是不会对接受对象引用计数+1(unsafe_unretained)。当对象被释放后,原有的内存地址可能已被重用。通知中心发送通知时仍然会将消息发给保存的内存地址,但是保存的内存地址已不再是原来的对象,接收到消息不能处理,程序发生crash错误。


基于野指针造成的崩溃经常发生,苹果在iOS9就已经优化了通知中心的使用。iOS9以后的版本,在对象释放时会自动移除所有通知。前提是对象能正常释放。同理还有在tabview的delegate和dataSource,在iOS9以前使用unsafe_unretained修饰,在iOS9及以后修改成使用weak修饰防野指针情况(使用weak修饰的指针在对象被释放后会自动置为nil)。如果App支持的最低版本是8.0,那么还需要额外关注delegate的野指针问题。


2、多线程共享资源导致的过度释放

开发中遇到的另一个错误也是由多线程引起的,是比较罕见的set方法赋值错误。App使用开源的lottie框架替代GIF图做复杂的氛围动画,以便降低内存消耗。获取本地兜底lottie文件时,因为是耗时操作,所以开辟子线程从本地获取和解压,完成后回归主线程渲染。  另外在App启动时会发送网络请求拉取最新的lottie文件,如果网络请求顺利且快速,界面会优先展示最新的lottie文件。


在查看崩溃统计日志和业务代码后很快就怀疑是多线程引起的错误。读取本地lottie文件为1号线程,网络请求是7号线程,在获取文件路径后1号线程和7号线程都会调用handleCacheFilePath函数获取lottie文件。handleCacheFilePath函数代码如下:

-(void)handleCacheFilePath:(NSString *)filePath {
if (!filePath) { return;; }
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSData *zipData = [NSData dataWithContentsOfFile:filePath];
/*省略zip解压等其他操作 ... */
manager.lottieData = zipData;
dispatch_async(dispatch_get_main_queue(), ^{ //回主线程 }); });}


在解压缩后将lottie的json数据赋值给self.lottieData,等待在接来下的界面展示lottie动画。实际运行过程中,在某些情况下会发生崩溃,因为在App业务比较复杂,触发场景及其苛刻,所以将lottie获取和展示模块移到新建Domo中进行复现。


崩溃日志解析后内容如下:


一看崩溃类型EXC_BAD_ACCESS (SIGSEGV),显示内存错误。应用的多线程错误通常都会导致一些内存问题,崩溃日志和内存错误很像。错误类型通常是EXC_BAD_ACCESS (SIGSEGV)。通过分析,崩溃发生在manager.lottieData = zipData赋值,继续看崩溃信息,显示崩溃的原因是变量过度释放。


我们知道给Object-C中给变量赋值就是调用setter方法,那为什么会过度释放呢,看看OC的底层源码是如何处理的 (源码链接:https://opensource.apple.com/source/objc4/objc4-723/runtime/objc-accessors.mm.auto.html)。通过汇编代码发现set方法调用了OC runtime的objc_setProperty_nonatomic函数,而苹果的部分run time源码是开源的,直接查看源码。

void objc_setProperty_nonatomic(id self, SEL _cmd, id newValue, ptrdiff_t offset){ reallySetProperty(self, _cmd, newValue, offset, false, false, false);}


在objc_setProperty_nonatomic中实际调用了reallySetProperty函数。

static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy){ if (offset == 0) { object_setClass(self, newValue); return; }
id oldValue; id *slot = (id*) ((char*)self + offset);//计算偏移量获取指针地址
if (copy) { newValue = [newValue copyWithZone:nil]; } else if (mutableCopy) { newValue = [newValue mutableCopyWithZone:nil]; } else { if (*slot == newValue) return; newValue = objc_retain(newValue);//retain新值newValue }
if (!atomic) {//非原子属性 oldValue = *slot;//第一步 *slot = newValue;//第二步 } else { spinlock_t& slotlock = PropertyLocks[slot]; slotlock.lock(); oldValue = *slot; *slot = newValue; slotlock.unlock(); }
objc_release(oldValue);//释放旧值 引用计数-1}

问题的关键就出在reallySetProperty函数第20~31行,如果是非原子(nonatomic)操作,直接将*slot赋值给oldValue对象然后将新值付给*slot,最后将oldValue释放引用计数减一。如果是原子操作(atomic修饰),在变量读取之前会先添加自旋锁,spinlock自旋锁在被当前线程获取后,另外一个线程获取自旋锁时获取不到,只能原地等待。


因为lottieData是个共享变量,且使用非原子nonatomic修饰1号线程赋值会进入到非原子条件中,假设当1号线程执行完oldValue = *slot赋值,时间片耗尽。此时CPU调度开始执行7号线程,7号线程也会执行相同的操作给oldValue赋值。赋值完成后执行objc_release(oldValue)指令,对oldValue指向的内存空间做了release操作。此时oldValue所指向的内存空间已经被释放。7号线程执行完毕后CPU在在转去执行1号线程,当1号线程再次执行objc_release(oldValue)方法时,会就发生崩溃。原因是对一个已经释放的内存再次做release操作。这也对应了崩溃堆栈中overrelease_error错误。


解决方法:深入了解了原理之后问题的解决就水到渠成了。lottieData是一个manager持有的共享变量,可以改成使用atomic修饰防止多线程竞争。因为atomic修饰的变量set和get方法都会添加自旋锁,如果是读取比较频繁的场景自旋锁是比较耗费CPU资源的。所幸我们结合业务分析lottieData的使用场景并不算多,并不会造成CPU资源的过度浪费。  另外还可以修改代码逻辑将lottieData声明成临时变量,使用临时变量作为函数返回值来解决多线程竞争问题。

3、方法调用异常

对于崩溃的千变万化来说,方法调用异常是最容易解决的问题之一了。在App中有疏忽导致的方法未实现,也有因为内存释放导致对象方法不存在,我们不讨论解决办法,因为开发者早已熟知并解决过很多次。在这主要探索一下“unrecognized selector sent to instance”异常抛出的过程。


我们都知道,在OC中调用对象的方法本质就是向对象发消息。在编译阶段方法调用会被转换成objc_msgSend函数。第一个必须参数是消息接收者,第二个必须参数是方法名,后面是传递的参数。

objc_msgSend(id self, SEL op, ... )


下面是消息发送的步骤:

(1) 检测selecter是否需要忽略,如某Mac OX系统存在垃圾回收机制,则会忽略retain/release函数。

(2) 查看响应对象是否为nil。向nil对象发送消息会被runtime系统忽略。

(3) 从cache中通过IMP函数指针查找方法实现,若存在则执行该方法。

(4) cache中找不到则从Class的方法列表中查找,并递归查找父类的方法列表。

(5) 如果都找不到则进入动态方法决议和消息转发流程。



通过objc_msgSend函数向一个对象发送消息,如果该对象经过多个流程仍无法处理,则会抛出异常。在crash之前OC的运行时系统会先经过以下两个步骤:


  • DynamicMethod Resolution(动态方法决议)

以对象方法举例,系统会调用resolveInstanceMethod:为对象动态添加方法,如果返回值为yes则会再次查找实例方法,如果返回值为No则会进入消息转发流程。

+ (BOOL)resolveInstanceMethod:(SEL)sel { if (sel == @selector(handleOpenPage:)) { IMP imp = class_getMethodImplementation([self class], @selector(openNewPage:)); class_addMethod([self class], sel, imp, "v@:"); return YES; } return [super resolveInstanceMethod:sel];}


上面的例子为实例对象的handleOpenPage:方法动态添加了实现(openNewPage:)。其中“v@:” 表示返回值和参数。每个字符含义可以查看Type Encodings。(Type Encodings链接:https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html)


  • Message Forwarding(消息转发)

消息转发会调用forwardingTargetForSelector 方法获取新的target 作为 receiver 重新执行selector。如果是对象方法需要复写- (id)forwardingTargetForSelector:(SEL)aSelector方法。如果是类方法,则复写+ (id)forwardingTargetForSelector:(SEL)aSelector方法。

- (id)forwardingTargetForSelector:(SEL)aSelector { if(aSelector == @selector(handleOpenPage:)){ return _otherObject; } return [super forwardingTargetForSelector:aSelector];}


如果返回的对象不合法(为nil或者跟旧receiver一样),则进入forwardInvocation流程。可以在程序中复写这个方法来定义转发逻辑。anInvocation参数是runtime系统调用methodSignatureForSelector:方法获取方法签名时生成的对象。重写forwardInvocation:的同时也要重写methodSignatureForSelector:方法,否则会抛异常。

- (void)forwardInvocation:(NSInvocation *)anInvocation {
if ([_otherObject respondsToSelector:[anInvocation selector]]) { [anInvocation invokeWithTarget:_otherObject]; } else { [super forwardInvocation:anInvocation];    }


消息转发整个流程如图所示:



当一个对象没有实现响应的方法,runtime系统将通过forwardInvocation消息通知该对象。每个对象都从NSObject类中继承了forwardInvocation:方法,NSObject中的方法实现只是简单地调用了doesNotRecognizeSelector:。通过实现我们自己的forwardInvocation:方法,可以在该方法实现中将消息转发给其它对象。如果在这些流程中都不处理,则会抛出异常导致崩溃。


以上是金融App在崩溃治理过程中的典型案例和源码、原理分析。在上面案例治理过程中研发团队沉淀了非常有用的问题查找经验。


四、实战经验沉淀


1、用户的操作路径是复现问题的最佳提示,崩溃日志中的堆栈信息能追溯用户进入了哪些页面,处于什么状态,前台运行还是在后台。再配合APM性能监控平台就能够更清晰的分析出App当前所处的状态。根据启动Id可以看到崩溃之前App的网络状态,发送了哪些网络请求。这些都能帮助开发人员更快的复现问题。


2、崩溃发生在App本身的业务代码中往往是容易解决的,如果发生在使用的第三方开源库当中,可以优先去开源社区寻找相同的问题,一些经常维护的开源库经过了诸多App真实场景测试,会有类似的问题和解决方案。如果是研发团队攻克的,也可以在开源社区提交相关pull request。并将解决的经验分享给遇到相同问题的研发人员。


3、任何崩溃都有其发生的特定条件,当实在无法复现时应该从另一个角度去寻找解决办法,阅读源码是一个非常非常好的了解问题本质的方式,苹果的系统是闭环生态,但部分源码是开源的,可以在线阅读或者下载相关源代码深入理解。


4、崩溃日志是解决崩溃问题的第一手资料,崩溃日志中包含了App崩溃时的堆栈信息和崩溃原因。在开发中崩溃日志的阅读是非常重要的。所以在下面部分有必要简要简绍一下崩溃日志相关内容。


五、崩溃日志分析


1、崩溃日志内容


崩溃发生以后,我们第一个想到的就是崩溃在哪一行代码、堆栈是什么、运行中有哪些线程,这些信息都包含在崩溃报告中。以WWDC中的Demo举例,ChocolateChip运行在模拟器上。崩溃日志的顶部包含了一些摘要信息,包括App名称、版本号、操作系统以及崩溃的日期和时间。



下面部分是崩溃的原因,错误发生在主线程,崩溃类型是SIGILL即CPU正在执行不存在或无效的指令。Fatal error中显示崩溃具体原因是强制拆包一个可选值为nil的变量。



再下面部分就是崩溃的堆栈信息,可以查当前崩溃的线程,崩溃时堆栈信息等。


崩溃的原始堆栈信息如下图所示:


崩溃的原始堆栈信息是不方便直接定位崩溃问题的,还需要将原始堆栈信息进行符号化()。将内存地址转化为方法名、文件名和行号的过程叫做符号化。崩溃日志符号化有3个必须要素。


(1) 崩溃日志,崩溃日志可以在Xcode的Window选项中打开Organizer窗口,从Crashes面板中获取,也可以从App提交的后台下载。具备完备监控能力的App可以通过App端采集上报给服务器端存储,然后从服务器端下载崩溃日志。


(2) 符号表。dSYM(debugging SYMbols)又称为调试符号表。每一个通过Xcode编译上传的应用都会自动归档,在Xcode的Window选项中打开Organizer窗口,在Archives菜单中会显示已编译应用文件。选中文件通过show in finder→显示包内容就能看到应用的dsym文件。


(3)/Applications/Xcode.app/Contents/SharedFrameworks/DVTFoundation.framework/Versions/A/Resources/symbolicatecrash路径可以获取Xcode自带的符号化工具。


将以上三个文件拷贝到同一个文件中,查看三个文件的UUID一致,然后使用终端进入当前目录,执行符号化命令./symbolicatecrash-vxxxx.crash xxxx.app.dSYM。完成后打开xxxx.crash文件,便能够看到已经符号化的堆栈,可以清楚的看到方法名、文件名和行号等信息。符号化的信息如下图所示:



当然,在日志的最下面还会有一些低级信息,包括崩溃线程的寄存器状态,还有加载到进程中的二进制数据镜像,这是App的可执行文件数据。xcode通过符号化查找符号、文件和行号信息,并显示在堆栈当中。


寄存器信息:


可执行文件镜像:


以上就是一个崩溃日志的所有内容,在这些内容中应该关注下面这些有用信息。


首先,从崩溃类型开始,例子中异常类型为EXC_BAD_INSTRUCTION异常,CPU在执行一个非法指令。崩溃信息中说明崩溃原因是强制拆包了一个可选对象。


其次,崩溃在主线程,堆栈中包含了崩溃时正在运行的函数堆栈。堆栈中看到了 fatalErrorMessage函数 这是一个系统函数,代码中的 一个函数调用了它。


从堆栈信息中(RecipeImage.swift:26)能够看到,调用发生在RecipeImage.swift文件的第26行。代码中有一个Recipe类,其image函数被调用并且该函数由于某些错误又调用了 fatalErrorMessage函数。在获取image时,代码中对可选路径进行了强制拆包,导致崩溃。见下图:



2、崩溃日志在哪查看

解读了崩溃日志的内容,那么在哪里可以查看崩溃日志

(1) 使用AppleID登录Xcode,在菜单栏中的organizer查看crash项。

(2) 如果能拿到崩溃的机器,可以直接获取设备中日志信息,并筛选出和App有关的日志信息。

(3) App监控平台,通过App端收集崩溃信息,在后台进行分类和解析。更方便的帮助开发定位问题。


六、崩溃专项治理攻略

1、成立崩溃专项,定一个阶段性目标。

在京东金融App崩溃专项治理之前,App的崩溃率是不稳定的,会随着版本发布和业务的迭代上下浮动。线上统计的崩溃问题种类繁多。有简单的数组越界崩溃、插入nil值崩溃,也有内存问题导致野指针、多线程异常。针对这些情况,团队成立崩溃治理专项小组,根据崩溃次数、紧急程度进行排序,循环解决崩溃列表前十问题。

2、崩溃模块定位和分发。

当前App已不局限于某一项业务,已经是多个业务功能的集合体。在整个App进行组件化拆分之后,各业务功能代码都以.a,.framwork等形式集成在App中。当一个崩溃发生后难以查找崩溃代码存在于哪个业务模块当中,给崩溃的分发和解决造成很大困难。基于这个问题,崩溃专项小组通过linkmap进行文件名称匹配,或者通过grep命令查找二进制中包含崩溃代码的.a和.framework文件。查找成功通过便捷形式发送各业务方及时处理。

3、建立App监控体系。

在以往的崩溃解决流程中,都是通过研发主动去苹果开发者后台或者第三方崩溃监控后台(bugly、友盟等)去查看当前崩溃趋势等,这种方式需要研发自驱并且是有延迟的,比如后台更改配置突然出现的大量崩溃很难及时响应。金融App通过APM性能监控系统对崩溃进行趋势监控,设定崩溃阈值,在多长时间之内崩溃数指定次数就会主动触发报警,一定时间崩溃率大于阈值也会触发报警,并以邮件、内部沟通工具等方式通知负责人及时处理。同时,每周自动发送性能周报来评估性能体系。 

4、持续性关注一直存在的问题、扼制新出现问题。

日常开发中一定有一些问题是持续的,难以复现的。这类问题在日活平稳时崩溃量不大,但持续贯穿多个版本,平时可能会淹没在其他的崩溃当中,但一到618、双十一等日活大增时,崩溃量就会上升。这也是发现问题的一个特殊时期。另外,在解决原有问题的同时,上线后对新增业务重点监控。以防灰度中未出现的问题在更多用户使用时崩溃爆发。

5、编码规范。

良好的编码规范有助于减少编码错误。程序中复杂的算法和逻辑只程序的很少部分,大部分的崩溃通过代码review都能够避免。常规的字典数组插入空值、方法找不到等在开发、测试、灰度过后基本上已全部消灭。


七、总结


本篇着重讲了崩溃相关基本知识,包括崩溃的发生、崩溃的典型场景、如何解读崩溃日志。同时结合金融App开发过程中遇到的实际崩溃例子,从问题发生原因,如何定位崩溃位置,以及怎么复现,如何修复等几个方面详细进行分析。开发中可以根据崩溃的类型快和崩溃日志速定位问题,为典型崩溃提供一种解决思路。App性能和用户体验是一个长期优化过程,崩溃不会随着优化而停止,持续关注和优化才能让当今已经代码量爆炸的App稳步前行。


在本篇的第六部分,提到APM性能监控系统,APM性能监控系统是京东科技移动团队和运维团队一起倾力打造的性能监控平台,从启动耗时,网络请求波动,webView打开耗时,用户轨迹、原生页面监控、崩溃卡顿、自定义监控等功能应有尽有,达到了从启动到连接服务器再到退出的全链路监控。目前京东科技内多个App已接入APM性能监控系统,给各App及业务团队提供了更高的质量保障。



往期好文推荐:

> 活动可视化怎么做?看京东乐高架构设计
> 京东科技Redis跨数据中心双向同步优化实践

> 轻量级工作流引擎的设计与实现

> AAAI 2021论文:门控记忆神经网络


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存