JNI

JNI

JNI是指程式運行時Java程式碼可以使用C或C++的lib,也可以在C或C++的lib使用Java程式碼。

NDK

是一個工具,包含Android的交叉編譯器,Android可以用的靜態庫與動態庫。

建立Java可以呼叫的C++

java sdk

我是MAC電腦,在終端機執行以下指令,就會顯示java sdk的路徑

$ /usr/libexec/java_home
/Users/cici/Library/Java/JavaVirtualMachines/openjdk-20.0.1/Contents/Home

native

在函式前面加上native,表示這個是呼叫c++函式。
MainActivity.java

1
public native String stringFromJNI();

c++函式與java函式相互跳躍

img

img

jni.h

先include jni.h的head file
native-lib.cpp

1
#include <jni.h>

extern c

若是c++寫的程式,前面都要加上extern c,Java才可以辦識這是C++程式
native-lib.cpp

1
extern "C" JNIEXPORT jstring JNICALL

傳回值

c++函式傳回值的型態是jstring
MainActivity.java

1
public native String stringFromJNI();

java函式傳回值是String

native-lib.cpp

1
extern "C" JNIEXPORT jstring JNICALL

Java的型態與c++型態

Java類別 jni類別
void void
boolean jboolean
byte jbyte
char jchar
short jshort
int jint
long jlor
float jfloat
double jdouble
Object jobject
Class jclass
String jstring
Object[] jobjectArray
boolean[] jbooleanArray
byte[] jbyteArray
char[] jcharArray
short[] jshortArray
int[] jintArray
long[] jlongArray
float[] jfloatArray
double[] jdoubleArray

jni函式參數

以下是c++中的jni函式

1
2
3
4
Java_com_example_ndkproj_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject thiz) {
}

JNIEnv

代表Java虛擬機,透過Java虛擬機可以取得Java類別物件。

jobject instance

呼叫c++函式的類別,以本頁的例子是MainActivity.java

動態庫so檔

project編譯完成會打包成.so檔,系統啟動的時候會去把專案ndkproj.so Load進來

1
2
3
4
5
6
public class MainActivity extends AppCompatActivity {
    static {
        System.loadLibrary("ndkproj");
    }
    ...
}

jni取得字串

MainActivity.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MainActivity extends AppCompatActivity {
    static {
        System.loadLibrary("ndkproj");
    }
    private ActivityMainBinding binding;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());

        // Example of a call to a native method
        TextView tv = binding.sampleText;
        tv.setText(stringFromJNI() + stringFromJNI2("abcdefg"));
    }
    public native String stringFromJNI();
    public native String stringFromJNI2(String str);
}

native-lib.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <jni.h>
#include <string>
#include <android/log.h>
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_ndkproj_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */) {
    // 使用 __android_log_print() 輸出到 Logcat
    __android_log_print(ANDROID_LOG_DEBUG, "NDK_TEST", "test() 返回值: %d", result);
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}
extern "C"
JNIEXPORT jstring JNICALL
Java_com_example_ndkproj_MainActivity_stringFromJNI2(JNIEnv *env, jobject thiz,
                                                     jstring _str) {
    const char* str = env->GetStringUTFChars(_str, 0);
    env->ReleaseStringUTFChars(_str, str);
    return env->NewStringUTF("test");
}

jni陣列類型轉型

MainActivity.java

1
2
3
4
5
6
7
8
9
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ...
        int intArr[] = {11, 22, 33, 44};
        String strArr[] = {"test1", "test2", "test3"};
        test2(1, "Hello World", intArr, strArr);
        Log.e("Java", Arrays.toString(intArr));
    }

native-lib.cpp要include Android寫Log的head file

#include <android/log.h>
// ...代表__VA_ARGS__,可變參數
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, "JNI",__VA_ARGS__);

