崩溃率从1%到0.02%,iOS稳定性解决之道
在《移动互联网技术质量体系的理解》中,提到了在团队中以数字衡量技术质量的体系建设。其中,iOS在今年双周迭代的情况下,两个项目都保持了一个季度崩溃率稳定万分之二的水准,而这个数字在两年前是1%以上。
本周在外部沙龙上做了相关工作的总结,enjoy~
相信大家都一样,在工作中做了非常多的工作,无论是业务上的还是技术上的。但是我们如何去衡量,去评判我们所做工作的好坏呢?作为技术人员,如何去量化我们为用户提供服务的优劣呢?我们需要一个相关性指标来衡量在线状态。
就像每个月末的KPI考核一样,在线上环境中设立了很多指标性数据,例如:
-
崩溃率 (衡量一个APP的稳定性)
-
卡顿率 (衡量一个APP的流畅性)
-
包体大小
-
启动时长
-
接口平均响应时间
其中,客户端最关注崩溃率和卡顿率。崩溃率的定义如下:
崩溃率 = 每天崩溃的用户数 / 每天使用APP的用户数
考虑到崩溃对用户带来的体验极差,很可能导致用户流失,所以我们计算了一下,每崩溃一个人用户我们所损失的直接利益:
-
假设一个有效注册用户的推广成本是10元
-
如果是1%的崩溃率,按照每天100w日活计算,则每天可能流失的用户为1w人
-
折合成市场费用就是10w元/天
-
更不用说挽回用户的成本,以及用户潜在付费的可能
如下图,听云提供了一个基准,千分之三为标准和优秀的分水岭。
Image虽然从标准上来看,千分之三已经是个优秀的值了。但是对于大体量的APP来说,上面所推算的成本就足以说明,千分之三远远不够。
我们自己定制了一个数据红线,不能连续七天超过万分之五。每天1万个人使用,崩溃人数不可超过5人。即使出现任何情况超过了此阈值,也要在七天内想尽各种方案解决问题,要保证这项指标的稳定。
用户第一是我们最重要的一条价值观,我们抱着不能失去任何一个的态度在进行我们的工作。当然,这个目标和标准也不是一蹴而就的。我们这个指标从2017年初开始建立,经过不断的探底深入,最终形成了现在一个稳定的阈值。
Image有了一个目标,那如何检测和收集线上数据呢?
- 开源的,可以自己搭建平台
-
QuincyKit + KSCrash
-
CrashKit
-
plcrashreporter
- 现有的服务
-
bugly
-
听云
-
友盟
-
Crashlytics
-
Flurry
我们选择是腾讯的bugly,有以下几点原因:
-
崩溃记录比较稳定,无论国内海外,服务器稳定
-
iOS/Android双端支持
-
缓存上传,不会漏掉信息
-
上传解析DSYM比较方便
-
信息全面,有UID,上传时间,崩溃发生时间等
-
接入方便,开发成本小
在解决这些问题的路上,我们总结出了一些方法和经验。
架构本身以及线程乱用所导致的Crash
遇到过很多奇怪的崩溃信息,例如
-
明明实现了方法却找不到
-
数据莫名其妙出错
在起初的修复中,只是分别治理,遇到一个地方治理一个地方,尤其是因为线程混乱导致的问题,改起来真的很头痛。
在经历了一段时间这种“有问题处理问题”临时解决方案的日子后,终于下定决心面对老工程。整理问题并分析这些问题发生的原因:网络层和数据解析层的隐患严重,其中主要由于线程混乱,服务器脏数据,以及数据未格式化彻底直接抛给业务层,导致业务层模块重,数据混乱导致的各种Crash。比较典型的是
-
数组越界
-
NSDictionary 的 key 或者 value为空
-
服务器数据结构变化
在重构之后,代码层级改为更加清晰的结构。其中,业务层只关心业务代码,发送请求。
- 网络层负责,格式化请求数据,并组织管理网络请求的生命周期。
-
重构结构,解耦请求层和数据解析层,让数据流转更清晰,结构层次更明晰
-
梳理网络层中的线程,做到一个线程只做一件事,所有相关数据不会在一个以上的线程中处理,保证代码的健壮性。
-
给每一个请求增加标识,使用实体的类名,以及请求参数的签名,做两级标识。
-
加入取消请求机制,避免网络层返回数据时,没有实体处理的问题。
但是,可以增加白名单,在统一取消请求时,不取消特定的实体,或者接口的请求。
-
加入不发送重复请求机制,避免不必要的资源浪费。
- 数据解析层,负责Json转model,以及格式化服务器数据的工作。
此层级重点解决的是服务器数据中存在脏数据,服务器数据结构变化导致的各种问题,业务层接到的数据,是完全“干净的”数据,可以放心使用。
Image缓存层的重构
重点解决线程混乱,导致脏数据,以及效率过低导致的卡顿问题。
Image在针对业务分析之后,分别尝试了WCDB和LevelDB作为底层数据库。它们都支持多线程并发读写,对于所有数据的前期格式化新开辟一个线程进行,不占用主线程资源。
最终的读取和存储都回到主线程中进行,保证存储和读写与UI保持线程和时序的一致。
经过以上的架构分析和重构,我们解决了大约60%的Crash。
Image制作工具类
崩溃多种多样,除了一个好的架构之外,很多事情都可以使用工具或者良好的封装来一次性避免。例如,使用NSObject+YGSafeAddition.h来避免NSArray和NSDictionary常见的一些问题。参考代码如下:
Image网上有很多使用MethodSwizzling替换NSArray方法的,经过线上实践验证,在iOS9系统上,系统键盘弹出时将应用切换到后台,导致替换方法崩溃,并且偶现。系统方法内部的实现会有很多意想不到的耦合情况,所以建议大家,还是老老实实写一个category,不要在工程中使用系统方法了。
面对常见的KVO错误,我们选择使用Facebook的KVOController来解决问题。避免过度释放的问题。
代码规范
Image Image
第三方SDK导致的问题
我们为了业务接入了各种第三方SDK,其中尤其是闭源的SDK中产生的崩溃最令人头痛。此时我们可以根据crash堆栈,以及用户行为(bugly看到经过的页面,用户行为日志看到调用的接口),来尽量分析还原用户崩溃时所在的场景。
如果可以通过自己解决的,先行解决,并知会相关第三方修复。如果是升级SDK导致的,我们需要衡量业务的情况下,尽量回退到稳定版本。而在引入一个新的第三方SDK时,一定要注意是否和其他第三方SDK存在冲突,无论是编译上的还是运行时。项目中曾遇到七鱼和gRPC在常量定义上的冲突,神策和有赞在UA定义上的冲突。
Image一些难缠的问题
-
iOS9大面积崩溃
这个问题在去年应该只要是iOS开发者应该都听过,xcode10打包的APP在iOS9上全面崩溃。和keep沟通,他们的选择是放弃了iOS9用户。。。相信很多同学也在网上搜索了很多方法,例如说是p3图片在Assets中导致,但是并没有起作用。最终发现,只要切换回旧编译器就好了。
-
UIWebView退到后台继续使用openGL渲染的问题
主要是因为APP进入后台以后,UIWebView继续使用openGL渲染,只要一帧就崩溃。目前我们的工程内部已经全局替换成了WKWebView,可以规避此类问题。并且Apple在接下来,也会彻底抛弃UIWebView。
-
objc_release问题
此问题上面已经说过,需要规定我们的代码规范去解决。而导致此问题的原因主要有二:
- 声明变量的时候,应该使用copy或者strong时,却错误的使用了weak或者assign,导致其在该持续持有此对象时,此对象已经被系统提前释放。
- 使用CF开头声明的,或者使用了其他底层对象,其内存管理并不归ARC管理,所以需要手动管理,这个时候如果忘记使用CFRelease等响应方法释放,就会导致对象被提前释放的情形
当时遇到此问题时,只觉得发生场景不定,发生时机不定,整体来说看不出来到底是哪里出现了问题。整体梳理了一遍工程代码之后,才发现很多老代码根本不够规范,变量声明随意,或者复制粘贴导致的错误。所以最终,将此问题重点列入了我们的代码规范和发版规范中。
上线之前一定要全局搜索property定义,避免属性定义错误的情况。很明显的一个就是对象定义成weak。如果不是特定情况,例如为了避免循环引用特别注明的,那么对象的声明都应该是strong。避免过度释放产生的object_release类型crash。
Image很多让人摸不着头脑的问题,经过仔细的分析,尤其是针对用户所在页面或者所处场景的还原,都可以稳定复现,其中:
-
堆栈信息,包含发生问题的各个线程信息
-
系统版本
-
设备型号(包括CPU架构)
-
发生时间(结合Kibana日志系统,分析用户行为)
-
应用版本
-
此版本应用所使用的IDE版本
等等一系列信息,都可以帮助我们分析解决问题。
提前发现和快速处理
调试问题和解决崩溃时,一个很重要的能力,即调试别人代码的能力,无论是开源的,还是闭源使用LLDB调试的。已经处理了这么多问题,但是随着业务增长,系统更新,新的问题必然会不断产生。如何保证在未知的情况下,继续保证线上的稳定呢?
我们主要做了以下两件事情:
-
灰度
灰度发布可以有效的在线上提前暴露出问题,无论是版本的灰度,还是单个功能的灰度。Android的灰度基本上没有限制,方案也非常成熟。而iOS的灰度,经过长时间的各种尝试后,采用了苹果爸爸提供的Testflight,参见《iOS也可以优雅地灰度》。
-
热修复
热修复可以在不发版的情况下,动态替换线上代码,以此来达到临时解决问题的目的。而iOS现在也严令禁止了JS交互热更新的方式。我们现在采用Lua脚本下发+Aspects+NSInvocation切面编程的方式来实现。很安全,苹果不会拒绝。但是此行为也不可滥用,线上救命使用而已。
总体来看,稳定性并非一蹴而就,从1%到0.02%,两个数量级的降低背后是上述的种种努力,将问题回归、整理,形成技术沉淀、适合团队工作方式、匹配产品业务的一套方法论。我们会继续将稳定的工作纳入到每天的日常工作中,为用户价值持续努力。