背景

2022 年的 3 月中旬,伴随着 iOS 15.4 系统的发布,夸克 iOS 客户端在崩溃率方面有了一波急速的上涨,严重影响了用户的使用体验。除此之外,距离上一次稳定性治理的集中推动也已经过去了很长的时间,线上也积攒了不少历史问题。因此,夸克 iOS 侧中精力持续推动了稳定性治理的工作,在清理新增问题的同时,也重点解决了长期盘踞在崩溃榜单的顽固性问题。
22 年 3 月上旬崩溃率走势
然后介绍一下整体稳定性治理的思路:
  • 针对 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 平台上,对于崩溃日志聚合的差异。
JSC 崩溃栈相关信息
通过 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 来进行实现的。
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 已经建设了一系列的基建能力。
具体功能大图如下图所示:
Crash SDK 相关基建大图
在 8 月份,crash SDK 加入 VMMap 的功能,并率先在夸克进行灰度,它能够使研发观察到,崩溃现场不同类型的内存使用情况,这方便我们进一步的观察内存情况,给问题的解决带来了转机。
这里的治理流程如下图所示:

2.1 问题的定位

第一步:在引入 VMMap 能力之后,再次观察分析 JSC 案例崩溃的日志。
此时就会发现一些共有的特征了:栈内存的使用极高
崩溃日志中,VMMap 展示内存占用情况
而一个普通的崩溃,对应的 Stack 内存信息则非常小:
在这个阶段发现一个额外的信息是,针对崩溃用户在崩溃前访问的站点进行聚合,发现一个异常的小说站点 xs635.com 迅速爬上的榜首。而在之前的阶段,Top 站点都是夸克自己的搜索业务。
第二步:尝试复现场景并栈内存观察
围绕上述线索,我们再次尝试通过 Instrument Memory Leak 分析栈内存的使用情况。
通过对该小说站点的反复访问,我们发现,在该站点的滑动、页面切换均会造成栈内存泄漏,并且永不释放。
Instrument 查看内存泄漏截图
检索一下对应符号,发现来源于某安全组件的的一个 SDK
相关符号
__FCZLb7vLCQLWhr

-[__NSSingleObjectSetI enumerateObjectsWithOptions:usingBlock:]

__FCyaaEyd5fwkLR

第三步
在 debug 环境正常运行,针对相关符号添加断点,观察 Xcode 断点捕获,发现主要泄漏点的调用,之前都会触发 UIPasteboard 的调用,并通过 NSNotification 通知进行传导。
进一步进行验证,Hook 掉 NSNotification 的通知,在控制台输出一些通知的名称,查看泄漏符号调用前触发的通知,发现主要源头是 剪切板的内容变更。进一步验证之后,发现前后台切换的系统通知同样符合筛选的条件。
再继续说一下相关的异常站点:夸克作为一个浏览器,会碰到很多极端的站点。相信很多人有遇到过,例如在用户进行操作后,疯狂进行广告站点的跳转、copy 特定信息到手机剪切板上。本案例就是该网站每秒进行一次内容 copy 操作,最后结合泄漏场景导致内存炸了。

2.2 问题的验证与解决

上述的操作可以导致栈内存泄漏,但是依旧不能证明该问题间接导致了 JSC 的相关崩溃。接下来就是需要进行问题的验证与解决。
第一步:泄漏问题与崩溃的直接关联性 首先尝试本地复现:在本地 debug 模式下, 批量进行剪切板赋值操作。在低端机型上可以验证出相关的崩溃,这证明它们是触发原因,起码是原因之一。
第二步:验证问题的全面性,即解决上述问题是否可以根绝 JSC 的崩溃。
为了追实效性,快速验证当前的治理方略是否全面有效,因此决定进行线上实验。
崩溃 url:指 app 在崩溃前,最后访问的 url
尝试针对崩溃 url 中,占比比较高的小说站点进行实验,禁用对应站点的剪切板操作能力,以期达到下面两个目的:
  1. 明确当前的解法可以完整的解决线上崩溃,没有遗漏项
  2. 缓解线上崩溃率,优化用户体验
具体禁用的策略,就是 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 崩溃出现前就已经存在。综合案例治理过程,我有以下几点认识:
  1. AppWorker 升级后触发的崩溃问题,是因为内存紧张触发了 Webikit 内部的异常。它是两个因素相互作用之后,将一直难以观察的栈泄漏问题显性化了,也为后续 VMMap 的内存获取提供了绝佳的时间节点;
  2. 内存问题出现在栈内存而非常见的堆内存,也触及了惯性思维的盲区。前期用户行为的难以聚合也加大了观察的难度;
  3. 科技还是第一生产力。持续的基础建设、不断丰富的场景信息在持续的推动和加速着一系列疑难问题的解决。
反思案例中核心问题的解决,还是存在一定的偶然性:如果没有 JSC 有效崩溃的出现,目前的基建不足以支撑我们定位到当前案例中的内存异常。
我们要展望的是如何把过程中的偶然变成必然——建设内存动态监控的能力,针对常见的大内存、持续的内存波动,可以实现内存的监控和对象的还原。针对常见的内存崩溃,可以还原崩溃某时间节点内的 vmmap 内存信息解析,结合用户的行为日志进行内存分析。
继续阅读
阅读原文