native-lib.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
extern "C"
JNIEXPORT void JNICALL
Java_com_example_ndkproj_MainActivity_test2(JNIEnv *env, jobject thiz, jint num, jstring str,
                                            jintArray int_arr, jobjectArray str_arr) {
    // 取得陣列指標
    jint *int_arr_p = env->GetIntArrayElements(int_arr, NULL);
    // 取得長度
    int32_t len = env->GetArrayLength(int_arr);
    for (int i = 0; i < len; i++) {
        // 參數1:Log級別
        // 參數2:TAG
        // 參數3:印出的值
        // 參數4:把值傳給參數3
        //__android_log_print(ANDROID_LOG_ERROR, "JNI", "取得值%d", *(int_arr_p + i));
        LOGE("取得值%d", *(int_arr_p + i));
        // 修改值
        *(int_arr_p + i) = *(int_arr_p + i) + 100;
    }
    // 使用GetIntArrayElements,就要釋放記憶體空間
    // 釋放記憶體空間
    // 參數1: java傳來的int_arr
    // 參數2: 由GetIntArrayElements得到的指標
    // 參數3: 模式
    // 0 : 更新java傳來的int_arr,並且釋放指標int_arr記憶體空間
    // 1 : 只更新java傳來的int_arr
    // 2 : 只釋放指標int_arr記憶體空間
    env->ReleaseIntArrayElements(int_arr, int_arr_p, 0);
    jint strlen = env->GetArrayLength(str_arr);
    for (int i = 0; i < strlen; i++) {
        jstring str = static_cast<jstring>(env->GetObjectArrayElement(str_arr, i));
        // 將jstring轉成c++
        const char* s = env->GetStringUTFChars(str, 0);
        LOGE("取得值%s", s);
        // release memory
        env->ReleaseStringUTFChars(str, s);
    }
}

Android Studio Log

img img

jni簽名

Java類別 簽名
boolean Z
short S
float F
byte B
int I
double D
char C
long J
void V
String Ljava/lang/String;
Array [+類別簽名

jni取得物件,執行成員函式,修改成員變數

Bean1.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.example.ndkproj;
import android.util.Log;
public class Bean {
    private static final String TAG = "Bean";
    private int data;
    public int getData() {
        return data;
    }
    public void setData(int data) {
        Log.e(TAG, "somebody setdata");
        this.data = data;
    }
    public static void printInfo(String msg) {
        Log.e(TAG, msg);
    }
    public static void printInfo(Bean2 bean2) {
        Log.e(TAG, "param is object:" + bean2.i);
    }
}

Bean2.java

1
2
3
4
5
6
7
8
9
package com.example.ndkproj;
public class Bean2 {
  int i = 12345;
  public Bean2() {
  }
  public Bean2(int i) {
    this.i = i;
  }
}

MainActivity.java

1
2
3
4
5
6
7
8
9
10
11
12
13
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());

        // Example of a call to a native method
        Bean bean = new Bean();
        bean.setData(5000);
        passObject(bean);
    }
    native void passObject(Bean bean);

