Android(安卓)JNI 详解
Android JNI 详解
简介
JNI 应该是所有Android老鸟都绕不过的“坎”吧,之所以说是“坎”是因为它比较难,因为他不仅涉及Android开发者的“本命”语言—Java,还要求开发者对C/C++有相当的基础,同时如何协调两种语言的运行时也是重难点之一。
难度是有点,不过,一旦掌握,无疑会给开发者打开一扇通往新世界的大门——openGL、openSL、OpenCV等一系列优秀成熟的库,任君采摘。既可以为你的应用插上高效的翅膀,也为你装逼提供了基础,哇哈哈哈,人生在世,何处不装逼!
本文详细简介JNI在Android中的使用。主要参考https://www3.ntu.edu.sg/home/ehchua/programming/java/JavaNativeInterface.html。不过里边都是英文, 不喜看本文即可。
本文工程目录地址:https://github.com/MrHeLi/JNItest
环境
任何没有环境就说程序的文章都是耍流氓(我估计耍了不少,^_^)。
Android Studio 3.1.2Build #AI-173.4720617, built on April 14, 2018JRE: 1.8.0_152-release-1024-b02 amd64JVM: OpenJDK 64-Bit Server VM by JetBrains s.r.oWindows 7 6.1
使用Android Studio创建JNI工程
相信想要学习JNI知识的都是些老鸟,创建步骤就简略一些:
File —–> New —–> New Project (在这一步需要如下图所示一样勾上C++ 支持)—-> Next —-> Next —-> Next。
和普通工程创建不同的是,创建成功后,支持C/C++会多 native-lib.cpp 和 CMakeLists.txt两个文件。关于CMakeLists.txt会另起一篇讲。
TODO: CMakeLists.txt讲解
本文的重点集中在native-lib.cpp所代表的C/C++代码和MainActivity.java代表的java代码之间的交互。
现在的开发工具真的是强大,Android studio 在创建工程时就可以将一些JNI的工具加入工程中,再也不用执行一些琐碎的javah、gcc等命令,为Android开发人员解放了双手(也可以说解放了恐惧),节约了时间
JNI下的MainActivity分析
public class MainActivity extends AppCompatActivity { /* 使用静态代码块加载'native-lib'库,该库即为C/C++代码编译后的共享库, * 加载后才能让java层调用C/C++的代码。 * 库名称由CMakeLists.txt文件中的add_library指定。通常此处名称是native-lib的话, * 那么编译成功的共享库名称为libnative-lib.so。 **/ static { System.loadLibrary("native-lib"); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // Example of a call to a native method TextView tv = (TextView) findViewById(R.id.sample_text); tv.setText(stringFromJNI());//本地方法调用和java函数调用毫无二致 } /** * 本地'native-lib'库中实现了的本地方法,共享库会打包到本应用中 * 区别于普通java函数,在函数申明中多了个native字段,以及没有函数体 */ public native String stringFromJNI();
JNI下的C/C++代码分析
#include #include #include #include #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, "In C/C++:", __VA_ARGS__);using namespace std;extern "C" JNIEXPORT jstring JNICALLJava_com_dali_jnitest_MainActivity_stringFromJNI( JNIEnv *env, jobject /* this */) { std::string hello = "Hello from C++"; return env->NewStringUTF(hello.c_str());}
JNI中C/C++代码对应于java中的函数,命令有一定的规则。
我们看一下本例中,java函数和C/C++函数的对应关系:
java部分:
类名:com.dali.jnitest.MainActivity
方法名:public native String stringFromJNI();
c/c++部分:
JNIEXPORT jstring JNICALL Java_com_dali_jnitest_MainActivity_stringFromJNI(JNIEnv *env, jobject /* this */)
- JNIEXPORT : 可以当做JNI方法的函数申明关键字。
- jstring : 函数返回值,jstring 对应的是java的String对象。
- JNICALL : 可以认为是JNI访问的关键字,固定格式啦。
- Java_com_dali_jnitest_MainActivity_stringFromJNI: 在C函数中的方法名格式为Java_{package_and_classname}_{function_name}(JNI_arguments) ,只需要将包名中的点换成下横线就行。
- JNIEnv *env: JNI的环境引用,一个非常有用的变量,可以通过它调用所有JNI函数。
- jobject /* this */:函数调用者的对象,相当于java层中的this
JNI 基础
上面做了点环境铺垫,接下来开始上正菜。
我们都知道,java的数据类型和C/C++的数据类型并不一致,典型的例子是:java中的String是一个引用数据类型,但在C语言中的String是以NULL结尾的字符串数组。所以协调数据类型,是JNI的重点内容。
JNI 定义了如下的JNI类型用于本地代码中,对应java的数据类型:
java 基础数据类型:
下表是对应关系
JNI数据类型 | java数据类型 |
---|---|
jint | int |
jbyte | byte |
jshort | short |
jlong | long |
jfloat | float |
jdouble | double |
jchar | char |
jboolean | boolean |
- java 引用数据类型:
JNI数据类型 | java数据类型 |
---|---|
jobject | java.lang.Object |
jclass | java.lang.Class |
jstring | java.lang.String |
jthrowable | java.lang.Throwable |
在java中,数组中的数据类型和JNI的数组类型对应定义:
JNI数据类型 | java数据类型 |
---|---|
jintArray | int [] |
jbyteArray | byte [] |
jshortArray | short [] |
jlongArray | long [] |
jfloatArray | float [] |
jdoubleArray | double [] |
jcharArray | char [] |
jbooleanArray | boolean [] |
jobjectArray | Object [] |
本地程序调用基本顺序
- 使用JNI数据类型接收参数(该参数通过java程序调用传递)
- 对于JNI引用数据类型,将参数转换或者复制为本地类型。比如:jstring 转为 C-string, jintArray转为C’s int[]等等。基本数据类型,例如jint, jdouble可以直接使用而不需要转换。
- 使用本地数据类型执行程序。
- 创建一个JNI类型的对象,用作返回(return),将程序运行的结果复制到返回对象中。
- 函数返回(return)。
在JNI程序开发过程中,比较困难而极具挑战的是JNI引用类型(例如
jstring
,jobject
,jintArray
,jobjectArray
) 和C本地数据类型(例如C-string
,int[]
)之间的转换。幸好,JNI环境提供了大量的函数来处理这种转换。
Java & Native 程序之间参数传递
基本数据类型传递
java中的8种基本数据类型可以直接被传递和使用,因为这些类型都在jni.h
中申明了:
/* Primitive types that match up with Java equivalents. */typedef uint8_t jboolean; /* unsigned 8 bits */typedef int8_t jbyte; /* signed 8 bits */typedef uint16_t jchar; /* unsigned 16 bits */typedef int16_t jshort; /* signed 16 bits */typedef int32_t jint; /* signed 32 bits */typedef int64_t jlong; /* signed 64 bits */typedef float jfloat; /* 32-bit IEEE 754 */typedef double jdouble; /* 64-bit IEEE 754 */
示例
Java 层:
public class MainActivity extends AppCompatActivity { // 在程序开始时使用静态代码块加载'native-lib'库 static { System.loadLibrary("native-lib"); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Log.i("In java", String.valueOf(average(3, 4))); } //基本数据类型在c/java之间的传递 public native double average(int arg1, int arg2);
C 层:
extern "C"JNIEXPORT jdouble JNICALLJava_com_dali_jnitest_MainActivity_average(JNIEnv *env, jobject instance, jint arg1, jint arg2) { jdouble result;//基本数据类型无需变化,在jni.h中已经设置了类型别名 /** *想要使用该打印,请在C文件头增加下列代码: *include *define LOGI(...) __android_log_print(ANDROID_LOG_INFO, "In C/C++:", __VA_ARGS__); */ LOGI("arg1: %d, ar2: %d", arg1, arg2); result = (arg1 + arg2) / 2; return result;}
运行程序:
com.dali.jnitest I/In C/C++: arg1: 3, ar2: 4com.dali.jnitest I/In java: 3.0
字符串传递
示例
java层:
public class MainActivity extends AppCompatActivity { // 在程序开始时使用静态代码块加载'native-lib'库 static { System.loadLibrary("native-lib"); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Log.i("In java", testString("hello")); } //字符串在c/java之间的传递 public native String testString(String str);
c层:
extern "C"JNIEXPORT jstring JNICALLJava_com_dali_jnitest_MainActivity_testString(JNIEnv *env, jobject instance, jstring str_) { //在C中的String是以NULL结尾的字符串数组,需要通过特定方法转换,但在C++中有对应的String,是否可以不 //转换? const char *str = env->GetStringUTFChars(str_, 0); LOGI("str: %s", str); char* returnValue = "hehe ,wo lai le"; env->ReleaseStringUTFChars(str_, str); return env->NewStringUTF(returnValue);}
JNI 定义了jstring
类型来代表java
的String
。C层函数的最后一个参数(JNI类型的jstring
)是Java层的String
传递到C层的引用。该程序的返回值同样也是jstring
类型。
传递字符串远比基本数据类型复杂,因为Java层的String
是一个对象(引用数据类型),然而C层中的string
是一个以NULL
结尾的char
数组。所以,使用时需要在Java层的String
(以JNI 的jstring
表示)和C层的string
(char*
)之间转换。
JNI环境(通过参数JNIENV*
调用)提供了这种转换的函数:
- 使用
const char* GetStringUTFChars(JNIEnv*, jstring, jboolean*)
将JNIstring
(jstring
)类型转换为C层的string
(char*
)。 - 使用
jstring NewStringUTF(JNIEnv*, char*)
将C层的string
(char*
)转为JNIstring
(jstring
)类型。
C层函数的实现步骤为:
- 从JNI的
jstring
接收数据,并通过GetStringUTFChars()
转为C层的string
(char*
)类型。 - 然后执行程序,显示接收到的参数数据,并返回另外一个字符串。
- 将C层的
string
(char*
)类型通过NewStringUTF()
函数转换为JNI的jstring
类型并返回。
运行程序:
com.dali.jnitest I/In C/C++: str: hellocom.dali.jnitest I/In java: hehe ,wo lai le
JNI本地String函数
JNI支持Unicode(16字节字符串)和UTF-8(1-3字节编码)不同格式字符串之间的转换。UTF-8编码的字符串和C语言中的字符串一样是以NULL
结尾的char
数组,用于C/C++程序中。
这些JNI字符串(jstring
)为:
/** UTF-8 String (encoded to 1-3 byte, backward compatible with 7-bit ASCII)* 获取以NULL结尾的字符数组,也就是C-string*/// 返回表示UTF-8编码字符串的数组指针const char * GetStringUTFChars(jstring string, jboolean *isCopy);// 通知VM 本地代码不再需要UTF引用。void ReleaseStringUTFChars(jstring string, const char *utf);// 根据字符串数组,构造一个UTF-8编码的java String新对象jstring NewStringUTF(const char *bytes);// 返回UTF-8编码字符串的长度jsize GetStringUTFLength(jstring string);// 将从偏移量start开始的length长度的Unicode字符转换为UTF-8编码,并将结果放在给定的缓冲区buf中。void GetStringUTFRegion(jstring str, jsize start, jsize length, char *buf);// Unicode Strings (16-bit character)// 返回指向Unicode字符数组的指针const jchar * GetStringChars(jstring string, jboolean *isCopy);// 通知VM本机代码不再需要访问字符。void ReleaseStringChars(jstring string, const jchar *chars);// 从Unicode字符数组构造一个新的java.lang.String对象。jstring NewString(const jchar *unicodeChars, jsize length);// 返回Java字符串的长度(Unicode字符数)。jsize GetStringLength(jstring string);// 将从偏移量=start开始的length长度的Unicode字符数复制到给定的缓冲区buf。void GetStringRegion(jstring str, jsize start, jsize length, jchar *buf);
UTF-8 strings & C-strings
GetStringUTFChars()
函数可用于从给定的Java的jstring
创建新的C字符串(char *
)。 如果无法分配内存,则该函数返回NULL
。 检查NULL
是一个好习惯。
第三个参数isCopy(of jboolean *)
,它是一个“in-out”参数,如果返回的字符串是原始java.lang.String
实例的副本,则将设置为JNI_TRUE
。 如果返回的字符串是指向原始String
实例的直接指针,则它将设置为JNI_FALSE
- 在这种情况下,本机代码不应修改返回的字符串的内容。 如果可能,JNI运行时将尝试返回直接指针; 否则,它返回一份副本。 尽管如此,我们很少对修改底层字符串感兴趣,并且经常传递NULL指针。
不使用使用GetStringUTFChars()
返回的字符串时,需要来释放内存和引用以便可以对其进行垃圾回收时,始终调用ReleaseStringUTFChars()
。
NewStringUTF()
函数使用给定的C字符串创建一个新的JNI字符串(jstring
)。
JDK 1.2引入了GetStringUTFRegion()
,它将jstring
(或从长度开始的一部分)复制到“预分配”的C的char
数组中。 可以使用它们代替GetStringUTFChars()
。 由于预先分配了C的数组,因此不需要isCopy
。
JDK 1.2还引入了Get / ReleaseStringCritical()
函数。 与GetStringUTFChars()
类似,如果可能,它返回一个直接指针; 否则,它返回一份副本。 本机方法不应阻止(对于IO或其他)一对GetStringCritical()
和ReleaseStringCritical()
调用。
有关详细说明,请始终参阅“Java Native Interface Specification”@ http://docs.oracle.com/javase/7/docs/technotes/guides/jni/index.html。
基本数据类型数组传递
示例
java层:
public class MainActivity extends AppCompatActivity { // 在程序开始时使用静态代码块加载'native-lib'库 static { System.loadLibrary("native-lib"); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); int[] numbers = {22, 33, 33}; double[] results = sumAndAverage(numbers); Log.i("In java, the sum is ", ""+results[0]); Log.i("In java, the average is", " "+results[1]); } //基础数据类型的数组传递 public native double[] sumAndAverage(int arr[]);
C层:
在Java中,数组(array
)和类(class
)一样,是引用数据类型。总共有9中数组类型,其中,8种基本数据类型数组和一个java.lang.Object
数组类型。对于8中基本数据类型,JNI分别定义了以下数据类型与之对应jintArray
, jbyteArray
, jshortArray
, jlongArray
, jfloatArray
, jdoubleArray
, jcharArray
, jbooleanArray
。
同样的,你在编码过程中需要在JNI数据和本地数组之间转换,例如:jintArray
与 C的jint[]
,jdoubleArray
与 C的jdouble[]
。JNI环境提供了支持这些转换的函数如下:
JNI ——》C本地代码:
使用
jint* GetIntArrayElements(JNIEnv *env, jintArray a, jboolean *iscopy)
,从JNI的jintArray
获取C语言的本地数组jint[]
。C本地代码 ——》JNI:
- 首先使用
jintArray NewIntArray(JNIEnv *env, jsize len)
申请一片内存 - 然后使用
void SetIntArrayRegion(JNIEnv *env, jintArray a, jsize start, jsize len, const jint *buf)
将数据从C本地代码的jint[]
复制到JNI层的jintArray
。
- 首先使用
如上面的两个函数特定用于int类型,与此类似,JNI中总共有8组类似函数对应于8中基本数据类型。
回到示例代码,基本数据类型数组传递的典型本地代码步骤如下:
- 从JNI参数接收数组数据,转换为C代码的本地数据(例如,
jint[]
)。 - 执行程序。
- 将本地C代码的数据(例如,
jdouble[]
)转换为JNI数组(例如,jdoubleArray
),并且返回JNI数据。
代码如下:
extern "C"JNIEXPORT jdoubleArray JNICALLJava_com_dali_jnitest_MainActivity_sumAndAverage(JNIEnv *env, jobject instance, jintArray arr_) { //从JNI参数接收数组数据,转换为C代码的本地数据(例如,jint[])。 jint *arr = env->GetIntArrayElements(arr_, NULL); if (NULL == arr) return NULL; jsize length = env->GetArrayLength(arr_); // 执行程序 jint sum = 0; int i; for (i = 0; i < length; i++) { sum += arr[i]; } jdouble average = (jdouble)sum / length; env->ReleaseIntArrayElements(arr_, arr, 0); // release resources jdouble outCArray[] = {(jdouble)sum, average}; // 将本地C代码的数据(例如,jdouble[])转换为JNI数组(例如,jdoubleArray),并且返回JNI数据。 jdoubleArray outJNIArray = env->NewDoubleArray(2); // allocate if (NULL == outJNIArray) return NULL; env->SetDoubleArrayRegion(outJNIArray, 0 , 2, outCArray); // copy return outJNIArray;}
执行结果:
com.dali.jnitest I/In java, the sum is: 88.0com.dali.jnitest I/In java, the average is: 29.333333333333332
JNI 基本数据类型数组相关函数
// ArrayType: jintArray, jbyteArray, jshortArray, jlongArray, jfloatArray, jdoubleArray, jcharArray, jbooleanArray// PrimitiveType: int, byte, short, long, float, double, char, boolean// NativeType: jint, jbyte, jshort, jlong, jfloat, jdouble, jchar, jbooleanNativeType * GetArrayElements(ArrayType array, jboolean *isCopy);void ReleaseArrayElements(ArrayType array, NativeType *elems, jint mode);void GetArrayRegion(ArrayType array, jsize start, jsize length, NativeType *buffer);void SetArrayRegion(ArrayType array, jsize start, jsize length, const NativeType *buffer);ArrayType NewArray(jsize length);void * GetPrimitiveArrayCritical(jarray array, jboolean *isCopy);void ReleasePrimitiveArrayCritical(jarray array, void *carray, jint mode);
总结如下:
GET|Release<*PrimitiveType*>ArrayElements()
函数可用于根据java的jxxxArray
创建C代码的本地jxxx[]数组。
GET | Set
可用于将ArrayRegion() jxxxArray
(或从长度开始的一部分)复制到预分配的C本地数组jxxx []
。New
可用于分配给定大小的新Array() jxxxArray
。 然后,您可以使用Set
函数从本机数组ArrayRegion() jxxx []
中填充其内容。Get | ReleasePrimitiveArrayCritical()
函数不允许在get
和elease
之间阻塞调用。
访问对象的变量和函数回调
访问对象的内部变量
示例
java层:
public class MainActivity extends AppCompatActivity { // 在程序开始时使用静态代码块加载'native-lib'库 static { System.loadLibrary("native-lib"); } // 实例的内部变量 private int number = 88; private String message = "帅呆了!"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); modifyInstanceVariable(); System.out.println("In Java, int is " + number); System.out.println("In Java, String is " + message); } public native void modifyInstanceVariable();}
C层:
extern "C"JNIEXPORT void JNICALLJava_com_dali_jnitest_MainActivity_modifyInstanceVariable(JNIEnv *env, jobject instance) { // 获得调用java层对象的引用 jclass thisClass = env->GetObjectClass(instance); // int // 获取调用java层对象中number变量的fieldID jfieldID fidNumber = env->GetFieldID(thisClass, "number", "I"); if (NULL == fidNumber) return; // 通过fieldID获取number变量中的值 jint number = env->GetIntField(instance, fidNumber); LOGI("In C, the int is %d\n", number); // 修改变量 number = 99; env->SetIntField(instance, fidNumber, number); // 获取调用java层对象中message变量的fieldID jfieldID fidMessage = env->GetFieldID(thisClass, "message", "Ljava/lang/String;"); if (NULL == fidMessage) return; // String // 通过fieldID获取object变量中的值 jstring message = static_cast(env->GetObjectField(instance, fidMessage)); // 通过JNI字符串创建C代码字符串 const char *cStr = env->GetStringUTFChars(message, NULL); if (NULL == cStr) return; LOGI("In C, the string is %s\n", cStr); env->ReleaseStringUTFChars(message, cStr); //创建C代码字符串,并分配给JNI字符串 message = env->NewStringUTF("C:不你很蠢"); if (NULL == message) return; // 修改实例变量 env->SetObjectField(instance, fidMessage, message);}
执行:
com.dali.jnitest I/In C/C++: In C, the int is 88com.dali.jnitest I/In C/C++: In C, the string is java:帅呆了!com.dali.jnitest I/In Java, int is: 99com.dali.jnitest I/In Java, String is: C:不你很蠢
访问实例变量的基本步骤
- 通过
GetObjectClass()
函数class对象的引用。 - 从类引用中通过
GetFieldID()
获取要访问的实例变量的字段ID。 参数中,需要提供变量名称及其字段描述符(或签名)。 对于Java类,字段描述符的形式为“L ”,点用正斜杠(/)替换,例如,String的类描述符是“Ljava / lang / String;”。 对于基本数据类型,使用“I”表示int,“B”表示字节,“S”表示short,“J”表示long,“F”表示float,“D”表示double,“C”表示char,以及“Z”表示 boolean。 对于数组,需要加上前缀“[”,例如“[Ljava / lang / Object;” 表示一个Object数组; “[I”代表一个int数组。 - 基于Field ID,通过
GetObjectField()
或Get
函数检索实例变量。Field() - 根据提供字段ID,并通过
SetObjectField()
或Set
函数,更新实例变量。Field()
用于访问实例变量的JNI函数如下:
//返回java层调用者实例classjclass GetObjectClass(jobject obj);//返回访问者实例的field ID jfieldID GetFieldID(jclass cls, const char *name, const char *sig);//Get/Set 实例中变量的值,// 包含8中基本数据类型,加上一个Object类型。 NativeType GetField(jobject obj, jfieldID fieldID);void SetField(jobject obj, jfieldID fieldID, NativeType value);
访问对象的静态变量
访问静态变量类似于访问实例变量,只是调用函数改为GetStaticFieldID()
,Get | SetStaticObjectField()
,Get | SetStatic
之类的。
示例
java层:
public class MainActivity extends AppCompatActivity { // 在程序开始时使用静态代码块加载'native-lib'库 static { System.loadLibrary("native-lib"); } // 静态变量 private static double number = 55.66; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); modifyStaticVariable(); Log.i("In Java, double is ", "" + number); } private native void modifyStaticVariable();}
C 层:
extern "C"JNIEXPORT void JNICALLJava_com_dali_jnitest_MainActivity_modifyStaticVariable(JNIEnv *env, jobject instance) { jclass cls = env->GetObjectClass(instance); jfieldID fieldID = env->GetStaticFieldID(cls, "number", "D"); if (fieldID == NULL) return; jdouble number = env->GetStaticDoubleField(cls, fieldID); LOGI("In C, the double is %f\n", number); number = 77.88; env->SetStaticDoubleField(cls, fieldID, number);}
执行:
com.dali.jnitest I/In C/C++: In C, the double is 55.660000com.dali.jnitest I/In Java, double is: 77.88
用于访问静态变量的JNI函数如下:
//返回类的静态变量的字段ID。jfieldID GetStaticFieldID(jclass cls, const char *name, const char *sig);//Get/Set 实例中静态变量的值,// 包含8中基本数据类型,加上一个Object类型。 NativeType GetStaticField(jclass clazz, jfieldID fieldID);void SetStaticField(jclass clazz, jfieldID fieldID, NativeType value);
回调java函数和静态函数
示例
java层:
public class MainActivity extends AppCompatActivity { // 在程序开始时使用静态代码块加载'native-lib'库 static { System.loadLibrary("native-lib"); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); nativeMethod(); } // 申明本地函数 private native void nativeMethod(); // 用于测试C代码调用 private void callback() { Log.i("In Java", ""); } private void callback(String message) { Log.i("In Java with ", message); } // 用于测试C代码调用 private double callbackAverage(int n1, int n2) { return ((double)n1 + n2) / 2.0; } // Static method to be called back private static String callbackStatic() { return "From static Java method"; }}
C 层:
java类声明一个名为nativeMethod()
的本地方法,并调用此nativeMethod()
。 反过来,在C层nativeMetho()
回调此类中定义的各种实例和静态方法。
extern "C"JNIEXPORT void JNICALLJava_com_dali_jnitest_MainActivity_nativeMethod(JNIEnv *env, jobject instance) { // 获取java层调用的class对象的引用 jclass thisClass = env->GetObjectClass(instance); // 获取Method ID 用于调用“callback”函数,该函数没有参数,无返回值 jmethodID midCallBack = env->GetMethodID(thisClass, "callback", "()V"); if (NULL == midCallBack) return; LOGI("In C, call back Java's callback()\n"); // 根据Method ID 调用该函数(无返回值) env->CallVoidMethod(instance, midCallBack); jmethodID midCallBackStr = env->GetMethodID(thisClass, "callback", "(Ljava/lang/String;)V"); if (NULL == midCallBackStr) return; LOGI("In C, call back Java's called(String)\n"); jstring message = env->NewStringUTF("Hello from C"); env->CallVoidMethod(instance, midCallBackStr, message); jmethodID midCallBackAverage = env->GetMethodID(thisClass, "callbackAverage", "(II)D"); if (NULL == midCallBackAverage) return; jdouble average = env->CallDoubleMethod(instance, midCallBackAverage, 2, 3); LOGI("In C, the average is %f\n", average); jmethodID midCallBackStatic = env->GetStaticMethodID(thisClass, "callbackStatic", "()Ljava/lang/String;"); if (NULL == midCallBackStatic) return; jstring resultJNIStr = static_cast(env->CallStaticObjectMethod(thisClass, midCallBackStatic)); const char *resultCStr = env->GetStringUTFChars(resultJNIStr, NULL); if (NULL == resultCStr) return; LOGI("In C, the returned string is %s\n", resultCStr); env->ReleaseStringUTFChars(resultJNIStr, resultCStr);}
执行代码:
com.dali.jnitest I/In C/C++: In C, call back Java's callback() In C, call back Java's called(String)com.dali.jnitest I/In Java with: Hello from Ccom.dali.jnitest I/In C/C++: In C, the average is 2.500000 In C, the returned string is From static Java method
在本地代码中回调java对象中的函数的基本步骤
通过
GetObjectClass()
获取class对象的引用。通过
GetMethodID()
以及class对象引用获取Method ID。该函数参数中,需要提供函数名和签名。签名的格式为: “(parameters)return-type
” 。太复杂了么,别着急,你可以通过javap
工具 (Class File Disassembler) 列出java代码中的函数签名,-s
用于打印签名,-p
用于显示私有成员(函数或变量),具体如下:private void callback(); Signature: ()V private void callback(java.lang.String); Signature: (Ljava/lang/String;)V private double callbackAverage(int, int); Signature: (II)D private static java.lang.String callbackStatic(); Signature: ()Ljava/lang/String;
基于Method ID, 你可以调用
Call
或者Method() CallVoidMethod()
亦或CallObjectMethod()
, 这些函数分别返回的类型为:<*Primitive-type*>
,void
andObject
。 在参数列表之前附加参数(如果有)。 对于非void返回类型,该方法返回一个值。
访问实例函数和实例的静态函数的JNI函数如下:
// 返回java层实例(类或接口)函数的method IDjmethodID GetMethodID(jclass cls, const char *name, const char *sig);// 调用类的method// 包含8种基本数据类型 NativeType CallMethod(jobject obj, jmethodID methodID, ...);NativeType CallMethodA(jobject obj, jmethodID methodID, const jvalue *args);NativeType CallMethodV(jobject obj, jmethodID methodID, va_list args);// 返回Java层实例静态函数的method ID jmethodID GetStaticMethodID(jclass cls, const char *name, const char *sig);// 调用对象的函数// 包含8种基本数据类型 NativeType CallStaticMethod(jclass clazz, jmethodID methodID, ...);NativeType CallStaticMethodA(jclass clazz, jmethodID methodID, const jvalue *args);NativeType CallStaticMethodV(jclass clazz, jmethodID methodID, va_list args);
访问父类的重载函数
JNI提供了一组CallNonvirtual
函数来调用已在此类中重写的超类’实例方法(类似于Java子类中的super。* methodName *()调用):
- 通过
GetMethodID()
函数获取Method ID。 - 基于Method ID,使用object, superclass, 和arguments 访问
CallNonvirtual
函数。Method()
访问超累的JNI函数如下:
NativeType CallNonvirtual<type>Method(jobject obj, jclass cls, jmethodID methodID, ...);NativeType CallNonvirtual<type>MethodA(jobject obj, jclass cls, jmethodID methodID, const jvalue *args);NativeType CallNonvirtual<type>MethodV(jobject obj, jclass cls, jmethodID methodID, va_list args);
创建对象和对象数组
创建对象
可以通过NewObject()
和newObjectArray()
函数在本机代码中构造jobject
和jobjectArray
,并将它们传递回Java程序。
示例
java代码:
public class MainActivity extends AppCompatActivity { // 在程序开始时使用静态代码块加载'native-lib'库 static { System.loadLibrary("native-lib"); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Log.i("In Java,the number is :", "" + getIntegerObject(9999)); } // 本地函数,调用之后返回创建完毕的java对象实例 // 使用给定的int返回Integer对象 private native Integer getIntegerObject(int number);
C 代码:
extern "C"JNIEXPORT jobject JNICALLJava_com_dali_jnitest_MainActivity_getIntegerObject(JNIEnv *env, jobject instance, jint number) { // 获取java.lang.Integer类的引用 jclass cls = env->FindClass("java/lang/Integer"); // 获取Integer的构造函数Method ID,使用int参数 jmethodID midInit = env->GetMethodID(cls, "" , "(I)V"); if (NULL == midInit) return NULL; // 使用jint参数调用构造函数,分配新的实例 jobject newObj = env->NewObject(cls, midInit, number); // 调用String的toString()方法调用打印 jmethodID midToString = env->GetMethodID(cls, "toString", "()Ljava/lang/String;"); if (NULL == midToString) return NULL; jstring resultStr = static_cast(env->CallObjectMethod(newObj, midToString)); const char *resultCStr = env->GetStringUTFChars(resultStr, NULL); LOGI("In C: the number is %s\n", resultCStr); return newObj;}
代码执行:
com.dali.jnitest I/In C/C++: In C: the number is 9999com.dali.jnitest I/In Java,the number is :: 9999用于创建对象(jobject)的JNI函数是:
用于创建对象(jobject)的JNI函数是:
jclass FindClass(JNIEnv *env, const char *name);// 构造一个新的Java对象。 method ID指定要调用的构造方法 jobject NewObject(JNIEnv *env, jclass cls, jmethodID methodID, ...);jobject NewObjectA(JNIEnv *env, jclass cls, jmethodID methodID, const jvalue *args);jobject NewObjectV(JNIEnv *env, jclass cls, jmethodID methodID, va_list args);// 在不调用对象的任何构造函数的情况下分配新的Java对象。 jobject AllocObject(JNIEnv *env, jclass cls);
对象数组
示例
java层:
public class MainActivity extends AppCompatActivity { // 在程序开始时使用静态代码块加载'native-lib'库 static { System.loadLibrary("native-lib"); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Integer[] numbers = {11, 22, 32}; // auto-box Double[] results = sumAndAverage2(numbers); // auto-unbox Log.i("In Java,the sum is ","" + results[0]); Log.i("In Java,the average is ","" + results[1]); } // 本地函数:接收一个Integer[], 返回Double[2],[0]为和,[1]为平均值 private native Double[] sumAndAverage2(Integer[] numbers);
C层:
extern "C"JNIEXPORT jobjectArray JNICALLJava_com_dali_jnitest_MainActivity_sumAndAverage2(JNIEnv *env,jobject instance,jobjectArray numbers) { // 获取java.lang.Integer类的引用 jclass classInteger = env->FindClass("java/lang/Integer"); // 使用Integer.intValue() 函数 jmethodID midIntValue = env->GetMethodID(classInteger, "intValue", "()I"); if (NULL == midIntValue) return NULL; // 获取数据中每个元素Get the value of each Integer object in the array jsize length = env->GetArrayLength(numbers); jint sum = 0; int i; for (i = 0; i < length; i++) { jobject objInteger = env->GetObjectArrayElement(numbers, i); if (NULL == objInteger) return NULL; jint value = env->CallIntMethod(objInteger, midIntValue); sum += value; } double average = (double)sum / length; LOGI("In C, the sum is %d\n", sum); LOGI("In C, the average is %f\n", average); // 获取java.lang.Double类的引用 jclass classDouble = env->FindClass("java/lang/Double"); // 创建一个长度为2的java.lang.Double类型数组()jobjectArray jobjectArray outJNIArray = env->NewObjectArray(2, classDouble, NULL); // 通过构造函数构造两个Double类型对象 jmethodID midDoubleInit = env->GetMethodID(classDouble, "" , "(D)V"); if (NULL == midDoubleInit) return NULL; jobject objSum = env->NewObject(classDouble, midDoubleInit, (double)sum); jobject objAve = env->NewObject(classDouble, midDoubleInit, average); // 初始化jobjectArray env->SetObjectArrayElement(outJNIArray, 0, objSum); env->SetObjectArrayElement(outJNIArray, 1, objAve); return outJNIArray;}
代码执行:
com.dali.jnitest I/In C/C++: In C, the sum is 65 In C, the average is 21.666667com.dali.jnitest I/In Java,the sum is: 65.0com.dali.jnitest I/In Java,the average is: 21.666666666666668
与可以批量处理的原始数组不同,对于对象数组,你需要使用Get | SetObjectArrayElement()
来处理每个元素。
用于创建和操作对象数组(jobjectArray)的JNI函数如下:
// 构造一个包含elementClass对象的数组//所有元素都设置为initialElement。jobjectArray NewObjectArray(jsize length, jclass elementClass, jobject initialElement);// 返回一个对象数组元素 jobject GetObjectArrayElement(jobjectArray array, jsize index);// 设置一个对象数组元素 void SetObjectArrayElement(jobjectArray array, jsize index, jobject value);
本地和全局引用
管理引用对于编写高效的程序至关重要。 例如,我们经常使用FindClass()
,GetMethodID()
,GetFieldID()
来检索本机函数中的jclass
,jmethodID
和jfieldID
。 应该获取一次并缓存以供后续使用的值,而不是执行重复调用,以减少开销。
JNI将C代码使用的对象引用(对于jobject
)分为两类:本地引用和全局引用:
- 在C函数中创建本地引用,并在方法退出后释放。 它在本地方法的持续时间内有效。 你还可以使用JNI函数
DeleteLocalRef()
显式地使本地引用无效,以便可以在中间进行垃圾回收。 对象作为本地引用传递给本地方法。 JNI函数返回的所有Java对象(jobject
)都是本地引用。 - 在程序员通过
DeleteGlobalRef()
JNI函数显式释放它之前,全局引用仍然存在。 您可以通过JNI函数NewGlobalRef()
从本地引用创建新的全局引用。
示例:
java代码:
public class MainActivity extends AppCompatActivity { // 在程序开始时使用静态代码块加载'native-lib'库 static { System.loadLibrary("native-lib"); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Log.i("java:", ""+getIntegerObject2(1)); Log.i("java:", ""+getIntegerObject2(2)); Log.i("java:", ""+anotherGetIntegerObject(11)); Log.i("java:", ""+anotherGetIntegerObject(12)); Log.i("java:", ""+getIntegerObject2(3)); Log.i("java:", ""+anotherGetIntegerObject(13)); } // 返回给定int值的java.lang.Integer的本地方法。 private native Integer getIntegerObject2(int number); // 另一个本地方法也返回带有给定int值的java.lang.Integer。 private native Integer anotherGetIntegerObject(int number);}
C代码:
// java类"java.lang.Integer"的全局引用static jclass classInteger;static jmethodID midIntegerInit;jobject getInteger(JNIEnv *env, jobject instance, jint number) { // 如果classInteger为NULL,获取java.lang.Integer的类引用 if (NULL == classInteger) { LOGI("Find java.lang.Integer\n"); classInteger = env->FindClass("java/lang/Integer"); // 不加下面这行会出现报错:jni error (app bug): accessed stale local reference classInteger = static_cast(env->NewGlobalRef(classInteger)); } if (NULL == classInteger) return NULL; // 如果midIntegerInit为空,获取Integer构造函数的method ID if (NULL == midIntegerInit) { LOGI("Get Method ID for java.lang.Integer's constructor\n"); midIntegerInit = env->GetMethodID(classInteger, "" , "(I)V"); } if (NULL == midIntegerInit) return NULL; // 调用构造函数以使用int参数分配新实例 jobject newObj = env->NewObject(classInteger, midIntegerInit, number); LOGI("In C, constructed java.lang.Integer with number %d\n", number); return newObj;}extern "C"JNIEXPORT jobject JNICALLJava_com_dali_jnitest_MainActivity_getIntegerObject2(JNIEnv *env, jobject instance, jint number) { return getInteger(env, instance, number);}extern "C"JNIEXPORT jobject JNICALLJava_com_dali_jnitest_MainActivity_anotherGetIntegerObject(JNIEnv *env, jobject instance,jint number) { return getInteger(env, instance, number);}
在上面的程序中,我们使用classInteger = static_cast
将classInteger
变为全局引用,记得要使用env->DeleteLocalRef(classInteger);
取消引用。
请注意,jmethodID和jfieldID不是jobject,并且无法创建全局引用。
Debug JNI 程序
TODO:
更多相关文章
- 箭头函数的基础使用
- Python技巧匿名函数、回调函数和高阶函数
- 浅析android通过jni控制service服务程序的简易流程
- Android(安卓)bluetooth介绍(四): a2dp connect流程分析
- Android中文API(144) —— JsonWriter
- Android架构分析之使用自定义硬件抽象层(HAL)模块
- Android中OpenMax的适配层
- android 包管理系统分析
- Android中获取屏幕相关信息(屏幕大小,状态栏、标题栏高度)