Android NDK开发(第三天)
JNIEnv类型
JNIEnv类型实际上代表了java环境,通过JNIEnv* 指针就可以对java端的代码进行操作,例如,创建java类中的对象,调用java对象的方法,获取java对象中的属性等,常用的类型如下:
#创建java类中的对象
jobject NewObject(jclass clazz, jmethodID methodID, ...)
#创建java类中的string对象
jstring NewString(const jchar* unicodeChars, jsize len)
#创建特定类型的数组
jobjectArray NewObjectArray(jsize length, jclass elementClass,
jobject initialElement)
#获取类型为type的字段
jfieldID GetFieldID(jclass clazz, const char* name, const char* sig)
更多的函数,可以查看jni.h中的函数名称
jclass类型
为了能够在C/C++中使用java类,jni.h头文件中专门定义了jclass类型来表示java中的class类
JNIEnv类中有如下几个简单的函数可以取得jclass:
-
jclass FindClass(const char* name)
通过类全名获取jclass,如:jclass str = env->FindClass("java/lang/String") jclass GetObjectClass(jobject obj)
通过对象实例获取jclass,类似于java中的getClass方法
- jclass GetSuperclass(jclass clazz)
通过class获取其父类的class对象
native中访问java层方法
在native层访问java端的代码,最常见的就是获取类的属性,和调用类的方法。为了在c/c++中表示属性和方法,JNI在jni.h中定义了jfieldId,jmethodId类型来分别表示java端的属性和方法。
使用下面方法获取jfield和jmethod
- GetFieldID / GetMethodID
- GetStaticFieldID / GetStaticMethodID
jfieldID GetFieldID(jclass clazz, const char* name, const char* sig)
参数说明:
# clazz: 这个方法依赖的类对象的class对象
# name 这个字段的名称
# sig这个字段的签名
可以通过javap查看勒种的字段和方法的签名如:
查看类中字段和方法的签名:
public class DataProvider {
/**
* 把java中的int传递给c语言,c语言处理完毕后,把结果相加返回给java
* @param x
* @param y
* @return
*/
public static native int add(int x, int y);
/**
* 把java中的string传递给c语言,c语言获取到java的string之后,在string后面添加一个hello字符串
* @param s
* @return
*/
public static native String sayHelloInC(String s);
static {
System.loadLibrary("test-lib");
}
}
使用javap -s -p ./app/build/intermediates/classes/debug/com/jni/test/DataProvider.class, 命令执行结果如下:
image.png
下面看个栗子:
新建Hello.java
import android.util.Log;
import java.util.Date;
/**
* Created by liuhang on 18-7-20.
*/
public class Hello {
public int property;
public int function(int foo, Date date, int[]arr) {
Log.e("liuhang", "function runs .....");
return 0;
}
public native void test();
static {
System.loadLibrary("test-lib");
}
}
Hello.java中属性和方法签名如下:
Compiled from "Hello.java"
public class com.jni.test.Hello {
public int property;
Signature: I
public com.jni.test.Hello();
Signature: ()V
public int function(int, java.util.Date, int[]);
Signature: (ILjava/util/Date;[I)I
public native void test();
Signature: ()V
}
首先使用javah生成hello.java对应的头文件
javah com.jni.test.Hello
编写hello.cpp文件,在native test方法中,调用java中的function方法
#include <jni.h>
#include <string>
#include <android/log.h>
#define LOG_TAG "liuhang"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
extern "C"
/*
* Class: com_jni_test_Hello
* Method: test
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_com_jni_test_Hello_test
(JNIEnv * env, jobject obj) {
jclass hello_clazz = env->GetObjectClass(obj);
jfieldID fieldId_prop = env->GetFieldID(hello_clazz, "property", "I");
jmethodID methodId = env->GetMethodID(hello_clazz, "function", "(ILjava/util/Date;[I)I");
env->CallIntMethod(obj, methodId, 0L, NULL, NULL);
}
此时执行test方法,就会在native中调用java中的function方法
new Hello().test();
结果如下:
image.png
native中更改java类属性值
创建ChangeProp.java
package com.jni.test;
/**
* Created by liuhang on 18-7-20.
*/
public class ChangeProp {
public String name = "zhangsan";
public static int number = 9;
public native void changePropFun();
static {
System.loadLibrary("test-lib");
}
}
对应的签名如下:
public class com.jni.test.ChangeProp {
public java.lang.String name;
Signature: Ljava/lang/String;
public static int number;
Signature: I
public com.jni.test.ChangeProp();
Signature: ()V
public native void changePropFun();
Signature: ()V
static {};
Signature: ()V
}
使用javah生成头文件
image.png创建changeprop.cpp
#include <jni.h>
#include <string>
#include <android/log.h>
#include <malloc.h>
#define LOG_TAG "liuhang"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
extern "C"
char* Jstring2CStr(JNIEnv* env, jstring jstr)
{
char* rtn = NULL;
jclass clsstring = (env)->FindClass("java/lang/String");
jstring strencode = (env)->NewStringUTF("GB2312");
jmethodID mid = (env)->GetMethodID(clsstring, "getBytes", "(Ljava/lang/String;)[B");
jbyteArray barr= (jbyteArray)(env)->CallObjectMethod(jstr,mid,strencode); // String .getByte("GB2312");
jsize alen = (env)->GetArrayLength(barr);
jbyte* ba = (env)->GetByteArrayElements(barr,JNI_FALSE);
if(alen > 0)
{
rtn = (char*)malloc(alen+1); //"\0"
memcpy(rtn,ba,alen);
rtn[alen]=0;
}
(env)->ReleaseByteArrayElements(barr,ba,0); //
return rtn;
}
extern "C"
/*
* Class: com_jni_test_ChangeProp
* Method: changePropFun
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_com_jni_test_ChangeProp_changePropFun
(JNIEnv *env, jobject jobject) {
jclass jclazz = env->GetObjectClass(jobject);
// 分别获取两个属性的id,,Ljava/lang/String; 这里的签名一定要由分号
jfieldID jfield_name = env->GetFieldID(jclazz, "name", "Ljava/lang/String;");
jfieldID jfiled_number = env->GetStaticFieldID(jclazz, "number", "I");
// 获取属性的值,需要注意string是object类型
jstring before_name = (jstring)env->GetObjectField(jobject, jfield_name);
jint before_number = env->GetStaticIntField(jclazz, jfiled_number);
// 打印修改前的属性值
LOGI("modify before name is %s ,, number is :%d", Jstring2CStr(env, before_name), before_number);
// 设置新的属性值
env->SetObjectField(jobject, jfield_name, env->NewStringUTF("lisi"));
env->SetStaticIntField(jclazz, jfiled_number, 44);
}
java代码调用
ChangeProp prop = new ChangeProp();
Log.e("liuhang", "in activity modify before name is :"+prop.name+" number is :"+ChangeProp.number);
prop.changePropFun();
Log.e("liuhang", "in activity modify after name is :"+prop.name+" number is :"+ChangeProp.number);
此时打印如下:
image.png
JNIEnv中,提供了众多的 Call<Type>Method和 CallStatic<Type>method,
还有CallNonvirtual<Type>Method函数,需要通过GetMethodID获取相
对应的方法的jmethodID来传入到上述函数参数中
调用示例方法的三种形式如下:
Call<Type>Method(jobject obj, jmethodID id, ....);
Call<Type>Method(jobject obj, jmethodID id, va_list lst);
Call<Type>Method(jobject obj, jmethodID id, jvalue * v);
java和c++中的多态机制
C++和java对于继承后执行的是父类还是子类的方法是由区别的,在java中,所有的方法都是虚拟的,所以总是调用子类的方法,因此CallNonVirtual<Type>Method
方法就出来了,这个方法可以帮助调用java中父亲的方法。看下面demo
创建ExtendsDemo.java
package com.jni.test;
/**
* Created by liuhang on 18-7-21.
*/
public class ExtendsDemo {
public Father father = new Child();
public native void testExtends();
static {
System.loadLibrary("test-lib");
}
}
对应的方法签名如下:
public class com.jni.test.ExtendsDemo {
public com.jni.test.Father father;
Signature: Lcom/jni/test/Father;
public com.jni.test.ExtendsDemo();
Signature: ()V
public native void testExtends();
Signature: ()V
static {};
Signature: ()V
}
创建父类Father.java和子类Child.java
package com.jni.test;
import android.util.Log;
/**
* Created by liuhang on 18-7-21.
*/
public class Father {
public void function() {
Log.e("liuhang", "father function runs.....");
}
}
package com.jni.test;
import android.util.Log;
/**
* Created by liuhang on 18-7-21.
*/
public class Child extends Father {
@Override
public void function() {
Log.e("liuhang", "child function runs....");
}
}
为ExtendsDemo生成头文件
liuhang@android:~/code/MyApplication/app/src/main/java$ javah com.jni.test.ExtendsDemo
创建Extends.cpp
#include <jni.h>
#include <string>
#include <android/log.h>
#define LOG_TAG "liuhang"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
extern "C"
/*
* Class: com_jni_test_ExtendsDemo
* Method: testExtends
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_com_jni_test_ExtendsDemo_testExtends
(JNIEnv * env, jobject obj) {
LOGI("liuhang jni testExtends runs...");
// 获取ExtendsEemo类对应的class对象
jclass clazz = env->GetObjectClass(obj);
// 获取father属性对应的filedid
jfieldID id_father = env->GetFieldID(clazz, "father", "Lcom/jni/test/Father;");
// 获取father属性对象
jobject father = env->GetObjectField(obj, id_father);
// 获取father类
jclass jfather_clazz = env->FindClass("com/jni/test/Father");
// 获取Father类中的function方法
jmethodID jfatherFun_id = env->GetMethodID(jfather_clazz, "function", "()V");
// 调用子类的方法
env->CallVoidMethod(father, jfatherFun_id);
// 调用父类的方法
env->CallNonvirtualVoidMethod(father, jfather_clazz, jfatherFun_id);
}
java中调用native方法
new ExtendsDemo().testExtends();
此时执行结果如下:
[图片上传失败...(image-23a951-1532242821008)]
native中创建java对象
在JNIEnv中有两种方法创建java对象,下面逐个介绍
通过NewObject创建java对象
jobject NewObject(jclass clazz, jmethodID methodID, ...)
#clazz是需要构建的java对象的class类对象
#methodID是传递一个方法ID,就是构造方法
#因为构造方法没有返回值,所以认为类的默认构造方法的返回值类型的签名始终是"()V",(因为默认的构造方法是没有参数的),方法的名称始终为"<init>"
在native中创建Date对象,打印getTime返回值
创建ConstructJavaObj.java
package com.jni.test;
/**
* Created by liuhang on 18-7-21.
*/
public class ConstructJavaObj {
public native void firstConstruct();
public native void secondConstruct();
static {
System.loadLibrary("test-lib");
}
}
使用javah生成头文件
liuhang@android:~/code/MyApplication/app/src/main/java$ javah com.jni.test.ConstructJavaObj
创建constructobj.cpp
#include <jni.h>
#include <string>
#include <android/log.h>
#define LOG_TAG "liuhang"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
extern "C"
/*
* Class: com_jni_test_ConstructJavaObj
* Method: firstConstruct
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_com_jni_test_ConstructJavaObj_firstConstruct
(JNIEnv *env, jobject obj) {
jclass date_clazz = env->FindClass("java/util/Date");
jmethodID date_construct = env->GetMethodID(date_clazz, "<init>", "()V");
jobject date_obj = env->NewObject(date_clazz, date_construct);
jmethodID method_id = env->GetMethodID(date_clazz, "getTime", "()J");
jlong time_current = env->CallLongMethod(date_obj, method_id);
LOGI("current time is :%lld",time_current);
}
java中调用native方法
new ConstructJavaObj().firstConstruct();
Log.e("liuhang", "mainactivity tine is :"+new Date().getTime());
image.png
<font color="red">最后别忘了,在CMakeLists.txt中添加对应的cpp文件引用</font>
#生成so动态库
ADD_LIBRARY(test-lib SHARED test-lib.cpp hello.cpp changeprop.cpp extends.cpp constructobj.cpp)
target_link_libraries(test-lib log)
通过AllocObject(jclass clazz)创建java对象
用AllocObject函数创建一个java对象,可以根据传入的jclass创建一个java对象,但是状态是非初始化的,在这个对象使用之前,需要用CallNonvirtualVoidMethod来调用该jclass的构造函数,这样做可以延迟构造函数的调用。还是实现上面的功能,打印date.getTime()返回值
实现secondConstruct本地方法
/*
* Class: com_jni_test_ConstructJavaObj
* Method: secondConstruct
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_com_jni_test_ConstructJavaObj_secondConstruct
(JNIEnv *env, jobject obj) {
jclass date_clazz = env->FindClass("java/util/Date");
jmethodID date_construct = env->GetMethodID(date_clazz, "<init>", "()V");
jobject date_obj = env->AllocObject(date_clazz);
env->CallNonvirtualVoidMethod(date_obj, date_clazz, date_construct);
jmethodID method_id = env->GetMethodID(date_clazz, "getTime", "()J");
jlong time_current = env->CallLongMethod(date_obj, method_id);
LOGI("int secondConstruct current time is :%lld",time_current);
}
image.png
native中操作java字符串
# 获取字符串长度
jsize GetStringLength(jstring string)
# 将jstring对象拷贝到const jchar* 指针字符串
void GetStringRegion(jstring str, jsize start, jsize len, jchar* buf)
#生成一个jstring对象
jstring NewString(const jchar* unicodeChars, jsize len)
# 将string对象转换成const jcahr* 字符串指针
const jchar* GetStringChars(jstring string, jboolean* isCopy)
在java中定义一个字符串属性,在C++中将该字符串进行倒序,在从Java中输出
package com.jni.test;
/**
* Created by liuhang on 18-7-21.
*/
public class StringJNI {
public String name = "zhangsan";
public native void convertStr();
static {
System.loadLibrary("test-lib");
}
}
编写convertstr.cpp类
#include <jni.h>
#include <string>
#include <android/log.h>
#include <algorithm>
#define LOG_TAG "liuhang"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
using namespace std;
extern "C"
/*
* Class: com_jni_test_StringJNI
* Method: convertStr
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_com_jni_test_StringJNI_convertStr
(JNIEnv * env, jobject jobject) {
jclass jclazz = env->GetObjectClass(jobject);
jfieldID j_name_id = env->GetFieldID(jclazz, "name", "Ljava/lang/String;");
jstring j_namestr = (jstring)env->GetObjectField(jobject, j_name_id);
const jchar * jstr = env->GetStringChars(j_namestr, NULL);
wstring wstr((const wchar_t*)jstr);
//释放指针
env->ReleaseStringChars(j_namestr, jstr);
reverse(wstr.begin(), wstr.end());
jstring j_newstr = env->NewString((const jchar*)wstr.c_str() , (jint)wstr.size());
env->SetObjectField(jobject, j_name_id, j_newstr);
}
native操作java中的数组
操作基本类型数组
Get<Type>ArrayElements方法
上述函数可以吧java基本类型的数组转换到c/c++中的数组,有两种处理方式:一种是拷贝一份传回本地代码,另一种是把指向java数组的指针直接传回到本地代码中,处理完本地化的数组后,通过Release<Type>ArrayElements
来释放数组。
Release<Type>ArrayElements
用上述方法可以选择将如何处理java和c++的数组,是提交还是撤销等,内存释放还是不释放等。
void* GetPrimitiveArrayCritical(jarray array, jboolean* isCopy)
void ReleasePrimitiveArrayCritical(jarray array, void* carray, jint mode)
get<Type>ArrayRegion
#在c/c++中预先开辟一段内存,然后把java基本类型的数组拷贝到这段内存中
set<Type>ArrayRegion
#把java基本类型数组中的指定范围的元素用c/c++数组中的元素来赋值
<Type>ArrayNew方法
指定一个长度然后返回相应的java基本类型的数组
操作对象类型数组
JNI没有提供把java对象类型数组(Object[])直接转换到c++中的Object[]数组的函数,而是通过Get/SetObjectArrayElement这样的函数来对java的Object[]数组进行操作,由于对象数组没有进行拷贝,所以不需要释放任何资源,NewObjectArray可以通过制定长度和初始值来创建某个类的数组
创建ArrayTest.java
package com.jni.test;
/**
* Created by liuhang on 18-7-21.
*/
public class ArrayTest {
int []arrays = {3, 4, 13, 56, 34, 22,88,21};
Father[] fathers = {new Father(), new Father(), new Father()};
public native void testArray();
static{
System.loadLibrary("test-lib");
}
}
签名如下:
public class com.jni.test.ArrayTest {
int[] arrays;
Signature: [I
com.jni.test.Father[] fathers;
Signature: [Lcom/jni/test/Father;
public com.jni.test.ArrayTest();
Signature: ()V
public native void testArray();
Signature: ()V
static {};
Signature: ()V
}
创建arraytest.cpp
#include <jni.h>
#include <string>
#include <android/log.h>
#define LOG_TAG "liuhang"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
extern "C"
/*
* Class: com_jni_test_ArrayTest
* Method: testArray
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_com_jni_test_ArrayTest_testArray
(JNIEnv * env, jobject jobject) {
jclass jclazz = env->GetObjectClass(jobject);
jfieldID arrayid = env->GetFieldID(jclazz, "arrays", "[I");
jintArray arrayobj = (jintArray)env->GetObjectField(jobject, arrayid);
jint* int_array = env->GetIntArrayElements(arrayobj, NULL);
jsize length = env->GetArrayLength(arrayobj);
LOGI("int array length is :%d", length);
for(int i = 0; i < length; i++) {
LOGI("int array %d place is :%d",i, int_array[i]);
}
}
此时执行程序,运行结果如下:
image.png
<font color="red">将上述数组中下表偶数位,保存到新的数组里</font>
// 将上述数组中下表偶数位,保存到新的数组里
jintArray jsave_array = env->NewIntArray(length);
jint* save_array = env->GetIntArrayElements(jsave_array, NULL);
jint count = 0;
for(int i = 0; i < length; i++) {
if (i % 2 == 0) {
save_array[count++] = int_array[i];
}
}
//打印新的数组内容
for(int j = 0; j < count; j++) {
LOGI("save_array %d place is :%d",j, save_array[j]);
}
image.png
<font color="red">将上述数组中0到3位,拷贝到数组,并且打印出来</font>
// 将上述数组中0到3位,拷贝到数组,并且打印出来
jint * jmemory_array = new jint[length];
env->GetIntArrayRegion(arrayobj, 0, 3, jmemory_array);
for(int j = 0; j < count; j++) {
LOGI("jmemory_array %d place is :%d",j, save_array[j]);
}
image.png
<font color='red'>把上述数组中的3-7位,赋新的值</font>
// 把上述数组中的3-7位,赋新的值
jint * replace_array = new jint[4];
for (int k = 0; k < 4; k++) {
replace_array[k] = k+1;
}
env->SetIntArrayRegion(arrayobj, 3, 4, replace_array);
// 重新获取数组指针
int_array = env->GetIntArrayElements(arrayobj, NULL);
for(int i = 0; i < length; i++) {
LOGI("int array after replace ... %d place is :%d",i, int_array[i]);
}
image.png
<font color='red'>调用sort函数对上述数组进行排序</font>
#include <algorithm>
using namespace std;
std :: sort(int_array, int_array+length);
for(int i = 0; i < length; i++) {
LOGI("int array after sort ... %d place is :%d",i, int_array[i]);
}
image.png
JNIEnv操作引用类型的数组
<font color='red'>获取上述fathers数组中的第一个Father类对象,并且执行其function方法</font>
// 获取fathers属性字段id
jfieldID j_father_id = env->GetFieldID(jclazz, "fathers", "[Lcom/jni/test/Father;");
// 获取fathers属性
jobjectArray j_father_obj = (jobjectArray)env->GetObjectField(jobject, j_father_id);
// 获取Father类class
jclass j_father_clazz = env->GetObjectClass(env->GetObjectArrayElement(j_father_obj, 1));
// 根据Father类class 获取function方法id
jmethodID j_father_method = env->GetMethodID(j_father_clazz, "function", "()V");
// 执行function方法
env->CallVoidMethod(env->GetObjectArrayElement(j_father_obj, 1), j_father_method);
此时打印如下:
07-21 00:51:36.828 3710 3710 E liuhang : father function runs..... name is :lisi
<font color='red'>创建一个大小为5的Father类型数组,并且值为fathers数组中的第三个元素,执行其function方法</font>
// 创建一个大小为5的Father类型数组,并且值为fathers数组中的第三个元素
jobjectArray j_new_obj = env->NewObjectArray(5, j_father_clazz, env->GetObjectArrayElement(j_father_obj, 2));
// 获取j_new_obj数组中的第四个对象,执行其function方法
env->CallVoidMethod(env->GetObjectArrayElement(j_new_obj, 3), j_father_method);
此时打印如下:
07-21 00:51:36.828 3710 3710 E liuhang : father function runs..... name is :wangwu
c/c++中的引用类型
局部引用
局部引用是最常见的引用类型,基本上通过JNI返回来的引用都是局部引用,例如:使用NewObject就会返回创建出来的实力的局部引用,局部引用只在该native函数中有效,所有在该函数中产生的局部引用,都会在该函数返回的时候,自动释放,也可以使用DeleteLocalRef函数手动释放该引用。
全局引用
全局引用可以跨越当前线程,在多个native函数中有效,不过需要开发人员手动释放该引用,全局引用存在期间会防止在java的垃圾回收器的回收。
与局部引用不同,全局引用的创建不是由JNI自动创建的,全局引用是需要调用NewGlobalRef函数,而释放它需要使用ReleaseGlobalRef函数
弱全局引用
弱全局引用是java 1.2新出来的功能,与全局引用相似,创建和删除都需要开发人员进行,这种引用与全局引用一样,可以在多个本地代码中有效,也跨越多线程有效,不同的是,这种引用将不会阻止垃圾回收器回收这个引用所指向的对象,使用NewWeakGLobalRef和ReleaseWeakGlobalRef来产生和解除引用。
ID缓存
缓存jfieldID/jmethodID,取得jfieldID和jmethodID时候,会通过该属性/方法名称加上签名来查询相应的jfieldID/jmethodID。这种查询相对来说开销大,我们可以将这些jfieldID/jmethodID缓存起来,这样需要查询一次,以后就使用缓存起来的jfieldID/jmethodID
在使用的时候缓存
在native代码中使用static局部变量保存已经查询过的id,这样就不会在每次函数调用时候查询,而只要第一次查询成功后就保存起来。
static jfieldID fieldID_str = NULL;
jclass clazz = env->GetObjectClass(obj);
if (fieldID_str == NULL) {
fieldID_str = env->GetFieldID(clazz, "string", "Ljava/lang/String;");
}
在java类初始化时缓存
更好的一个方式是在任何native函数调用前把ID全部存起来,可以让java在第一次加载这个类的时候,首先调用本地代码初始化所有的jfieldID/jmethodID
jfieldID g_propInt_id = 0;
jfieldID g_propStr_id = 0;
JNIEXPORT void JNICALL Java_com_jni_test_Hello_test
(JNIEnv * env, jobject obj) {
g_propInt_id = env->GetFieldID(hello_clazz, "propertyInt", "I");
g_propStr_id = env->GetFieldID(hello_clazz, "propertyStr", "Ljava/lang/String;");
}