Android 15/16 Cocos2d-x V8 Crash 问题分析与 GLThread 复用解决方案

背景

修复v8::HandleScope::Initialize(v8::Isolate*)+144

接上一篇文章。由于平时更新不算频繁,加上个人也有些懒,一直没有深入思考 Cocos2d-x 2.x 在 Android 15/16 上的 V8 Crash 问题如何彻底修复

最近重新整理这个问题时,想到一个新的思路:
如何可以通过复用同一个 GLThread 来避免问题?

因为之前分析发现,Crash 的核心原因很可能来自 GLSurfaceView 在 View 重建时重新创建 GLThread。如果能够保证整个生命周期中始终使用 同一个 GLThread,也许就可以绕开这个问题。

于是开始重新阅读 GLSurfaceView 的源码,并尝试寻找可行的干预点。


源码分析

在上一篇文章中已经提到,V8 Crash 的主要原因在于:

  • Cocos2dxGLSurfaceViewView 被 remove 或 Activity 重建时
  • GLSurfaceView重新创建 GLThread
  • 导致 V8 所在线程状态异常

Cocos2dxGLSurfaceView 继承自 GLSurfaceView,在查看 GLSurfaceView 源码后发现:

GLThread 创建的入口主要有两处:

  1. setRenderer
  2. onAttachedToWindow

setRenderer 比较简单,通常只会调用一次。

onAttachedToWindow 则会在 View detach 后重新 attach 时触发,这也是 Activity 重建或 View 重新添加时的关键路径。

只要能够干预这两个位置的 GLThread 创建逻辑,就有机会避免线程被重新创建。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public void setRenderer(Renderer renderer) {
checkRenderThreadState();
//...
mRenderer = renderer;
mGLThread = new GLThread(mThisWeakRef);
mGLThread.start();
}

protected void onAttachedToWindow() {
super.onAttachedToWindow();
if (LOG_ATTACH_DETACH) {
Log.d(TAG, "onAttachedToWindow reattach =" + mDetached);
}
if (mDetached && (mRenderer != null)) {
int renderMode = RENDERMODE_CONTINUOUSLY;
if (mGLThread != null) {
renderMode = mGLThread.getRenderMode();
}
mGLThread = new GLThread(mThisWeakRef);
if (renderMode != RENDERMODE_CONTINUOUSLY) {
mGLThread.setRenderMode(renderMode);
}
mGLThread.start();
}
mDetached = false;
}


解决方案设计

基于上述分析,最终形成了三个核心思路:

1. 全局只使用一个 View

整个游戏只使用 一个 Cocos2dxSurfaceView 实例

当 Activity 切换时,不重新创建 View,而是通过 Android 提供的:

MutableContextWrapper

动态替换 View 所绑定的 Context

这样可以做到:

  • View 不销毁
  • GLThread 不重建
  • 渲染线程保持稳定

2. 破坏 onAttachedToWindow 中的线程创建

避免执行:

1
glThread = new GLThread(mThisWeakRef);

从而防止重新 attach 时创建新的渲染线程。


3. 修改 onDetachedFromWindow

原始实现中:

1
mGLThread.requestExitAndWait();

会直接退出渲染线程。

在新的方案中,将其替换为:

1
onPause()

这样只暂停渲染,而不会销毁线程。


简单测试

Cocos2dxSurfaceView 和 Renderer 单例实现

