很久没发文章了,这篇文章是是10月底开始计划的,转眼到如今12月都快过一半了,我太难了……,不过好在终于完成了,今晚必须去吃宵夜。深圳北,往北两千米的**烧烤,有木有人过来?我请客,没有到时候我再来问一遍。html
先看目录,各位以为内容对你有用再继续往下看,毕竟显示有一万多个字呢,怕没用的话耽误你们宝贵的时间。java
以前写过一篇关于C代码生成和调试so库的文章。前段时间在继承一个音频检测库的时候出现了点问题,又复习了下JNI部分,顺便整理成文,分享给你们。android
本文是一个 NDK/JNI 系列基础到进阶教程,目标是但愿观看这篇文章的朋友们能对Android中使用C/C++代码,集成C/C++库有一个比较基本的了解,而且能巧妙的应用到项目中。git
好了,说完目的,我们一如既往,学JNI以前,先来个给本身提几个问题:github
了解是什么?用来作什么?以及为何?数组
什么是JNI?缓存
JNI,全名 Java Native Interface,是Java本地接口,JNI是Java调用Native 语言的一种特性,经过JNI可使得Java与C/C++机型交互。简单点说就是JNI是Java中调用C/C++的统称。安全
什么是NDK?bash
NDK 全名Native Develop Kit,官方说法:Android NDK 是一套容许您使用 C 和 C++ 等语言,以原生代码实现部分应用的工具集。在开发某些类型的应用时,这有助于您重复使用以这些语言编写的代码库。oracle
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 类型
JNIEnv类型实际上表明了Java环境,经过JNIEnv*指针就能够对Java端的代码进行操做。好比咱们可使用JNIEnv来建立Java类中的对象,调用Java对象的方法,获取Java对象中的属性等。
JNIEnv类中有不少函数能够用,以下所示:
好了,说完JNIEnv,接下来咱们讲第二个 jobject。
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 类型和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交流群。