Android JNI/NDK 使用全解

很久没发文章了,这篇文章是是10月底开始计划的,转眼到如今12月都快过一半了,我太难了……,不过好在终于完成了,今晚必须去吃宵夜。深圳北,往北两千米的**烧烤,有木有人过来?我请客,没有到时候我再来问一遍。html

先看目录,各位以为内容对你有用再继续往下看,毕竟显示有一万多个字呢,怕没用的话耽误你们宝贵的时间。java

闲聊一下为何写这篇文章?

以前写过一篇关于C代码生成和调试so库的文章。前段时间在继承一个音频检测库的时候出现了点问题,又复习了下JNI部分,顺便整理成文,分享给你们。android

文章目标和指望

本文是一个 NDK/JNI 系列基础到进阶教程,目标是但愿观看这篇文章的朋友们能对Android中使用C/C++代码,集成C/C++库有一个比较基本的了解,而且能巧妙的应用到项目中。git

好了,说完目的,我们一如既往,学JNI以前,先来个给本身提几个问题:github

学前三问?

了解是什么?用来作什么?以及为何?数组

什么是JNI/NDK?两者的区别是什么?

什么是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到底有什么用呢?

JNI/NDK用来作什么?

一句话,快速调用C/C++的动态库。除了调用C/C++以外别无它用。

就是这么简单好吧。知道作什么以后,我们学这玩意有啥用呢?

学JNI/NDK能给我带来什么好处?

暂时能想到的两个点,一个是能让我在开发中愉快的使用C/C++库,第二个就是能在安全攻防这一块有更深刻的了解。其实不管这两个点中的哪一个点都能让我有足够动力学下去。因此,想啥呢,搞定他。

JNI/NDK如何使用?

如何配置JNI/NDK环境?

配置NDK的环境比较简单。咱们能够经过简单三步来实现:

  • 第一步:下载NDK。能够在Google官方下载,也能够直接打开AS进行下载,建议选后者。这里能够将LLDB和CMake也下载上。
  • 第二步:配置NDK路径,能够直接在AS里面进行配置,方便快捷。
  • 第三步: 打开控制台,cd到NDK的指定目录下,验证NDK环境是否成功。

ok,验证如上图所示说明你NDK配置成功了。so easy。

HelloWorld一块儿进入C/C++的世界

如今开始,我们一块儿进入HelloWorld的世界。咱们一块儿来经过AS建立一个Native C++项目。主要步骤以下:

  • 第一步:File --> New --> New Project 滑动到选框底部,选中Native C++,点击下一步。
  • 第二步:选个名字,而后一直点Next,直到Finish完成。

简单通俗易懂有木有?好了,项目建立成功,运行,看界面,显示Hello World,项目建立成功。

如何在Android中调用C/C++代码?

从上面新建的项目中咱们看到一个cpp目录,咱们所写的C/C++代码就这这个目录下面。其中会发现有一个名为native-lib.cpp的文件,这就是用C/C++赋值Hello World的地方。

Android 中调用C/C++库的步骤:

  • 第一步:经过System.loadLibrary引入C代码库名。
  • 第二步:在cpp目录下的natice-lib.cpp中编写C/C++代码。
  • 第二步:调用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。

JNI/NDK的API

在C/C++本地代码中访问Java端的代码,一个常见的应用就是获取类的属性和调用类的方法,为了在C/C++中表示属性和方法,JNI在jni.h头文件中定义了jfieldID,jmethodID类型来分别表明Java端的属性和方法。在访问或者设置Java属性的时候,首先就要先在本地代码取得表明该Java属性的jfeldID,而后才能在本地代码中进行Java属性操做,一样,须要调用Java端的方法时,也是须要取得表明该方法的jmethodID才能进行Java方法调用。

接下来,我们来尝试下如何在native中调用Java中的方法。先看下两个常见的类型:

JNIEnv 类型和jobject类型

在上面的native-lib.cpp中,咱们看到getCarName方法中有两个参数,分别是JNIEnv *env,一个是jobjet instance。简单介绍下这两个类型的做用。

JNIEnv 类型

JNIEnv类型实际上表明了Java环境,经过JNIEnv*指针就能够对Java端的代码进行操做。好比咱们可使用JNIEnv来建立Java类中的对象,调用Java对象的方法,获取Java对象中的属性等。

