Android JNI 基础学习讲解

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

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

闲聊一下为什么写这篇文章?

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

文章目标和期望

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

好了,说完目的,咱们一如既往,学JNI之前,先来个给自己提几个问题:

学前三问?

了解是什么?用来做什么?以及为什么?

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

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

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对象。
  • New<Type>Array: 创建类型为Type的数组对象。
  • Get<Type>Field: 获取类型为Type的字段。
  • Set<Type>Field: 设置类型为Type的字段的值。
  • GetStatic<Type>Field: 获取类型为Type的static的字段。
  • SetStatic<Type>Field: 设置类型为Type的static的字段的值。
  • Call<Type>Method: 调用返回类型为Type的方法。
  • CallStatic<Type>Method: 调用返回值类型为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* Get<Type>ArrayElements((<Type>Array array, jboolean* isCopy)):这类方法可以把Java的基本类型数组转换成C/C++中的数组。isCopy为true的时候表示数据会拷贝一份,返回的数据的指针是副本的指针。如果false则不会拷贝,直接使用Java数据的指针。不适用isCopy可以传NULL或者0。
  • void Release<Type>ArrayElements(jTypeArray array, j<Type>* elems,jint mode):释放操作,只要有调用Get<Type>ArrayElements方法,就必须要调用一次对应的Release<Type>ArrayElements方法,因为这样会删除掉可能会阻止垃圾回收的JNI本地引用。这里我们注意以下这个方法的最后一个参数mode,他的作用主要用于避免在处理副本数据的时产生对Java堆不必要的影响。如果Get<Type>ArrayElements中的isCopy为true,我们才需要设置mode,为false我们mode可以不用处理,赋值0。mode有三个值:
    • 0:更新Java堆上的数据并释放副本使用所占有的空间。
    • JNI_COMMIT:提交,更新Java堆上的数据,不释放副本使用的空间。
    • JNI_ABORT:撤销,不更新Java堆上的数据,释放副本使用所占有的空间。
  • void* GetPrimitiveArrayCritical(jarray array, jboolean* isCopy):作用类似与Get<Type>ArrayElements。这个方法可能会通过VM返回指向原始数组的指针。注意在使用此方法的时候避免死锁问题。
  • void ReleasePrimitiveArrayCritical(jarray array, void* carray, jint mode):上面方法对应的释放方法。注意这两个方法之间不要调用任何JNI的函数方法。因为可能会导致当前线程阻塞。
  • void Get<Type>ArrayRegion(JNIEnv *env, ArrayType array,jsize start, jsize len, Type *buf):和GetStringRegion的作用是相似的,事先在C/C++中创建一个缓存区,然后将Java中的原始数组拷贝到缓冲区中去。
  • void Set<Type>ArrayRegion(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里面的,后来又偷懒,新建了个项目,整篇文章的源码存放地址在:https://github.com/aserbao/AndroidNDK

参考文章及链接

文章总结

这篇文章从开始动笔到最后完工差不多断断续续一个多月时间了,转眼都快过年了,目测这是年前最后一篇,原本计划想着将so的相关知识点也写到这篇文章里面,后面由于多方面考虑就改变主意了,关于so的相关知识会重新出一篇较详细的文章。

这篇文章讲的还是学习JNI中必备的一些东西,希望对大家有用吧,后期有时间再出第二篇关于C/C++库的接入和使用吧。

最后,还是那句老话,如果大家在开发Android中有遇到我写过文章中的问题,可以在我公众号「aserbaocool」给我留言,知无不言,同时也欢迎大家来加入Android交流群。