native-lib.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
extern "C"
JNIEXPORT void JNICALL
Java_com_example_ndkproj_MainActivity_passObject(JNIEnv *env, jobject thiz, jobject bean) {
  // 1. 取得class
  jclass beanCls = env->GetObjectClass(bean);
  // 2. 取得get方法
  // 參數1:class
  // 參數2:函式名
  // 參數3:簽名()I代表無法參數,回傳int
  jmethodID getI = env->GetMethodID(beanCls, "getData", "()I");
  // 3. 呼叫方法
  jint i = env->CallIntMethod(bean, getI);
  LOGE("getI():%d", i);
  // set方法
  // (I)V參數是Int,傳回值是Void
  jmethodID setI = env->GetMethodID(beanCls, "setData", "(I)V");
  env->CallVoidMethod(bean, setI, 1000);
  // static方法
  jmethodID printInfo = env->GetStaticMethodID(beanCls, "printInfo", "(Ljava/lang/String;)V");
  // 建立java 字串
  jstring param1 = env->NewStringUTF("call static method");
  env->CallStaticVoidMethod(beanCls,printInfo,param1);
  // 參數是自定義類別
  // static方法
  jmethodID printInfo2 = env->GetStaticMethodID(beanCls, "printInfo", "(Lcom/example/ndkproj/Bean2;)V");
  // 建立物件jclass
  jclass bean2Cls = env->FindClass("com/example/ndkproj/Bean2");
  // 呼叫空建構子()V
  // 呼叫有int建構子(I)V
  jmethodID constructor = env->GetMethodID(bean2Cls, "<init>", "(I)V");
  // 呼叫建構子取得物件
  jobject bean2 = env->NewObject(bean2Cls, constructor, 1111);
  env->CallStaticVoidMethod(beanCls,printInfo2, bean2);
  // 釋放區域變數
  // new出來的都要釋放記憶體
  // delete方法針對jobject(FindClass,NewObject)
  env->DeleteLocalRef(bean2Cls);
  env->DeleteLocalRef(bean2);
  // 自已建立的物件,包含自己建立的jstring用delete
  env->DeleteLocalRef(param1);
  // release用在GetStringUTFChars
  //env->ReleaseStringUTFChars(str, s);
  // 取得成員變數
  jfieldID jfieldId = env->GetFieldID(beanCls, "data", "I");
  // 修改成員變數
  env->SetIntField(bean, jfieldId, 666);
}
2025-03-18 12:41:58.614 16753-16753 JNI                     com.example.ndkproj                  E  getI():5000
2025-03-18 12:41:58.610 16753-16753 Bean                    com.example.ndkproj                  E  somebody setdata
2025-03-18 12:41:58.616 16753-16753 Bean                    com.example.ndkproj                  E  somebody setdata
2025-03-18 12:41:58.620 16753-16753 Bean                    com.example.ndkproj                  E  call static method
2025-03-18 12:41:58.622 16753-16753 Bean                    com.example.ndkproj                  E  param is object:1111

記憶體釋放

以下的寫法是在建立指標(請見下一個程式碼jni.h),使用typedef把指標取成去掉*的別名,但實際上是指標。

1
2
3
jobject bean2 = env->NewObject(bean2Cls, constructor, 1111);
jstring param1 = env->NewStringUTF("call static method");
jclass bean2Cls = env->FindClass("com/example/ndkproj/Bean2");  

jni.h

1
2
3
typedef _jobject*       jobject;
typedef _jclass*        jclass;
typedef _jstring*       jstring;

jni已經做到在函式中建立指標,離開函式這些指標會自動被JVM釋放記憶體,就算沒寫以下的程式碼,也會被記憶體釋放。

1
2
3
4
5
  // delete方法針對jobject(FindClass,NewObject)
  env->DeleteLocalRef(bean2Cls);
  env->DeleteLocalRef(bean2);
  // 自已建立的物件,包含自己建立的jstring用delete
  env->DeleteLocalRef(param1);

全域參考與區域指標

NewGlobalRef

區域參考離開函式就會被記憶體回收,使用全域參考則不會被JVM釋放記憶體。

Delete GlobalRef

刪除全域參考

env->DeleteGlobalRef(global_bean_cls);
1
2
3
4
5
6
7
8
9
10
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());
        invokeBean2Method();
        invokeBean2Method();
    }
    native void invokeBean2Method();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 全域變數
jclass global_bean_cls;
//
jclass weak_bean_cls;
extern "C"
JNIEXPORT void JNICALL
Java_com_example_ndkproj_MainActivity_invokeBean2Method(JNIEnv *env, jobject thiz) {
  if (global_bean_cls == NULL) {
    // 區域變數
    jclass cls = env->FindClass("com/example/ndkproj/Bean2");
    // 建立全域參考
    global_bean_cls = static_cast<jclass>(env->NewGlobalRef(cls));
    // 釋放區域變數
    env->DeleteLocalRef(cls);
    // 釋放全域變數
    //env->DeleteGlobalRef(global_bean_cls);
  }
  jmethodID contructor = env->GetMethodID(global_bean_cls, "<init>", "(I)V");
  jobject bean2 = env->NewObject(global_bean_cls, contructor, 666);
  // 弱參考會被記憶體回收
  // 判斷弱參考是否存在
  jboolean isExist = env->IsSameObject(weak_bean_cls, NULL);
  if (isExist) {
    // 區域變數
    jclass cls = env->FindClass("com/example/ndkproj/Bean2");
    // 弱參考
    weak_bean_cls = static_cast<jclass>(env->NewWeakGlobalRef(cls));
    env->DeleteLocalRef(cls);
    // delete weak
    //env->DeleteWeakGlobalRef(weak_bean_cls);
  }
}