JNIEnv类中有不少函数能够用,以下所示:

  • NewObject: 建立Java类中的对象。
  • NewString: 建立Java类中的String对象。
  • NewArray: 建立类型为Type的数组对象。
  • GetField: 获取类型为Type的字段。
  • SetField: 设置类型为Type的字段的值。
  • GetStaticField: 获取类型为Type的static的字段。
  • SetStaticField: 设置类型为Type的static的字段的值。
  • CallMethod: 调用返回类型为Type的方法。
  • CallStaticMethod: 调用返回值类型为Type的static 方法。 固然,除了这些经常使用的函数方法外,还有更多可使用的函数,能够在jni.h文件中进行查看,或者参考https://docs.oracle.com/javase/6/docs/technotes/guides/jni/spec/jniTOC.html连接去查询相关方法,上面都说得特别清楚。

好了,说完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代码。

如何获取Java中的类并生成对象

JNIEnv类中有以下几个方法能够获取java中的类:

  • jclass FindClass(const char* name) 根据类名来查找一个类,完整类名

须要咱们注意的是,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;
}
复制代码
  • jclass GetObjectClass(jobject obj) 根据一个对象,获取该对象的类

这个方法比较好理解,根据上面咱们讲的根据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获取到的
    ……
}
复制代码
  • jclass GetSuperClass(jclass obj) 获取一个传入的对象获取他的父类的jclass。

好了,咱们知道怎么经过JNIEnv中获取Java中的类,接下来咱们来学习如何获取并调用Java中的方法。

如何在C/C++中调用Java方法?

在JNIEnv环境下,咱们有以下两种方法能够获取方法和属性:

  • GetMethodID: 获取非静态方法的ID;
  • GetStaticMethodID: 获取静态方法的ID; 来取得相应的jmethodID。

GetMethodID方法以下:

jmethodID GetMethodID(jclass clazz, const char* name, const char* sig)
复制代码

方法的参数说明:

  • clazz: 这个方法依赖的类对象的class对象。
  • name: 这个字段的名称。
  • sign: 这个字段的签名(每一个变量,每一个方法都有对应的签名)。

举一个小例子,好比咱们要在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对象便可调用静态方法,非静态方法则须要实例化以后再调用。

如何在C/C++中调用父类的方法?

针对多态状况,我们如何准确调用咱们想要的方法呢?举一个例子,我有个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变量。

如何在C/C++中修改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。

如何在C/C++中操做Java字符串?

  1. Java 中字符串和C/C++中字符创的区别在于:Java中String对象是Unicode的时候,不管是中文,字母,仍是标点符号,都是一个字符占两个字节的。

JNIEnv中获取字符串的一些方法:

  • jstring NewString(const jchar* unicodeChars, jsize len):生成jstring对象,将(Unicode)char数组换成jstring对象。好比下面这样:
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);
}
复制代码
  • jstring NewStringUTF(const char* bytes):利用(UTF-8)char数组生成并返回 java String对象。操做以下:
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串相同.
}
复制代码
  • jsize GetStringLength(jstring jmsg):获取字符串(Unicode)的长度。
  • jsize GetStringUTFLength(jstring string): 获取字符串((UTF-8))的长度。
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;
}
复制代码
  • void GetStringRegion(jstring str, jsize start, jsize len, jchar* buf):拷贝Java字符串并以UTF-8编码传入jstr。
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);
}

复制代码
  • void GetStringUTFRegion(jstring str, jsize start, jsize len, char* buf):拷贝Java字符串并以UTF-16编码传入jstr
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死锁。

