背景
修复v8::HandleScope::Initialize(v8::Isolate*)+144
接上一篇文章。由于平时更新不算频繁,加上个人也有些懒,一直没有深入思考 Cocos2d-x 2.x 在 Android 15/16 上的 V8 Crash 问题如何彻底修复。
最近重新整理这个问题时,想到一个新的思路:
如何可以通过复用同一个 GLThread 来避免问题?
因为之前分析发现,Crash 的核心原因很可能来自 GLSurfaceView 在 View 重建时重新创建 GLThread。如果能够保证整个生命周期中始终使用 同一个 GLThread,也许就可以绕开这个问题。
于是开始重新阅读 GLSurfaceView 的源码,并尝试寻找可行的干预点。
源码分析
在上一篇文章中已经提到,V8 Crash 的主要原因在于:
Cocos2dxGLSurfaceView 在 View 被 remove 或 Activity 重建时
GLSurfaceView 会 重新创建 GLThread
- 导致 V8 所在线程状态异常
Cocos2dxGLSurfaceView 继承自 GLSurfaceView,在查看 GLSurfaceView 源码后发现:
GLThread 创建的入口主要有两处:
setRenderer
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();
|
会直接退出渲染线程。
在新的方案中,将其替换为:
这样只暂停渲染,而不会销毁线程。
简单测试
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); gameView.setBackgroundColor(Color.TRANSPARENT); gameView.setEGLConfigChooser(5, 6, 5, 0, 16, 8); 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();
} mDetached = true; super.onDetachedFromWindow(); }
|
完成这一套修改后,再调整 AppActivity 的 onCreateView。
测试发现:
- 即使在
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
|
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) { nativeSurfaceViewOnAttachedToWindow(); } else { super.onAttachedToWindow(); } }
@Override protected void onDetachedFromWindow() { if (Build.VERSION.SDK_INT >= 35) { nativeSurfaceViewonDetachedFromWindow(); onPause(); } else { super.onDetachedFromWindow(); } }
public native void nativeSurfaceViewOnAttachedToWindow();
public native void nativeSurfaceViewonDetachedFromWindow(); }
|
总结
核心思路是:
- 复用 GLThread,从而考虑使用同一个GLSurfaceView
- 避免 GLSurfaceView 在 View 状态改变时重新创建线程
- 加入版本控制,不破坏没问题的系统版本逻辑
经过两天的调试与验证,最终形成了一套较为稳定且最小改动的解决方案:
最终实现方式包括:
- 全局只使用一个
GLSurfaceView
- 通过
MutableContextWrapper 动态切换 Activity Context
- 使用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
| public void init() { 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(); }
|
其实一开始,我通过反射修改mDetached和mGLThread做过简单验证,算是一个临时测试方案。可参考GLSurfaceView源码。
onAttachedToWindow先反射替换mDetached = false,再调用super函数,此时if (mDetached && (mRenderer != null))就进不了了
onDetachedFromWindow先反射替换mGLThread = null,再调用super函数,完成后再把mGLThread换回。