LCODER之JNI系列: JNI详解

437 阅读29分钟

JNI大纲

一、JNI概述

1、什么是JNI?

Java Native Interface 即Java本地接口。

2、 JNI的作用是什么?

使Java和本地其他类型的语言(C\C++)进行交互。JNI是属于Java的,与Android并无直接的关系,实际中的驱动都是C\C++开发的,通过JNI,Java可以调用C\C++实现的驱动,从而扩展Java虚拟机的能力。实际使用中,Java需要与本地代码进行交互,因为Java具备跨平台的特点,所以Java与本地代码交互的能力非常弱,采用JNI增强Java与本地代码交互的能力。

3、 JNI的官方API

JNI官方APIJNI API中文版

二、宏:

1. 什么是宏?

宏(Macro)是一种批量处理的称谓。计算机科学里的宏是一种抽象(Abstraction),它根据一系列预定义的规则替换一定的文本模式。解释器或编译器在遇到宏时会自动进行这一模式替换。对于编译语言,宏展开在编译时发生,进行宏展开的工具常被称为宏展开器。

2、预处理器:

定义:

预处理器是在真正的编译开始之前由编译器调用的独立程序。预处理器可以删除注释、包含其他文件以及执行宏替代。
预处理器不是编译器,预处理器主要完成文本替换的操作,预处理器都是用 #xxx 的写法,并不是注释。预处理器最先执行。

预处理器包括:
#include  导入头文件
#if       if判断操作  【if的范畴 必须endif
#elif     else if
#else     else
#endif    结束if
#define   定义一个宏
#ifndef   如果没有定义这个宏 【if的范畴 必须endif
#undef    取消宏定义
#pragma   设定编译器的状态

2.1 #if

#include <iostream>
using namespace std;

int main() {
    // std::cout << "宏" << std::endl;

#if 1 // if 非 0 即 true
    cout <<  "真" << endl;

#elif 0 // else if
    cout <<  "假" << endl;

#else
    cout << "都不满足" << endl;

#endif // 结束if  y
    cout << "结束if" << endl;

    return 0;
}

2.2 定义宏

#ifndef CLIONCPPPROJECT_T2_H // 如果没有定义这个宏  解决循环拷贝的问题
#define CLIONCPPPROJECT_T2_H // 我就定义这个宏
案例: 使用宏来实现测试环境和正式环境的切换
(1) 编写T.h 头文件,在其中定义宏
#ifndef ISRELEASE // 如果没有定义ISRELEASE这个宏
#define ISRELEASE 0 // 那么就定义ISRELEASE这个宏  非0即true 

#if ISRELEASE == true
#define RELEASE // 定义代表正式环境的宏 : RELEASE

#elif ISRELEASE == false
#define DEBUG // 定义代表测试环境的宏: DEBUG

#endif
#endif //UNTITLED_MAIN_H

(2)在T.cpp中使用T.h中的宏,来切换正式环境和测试环境
#include <iostream>
#include "main.h" // 导入main.h
using namespace std;

int main() {
    std::cout << "Hello, World!" << std::endl;

    // 在这里使用宏,来进行测试环境和正式环境的交替
#ifdef DEBUG
    cout <<"在测试环境下,迭代功能"<<endl;

#else RELEASE
    cout <<"在正式环境下,功能上线中..."<< endl;

#endif
    return 0;
}

2.3. 取消宏

使用#undef来取消宏
#include <iostream>
using namespace std;
int main(){

    // 定义宏
#ifndef KIMLIU // 如果没有定义这个宏,
#define KIMLIU // 那么就定义这个宏

#ifdef KIMLIU // 是否定义了这个宏
cout<<"定义了KIMLIU这个宏~"<<endl;

#ifdef KIMLIU // 是否定义了这个宏
cout<<"定义了KIMLIU这个宏"<<endl;

#undef KIMLIU // 取消宏定义

#ifdef KIMLIU
cout <<"定义了KIMLIU这个宏"<< endl;
#else
cout<<"取消了KIMLIU这个宏"<<endl;

#endif
#endif
#endif
#endif

    return 0;
}

2.4. 宏变量

什么是宏变量?

Java中,一个用final定义的变量,不管它是类型的变量,只要用final定义了并同时指定了初始值,并且这个初始值是在编译时就被确定下来的,那么这个final变量就是一个宏变量。编译器会把程序所有用到该变量的地方直接替换成该变量的值,也就是说编译器能对宏变量进行宏替换。

通过将一段文本赋值给一个宏变量,从而可以很灵活的通过引用这个宏变量来达到使用这段文本的效果。宏变量的长度是由自身的文本长度决定的,而不是通过设定得到,所以宏变量的值是随着文本的长度而任意发生变化。宏变量包含的只是字符数据。宏变量分为用户自定义宏变量和自动宏变量。宏变量从使用范围上分还分为局部宏变量和全局宏变量。

在下面的例子中,关于宏变量的使用,已经在注释中详细的写出来了。

// 宏变量
#include <iostream>
using namespace std;

#define VALUE_I 9527  // 定义宏变量
#define VALUE_S "AAA"
#define VALUE_F 545.3f

int main(){

    int i = VALUE_I; // 预处理阶段, 宏会直接完成文本替换工作,替换后的样子 int i = 9527
    string s = VALUE_S; // 预处理阶段, 宏会直接完成文本替换工作,替换后的样子 string s = "AAA"
    float f = VALUE_F; // 预处理阶段, 宏会直接完成文本替换工作,替换后的样子 float f = 545.3f

    cout << i << "---" << s << "---" << f << endl;

    return 0;
}

2.5.宏函数

宏函数是指由宏语句组成的实现某些特定功能的函数。大多数编程库提供了很多现成的宏函数,可以实现各种功能。宏函数的调用和用户定义的宏函数使用方法相同。

// 宏函数
// 真实开发中,宏函数都是大写
#include <iostream>
using namespace std;

#define SHOW(V) cout << V << endl; // 参数列表 无需类型 返回值 看不到
#define ADD(n1,n2) n1+n2
#define CHE(n1,n2) n1*n2

// 复杂的宏函数
#define LOGIN(V) if(V==1){                    \
     cout << "满足:输入的是" << V << endl;      \
}else{                                         \
     cout << "不满足:输入的是:" << V << endl;     \
}  // 结尾不需要反斜杠

int main(){
    SHOW(8);
    SHOW(8.8f);
    SHOW(8.99);

    int r = ADD(1,2);
    cout << r << endl;

    r = ADD(1+1,2+2);
    cout << r << endl;

    LOGIN(1);

    return 0;
}

宏函数的优缺点:

优点: 宏函数的本质是文本替换,因此不会造成函数的调用开销,如:开辟栈空间(进栈)、形参压栈、函数弹栈释放....
缺点: 调用宏函数,实际上相当于是把宏函数整体拷贝到调用它的地方,因此会导致代码体积增大。

三、使用JNI

看完了前面的准备工作,下面就用一个Demo了解一下什么是JNI吧。

3.1 在AS中创建JNI项目

创建新项目,选择Native C++项目 ,然后一路点击Next.

image.png

这里AS已经自动创建了cpp文件和CMake文件。

image.png

cpp文件当然是用来写Native代码,CMakeLists.txt是用来配置so库的,关于里面的语法,会在下面注释中写清楚。

cmake_minimum_required(VERSION 3.10.2) #定义cmake支持的最小版本号

project("myapplication")

add_library( # 设置生成so库的文件名称,例如此处生成的so库文件名称应该为:libnative-lib.so
             native-lib

             # 设置生成的so库类型,类型只包含两种:
             # STATIC:静态库,为目标文件的归档文件,在链接其他目标的时候使用
             # SHARED:动态库,会被动态链接,在运行时被加载
             SHARED

             # 设置源文件的位置,可以是很多个源文件,都要添加进来,注意相对位置
             native-lib.cpp )

