在 Android 遊戲/圖形引擎開發中,核心渲染循環通常運行在 C++(NDK)層以追求極致效能,但 Android 的文字繪製 API(Canvas.drawText、Paint、FontMetrics)僅存在於 Java/Kotlin 層。本文完整記錄如何在 NDK 中實例化 Java 類別、呼叫方法、取得 Bitmap 像素、轉為 OpenGL 紋理,並解決 JNI 簽名獲取、全域/區域引用管理、執行緒附加等關鍵坑點。
負責文字 → Bitmap → 像素陣列 → 釋放資源的完整鏈路:
package com.clouddevelop.cloudbox;
import android.graphics.*;
public class TextManager {
public Bitmap create(String text, float size) {
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(Color.WHITE);
paint.setTextSize(size);
paint.setTextAlign(Paint.Align.LEFT);
float[] widths = new float[text.length()];
paint.getTextWidths(text, widths);
float width = 0;
for (float w : widths) width += w;
FontMetrics fm = paint.getFontMetrics();
int height = (int) Math.ceil(fm.descent - fm.top) + 2;
Bitmap bitmap = Bitmap.createBitmap((int) width, height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
float y = (fm.ascent < 0) ? Math.abs(fm.ascent) : -fm.ascent;
canvas.drawText(text, 0, y, paint);
return bitmap;
}
public int getWidth(Bitmap bmp) { return bmp.getWidth(); }
public int getHeight(Bitmap bmp) { return bmp.getHeight(); }
public void getPixels(Bitmap bmp, int[] pixels) {
bmp.getPixels(pixels, 0, bmp.getWidth(), 0, 0, bmp.getWidth(), bmp.getHeight());
}
public void close(Bitmap bmp) { bmp.recycle(); }
}
static JNIEnv* g_env = nullptr;
static jobject g_textMgr = nullptr; // 全域引用
JNIEXPORT void JNICALL Java_com_clouddevelop_cloudbox_CloudRenderer_nativeTextInit(
JNIEnv* env, jclass cls, jobject textManager) {
g_env = env;
g_textMgr = env->NewGlobalRef(textManager); // 必須升級為全域引用,防止 GC 回收
}
方法簽名必須精確,使用 javap -s TextManager 獲取:
// create(String, float) -> Bitmap
// 簽名:(Ljava/lang/String;F)Landroid/graphics/Bitmap;
jmethodID midCreate = g_env->GetMethodID(cls, "create", "(Ljava/lang/String;F)Landroid/graphics/Bitmap;");
// getWidth(Bitmap) -> int
// 簽名:(Landroid/graphics/Bitmap;)I
jmethodID midWidth = g_env->GetMethodID(cls, "getWidth", "(Landroid/graphics/Bitmap;)I");
簽名規則:L全限定類名;表示物件,基本型別單字母(I=int, F=float, V=void),陣列前綴[。
GLuint createText(const char* text, float size, float* rWidth, float* rHeight) {
// 1. 取得類別
jclass clazz = g_env->FindClass("com/clouddevelop/cloudbox/TextManager");
// 2. 建立實例(無參建構子)
jmethodID ctor = g_env->GetMethodID(clazz, "", "()V");
jobject mgr = g_env->NewObject(clazz, ctor);
// 3. 呼叫 create 產生 Bitmap
jstring jText = g_env->NewStringUTF(text);
jobject bitmap = g_env->CallObjectMethod(mgr, midCreate, jText, size);
g_env->DeleteLocalRef(jText);
// 4. 取得寬高
*rWidth = g_env->CallIntMethod(mgr, midWidth, bitmap);
*rHeight = g_env->CallIntMethod(mgr, g_env->GetMethodID(clazz, "getHeight", "(Landroid/graphics/Bitmap;)I"), bitmap);
// 5. 取得像素陣列
jintArray arr = g_env->NewIntArray((int)(*rWidth * *rHeight));
g_env->CallVoidMethod(mgr, g_env->GetMethodID(clazz, "getPixels", "(Landroid/graphics/Bitmap;[I)V"), bitmap, arr);
jint* pixels = g_env->GetIntArrayElements(arr, 0);
// 6. 上傳 OpenGL 紋理
GLuint tex; glGenTextures(1, &tex);
glBindTexture(GL_TEXTURE_2D, tex);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, *rWidth, *rHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
// 7. 釋放資源(逆序)
g_env->ReleaseIntArrayElements(arr, pixels, 0);
g_env->DeleteLocalRef(arr);
g_env->CallVoidMethod(mgr, g_env->GetMethodID(clazz, "close", "(Landroid/graphics/Bitmap;)V"), bitmap);
g_env->DeleteLocalRef(bitmap);
g_env->DeleteLocalRef(mgr);
g_env->DeleteLocalRef(clazz);
return tex;
}
| 引用類型 | 建立方式 | 釋放方式 | 生命週期 |
|---|---|---|---|
| LocalRef | CallObjectMethod、NewObject、NewStringUTF 等 | DeleteLocalRef / 自動釋放(native 方法返回時) | 當前 JNI 呼叫 |
| GlobalRef | NewGlobalRef | DeleteGlobalRef | 手動釋放前永久有效 |
| WeakGlobalRef | NewWeakGlobalRef | DeleteWeakGlobalRef | 不阻止 GC,需檢查是否存活 |
NewGlobalRef,否則 Java 端 GC 可能回收導致 Native 崩潰。DeleteLocalRef 或使用 PushLocalFrame/PopLocalFrame 避免引用表溢位(預設 512)。GetIntArrayElements 返回的指標需配對 ReleaseIntArrayElements,否則記憶體洩漏或陣列鎖死。com/clouddevelop/cloudbox/TextManager),且類必須已被類載入器載入(通常由 Java 端先實例化觸發)。GetMethodID 返回 NULL,務必用 javap -s 核對編譯後的 .class 檔案。JavaVM->AttachCurrentThread(&env, nullptr),用畢 DetachCurrentThread。env->ExceptionCheck(),若有例外需 ExceptionDescribe() 並 ExceptionClear(),否則後續 JNI 呼叫失效。jclass、jmethodID 靜態快取,避免重複 FindClass/GetMethodID。BitmapFont / SDF 字體圖集,完全在 Native 層完成,繞過 JNI 開銷。JNI 橋接 Java 圖形 API 是 Android NDK 開發的必修課。掌握引用生命週期、方法簽名、執行緒附加、例外處理四大核心,即可穩定高效地在 C++ 層複用 Java 豐富的平台能力。本文完整流程可直接套用於遊戲引擎、即時標記、動態 UI 等場景,助你打通 Native ↔ Java 雙向通道。