JNI GlobalRef 管理:Android Plugin におけるメモリリーク回避のベストプラクティス
Android Plugin の開発において、Java とネイティブコード間の相互運用を可能にする JNI (Java Native Interface) は不可欠なツールです。しかし、JNI を不適切に利用すると、特に GlobalRef の管理において深刻なメモリリークを引き起こす可能性があります。本講義では、Android Plugin の文脈に特化し、GlobalRef の基本的な概念から、その課題、そしてメモリリークを回避するための実践的なベストプラクティスまでを深く掘り下げていきます。
JNI と Android Plugin における GlobalRef の役割と重要性
JNI は、Java 仮想マシン (JVM) 上で動作する Java コードと、C/C++ などのネイティブコードとの間で相互に呼び出しを行うための標準インターフェースです。Android アプリケーションや特に Android Plugin の開発では、パフォーマンスクリティカルな処理、既存のネイティブライブラリの利用、OS レベルの機能へのアクセスなど、様々な理由で JNI が活用されます。
Android Plugin は、通常、独立したプロセスやコンテキストで動作することが多く、そのライフサイクルはホストアプリケーションのそれと密接に関連している一方で、独自の管理が必要となるケースも存在します。このような環境下で、ネイティブコードが Java オブジェクトへの参照を長期間保持する必要がある場合、JNI の提供する参照メカニズムを適切に理解し、管理することが極めて重要になります。
JNI には主に三種類のオブジェクト参照が存在します。
- LocalRef (ローカル参照): JNI 関数が呼び出された際のネイティブスタックフレームに関連付けられ、JNI 呼び出しが完了するか、明示的に
DeleteLocalRefが呼び出されるまで有効です。通常、JNI メソッド内でのみ短期間使用されます。 - GlobalRef (グローバル参照): JVM のガベージコレクタに到達可能なオブジェクトへの参照を、明示的に
DeleteGlobalRefが呼び出されるまで保持します。これは JVM 全体で有効であり、複数のスレッドや JNI 呼び出しを超えてオブジェクトへの参照を維持したい場合に利用されます。 - WeakGlobalRef (弱グローバル参照): GlobalRef に似ていますが、JVM のガベージコレクタが参照先の Java オブジェクトを回収することを妨げません。参照先のオブジェクトが回収されると、WeakGlobalRef は NULL になります。
Android Plugin のネイティブコンポーネントが、Java 側のコールバックオブジェクト、設定オブジェクト、または UI コンポーネントなど、JNI 呼び出しのスコープを超えて長く存続する Java オブジェクトへのアクセスを必要とする場合、GlobalRef が選択されます。しかし、GlobalRef は明示的な解放が必要であり、その管理を怠ると、参照先の Java オブジェクトがガベージコレクションされず、結果としてメモリリークが発生します。特に Android Plugin は、その性質上、ホストアプリケーションのクラッシュや予期せぬ挙動につながる可能性があり、堅牢な GlobalRef 管理が必須となるのです。
JNI 参照の基本とライフサイクル
JNI 参照の適切な管理は、メモリリークを回避するための基礎となります。それぞれの参照タイプの動作原理を理解することが最初のステップです。
LocalRef (ローカル参照)
LocalRef は、ネイティブメソッドが Java メソッドから呼び出された際に作成され、そのネイティブメソッドが終了すると自動的に解放されます。JNIEnv* ポインタを通じてアクセスできるほとんどの JNI 関数は LocalRef を返します。
特徴:
- ネイティブメソッドの実行スコープに限定される。
- 自動的に解放されるため、明示的な解放は通常不要(ただし、大量の
LocalRefを短期間に作成する場合は注意が必要)。 PushLocalFrameとPopLocalFrameを使用して、カスタムのローカル参照フレームを作成し、参照の寿命を制御することも可能。
コード例 (C++):
#include <jni.h>
#include <iostream>
extern "C" JNIEXPORT void JNICALL
Java_com_example_plugin_MyPlugin_processString(JNIEnv* env, jobject thiz, jstring javaString) {
// javaString は LocalRef。メソッド終了時に自動解放される。
const char* nativeString = env->GetStringUTFChars(javaString, nullptr);
if (nativeString == nullptr) {
// メモリ不足またはその他のエラー
return;
}
std::cout << "Received string: " << nativeString << std::endl;
env->ReleaseStringUTFChars(javaString, nativeString);
// 新しい LocalRef を作成
jclass stringClass = env->FindClass("java/lang/String");
if (stringClass == nullptr) {
return; // 例外が発生している
}
// "Hello from JNI" は LocalRef
jstring newJavaString = env->NewStringUTF("Hello from JNI");
if (newJavaString == nullptr) {
// エラー処理
env->DeleteLocalRef(stringClass); // stringClass は LocalRef なので解放
return;
}
// newJavaString はメソッド終了時に自動解放されるが、
// 大量の LocalRef を作成する場合は手動で解放することも検討する。
// env->DeleteLocalRef(newJavaString);
env->DeleteLocalRef(stringClass); // FindClass が返すクラス参照も LocalRef
}
大量の LocalRef をループ内で作成する場合、メモリが枯渇する可能性があります。この場合、DeleteLocalRef を適宜呼び出すか、PushLocalFrame/PopLocalFrame を使用してメモリ消費を制御する必要があります。
// ループ内で LocalRef を効率的に管理する例
extern "C" JNIEXPORT void JNICALL
Java_com_example_plugin_MyPlugin_processManyStrings(JNIEnv* env, jobject thiz, jobjectArray stringArray) {
jsize length = env->GetArrayLength(stringArray);
for (int i = 0; i < length; ++i) {
// 新しいローカルフレームを作成
env->PushLocalFrame(10); // 最大10個の LocalRef を管理できるフレームを作成
jstring element = (jstring)env->GetObjectArrayElement(stringArray, i); // LocalRef
if (element == nullptr) {
// エラー処理
env->PopLocalFrame(nullptr); // フレームを破棄し、その中の LocalRef を解放
continue;
}
const char* nativeElement = env->GetStringUTFChars(element, nullptr);
if (nativeElement == nullptr) {
env->PopLocalFrame(nullptr);
continue;
}
std::cout << "Processing: " << nativeElement << std::endl;
env->ReleaseStringUTFChars(element, nativeElement);
// 現在のフレーム内の LocalRef を全て解放し、前のフレームに戻る
env->PopLocalFrame(nullptr);
}
}
GlobalRef (グローバル参照)
GlobalRef は、ネイティブコードが Java オブジェクトへの参照を JNI 呼び出しのスコープを超えて、かつ複数のスレッドにわたって維持する必要がある場合に作成されます。GlobalRef は、明示的に DeleteGlobalRef が呼び出されるまで、JVM のガベージコレクションによって参照先の Java オブジェクトが回収されるのを防ぎます。
特徴:
- JVM のガベージコレクタからオブジェクトを保護する。
- 明示的に
DeleteGlobalRefを呼び出して解放する必要がある。これを怠るとメモリリークが発生する。 jobject型の任意のLocalRefまたは別のGlobalRefから作成できる。- 作成コストが
LocalRefより高い。
コード例 (C++):
#include <jni.h>
#include <iostream>
#include <mutex> // GlobalRef へのアクセスを同期するため
// ネイティブコード側で保持するグローバル参照
static jobject globalCallbackObject = nullptr;
static std::mutex globalCallbackMutex; // GlobalRef へのアクセスを保護するミューテックス
extern "C" JNIEXPORT void JNICALL
Java_com_example_plugin_MyPlugin_setCallback(JNIEnv* env, jobject thiz, jobject callback) {
std::lock_guard<std::mutex> lock(globalCallbackMutex);
// 既存の GlobalRef があれば解放する
if (globalCallbackObject != nullptr) {
env->DeleteGlobalRef(globalCallbackObject);
globalCallbackObject = nullptr;
}
// 新しい GlobalRef を作成
if (callback != nullptr) {
globalCallbackObject = env->NewGlobalRef(callback);
if (globalCallbackObject == nullptr) {
// NewGlobalRef が失敗した場合 (メモリ不足など)
// 例外がスローされているはずなので、JNI 例外処理を行う
env->ExceptionDescribe();
env->ExceptionClear();
std::cerr << "Failed to create GlobalRef for callback object." << std::endl;
return;
}
std::cout << "GlobalRef created for callback object." << std::endl;
} else {
std::cout << "Callback object is null, GlobalRef will be cleared." << std::endl;
}
}
extern "C" JNIEXPORT void JNICALL
Java_com_example_plugin_MyPlugin_triggerCallback(JNIEnv* env, jobject thiz, jstring message) {
std::lock_guard<std::mutex> lock(globalCallbackMutex);
if (globalCallbackObject != nullptr) {
// GlobalRef を使用して Java メソッドを呼び出す
jclass callbackClass = env->GetObjectClass(globalCallbackObject); // LocalRef
if (callbackClass == nullptr) {
env->ExceptionDescribe();
env->ExceptionClear();
std::cerr << "Failed to get class of callback object." << std::endl;
return;
}
jmethodID callbackMethod = env->GetMethodID(callbackClass, "onMessage", "(Ljava/lang/String;)V"); // LocalRef
if (callbackMethod == nullptr) {
env->ExceptionDescribe();
env->ExceptionClear();
std::cerr << "Failed to get method ID for onMessage." << std::endl;
env->DeleteLocalRef(callbackClass);
return;
}
env->CallVoidMethod(globalCallbackObject, callbackMethod, message);
if (env->ExceptionCheck()) {
env->ExceptionDescribe();
env->ExceptionClear();
std::cerr << "Exception occurred during callback invocation." << std::endl;
}
env->DeleteLocalRef(callbackClass);
} else {
std::cout << "No callback object set." << std::endl;
}
}
// Plugin アンロード時などに GlobalRef を解放するメソッド
extern "C" JNIEXPORT void JNICALL
Java_com_example_plugin_MyPlugin_releaseCallback(JNIEnv* env, jobject thiz) {
std::lock_guard<std::mutex> lock(globalCallbackMutex);
if (globalCallbackObject != nullptr) {
env->DeleteGlobalRef(globalCallbackObject);
globalCallbackObject = nullptr;
std::cout << "GlobalRef for callback object released." << std::endl;
}
}
WeakGlobalRef (弱グローバル参照)
WeakGlobalRef は、GlobalRef と同様に JNI 呼び出しのスコープを超えて Java オブジェクトへの参照を保持できますが、ガベージコレクタがオブジェクトを回収することを妨げません。参照先の Java オブジェクトがガベージコレクションされると、WeakGlobalRef は自動的に NULL になります。
特徴:
- ガベージコレクタがオブジェクトを回収することを妨げない。
- 参照が有効かどうかを
IsSameObject(ref, NULL)で確認する必要がある。 - 参照が NULL になった場合、その
WeakGlobalRefは無効となる。 DeleteWeakGlobalRefを呼び出して明示的に解放する必要がある(NULL になった後でも)。
コード例 (C++):
#include <jni.h>
#include <iostream>
#include <mutex>
static jweak weakCallbackObject = nullptr;
static std::mutex weakCallbackMutex;
extern "C" JNIEXPORT void JNICALL
Java_com_example_plugin_MyPlugin_setWeakCallback(JNIEnv* env, jobject thiz, jobject callback) {
std::lock_guard<std::mutex> lock(weakCallbackMutex);
if (weakCallbackObject != nullptr) {
env->DeleteWeakGlobalRef(weakCallbackObject);
weakCallbackObject = nullptr;
}
if (callback != nullptr) {
weakCallbackObject = env->NewWeakGlobalRef(callback);
if (weakCallbackObject == nullptr) {
env->ExceptionDescribe();
env->ExceptionClear();
std::cerr << "Failed to create WeakGlobalRef." << std::endl;
return;
}
std::cout << "WeakGlobalRef created for callback object." << std::endl;
}
}
extern "C" JNIEXPORT void JNICALL
Java_com_example_plugin_MyPlugin_triggerWeakCallback(JNIEnv* env, jobject thiz, jstring message) {
std::lock_guard<std::mutex> lock(weakCallbackMutex);
if (weakCallbackObject != nullptr) {
// WeakGlobalRef がまだ有効かチェック
jobject strongRef = env->NewLocalRef(weakCallbackObject); // WeakGlobalRef を LocalRef に変換
if (strongRef != nullptr) { // オブジェクトがまだ存在している
jclass callbackClass = env->GetObjectClass(strongRef); // LocalRef
if (callbackClass == nullptr) {
env->ExceptionDescribe();
env->ExceptionClear();
std::cerr << "Failed to get class of weak callback object." << std::endl;
env->DeleteLocalRef(strongRef);
return;
}
jmethodID callbackMethod = env->GetMethodID(callbackClass, "onMessage", "(Ljava/lang/String;)V"); // LocalRef
if (callbackMethod == nullptr) {
env->ExceptionDescribe();
env->ExceptionClear();
std::cerr << "Failed to get method ID for onMessage (weak)." << std::endl;
env->DeleteLocalRef(callbackClass);
env->DeleteLocalRef(strongRef);
return;
}
env->CallVoidMethod(strongRef, callbackMethod, message);
if (env->ExceptionCheck()) {
env->ExceptionDescribe();
env->ExceptionClear();
std::cerr << "Exception occurred during weak callback invocation." << std::endl;
}
env->DeleteLocalRef(callbackClass);
env->DeleteLocalRef(strongRef); // LocalRef を解放
} else {
// オブジェクトは既にガベージコレクションされた
std::cout << "Weak callback object has been garbage collected." << std::endl;
// WeakGlobalRef 自体も解放する
env->DeleteWeakGlobalRef(weakCallbackObject);
weakCallbackObject = nullptr;
}
} else {
std::cout << "No weak callback object set." << std::endl;
}
}
// Plugin アンロード時などに WeakGlobalRef を解放するメソッド
extern "C" JNIEXPORT void JNICALL
Java_com_example_plugin_MyPlugin_releaseWeakCallback(JNIEnv* env, jobject thiz) {
std::lock_guard<std::mutex> lock(weakCallbackMutex);
if (weakCallbackObject != nullptr) {
env->DeleteWeakGlobalRef(weakCallbackObject);
weakCallbackObject = nullptr;
std::cout << "WeakGlobalRef for callback object released." << std::endl;
}
}
参照タイプの比較表:
| 特徴 | LocalRef | GlobalRef | WeakGlobalRef |
|---|---|---|---|
| 寿命 | JNI 呼び出しのスコープ内 (またはフレーム) | 明示的な DeleteGlobalRef が呼び出されるまで |
明示的な DeleteWeakGlobalRef が呼び出されるまで、または参照先のオブジェクトが GC されるまで |
| GC 防止 | 可 | 可 | 不可 |
| 解放方法 | 通常自動、または DeleteLocalRef/PopLocalFrame |
DeleteGlobalRef を明示的に呼び出す |
DeleteWeakGlobalRef を明示的に呼び出す (GC 後も必要) |
| 使用シナリオ | 短期間の JNI 呼び出し内での一時的な参照 | JNI 呼び出しを超えて長期間参照を保持したい場合、GC を防ぎたい場合 | JNI 呼び出しを超えて長期間参照を保持したいが、GC を妨げたくない場合 |
| スレッド | 参照を作成したスレッドでのみ有効 | 全ての JVM スレッドで有効 | 全ての JVM スレッドで有効 |
GlobalRef の課題とメモリリークのメカニズム
GlobalRef はその強力な機能ゆえに、不適切な管理がメモリリークの主要な原因となります。リークは、ネイティブコードが GlobalRef を作成した後、参照先の Java オブジェクトが不要になっても DeleteGlobalRef が呼び出されない場合に発生します。
リークが発生する典型的なシナリオ
- 解放忘れ: 最も一般的なケースです。ネイティブコードが
NewGlobalRefを呼び出して参照を作成したが、対応するDeleteGlobalRefを呼び出すロジックがない、または呼び出されるべきタイミングで呼び出されない。 - 例外処理の欠落: JNI メソッド内で
GlobalRefを作成し、その後で例外が発生した場合、DeleteGlobalRefに到達せず、参照がリークする可能性があります。 - エラーパスでの解放忘れ: 特定のエラー処理パスにおいて、リソースの解放が適切に行われない場合。
- オブジェクトの寿命と非同期性: Java オブジェクトの寿命がネイティブコードの管理下にある
GlobalRefの寿命と一致しない場合。特に、Java オブジェクトが既に破棄されているにもかかわらず、ネイティブコードがそのGlobalRefを保持し続ける場合。 - プラグインのアンロード: Android Plugin がアンロードされる際に、ネイティブコードが保持していたすべての
GlobalRefを解放しない場合。これは特に危険で、プロセス全体に影響を及ぼす可能性があります。
Android Plugin における具体的な問題点
- Plugin のライフサイクルと JVM のライフサイクル: Plugin は通常、ホストアプリケーションの JVM プロセス内で動作しますが、Plugin 自体のロード/アンロードのメカニズムはアプリケーションのそれとは異なる場合があります。Plugin がアンロードされたときに、ネイティブコードが保持する
GlobalRefも適切に解放される必要があります。JNI_OnUnload関数がこの目的のために提供されています。 - 複数のスレッドからの JNI 呼び出し: ネイティブコードが複数のスレッドから Java オブジェクトにアクセスする場合、
JNIEnvポインタはスレッドローカルであるため、JavaVM*ポインタを保持し、各スレッドでAttachCurrentThreadを呼び出してJNIEnvを取得する必要があります。この際、GlobalRefへのアクセスはスレッドセーフに行われる必要があります(ミューテックスなどを使用)。 - コンテキストの消失: Android アプリケーションでは、Activity や Fragment などの UI コンポーネントが頻繁に作成・破棄されます。Plugin がこれらのコンポーネントへの
GlobalRefを保持している場合、コンポーネントが破棄された後もGlobalRefが残ってしまうと、メモリリークにつながります。このようなケースではWeakGlobalRefの利用が有効です。
GlobalRef 管理のベストプラクティス:設計と実装パターン
メモリリークを回避し、堅牢な JNI コードを記述するためには、GlobalRef の管理に明確な戦略が必要です。ここではいくつかのベストプラクティスと実装パターンを紹介します。
パターン1: 短期的な GlobalRef の使用と即時解放
一時的に GlobalRef が必要な場合(例: バックグラウンドスレッドで短い間だけ Java オブジェクトにアクセスする必要がある場合)は、使用後すぐに解放することが原則です。
// C++コード
#include <jni.h>
#include <iostream>
#include <thread>
JavaVM* g_JavaVM = nullptr; // JNI_OnLoad で初期化される
// バックグラウンドスレッドで Java オブジェクトのメソッドを呼び出す例
void callJavaMethodInBackground(jobject javaObjectRef) {
JNIEnv* env;
// スレッドを JVM にアタッチし、JNIEnv を取得
jint res = g_JavaVM->AttachCurrentThread(&env, nullptr);
if (res != JNI_OK) {
std::cerr << "Failed to attach current thread." << std::endl;
return;
}
// LocalRef から GlobalRef を作成
jobject globalObj = env->NewGlobalRef(javaObjectRef);
if (globalObj == nullptr) {
std::cerr << "Failed to create GlobalRef in background thread." << std::endl;
g_JavaVM->DetachCurrentThread();
return;
}
// ここで GlobalRef を使用して Java メソッドを呼び出す
// ... (前述の triggerCallback のロジックと同様)
jclass objClass = env->GetObjectClass(globalObj);
jmethodID methodId = env->GetMethodID(objClass, "doSomething", "()V");
if (methodId) {
env->CallVoidMethod(globalObj, methodId);
}
env->DeleteLocalRef(objClass);
// GlobalRef を使い終わったらすぐに解放
env->DeleteGlobalRef(globalObj);
std::cout << "GlobalRef released in background thread." << std::endl;
// スレッドを JVM からデタッチ
g_JavaVM->DetachCurrentThread();
}
extern "C" JNIEXPORT void JNICALL
Java_com_example_plugin_MyPlugin_startBackgroundWork(JNIEnv* env, jobject thiz, jobject javaObject) {
// Java スレッドからバックグラウンドスレッドを起動し、Java オブジェクトを渡す
// javaObject は LocalRef なので、そのままバックグラウンドスレッドに渡すと無効になる
// そのため、バックグラウンドスレッド内で GlobalRef を作成する必要がある
std::thread worker(callJavaMethodInBackground, javaObject);
worker.detach(); // スレッドをデタッチして、メインスレッドの終了を待たずに実行を継続
}
// JNI_OnLoad で JavaVM ポインタを取得
extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
g_JavaVM = vm;
return JNI_VERSION_1_6;
}
パターン2: オブジェクトのライフサイクルと GlobalRef の紐付け (RAII)
C++ の RAII (Resource Acquisition Is Initialization) パターンは、リソースの寿命をオブジェクトの寿命と一致させることで、リソース管理を自動化する強力な手法です。GlobalRef の管理にもこれを適用できます。C++ クラスのコンストラクタで NewGlobalRef を呼び出し、デストラクタで DeleteGlobalRef を呼び出すことで、C++ オブジェクトのスコープを抜けるときに GlobalRef が自動的に解放されることを保証します。
// C++コード
#include <jni.h>
#include <iostream>
#include <stdexcept> // for std::runtime_error
#include <mutex>
// JavaVM ポインタはグローバルに保持
static JavaVM* g_JavaVM = nullptr;
// RAII パターンを適用した JniGlobalRef ラッパークラス
class JniGlobalRef {
public:
JniGlobalRef(JNIEnv* env, jobject obj) : m_env(env), m_ref(nullptr) {
if (env == nullptr) {
throw std::runtime_error("JNIEnv is null.");
}
if (obj != nullptr) {
m_ref = env->NewGlobalRef(obj);
if (m_ref == nullptr) {
// NewGlobalRef が失敗した場合、例外をスロー
env->ExceptionDescribe();
env->ExceptionClear();
throw std::runtime_error("Failed to create GlobalRef.");
}
}
}
// ムーブコンストラクタ
JniGlobalRef(JniGlobalRef&& other) noexcept :
m_env(other.m_env), m_ref(other.m_ref) {
other.m_ref = nullptr; // ムーブ元は nullptr にする
}
// ムーブ代入演算子
JniGlobalRef& operator=(JniGlobalRef&& other) noexcept {
if (this != &other) {
release(); // 既存のリソースを解放
m_env = other.m_env;
m_ref = other.m_ref;
other.m_ref = nullptr;
}
return *this;
}
// コピーコンストラクタとコピー代入演算子は削除 (GlobalRef はコピーできない)
JniGlobalRef(const JniGlobalRef&) = delete;
JniGlobalRef& operator=(const JniGlobalRef&) = delete;
~JniGlobalRef() {
release();
}
jobject get() const {
return m_ref;
}
operator jobject() const {
return m_ref;
}
bool isValid() const {
return m_ref != nullptr;
}
private:
JNIEnv* m_env; // NewGlobalRef を作成した JNIEnv を保持(通常は JNI_OnLoad から取得した JavaVM で代用可能だが、ここではシンプルにするため)
jobject m_ref;
void release() {
if (m_ref != nullptr) {
// JNIEnv が null の場合、JavaVM から JNIEnv を取得する
JNIEnv* currentEnv = m_env;
if (currentEnv == nullptr && g_JavaVM != nullptr) {
// デタッチされている可能性を考慮し、AttachCurrentThread を試みる
// ただし、デストラクタ内で AttachCurrentThread/DetachCurrentThread は複雑になるため、
// 通常は JNI_OnLoad から取得した JNIEnv を使用するか、
// 確実に Attach されているスレッドからデストラクタが呼び出されるように設計する
// ここではシンプルに m_env が有効であることを前提とするか、g_JavaVM->GetEnv を使う
// JNI_OnLoad/JNI_OnUnload のような特別なケース以外で、デストラクタ内で Attach/Detach は避けるべき
jint getEnvStat = g_JavaVM->GetEnv((void**)¤tEnv, JNI_VERSION_1_6);
if (getEnvStat == JNI_EDETACHED) {
// スレッドがアタッチされていない場合は解放できない
std::cerr << "Warning: Cannot release GlobalRef in detached thread. Skipping." << std::endl;
return;
} else if (getEnvStat == JNI_EVERSION) {
std::cerr << "Warning: JNI version not supported. Skipping GlobalRef release." << std::endl;
return;
}
}
if (currentEnv != nullptr) {
currentEnv->DeleteGlobalRef(m_ref);
m_ref = nullptr;
std::cout << "JniGlobalRef released automatically." << std::endl;
} else {
std::cerr << "Error: JNIEnv is null, cannot release GlobalRef." << std::endl;
}
}
}
};
// Plugin のネイティブコンポーネントクラス
class MyNativeComponent {
public:
MyNativeComponent(JNIEnv* env, jobject callback)
: m_callback(env, callback) { // コンストラクタで GlobalRef を作成
std::cout << "MyNativeComponent created." << std::endl;
}
void triggerCallback(JNIEnv* env, jstring message) {
if (m_callback.isValid()) {
jclass callbackClass = env->GetObjectClass(m_callback.get());
jmethodID callbackMethod = env->GetMethodID(callbackClass, "onMessage", "(Ljava/lang/String;)V");
if (callbackMethod) {
env->CallVoidMethod(m_callback.get(), callbackMethod, message);
}
env->DeleteLocalRef(callbackClass);
} else {
std::cout << "Callback not set or invalid." << std::endl;
}
}
// デストラクタで JniGlobalRef のデストラクタが呼び出され、GlobalRef が解放される
~MyNativeComponent() {
std::cout << "MyNativeComponent destroyed." << std::endl;
}
private:
JniGlobalRef m_callback; // callback オブジェクトへの GlobalRef
};
// MyNativeComponent のインスタンスを保持するポインタ
static MyNativeComponent* s_nativeComponent = nullptr;
static std::mutex s_componentMutex;
extern "C" JNIEXPORT void JNICALL
Java_com_example_plugin_MyPlugin_createNativeComponent(JNIEnv* env, jobject thiz, jobject callback) {
std::lock_guard<std::mutex> lock(s_componentMutex);
if (s_nativeComponent != nullptr) {
// 既存のコンポーネントがあれば破棄
delete s_nativeComponent;
s_nativeComponent = nullptr;
}
try {
s_nativeComponent = new MyNativeComponent(env, callback);
} catch (const std::runtime_error& e) {
std::cerr << "Error creating native component: " << e.what() << std::endl;
}
}
extern "C" JNIEXPORT void JNICALL
Java_com_example_plugin_MyPlugin_callComponentCallback(JNIEnv* env, jobject thiz, jstring message) {
std::lock_guard<std::mutex> lock(s_componentMutex);
if (s_nativeComponent != nullptr) {
s_nativeComponent->triggerCallback(env, message);
} else {
std::cout << "Native component not created." << std::endl;
}
}
extern "C" JNIEXPORT void JNICALL
Java_com_example_plugin_MyPlugin_destroyNativeComponent(JNIEnv* env, jobject thiz) {
std::lock_guard<std::mutex> lock(s_componentMutex);
if (s_nativeComponent != nullptr) {
delete s_nativeComponent; // デストラクタが呼び出され、GlobalRef が解放される
s_nativeComponent = nullptr;
std::cout << "Native component destroyed." << std::endl;
}
}
// JNI_OnLoad で JavaVM ポインタを取得
extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
g_JavaVM = vm;
return JNI_VERSION_1_6;
}
// JNI_OnUnload で残存するコンポーネントを確実に破棄
extern "C" JNIEXPORT void JNICALL JNI_OnUnload(JavaVM* vm, void* reserved) {
std::lock_guard<std::mutex> lock(s_componentMutex);
if (s_nativeComponent != nullptr) {
delete s_nativeComponent;
s_nativeComponent = nullptr;
std::cout << "JNI_OnUnload: MyNativeComponent destroyed." << std::endl;
}
g_JavaVM = nullptr;
}
この JniGlobalRef クラスは、std::unique_ptr のように振る舞い、ムーブセマンティクスをサポートすることで、リソースの所有権を安全に転送できます。
パターン3: WeakGlobalRef の活用
Java オブジェクトへの参照が必要だが、そのオブジェクトがガベージコレクションされることを妨げたくない場合(例: コールバックオブジェクトが一時的なもので、UI コンポーネントの寿命に依存しない場合)、WeakGlobalRef が適しています。
// C++コード (前述の WeakGlobalRef の例を参照)
// JniWeakGlobalRef クラスとして RAII パターンを適用することも可能
class JniWeakGlobalRef {
public:
JniWeakGlobalRef(JNIEnv* env, jobject obj) : m_env(env), m_ref(nullptr) {
if (env == nullptr) {
throw std::runtime_error("JNIEnv is null.");
}
if (obj != nullptr) {
m_ref = env->NewWeakGlobalRef(obj);
if (m_ref == nullptr) {
env->ExceptionDescribe();
env->ExceptionClear();
throw std::runtime_error("Failed to create WeakGlobalRef.");
}
}
}
JniWeakGlobalRef(JniWeakGlobalRef&& other) noexcept :
m_env(other.m_env), m_ref(other.m_ref) {
other.m_ref = nullptr;
}
JniWeakGlobalRef& operator=(JniWeakGlobalRef&& other) noexcept {
if (this != &other) {
release();
m_env = other.m_env;
m_ref = other.m_ref;
other.m_ref = nullptr;
}
return *this;
}
JniWeakGlobalRef(const JniWeakGlobalRef&) = delete;
JniWeakGlobalRef& operator=(const JniWeakGlobalRef&) = delete;
~JniWeakGlobalRef() {
release();
}
jobject get(JNIEnv* env) const { // WeakGlobalRef から LocalRef を作成して返す
if (m_ref != nullptr) {
jobject strongRef = env->NewLocalRef(m_ref);
if (strongRef == nullptr) {
// オブジェクトは既に GC されたか、参照が無効
return nullptr;
}
return strongRef;
}
return nullptr;
}
bool isValid(JNIEnv* env) const {
return m_ref != nullptr && !env->IsSameObject(m_ref, nullptr);
}
private:
JNIEnv* m_env;
jweak m_ref;
void release() {
if (m_ref != nullptr) {
// ここも JniGlobalRef と同様に JNIEnv の有効性を確認する
JNIEnv* currentEnv = m_env;
if (currentEnv == nullptr && g_JavaVM != nullptr) {
jint getEnvStat = g_JavaVM->GetEnv((void**)¤tEnv, JNI_VERSION_1_6);
if (getEnvStat != JNI_OK) {
std::cerr << "Warning: JNIEnv not available to release WeakGlobalRef. Skipping." << std::endl;
return;
}
}
if (currentEnv != nullptr) {
currentEnv->DeleteWeakGlobalRef(m_ref);
m_ref = nullptr;
std::cout << "JniWeakGlobalRef released automatically." << std::endl;
} else {
std::cerr << "Error: JNIEnv is null, cannot release WeakGlobalRef." << std::endl;
}
}
}
};
// ... MyNativeComponent で JniWeakGlobalRef を使用する例 ...
// クラス定義は省略しますが、JniGlobalRef の代わりに JniWeakGlobalRef をメンバーとして持つ
// そして get() メソッド呼び出し時には、返された LocalRef を使い終わったら DeleteLocalRef する責任を負う
WeakGlobalRef を使用する際は、get() メソッドで LocalRef に変換し、その LocalRef を使用し終わったら DeleteLocalRef で解放することを忘れてはなりません。
パターン4: Java 側からの参照解除通知
ネイティブコードが GlobalRef を保持している場合でも、Java 側から「この参照はもう必要ない」とネイティブコードに通知するメカニズムを提供することが有効です。これは、Java 側の close() や destroy() メソッドから JNI 経由でネイティブコードの解放ロジックを呼び出すことで実現できます。
// Javaコード
package com.example.plugin;
public class MyPlugin {
private long nativeHandle; // ネイティブコンポーネントへのポインタ
public MyPlugin() {
// ネイティブコンポーネントを作成し、そのハンドルを取得
this.nativeHandle = createNativeComponent(new MyCallback());
}
// ネイティブメソッドの宣言
private native long createNativeComponent(MyCallback callback);
private native void callComponentCallback(long handle, String message);
private native void destroyNativeComponent(long handle);
public void doWork(String message) {
if (nativeHandle != 0) {
callComponentCallback(nativeHandle, message);
}
}
// Java 側でリソースが不要になったことをネイティブに通知するメソッド
public void release() {
if (nativeHandle != 0) {
destroyNativeComponent(nativeHandle);
nativeHandle = 0; // ハンドルをクリア
}
}
// アプリケーションライフサイクルで release() を確実に呼び出す
@Override
protected void finalize() throws Throwable {
try {
release(); // GC 時に呼ばれるが、確実性がないため推奨されない
} finally {
super.finalize();
}
}
// コールバックインターフェース (またはクラス)
public interface Callback {
void onMessage(String message);
}
// コールバックの実装
private static class MyCallback implements Callback {
@Override
public void onMessage(String message) {
System.out.println("Java Callback: " + message);
}
}
// JNI ライブラリのロード
static {
System.loadLibrary("myplugin");
}
}
// C++コード
// MyNativeComponent クラスは前述のものを再利用
// JNI 関数は Java の release() メソッドから呼び出される destroyNativeComponent() に対応
// MyNativeComponent インスタンスをマップで管理 (nativeHandle -> MyNativeComponent*)
static std::map<long, MyNativeComponent*> s_nativeComponents;
static std::atomic<long> s_nextNativeHandle(1); // ユニークなハンドルを生成
static std::mutex s_componentsMapMutex;
extern "C" JNIEXPORT jlong JNICALL
Java_com_example_plugin_MyPlugin_createNativeComponent(JNIEnv* env, jobject thiz, jobject callback) {
std::lock_guard<std::mutex> lock(s_componentsMapMutex);
long handle = s_nextNativeHandle++;
try {
MyNativeComponent* component = new MyNativeComponent(env, callback);
s_nativeComponents[handle] = component;
std::cout << "Native component created with handle: " << handle << std::endl;
return handle;
} catch (const std::runtime_error& e) {
std::cerr << "Error creating native component: " << e.what() << std::endl;
return 0; // エラー時は 0 を返す
}
}
extern "C" JNIEXPORT void JNICALL
Java_com_example_plugin_MyPlugin_callComponentCallback(JNIEnv* env, jobject thiz, jlong handle, jstring message) {
std::lock_guard<std::mutex> lock(s_componentsMapMutex);
auto it = s_nativeComponents.find(handle);
if (it != s_nativeComponents.end()) {
it->second->triggerCallback(env, message);
} else {
std::cout << "Native component with handle " << handle << " not found." << std::endl;
}
}
extern "C" JNIEXPORT void JNICALL
Java_com_example_plugin_MyPlugin_destroyNativeComponent(JNIEnv* env, jobject thiz, jlong handle) {
std::lock_guard<std::mutex> lock(s_componentsMapMutex);
auto it = s_nativeComponents.find(handle);
if (it != s_nativeComponents.end()) {
delete it->second; // GlobalRef は MyNativeComponent のデストラクタで解放される
s_nativeComponents.erase(it);
std::cout << "Native component with handle " << handle << " destroyed from Java." << std::endl;
} else {
std::cout << "Native component with handle " << handle << " not found for destruction." << std::endl;
}
}
// JNI_OnLoad, JNI_OnUnload は前述のコードと共通
// JNI_OnUnload ではマップに残っている全てのコンポーネントを破棄するロジックを追加
extern "C" JNIEXPORT void JNICALL JNI_OnUnload(JavaVM* vm, void* reserved) {
std::lock_guard<std::mutex> lock(s_componentsMapMutex);
for (auto const& [handle, component] : s_nativeComponents) {
std::cerr << "Warning: JNI_OnUnload: Native component with handle " << handle << " was not explicitly destroyed. Releasing." << std::endl;
delete component;
}
s_nativeComponents.clear();
g_JavaVM = nullptr;
std::cout << "JNI_OnUnload: All native components cleaned up." << std::endl;
}
このパターンでは、Java 側がネイティブリソースの所有権を持ち、その解放を明示的に制御します。これにより、ネイティブコードが GlobalRef を無期限に保持するリスクを低減できます。
パターン5: スレッドセーフな GlobalRef 管理
複数のネイティブスレッドから同じ GlobalRef にアクセスする場合、データ競合を防ぐために同期メカニズム(ミューテックスなど)を使用する必要があります。また、各スレッドは AttachCurrentThread と DetachCurrentThread を使って JVM に自身をアタッチ・デタッチし、自身の JNIEnv* を取得する必要があります。
// C++コード (前述の GlobalRef ラッパークラスと MyNativeComponent を再利用)
// g_JavaVM は JNI_OnLoad で初期化されていると仮定
// JniGlobalRef クラスのデストラクタで JNIEnv が取得できない問題は、
// JNI_OnLoad で取得した JNIEnv を JniGlobalRef のコンストラクタに渡し、
// それをメンバとして保持するか、JavaVM* を使用して JNIEnv を取得するロジックを強化することで対処できる。
// 例: JniGlobalRef の改善版 (JavaVM を通じて JNIEnv を取得)
class JniGlobalRefSafe {
public:
JniGlobalRefSafe(JNIEnv* env, jobject obj) : m_ref(nullptr) {
if (env == nullptr) {
throw std::runtime_error("JNIEnv is null.");
}
if (obj != nullptr) {
m_ref = env->NewGlobalRef(obj);
if (m_ref == nullptr) {
env->ExceptionDescribe();
env->ExceptionClear();
throw std::runtime_error("Failed to create GlobalRef.");
}
}
}
JniGlobalRefSafe(JniGlobalRefSafe&& other) noexcept : m_ref(other.m_ref) {
other.m_ref = nullptr;
}
JniGlobalRefSafe& operator=(JniGlobalRefSafe&& other) noexcept {
if (this != &other) {
release();
m_ref = other.m_ref;
other.m_ref = nullptr;
}
return *this;
}
JniGlobalRefSafe(const JniGlobalRefSafe&) = delete;
JniGlobalRefSafe& operator=(const JniGlobalRefSafe&) = delete;
~JniGlobalRefSafe() {
release();
}
jobject get() const {
return m_ref;
}
operator jobject() const {
return m_ref;
}
bool isValid() const {
return m_ref != nullptr;
}
private:
jobject m_ref;
void release() {
if (m_ref != nullptr) {
JNIEnv* env = nullptr;
bool attached = false;
if (g_JavaVM->GetEnv((void**)&env, JNI_VERSION_1_6) != JNI_OK) {
// 現在のスレッドがアタッチされていない場合はアタッチを試みる
// ただし、デストラクタで Attach/Detach は注意が必要で、
// 一般的には JNI_OnUnload のような特別な場所でのみ許容されるべき
// ここでは簡略化のため、エラーハンドリングのみ
std::cerr << "Warning: Cannot get JNIEnv for GlobalRef release. Assuming detached." << std::endl;
return;
}
if (env != nullptr) {
env->DeleteGlobalRef(m_ref);
m_ref = nullptr;
std::cout << "JniGlobalRefSafe released automatically." << std::endl;
} else {
std::cerr << "Error: JNIEnv is null after GetEnv, cannot release GlobalRef." << std::endl;
}
}
}
};
// MyNativeComponent クラスを JniGlobalRefSafe を使用するように修正
// ... (MyNativeComponent の定義を JniGlobalRef -> JniGlobalRefSafe に変更) ...
スレッドセーフなアクセスを保証するためのミューテックスは、MyNativeComponent のインスタンスや s_nativeComponents マップへのアクセスを保護するために使用されます。JniGlobalRefSafe 自体はスレッドセーフな操作を提供しますが、それを保持するコンテナやそれを使用するロジックは別途同期が必要です。
パターン6: JNI_OnLoad/JNI_OnUnload の活用
Android Plugin のネイティブライブラリがロードされる際、JVM は JNI_OnLoad 関数を呼び出します。同様に、ライブラリがアンロードされる際には JNI_OnUnload が呼び出されます。これらの関数は、GlobalRef の初期化と最終的なクリーンアップを行うための理想的な場所です。
- JNI_OnLoad:
JavaVM*ポインタをグローバル変数に保存します。これは、後でネイティブスレッドからAttachCurrentThreadを呼び出すために必要です。- 必要なクラスやメソッド ID の
GlobalRefを作成し、キャッシュします。
- JNI_OnUnload:
JNI_OnLoadで作成されたすべてのGlobalRefを解放します。JavaVM*ポインタをクリアし、その他のネイティブリソースを解放します。- この関数は、メモリリークを確実に防ぐための最後の砦です。
// JNI_OnLoad と JNI_OnUnload の完全な例 (前述のコードと組み合わせ)
#include <jni.h>
#include <iostream>
#include <stdexcept>
#include <mutex>
#include <map>
#include <atomic>
#include <thread> // for background thread example
// グローバルな JavaVM ポインタ
static JavaVM* g_JavaVM = nullptr;
// RAII を利用した JniGlobalRefSafe クラス
// ... (JniGlobalRefSafe の定義は前述のものをここに挿入) ...
// Plugin のネイティブコンポーネントクラス
// ... (MyNativeComponent の定義は前述のものをここに挿入し、JniGlobalRefSafe を使用) ...
// MyNativeComponent のインスタンスをマップで管理
static std::map<long, MyNativeComponent*> s_nativeComponents;
static std::atomic<long> s_nextNativeHandle(1);
static std::mutex s_componentsMapMutex;
// JNI_OnLoad 実装
extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
g_JavaVM = vm;
std::cout << "JNI_OnLoad: Native library loaded. JavaVM obtained." << std::endl;
// 必要に応じて、ここでキャッシュする GlobalRef を作成
// 例: 特定の Java クラスへの GlobalRef
JNIEnv* env = nullptr;
if (vm->GetEnv((void**)&env, JNI_VERSION_1_6) != JNI_OK) {
std::cerr << "JNI_OnLoad: Failed to get JNIEnv." << std::endl;
return JNI_ERR;
}
// 例: String クラスの GlobalRef をキャッシュ
// static JniGlobalRefSafe s_stringClassRef(env, env->FindClass("java/lang/String"));
// (JniGlobalRefSafe はコンストラクタで env を取るため、ここで直接作成できる)
return JNI_VERSION_1_6;
}
// JNI_OnUnload 実装
extern "C" JNIEXPORT void JNICALL JNI_OnUnload(JavaVM* vm, void* reserved) {
std::lock_guard<std::mutex> lock(s_componentsMapMutex);
std::cout << "JNI_OnUnload: Native library is being unloaded." << std::endl;
// s_nativeComponents マップに残っている全てのコンポーネントを破棄
for (auto const& [handle, component] : s_nativeComponents) {
std::cerr << "Warning: JNI_OnUnload: Native component with handle " << handle
<< " was not explicitly destroyed by Java. Releasing." << std::endl;
delete component; // これにより MyNativeComponent のデストラクタが呼ばれ、内部の GlobalRef が解放される
}
s_nativeComponents.clear();
// JNI_OnLoad でキャッシュした GlobalRef もここで解放 (JniGlobalRefSafe のデストラクタが呼ばれる)
// s_stringClassRef が static で定義されていれば、プログラム終了時に自動的にデストラクタが呼ばれる
// しかし、JNIEnv が有効でない可能性があるので、明示的に解放する方が安全な場合もある
// 例: if (s_stringClassRef.isValid()) s_stringClassRef.~JniGlobalRefSafe();
g_JavaVM = nullptr;
std::cout << "JNI_OnUnload: All native resources cleaned up." << std::endl;
}
// JNIEXPORT 関数群は前述のものをここに挿入
// Java_com_example_plugin_MyPlugin_createNativeComponent
// Java_com_example_plugin_MyPlugin_callComponentCallback
// Java_com_example_plugin_MyPlugin_destroyNativeComponent
Android Plugin 開発における具体的な考慮事項
Plugin のライフサイクルと JNI_OnUnload
Android の Plugin フレームワーク(例: Gradle Plugin, IDE Plugin, またはカスタムのインプロセス/アウトプロセス Plugin)は、その性質上、動的なロードとアンロードが頻繁に発生します。JNI_OnUnload は、このアンロードイベントをフックし、ネイティブリソースをクリーンアップするための決定的な機会を提供します。GlobalRef を保持するすべてのネイティブモジュールは、アンロード時にこれらの参照を確実に解放する責任があります。これを怠ると、Plugin がアンロードされた後も Java オブジェクトが JVM 上に残り続け、メモリリークとして認識されます。
ClassLoader と GlobalRef
Android では、アプリやライブラリが独自の ClassLoader を使用することがあります。Plugin が独自の ClassLoader を使用して Java クラスをロードし、そのクラスのインスタンスへの GlobalRef をネイティブコードが保持する場合、その ClassLoader がアンロードされる際に、関連するすべての GlobalRef も解放される必要があります。GlobalRef は ClassLoader とは独立して JVM 全体で有効ですが、参照先のオブジェクトが ClassLoader のスコープ外にある場合、WeakGlobalRef を使用することで、ClassLoader がアンロードされ、オブジェクトがガベージコレクションされたときにネイティブ参照が自動的に無効になるようにできます。
コンテキストの喪失
Android の UI コンポーネント(Activity、Fragment、View など)はライフサイクルが複雑で、頻繁に再作成されたり破棄されたりします。もしネイティブコードがこれらの一時的な UI コンポーネントへの GlobalRef を保持している場合、コンポーネントが破棄された後に GlobalRef が解放されなければ、メモリリークが発生します。このシナリオでは、WeakGlobalRef が非常に有効です。WeakGlobalRef を使用することで、UI コンポーネントが不要になりガベージコレクションされた場合でも、ネイティブコードは安全にその参照を失効させることができます。ネイティブコードが WeakGlobalRef を使用する際には、常に env->NewLocalRef(weakRef) を呼び出して LocalRef に変換し、NULL チェックを行うことで、参照が有効であるかを確認する必要があります。
デバッグとプロファイリング
GlobalRef に起因するメモリリークを特定し、解決するためには、適切なデバッグツールとプロファイリングツールの活用が不可欠です。
- Android Studio Memory Profiler: Android Studio に組み込まれている Memory Profiler は、アプリのメモリ使用量をリアルタイムで監視し、ヒープダンプを取得できます。ヒープダンプを分析することで、どの Java オブジェクトがどこから参照されているかを確認し、リークしている
GlobalRefに関連するオブジェクトを特定できます。 - LeakCanary: Square 社が提供する LeakCanary は、Android のメモリリーク検出ライブラリです。
ActivityやFragmentなどのコンポーネントのリークを自動的に検出し、ヒープダンプを分析してリークパスを報告してくれます。JNI レイヤーでのリークを直接検出するわけではありませんが、JNI 経由で保持されている Java オブジェクトがリークしている場合に、その Java オブジェクトを特定するのに役立ちます。 - MAT (Memory Analyzer Tool): Eclipse Memory Analyzer Tool (MAT) は、ヒープダンプ(.hprof ファイル)を詳細に分析するための強力なスタンドアロンツールです。MAT を使用すると、オブジェクトの保持ツリーを探索し、
GlobalRefやWeakGlobalRefがどこからどのように参照されているかを深く掘り下げて確認できます。GlobalRefは通常、GC ルートとして表示されるため、MAT はリークの根本原因を特定する上で非常に価値があります。 - JNI
GetReferenceType関数: デバッグ時には、JNIEnv* env->GetReferenceType(jobject ref)関数を使用して、特定のjobjectがJNILocalRefType、JNIGlobalRefType、JNIWeakGlobalRefTypeのいずれであるかを確認できます。これは、ネイティブコード内で参照タイプを動的に確認し、意図しない参照が作成されていないか、あるいは意図した参照が正しく管理されているかを検証するのに役立ちます。 - Logcat での JNI エラーメッセージ: JNI は、不適切な関数呼び出しやメモリ不足などの問題が発生した場合、Logcat にエラーメッセージを出力します。これらのメッセージを注意深く監視し、
DeleteGlobalRefの失敗やNewGlobalRefの失敗に関する警告がないかを確認することが重要です。
堅牢な GlobalRef 管理への道
JNI の GlobalRef 管理は、Android Plugin の安定性とパフォーマンスを保証するために不可欠な要素です。本講義で紹介したベストプラクティス、特に RAII パターンに基づいたラッパークラスの利用、WeakGlobalRef の適切な適用、そして JNI_OnUnload での確実なクリーンアップは、メモリリークを回避するための強力な基盤となります。
常にオブジェクトのライフサイクルを明確に定義し、GlobalRef が不要になったら直ちに解放するという原則を徹底してください。また、開発段階からプロファイリングツールを積極的に活用し、潜在的なリークを早期に発見し対処することで、より堅牢で信頼性の高い Android Plugin を構築できるでしょう。