# 从系统里查找依赖库,可添加多个
find_library( # 例如查找系统中的log库liblog.so
              log-lib

              # liblog.so库指定的名称即为log,如同上面指定生成的libnative-lib.so库名称为native-lib一样
              log )

# 配置目标库的链接,即相互依赖关系

target_link_libraries( # 目标库(最终生成的库)
                       native-lib

                       # 依赖于log库,一般情况下,如果依赖的是系统中的库,需要加 ${} 进行引用,
                       # 如果是第三方库,可以直接引用库名,例如:
                       # 引用第三方库libthird.a,引用时直接写成third;注意,引用时,每一行只能引用一个库
                       ${log-lib} )

更多的语法,可以看看CMake的官方文档: developer.android.google.cn/ndk/guides/…

3.2 在Android代码中写入一段native代码,使用命令javah生成头文件

(1)编写本地native方法

package com.example.as_jni; // 包名,跟在javah 后面 javah com.example.as_jni.MainActivity

import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;

import com.example.as_jni.databinding.ActivityMainBinding;

public class MainActivity extends AppCompatActivity {

    // Used to load the 'native-lib' library on application startup.
    static {
//        System.load(""); // 传入绝对路径
        System.loadLibrary("native-lib");
    }

    private ActivityMainBinding binding;

    static final int A = 100;

    public native String getStringJNI();

    public String name = "KimLiu";

    public native void changeName();
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());
        changeName();
        Log.i("ly=== Android ", "onCreate: "+ name);
    }
}

(2)使用window power shell 进入到代码所在目录的java目录,使用javah命令,生成头文件

如下图所示:

image.png

3.3 头文件详解

生成文件名为com_example_as_jni_MainActivity.h的头文件,头文件的内容如下 : (每一行代码的解释都写在了注释中)

#include <jni.h>

#ifndef _Included_com_example_as_jni_MainActivity  // 如果没有定义这个宏
#define _Included_com_example_as_jni_MainActivity  // 我就定义这个宏
#ifdef __cplusplus // 如果是C++的环境
extern "C" {  // 全部采用C的标准 (禁止函数重载
#endif
#undef com_example_as_jni_MainActivity_A
#define com_example_as_jni_MainActivity_A 100L  // 常量生成的宏
    
// 函数的声明 (在native-lib.cpp中编写函数的实现
JNIEXPORT jstring
Java_com_example_as_1jni_MainActivity_getStringJNI
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif


extern "C"
JNIEXPORT void JNICALL
Java_com_example_as_1jni_MainActivity_changeName(JNIEnv *, jobject) ;

3.4 实现头文件中的函数

在com_example_as_jni_MainActivity.h头文件中声明的函数,需要在native-lib.cpp中实现。如下面这段代码所示(对函数的详细解释已经在注释中写明)

#include "com_example_as_jni_MainActivity.h"

#include <android/log.h>  // 引入打印库
// 定义打印时的TAG
#define TAG "KimLiu" // 定义宏
// 定义打印的宏函数
/**
 * __android_log_print 这个方法在 android/log.h 中
 * ... 不知道传入什么,借助JNI中的宏_VA_ARGS__来帮助填充
 */
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,TAG,__VA_ARGS__);
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,TAG,__VA_ARGS__);
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,TAG,__VA_ARGS__);
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN,TAG,__VA_ARGS__);




/**
 *  
 * 一 、extern "C"  必须加上这句话,否则报错,这里的意思就是 : 必须采用C的编译方式  为什么?看JNIEnv内部源码
 *     JNIEnv内部源码  :
 *     #if defined(__cplusplus)  // 如果是C++的方式 使用_JNIEnv
         typedef _JNIEnv JNIEnv; // 取别名
         typedef _JavaVM JavaVM;
       #else  // 否则 使用 JNINativeInterface
         typedef const struct JNINativeInterface* JNIEnv;  // 取别名
         typedef const struct JNIInvokeInterface* JavaVM;
      #endif


       JNINativeInterface :在jni.h中  是一个结构体   JNI就是学习其中的所有函数

       _JNIEnv : 也是一个结构体  源码:

       struct _JNIEnv {
            const struct JNINativeInterface* functions;  这里还是调用到了 JNINativeInterface
            .....
          }

       无论是 C 还是 C++ 最终都会调用到 JNINativeInterface  所以必须采用C的编译方式

  二、JNIEXPORT 标记该方法,可以被外部调用(在VS上不加运行会报错)
  三、jstring 相当于Java中的String 用来翻译Java中的String 给 C C++ 看,C C++没有String 只有char *
     也可以翻译C C++中的char* 给Java 看
  四、JNICALL // 代表是JNI的标记 可省略
  五、函数名称 : Java_com_example_as_1jni_MainActivity_getStringJNI:
               1. Java_com_example_as_1jni : 包名,如果包名中已经包含了下划线,那么会用1代替下划线
               2. MainActivity : 类名
               3. getStringJNI : 方法名
  六、JNIEnv :桥梁: 其中有300多个函数 所有的JNI操作都靠它
  七、jobject / jclass: jobject这个JNI方法被谁调用,就是谁的实例 MainActivity / jclass 谁调用就是谁的实例 MainActivity.class
 *
 */
extern "C"
JNIEXPORT
jstring
JNICALL
Java_com_example_as_1jni_MainActivity_getStringJNI
        (JNIEnv *env, jobject thiz){

}


/**
 * 这个方法,用来实现更改MainActivity中的name值
 */
extern "C"
JNIEXPORT
void
JNICALL
Java_com_example_as_1jni_MainActivity_changeName(JNIEnv *env, jobject thiz) {
    // 原理: 利用反射
    //1. 获取类 通过jobject 获取到jclass
   jclass _jclass =  env->GetObjectClass(thiz);
    // 2. 获取属性
    /**
     * 源码:
     *  jfieldID GetFieldID(jclass clazz, const char* name, const char* sig)
     *  1. jclass clazz : class
     *  2. const char* name : 属性名
     *  3. const char* sig : 属性的签名 Ljava/lang/String; L : 引用类型(对象类型都需要L) + 包名 + ;
     */
    jfieldID _jfieldID = env->GetFieldID(_jclass,"name","Ljava/lang/String;");

    // 3. 转换工作: 把属性转换成jstring
    /**
     * 源码:jobject GetObjectField(jobject obj, jfieldID fieldID)
     * 参数需要传入 :jobject 和 jfieldID
     * static_cast<jstring> : 静态转换
     */
    jstring _j_str = static_cast<jstring>(env->GetObjectField(thiz,_jfieldID));

    //4. jstring 转换成 char *
    char * c_str = const_cast<char *>(env->GetStringUTFChars(_j_str,NULL));

    // 打印 使用上面定义的宏函数
    LOGI("native: %s\n" , c_str);
    LOGD("native: %s\n" , c_str);
    LOGW("native: %s\n" , c_str);
    LOGE("native: %s\n" , c_str);

    // 修改Name
   jstring jName = env->NewStringUTF("Beyond");
   env->SetObjectField(thiz,_jfieldID,jName);
}

其中生成的JNI方法:

extern "C"
JNIEXPORT
void
JNICALL
Java_com_example_as_1jni_MainActivity_changeName(JNIEnv *env, jobject thiz) {
    // TODO 
}

对该方法其中每一项的具体解释:

1. extern "C"

必须加上这句话,否则报错,这里的意思就是 : 必须采用C的编译方式。 为什么必须采用C的编译方式呢?在JNIEnv的源码中可以找到答案

