一个JavaScriptCore框架中对象与Timer引发的死锁问题内幕

背景介绍

在奥运项目中,快手主站的首页入口挂件、金牌时刻弹窗需要进行动态布局和渲染,因对性能要求较高,我们采用了 JavaScriptCore 与 Native 交互的技术方案。在上线灰度放量的过程中,我们发现一个长卡顿问题占比明显上升,于是我们开始排查。其最后的调用栈信息如下:

图片

经过分析排查,我们发现此问题具有以下特点:

1.Native 执行 JS 的方法时,问题发生概率较大。

2.从系统分布来看,问题只会在 iOS 13 系统出现。

3.从堆栈信息来看,无法判定问题发生的业务代码位置,只能判定是在 RunLoop 处理 Timer 相关逻辑时,如果同时有 [JSValue dealloc] 的操作,出现问题概率较大。

为了降低对线上用户的影响,我们通过服务端逻辑控制对该问题进行规避。为了深入了解其背后的原因,我们对问题内部机制进行了深入挖掘。

基于问题的特征和工程经验,技术团队认为虽然该问题只发生在 iOS 13,但同样涉及到移动端 Native 和 JavaScriptCore 交互处理中的核心问题:

1.Native 与 JavaScriptCore 框架中虚拟机的内存管控和生命周期问题。

2.Native 与 JavaScriptCore 框架中虚拟机双运行环境,单/多线程调度下的资源竞争问题。

在探寻内部具体发生机理的过程中,团队强化了对移动端 Native 和 JavaScriptCore 双运行环境场景下的死锁、内存管控、生命周期等问题的理解,提升了对 WebKit 和 JavaScriptCore 内核相关源码、苹果操作系统以及框架底层机制的治理能力。

回归“现场”

如同破案一样,面对一个新问题时,我们首先需要对“现场”进行查验。通过查看发生长卡顿的所有调用栈信息,我们认为 Main Thread  和 JavaScriptCore Heap Collector Thread(以下简称为 JSC Heap Collector Thread) 导致出现问题的嫌疑最大。

Main Thread 调用栈信息如下:

图片

JSC Heap Collector Thread 调用栈信息如下:

图片

我们初步判断的结论是:Main Thread 与 JSC Heap Collector Thread 互相等待发生了死锁。此时,主线程 RunLoop 在执行 __CFRunLoopDoTimers 内部的 AutoreleasePoolPop 相关操作,而 JSC Heap Collector Thread 则正在执行 JavaScriptCore 内部的 GC 相关逻辑。

基于以上判断思路, 我们尝试构造如下场景:

主线程 Main Thread 创建一个频率高且重复的 Timer,在 Timer Callback 中创建 JSValue 对象实例,使得在主线程的 RunLoop 内执行 CFRunLoopDoTimers 内部的 AutoreleasePoolPop 函数时能够触发内存回收的 JSValueUnprotect。此时,如果 JSC Head Collector Thread 也恰好正在执行内部 GC 相关逻辑,可能就会复现死锁现象。

基于以上分析,经过多次尝试、观测对比,我们找到了 iOS 13 可以稳定复现死锁的最小场景,核心代码逻辑如下:

图片

调用 demo_scheduleTimerWithTimeInterval 方法构造一个定时器,定时器触发时多次执行 JS 代码,在此 JS 代码内部会多次创建用于展示动画图片的 DemoAnimatedImage 视图对象。

在场景复现实验的过程中,我们发现:

在其他代码保持一致的情况下,如果不使用 demo_scheduleTimerWithTimeInterval 方法,而是使用 Foundation 框架提供的 NSTimer 初始化方法,就无法复现此死锁现象。二者唯一的不同在于 demo_scheduleTimerWithTimeInterval 内部实际上使用的是 CoreFoundation 框架的 CFTimer 。

还原死锁的场景后,我们面临着以下四个问题:

1.Main Thread 与 JSC Heap Collector Thread 是如何互相等待发生死锁的?

2.为何死锁会发生在涉及到内存回收的相关场景中?

3.为何同样是Timer,NSTimer 和 CFTimer 触发死锁问题的表现完全不同?

4.为何此问题仅在 iOS 13 系统上出现?iOS 13 系统和其他版本实现有何不同?

深入内幕

众所周知,线程间发生死锁时需要满足四个必要条件:

1.互斥条件:一个资源每次只能被一个线程使用。

2.请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。

3.不剥夺条件:线程已获得的资源,在未使用完之前,不能强行剥夺。

4.循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。

那么我们当前判断互相等待从而发生死锁的 Main Thread 与 JSC Heap Collector Thread,是否满足这四个必要条件呢?

JS执行路径追踪

对于上述四个条件,我们首先

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值