動態註冊jni

之前的都是靜態註冊,缺點是函式名很長,接下來要說明的是動態註冊。

JNI_OnLoad

project編譯完成會打包成.so檔,系統啟動的時候會去把專案ndkproj.so Load進來

1
2
3
4
5
6
public class MainActivity extends AppCompatActivity {
    static {
        System.loadLibrary("ndkproj");
    }
    ...
}

Load進來的時候會呼叫JNI_OnLoad()函式

1
2
3
jint JNI_OnLoad(JavaVM *vm, void *reserved) {
    return JNI_VERSION_1_6;
}

第一個參數*vm是虛擬機,第二個參數可以不用管它。
最後一定要傳回JNI_VERSION_1_6

g_method[]

是一個陣列,陣列裡包含java的函式(第1個參數)與函式簽名(第2個參數),c++的函式名(第3個參數),記得c++函式名前面一定要加上(void*)

1
2
3
static JNINativeMethod g_methods[] = {
  {"_test","()Ljava/lang/String;",(void*)my_test_register},
};

以下是java的native函式名_test(),函式簽名是()Ljava/lang/String;傳回值是String物件,沒參數。
MainActivity.java

1
2
3
4
5
public class MainActivity extends AppCompatActivity {
  ...
  public native String _test();
  ...
}

以下是C++函式要與java函式對映。
參數1是java虛擬機要寫
參數2是呼叫c++函式的類別,以本頁的例子是MainActivity.java
native-lib.cpp

1
2
3
4
5
6
extern "C"
JNIEXPORT jstring JNICALL
my_test_register(JNIEnv *env,
                 jobject instance) {
    return env->NewStringUTF("this is a test of register!");
}

取得env

第一個參數&env指標參考為要轉為(void**),第二個參數一定要是JNI_VERSION_1_6

1
2
3
4
5
6
7
8
jint JNI_OnLoad(JavaVM *vm, void *reserved) {
    // 空的env指標
    JNIEnv *env = NULL;
    // 取得env,第一個參數env指標的位址
    vm->GetEnv((void**)&env, JNI_VERSION_1_6);
    ...
    return JNI_VERSION_1_6;
}

若要檢查是否有取得到java虛擬機,可用以下程式碼。

1
2
3
4
5
6
7
    // ret = 0 JNI_OK 成功
    // ret < 0 JNI_ERR 失敗
    int ret = vm->GetEnv((void**)&env, JNI_VERSION_1_6);
    if (ret != JNI_OK) {
      // crush
      return -1;
    }

取得g_method[]長度

以下語法取得g_method長度

sizeof(g_methods)/sizeof(g_methods[0])

clazz

1
#define JNI_CLASS_PATH "com/example/ndkproj/MainActivity"

或者寫成以下方式也可以

1
static const char* clazz_path = "com/example/ndkproj/MainActivity";
1
2
    // java函式的所在的類別
    jclass clazz = env->FindClass(JNI_CLASS_PATH);

註冊

  • 參數1是java函式的類別
  • 參數2是g_methods
  • 參數3是g_methods的長度
1
env->RegisterNatives(clazz, g_methods, sizeof(g_methods)/sizeof(g_methods[0]));

完整程式碼