JNIEnv内部源码  :
 *     #if defined(__cplusplus)  // 如果是C++的方式 使用_JNIEnv
         typedef _JNIEnv JNIEnv; // 取别名
         typedef _JavaVM JavaVM;
       #else  // 否则 使用 JNINativeInterface
         typedef const struct JNINativeInterface* JNIEnv;  // 取别名
         typedef const struct JNIInvokeInterface* JavaVM;
      #endif


       JNINativeInterface :在jni.h中  是一个结构体   JNI就是学习其中的所有函数

       _JNIEnv : 也是一个结构体  源码:

       struct _JNIEnv {
            const struct JNINativeInterface* functions;  这里还是调用到了 JNINativeInterface
            .....
          }

       无论是 C 还是 C++ 最终都会调用到 JNINativeInterface  所以必须采用C的编译方式
2. JNIEXPORT

标记该方法,可以被外部调用(在VS上不加运行会报错)

3. jstring

相当于Java中的String 用来翻译Java中的String 给 C C++ 看,C C++没有String,只有char *,

也可以翻译C C++中的char* 给Java 看

4. JNICALL

代表是JNI的标记 可省略

5. Java_com_example_as_1jni_MainActivity_getStringJNI

函数名称:

  1. Java_com_example_as_1jni : 包名,如果包名中已经包含了下划线,那么会用1代替下划线
  2. MainActivity : 类名
  3. getStringJNI : 方法名
6. JNIEnv

桥梁: 其中有300多个函数 所有的JNI操作都靠它

7. jobject / jclass

jobject这个JNI方法被谁调用,就是谁的实例 MainActivity / jclass 谁调用就是谁的实例 MainActivity.class

上面的代码中,提到了jstring,jstring是JNI中的引用数据类型,下面就来具体列举一下JNI中的数据类型。

3.4 静态方法和静态属性在JNI中的运用

(1)在Android代码中编写静态属性和静态native方法

package com.example.as_jni; // 包名,跟在javah 后面 javah com.example.as_jni.MainActivity

import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import com.example.as_jni.databinding.ActivityMainBinding;

public class MainActivity extends AppCompatActivity {

    // Used to load the 'native-lib' library on application startup.
    static {
//        System.load(""); // 传入绝对路径
        System.loadLibrary("native-lib");
    }

    private ActivityMainBinding binding;

    public static int age = 10; // 静态属性

    public static native void changeAge(); // 静态的JNI方法

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());
        changeAge();
    }
}

(2)在com_example_as_jni_MainActivity.h头文件中声明函数

#include <jni.h>

#ifndef _Included_com_example_as_jni_MainActivity  // 如果没有定义这个宏
#define _Included_com_example_as_jni_MainActivity  // 我就定义这个宏
#ifdef __cplusplus // 如果是C++的环境
extern "C" {  // 全部采用C的标准 (禁止函数重载
#endif
#endif


extern "C"
JNIEXPORT void JNICALL
Java_com_example_as_1jni_MainActivity_changeAge(JNIEnv *, jclass);

}

(3)在native-lib.cpp中实现函数

#include "com_example_as_jni_MainActivity.h"

#include <android/log.h>  // 引入打印库
// 定义打印时的TAG
#define TAG "KimLiu" // 定义宏
// 定义打印的宏函数
/**
 * __android_log_print 这个方法在 android/log.h 中
 * ... 不知道传入什么,借助JNI中的宏_VA_ARGS__来帮助填充
 */
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,TAG,__VA_ARGS__);
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,TAG,__VA_ARGS__);
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,TAG,__VA_ARGS__);
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN,TAG,__VA_ARGS__);

// 修改Android代码中的age值
extern "C"
JNIEXPORT void JNICALL
Java_com_example_as_1jni_MainActivity_changeAge(JNIEnv *env, jclass clazz) {
    // 1. 获取静态属性
    /**
     *  源码: jfieldID GetStaticFieldID(jclass clazz, const char* name, const char* sig)
     *       1. jclass clazz
     *       2. const char* name 属性名
     *       3. const char* sig 属性签名
     */


    jfieldID _jfieldId = env->GetStaticFieldID(clazz,"age","I");
    // 获取属性值
    // 源码:  jint GetStaticIntField(jclass clazz, jfieldID fieldID)
    jint  age = env->GetStaticIntField(clazz,_jfieldId);
    // 修改属性值
    age += 10;
    // 设置属性值
    env->SetStaticIntField(clazz,_jfieldId,age);

    LOGI("native: %d",age);

}

3.6 在JNI中调用Anrdroid中的方法

native层调用Android层的add方法:

  1. 编写Android代码:
package com.example.as_jni; // 包名,跟在javah 后面 javah com.example.as_jni.MainActivity

import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;

import com.example.as_jni.databinding.ActivityMainBinding;

public class MainActivity extends AppCompatActivity {

    // Used to load the 'native-lib' library on application startup.
    static {
//        System.load(""); // 传入绝对路径
        System.loadLibrary("native-lib");
    }

    private ActivityMainBinding binding;


    public native void callAddMethod();


    public int add(int n1,int n2){
        return n1+n2;
    }


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());
        callAddMethod();

    }
}
  1. 编写native代码
#include "com_example_as_jni_MainActivity.h"

#include <android/log.h>  // 引入打印库
// 定义打印时的TAG
#define TAG "KimLiu" // 定义宏
// 定义打印的宏函数
/**
 * __android_log_print 这个方法在 android/log.h 中
 * ... 不知道传入什么,借助JNI中的宏_VA_ARGS__来帮助填充
 */
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,TAG,__VA_ARGS__);
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,TAG,__VA_ARGS__);
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,TAG,__VA_ARGS__);
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN,TAG,__VA_ARGS__);

extern "C"
JNIEXPORT void JNICALL
Java_com_example_as_1jni_MainActivity_callAddMethod(JNIEnv *env, jobject thiz) {
    // TODO: implement callAddMethod()
    // 1. 获取jclass
    // 源码: jclass GetObjectClass(jobject obj)
    jclass _jclass = env->GetObjectClass(thiz);
    // 2. 获得MethodId
    /**
     * 源码: jmethodID GetMethodID(jclass clazz, const char* name, const char* sig)
     *       1. jclass clazz
     *       2. const char* name 方法名
     *       3. const char* sig  方法签名 (参数类型参数类型...)返回值类型
     */
    jmethodID _jmethodID = env->GetMethodID(_jclass,"add","(II)I");

    // 调用Java方法
    jint sum = env->CallIntMethod(thiz,_jmethodID,3,3);
    LOGE("native:%d",sum);
}

3.7 JNI数组的操作

Android层的代码:

package com.example.as_jni; // 包名,跟在javah 后面 javah com.example.as_jni.MainActivity

import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;

import com.example.as_jni.databinding.ActivityMainBinding;

public class MainActivity extends AppCompatActivity {

    // Used to load the 'native-lib' library on application startup.
    static {
//        System.load(""); // 传入绝对路径
        System.loadLibrary("native-lib");//从库目录遍历层级目录,去自动的寻找   apk里面的lib/libnative-lib.so
    }

    private ActivityMainBinding binding;


    int[] ints = new int[]{1,2,3,4,5,6};

    String[] strs = new String[]{"AAA","BBB","CCC"};

    // 编写Native方法 操作数组
    public native void testArrayAction(int count, String textInfo, int[] ints, String[] strs);

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());
        testArrayAction(99,"你好",ints,strs);
        for(int i = 0; i < ints.length ; ++i){
            Log.i("KimLiu", "onCreate: "+ints[i]);
        }
    }
}
  1. Native层代码
#include "com_example_as_jni_MainActivity.h"