通过一个 Manager 管理全局唯一的 SurfaceView 与 Renderer。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
public class CocosSurfaceViewManager {

private static final String TAG = "CocosSurfaceViewManager";

private static CocosSurfaceViewManager instance = null;
private MutableContextWrapper mutableContextWrapper;

private CocosSurfaceViewManager() {
}

private Cocos2dxGLSurfaceView gameView;
private Cocos2dxRenderer renderer;

public static synchronized CocosSurfaceViewManager getInstance() {
if (instance == null) {
instance = new CocosSurfaceViewManager();
}
return instance;
}

public void setContext(Context context) {
if (mutableContextWrapper == null) {
mutableContextWrapper = new MutableContextWrapper(context);
} else {
mutableContextWrapper.setBaseContext(context);
}
}

public Cocos2dxGLSurfaceView getGameView() {
if (gameView == null) {
gameView = new Cocos2dxGLSurfaceView(mutableContextWrapper);
gameView.setPreserveEGLContextOnPause(true);
// Should set to transparent, or it will hide EditText
// https://stackoverflow.com/questions/2978290/androids-edittext-is-hidden-when-the-virtual-keyboard-is-shown-and-a-surfacevie
gameView.setBackgroundColor(Color.TRANSPARENT);
gameView.setEGLConfigChooser(5, 6, 5, 0, 16, 8);
// Switch to supported OpenGL (ARGB888) mode on emulator
if (isAndroidEmulator())
gameView.setEGLConfigChooser(8, 8, 8, 8, 16, 0);
renderer = new Cocos2dxRenderer();
gameView.setCocos2dxRenderer(renderer);
}
return gameView;
}

public Cocos2dxRenderer getRenderer() {
return renderer;
}

private static boolean isAndroidEmulator() {
String model = Build.MODEL;
Log.d(TAG, "model=" + model);
String product = Build.PRODUCT;
Log.d(TAG, "product=" + product);
boolean isEmulator = false;
if (product != null) {
isEmulator = product.equals("sdk") || product.contains("_sdk") || product.contains("sdk_");
}
Log.d(TAG, "isEmulator=" + isEmulator);
return isEmulator;
}

}

修改 GLSurfaceView 生命周期逻辑

最直接的方法是 复制一份 GLSurfaceView 源码,然后修改关键生命周期函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
protected void onAttachedToWindow() {
super.onAttachedToWindow();
mDetached = false;
}

@Override
protected void onDetachedFromWindow() {
if (LOG_ATTACH_DETACH) {
Log.d(TAG, "onDetachedFromWindow");
}
if (mGLThread != null) {
mGLThread.onPause();
// mGLThread.requestExitAndWait();
}
mDetached = true;
super.onDetachedFromWindow();
}

完成这一套修改后,再调整 AppActivityonCreateView

测试发现:

  • 即使在 onStop 中 remove View
  • 之前必现的 V8 Crash 不再出现

不过这种方案侵入性较强,相当于直接替换 SDK 中的 GLSurfaceView 实现。


进一步优化

为什么不直接使用继承?

一开始想到的方案其实是:

1
class BaseSurfaceView extends GLSurfaceView

但很快遇到一个问题。

GLSurfaceView 继承自 SurfaceView,如果直接 override:

1
2
onAttachedToWindow
onDetachedFromWindow

并调用 super,实际调用的是:

1
GLSurfaceView -> SurfaceView

而我们需要的是:

跳过 GLSurfaceView 的实现,直接调用 SurfaceView 的版本。

然而 Java 本身并不支持:

1
super.super.onAttachedToWindow()

经过多次尝试(甚至尝试过反射调用),最终发现 Java 层无法优雅实现这一点。咨询AI给出的方案是使用 JNI:env->CallNonvirtualVoidMethod,该函数可以直接调用父类实现。


JNI 调用 父类SurfaceView 的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// cocos/platform/android/jni/JniImp.cpp
// fix glthread
JNIEXPORT void JNICALL Java_org_cocos2dx_lib_Cocos2dxFixGLSurfaceView_nativeSurfaceViewonDetachedFromWindow(JNIEnv* env, jobject thiz) {
jclass surfaceViewClass = env->FindClass("android/view/SurfaceView");
jmethodID methodID = env->GetMethodID(surfaceViewClass, "onDetachedFromWindow", "()V");
if (methodID != nullptr) {
env->CallNonvirtualVoidMethod(thiz, surfaceViewClass, methodID);
}
env->DeleteLocalRef(surfaceViewClass);
}
JNIEXPORT void JNICALL Java_org_cocos2dx_lib_Cocos2dxFixGLSurfaceView_nativeSurfaceViewOnAttachedToWindow(JNIEnv* env, jobject thiz) {
jclass surfaceViewClass = env->FindClass("android/view/SurfaceView");
jmethodID methodID = env->GetMethodID(surfaceViewClass, "onAttachedToWindow", "()V");
if (methodID != nullptr) {
env->CallNonvirtualVoidMethod(thiz, surfaceViewClass, methodID);
}
env->DeleteLocalRef(surfaceViewClass);
}

