JNI編程基礎
代碼地址:https://github.com/itlgc/AndroidJniSimple
JNI數據類型
JNIEXPORT 和 JNICALL,定義在
jni_md.h
頭文件中。JNIEXPORT:
在 Windows 中,定義爲
__declspec(dllexport)
。因爲Windows編譯 dll 動態庫規定,如果動態庫中的函數要被外部調用,需要在函數聲明中添加此標識,表示將該函數導出在外部可以調用。 在 Linux/Unix/Mac os/Android 這種 Like Unix系統中,定義爲
__attribute__ ((visibility ("default")))
GCC 有個visibility屬性, 該屬性是說, 啓用這個屬性:
當-fvisibility=hidden時
動態庫中的函數默認是被隱藏的即 hidden. 除非顯示聲明爲
__attribute__((visibility("default")))
.當-fvisibility=default時
動態庫中的函數默認是可見的.除非顯示聲明爲
__attribute__((visibility("hidden")))
.JNICALL:
在類Unix中無定義,在Windows中定義爲:
_stdcall
,一種函數調用約定類Unix系統中這兩個宏可以省略不加。
Java類型 | 本地類型 | 描述 |
---|---|---|
boolean | jboolean | C/C++8位整型 |
byte | jbyte | C/C++帶符號的8位整型 |
char | jchar | C/C++無符號的16位整型 |
short | jshort | C/C++帶符號的16位整型 |
int | jint | C/C++帶符號的32位整型 |
long | jlong | C/C++帶符號的64位整型 |
float | jfloat | C/C++32位浮點型 |
double | jdouble | C/C++64位浮點型 |
Object | jobject | 任何Java對象,或者沒有對應java類型的對象 |
Class | jclass | Class對象 |
String | jstring | 字符串對象 |
Object[] | jobjectArray | 任何對象的數組 |
boolean[] | jbooleanArray | 布爾型數組 |
byte[] | jbyteArray | 比特型數組 |
char[] | jcharArray | 字符型數組 |
short[] | jshortArray | 短整型數組 |
int[] | jintArray | 整型數組 |
long[] | jlongArray | 長整型數組 |
float[] | jfloatArray | 浮點型數組 |
double[] | jdoubleArray | 雙浮點型數組 |
C/C++中獲取java的數組時:
extern "C"
JNIEXPORT jstring JNICALL
Java_com_it_jnitest_MainActivity_test(JNIEnv *env, jobject instance, jobjectArray a_,jintArray b_) {
//1、 獲得字符串數組
//獲得數組長度
int32_t str_length = env->GetArrayLength(a_);
LOGE("字符串 數組長度:%d",str_length);
//獲得字符串數組的數據
for (int i = 0; i < str_length; ++i) {
jstring str = static_cast<jstring>(env->GetObjectArrayElement(a_, i));
const char* c_str = env->GetStringUTFChars(str, 0);
LOGE("字符串有:%s",c_str);
//使用完釋放
env->ReleaseStringUTFChars(str,c_str);
}
//2、獲得基本數據類型數組
int32_t int_length = env->GetArrayLength(b_);
LOGE("int 數組長度:%d",int_length);
//對應的有 GetBoolean 、GetFloat等
jint *b = env->GetIntArrayElements(b_, 0);
for (int i = 0; i < int_length; i++) {
LOGE("int 數據有:%d",b[i]);
}
env->ReleaseIntArrayElements(b_, b, 0);
return env->NewStringUTF("222");
}
C/C++反射Java
反射方法
在C/C++中反射創建Java的對象,調用Java的方法
package com.it.jnitest;
import android.util.Log;
public class Helper {
private static final String TAG = "Helper";
//private和public 對jni開發來說沒任何區別 都能反射調用
public void instanceMethod(String a,int b,boolean c){
Log.e(TAG,"instanceMethod a=" +a +" b="+b+" c="+c );
}
public static void staticMethod(String a,int b,boolean c){
Log.e(TAG,"staticMethod a=" +a +" b="+b+" c="+c);
}
}
extern "C"
JNIEXPORT void JNICALL
Java_com_it_jnitest_MainActivity_invokeHelper(JNIEnv *env, jobject instance) {
jclass clazz = env->FindClass("com/it/jnitest/Helper");
//獲得具體的靜態方法 參數3:簽名(下方說明)
//如果不會填 可以使用javap
jmethodID staticMethod = env->GetStaticMethodID(clazz,"staticMethod","(Ljava/lang/String;IZ)V");
//調用靜態方法
jstring staticStr= env->NewStringUTF("C++調用靜態方法");
env->CallStaticVoidMethod(clazz,staticMethod,staticStr,1,1);
//獲得構造方法 <init>:構造方法寫法
jmethodID constructMethod = env->GetMethodID(clazz,"<init>","()V");
//創建對象
jobject helper = env->NewObject(clazz,constructMethod);
jmethodID instanceMethod = env->GetMethodID(clazz,"instanceMethod","(Ljava/lang/String;IZ)V");
jstring instanceStr= env->NewStringUTF("C++調用實例方法");
env->CallVoidMethod(helper,instanceMethod,instanceStr,2,0);
//釋放
env->DeleteLocalRef(clazz);
env->DeleteLocalRef(staticStr);
env->DeleteLocalRef(instanceStr);
env->DeleteLocalRef(helper);
}
基本數據類型的簽名採用一系列大寫字母來表示, 如下表所示:
Java類型 | 簽名 |
---|---|
boolean | Z |
short | S |
float | F |
byte | B |
int | I |
double | D |
char | C |
long | J |
void | V |
引用類型 | L + 全限定名 + ; |
數組 | [+類型簽名 |
可以使用javap來獲取反射方法時的簽名
#cd 進入 class所在的目錄 執行: javap -s 全限定名,查看輸出的 descriptor
D:\Lance\ndk\lsn7_jni\JniTest\app\build\intermediates\classes\debug>javap -s com.it.jnitest.Helper
Compiled from "Helper.java"
public class com.it.jnitest.Helper {
public com.it.jnitest.Helper();
descriptor: ()V
public void instanceMethod(java.lang.String, int, boolean);
descriptor: (Ljava/lang/String;IZ)V
public static void staticMethod(java.lang.String, int, boolean);
descriptor: (Ljava/lang/String;IZ)V
}
新的AS目錄結構有變化,可以直接採用完整路徑查看
lgc@LGCdeMBP NDK_Master % javap -s ndk00_HelloCMake/build/intermediates/javac/debug/classes/com/it/ndk00_hellocmake/Helper.class
Compiled from "Helper.java"
public class com.it.ndk00_hellocmake.Helper {
public com.it.ndk00_hellocmake.Helper();
descriptor: ()V
public void instanceMethod(java.lang.String, int, boolean);
descriptor: (Ljava/lang/String;IZ)V
public static void staticMethod(java.lang.String, int, boolean);
descriptor: (Ljava/lang/String;IZ)V
}
反射屬性
int a = 10;
static String b = "java字符串";
private static final String TAG = "Helper";
public void testReflect() {
Log.e(TAG,"修改前 : a = " +a +" b="+b);
reflectHelper();
Log.e(TAG,"修改後 : a = " +a +" b="+b);
}
public native void reflectHelper();
extern "C"
JNIEXPORT void JNICALL
Java_com_it_jnitest_Helper_reflectHelper(JNIEnv *env, jobject instance) {
//instance 就是 helper
jclass clazz = env->GetObjectClass(instance);
//獲得int a的標示
jfieldID a = env->GetFieldID(clazz,"a","I");
int avalue = env->GetIntField(instance,a);
LOGE("獲得java屬性a:%d",avalue);
//修改屬性值
env->SetIntField(instance,a,100);
jfieldID b = env->GetStaticFieldID(clazz,"b","Ljava.lang.String;");
//獲取值
jstring bstr = static_cast<jstring>(env->GetStaticObjectField(clazz, b));
const char* bc_str = env->GetStringUTFChars(bstr,0);
LOGE("獲得java屬性b:%s",bc_str);
//修改
jstring new_str = env->NewStringUTF("C++字符串");
env->SetStaticObjectField(clazz,b,new_str);
env->ReleaseStringUTFChars(bstr,bc_str);
env->DeleteLocalRef(new_str);
env->DeleteLocalRef(clazz);
}
JNI引用
在 JNI 規範中定義了三種引用:局部引用(Local Reference)、全局引用(Global Reference)、弱全局引用(Weak Global Reference)。
局部引用
大多數JNI函數會創建局部引用。NewObject/FindClass/NewStringUTF 等等都是局部引用。
局部引用只有在創建它的本地方法返回前有效,本地方法返回後,局部引用會被自動釋放。
因此無法跨線程、跨方法使用。
extern "C"
JNIEXPORT jstring JNICALL
xxx(JNIEnv *env, jobject instance) {
//錯誤
//不能在本地方法中把局部引用存儲在靜態變量中緩存起來供下一次調用時使用。
// 第二次執行 str依然有值,但是其引用的 “C++字符串” 已經被釋放
static jstring str;
if(str == NULL){
str = env->NewStringUTF("C++字符串");
}
return str;
}
釋放一個局部引用有兩種方式:
1、本地方法執行完畢後VM自動釋放;
2、通過DeleteLocalRef手動釋放;
VM會自動釋放局部引用,爲什麼還需要手動釋放呢?
因爲局部引用會阻止它所引用的對象被GC回收。
全局引用
全局引用可以跨方法、跨線程使用,直到它被手動釋放纔會失效 。
由 NewGlobalRef 函數創建
extern "C"
JNIEXPORT jstring JNICALL
Java_com_it_jnitest_MainActivity_test1(JNIEnv *env, jobject instance) {
//正確
static jstring globalStr;
if(globalStr == NULL){
jstring str = env->NewStringUTF("C++字符串");
//刪除全局引用調用 DeleteGlobalRef
globalStr = static_cast<jstring>(env->NewGlobalRef(str));
//可以釋放,因爲有了一個全局引用使用str,局部str也不會使用了
env->DeleteLocalRef(str);
}
return globalStr;
}
弱引用
與全局引用類似,弱引用可以跨方法、線程使用。與全局引用不同的是,弱引用不會阻止GC回收它所指向的VM內部的對象 。
在對Class進行弱引用是非常合適(FindClass),因爲Class一般直到程序進程結束纔會卸載。
在使用弱引用時,必須先檢查緩存過的弱引用是指向活動的對象,還是指向一個已經被GC的對象
extern "C"
JNIEXPORT jclass JNICALL
Java_com_it_jnitest_MainActivity_test1(JNIEnv *env, jobject instance) {
static jclass globalClazz = NULL;
//對於弱引用 如果引用的對象被回收返回 true,否則爲false
//對於局部和全局引用則判斷是否引用java的null對象
jboolean isEqual = env->IsSameObject(globalClazz, NULL);
if (globalClazz == NULL || isEqual) {
jclass clazz = env->GetObjectClass(instance);
//刪除使用 DeleteWeakGlobalRef
globalClazz = static_cast<jclass>(env->NewWeakGlobalRef(clazz));
env->DeleteLocalRef(clazz);
}
return globalClazz;
}
JNI_OnLoad
調用System.loadLibrary()函數時, 內部就會去查找so中的 JNI_OnLoad 函數,如果存在此函數則調用。
JNI_OnLoad會:
告訴 VM 此 native 組件使用的 JNI 版本。
對應了Java版本,android中只支持JNI_VERSION_1_2 、JNI_VERSION_1_4、JNI_VERSION_1_6
在JDK1.8有 JNI_VERSION_1_8。
jint JNI_OnLoad(JavaVM* vm, void* reserved){
// 2、4、6都可以
return JNI_VERSION_1_4;
}
動態註冊
在此之前我們一直在jni中使用的 Java_PACKAGENAME_CLASSNAME_METHODNAME 來進行與java方法的匹配,這種方式我們稱之爲靜態註冊。
而動態註冊則意味着方法名可以不用這麼長了,在android aosp源碼中就大量的使用了動態註冊的形式
//Java:
native void dynamicNative();
native String dynamicNative(int i);
//C++:
void dynamicNative1(JNIEnv *env, jobject jobj){
LOGE("dynamicNative1 動態註冊");
}
jstring dynamicNative2(JNIEnv *env, jobject jobj,jint i){
return env->NewStringUTF("我是動態註冊的dynamicNative2方法");
}
//需要動態註冊的方法數組
static const JNINativeMethod mMethods[] = {
{"dynamicNative","()V", (void *)dynamicNative1},
{"dynamicNative", "(I)Ljava/lang/String;", (jstring *)dynamicNative2}
};
//需要動態註冊native方法的類名
static const char* mClassName = "com/it/jnitest/MainActivity";
jint JNI_OnLoad(JavaVM* vm, void* reserved){
JNIEnv* env = NULL;
//獲得 JniEnv
int r = vm->GetEnv((void**) &env, JNI_VERSION_1_4);
if( r != JNI_OK){
return -1;
}
jclass mainActivityCls = env->FindClass( mClassName);
// 註冊 如果小於0則註冊失敗
r = env->RegisterNatives(mainActivityCls,mMethods,2);
if(r != JNI_OK )
{
return -1;
}
return JNI_VERSION_1_4;
}
native線程調用Java
native調用java需要使用JNIEnv這個結構體,而JNIEnv是由Jvm傳入與線程相關的變量。
但是可以通過JavaVM的AttachCurrentThread方法來獲取到當前線程中的JNIEnv指針。
JavaVM* _vm = 0;
jobject _instance = 0;
jint JNI_OnLoad(JavaVM* vm, void* reserved){
_vm = vm;
return JNI_VERSION_1_4;
}
void *task(void *args){
JNIEnv *env;
//將本地當前線程附加到jvm,並獲得jnienv
//成功則返回0
_vm->AttachCurrentThread(&env,0);
jclass clazz = env->GetObjectClass(_instance);
//獲得具體的靜態方法 參數3:方法簽名
//如果不會填 可以使用javap
jmethodID staticMethod = env->GetStaticMethodID(clazz,"staticMethod","(Ljava/lang/String;IZ)V");
//調用靜態方法
jstring staticStr= env->NewStringUTF("C++調用靜態方法");
env->CallStaticVoidMethod(clazz,staticMethod,staticStr,1,1);
//獲得構造方法
jmethodID constructMethod = env->GetMethodID(clazz,"<init>","()V");
//創建對象
jobject helper = env->NewObject(clazz,constructMethod);
jmethodID instanceMethod = env->GetMethodID(clazz,"instanceMethod","(Ljava/lang/String;IZ)V");
jstring instanceStr= env->NewStringUTF("C++調用實例方法");
env->CallVoidMethod(helper,instanceMethod,instanceStr,2,0);
//釋放
env->DeleteLocalRef(clazz);
env->DeleteLocalRef(staticStr);
env->DeleteLocalRef(instanceStr);
env->DeleteLocalRef(helper);
//分離
_vm->DetachCurrentThread();
return 0;
}
//Helper 類方法
extern "C"
JNIEXPORT void JNICALL
Java_com_it_jnitest_Helper_nativeThread(JNIEnv *env, jobject instance) {
pthread_t pid;
//這裏注意釋放
_instance = env->NewGlobalRef(instance);
pthread_create(&pid,0,task,0);
}