Cocos2dx-js v3でのSpiderMonkeyのVMの再起動のコツ
感想
正しいスレッドを使うこととメモリ解放は人類には早すぎる。
これなんですか
昔に作ったCocos2dx-jsアプリをメンテナンスしてる人で、Activityの再起動問題(実際はGLTreadの変更)に対応しようとしている少数の人の向け。きっと10年ぐらい前に作ったアプリをメンテナンスしてる人向けで、もう、とっくにフレームワークのメンテナンスも止まっている。
アプリが動いてると時代の流れに対応が必要になることもあって、最近、AndroidでAPI Level 35あたりからアクティビティの再起動への対応を促すため警告などでて対応への圧力が強まっている。36ではまだ回避できるのですが、Level 37あたりからは再起動対応をしないとに落ちる場合もありそう。(ゲームカテゴリーなら許されるフラグはできそう。)
アクティビティの再起動が起こる時などにGLSurefaceViewが再作成される。するとGLTreadが変わり、そうするとJavascript EngineであるSpiderMonkeyの再起動が必要になる。しないとちょっと動いてアプリが落ちる。
SpiderMonkeyの再起動に対応に成功したという記事もなく多くの人がうまくいかなかったと思うのですが、自分自身も長年、「これを直すは無理」と思っていました。3日、Claude CodeとChatGPTを問い詰めて直したところ、3.17.12のデフォルトのプロジェクトで対応できたので簡単な覚書です。
最大のはまりどころ
SpiderMonkeyのVMを再起動するにはどうすればいいのだろうか。
ネットにはよく、こうすればいいと書いてある。
auto engine = ScriptingCore::getInstance();
engine->cleanup();
engine->start();
engine->runScript("main.js");
これ自体は間違いでないのだが3つ問題がある。
スレッドの問題
メモリの取得と解放は同じスレッドで行う必要があります。
そのためGLTreadが変わる時に上記を一箇所で行うと落ちる。
①cleanup()は、元のGLThreadで行う必要がある。
取得したスレッドで解放が必要だから。
②start()は、新しいGLThreadで行う必要がある。
これからこのスレッドで管理するため。
よって、cleanup()はGLTreadの破棄前、Start()は再起動後。
cleanupの問題
GLTreadで作ったものをどの範囲で解放するか。
vmのStartの問題
どう起動するのか。
スレッドの問題について
cleanup()のタイミング
結論としては、surfaceDestroyed()にてcleanup()処理を行う。
実際、onPause()とsurfaceDestroyed()の後に新アクティビティが構築される場合がある。
(片方だけの時、両方呼ばれる時がある。)
onPause()はglSurfaceView.setPreserveEGLContextOnPause(true)としておいて、コンテキストを保持するようにすでに設定されている。(それでもpauseからアクティビティ再起動と場合があるようだが少数の機種なので無視する。)
また、前提としてできるだけアクティビティーの再起動が起きないように配慮しておく。
具体的には、minifestのandroid:configChangesでorientationやscreenSizeなど、できるだけ多く指定しておく。
※検証ではconfigChangesでorientationやscreenSizeを無効にしてアクティビティが変更されるようにしてonPauseの後に再起動が行われるようにしてテストを行った。
start()のタイミング
onSurfaceCreatedでGLThreadが変わったらstartすれば良い。
cleanupの問題
何を対象にメモリ解放するのか、JavaScriptで使ったものを全部解放すればいい。
細かい部分はバージョンにより異なるかも。
cocos2dx-jsの3.17.2では以下のcleanup関数とした。
なお、Scene graphのクリーンナップは各自実装ください。
void ScriptingCore::safeCleanup()
{
cleanAllScript();
removeAllRoots(_cx);
if (_cx) {
JS_DestroyContext(_cx);
_cx = nullptr;
}
if (_rt) {
JS_DestroyRuntime(_rt);
_rt = nullptr;
}
_js_global_type_map.clear();
// Clean scene graph
auto director = Director::getInstance();
if (director->getRunningScene()) {
director->getActionManager()->removeAllActions();
director->getScheduler()->unscheduleAll();
director->popToRootScene();
}
director->getTextureCache()->removeAllTextures();
FileUtils::getInstance()->purgeCachedEntries();
_needCleanup = false;
}
vmのStartの問題
再起動のために起動処理を参考に作成。ソースだけ。
JNIEXPORT void JNICALL Java_org_cocos2dx_lib_Cocos2dxRenderer_nativeRestartSpiderMonkeyVM(JNIEnv* env, jclass clazz) {
LOGI("=== SpiderMonkey VM Restart Started ===");
auto sc = ScriptingCore::getInstance();
if (sc) {
sc->start();
sc->runScript("script/jsb_boot.js");
sc->runScript("main.js");
JSContext* cx = sc->getGlobalContext();
if (cx) {
ScriptingCore::forceGC(cx, 0, nullptr);
}
LOGI("=== SpiderMonkey VM Restart Completed ===");
} else {
LOGI("ScriptingCore instance not found");
}
}
最後に
後は、VM上のデーターをC++の領域かストレージに退避してvm再起動して戻してUIの状態を構築することが必要です。
と思っていましがまだ入り口にも到達していませんでした。
上のプログラムは間違えです。動きません。
サンプルプロジェクトでテストした時はうまく行ったと思ったのですが、実プロジェクトに適用するとうまく動きませんでした。
今見ると着眼点は良いのですが、色々、
①解放すべきものが足りてない。
②古いGLTreadの情報が残ってる。
などなど。
うまく動かない部分の真の原因をもとめてAIを使ってエラーログを解析しプログラムを書いてはテストを行い幾つもの障害を乗り越えて時には変な道に迷い込んで最終的には動くようになりました。
1週間ほどAIでなぜ動かないのか、仮説検証を数多く行い、コードを生成しては検証してうまくいかなければ破棄することを繰り返しました。とても人間業ではありません。さすがAI。
解説すると終わりそうにないので、修正点のドキュメントを以下、置いておきます。
AIに読ませて修正を再現できるように、コード差分がわかるように生成したドキュメントとなります。
Cocos2d-x 3.17.2 SpiderMonkey VM Restart 実装ガイド
📋 完全実装チェックリスト
- ✅ Priority A: ActionManager復旧、GLThread検出、Thread-safe Context管理、GLProgramStateCache再初期化
- ✅ Priority B: LayerColor対応、AppDelegate統合管理、Weak Reference設計
- ✅ Priority C: JavaScript包括エラーハンドリング、再帰防止
Table of Contents
- 実装優先度マトリックス
- Priority A: 必須実装(クラッシュ回避)
- Priority B: 推奨実装(UX向上)
- Priority C: 安全性向上(運用安定)
- 完全実装チェックリスト
- 検証とトラブルシューティング
実装優先度マトリックス
🔥 Priority A: 必須実装(クラッシュ回避)
| 項目 | 効果 | 実装工数 | 影響度 |
|---|---|---|---|
| ActionManager Scheduler再登録 | Action実行システム復旧 | 1分 | ⭐⭐⭐⭐⭐ |
| GLThread変更検出システム | VM restart トリガー | 10分 | ⭐⭐⭐⭐⭐ |
| Thread-safe Context管理 | SpiderMonkey制約対応 | 20分 | ⭐⭐⭐⭐⭐ |
| GLProgramStateCache再初期化 | OpenGL状態同期 | 2分 | ⭐⭐⭐⭐ |
⚡ Priority B: 推奨実装(UX向上)
| 項目 | 効果 | 実装工数 | 影響度 |
|---|---|---|---|
| LayerColor EVENT_RENDERER_RECREATED対応 | TransitionFade安定化 | 15分 | ⭐⭐⭐⭐ |
| AppDelegate包括管理 | Surface/VM restart統合 | 30分 | ⭐⭐⭐ |
| Weak Reference設計 | モジュール疎結合 | 10分 | ⭐⭐⭐ |
| ※ LayerColor EVENT_RENDERER_RECREATED対応は必要ないかも? |
🛡️ Priority C: 安全性向上(運用安定)
| 項目 | 効果 | 実装工数 | 影響度 |
|---|---|---|---|
| Screen Size Change再帰防止 | 無限ループ防止 | 15分 | ⭐⭐ |
| 包括エラーハンドリング | 予期しない障害保護 | 45分 | ⭐⭐ |
Priority A: 必須実装(クラッシュ回避)
1. ActionManager Scheduler再登録
問題: VM restart後、ActionManagerがSchedulerから外され、全Actionが停止
解決: AppDelegate でのVM restart時にActionManagerを再スケジューリング
注意: この問題はActionを多用するアプリで特に重要です。Actionを使用しないアプリでは影響が少ない場合があります。
1.1 AppDelegate.h 修正
ファイル: frameworks/runtime-src/Classes/AppDelegate.h
#include "cocos2d.h"
USING_NS_CC;
class AppDelegate : public cocos2d::Application
{
public:
AppDelegate();
virtual ~AppDelegate();
virtual void initGLContextAttrs();
virtual bool applicationDidFinishLaunching();
virtual void applicationDidEnterBackground();
virtual void applicationWillEnterForeground();
// 🎯 VM Restart 包括管理メソッド追加
static void handleSurfaceDestroyedCleanup();
private:
};
1.2 AppDelegate.cpp 修正
ファイル: frameworks/runtime-src/Classes/AppDelegate.cpp
必要なヘッダーをインクルード:
#include "AppDelegate.h"
#include "CCApplication.h"
#include "audio/include/SimpleAudioEngine.h"
#include "scripting/js-bindings/manual/ScriptingCore.h"
#include <thread>
#include "base/CCEventType.h"
#include "base/CCEventDispatcher.h"
VM restart処理関数を追加:
// 🎯 新規追加: VM restart処理関数
extern "C" void handleAppDelegateVMRestart() {
auto sc = ScriptingCore::getInstance();
if (sc) {
sc->forceGLThreadRestart();
sc->createGlobalContext();
// 🎯 重要: ActionManager復旧
auto director = Director::getInstance();
if (director) {
director->getScheduler()->scheduleUpdate(director->getActionManager(), Scheduler::PRIORITY_SYSTEM, false);
}
// JavaScript環境復旧
if (sc->runScript("script/jsb_boot.js")) {
sc->runScript("main.js");
} else {
CCLOG("AppDelegate VM restart - jsb_boot.js failed");
}
} else {
CCLOG("AppDelegate VM restart - No ScriptingCore instance");
}
}
void AppDelegate::handleSurfaceDestroyedCleanup() {
auto sc = ScriptingCore::getInstance();
if (sc) {
sc->safeCleanup();
}
}
// 🎯 JNI層から呼び出される関数
extern "C" void handleAppDelegateSurfaceDestroyed() {
AppDelegate::handleSurfaceDestroyedCleanup();
}
2. Surface Lifecycle + ThreadID変更検出システム(重要な連携)
核心概念: Surface Destroyed時のsafeCleanup()とSurface Created時のThreadID変更検出によるVM restartの連携がシステムの心臓部
2.0 実装フロー概要
🎯 重要ポイント:
-
Surface Destroyed → 必ず
safeCleanup()実行でJavaScript環境完全クリア -
Surface Created + ThreadID変更 → 必ず
VM restart実行で環境再構築 - この2段階処理により画面回転時のクラッシュ完全回避
2.1 Surface Destroyed処理:safeCleanup()呼び出し実装
2.1.1 Java層:handleSurfaceDestroyed()
ファイル: frameworks/cocos2d-x/cocos/platform/android/java/src/org/cocos2dx/lib/Cocos2dxRenderer.java
メソッド: handleSurfaceDestroyed()に追加
public void handleSurfaceDestroyed() {
if (!mNativeInitCompleted) {
android.util.Log.i("Cocos2dxRenderer", "handleSurfaceDestroyed: Native not initialized, skipping");
return;
}
String currentThreadName = Thread.currentThread().getName();
android.util.Log.i("Cocos2dxRenderer", "handleSurfaceDestroyed: Calling native cleanup on thread: " + currentThreadName);
// 🎯 Surface破棄時に必ずsafeCleanup()実行のためJNI呼び出し
Cocos2dxRenderer.nativeOnSurfaceDestroyed();
android.util.Log.i("Cocos2dxRenderer", "handleSurfaceDestroyed: Native cleanup completed");
}
2.1.2 JNI層:nativeOnSurfaceDestroyed()
ファイル: frameworks/cocos2d-x/cocos/platform/android/jni/Java_org_cocos2dx_lib_Cocos2dxRenderer.cpp
メソッド: Java_org_cocos2dx_lib_Cocos2dxRenderer_nativeOnSurfaceDestroyed()に追加
JNIEXPORT void JNICALL Java_org_cocos2dx_lib_Cocos2dxRenderer_nativeOnSurfaceDestroyed() {
cocos2d::log("nativeOnSurfaceDestroyed: Surface destroyed - checking for cleanup function");
// 🎯 AppDelegate cleanup関数が利用可能な場合のみ呼び出し(Weak Reference設計)
if (handleAppDelegateSurfaceDestroyed) {
cocos2d::log("nativeOnSurfaceDestroyed: Calling AppDelegate cleanup");
handleAppDelegateSurfaceDestroyed();
cocos2d::log("nativeOnSurfaceDestroyed: AppDelegate cleanup completed");
} else {
cocos2d::log("nativeOnSurfaceDestroyed: No cleanup function available");
}
}
2.1.3 AppDelegate層:handleSurfaceDestroyedCleanup()
ファイル: frameworks/runtime-src/Classes/AppDelegate.cpp
メソッド: handleSurfaceDestroyedCleanup()に追加
void AppDelegate::handleSurfaceDestroyedCleanup() {
// 🎯 Step 1: JavaScript環境からのデータバックアップ収集
CCLOG("AppDelegate - Starting backup data collection");
collectBackupDataFromJS();
// 🎯 Step 2: ScriptingCoreのsafeCleanup()実行(130行の完全クリーンアップ)
auto sc = ScriptingCore::getInstance();
if (sc) {
sc->safeCleanup();
}
CCLOG("AppDelegate - Surface destroyed cleanup completed");
}
// 🎯 JNI層から呼び出される外部C関数
extern "C" void handleAppDelegateSurfaceDestroyed() {
AppDelegate::handleSurfaceDestroyedCleanup();
}
2.2 Surface Created処理:ThreadID変更検出→VM restart実装
2.2.1 Java層:onSurfaceCreated()でThreadID変更検出
ファイル: frameworks/cocos2d-x/cocos/platform/android/java/src/org/cocos2dx/lib/Cocos2dxRenderer.java
追加するメンバー変数:
public class Cocos2dxRenderer implements GLSurfaceView.Renderer {
// 🎯 静的変数で永続的なGLThread追跡(画面回転でも保持)
private static long sLastKnownGLThreadId = -1;
private boolean mNativeInitCompleted = false;
// 既存メンバー変数...
private int mScreenWidth;
private int mScreenHeight;
private long mLastTickInNanoSeconds;
メソッド: onSurfaceCreated()に追加(ThreadID変更検出→VM restart実行)
@Override
public void onSurfaceCreated(final GL10 GL10, final EGLConfig EGLConfig) {
long currentThreadId = Thread.currentThread().getId();
boolean glThreadChanged = false;
// 🎯 GLThread変更検出(初回起動は除外)
if (sLastKnownGLThreadId != -1 && sLastKnownGLThreadId != currentThreadId) {
android.util.Log.i("VM_RESTART", "GLThread changed: " + sLastKnownGLThreadId + " -> " + currentThreadId);
glThreadChanged = true;
}
// Cocos2d-x通常初期化
Cocos2dxRenderer.nativeInit(this.mScreenWidth, this.mScreenHeight);
this.mLastTickInNanoSeconds = System.nanoTime();
mNativeInitCompleted = true;
// 🎯 重要:GL初期化後にVM restart実行(ThreadID変更時のみ)
if (glThreadChanged) {
Cocos2dxRenderer.nativeRestartSpiderMonkeyVM();
}
sLastKnownGLThreadId = currentThreadId;
}
2.2.2 JNI層:nativeRestartSpiderMonkeyVM()
ファイル: frameworks/cocos2d-x/cocos/platform/android/jni/Java_org_cocos2dx_lib_Cocos2dxRenderer.cpp
追加するWeak Reference宣言:
// 🎯 AppDelegate関数への弱参照(モジュール疎結合)
extern "C" __attribute__((weak)) void handleAppDelegateSurfaceDestroyed();
extern "C" __attribute__((weak)) void handleAppDelegateVMRestart();
メソッド: Java_org_cocos2dx_lib_Cocos2dxRenderer_nativeRestartSpiderMonkeyVM()に追加
JNIEXPORT void JNICALL Java_org_cocos2dx_lib_Cocos2dxRenderer_nativeRestartSpiderMonkeyVM() {
cocos2d::log("nativeRestartSpiderMonkeyVM: Restarting SpiderMonkey VM - checking for restart function");
// 🎯 AppDelegate VM restart関数が利用可能な場合のみ呼び出し(Weak Reference設計)
if (handleAppDelegateVMRestart) {
cocos2d::log("nativeRestartSpiderMonkeyVM: Calling AppDelegate VM restart");
handleAppDelegateVMRestart();
cocos2d::log("nativeRestartSpiderMonkeyVM: AppDelegate VM restart completed");
} else {
cocos2d::log("nativeRestartSpiderMonkeyVM: No VM restart function available");
}
}
2.2.3 AppDelegate層:handleAppDelegateVMRestart()
ファイル: frameworks/runtime-src/Classes/AppDelegate.cpp
メソッド: handleAppDelegateVMRestart()に追加
// 🎯 新規追加: VM restart処理関数
extern "C" void handleAppDelegateVMRestart() {
auto sc = ScriptingCore::getInstance();
if (sc) {
// Step 1: GLThread restart強制フラグ設定
sc->forceGLThreadRestart();
// Step 2: JavaScript Context再構築
sc->createGlobalContext();
// 🎯 Step 3: ActionManager復旧(重要)
auto director = Director::getInstance();
if (director) {
director->getScheduler()->scheduleUpdate(director->getActionManager(), Scheduler::PRIORITY_SYSTEM, false);
CCLOG("AppDelegate VM restart - ActionManager restored");
}
// Step 4: JavaScript環境復旧(jsb_boot.js → main.js)
if (sc->runScript("script/jsb_boot.js")) {
sc->runScript("main.js");
CCLOG("AppDelegate VM restart - JavaScript environment restored");
} else {
CCLOG("AppDelegate VM restart - jsb_boot.js failed");
}
} else {
CCLOG("AppDelegate VM restart - No ScriptingCore instance");
}
}
2.3 実装における重要なメソッド一覧表
| 処理段階 | ファイル | メソッド | 主な処理内容 |
|---|---|---|---|
| Surface Destroyed | Cocos2dxRenderer.java | handleSurfaceDestroyed() |
nativeOnSurfaceDestroyed()呼び出し |
| Java_org_cocos2dx_lib_Cocos2dxRenderer.cpp | Java_org_cocos2dx_lib_Cocos2dxRenderer_nativeOnSurfaceDestroyed() |
handleAppDelegateSurfaceDestroyed()呼び出し | |
| AppDelegate.cpp | handleSurfaceDestroyedCleanup() |
collectBackupDataFromJS() + safeCleanup()実行 | |
| ScriptingCore.cpp | safeCleanup() |
JavaScript Context完全クリーンアップ(130行処理) | |
| Surface Created + ThreadID変更 | Cocos2dxRenderer.java | onSurfaceCreated() |
ThreadID変更検出 + nativeRestartSpiderMonkeyVM()呼び出し |
| Java_org_cocos2dx_lib_Cocos2dxRenderer.cpp | Java_org_cocos2dx_lib_Cocos2dxRenderer_nativeRestartSpiderMonkeyVM() |
handleAppDelegateVMRestart()呼び出し | |
| AppDelegate.cpp | handleAppDelegateVMRestart() |
forceGLThreadRestart() + ActionManager復旧 + JavaScript再実行 | |
| ScriptingCore.cpp |
forceGLThreadRestart() + createGlobalContext()
|
Thread-safe Context再構築 |
2.4 アプリケーション固有実装:データバックアップ・復元
⚠️ 重要: collectBackupDataFromJS() 関数はアプリケーション固有の独自実装が必要です。
実行タイミング: Surface Destroyed時(handleSurfaceDestroyedCleanup()内)、safeCleanup()実行前
実装: 必要に応じてJavaScript環境からアプリ状態を取得・保存。不要な場合は空実装でOK。
3. Thread-safe Context管理
問題: SpiderMonkey VM restartは異なるスレッド間でのJSContext操作によりJS_AbortIfWrongThreadエラーが発生
解決: Thread-safe な Context管理による安全なクリーンアップと再構築
3.1 ScriptingCore.h 修正
ファイル: frameworks/cocos2d-x/cocos/scripting/js-bindings/manual/ScriptingCore.h
必要なヘッダーをインクルード:
#include <thread>
クラス定義に追加:
class CC_JS_DLL ScriptingCore : public ScriptEngineProtocol
{
// 既存のprivateメンバー...
private:
std::thread::id _jsContextThreadId; // 🎯 Context所有スレッド追跡
bool _forceGLThreadRestart; // 🎯 GLThread restart フラグ
// 既存のpublicメンバー...
public:
// 🎯 新規追加メソッド
void safeCleanup();
void cleanupExistingJSContext(bool forceGLThreadRestart);
void forceGLThreadRestart() { _forceGLThreadRestart = true; }
// 🎯 補助関数(safeCleanup内部で使用)
void performPostVMRestartSceneCleanup();
void resetSceneTransitionState();
void cleanupJavaScriptEventCallbacks();
void performPostVMRestartEventCleanup();
// 既存メソッド...
};
3.2 ScriptingCore.cpp 修正
ファイル: frameworks/cocos2d-x/cocos/scripting/js-bindings/manual/ScriptingCore.h
必要なヘッダーをインクルード:
#include <thread>
クラス定義に追加:
class CC_JS_DLL ScriptingCore : public ScriptEngineProtocol
{
// 既存のprivateメンバー...
private:
std::thread::id _jsContextThreadId; // 🎯 Context所有スレッド追跡
bool _forceGLThreadRestart; // 🎯 GLThread restart フラグ
// 既存のpublicメンバー...
public:
// 🎯 新規追加メソッド
void safeCleanup();
void cleanupExistingJSContext(bool forceGLThreadRestart);
void forceGLThreadRestart() { _forceGLThreadRestart = true; }
// 🎯 補助関数(safeCleanup内部で使用)
void performPostVMRestartSceneCleanup();
void resetSceneTransitionState();
void cleanupJavaScriptEventCallbacks();
void performPostVMRestartEventCleanup();
// 既存メソッド...
};
3.2 ScriptingCore.cpp 修正
ファイル: frameworks/cocos2d-x/cocos/scripting/js-bindings/manual/ScriptingCore.cpp
必要なヘッダーをインクルード:
#include <thread>
#include "base/CCEventType.h"
#include "base/CCEventDispatcher.h"
#include "renderer/CCGLProgramCache.h"
3.2.1 Thread-safe Context Cleanup実装
void ScriptingCore::cleanupExistingJSContext(bool forceGLThreadRestart) {
if (!_cx || !_rt) return;
std::thread::id currentThreadId = std::this_thread::get_id();
bool needsGLThreadRestart = forceGLThreadRestart || (_jsContextThreadId != currentThreadId);
if (needsGLThreadRestart) {
// 🎯 CRITICAL: 異なるスレッドからは JSContext を触らない
// SpiderMonkey は JS_AbortIfWrongThread でクラッシュする
LOGD("cleanupExistingJSContext: Abandoning old context safely");
_cx = NULL; // 安全に破棄
_rt = NULL;
} else {
// 同一スレッド - 通常クリーンアップ
LOGD("cleanupExistingJSContext: Normal cleanup on same thread");
ScriptingCore::removeAllRoots(_cx);
JS_DestroyContext(_cx);
JS_DestroyRuntime(_rt);
_cx = NULL;
_rt = NULL;
}
_forceGLThreadRestart = false;
}
3.2.2 Context作成時スレッド追跡
createGlobalContext()に追加:
void ScriptingCore::createGlobalContext() {
// 統合ヘルパーでContext cleanup
cleanupExistingJSContext(_forceGLThreadRestart);
// SpiderMonkey初期化
if (!_jsInited && !JS_Init()) return;
_jsInited = true;
_rt = JS_NewRuntime(8L * 1024L * 1024L);
_cx = JS_NewContext(_rt, 32 * 1024);
// 🎯 Contextを作成したスレッドを記録
_jsContextThreadId = std::this_thread::get_id();
LOGD("JS context created on thread: %d", std::hash<std::thread::id>{}(_jsContextThreadId));
// 既存初期化コード継続...
JS_SetErrorReporter(_cx, ScriptingCore::reportError);
// ... 他の初期化処理
}
3.2.3 完全safeCleanup実装
// 🎯 完全なsafeCleanup実装(130行以上の包括的処理)
void ScriptingCore::safeCleanup()
{
if (!_cx) {
LOGD("safeCleanup: No JavaScript context available, skipping cleanup");
return;
}
LOGD("Starting safe cleanup for VM restart");
// Step 1: Garbage collect while context is still valid
try {
if (_cx && _rt) {
garbageCollect();
LOGD("Garbage collection completed");
}
} catch (...) {
LOGD("Garbage collection failed, continuing cleanup");
}
// Step 2: Free localStorage
localStorageFree();
LOGD("localStorage freed");
// Step 3: Clean compiled scripts
for (auto& s : filename_script) {
CC_SAFE_DELETE(s.second);
}
filename_script.clear();
LOGD("Compiled scripts cleared");
// Step 4: Remove JS roots and clear proxy maps
removeAllRoots(_cx);
LOGD("JS roots removed");
// Clear native<->JS proxy maps
_native_js_global_map.clear();
auto it_js = _js_native_global_map.begin();
while (it_js != _js_native_global_map.end()) {
free(it_js->second);
it_js = _js_native_global_map.erase(it_js);
}
_js_native_global_map.clear();
LOGD("Native<->JS proxy maps cleared");
// Step 5: Clear type map to prevent prototype reference issues
for (auto iter = _js_global_type_map.begin(); iter != _js_global_type_map.end(); ++iter) {
if (iter->second) {
if (iter->second->jsclass) {
free(iter->second->jsclass);
iter->second->jsclass = nullptr;
}
free(iter->second);
}
}
_js_global_type_map.clear();
LOGD("JS global type map cleared");
// Step 6: Clean autorelease pool
PoolManager::getInstance()->getCurrentPool()->clear();
LOGD("Autorelease pool cleared");
// Step 7: Phase 1 Scene State Cleanup
performPostVMRestartSceneCleanup();
resetSceneTransitionState();
// Step 8: Clean event dispatcher and custom events
auto director = Director::getInstance();
if (director) {
// Clean all actions and scheduled callbacks
if (director->getActionManager()) {
director->getActionManager()->removeAllActions();
LOGD("All actions removed from ActionManager");
}
if (director->getScheduler()) {
director->getScheduler()->unscheduleAll();
LOGD("All scheduled callbacks removed");
}
// Clean event dispatcher and custom events (Phase 2: Custom Event System Cleanup)
auto eventDispatcher = director->getEventDispatcher();
if (eventDispatcher) {
LOGD("Starting comprehensive event dispatcher cleanup");
// Phase 2.1: Remove all event listeners by type
eventDispatcher->removeAllEventListeners();
LOGD("All event listeners removed");
// Phase 2.2: Clean specific event listener types
eventDispatcher->removeEventListenersForType(EventListener::Type::KEYBOARD);
eventDispatcher->removeEventListenersForType(EventListener::Type::TOUCH_ONE_BY_ONE);
eventDispatcher->removeEventListenersForType(EventListener::Type::TOUCH_ALL_AT_ONCE);
eventDispatcher->removeEventListenersForType(EventListener::Type::MOUSE);
eventDispatcher->removeEventListenersForType(EventListener::Type::ACCELERATION);
LOGD("Specific event listener types cleaned");
// Phase 2.3: Clean common custom event names
std::vector<std::string> customEventNames = {
"game_state_changed", "user_input_received", "animation_completed",
"network_response", "file_operation_complete", "ui_interaction",
"scene_transition_start", "scene_transition_end"
};
for (const auto& eventName : customEventNames) {
eventDispatcher->removeCustomEventListeners(eventName);
}
LOGD("Common custom events cleaned");
// Phase 2.4: Clean application-specific custom events for pixel art maker
std::vector<std::string> pixelArtEvents = {
"canvas_updated", "color_selected", "tool_changed", "layer_modified",
"export_completed", "save_state_changed", "undo_redo_state_changed",
"brush_size_changed", "animation_frame_changed"
};
for (const auto& eventName : pixelArtEvents) {
eventDispatcher->removeCustomEventListeners(eventName);
}
LOGD("Pixel art application events cleaned");
// Phase 2.5: Reset event dispatcher to clean state
eventDispatcher->setEnabled(false);
eventDispatcher->setEnabled(true);
LOGD("Event dispatcher reset to clean state");
LOGD("Comprehensive event dispatcher cleanup completed");
}
}
// Step 9: Phase 3 JavaScript-Native Bridge Cleanup
cleanupJavaScriptEventCallbacks();
LOGD("Safe cleanup completed successfully");
}
3.2.4 補助関数実装
// Phase 1: Scene State Cleanup for VM restart
void ScriptingCore::performPostVMRestartSceneCleanup()
{
auto director = Director::getInstance();
if (!director) return;
LOGD("Starting post-VM restart scene cleanup");
// Clear running scene and clean its resources
auto runningScene = director->getRunningScene();
if (runningScene) {
LOGD("Cleaning running scene: %p", runningScene);
// Recursively clean all child nodes
runningScene->removeAllChildrenWithCleanup(true);
// Clear scene-specific resources
runningScene->cleanup();
}
// Pop all scenes from stack to clear scene stack completely
// Use popToSceneStackLevel(0) to clear entire stack
director->popToRootScene();
LOGD("Cleared scene stack to root");
// If there's still a running scene after popping, we need to replace it with null
if (director->getRunningScene()) {
// In Cocos2d-x 3.17.2, we can't directly set running scene to null
// The safest approach is to end the director to clean everything
LOGD("Running scene still exists after cleanup");
}
LOGD("Post-VM restart scene cleanup completed");
}
void ScriptingCore::resetSceneTransitionState()
{
auto director = Director::getInstance();
if (!director) return;
LOGD("Starting scene transition state reset");
// Reset animation properties
director->setAnimationInterval(1.0f / 60.0f); // Default 60 FPS
director->setDisplayStats(false);
LOGD("Reset animation interval to 60 FPS and disabled display stats");
// Clear paused status
if (director->isPaused()) {
director->resume();
LOGD("Resumed director from paused state");
}
LOGD("Scene transition state reset completed");
}
// Phase 3: JavaScript-Native Bridge Cleanup for VM restart
void ScriptingCore::cleanupJavaScriptEventCallbacks()
{
if (!_cx) {
LOGD("cleanupJavaScriptEventCallbacks: No JavaScript context available");
return;
}
LOGD("Starting JavaScript-Native Bridge cleanup");
// Step 1: Clean up registered callbacks and JSFunction references
try {
// Remove all rooted objects to clear JS callback references
removeAllRoots(_cx);
LOGD("All rooted objects removed to clear callback references");
// Step 2: Clean up proxy objects that might hold callback references
// Clear native proxy to JS object mappings
_native_js_global_map.clear();
_js_native_global_map.clear();
LOGD("Native-JS proxy mappings cleared");
// Step 3: Clean up any component system callbacks
auto director = Director::getInstance();
if (director) {
// Clean up any JS callbacks registered with native components
auto scheduler = director->getScheduler();
if (scheduler) {
// Unschedule any JS callback functions
scheduler->unscheduleAll();
LOGD("All JS callback schedules cleared");
}
// Step 4: Clean up UI callback systems
auto eventDispatcher = director->getEventDispatcher();
if (eventDispatcher) {
// Remove all event listeners as we can't target ScriptingCore specifically
eventDispatcher->removeAllEventListeners();
LOGD("All event listeners removed during JS callback cleanup");
}
}
// Step 5: Clean up SpiderMonkey specific callback storage
// Clear any stored JSFunction* references in callback systems
if (_global) {
// Clear global object references that might hold callbacks
JS::RootedObject globalObj(_cx, _global->get());
if (globalObj) {
// Clear any global callback registrations
JS::RootedValue undefined(_cx);
undefined.setUndefined();
// Clear common callback properties
std::vector<std::string> callbackProps = {
"onUpdate", "onDraw", "onCreate", "onDestroy",
"onEnter", "onExit", "onPause", "onResume",
"onTouchBegan", "onTouchMoved", "onTouchEnded",
"onKeyPressed", "onKeyReleased"
};
for (const auto& prop : callbackProps) {
// Use simple property name setting for SpiderMonkey compatibility
JS_SetProperty(_cx, globalObj, prop.c_str(), undefined);
}
LOGD("Global JavaScript callback properties cleared");
}
}
// Step 6: Clear any remaining JSFunction callback storage in native code
// This includes any std::function<> objects that capture JS callbacks
// Step 7: Clear cached compiled scripts that might hold callbacks
for (auto& entry : filename_script) {
if (entry.second) {
// Clear the compiled script reference - proper SpiderMonkey cleanup
delete entry.second;
entry.second = nullptr;
}
}
filename_script.clear();
LOGD("Compiled script callback references cleared");
} catch (const std::exception& e) {
LOGD("Exception during JavaScript-Native Bridge cleanup: %s", e.what());
} catch (...) {
LOGD("Unknown exception during JavaScript-Native Bridge cleanup");
}
LOGD("JavaScript-Native Bridge cleanup completed");
}
4. GLProgramStateCache再初期化
ファイル: frameworks/cocos2d-x/cocos/renderer/CCGLProgramCache.cpp
void GLProgramCache::reloadDefaultGLPrograms()
{
CCLOG("GLProgramCache::reloadDefaultGLPrograms() called - resetting all GL programs");
// 🎯 GLProgramStateCacheを先にクリア - 既存のGLProgramStateは全て無効になる
GLProgramStateCache::getInstance()->removeAllGLProgramState();
// 既存のプログラムリセット・再読み込み(既存コード継続)
// ... ここに既存のreloadDefaultGLPrograms()の実装を継続
}
Priority B: 推奨実装(UX向上)
5. LayerColor EVENT_RENDERER_RECREATED対応
5.1 CCLayer.cpp 修正
ファイル: frameworks/cocos2d-x/cocos/2d/CCLayer.cpp
ヘッダーインクルード:
#include "renderer/CCGLProgramCache.h"
#include "base/CCEventType.h"
#include "base/CCEventListenerCustom.h"
#include "base/CCEventCustom.h"
LayerColor自動復旧機能:
LayerColor::LayerColor()
{
_blendFunc = BlendFunc::ALPHA_PREMULTIPLIED;
#if CC_ENABLE_CACHE_TEXTURE_DATA
// 🎯 EVENT_RENDERER_RECREATED 対応追加
CCLOG("create rendererRecreatedListener for LayerColor");
_rendererRecreatedListener = EventListenerCustom::create(EVENT_RENDERER_RECREATED,
[this](EventCustom*)
{
CCLOG("LayerColor: Reapplying GLProgramState after renderer recreation");
// GLProgramState再設定
setGLProgramState(GLProgramState::getOrCreateWithGLProgramName(GLProgram::SHADER_NAME_POSITION_COLOR_NO_MVP));
});
Director::getInstance()->getEventDispatcher()->addEventListenerWithFixedPriority(_rendererRecreatedListener, -1);
#endif
}
LayerColor::~LayerColor()
{
#if CC_ENABLE_CACHE_TEXTURE_DATA
Director::getInstance()->getEventDispatcher()->removeEventListener(_rendererRecreatedListener);
#endif
}
5.2 CCLayer.h 修正
ファイル: frameworks/cocos2d-x/cocos/2d/CCLayer.h
class CC_DLL LayerColor : public Layer, public RGBAProtocol, public BlendProtocol
{
// ... 既存メンバー ...
protected:
#if CC_ENABLE_CACHE_TEXTURE_DATA
EventListenerCustom* _rendererRecreatedListener; // 🎯 リスナー追加
#endif
// ... 既存メンバー ...
};
6. Weak Reference設計(JNI層)
ファイル: frameworks/cocos2d-x/cocos/platform/android/jni/Java_org_cocos2dx_lib_Cocos2dxRenderer.cpp
Weak Reference宣言:
// 🎯 AppDelegate関数への弱参照(モジュール疎結合)
extern "C" __attribute__((weak)) void handleAppDelegateSurfaceDestroyed();
extern "C" __attribute__((weak)) void handleAppDelegateVMRestart();
JNI VM Restart実装:
JNIEXPORT void JNICALL Java_org_cocos2dx_lib_Cocos2dxRenderer_nativeRestartSpiderMonkeyVM() {
cocos2d::log("nativeRestartSpiderMonkeyVM: Restarting SpiderMonkey VM");
// 🎯 AppDelegate VM restart関数が利用可能な場合のみ呼び出し
if (handleAppDelegateVMRestart) {
cocos2d::log("nativeRestartSpiderMonkeyVM: Calling AppDelegate VM restart");
handleAppDelegateVMRestart();
cocos2d::log("nativeRestartSpiderMonkeyVM: AppDelegate VM restart completed");
} else {
cocos2d::log("nativeRestartSpiderMonkeyVM: No VM restart function available");
}
}
JNIEXPORT void JNICALL Java_org_cocos2dx_lib_Cocos2dxRenderer_nativeOnSurfaceDestroyed() {
cocos2d::log("nativeOnSurfaceDestroyed: Surface destroyed");
// 🎯 AppDelegate cleanup関数が利用可能な場合のみ呼び出し
if (handleAppDelegateSurfaceDestroyed) {
cocos2d::log("nativeOnSurfaceDestroyed: Calling AppDelegate cleanup");
handleAppDelegateSurfaceDestroyed();
cocos2d::log("nativeOnSurfaceDestroyed: AppDelegate cleanup completed");
} else {
cocos2d::log("nativeOnSurfaceDestroyed: No cleanup function available");
}
}
Priority C: 安全性向上(運用安定)
7. 包括的エラーハンドリング
ファイル: main.js
main.js初期化の安全化:
cc.game.onStart = function(){
cc.log("=== main.js cc.game.onStart STARTED ===");
try {
// 🎯 全初期化処理をtry-catchでラップ
// 基本的なCocos2d-x設定
if(!cc.sys.isNative && document.getElementById("cocosLoading")) {
document.body.removeChild(document.getElementById("cocosLoading"));
}
// 画面設定
cc.view.enableRetina(cc.sys.os === cc.sys.OS_IOS ? true : false);
cc.view.adjustViewPort(true);
cc.director.setProjection(cc.Director.PROJECTION_2D);
cc.view.resizeWithBrowserSize(true);
// ファイルパス設定
if (cc.sys.isNative) {
var searchPaths = jsb.fileUtils.getSearchPaths();
searchPaths.push("res");
jsb.fileUtils.setSearchPaths(searchPaths);
}
// 🎯 ここにあなたのアプリケーション固有の初期化コードを記述
// 例:データマネージャ初期化、設定読み込み、等
// Scene作成・開始
var scene = new YourMainScene(); // あなたのメインScene
cc.director.runScene(scene);
cc.log("=== main.js cc.game.onStart COMPLETED SUCCESSFULLY ===");
} catch (error) {
// 🎯 致命的エラーの完全キャッチ
cc.log("main.js: CRITICAL ERROR in onStart - " + error.toString());
cc.log("main.js: Error stack: " + (error.stack || "no stack available"));
throw error; // 再スローして開発者に通知
}
};
8. Screen Size Change無限再帰防止
ファイル: main.js
// 🎯 Screen Size Change無限再帰防止システム
window._isScreenSizeChanging = false;
window.setScreenSizeChangedSafely = function(handler) {
window.onScreenSizeChanged = function(width, height) {
if (window._isScreenSizeChanging) {
cc.log("RECURSION PREVENTION: Screen size change already in progress, ignoring call");
return;
}
window._isScreenSizeChanging = true;
try {
handler(width, height);
} catch (e) {
cc.log("ERROR in screen size change handler: " + e.toString());
} finally {
// 短い遅延の後にフラグをリセット
setTimeout(function() {
window._isScreenSizeChanging = false;
}, 200);
}
};
};
Discussion