Cocos2.x版本 HandleScope::Initialize(v8::Isolate*) 问题分析

问题背景

公司使用 Cocos 2.4.15 开发游戏时,发现一个 crash 在 Android 15 以上的设备上非常频繁。起初以为是 16KB 的兼容性问题,但分析后发现包其实是兼容 16KB 的。

在 Cocos 社区中,也有相关讨论:v8::HandleScope::Initialize(v8::Isolate*)。官方曾提供过基于 Activity 在 onDestroy 时的修复方案,但我们发现还有其它触发场景。

Crash 日志示例:

1
2
(v8::HandleScope::Initialize(v8::Isolate*)+144) (BuildId: a222c0a549189d49b84a5433428e92f58cba47f0)
(se::AutoHandleScope::AutoHandleScope()+36) (BuildId: a222c0a549189d49b84a5433428e92f58cba47f0)

简单分析 crash 的代码片段如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
void ScriptEngine::cleanup()
{
SE_LOGD("ScriptEngine::cleanup");
if (!_isValid)
return;
SE_LOGD("ScriptEngine::cleanup begin ...\n");
_isInCleanup = true;

{
AutoHandleScope hs;
// ...
}
}

问题就出在这一行:

1
AutoHandleScope hs;

查看 AutoHandleScope 构造函数可以发现,它会创建一个 _handleScope 并传入 v8::Isolate::GetCurrent()

1
2
3
4
5
6
7
8
9
10
public:
AutoHandleScope()
: _handleScope(v8::Isolate::GetCurrent())
{
}
~AutoHandleScope()
{
}
private:
v8::HandleScope _handleScope;

如果 v8::Isolate::GetCurrent() 返回 nullptr,就会直接导致 crash。

问题原因

经过分析发现,当 GLSurfaceView 被回收后,Render 线程也会随之销毁。下次再创建新的 GLThread 时,由于 V8 的 Isolate 是线程独立的,第二次进入 ScriptEngine::init() -> cleanup() -> AutoHandleScope hs 时,v8::Isolate::GetCurrent() 就会返回 nullptr,从而触发 crash。

修复方案

尝试过的几种方法:

  1. **绕过 AutoHandleScope hs**:不可行,会破坏 V8 资源管理。
  2. 传入之前的 Isolate:也不可行,因为 Isolate 与线程绑定,线程已销毁。

最终,官方提供的方案是:当 Activity 的 onDestroy 被调用时,直接结束进程(调用 Cocos2dxHelper.terminateProcess())。考虑到该 bug 主要在 Android 15 和 16 上,我们加了版本判断:

1
if (Build.VERSION.SDK_INT >= 35) Cocos2dxHelper.terminateProcess();

通过直接杀掉进程,可以彻底避免 crash。

其他触发场景

Activity onDestroy 的问题解决后,仍然存在另一个触发点:GLSurfaceView.onDetachFromWindow。也就是当调用 parent.removeView(glView)parent.addView(glView) 时,同样可能触发 crash。

解决方法同样可以是直接在 onDetachFromWindow 时结束进程。对于大部分游戏来说,removeView 多发生在后台或者资源已回收的情况下,杀掉进程并重新启动并不会影响用户体验。

但在接入某些第三方 SDK 时,如果 SDK 在启动时将 root view 加入它们的容器中,就可能在启动时就被杀掉,这也是我们后来放弃在 2.x 版本接入该 SDK 的原因。Cocos 3.x 则没有这个问题。

可能的优化方向

可以尝试通过控制线程或 Render 的生命周期,复用之前的 Render 或线程,避免 Isolate 为空的情况。这个方向仍需要进一步实验。