首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

JNI 實戰:在 NDK 中呼叫 Java 類別生成文字貼圖完整流程

     編輯:Java 相關知識

前言:為什麼要在 Native 層呼叫 Java?

在 Android 遊戲/圖形引擎開發中,核心渲染循環通常運行在 C++(NDK)層以追求極致效能,但 Android 的文字繪製 API(Canvas.drawText、Paint、FontMetrics)僅存在於 Java/Kotlin 層。本文完整記錄如何在 NDK 中實例化 Java 類別、呼叫方法、取得 Bitmap 像素、轉為 OpenGL 紋理,並解決 JNI 簽名獲取、全域/區域引用管理、執行緒附加等關鍵坑點。

一、Java 端工具類:TextManager

負責文字 → 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(); }
}

二、JNI 關鍵步驟解析

1. 快取 JNIEnv 與 jobject(全域引用)

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 回收
}

2. 取得類別與方法 ID(簽名核對)

方法簽名必須精確,使用 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),陣列前綴 [。

3. 在 Native 函式中建立 Java 實例並呼叫

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;
}

三、記憶體與引用管理重點

引用類型建立方式釋放方式生命週期
LocalRefCallObjectMethod、NewObject、NewStringUTF 等DeleteLocalRef / 自動釋放(native 方法返回時)當前 JNI 呼叫
GlobalRefNewGlobalRefDeleteGlobalRef手動釋放前永久有效
WeakGlobalRefNewWeakGlobalRefDeleteWeakGlobalRef不阻止 GC,需檢查是否存活
  • g_textMgr 必須用 NewGlobalRef,否則 Java 端 GC 可能回收導致 Native 崩潰。
  • 大量建立 LocalRef 時(如迴圈),需手動 DeleteLocalRef 或使用 PushLocalFrame/PopLocalFrame 避免引用表溢位(預設 512)。
  • GetIntArrayElements 返回的指標需配對 ReleaseIntArrayElements,否則記憶體洩漏或陣列鎖死。

四、常見坑點與避坑指南

  1. FindClass 失敗:類名必須用「/」分隔(com/clouddevelop/cloudbox/TextManager),且類必須已被類載入器載入(通常由 Java 端先實例化觸發)。
  2. 方法簽名不匹配:GetMethodID 返回 NULL,務必用 javap -s 核對編譯後的 .class 檔案。
  3. 執行緒附加:若在非 Java 建立的執行緒(如渲染執行緒)呼叫 JNI,必須先 JavaVM->AttachCurrentThread(&env, nullptr),用畢 DetachCurrentThread。
  4. 例外處理:每次 JNI 呼叫後檢查 env->ExceptionCheck(),若有例外需 ExceptionDescribe() 並 ExceptionClear(),否則後續 JNI 呼叫失效。

五、效能優化建議

  • 將 jclass、jmethodID 靜態快取,避免重複 FindClass/GetMethodID。
  • 文字內容不變時複用紋理 ID,避免重複生成 Bitmap、上傳 GPU。
  • 大量文字渲染考慮使用 BitmapFont / SDF 字體圖集,完全在 Native 層完成,繞過 JNI 開銷。

六、結語

JNI 橋接 Java 圖形 API 是 Android NDK 開發的必修課。掌握引用生命週期、方法簽名、執行緒附加、例外處理四大核心,即可穩定高效地在 C++ 層複用 Java 豐富的平台能力。本文完整流程可直接套用於遊戲引擎、即時標記、動態 UI 等場景,助你打通 Native ↔ Java 雙向通道。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved