好久没发文章了,这篇文章是是10月底开始计划的,转眼到现在12月都快过一半了,我太难了……,不过好在终于完成了,今晚必须去吃宵夜。深圳北,往北两公里的**烧烤,有木有人过来?我请客,没有到时候我再来问一遍。
先看目录,各位觉得内容对你有用再继续往下看,毕竟显示有一万多个字呢,怕没用的话耽误大家宝贵的时间。
之前写过一篇关于C代码生成和调试so库的文章。前段时间在继承一个音频检测库的时候出现了点问题,又复习了下JNI部分,顺便整理成文,分享给大家。
本文是一个 NDK/JNI 系列基础到进阶教程,目标是希望观看这篇文章的朋友们能对Android中使用C/C++代码,集成C/C++库有一个比较基本的了解,并且能巧妙的应用到项目中。
好了,说完目的,咱们一如既往,学JNI之前,先来个给自己提几个问题:
了解是什么?用来做什么?以及为什么?
什么是JNI?
JNI,全名 Java Native Interface,是Java本地接口,JNI是Java调用Native 语言的一种特性,通过JNI可以使得Java与C/C++机型交互。简单点说就是JNI是Java中调用C/C++的统称。
什么是NDK?
NDK 全名Native Develop Kit,官方说法:Android NDK 是一套允许您使用 C 和 C++ 等语言,以原生代码实现部分应用的工具集。在开发某些类型的应用时,这有助于您重复使用以这些语言编写的代码库。
JNI和NDK都是调用C/C++代码库。所以总体来说,除了应用场景不一样,其他没有太大区别。细微的区别就是:JNI可以在Java和Android中同时使用,NDK只能在Android里面使用。
好了,讲了是什么之后,咱们来了解下JNI/NDK到底有什么用呢?
一句话,快速调用C/C++的动态库。除了调用C/C++之外别无它用。
就是这么简单好吧。知道做什么之后,咱们学这玩意有啥用呢?
暂时能想到的两个点,一个是能让我在开发中愉快的使用C/C++库,第二个就是能在安全攻防这一块有更深入的了解。其实无论这两个点中的哪个点都能让我有足够动力学下去。所以,想啥呢,搞定他。
配置NDK的环境比较简单。我们可以通过简单三步来实现:
ok,验证如上图所示说明你NDK配置成功了。so easy。
现在开始,咱们一起进入HelloWorld的世界。我们一起来通过AS创建一个Native C++项目。主要步骤如下:
简单通俗易懂有木有?好了,项目创建成功,运行,看界面,显示Hello World,项目创建成功。
从上面新建的项目中我们看到一个cpp目录,我们所写的C/C++代码就这这个目录下面。其中会发现有一个名为native-lib.cpp的文件,这就是用C/C++赋值Hello World的地方。
Android 中调用C/C++库的步骤:
Hello World Demo的代码:
Android代码:
public class MainActivity extends AppCompatActivity { static { System.loadLibrary("native-lib"); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); TextView tv = findViewById(R.id.sample_text); tv.setText(stringFromJNI()); } public native String stringFromJNI(); } 复制代码
natice-lib.cpp代码:
#include <jni.h> #include <string> extern "C" JNIEXPORT jstring JNICALL Java_com_example_testndk_MainActivity_stringFromJNI( JNIEnv *env, jobject /* this */) { std::string hello = "Hello from C++"; return env->NewStringUTF(hello.c_str()); } 复制代码
ok,我们现在调用是调用通了,但是我们要在JNI中生成对象实例,调用对应方法,操作对应属性,我们应该怎么做呢?OK,接下来要讲的内容将解答这些问题,咱们一起来学习下JNI/NDK中的API。
在C/C++本地代码中访问Java端的代码,一个常见的应用就是获取类的属性和调用类的方法,为了在C/C++中表示属性和方法,JNI在jni.h头文件中定义了jfieldID,jmethodID类型来分别代表Java端的属性和方法。在访问或者设置Java属性的时候,首先就要先在本地代码取得代表该Java属性的jfeldID,然后才能在本地代码中进行Java属性操作,同样,需要调用Java端的方法时,也是需要取得代表该方法的jmethodID才能进行Java方法调用。
接下来,咱们来尝试下如何在native中调用Java中的方法。先看下两个常见的类型:
在上面的native-lib.cpp中,我们看到getCarName方法中有两个参数,分别是JNIEnv *env,一个是jobjet instance。简单介绍下这两个类型的作用。
JNIEnv类型实际上代表了Java环境,通过JNIEnv*指针就可以对Java端的代码进行操作。比如我们可以使用JNIEnv来创建Java类中的对象,调用Java对象的方法,获取Java对象中的属性等。
JNIEnv类中有很多函数可以用,如下所示:
好了,说完JNIEnv,接下来我们讲第二个 jobject。
jobject可以看做是java中的类实例的引用。当然,情况不同,意义也不一样。
如果native方法不是static, obj 就代表native方法的类实例。
如果native方法是static, obj就代表native方法的类的class 对象实例(static 方法不需要类实例的,所以就代表这个类的class对象)。
举一个简单的例子:我们在TestJNIBean中创建一个静态方法testStaticCallMethod和非静态方法testCallMethod,我们看在cpp文件中该如何编写?
TestJNIBean的代码:
public class TestJNIBean{ public static final String LOGO = "learn android with aserbao"; static { System.loadLibrary("native-lib"); } public native String testCallMethod(); //非静态 public static native String testStaticCallMethod();//静态 public String describe(){ return LOGO + "非静态方法"; } public static String staticDescribe(){ return LOGO + "静态方法"; } } 复制代码
cpp文件中实现:
extern "C" JNIEXPORT jstring JNICALL Java_com_example_androidndk_TestJNIBean_testCallMethod(JNIEnv *env, jobject instance) { jclass a_class = env->GetObjectClass(instance); //因为是非静态的,所以要通过GetObjectClass获取对象 jmethodID a_method = env->GetMethodID(a_class,"describe","()Ljava/lang/String;");// 通过GetMethod方法获取方法的methodId. jobject jobj = env->AllocObject(a_class); // 对jclass进行实例,相当于java中的new jstring pring= (jstring)(env)->CallObjectMethod(jobj,a_method); // 类调用类中的方法 char *print=(char*)(env)->GetStringUTFChars(pring,0); // 转换格式输出。 return env->NewStringUTF(print); } extern "C" JNIEXPORT jstring JNICALL Java_com_example_androidndk_TestJNIBean_testStaticCallMethod(JNIEnv *env, jclass type) { jmethodID a_method = env->GetMethodID(type,"describe","()Ljava/lang/String;"); // 通过GetMethod方法获取方法的methodId. jobject jobj = env->AllocObject(type); // 对jclass进行实例,相当于java中的new jstring pring= (jstring)(env)->CallObjectMethod(jobj,a_method); // 类调用类中的方法 char *print=(char*)(env)->GetStringUTFChars(pring,0); // 转换格式输出。 return env->NewStringUTF(print); } 复制代码
上面的两个方法最大的区别就是静态方法会直接传入jclass,从而我们可以省去获取jclass这一步,而非静态方法传入的是当前类
ok,接下来简单讲一下Java中类型和native中类型映射关系。
Java类型 | 本地类型 | JNI定义的别名 |
---|---|---|
int | long | jint/jsize |
short | short | jshort |
long | _int64 | jlong |
float | float | jfloat |
byte | signed char | jbyte |
double | double | jdouble |
boolean | unsigned char | jboolean |
Object | _jobject* | jobject |
char | unsigned short | jchar |
这些后面我们在使用的时候也会讲到。好了,讲了这么多基础,也讲了Android中对C/C++库的基本调用。方便快捷的。直接调用native的方法就可以了。但是大部分情况下,我们需要在C/C++代码中对Java代码进行相应的操作以达到我们的加密或者方法调用的目的。这时候该怎么办呢?不急,咱们接下来就将如何在C/C++中调用Java代码。
JNIEnv类中有如下几个方法可以获取java中的类:
需要我们注意的是,FindClass方法参数name是某个类的完整路径。比如我们要调用Java中的Date类的getTime方法,那么我们就可以这么做:
extern "C" JNIEXPORT jlong JNICALL Java_com_example_androidndk_TestJNIBean_testNewJavaDate(JNIEnv *env, jobject instance) { jclass class_date = env->FindClass("java/util/Date");//注意这里路径要换成/,不然会报illegal class name jmethodID a_method = env->GetMethodID(class_date,"<init>","()V"); jobject a_date_obj = env->NewObject(class_date,a_method); jmethodID date_get_time = env->GetMethodID(class_date,"getTime","()J"); jlong get_time = env->CallLongMethod(a_date_obj,date_get_time); return get_time; } 复制代码
这个方法比较好理解,根据上面我们讲的根据jobject的类型,我们在JNI中写方法的时候如果是非静态的都会传一个jobject的对象。我们可以根据传入的来获取当前对象的类。代码如下:
extern "C" JNIEXPORT jstring JNICALL Java_com_example_androidndk_TestJNIBean_testCallMethod(JNIEnv *env, jobject instance) { jclass a_class = env->GetObjectClass(instance);//这里的a_class就是通过instance获取到的 …… } 复制代码
好了,我们知道怎么通过JNIEnv中获取Java中的类,接下来我们来学习如何获取并调用Java中的方法。
在JNIEnv环境下,我们有如下两种方法可以获取方法和属性:
GetMethodID方法如下:
jmethodID GetMethodID(jclass clazz, const char* name, const char* sig) 复制代码
方法的参数说明:
举一个小例子,比如我们要在JNI中调用TestJNIBean中的describe方法,我们可以这样做。
extern "C" JNIEXPORT jstring JNICALL Java_com_example_androidndk_TestJNIBean_testStaticCallMethod(JNIEnv *env, jclass type) { jmethodID a_method = env->GetMethodID(type,"describe","()Ljava/lang/String;"); // 通过GetMethod方法获取方法的methodId. jobject jobj = env->AllocObject(type); // 对jclass进行实例,相当于java中的new jstring pring= (jstring)(env)->CallObjectMethod(jobj,a_method); // 类调用类中的方法 char *print=(char*)(env)->GetStringUTFChars(pring,0); // 转换格式输出。 return env->NewStringUTF(print); } 复制代码
GetStaticMethodID的方法和GetMoehodID相同,只是用来获取静态方法的ID而已。同样,我们在cpp文件中调用TestJNiBean中的staticDescribe方法,代码如下:
extern "C" JNIEXPORT jstring JNICALL Java_com_example_androidndk_TestJNIBean_testStaticCallStaticMethod(JNIEnv *env, jclass type) { jmethodID a_method = env->GetStaticMethodID(type,"staticDescribe","()Ljava/lang/String;"); // 通过GetStaticMethodID方法获取方法的methodId. jstring pring= (jstring)(env)->CallStaticObjectMethod(type,a_method); // 类调用类中的方法 char *print=(char*)(env)->GetStringUTFChars(pring,0); // 转换格式输出。 return env->NewStringUTF(print); } 复制代码
上面的调用其实很好区别,和我们平常在Java中使用一致,当时静态的只需要传个jclass对象即可调用静态方法,非静态方法则需要实例化之后再调用。
针对多态情况,咱们如何准确调用我们想要的方法呢?举一个例子,我有个Father类,里面有个toString方法,然后Child 继承Father并重写toString方法,这时候我们如何在JNIEnv环境中分别调用Father和Child的toString呢?
代码实现如下:
public class Father { public String toString(){ return "调用的父类中的方法"; } } public class Child extends Father { @Override public String toString(){ return "调用的子类中的方法"; } } public class TestJNIBean{ static { System.loadLibrary("native-lib"); } public Father father = new Child(); public native String testCallFatherMethod(); //调用父类toString方法 public native String testCallChildMethod(); // 调用子类toString方法 } 复制代码
cpp中代码实现:
extern "C" JNIEXPORT jstring JNICALL Java_com_example_androidndk_TestJNIBean_testCallFatherMethod(JNIEnv *env, jobject instance) { jclass clazz = env -> GetObjectClass(instance); jfieldID father_field = env -> GetFieldID(clazz,"father","Lcom/example/androidndk/Father;"); jobject mFather = env -> GetObjectField(instance,father_field); jclass clazz_father = env -> FindClass("com/example/androidndk/Father"); jmethodID use_call_non_virtual = env -> GetMethodID(clazz_father,"toString","()Ljava/lang/String;"); // 如果调用父类方法用CallNonvirtual***Method jstring result = (jstring) env->CallNonvirtualObjectMethod(mFather,clazz_father,use_call_non_virtual); return result; } extern "C" JNIEXPORT jstring JNICALL Java_com_example_androidndk_TestJNIBean_testCallChildMethod(JNIEnv *env, jobject instance) { jclass clazz = env -> GetObjectClass(instance); jfieldID father_field = env -> GetFieldID(clazz,"father","Lcom/example/androidndk/Father;"); jobject mFather = env -> GetObjectField(instance,father_field); jclass clazz_father = env -> FindClass("com/example/androidndk/Father"); jmethodID use_call_non_virtual = env -> GetMethodID(clazz_father,"toString","()Ljava/lang/String;"); // 如果调用父类方法用Call***Method jstring result = (jstring) env->CallObjectMethod(mFather,use_call_non_virtual); return result; } 复制代码
分别调用运行testCallFatherMethod和testCallChildMethod后的输出结果为:
调用的父类中的方法 调用的子类中的方法 复制代码
从上面的例子我们也可以看出,JNIEnv中调用父类和子类方法的唯一区别在于调用方法时,当调用父类的方法时使用CallNonvirtual***Method,而调用子类方法时则是直接使用Call***Method。
好了,现在我们已经理清了JNIEnv中如何运用多态。现在咱们来了解下如何修改Java变量。
修改Java中对应的变量思路其实也很简单。
代码如下:
public class TestJNIBean{ static { System.loadLibrary("native-lib"); } public int modelNumber = 1; /** * 修改modelNumber属性 */ public native void testChangeField(); } /* * 修改属性 */ extern "C" JNIEXPORT void JNICALL Java_com_example_androidndk_TestJNIBean_testChangeField(JNIEnv *env, jobject instance) { jclass a_class = env->GetObjectClass(instance); // 获取当前对象的类 jfieldID a_field = env->GetFieldID(a_class,"modelNumber","I"); // 提取类中的属性 env->SetIntField(instance,a_field,100); // 重新给属性赋值 } 复制代码
调用testChangeField()方法后,TestJNIBean中的modelNumber将会修改为100。
JNIEnv中获取字符串的一些方法:
extern "C" JNIEXPORT jstring JNICALL Java_com_example_androidndk_TestJNIBean_testNewString(JNIEnv *env, jclass type) { jchar* data = new jchar[7]; data[0] = 'a'; data[1] = 's'; data[2] = 'e'; data[3] = 'r'; data[4] = 'b'; data[5] = 'a'; data[6] = '0'; return env->NewString(data, 5); } 复制代码
extern "C" JNIEXPORT jstring JNICALL Java_com_example_androidndk_TestJNIBean_testNewStringUTF(JNIEnv *env, jclass type) { std::string learn="learn android from aserbao"; return env->NewStringUTF(learn.c_str());//c_str()函数返回一个指向正规C字符串的指针, 内容与本string串相同. } 复制代码
extern "C" JNIEXPORT jint JNICALL Java_com_example_androidndk_TestJNIBean_testStringLength(JNIEnv *env, jclass type, jstring inputString_) { jint result = env -> GetStringLength(inputString_); jint resultUTF = env -> GetStringUTFLength(inputString_); return result; } 复制代码
extern "C" JNIEXPORT jstring JNICALL Java_com_example_androidndk_TestJNIBean_testGetStringRegion(JNIEnv *env, jclass type, jstring inputString_) { jint length = env -> GetStringUTFLength(inputString_); jint half = length /2; jchar* chars = new jchar[half]; env -> GetStringRegion(inputString_,0,length/2,chars); return env->NewString(chars,half); } 复制代码
extern "C" JNIEXPORT jstring JNICALL Java_com_example_androidndk_TestJNIBean_testGetStringUTFRegion(JNIEnv *env, jclass type, jstring inputString_) { jint length = env -> GetStringUTFLength(inputString_); jint half = length /2; char* chars = new char[half]; env -> GetStringUTFRegion(inputString_,0,length/2,chars); return env->NewStringUTF(chars); } 复制代码
jchar* GetStringChars(jstring string, jboolean* isCopy):将jstring对象转成jchar字符串指针。此方法返回的jchar是一个UTF-16编码的宽字符串。
注意:返回的指针可能指向 java String 对象,也可能是指向 jni 中的拷贝,参数 isCopy 用于返回是否是拷贝,如果isCopy参数设置的是NUll,则不会关心是否对Java的String对象进行拷贝。返回值是用 const修饰的,所以获取的(Unicode)char数组是不能被更改的;还有注意在使用完了之后要对内存进行释放,释放方法是:ReleaseStringChars(jstring string, const jchar* chars)。
char* GetStringUTFChars(jstring string, jboolean* isCopy):将jstring对象转成jchar字符串指针。方法返回的jchar是一个UTF-8编码的字符串。
返回指针同样可能指向 java String对象。取决与isCopy的值。返回值是const修饰,不支持修改。使用完了也需释放,释放的方法为:ReleaseStringUTFChars(jstring string, const char* utf)。
const jchar* GetStringCritical(jstring string, jboolean* isCopy):将jstring转换成const jchar*。他和GetStringChars/GetStringUTF的区别在于GetStringCritical更倾向于获取 java String 的指针,而不是进行拷贝;
对应的释放方法:ReleaseStringCritical(jstring string, const jchar* carray)。
特别注意的是,在GetStringCritical调用和ReleaseStringCritical释放这两个方法调用的之间是一个关键区,不能调用其他JNI函数。否则将造成关键区代码执行期间垃圾回收器停止运作,任何触发垃圾回收器的线程也会暂停,其他的触发垃圾回收器的线程不能前进直到当前线程结束而激活垃圾回收器。就是说在关键区域中千万不要出现中断操作,或在JVM中分配任何新对象;否则会 造成JVM死锁。
通过一个方法来使用下上面方法,代码如下:
extern "C" JNIEXPORT void JNICALL Java_com_example_androidndk_TestJNIBean_testGetTArrayElement(JNIEnv *env, jobject instance) { jclass jclazz = env -> GetObjectClass(instance); //获取Java中数组属性arrays的id jfieldID fid_arrays = env-> GetFieldID(jclazz , "testArrays","[I") ; //获取Java中数组属性arrays的对象 jintArray jint_arr = (jintArray) env->GetObjectField(instance, fid_arrays) ; //获取arrays对象的指针 jint* int_arr = env->GetIntArrayElements(jint_arr, NULL) ; //获取数组的长度 jsize len = env->GetArrayLength(jint_arr) ; LOGD("---------------获取到的原始数据为---------------"); for(int i = 0; i < len; i++){ LOGD("len %d",int_arr[i]); } //新建一个jintArray对象 jintArray jint_arr_temp = env->NewIntArray (len) ; //获取jint_arr_temp对象的指针 jint* int_arr_temp = env->GetIntArrayElements (jint_arr_temp , NULL) ; //计数 jint count = 0; LOGD("---------------打印其中是奇数---------------"); //奇数数位存入到int_ _arr_ temp内存中 for (jsize j=0;j<len;j++) { jint result = int_arr[j]; if (result % 2 != 0) { int_arr_temp[count++] = result; } } //打印int_ _arr_ temp内存中的数组 for(int k = 0; k < count; k++){ LOGD("len %d",int_arr_temp[k]); } LOGD("---------------打印前两位---------------"); //将数组中一段(1-2)数据拷贝到内存中,并且打印出来 jint* buffer = new jint[len] ; //获取数组中从0开始长度为2的一段数据值 env->GetIntArrayRegion(jint_arr,0,2,buffer) ; for(int z=0;z<2;z++){ LOGD("len %d",buffer[ z]); } LOGD("---------------重新赋值打印---------------"); //创建一个新的int数组 jint* buffers = new jint[3]; jint start = 100; for (int n = start; n < 3+start ; ++n) { buffers[n-start] = n+1; } //重新给jint_arr数组中的从第1位开始往后3个数赋值 env -> SetIntArrayRegion(jint_arr,1,3,buffers); //从新获取数据指针 int_arr = env -> GetIntArrayElements(jint_arr,NULL); for (int i = 0; i < len; ++i) { LOGD("重新赋值之后的结果为 %d",int_arr[i]); } LOGD("---------------排序---------------"); std::sort(int_arr,int_arr+len); for (int i = 0; i < len; ++i) { LOGD("排序结果为 %d",int_arr[i]); } LOGD("---------------数据处理完成---------------"); } 复制代码
运行结果:
D/learn JNI: ---------------获取到的原始数据为--------------- D/learn JNI: len 1 D/learn JNI: len 2 D/learn JNI: len 3 D/learn JNI: len 4 D/learn JNI: len 5 D/learn JNI: len 8 D/learn JNI: len 6 D/learn JNI: ---------------打印其中是奇数--------------- D/learn JNI: len 1 D/learn JNI: len 3 D/learn JNI: len 5 D/learn JNI: ---------------打印前两位--------------- D/learn JNI: len 1 D/learn JNI: len 2 D/learn JNI: ---------------重新赋值打印--------------- D/learn JNI: 重新赋值之后的结果为 1 D/learn JNI: 重新赋值之后的结果为 101 D/learn JNI: 重新赋值之后的结果为 102 D/learn JNI: 重新赋值之后的结果为 103 D/learn JNI: 重新赋值之后的结果为 5 D/learn JNI: 重新赋值之后的结果为 8 D/learn JNI: 重新赋值之后的结果为 6 D/learn JNI: ---------------排序--------------- D/learn JNI: 排序结果为 1 D/learn JNI: 排序结果为 5 D/learn JNI: 排序结果为 6 D/learn JNI: 排序结果为 8 D/learn JNI: 排序结果为 101 D/learn JNI: 排序结果为 102 D/learn JNI: 排序结果为 103 D/learn JNI: ---------------数据处理完成--------------- 复制代码
从JVM创建的对象传递到C/C++代码时会产生引用,由于Java的垃圾回收机制限制,只要对象有引用存在就不会被回收。所以无论在C/C++中还是Java中我们在使用引用的时候需要特别注意。下面讲下C/C++中的引用:
全局引用可以跨多个线程,在多个函数中都有效。全局引用需要通过NewGlobalRef方法手动创建,对应的释放全局引用的方法为DeleteGlobalRef
局部引用很常见,基本上通过JNI函数获取到的返回引用都算局部引用,局部引用只在单个函数中有效。局部引用会在函数返回时自动释放,当然我们也可以通过DeleteLocalRef方法手动释放。
弱引用也需要自己手动创建,作用和全局引用的作用相似,不同点在于弱引用不会阻止垃圾回收器对引用所指对象的回收。我们可以通过NewWeakGlobalRef方法来创建弱引用,也可以通过DeleteWeakGlobalRef来释放对应的弱引用。
在Jni中C/C++层打印日志是帮助我们调试代码较为重要的一步。简单分为三步:
#include <android/log.h> 复制代码
#define TAG "learn JNI" // 这个是自定义的LOG的标识 #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,TAG ,__VA_ARGS__) // 定义LOGD类型 #define LOGI(...) __android_log_print(ANDROID_LOG_INFO,TAG ,__VA_ARGS__) // 定义LOGI类型 #define LOGW(...) __android_log_print(ANDROID_LOG_WARN,TAG ,__VA_ARGS__) // 定义LOGW类型 #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,TAG ,__VA_ARGS__) // 定义LOGE类型 #define LOGF(...) __android_log_print(ANDROID_LOG_FATAL,TAG ,__VA_ARGS__) // 定义LOGF类型 复制代码
LOGE("my name is %s/n", "aserbao");//简约型 __android_log_print(ANDROID_LOG_INFO, "android", "my name is %s/n", "aserbao"); //如果第二步省略也可以通过这个直接打印日志。 复制代码
上面是我们新建项目自动创建的cpp目录和.cpp文件。如果想自己写一个该怎么办呢?且听我娓娓道来:
比如我现在创建一个工具类Car,里面想写个native方法叫getCarName(),我们如何快速得到对应的.cpp文件呢?方法也很简单,我们只需要按步骤运行几个命令就行了。步骤如下:
public class Car { static { System.loadLibrary("native-lib"); } public native String getCarName(); } 复制代码
aserbao:androidndk aserbao$ cd /Users/aserbao/aserbao/code/code/framework/AndroidNDK/app/src/main/java/com/example/androidndk aserbao:androidndk aserbao$ javac -h . Car.java 复制代码
#include <jni.h> #include <string> extern "C" JNIEXPORT jstring JNICALL Java_com_example_androidndk_Car_getCarName(JNIEnv *env, jobject instance) { std::string hello = "This is a beautiful car"; return env->NewStringUTF(hello.c_str()); } 复制代码
我将返回修改为”This is a beautiful car“,所以运行后我们可以看到hello world C++ 变成了”This is a beautiful car“。大功告成。
在学习C/C++调用Java代码之前,我们先讲一个小知识点。Java中方法的签名。不知道大家有没有了解过,其实Java中每个方法,都有其对应的签名的。在接下来的调用过程中,我们会多次运用到方法签名。
首先讲一下方法签名如何获取? 很简单,比如上面的对象Car,我们在里面写一个toString方法。我们可以首先通过javac命令生成.class文件,然后再通过javap命令来获取对应的方法签名,使用方法及结果如下:
javap -s **.class 复制代码
对应的签名类型如下:
类型 | 相应的签名 |
---|---|
boolean | Z |
float | F |
byte | B |
double | D |
char | C |
void | V |
short | S |
object | L用/分割包的完整类名; Ljava/lang/String; |
int | I |
Array | [签名[I [Ljava/lang/Object; |
long | L |
Method | (参数类型签名..)返回值类型签名 |
好了,拿到方法签名了,我们就可以开始在C/C++中来调用Java代码了。来来来,现在我们一起来学习如何在C/C++中调用Java代码。
javac *.java 复制代码
javac -h . *.java 复制代码
javap -s -p *.class 复制代码
异常处理通常我们分为两步,捕获异常和抛出异常。在C/C++中实现这两步也相当简单。我们先看几个函数:
代码实例:
//Java代码 public class TestJNIBean{ static { System.loadLibrary("native-lib"); } public native void testThrowException(); private void throwException() throws NullPointerException{ throw new NullPointerException("this is an NullPointerException"); } } //JNI代码 extern "C" JNIEXPORT void JNICALL Java_com_example_androidndk_TestJNIBean_testThrowException(JNIEnv *env, jobject instance) { jclass jclazz = env -> GetObjectClass(instance); jmethodID throwExc = env -> GetMethodID(jclazz,"throwException","()V"); if (throwExc == NULL) return; env -> CallVoidMethod(instance,throwExc); jthrowable excOcc = env -> ExceptionOccurred(); if (excOcc){ jclass newExcCls ; env -> ExceptionDescribe();//打印异常堆栈信息 env -> ExceptionClear(); jclass newExcClazz = env -> FindClass("java/lang/IllegalArgumentException"); if (newExcClazz == NULL) return; env -> ThrowNew(newExcClazz,"this is a IllegalArgumentException"); } } 复制代码
运行结果:
12-05 15:20:27.547 8077-8077/com.example.androidndk E/AndroidRuntime: FATAL EXCEPTION: main Process: com.example.androidndk, PID: 8077 java.lang.IllegalArgumentException: this is a IllegalArgumentException at com.example.androidndk.TestJNIBean.testThrowException(Native Method) at com.example.androidndk.MainActivity.itemClickBack(MainActivity.java:90) at com.example.androidndk.base.viewHolder.BaseClickViewHolder$1.onClick(BaseClickViewHolder.java:32) at android.view.View.performClick(View.java:5198) at android.view.View$PerformClick.run(View.java:21147) at android.os.Handler.handleCallback(Handler.java:739) at android.os.Handler.dispatchMessage(Handler.java:95) at android.os.Looper.loop(Looper.java:148) at android.app.ActivityThread.main(ActivityThread.java:5417) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616) --------- beginning of system 复制代码
本来想这将这个项目也放到AserbaoAndroid里面的,后来又偷懒,新建了个项目,整篇文章的源码存放地址在: github.com/aserbao/And…
这篇文章从开始动笔到最后完工差不多断断续续一个多月时间了,转眼都快过年了,目测这是年前最后一篇,原本计划想着将so的相关知识点也写到这篇文章里面,后面由于多方面考虑就改变主意了,关于so的相关知识会重新出一篇较详细的文章。
这篇文章讲的还是学习JNI中必备的一些东西,希望对大家有用吧,后期有时间再出第二篇关于C/C++库的接入和使用吧。
最后,还是那句老话,如果大家在开发Android中有遇到我写过文章中的问题,可以在我公众号「aserbaocool」给我留言,知无不言,同时也欢迎大家来加入Android交流群。