MainActivity.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class MainActivity extends AppCompatActivity {

    // Used to load the 'ndkproj' library on application startup.
    static {
        System.loadLibrary("ndkproj");
    }

    private ActivityMainBinding binding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());

        // Example of a call to a native method
        TextView tv = binding.sampleText;
        tv.setText(_test());
    }
    public native String _test();
}

native-lib.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <jni.h>
#include <string>
#define JNI_CLASS_PATH "com/example/ndkproj/MainActivity"
#include <android/log.h>
extern "C"
JNIEXPORT jstring JNICALL
my_test_register(JNIEnv *env,
                 jobject instance) {
    return env->NewStringUTF("this is a test of register!");
}
static JNINativeMethod g_methods[] = {
        {
            "_test",
            "()Ljava/lang/String;",
            (void*)my_test_register
        },
};
jint JNI_OnLoad(JavaVM *vm, void *reserved) {
    JNIEnv *env = NULL;
    vm->GetEnv((void**)&env, JNI_VERSION_1_6);
    jclass clazz = env->FindClass(JNI_CLASS_PATH);
    env->RegisterNatives(clazz, g_methods, sizeof(g_methods)/sizeof(g_methods[0]));
    return JNI_VERSION_1_6;
}

JNI執行緒

MainActivity.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class MainActivity extends AppCompatActivity {
    // Used to load the 'ndkproj' library on application startup.
    static {
        System.loadLibrary("ndkproj");
    }
    private ActivityMainBinding binding;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());
        testThread();
    }
    public void updateUI() {
        // 主執行緒 main thread
        if (Looper.myLooper() == Looper.getMainLooper()) {
            Toast.makeText(this, "update UI", Toast.LENGTH_SHORT).show();
        } else {
            // 非主執行緒
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    Toast.makeText(MainActivity.this, "update ui2", Toast.LENGTH_SHORT).show();
                }
            });
        }
    }
    native void testThread();
}

native-lib.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#include <jni.h>
#include <string>
#include <android/log.h>
#include <pthread.h>
// ...代表__VA_ARGS__,可變參數
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, "JNI",__VA_ARGS__);
// 全域變數
JavaVM* _vm = nullptr;
jint JNI_OnLoad(JavaVM *vm, void *reserved) {
  // 透過JNI_OnLoad取得jvm
  _vm = vm;
  return JNI_VERSION_1_6;
}
// 建立結構體
struct Context {
  // instance為mainActivity放置java函式的類別
  jobject instance;
};
void* threadTask(void* args) {
  JNIEnv *env;
  // env本來就不是多執行緒,要變成可被多執行緒使用,要用jvm中的AttachCurrentThread
  jint rtn = _vm->AttachCurrentThread(&env, 0);
  // 判斷有沒有把env加進執行緒中
  if (rtn != JNI_OK) {
    return 0;
  }
  // 參數是傳進執行緒中的context,將void*轉成Context
  Context *context = static_cast<Context*>(args);
  jclass cls = env->GetObjectClass(context->instance);
  // 取得mainActivity放置java函類的類別
  jmethodID updateUI = env->GetMethodID(cls, "updateUI", "()V");
  // 呼叫java函式
  env->CallVoidMethod(context->instance, updateUI);
  // 釋放context記憶體
  delete(context);
  context = nullptr;
  // 把env從多執行緒分離
  _vm->DetachCurrentThread();
  return 0;
}
extern "C"
JNIEXPORT void JNICALL
Java_com_example_ndkproj_MainActivity_testThread(JNIEnv *env, jobject thiz) {
  // pthread_t是long,以下可作為long pid
  pthread_t pid;
  // 建立自定義context物件
  Context *context = new Context;
  // thiz是mainActivity放置java函式的類別
  // 建立全域參考
  context->instance = env->NewGlobalRef(thiz);
  // 啟動執行緒
  // 參數1: pid
  // 參數2: 0
  // 參數3: 執行緒呼叫的函式
  // 參數4: 給函式傳參數,參數是context
  pthread_create(&pid, 0 ,threadTask, context);
}

lsn7_jni資料 jni+Specification.CHM

results matching ""

    No results matching ""