如何在C/C++中操做Java数组?

  • jType* GetArrayElements((Array array, jboolean* isCopy)):这类方法能够把Java的基本类型数组转换成C/C++中的数组。isCopy为true的时候表示数据会拷贝一份,返回的数据的指针是副本的指针。若是false则不会拷贝,直接使用Java数据的指针。不适用isCopy能够传NULL或者0。
  • void ReleaseArrayElements(jTypeArray array, j* elems,jint mode):释放操做,只要有调用GetArrayElements方法,就必需要调用一次对应的ReleaseArrayElements方法,由于这样会删除掉可能会阻止垃圾回收的JNI本地引用。这里咱们注意如下这个方法的最后一个参数mode,他的做用主要用于避免在处理副本数据的时产生对Java堆没必要要的影响。若是GetArrayElements中的isCopy为true,咱们才须要设置mode,为false咱们mode能够不用处理,赋值0。mode有三个值:
    • 0:更新Java堆上的数据并释放副本使用所占有的空间。
    • JNI_COMMIT:提交,更新Java堆上的数据,不释放副本使用的空间。
    • JNI_ABORT:撤销,不更新Java堆上的数据,释放副本使用所占有的空间。
  • void* GetPrimitiveArrayCritical(jarray array, jboolean* isCopy):做用相似与GetArrayElements。这个方法可能会经过VM返回指向原始数组的指针。注意在使用此方法的时候避免死锁问题。
  • void ReleasePrimitiveArrayCritical(jarray array, void* carray, jint mode):上面方法对应的释放方法。注意这两个方法之间不要调用任何JNI的函数方法。由于可能会致使当前线程阻塞。
  • void GetArrayRegion(JNIEnv *env, ArrayType array,jsize start, jsize len, Type *buf):和GetStringRegion的做用是类似的,事先在C/C++中建立一个缓存区,而后将Java中的原始数组拷贝到缓冲区中去。
  • void SetArrayRegion(JNIEnv *env, ArrayType array,jsize start, jsize len, const Type *buf):上面方法的对应方法,将缓冲区的部分数据设置回Java原始数组中。
  • jsize GetArrayLength(JNIEnv *env, jarray array):获取数组长度。
  • jobjectArray NewObjectArray(JNIEnv *env, jsize length,jclass elementClass, jobject initialElement):建立指定长度的数组。

经过一个方法来使用下上面方法,代码以下:

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: ---------------数据处理完成---------------
复制代码

JNI中几种引用的区别?

从JVM建立的对象传递到C/C++代码时会产生引用,因为Java的垃圾回收机制限制,只要对象有引用存在就不会被回收。因此不管在C/C++中仍是Java中咱们在使用引用的时候须要特别注意。下面讲下C/C++中的引用:

全局引用

全局引用能够跨多个线程,在多个函数中都有效。全局引用须要经过NewGlobalRef方法手动建立,对应的释放全局引用的方法为DeleteGlobalRef

局部引用

局部引用很常见,基本上经过JNI函数获取到的返回引用都算局部引用,局部引用只在单个函数中有效。局部引用会在函数返回时自动释放,固然咱们也能够经过DeleteLocalRef方法手动释放。

弱引用

弱引用也须要本身手动建立,做用和全局引用的做用类似,不一样点在于弱引用不会阻止垃圾回收器对引用所指对象的回收。咱们能够经过NewWeakGlobalRef方法来建立弱引用,也能够经过DeleteWeakGlobalRef来释放对应的弱引用。

小技巧

如何在C/C++中打印日志?

在Jni中C/C++层打印日志是帮助咱们调试代码较为重要的一步。简单分为三步:

  • 第一步:在须要打印日志的文件头部导入android下的log日志功能。
#include <android/log.h>
复制代码
  • 第二步:自定义LOGD标记。(可省略)
#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文件。若是想本身写一个该怎么办呢?且听我娓娓道来:

如何经过*.java生成*.cpp?

好比我如今建立一个工具类Car,里面想写个native方法叫getCarName(),咱们如何快速获得对应的.cpp文件呢?方法也很简单,咱们只须要按步骤运行几个命令就好了。步骤以下:

  • 第一步:新建工具类Car,写一个本地静态方法getCarName()。
public class Car {
    static {
        System.loadLibrary("native-lib");
    }
    public native String getCarName();
}
复制代码
  • 第二步:到Terimal中cd到Car目录,运行命令javac -h . Car.java就能在当前目录获得对应的.h结尾的文件。
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
复制代码
  • 第三步:将.h修改成natice-lib.cpp并放到cpp目录下,并在对应方法下修改返回。
#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“。大功告成。

如何获取Java中方法的签名?

在学习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代码。

  • .java 生成.class
javac *.java 
复制代码
  • *.java 生成 *.h
javac -h . *.java
复制代码
  • 查看*.class中的方法和签名
javap -s -p *.class
复制代码

如何在C/C++中处理异常?

异常处理一般咱们分为两步,捕获异常和抛出异常。在C/C++中实现这两步也至关简单。咱们先看几个函数:

  • ExceptionCheck:检测是否有异常,有返回JNI_TRUE,不然返回FALSE。
  • ExceptionOccurred:判断是否有异常,有返回异常,没有返回NULL。
  • ExceptionClear:清除异常堆栈信息。
  • Throw:抛出当前异常。
  • ThrowNew:建立一个新异常,并自定义异常信息。
  • FatalError:致命错误,而且终止当前VM。

代码实例:

//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交流群。

相关文章
相关标签/搜索