Android NDK开发(六):Java调用本地函数

1 Java如何通过JNI调用本地(C/C++)方法?

        主要分为4步:        
        (1)在java中利用native关键字定义native方法,表示这个方法是映射到JNI层的,调用时实际上是调用的JNI层函数。
        (2)根据JNI标准用C/C++语言建立与java方法对应的JNI层函数,并建立映射关系,实现java层与JNI层的相互调用。
        (3)实现JNI层与本地层的相互调用逻辑,并将本地层与JNI层源文件及库编译成so库。
        (4)利用Java中System类的public static void loadLibrary(String libname)方法加载so库,成功加载即可实现java调用本地方法。
        下面我们通过例子详细说明下。

2 理论结合实践

        这里通过一个简单的例子,详细说明上述步骤。

(1)在java层创建native方法

        通过Android studio创建一个新工程,选择Native C++,工程会自动为你配置好NDK、CMakeLists(Android studio默认使用的本地代码编译工具是Cmake)和JNI示例,如果你选的是普通工程,也可以自己编写CMakeLists,并在app的build.gradle中手动配置NDK、CMakeLists和JNI。我们定义一个LearnJNI类,专门用于定义java native方法,并在其中定义了实例native方法doPlus()和类native方法doMinus(),代码如下:

public class LearnJNI {
    ......
    public native int doPlus(int a,int b);
    public static native int doMinus(int a, int b);
}

(2)创建本地方法与Java方法的映射

        创建映射的方法有两种,分别称为 静态注册 动态注册

1)静态注册

a.概念:按照JNI指定命名规则,通过方法名字建立JNI本地方法与java native方法的映射。
b.本地方法命名规则为:
                      
Java_包名_类名_方法名 (注意:包名单词之间也用 _ 下划线分隔)
c.示例:在main\cpp\目录下创建learn_jni.cpp文件,将该cpp文件包含到CMakeLists.txt的源码变量中(这样能让该cpp文件拥有本地环境,可调用NDK的本地库),在该文件中直接定义(其实不用写头文件)与上述java native方法对应的本地方法为:

extern "C"
jint Java_com_boe_jnilearn_LearnJNI_doPlus(JNIEnv *env, jobject thiz, jint a, jint b) {
    return a+b;
}
extern "C"
jint Java_com_boe_jnilearn_LearnJNI_doMinus(JNIEnv *env, jclass clazz, jint a, jint b) {
    return a-b;
}

        其中本地方法名就是按照静态注册的规则写的,上述就完成了java native方法在JNI层的静态注册。

d.说明:
         extern “C” 是指定该方法用C语言的编译方式编译,而不是C++的编译方式,因为由于C++支持函数重载,用C++编译器编译出的函数名字会变为_函数名_形参1类型_形参2类型_...的方式,例如:函数void foo(int x, int y),被C编译器编译后在符号库中的名字为_foo,被C++编译器编译后在符号库中的名字为_foo_int_int,名字变了,这会导致java native找不到对应的本地方法,所以extern“C”必须加
        ● 每个JNI本地方法必须有两个形参,一个是JNIEnv* 类型形参,一个是jobject/jclass类型形参,JNIEnv前面说过用于调用JNI预定义方法,线程唯一,同一个进程可以有多个;如果java native方法是实例方法,那么第二个参数类型就是jobject,表示调用该方法的java实例,如果java native方法是类方法,那么第二个参数类型就是jclass,表示该方法的类。
        如果记不住命名规则,AS可帮助我们快捷静态注册,在JNI本地方法所在的C++文件中,输入java native名字就会有补全,选在下面的两个回车,即可生成。

或者在java native方法所在的类中,光标移动到native方法,alt+enter,选择create...,即可补全。

或者执行javah命令,先生成java native方法对应的JNI静态注册本地方法声明,然后再定义,也可以,这个方法这里不介绍了,网上比较多。
        通过AS自动生成的JNI静态注册本地方法如下:

extern "C"
JNIEXPORT jint JNICALL
Java_com_boe_jnilearn_LearnJNI_doPlus(JNIEnv *env, jobject thiz, jint a, jint b) {
    return a+b;
}
extern "C"
JNIEXPORT jint JNICALL
Java_com_boe_jnilearn_LearnJNI_doMinus(JNIEnv *env, jclass clazz, jint a, jint b) {
    return a-b;
}

会多了JNIEXPORT及JNICALL是JNI的两个宏,上一章介绍过,前者说明该本地方法是可见的,能被java调用,可以不加,不加默认是对外可见;JNICALL只用于说明这是一个jni方法,用于区别普通本地方法的标识,可以不加

2)动态注册

a.概念:手动建立JNI本地方法与java native方法的映射关系并注册给JNI。
b.相关JNI接口说明:

/**
 * 作用:动态注册java native方法
 * @param clazz java native方法所在类的class实例
 * @param methods 映射关系数组
 * @param nMethods 映射关系个数
 * @return 成功返回JNI_OK;失败返回负值
 *
 * @exceptions NoSuchMethodError 若找不到本地方法,会抛出该异常
 */
jint RegisterNatives(jclass clazz, const JNINativeMethod* methods,jint nMethods);

/**
 * 作用:取消动态注册的函数映射关系
 * @param clazz java native方法所在类的class实例
 * @return 成功返回JNI_OK;失败返回负值
 *
 * @exceptions 无异常
 */
jint UnregisterNatives(jclass clazz);

/**
 * 作用:调用System.loadLibrary()加载so库时,如果so库中定义了该方法,会回调它,可在该函数中获取JavaVM实例、
 * 初始化一些东西、动态注册等
 *
 * @param vm 当前进程的JavaVM实例
 * @param reserved JNI标准的保留参数,未来可能会规划,目前设置为Null即可
 * @return 该方法返回值为so库需要的JNI版本,可选值为JNI_VERSION_1_1,JNI_VERSION_1_2,JNI_VERSION_1_4,JNI_VERSION_1_6,
 * 如果返回的JNI版本虚拟机不能识别,则无法加载so库;如果so库中不定义该方法,则so库默认使用JNI 1.1版本。
 *
 * @exceptions 无异常
 */
jint JNI_OnLoad(JavaVM* vm, void* reserved);

/**
 * 作用:当包含本地库的类加载器被垃圾回收时,虚拟机会调用该方法,可在该方法中释放一些东西、取消动态注册的映射关系等。
 * @param vm 当前进程的JavaVM实例
 * @param reserved JNI标准的保留参数,未来可能会规划,目前设置为Null即可
 *
 * @exceptions 无异常
 */
void JNI_OnUnload(JavaVM* vm, void* reserved);

/**
 * 作用:该结构体用于存储java native方法与JNI本地方法的映射关系,其包括三个成员,
 * 分别为:java native方法名字;java native方法签名;JNI本地方法指针。
 * 
 * 注意:使用时,JNI本地方法指针前需要加上(void *),是固定的,不要修改。
 */
typedef struct {
    const char* name;
    const char* signature;
    void*       fnPtr;
} JNINativeMethod;

c.示例:

//1 首先定义JNI本地函数,名字随便取
extern "C"
jint do_plus(JNIEnv *env, jobject thiz, jint a, jint b) {
    return a+b;
}
extern "C"
jint do_minus(JNIEnv *env, jclass clazz, jint a, jint b) {
    return a-b;
}

//2 构建映射结构体数组,一个元素就是一个JNI本地方法与java native方法的映射
JNINativeMethod methodMap[] = {
        {"doPlus","(II)I",(void *) do_plus},
        {"doMinus","(II)I",(void *) do_minus}
};

//3 定义动态注册方法,在其中调用JNI的RegisterNatives()方法
int registNativeMethod(JNIEnv *env) {
    //获取java native方法所在类的class实例
    jclass class_LearnJNI = env->FindClass("com/boe/jnilearn/LearnJNI");
    //将映射关系注册给JNI,成功返回JNI_OK
    int result = env->RegisterNatives(class_LearnJNI, methodMap,
                                      sizeof(methodMap) / sizeof(methodMap[0]));
    if(result != JNI_OK){
        result = -1;
    }
    return result;
}

//4 定义jni.h中声明的JNI_OnLoad函数,并在该函数中调用注册逻辑。
jint JNI_OnLoad(JavaVM *vm, void *reserved) {
    JNIEnv *env = NULL;
    int result = -1;
    if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) == JNI_OK) {
        if (registNativeMethod(env) == JNI_OK) {
            result = JNI_VERSION_1_6;
        }
        return result;
    }
}

//5 定义jni.h中声明的JNI_OnUnload函数,并在该函数中取消注册,这步不是必须的。
void JNI_OnUnload(JavaVM* vm, void* reserved){
    JNIEnv *env = NULL;
    if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) == JNI_OK) {
        //获取java native方法所在类的class实例
        jclass class_LearnJNI = env->FindClass("com/boe/jnilearn/LearnJNI");
        env->UnregisterNatives(class_LearnJNI);
    }
}

d.说明:
        ● 