#include <android/log.h>  // 引入打印库
// 定义打印时的TAG
#define TAG "KimLiu" // 定义宏
// 定义打印的宏函数
/**
 * __android_log_print 这个方法在 android/log.h 中
 * ... 不知道传入什么,借助JNI中的宏_VA_ARGS__来帮助填充
 */
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,TAG,__VA_ARGS__);
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,TAG,__VA_ARGS__);
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,TAG,__VA_ARGS__);
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN,TAG,__VA_ARGS__);


/**
 * 几种JNI中的数据类型
 * jint 相当于 Java中的 int
 * jstring            String
 * jintArray          int[]
 * jobjectArray       引用类型对象 如 String[] Test[] Student[] Person[]
 */
extern "C"
JNIEXPORT void JNICALL
Java_com_example_as_1jni_MainActivity_testArrayAction(JNIEnv *env, jobject thiz, jint count,
                                                      jstring text_info, jintArray ints,
                                                      jobjectArray strs) {
    // 1,基本数据类型的使用 参数中的 jint count
    /**
     * 通过源码查看jint的本质:
     * typedef int __int32_t; // __int32_t 是 int类型的别名
     * typedef __int32_t     int32_t; // int32_t 是 __int32_t类型的别名
     * typedef int32_t  jint;  // jint是int32_t类型的别名
     *
     * 因此 jint的本质就是int  所以可以使用int 直接接收
     */

    int countInt = count;
    // 打印
    LOGI("native参数一 countInt : %d\n",countInt);

    // 2. jstring类型
    // 源码: const char* GetStringUTFChars(jstring string, jboolean* isCopy)
    const char* textinfo  = env->GetStringUTFChars(text_info,NULL);
    LOGI("native参数二 textinfo : %s\n",textinfo);

    // 3. jintArray
    // 3.1 把int[] 转换成 int*
    // 源码: jint* GetIntArrayElements(jintArray array, jboolean* isCopy)
    int * jintArray = env->GetIntArrayElements(ints,NULL);

    /**
     * 源码: jsize GetArrayLength(jarray array)
     * 查看源码,探究jsize是什么:
     * typedef jint     jsize; // jsize是jint的别名
     * jint 就是 int  ,jsize也是int类型
     *
     */

    jsize size = env->GetArrayLength(ints);

    for(int i = 0 ; i < size ; ++i){
        *(jintArray+i) += 100; // C++层的修改 影响不了Java层 需要env调用ReleaseIntArrayElements方法通知Java层刷新数组
        LOGI("native参数三:%d\n",*jintArray + i);
    }

    // C++层数据更改之后,需要通知Java层刷新
    /**
     * 源码:
     * void ReleaseIntArrayElements(jintArray array, jint* elems,jint mode)
     * jint mode 有三种情况: 0 刷新Java数组 并释放C++层数组
     *                     JNI_COMMIT 只提交  只刷新Java数组,不释放C++层数组
     *                     JNI_ABORT  只释放C++层数组
     */
    env->ReleaseIntArrayElements(ints,jintArray,0);

    // 4. jobjectArray Java的引用类型数组
    jsize strSize = env->GetArrayLength(strs);
    for (int i = 0; i < strSize; ++i) {
        jstring j_str = static_cast<jstring>(env->GetObjectArrayElement(strs,i));
        // 源码:  const char* GetStringUTFChars(jstring string, jboolean* isCopy)
        const char* jobp =  env->GetStringUTFChars(j_str,NULL);
        LOGI("native参数四:jobp: %s\n ",jobp);

        // 释放jstring
        env->ReleaseStringUTFChars(j_str,jobp);
    }
}

这里需要注意的是,在C++层操作数组时,需要使用JNIEnv调用ReleaseIntArrayElements刷新Java层的数组,更新才会在Java层生效。

四、一些定义和补充

4.1 Native动态注册

在上面第三节的3.1-3.3小节中,使用javah生成com_example_as_jni_MainActivity.h头文件的过程,就是静态注册的过程。
使用静态注册时,当我们在Java中调用changeName方法时,就会从JNI中寻招Java_com_example_as_1jni_MainActivity_changeName函数,如果没有就报错,如果找到就会为changeName和Java_com_example_as_1jni_MainActivity_changeName建立关联,其实是保存JNI的函数指针,这样再次调用changeName方法时直接使用这个函数指针就可以了。静态注册就是根据方法名,讲Java方法和JNI函数建立关联,但是它有一些缺点:

  1. JNI层的函数名称过长
  2. 声明Natvie方法的类需要用javah生成头文件
  3. 初次调用Native方法时需要建立关联,影响效率

我们知道,静态注册就是Java的Native方法通过方法指针来与JNI进行关联。如果Java的Native方法知道它在JNI中对应的函数指针,就可以避免上述缺点,这就是动态注册。

先看一个动态注册的例子。 Android层的代码:

public class MainActivity extends AppCompatActivity {

    // Used to load the 'native-lib' library on application startup.
    static {
        System.loadLibrary("native-lib");
    }


    // 动态注册的方法,不需要在头文件中声明
    public native void dynamicJavaMethod();


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 动态注册
        dynamicJavaMethod();
    }
}

Native层代码:

#include <jni.h>
#include <string>

// 日志输出
#include <android/log.h>

#define TAG "KimLiu"
// __VA_ARGS__ 代表 ...的可变参数
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG,  __VA_ARGS__);
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG,  __VA_ARGS__);
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG,  __VA_ARGS__);


// 动态注册
// 1. 首先定义对应的native函数
void dynamicNativeMethod(){
    LOGI("动态注册函数1")


}

// 这里要使用传过来的string 所以要加上 参数
void dynamicNativeMethod2(JNIEnv *env, jobject thiz, jstring valueStr){
    const char * str = env->GetStringUTFChars(valueStr, nullptr);
    LOGI("动态注册函数2,%s",str);
    env->ReleaseStringUTFChars(valueStr, nullptr);
}

/**
 * 2. 使用JNINativeMethod 记录Java方法和JNI方法的关联关系
 *
 * typedef struct {
    const char* name;  // Java方法名
    const char* signature; // Java方法签名信息
    void*       fnPtr; // JNI中对应的方法指针
     } JNINativeMethod;

     JNINativeMethod 是一个结构体
 *
 *
 */


static const JNINativeMethod jniNativeMethod[] = {
        // 这其中是一个个方法的对应关系
        {"dynamicJavaMethod","()V",(void *)(dynamicNativeMethod)},
        {"dynamicJavaMethod2","(Ljava/lang/String;)V",(void *)(dynamicNativeMethod2)}
};


JavaVM * jvm = nullptr; // nullptr = 0x003545   如果不赋初始值 会有个系统乱值 造成很多莫名其妙的问题
const char * mainactivityClassPath = "com/kimliu/myapplication/MainActivity";

// 3,使用JNI_OnLoad函数注册
extern "C"
JNIEXPORT jint JNI_OnLoad(JavaVM * javaVm,void *){
    jvm = javaVm;

    JNIEnv * jniEnv = nullptr;
    int result = javaVm->GetEnv(reinterpret_cast<void **>(&jniEnv),JNI_VERSION_1_6);

    // result == 0 是成功
    if(result != JNI_OK){
        return -1; // 不成功就崩溃
    }

    jclass _jclass = jniEnv->FindClass(mainactivityClassPath);

    // 动态注册  参数一 : class 参数二 : 数组 jniNativeMethod 参数三: 注册的数量 sizeof(jniNativeMethod) / sizeof(JNINativeMethod) = 2
    jniEnv->RegisterNatives(_jclass,jniNativeMethod,2);

    LOGE("动态注册完成");

    return JNI_VERSION_1_6;
}

