在 Android NDK 開發中,常需從 Native 代碼 (C/C++) 呼叫 Java 層的類別與方法,例如利用 Java 的 Canvas 繪製文字生成 Bitmap,再傳回 OpenGL 紋理。本文完整記錄從 Java 類別設計、JNI 簽名獲取、到 Native 端實例化、呼叫方法、處理回傳值的全流程。
package com.clouddevelop.cloudbox;
import android.graphics.*;
public class TextManager {
public Bitmap create(String text, float size) {
try {
Paint paint = new Paint();
paint.setColor(Color.WHITE);
paint.setTextSize(size);
paint.setAlpha(255);
paint.setFlags(Paint.ANTI_ALIAS_FLAG);
paint.setTextAlign(Paint.Align.LEFT);
paint.setAntiAlias(true);
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;
} catch (Exception e) {
return null;
}
}
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(); }
}
使用 javap -s 指令輸出類別的方法簽名:
$ javap -s com.clouddevelop.cloudbox.TextManager
Compiled from "TextManager.java"
public class com.clouddevelop.cloudbox.TextManager {
public com.clouddevelop.cloudbox.TextManager();
descriptor: ()V
public android.graphics.Bitmap create(java.lang.String, float);
descriptor: (Ljava/lang/String;F)Landroid/graphics/Bitmap;
public int getWidth(android.graphics.Bitmap);
descriptor: (Landroid/graphics/Bitmap;)I
public int getHeight(android.graphics.Bitmap);
descriptor: (Landroid/graphics/Bitmap;)I
public void getPixels(android.graphics.Bitmap, int[]);
descriptor: (Landroid/graphics/Bitmap;[I)V
public void close(android.graphics.Bitmap);
descriptor: (Landroid/graphics/Bitmap;)V
}
簽名規則:()V 無參無回傳、(Ljava/lang/String;F)Landroid/graphics/Bitmap; 接受 String、float 回傳 Bitmap、[I 代表 int 陣列。
JNIEnv* g_env = nullptr;
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 回收
}
jobject createJavaInstance(JNIEnv* env, jclass clazz) {
jmethodID ctor = env->GetMethodID(clazz, "", "()V");
return env->NewObject(clazz, ctor);
}
GLuint createText(const char* text, float size, float* rWidth, float* rHeight) {
// 1. 尋找類別
jclass clazz = g_env->FindClass("com/clouddevelop/cloudbox/TextManager");
// 2. 建立實例 (或使用傳入的 g_textmgr)
jobject textMgr = createJavaInstance(g_env, clazz);
// 3. 獲取 create 方法 ID
jmethodID midCreate = g_env->GetMethodID(clazz, "create",
"(Ljava/lang/String;F)Landroid/graphics/Bitmap;");
// 4. 準備參數並呼叫
jstring jText = g_env->NewStringUTF(text);
jobject bitmap = g_env->CallObjectMethod(textMgr, midCreate, jText, size);
g_env->DeleteLocalRef(jText);
// 5. 獲取寬高
jmethodID midW = g_env->GetMethodID(clazz, "getWidth", "(Landroid/graphics/Bitmap;)I");
jmethodID midH = g_env->GetMethodID(clazz, "getHeight", "(Landroid/graphics/Bitmap;)I");
int width = g_env->CallIntMethod(textMgr, midW, bitmap);
int height = g_env->CallIntMethod(textMgr, midH, bitmap);
*rWidth = width; *rHeight = height;
// 6. 取得像素資料
jintArray pixelsArray = g_env->NewIntArray(width * height);
jmethodID midPixels = g_env->GetMethodID(clazz, "getPixels", "(Landroid/graphics/Bitmap;[I)V");
g_env->CallVoidMethod(textMgr, midPixels, bitmap, pixelsArray);
jint* pixels = g_env->GetIntArrayElements(pixelsArray, 0);
// 7. 建立 OpenGL 紋理
GLuint texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
// 8. 清理資源
g_env->ReleaseIntArrayElements(pixelsArray, pixels, 0);
g_env->DeleteLocalRef(pixelsArray);
jmethodID midClose = g_env->GetMethodID(clazz, "close", "(Landroid/graphics/Bitmap;)V");
g_env->CallVoidMethod(textMgr, midClose, bitmap);
g_env->DeleteLocalRef(bitmap);
g_env->DeleteLocalRef(textMgr);
return texture;
}
NewGlobalRef / DeleteGlobalRef 配對使用,避免記憶體洩漏或過早回收。AttachCurrentThread 獲取有效 JNIEnv*。ExceptionCheck(),防止 Java 異常傳播導致 Native 崩潰。GetPrimitiveArrayCritical (需謹慎使用)。NDK 呼叫 Java 類別的核心流程為:FindClass → GetMethodID → NewObject/CallMethod → 處理回傳值 → 釋放引用。掌握 javap -s 獲取簽名、區分區域/全域引用、正確管理 JNI 環境指標,是穩健開發混合應用的關鍵。