JNINativeMethod 结构体用于存储java native方法与JNI本地方法的映射关系,其包括  三个成员,分别为:java native方法名字;java native方法签名;JNI本地方法指针。使用时,JNI本地方法指针前需要加上(void *),是固定的,不要修改。
        ● RegisterNatives() 方法的参数分别为:java native方法所在类的class实例;映射关系数组;映射关系个数。返回值为注册结果,成功返回JNI_OK。
        JNI_OnLoad() :调用System.loadLibrary()加载so库时,如果so库中定义了该方法,会回调它,在该函数中获取JavaVM实例,从而获取JNIEnv实例,在其中调用注册逻辑,完成动态注册。该方法返回值为so库需要的JNI版本,可选值为JNI_VERSION_1_1,JNI_VERSION_1_2,JNI_VERSION_1_4,JNI_VERSION_1_6,如果返回的JNI版本虚拟机不能识别,则无法加载so库如果so库中不定义该方法。则so库默认使用JNI 1.1版本。

3)对比

静态注册动态注册
优点

代码少,使用简单

AS可以自动完成静态注册

代码多,使用稍显复杂

只能自己手动完成

缺点

JNI本地方法名字太长且固定

方法查找效率低

JNI本地方法名字随便取

方法查找效率高

(3)JNI层调用本地层函数,并编译so

        我们创建两个本地层方法供JNI方法调用,实现JNI层和本地层的交互,并将JNI层和本地层编译成so,供java加载。在cpp目录下创建prebuilt目录,并在其中创建include、lib、source三个目录,include用于放置本地层头文件,source用于放置本地层源码,lib用于放置本地库(暂时不用),如下图:

my_math.h内容为:

int minus();
int plus(int a,int b);

my_math.cpp内容为:

#include <my_math.h>

int minus(int a, int b){
    return a - b;
}
int plus(int a,int b){
    return a + b;
}

在JNI层调用本地层函数,learn_jni.cpp即为JNI层具体实现,文件内容为:

#include <jni.h>
#include <string>
//引用本地层头文件
#include <my_math.h>

//静态注册
extern "C"
jint Java_com_boe_jnilearn_LearnJNI_doPlus(JNIEnv *env, jobject thiz, jint a, jint b) {
    //JNI层调用本地层函数
    return minus(a,b);
}
extern "C"
jint Java_com_boe_jnilearn_LearnJNI_doMinus(JNIEnv *env, jclass clazz, jint a, jint b) {
    //JNI层调用本地层函数
    return plus(a,b);
}

CMakeLists.txt文件内容为:

#指定cmake版本
cmake_minimum_required(VERSION 3.4.1)
#指定头文件路径
include_directories(./prebuilt/include)
#指定源文件
file(GLOB src_list
        ./*.cpp
        ./prebuilt/source/*.cpp)
#编译成动态库,名字为 native-lib
add_library(native-lib SHARED ${src_list})
#查找log系统库绝对路径
find_library(log-lib log)
#依赖log库
target_link_libraries(native-lib ${log-lib})

编译一下工程生成apk,双击apk,会发现gradle不仅帮我们编译好了so,还帮我们将so打到了apk中,不用我们手动将so放到jniLibs下了:

编译一下工程,生成so,在如下目录:

 (4)在java中加载so并调用

        在LearnJni.java中添加加载so代码:

public class LearnJNI {
    static {
        //加载so名字,参数为libxxx.so的名字 xxx
        System.loadLibrary("native-lib");
    }
    public native int doPlus(int a,int b);
    public static native int doMinus(int a, int b);
}

         加载so库的逻辑一般放在静态代码块中,通过System.loadLibrary()方法加载,该方法的参数为so库名字。值得注意的是,当System.loadLibrary()方法刚开始加载so时,就会立即回调so中的JNI_OnLoad()函数,可在该函数中动态注册、持久化JavaVM实例等,JNI_OnLoad函数会返回so想用的JNI版本,若JNI版本没有被java虚拟机识别,则虚拟机不会加载so。在MainActivity中调用java native方法完成java层对本地层的调用:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Log.e("yy","" + LearnJNI.doMinus(1,2));
    }
}

         至此已完成java方法通过JNI调用本地函数的开发,运行下工程即可。细心的朋友会发现,整个过程中我们并没有手动为工程添加so依赖,即将so放到jniLibs中,这是因为gradle已经为我们自动将so添加到了apk中,双击apk,你就看到了:

        至于为啥gradle会为我们自动添加了so依赖,是因在build.gradle中添加了externalNativeBuild:

apply plugin: 'com.android.application'

android {
......
    defaultConfig {
......
        externalNativeBuild {
            cmake {
                cppFlags ""
            }
        }
    }
......
    externalNativeBuild {
        cmake {
            path "src/main/cpp/CMakeLists.txt"
            version "3.10.2"
        }
    }
    ndkVersion '21.4.7075529'
}

dependencies {
    ......
}

         好了分享就到这里,如有问题请告知,希望大家点个赞支持一下!!!

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值