背景介绍
在奥运项目中,快手主站的首页入口挂件、金牌时刻弹窗需要进行动态布局和渲染,因对性能要求较高,我们采用了 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执行路径追踪
对于上述四个条件,我们首先