夸克 iOS Top1 JSC 崩溃攻克之旅
背景
2022 年的 3 月中旬,伴随着 iOS 15.4 系统的发布,夸克 iOS 客户端在崩溃率方面有了一波急速的上涨,严重影响了用户的使用体验。除此之外,距离上一次稳定性治理的集中推动也已经过去了很长的时间,线上也积攒了不少历史问题。因此,夸克 iOS 侧中精力持续推动了稳定性治理的工作,在清理新增问题的同时,也重点解决了长期盘踞在崩溃榜单的顽固性问题。
然后介绍一下整体稳定性治理的思路:
针对 Top 稳定性问题进行重点攻关 丰富和提升夸克现有的稳定性基建能力 长期来看,添加更有效的监控手段,防止劣化 细化崩溃指标的数据构成,明确各项的实际边界
其中,Top 问题的攻关是最能直接带来崩溃率数字下降的一项工作。我们在清理了 iOS15.4 带来的相关崩溃之后,持续推动治理了历史积攒的崩溃问题,JSC 的相关崩溃治理就是本文要介绍的,比较有代表的一个工作点。
案例分析
相关崩溃问题是在夸克浏览器 5.4.* 版本出现的问题,占据了 Top10 问题中的三个席位,并且有明显的系统版本集中现象。
问题排名 | 崩溃栈 | 系统特征 |
---|---|---|
Top1 | [JavaScriptCore]WTF::Thread::create(char const*, WTF::Function<void ()()>&&, WTF::ThreadType, WTF::Thread::QOS) | iOS15 |
Top2 | [JavaScriptCore]WTF::initializeThreading() | <= iOS13 |
Top4 | [JavaScriptCore]WTF::initialize() | iOS14 |
案例的推动解决
1. 问题的初步治理
1.1 通过崩溃信息定位问题
首先的思路是查看 crashSDK 捕获的崩溃日志,明确实际的原因。分析相关信息,我们发现以下特征
上述的三种崩溃问题是同时出现的 在不同系统版本上有聚集现象,彼此有比较明显的界限 崩溃的最后一条信息都是一致的 WTFCrashWithInfo(int, char const*, char const*, int)
基于以上的信息,可以得出的结论是:几个问题其实是同系列的问题。因为不同系统下的 javaScriptCore 的版本不同,导致 crash 平台上,对于崩溃日志聚合的差异。
通过 Top1 问题的调用栈查看,在 webkit 源代码中查看问题。是在调用过程中触发了断言引发了崩溃。按照通常的经验,该问题主要存在 2 种可能性
线程过多导致创建线程失败或者线程不安全 内存紧张
1.1.1 线程过多可能性验证
通过大量查看线上的崩溃案例信息,三种崩溃日志中体现出来的,崩溃时的线程个数达多处于 50-80 个左右。远远低于 iphone 上线程个数的风险区间,因此判定该问题和线程数目无关。
1.1.2 内存问题可能性验证
夸克当前使用的 crash SDK,目前采集了多种维度的信息,用户还原用户崩溃时整个 app 的场景。下图为在啄木鸟平台上观察崩溃用户的相关信息页面。
其中有两部分的信息对我们比较有帮助
右上角的内容区域,展示了用户崩溃时整个设备的信息 下方 自定义信息
采集了用户的页面切换、按钮操作核心操作的行为日志。并伴随着常规内存信息
通过观察 app 行为日志中 mava 反应了当前 app 可以直接使用的内存大小。它不需要动用压缩内存,是可以快速直接访问的内存空间。107MB 是一个比较中等的大小。
实际观察发现,崩溃的用户没有出现绝对的内存偏低的现象。
有相当一部分用户,在发生对应的 JSC 崩溃时,mava 参数<=60(MB) 也存在一部分用户,实际崩溃发生时,内存处于一个相对安全的水位。
基与上述的观察,夸克 iOS 侧推动了 2 轮的内存问题走查:
通过 Instrument Memory Leak
工具,查看内存泄漏问题和实际调用情况通过掌中宝调试工具观察进入页面以及功能的内存水位走势 通过 Xcode Memory Graph
分析 app 内部整体的对象泄漏以及常驻情况。
经过两次治理之后,整体的 mem 崩溃走势有所下降。但是 JSC 的崩溃未见好转。
名词解释:夸克当前将崩溃分为两种
有效崩溃:在发生崩溃时,crashSDK 能够捕获到崩溃日志的案例,例如本文要治理的案例 无日志崩溃: 在发生崩溃时,crashSDK 未能捕获到崩溃日志的案例。这种只能收集到崩溃前的用户基本行为信息。
1.2 定位引入模块,尝试降低崩溃率。
在短时间内,无法有效解决问题的情况下,判断出引入问题的功能模块,并尽可能在保障业务的情况下,对尽可能精确的问题模块做限制,降低崩溃的发生概率也是一个比较好的选择。
仔细分析出现问题的版本,发现最有可能发生问题的功能变更,可能就是 AppWorker 的升级了。
名词解释 AppWorker:AppWorker 是基于基于 JSI ( 跨端通用 JS 引擎)实现的后台 Worker。
夸克作为一个浏览器产品,封装了大量了基础功能暴露给 JS 使用。Appworker 可以借助这些能力,调度 app 内部大部分的核心能力,从而自己进行业务调度。当前夸克内部有多个业务是依赖 AppWorker 来进行实现的。
目前针对问题模块的定位方向有 3 种:
业务层的问题。这里可能是由于一些功能特定的 js 写法或者调用导致的。针对性的验证方式是小规模关闭特定功能进行验证 基础能力层的问题:这里尤其是事件通信能力。短时间高频次的 js 通信,也可能导致线程或者内存异常 JSI 层的问题。这里可能是由于特定 feature、或者接口的引入导致的问题
实际针对三个方向分别进行小规模的实现,通过开关下发 ab 对比,观察到以下现象
分别关闭当前线上的 AppWorker 支持的业务模块,崩溃有轻微下降 关闭通信能力,崩溃有轻微下降 AppWorker 在完全关闭后, 崩溃消失
通过一系列的验证说明,相关的 JSC 的崩溃与引擎层有关。验证到了这一步,已经不能进一步进行拆分验证。
由于线上业务重要性的问题,也不能完全关闭 AppWorker 来实现降低崩溃率的问题。
通过进一步观察相关崩溃的聚集表现,发现特定低端机聚集比较严重,这类用户的体验也非常受影响。经过评估之后,针对系统老旧、用户占比低但是崩溃贡献超高的群体关闭了 AppWorker。
1.3 分析
经过一系列的治理工作,得到一些阶段性结论:崩溃的引入与 AppWorker 的升级有关, 但是没能从根本上解决这个问题。
2.深挖异常信息
在针对 JSC 崩溃的问题暂时陷入了瓶颈,但是整个工程的稳定性治理始终在有条不紊的推进。其中就包括稳定基建的建设:
对接 metricKit 来实现崩溃日志的来源的扩展 通过升级 crashSDK 从 PLCrash 到 KSCrash,来优化稳定性日志的收集 对 crashSDK 持续集成新的功能,还原更多的现场信息,协助进行崩溃的定位
升级后的 crash SDK 是 UC 内核团队持续建设的一个崩溃捕获 SDK。围绕异常现场信息的捕获与展示的原始需求,UC 已经建设了一系列的基建能力。
具体功能大图如下图所示:
在 8 月份,crash SDK 加入 VMMap 的功能,并率先在夸克进行灰度,它能够使研发观察到,崩溃现场不同类型的内存使用情况,这方便我们进一步的观察内存情况,给问题的解决带来了转机。
这里的治理流程如下图所示:
2.1 问题的定位
第一步:在引入 VMMap 能力之后,再次观察分析 JSC 案例崩溃的日志。
此时就会发现一些共有的特征了:栈内存的使用极高
而一个普通的崩溃,对应的 Stack 内存信息则非常小:
在这个阶段发现一个额外的信息是,针对崩溃用户在崩溃前访问的站点进行聚合,发现一个异常的小说站点 xs635.com 迅速爬上的榜首。而在之前的阶段,Top 站点都是夸克自己的搜索业务。
第二步:尝试复现场景并栈内存观察
围绕上述线索,我们再次尝试通过 Instrument Memory Leak
分析栈内存的使用情况。
通过对该小说站点的反复访问,我们发现,在该站点的滑动、页面切换均会造成栈内存泄漏,并且永不释放。
检索一下对应符号,发现来源于某安全组件的的一个 SDK
__FCZLb7vLCQLWhr
-[__NSSingleObjectSetI enumerateObjectsWithOptions:usingBlock:]
__FCyaaEyd5fwkLR
第三步
在 debug 环境正常运行,针对相关符号添加断点,观察 Xcode 断点捕获,发现主要泄漏点的调用,之前都会触发 UIPasteboard 的调用,并通过 NSNotification 通知进行传导。
进一步进行验证,Hook 掉 NSNotification 的通知,在控制台输出一些通知的名称,查看泄漏符号调用前触发的通知,发现主要源头是 剪切板的内容变更。进一步验证之后,发现前后台切换的系统通知同样符合筛选的条件。
再继续说一下相关的异常站点:夸克作为一个浏览器,会碰到很多极端的站点。相信很多人有遇到过,例如在用户进行操作后,疯狂进行广告站点的跳转、copy 特定信息到手机剪切板上。本案例就是该网站每秒进行一次内容 copy 操作,最后结合泄漏场景导致内存炸了。
2.2 问题的验证与解决
上述的操作可以导致栈内存泄漏,但是依旧不能证明该问题间接导致了 JSC 的相关崩溃。接下来就是需要进行问题的验证与解决。
第一步:泄漏问题与崩溃的直接关联性 首先尝试本地复现:在本地 debug 模式下, 批量进行剪切板赋值操作。在低端机型上可以验证出相关的崩溃,这证明它们是触发原因,起码是原因之一。
第二步:验证问题的全面性,即解决上述问题是否可以根绝 JSC 的崩溃。
为了追实效性,快速验证当前的治理方略是否全面有效,因此决定进行线上实验。
崩溃 url:指 app 在崩溃前,最后访问的 url
尝试针对崩溃 url 中,占比比较高的小说站点进行实验,禁用对应站点的剪切板操作能力,以期达到下面两个目的:
明确当前的解法可以完整的解决线上崩溃,没有遗漏项 缓解线上崩溃率,优化用户体验
具体禁用的策略,就是 hook 掉 js 中执行剪切板操作的几个核心接口。
剪切板相关接口
navigator.clipboard.write
navigator.clipboard.writeText
document.execCommand("copy")
最终执行 js
window.navigator["clipboard"]["write"]=function(){};
window.navigator["clipboard"]["writeText"]=function(strnum){};
document.execCommandCopy = document.execCommand;
document["execCommand"] = function(commandparam){
if (commandparam == "copy"){}
else
{
document.execCommandCopy(commandparam);
}
}
该脚本下发后,发现相关站点已经没有出现在崩溃案例中了,符合实验的预期。
第三步 升级问题安全组件,从根本上解决问题
当前使用的问题版本的问题安全组件 **** 6.5.11
已经是一个两年前的古老版本了。与当前最新版本差距也比较大。回顾版本老旧的原因,是因为当前大量的业务模块直接或间接使用到该安全组件,涉及到大量的回归成本,因此通常没有新功能上的需求,是不会推动类似重量级 sdk 的升级的。
实际将 sdk 升级到最新之后,相应的场景不再出现泄漏问题。新版本 sdk 已经没有类似的问题了。
3. 分析与反思
从上一个部分中,我们已经确定相关 JSC 的崩溃是由栈内存泄漏导致的。但是内存泄漏问题的治理,带来的收益应该不止于 JSC 崩溃在崩溃数字上的影响。
前文中讲到,夸克当前把崩溃粗略的分为两种。实际在夸克中分布如下图所示:
其中无日志崩溃中,内存崩溃是其中的一个最大的一个类型。在我们治理 JSC 崩溃,带来有效崩溃下降的同时,对于内存崩溃肯定也会有所缓解,因为这本身就是一个内存问题。
进一步分析这个崩溃:这是一个拔了萝卜带着泥的问题。萝卜是存在崩溃日志的 JSC 崩溃, 泥是潜藏的内存泄漏本身。我们在 AppWorker 升级之后,面对爆发的 JSC 有效崩溃进而投入精力进行治理,但是在这之前,问题安全组件的内存泄漏现象就存在一段时间了。在治理该崩溃问题的同时,影响面更大的应该是内存方面的提升。
4. 效果展示
8 月中下旬上线了新的修复版本之后, 整体崩溃率的下降非常非常明显。并且随着版本放量,崩溃率在持续的缓慢下行。
总结与展望
夸克 iOS 侧的稳定性问题,经过持续的治理,整体的稳定性获得了极大的提升。在整个过程中,稳定性相关的基建能力均得到了不同程度的提升,大量长期盘踞在崩溃榜单的顽疾得到治理。本文阐述了治理过程中一个比较有代表性的案例:JSC 相关的崩溃治理。
问题存在的时间比较久远了,根本性的问题(内存问题)在 JSC 崩溃出现前就已经存在。综合案例治理过程,我有以下几点认识:
在 AppWorker 升级后触发的崩溃问题,是因为内存紧张触发了 Webikit 内部的异常。它是两个因素相互作用之后,将一直难以观察的栈泄漏问题显性化了,也为后续 VMMap 的内存获取提供了绝佳的时间节点; 内存问题出现在栈内存而非常见的堆内存,也触及了惯性思维的盲区。前期用户行为的难以聚合也加大了观察的难度; 科技还是第一生产力。持续的基础建设、不断丰富的场景信息在持续的推动和加速着一系列疑难问题的解决。
反思案例中核心问题的解决,还是存在一定的偶然性:如果没有 JSC 有效崩溃的出现,目前的基建不足以支撑我们定位到当前案例中的内存异常。
我们要展望的是如何把过程中的偶然变成必然——建设内存动态监控的能力,针对常见的大内存、持续的内存波动,可以实现内存的监控和对象的还原。针对常见的内存崩溃,可以还原崩溃某时间节点内的 vmmap 内存信息解析,结合用户的行为日志进行内存分析。