动态注册的三步:

  1. 定义对应的Native方法
  2. 使用JNINativeMethod来记录Java中native方法和JNI方法的关联关系。 JNINativeMethod是一个结构体,在jni.h中被定义:
typedef struct {
    const char* name;  // Java方法名
    const char* signature; // Java方法签名信息
    void*       fnPtr; // JNI中对应的方法指针
     } JNINativeMethod;
  1. 在JNI_OnLoad中调用JNIEnv的RegisterNatives函数来完成JNI的注册。 JNI_OnLoad函数会在调用System.loadLibrary函数后调用,所以一般注册函数会被统一的定义在JNI_OnLoad函数中。

4.2 一些定义

4.2.1 JNI和Java的数据类型转换

4.2.1.1 基本数据类型转换
Java类型JNI类型方法签名描述
booleanjbooleanZunsigned 8 bits
bytejbyteBsigned 8 bits
charjcharCunsigned 16 bits
shortjshortSsigned 16 bits
intjintIsigned 32 bits
longjlongJsigned 64 bits
floatjfloatF32 bits
doublejdoubleD64 bits
voidvoidVvoid

为了方便使用,JNI中定义了两个宏来表示Boolean的值

#define JNI_FALSE 0
#define JNI_TRUE 1
4.2.1.2 引用类型转换
Java类型JNI类型方法签名
所有对象jobjectLclassName;
ClassjclassLjava/lang/Class;
StringjstringLjava/lang/String;
ThrowablejthrowableLjava/lang/Throwable;
Object[]jobjectArray[LclassName;
byte[]jbyteArray[B
char[]jcharArray[C
double[]jdoubleArray[D
float[]jfloatArray[F
int[]jintArray[I
short[]jshortArray[S
long[]jlongArray[J
boolean[]jbooleanArray[Z
double[][][]三维数组[[[D

引用数据的继承关系如下:

  • jobject
  •  jclass (java.lang.Class objects)
     jstring (java.lang.String objects)
     jarray (arrays)
         jobjectArray (object arrays)
         jbooleanArray (boolean arrays)
         jbyteArray (byte arrays)
         jcharArray (char arrays)
         jshortArray (short arrays)
         jintArray (int arrays)
         jlongArray (long arrays)
         jfloatArray (float arrays)
         jdoubleArray (double arrays)
    
  •   jthrowable (java.lang.Throwable objects)
    

如果要查看class文件中的所有属性和方法签名,可以使用命令javap -s -p xxx.class,其中-s 输出xxxx.class的所有属性和方法的签名, -p 忽略私有公开的所有属性方法全部输出。这些内容在前面的JVM系列中已经讲到过,可参考# LCODER之JVM系列:Class文件结构

4.2.2 方法签名

我们在上面的demo中,查找方法的时,经常会用到方法签名,所以对方法签名应该也不陌生。但是为什么要使用方法签名呢?这是因为Java是有重载方法的,可以定义方法名相同,参数不同的方法,所以在JNI中仅仅通过方法名是无法找到Java中的具体方法的,JNI为了解决这一问题,就将参数类型和返回值类型组合在一起作为方法签名,通过方法签名和方法名就可以找到对应的Java方法。

JNI方法签名的格式:

(参数签名格式...)返回值签名格式

4.2.3 JNIEnv

JNIEnv是Native世界中Java环境的代表,通过JNIEnv * 指针就可以在Native世界中访问Java世界的代码进行操作,它只在创建它的线程中有效,不能跨线程传递,因此不同线程的JNIEnv是彼此独立的,JNIEnv的主要作用有两点:

  1. 调用Java方法
  2. 操作Java(获取Java中的变量和对象等等)。

JNIEnv也是一个结构体,在jni.h中定义

#if defined(__cplusplus)
// C++中 JNIEnv 的类型 _JNIEnv
typedef _JNIEnv JNIEnv;
typedef _JavaVM JavaVM;
#else
// C中 JNIEnv 的类型 JNINativeInterface*
typedef const struct JNINativeInterface* JNIEnv;
typedef const struct JNIInvokeInterface* JavaVM;
#endif

这里使用预定义宏__cplusplus来区分C和C++两种代码,如果定义了__cplusplus,则是C++代码中的定义,否则就是C代码中的定义。
在这里我们也看到了JavaVM,它是虚拟机在JNI层的代表,在一个虚拟机进程中只有一个JavaVM,因此,该进程的所有线程都可以使用这个JavaVM。通过JavaVM的AttachCurrentThread函数可以获取这个线程的JNIEnv,这样就可以在不同的线程中调用Java方法了。还要记得在使用AttachCurrentThread函数的线程退出前,务必要调用DetachCurrentThread函数来释放资源。

首先来看C++中JNIEnv的类型_JNIEnv

struct _JNIEnv {
    /* do not rename this; it does not seem to be entirely opaque */
    const struct JNINativeInterface* functions;

#if defined(__cplusplus)

    jclass FindClass(const char* name)
    { return functions->FindClass(this, name); }
  
    jmethodID GetMethodID(jclass clazz, const char* name, const char* sig)
    { return functions->GetMethodID(this, clazz, name, sig); }
...
}

_JNIEnv是一个结构体,其内部包含了JNINativeInterface。在_JNIEnv中定义了很多函数,这些方法在上面的demo中已经很熟悉了。 下面来看看结构体JNINativeInterface

struct JNINativeInterface {
    ...
    
    jclass      (*FindClass)(JNIEnv*, const char*);

    jmethodID   (*GetMethodID)(JNIEnv*, jclass, const char*, const char*);
   
...
}

在JNINativeInterface结构体中,定义了很多和JNIEnv结构体对应的函数指针,这里只给出了上面JNIEnv结构体中对应的三个函数指针定义。通过这些函数指针定义,就能够定位到虚拟机中的JNI函数表,从而实现了JNI层在虚拟机中的函数调用,这样JNI层就可以调用Java世界的方法了。

4.2.3.1 jfieldID和jmethodID

在JNI中用jfieldID和jmethodID来代表Java类中的成员变量和方法,可以通过JNIEnv的下面两个方法来分别得到:

jfieldID  GetFieldID(jclass clazz,const char *name,const char *sig);
jmethodID  GetFieldID(jclass clazz,const char *name,const char *sig);

五、C++层使用Java层的对象

5.1 JNI对象操作

5.1.1 创建对象

package com.example.as_jni;

import android.util.Log;

public class Student {

    private final static String TAG = Student.class.getSimpleName();

    public String name;
    public int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        Log.d(TAG, "Java setName name:" + name);
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        Log.d(TAG, "Java setAge age:" + age);
        this.age = age;
    }

    public static void showInfo(String info) {
        Log.d(TAG, "showInfo info:" + info);
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + ''' +
                ", age=" + age +
                '}';
    }
}

5.1.2 在Android层使用对象

package com.example.as_jni; // 包名,跟在javah 后面 javah com.example.as_jni.MainActivity

import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;

import com.example.as_jni.databinding.ActivityMainBinding;

public class MainActivity extends AppCompatActivity {

    // Used to load the 'native-lib' library on application startup.
    static {
//        System.load(""); // 传入绝对路径
        System.loadLibrary("native-lib");//从库目录遍历层级目录,去自动的寻找   apk里面的lib/libnative-lib.so
    }

    private ActivityMainBinding binding;

    public native void putObject(Student student,String name);

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());
        // 创建对象
        Student student = new Student();
        student.setAge(190);
        student.setName("KimLiu");
        putObject(student,"郭靖");



    }
}

5.1.3 在native层使用对象,调用对象中的方法

#include "com_example_as_jni_MainActivity.h"

#include <android/log.h>  // 引入打印库
// 定义打印时的TAG
#define TAG "KimLiu" // 定义宏
// 定义打印的宏函数
/**
 * __android_log_print 这个方法在 android/log.h 中
 * ... 不知道传入什么,借助JNI中的宏_VA_ARGS__来帮助填充
 */
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,TAG,__VA_ARGS__);
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,TAG,__VA_ARGS__);
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,TAG,__VA_ARGS__);
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN,TAG,__VA_ARGS__);

extern "C"
JNIEXPORT void JNICALL
Java_com_example_as_1jni_MainActivity_putObject(JNIEnv *env, jobject thiz, jobject student,
                                                jstring name) {
    // 查看name
    // 源码:  const char* GetStringUTFChars(jstring string, jboolean* isCopy)
    const char* str =  env->GetStringUTFChars(name,NULL);
    LOGI("native 传入的name: %s\n",str);
    // 释放str
    env->ReleaseStringUTFChars(name,str);

    // 调用类中的方法
    // ====  1. 寻找类 Student
    jclass studentClass = env->FindClass("com/example/as_jni/Student");  // 第一种方式
    // 第二种方法
    jclass studentClass2 = env->GetObjectClass(student);

    // ==== 2. Student类中的方法
    jmethodID setName = env->GetMethodID(studentClass2,"setName","(Ljava/lang/String;)V");
    jmethodID getName = env->GetMethodID(studentClass2,"getName", "()Ljava/lang/String;");
    jmethodID showInfo = env->GetStaticMethodID(studentClass2,"showInfo","(Ljava/lang/String;)V");

    // ==== 3. 调用方法
    // 源码: void CallVoidMethod(jobject obj, jmethodID methodID, ...)
    env->CallVoidMethod(student,setName,name);
    jstring getNameResult =static_cast<jstring>(env->CallObjectMethod(student,getName));
    // 把jstring 转换成 const char *
    const char * nameStr = env->GetStringUTFChars(getNameResult,NULL);
    LOGI("native 获取Name : %s\n",nameStr);

    // 调用静态方法 showInfo
    jstring jstringValue = env->NewStringUTF("native");
    env->CallStaticVoidMethod(studentClass2,showInfo,jstringValue);
}

在native层调用对象中的方法,主要就是使用env中的几个方法:

  1. jclass GetObjectClass(jobject obj) 获取类
  2. jmethodID GetMethodID(jclass clazz, const char* name, const char* sig) 获取方法ID
  3. jmethodID GetStaticMethodID(jclass clazz, const char* name, const char* sig) 获取静态方法ID
  4. void CallVoidMethod(jobject obj, jmethodID methodID, ...) 返回值为Void的方法的调用
  5. jobject CallObjectMethod(jobject obj, jmethodID methodID, ...) 有返回值的方法的调用
  6. void CallStaticVoidMethod(jclass clazz, jmethodID methodID, ...) 调用静态方法

5.2 JNI创建对象

创建Student对象,Native层代码

#include "com_example_as_jni_MainActivity.h"

#include <android/log.h>  // 引入打印库
// 定义打印时的TAG
#define TAG "KimLiu" // 定义宏
// 定义打印的宏函数
/**
 * __android_log_print 这个方法在 android/log.h 中
 * ... 不知道传入什么,借助JNI中的宏_VA_ARGS__来帮助填充
 */
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,TAG,__VA_ARGS__);
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,TAG,__VA_ARGS__);
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,TAG,__VA_ARGS__);
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN,TAG,__VA_ARGS__);

extern "C"
JNIEXPORT void JNICALL
Java_com_example_as_1jni_MainActivity_insertObject(JNIEnv *env, jobject thiz) {
    // 1.通过包名 + 类名 拿到Student的class
    const char * studentStr = "com/example/as_jni/Student";
    jclass studentClass = env->FindClass(studentStr);

    // 2.通过Student的class 实例化Student对象
    jobject studentObj = env->AllocObject(studentClass);// AllocObject 只实例化对象,不调用其构造方法


    // 3. 获取MethodID
    jmethodID setName  =  env->GetMethodID(studentClass,"setName", "(Ljava/lang/String;)V");
    jmethodID setAge =  env->GetMethodID(studentClass,"setAge", "(I)V");

    // 4. 调用方法
    jstring j_name  =  env->NewStringUTF("KimLiu");
    env->CallVoidMethod(studentObj,setName,j_name);
    env->CallVoidMethod(studentObj,setAge,10);

    // 5. 释放资源
    // 对象类
    env->DeleteLocalRef(studentClass);
    env->DeleteLocalRef(studentObj);

//    // string类
//    env->ReleaseStringUTFChars();
}

这里使用了jobject AllocObject(jclass clazz) 方法创建对象,这是第一种创建对象的方式。还有一种创建对象的方式,是使用jobject NewObject(jclass clazz, jmethodID methodID, ...) 创建对象

  1. jobject AllocObject(jclass clazz) 创建对象,不调用其构造方法。
  2. jobject NewObject(jclass clazz, jmethodID methodID, ...) 创建对象,调用其构造方法。

创建一个Person对象,在Person对象中传入Student对象。

(1) 创建Person对象
package com.example.as_jni;

import android.util.Log;

public class Person {

    private static final String TAG = Person.class.getSimpleName();

    public Student student;

    public void setStudent(Student student) {
        Log.d(TAG, "call setStudent student:" + student.toString());
        this.student = student;
    }

    public static void putStudent(Student student) {
        Log.d(TAG, "call static putStudent student:" + student.toString());
    }
}
(2) Native层代码
#include "com_example_as_jni_MainActivity.h"

#include <android/log.h>  // 引入打印库
// 定义打印时的TAG
#define TAG "KimLiu" // 定义宏
// 定义打印的宏函数
/**
 * __android_log_print 这个方法在 android/log.h 中
 * ... 不知道传入什么,借助JNI中的宏_VA_ARGS__来帮助填充
 */
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,TAG,__VA_ARGS__);
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,TAG,__VA_ARGS__);
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,TAG,__VA_ARGS__);
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN,TAG,__VA_ARGS__);

extern "C"
JNIEXPORT void JNICALL
Java_com_example_as_1jni_MainActivity_insertObject(JNIEnv *env, jobject thiz) {
    // 创建Person
    // 1,通过包名+类名 拿到Person的Class
    const char * personStr = "com/example/as_jni/Person";
    jclass personClass = env->FindClass(personStr);
    jobject personObj = env->AllocObject(personClass);

    jmethodID setStudent =
            env->GetMethodID(personClass,"setStudent", "(Lcom/example/as_jni/Student;)V");
    
    // 创建Student对象
    const char * studentCstr = "com/example/as_jni/Student";
    jclass studentClass = env->FindClass(studentCstr);
    jobject studentObj = env->AllocObject(studentClass);

    // 调用setStudent方法
    env->CallVoidMethod(personObj,setStudent,studentObj);
    
}

5.3 JNI全局引用与局部引用

和Java引用类型一样,JNI也有引用类型,它们分别是本地引用(Local References)、全局引用(Global References)和弱全局引用(Weak Global References)。

5.3.1 本地引用

JNIEnv提供的函数所返回的引用基本上都是本地引用,因此本地引用是JNI中最常见的引用类型,本地引用的特点主要有以下几点:

  1. 当Native函数返回时,这个本地引用就会被自动释放。
  2. 只在创建它的线程中有效,不能够跨线程使用。
  3. 局部引用是JVM负责的引用类型,受JVM管理。

5.3.2 全局引用

全局引用和局部引用几乎是相反的,它主要有以下特点:

  1. 在Native函数返回时不会被自动释放,因此全局引用需要手动来进行释放,并且不后悔被GC回收。
  2. 全局引用可以跨线程使用。
  3. 全局引用不受到JVM管理。

JNIEnv的NewGlobalRef函数用来创建全局引用,DeleteGlobalRef函数来释放全局引用。

5.3.3 弱全局引用

弱全局引用是一种特殊的全局引用,它和全局引用的特点相似,不同的是弱全局引用是可以被GC回收的,弱全局引用被GC回收之后会指向NULL。JNIEnv的NewWeakGlobalRef函数用来创建弱全局引用,调用JNIEnv的DeleteWeakGlobalRef函数来释放弱全局引用。由于弱全局引用可能被GC回收,因此在使用它之前,需要先判断它是都被回收了,方法就是使用JNIEnv的IsSameObject函数来判断:

    /**
     * IsSameObject(jobj1,jobj2)
     * 判断传入的两个引用是否相等,如果相等返回JNI_TRUE,如果不相等返回JNI_FALSE 
     */
jniEnv->IsSameObject(weakGlobalRef,NULL);

5.3.4 JNI中的引用实战

5.3.4.1 定义一个对象
package com.example.as_jni;


import android.util.Log;

public class Dog { // NewObject 调用我们的构造函数

    public Dog() { // <init>
        Log.d("Dog", "Dog init...");
    }

    public Dog(int n1) { // <init>
        Log.d("Dog", "Dog init... n1:" + n1);
    }

    public Dog(int n1, int n2) { // <init>
        Log.d("Dog", "Dog init... n1:" + n1 + " n2:" + n2);
    }

    public Dog(int n1, int n2, int n3) { // <init>
        Log.d("Dog", "Dog init... n1:" + n1 + " n2:" + n2 + " n3:" + n3);
    }

}
5.3.4.2 native层代码
#include "com_example_as_jni_MainActivity.h"

#include <android/log.h>  // 引入打印库
// 定义打印时的TAG
#define TAG "KimLiu" // 定义宏
// 定义打印的宏函数
/**
 * __android_log_print 这个方法在 android/log.h 中
 * ... 不知道传入什么,借助JNI中的宏_VA_ARGS__来帮助填充
 */
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,TAG,__VA_ARGS__);
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,TAG,__VA_ARGS__);
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,TAG,__VA_ARGS__);
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN,TAG,__VA_ARGS__);


// 定义一个DOG
jclass dogClass ;

extern "C"
JNIEXPORT void JNICALL
Java_com_example_as_1jni_MainActivity_testRefrence(JNIEnv *env, jobject thiz) {
    if(NULL == dogClass){
        const char * dogStr = "com/example/as_jni/Dog";
        jclass tempClass = env->FindClass(dogStr);
        // 升级成全局引用  全局因为在JNI方法结束后不会自动释放,需要手动释放 所以可以再写一个释放全局引用的方法
        dogClass = static_cast<jclass>(env->NewGlobalRef(tempClass));
        // 释放tempClass
        env->DeleteGlobalRef(tempClass);
    }

    // 调用构造函数  方法名 "<init>" 和 返回值 V 是固定的
    // 构造函数一
    jmethodID init = env->GetMethodID(dogClass,"<init>","()V");
    jobject dog = env->NewObject(dogClass,init); // 使用NewObject创建Dog对象

    // 构造函数二
    init = env->GetMethodID(dogClass,"<init>","(I)V");
    dog = env->NewObject(dogClass,init,100);

    // 构造函数三
    init = env->GetMethodID(dogClass,"<init>","(II)V");
    dog = env->NewObject(dogClass,init,100,200);

    // 构造函数四
    init = env->GetMethodID(dogClass,"<init>","(III)V");
    dog = env->NewObject(dogClass,init,100,200,300);


}

// 释放全局引用
extern "C"
JNIEXPORT void JNICALL
Java_com_example_as_1jni_MainActivity_deleteRefrence(JNIEnv *env, jobject thiz) {
    if(dogClass != NULL){
        LOGI("native 全局引用释放完毕");
        env->DeleteGlobalRef(dogClass);
        dogClass = NULL; // 避免悬空指针
    }
}

六、JNI基础实战

使用fmod库更改声音

6.1 环境搭建

这里的环境搭建,不仅只适用于这个fmod库,同样也适用于集成别的so库。

6.1.1 拷贝fmod库的头文件:

1634211263(1).png

6.1.2 拷贝fmod库的so库:

1634211341(1).png

6.1.3 拷贝fmod库的jar包:

1634214213(1).png

6.1.4 导入fmod库的文件

这里要使用到CMakeList.txt,在其中编写导入的代码

cmake_minimum_required(VERSION 3.10.2) # 最低支持的cmake版本

project("fmodproject")

# 第一步 导入头文件
include_directories("inc")

# 批量导入所有源文件
file(allCPP *.c *.h *.cpp)

add_library(native-lib  # libnative-lib.so
             SHARED # 动态库  STATIC 静态库
             ${allCPP} )

# 第二步 导入库文件
# CMAKE_SOURCE_DIR CMakeLists.txt 所在的路径
# CMAKE_ANDROID_ARCH_ABI 当前的CPU架构
# ${CMAKE_CXX_FLAGS} -L${CMAKE_SOURCE_DIR}/../jniLibs/${CMAKE_ANDROID_ARCH_ABI} 库文件的路径
# 
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -L${CMAKE_SOURCE_DIR}/../jniLibs/${CMAKE_ANDROID_ARCH_ABI}")

# 第三步 导入了库 还需要链接具体的库到libnative-lib.so总库
target_link_libraries(native-lib
                      log  # 打印的库  自动寻找 
                      fmod  # fmod库
                      fmodL # fmodL库
        )

6.1.5 在gradle中指定CPU架构

android {
      
      ...
    
        externalNativeBuild {
            cmake {
                // cppFlags "" // 默认四大平台  armeabi-v7a arm64-v8a armeabi x86

                // ======= 第四步 指定CPU架构 CMake的本地库  这里指定了 还需要在第五步指定 打入APK的CPU平台
                abiFilters "arm64-v8a" //指定架构 arm64-v8a 这样编译的时候就只会编译arm64-v8a的so库

            }
        }

        //  ======= 第五步 指定CPU架构 打入APK的CPU平台
        ndk{
            abiFilters "arm64-v8a"
        }
    }

    
    externalNativeBuild {
        cmake {
            path "src/main/cpp/CMakeLists.txt"
            version "3.10.2"
        }
    }
    
    ...
   
}

6.1.6 在Android代码中初始化Fmod库

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    // 第七步 : 初始化 FMOD 库
    FMOD.init(this);

}


@Override
protected void onDestroy() {
    super.onDestroy();
    // 使用完成后 释放资源
    FMOD.close();
}

6.2 使用FMOD库进行改变声音

6.2.1 Android层的代码

比较简单,就不再赘述了。直接上代码

public class MainActivity extends AppCompatActivity {

    private static final int MODE_NORMAL = 0; // 正常
    private static final int MODE_LUOLI = 1; // 萝莉
    private static final int MODE_DASHU = 2; // 大叔
    private static final int MODE_JINGSONG = 3; // 惊悚
    private static final int MODE_GAOGUAI = 4; // 搞怪
    private static final int MODE_KONGLING = 5; // 空灵

    // Used to load the 'native-lib' library on application startup.
    static {
        System.loadLibrary("native-lib");
    }

    private String path;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        path = "file:///android_asset/kimliu.mp3";

        FMOD.init(this);

    }


    @Override
    protected void onDestroy() {
        super.onDestroy();
        FMOD.close();
    }

    public void onFix(View view) {
        switch (view.getId()) {
            case R.id.btn_normal:
                voiceChangeNative(MODE_NORMAL, path); // 真实开发中,必须子线程  JNI线程(很多坑)
                break;
            case R.id.btn_luoli:
                voiceChangeNative(MODE_LUOLI, path);
                break;
            case R.id.btn_dashu:
                voiceChangeNative(MODE_DASHU, path);
                break;
            case R.id.btn_jingsong:
                voiceChangeNative(MODE_JINGSONG, path);
                break;
            case R.id.btn_gaoguai:
                voiceChangeNative(MODE_GAOGUAI, path);
                break;
            case R.id.btn_kongling:
                voiceChangeNative(MODE_KONGLING, path);
                break;
        }
    }

    /**
     * 这个函数 给C++调用
     * JNI调用Java函数的时候,会忽略掉private public
     * @param msg
     */
    private void playEnd(String msg){
        Toast.makeText(this,""+ msg, Toast.LENGTH_SHORT).show();
    }
    // native方法  改变声音
    private native void voiceChangeNative(int modeNormal,String path);
}

6.2.2 Native层代码

重点来了,主要就是在Native层调用FMOD库中的方法,来改变声音。代码的注释已经写得很详细了。

#include <unistd.h>
#include "com_kimliu_fmodproject_MainActivity.h"

using namespace FMOD; // fmod的命名空间

extern "C" 
JNIEXPORT 
void 
JNICALL 
Java_com_kimliu_fmodproject_MainActivity_voiceChangeNative
        (JNIEnv * env, jobject thiz, jint mode, jstring path) {

    char * content_ = "默认 播放完毕";

    // C认识的字符串
    const char * path_ = env->GetStringUTFChars(path, NULL);

    // Java  对象
    // C     指针
    // Linux 文件

    // 音效引擎系统 指针
    System * system = 0;

    // 声音 指针
    Sound * sound = 0;

    // 通道,音轨,声音在上面跑 跑道 指针
    Channel * channel = 0;

    // DSP:digital signal process  == 数字信号处理  指针
    DSP * dsp = 0;

    // Java思想 去初始化
    // system = xxxx();

    // C的思想 初始化
    // xxxx(&system);

    // TODO 第一步 创建系统
    System_Create(&system);

    // TODO 第二步 系统的初始化 参数1:最大音轨数,  参数2:系统初始化标记, 参数3:额外数据
    system->init(32, FMOD_INIT_NORMAL, 0);

    // TODO 第三步 创建声音  参数1:路径,  参数2:声音初始化标记, 参数3:额外数据, 参数4:声音指针
    system->createSound(path_, FMOD_DEFAULT, 0, &sound);

    // TODO 第四步:播放声音  音轨 声音
    // 参数1:声音,  参数2:分组音轨, 参数3:控制, 参数4:通道
    system->playSound(sound, 0, false, &channel);

    // 作业:截取字符串 rry  用的二级指针   我需要拿到你的引用 来修改你main函数的 result

    // TODO 第五步:增加特效
    switch (mode) {
        case com_derry_derry_voicechange_MainActivity_MODE_NORMAL: // 原生
            content_ = "原生 播放完毕";
            break;
        case com_derry_derry_voicechange_MainActivity_MODE_LUOLI: // 萝莉
            content_ = "萝莉 播放完毕";

            // 音调高 -- 萝莉 2.0
            // 1.创建DSP类型的Pitch 音调条件
            system->createDSPByType(FMOD_DSP_TYPE_PITCHSHIFT, &dsp);
            // 2.设置Pitch音调调节2.0
            dsp->setParameterFloat(FMOD_DSP_PITCHSHIFT_PITCH, 2.0f);
            // 3.添加音效进去 音轨
            channel->addDSP(0, dsp);
            break;
        case com_derry_derry_voicechange_MainActivity_MODE_DASHU: // 大叔
            content_ = "大叔 播放完毕";

            // 音调低 -- 大叔 0.7
            // 1.创建DSP类型的Pitch 音调条件
            system->createDSPByType(FMOD_DSP_TYPE_PITCHSHIFT, &dsp);
            // 2.设置Pitch音调调节2.0
            dsp->setParameterFloat(FMOD_DSP_PITCHSHIFT_PITCH, 0.7f);
            // 3.添加音效进去 音轨
            channel->addDSP(0, dsp);
            break;
        case com_derry_derry_voicechange_MainActivity_MODE_GAOGUAI: // 搞怪
            content_ = "搞怪 小黄人 播放完毕";

            // 小黄人声音 频率快

            // 从音轨拿 当前 频率
            float mFrequency;
            channel->getFrequency(&mFrequency);

            // 修改频率
            channel->setFrequency(mFrequency * 1.5f); // 频率加快  小黄人的声音
            break;
        case com_derry_derry_voicechange_MainActivity_MODE_JINGSONG: // 惊悚
            content_ = "惊悚 播放完毕";

            // 惊悚音效:特点: 很多声音的拼接

            // TODO 音调低
            // 音调低 -- 大叔 0.7
            // 1.创建DSP类型的Pitch 音调条件
            system->createDSPByType(FMOD_DSP_TYPE_PITCHSHIFT, &dsp);
            // 2.设置Pitch音调调节2.0
            dsp->setParameterFloat(FMOD_DSP_PITCHSHIFT_PITCH, 0.7f);
            // 3.添加音效进去 音轨
            channel->addDSP(0, dsp); // 第一个音轨

            // TODO 搞点回声
            // 回音 ECHO
            system->createDSPByType(FMOD_DSP_TYPE_ECHO, &dsp);
            dsp->setParameterFloat(FMOD_DSP_ECHO_DELAY, 200); // 回音 延时    to 5000.  Default = 500.
            dsp->setParameterFloat(FMOD_DSP_ECHO_FEEDBACK, 10); // 回音 衰减度 Default = 50   0 完全衰减了
            channel->addDSP(1,dsp); // 第二个音轨

            // TODO 颤抖
            // Tremolo 颤抖音 正常5    非常颤抖  20
            system->createDSPByType(FMOD_DSP_TYPE_TREMOLO, &dsp);
            dsp->setParameterFloat(FMOD_DSP_TREMOLO_FREQUENCY, 20); // 非常颤抖
            dsp->setParameterFloat(FMOD_DSP_TREMOLO_SKEW, 0.8f); // ???
            channel->addDSP(2, dsp); // 第三个音轨

            // 调音师:才能跳出来  同学们自己去调
            break;
        case com_derry_derry_voicechange_MainActivity_MODE_KONGLING: // 空灵  学校广播
            content_ = "空灵 播放完毕";

            // 回音 ECHO
            system->createDSPByType(FMOD_DSP_TYPE_ECHO, &dsp);
            dsp->setParameterFloat(FMOD_DSP_ECHO_DELAY, 200); // 回音 延时    to 5000.  Default = 500.
            dsp->setParameterFloat(FMOD_DSP_ECHO_FEEDBACK, 10); // 回音 衰减度 Default = 50   0 完全衰减了
            channel->addDSP(0,dsp);
            break;

    }

    // 等待播放完毕 再回收
    bool isPlayer = true; // 你用不是一级指针  我用一级指针接收你,可以修改给你
    while (isPlayer) {
        channel->isPlaying(&isPlayer); // 如果真的播放完成了,音轨是知道的,内部会修改isPlayer=false
        usleep(1000 * 1000); // 每个一秒
    }

    // 时时刻刻记得回收
    sound->release();
    system->close();
    system->release();
    env->ReleaseStringUTFChars(path, path_);

    // 告知Java播放完毕
    jclass mainCls = env->GetObjectClass(thiz);
    jmethodID endMethod = env->GetMethodID(mainCls, "playerEnd", "(Ljava/lang/String;)V");
    jstring value = env->NewStringUTF(content_);
    env->CallVoidMethod(thiz, endMethod, value);
}

完整代码:github.com/kimlllll/Fm…