新的 View 结构

最终方案:

  • 新建 Cocos2dxFixGLSurfaceView继承自 GLSurfaceView
  • 在 Android 15 以上系统中绕过 GLSurfaceView 的线程逻辑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class Cocos2dxFixGLSurfaceView extends GLSurfaceView {
public Cocos2dxFixGLSurfaceView(Context context) {
super(context);
}

public Cocos2dxFixGLSurfaceView(Context context, AttributeSet attrs) {
super(context, attrs);
}

@Override
protected void onAttachedToWindow() {
if (Build.VERSION.SDK_INT >= 35) {
// 1. 获取 SurfaceView 类的该方法(假设你想跳过 GLSurfaceView 直接调 SurfaceView)
nativeSurfaceViewOnAttachedToWindow();
} else {
super.onAttachedToWindow();
}
}

@Override
protected void onDetachedFromWindow() {
if (Build.VERSION.SDK_INT >= 35) {
// 1. 获取 SurfaceView 类的该方法(假设你想跳过 GLSurfaceView 直接调 SurfaceView)
nativeSurfaceViewonDetachedFromWindow();
onPause();
} else {
super.onDetachedFromWindow();
}
}

public native void nativeSurfaceViewOnAttachedToWindow();

public native void nativeSurfaceViewonDetachedFromWindow();
}

总结

核心思路是:

  • 复用 GLThread,从而考虑使用同一个GLSurfaceView
  • 避免 GLSurfaceView 在 View 状态改变时重新创建线程
  • 加入版本控制,不破坏没问题的系统版本逻辑

经过两天的调试与验证,最终形成了一套较为稳定且最小改动的解决方案:

最终实现方式包括:

  1. 全局只使用一个 GLSurfaceView
  2. 通过 MutableContextWrapper 动态切换 Activity Context
  3. 使用JNI调用super.super父类的函数,达到绕过线程创建逻辑

通过 API Level 判断 控制影响范围:

  • API 35 以下保持原逻辑
  • API 35 以上使用修复方案

这样既解决了 Android 新系统上的 V8 Crash,又避免影响旧设备的稳定性。

相比最初直接复制 GLSurfaceView 的实现,这种 继承 + JNI 的方式更加工程化,也更易维护

另外,还有些其它地方的改动,像Activity中一些判断,setEGLConfigChooser只能调用一次,替换MutableContextWrapper,AppActivity(有个单独的glSurfaceView,实际没什么用)的onCreateView替换修改等,这些具体的代码我就不全放了,如果有需要交流,欢迎联系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Cocos2dxActivity
public void init() {
// ...
// this.addSurfaceView();
Cocos2dxRenderer renderer = Build.VERSION.SDK_INT >= 35 ? this.add35SurfaceView() : this.addSurfaceView();
//...
}

private Cocos2dxRenderer add35SurfaceView() {
this.mGLSurfaceView = onCreate35View();
mFrameLayout.addView(this.mGLSurfaceView);
return CocosSurfaceViewManager.getInstance().getRenderer();
}

public Cocos2dxGLSurfaceView onCreate35View() {
return CocosSurfaceViewManager.getInstance().getGameView();
}

其实一开始,我通过反射修改mDetachedmGLThread做过简单验证,算是一个临时测试方案。可参考GLSurfaceView源码。

  1. onAttachedToWindow先反射替换mDetached = false,再调用super函数,此时if (mDetached && (mRenderer != null))就进不了了
  2. onDetachedFromWindow先反射替换mGLThread = null,再调用super函数,完成后再把mGLThread换回。