Android JNI 基础入门指南

前言

请添加图片描述
请添加图片描述

Java 和 c/c++ 属于两种完全独立得编程语言,他们之间是不能直接进行交互的。但是出于某些原因,我们希望 Java 调用 c/c++ 的代码来实现一些特殊的需求。那么,JNI 就成为了两种语言交互的桥梁。

什么是 JNI

JNI(Java Native Interface,Java 本地接口)是 Java 生态的特性,它扩展了 Java 虚拟机的能力,使得 Java 代码可以与 C/C++ 代码进行交互。 通过 JNI 接口,Java 代码可以调用 C/C++ 代码,C/C++ 代码也可以调用 Java 代码。所以,如果想要和 C/C++ 代码进行交互,我们就必须使用 JNI。

在 Java 的源码当中,也有很多关于 JNI 的使用。因为 Java 虽然是平台无关性语言,但是 Java 的虚拟机是运行在具体的平台上的(Windows/Linux),在调用平台 API 的功能时,Java 的底层实际还是通过 JNI 调用了 Native 层的不同方法。例如,打开文件在 Windows 平台的实现是 openFile 函数,而在 Linux 上是 open 函数。因此,JNI 对于 Java 来说,是必不可少的。

使用 JNI 的好处

  • C/C++ 运行效率优于 Java:对于密集计算的程序来说,会选择使用 C/C++ 来实现,然后再通过 JNI 调用。所以大多数游戏的底层,都是使用 C/C++ 语言来实现的。

  • C/C++ 层代码安全性更高:反编译 so 文件的难度比反编译 class 文件高,所以秘钥存储、加解密、鉴权等相关的操作都可以放在 C/C++ 中实现,然后通过 JNI 调用,提高程序的安全指数。目前我们在项目中也有类似的实践。

  • 复用代码:当 Android、iOS、Windows、Linux 四端都需要开发同样一块功能时,可以都用 C/C++ 来实现共同逻辑,四端只需要实现各自平台的 API 适配,达到代码复用的效果。目前我们项目组中部分 SDK 就是使用 C/C++ 来实现的,能极大减少人力消耗,并且能保证各端逻辑高度统一。

JNI 学习路线

请添加图片描述

考虑到篇幅原因,我挑选了其中较为重要的几个方面来介绍 JNI 相关内容,来帮助大家快速上手 JNI 开发:

  • so 编译

  • JNI 方法注册

  • 对象引用

  • 异常处理

so 编译

其实简单来说,JNI 开发就是将 c/c++ 代码编译构建成 so 动态库提供给 Java 程序使用。目前,Android Studio 基本都是使用 CMake 来作为编译构建系统。CMake 可以用简单的语句来描述所有平台的安装(编译过程),并且能够输出各种各样的 Makefile 或者 project 文件。但 CMake 并不直接建构出最终的软件,而是产生其他工具的脚本(如Makefile)。它可以根据不同平台、不同的编译器,生成相应的 Makefile 或者 vcproj 项目,从而达到跨平台的目的。Android Studio 利用 CMake 生成的是 ninja 脚本(ninja 是一个小型的高速的构建系统)。主要是通过编写 CMakeLists.txt 文件,然后用 cmake 命令将 CMakeLists.txt 文件转化为 make 所需要的 makefile 文件,最后用 make 命令编译源码生成可执行程序或原生库(so 库)。Android Studio 已经将整个 CMake 构建流程进行了封装,我们只需要对项目进行构建,就能够得到最终的成果物(如下图)

请添加图片描述

开发者真正需要关注的就只是 CMakeLists.txt 文件的编写。一般来说,我们会声明一个库文件,然后往库文件中添加源文件以及头文件,再链接三方库即可。下面,我们就以一个简单的例子来介绍下常用的 CMake 语法。

# 设置 CMake 要求的最低版本
cmake_minimum_required(VERSION 3.10.2)

# 声明并命名项目
project("jnimethoddemo")

# 遍历当前文件,添加所有的 .cpp 文件到 SRC_JNI 列表中
file(GLOB_RECURSE SRC_JNI ./*.cpp)
list(APPEND SRC_LIST ${SRC_JNI})

# 创建一个库,可以是 STATIC 静态库,也可以是 SHARED 动态库
add_library(
        jnimethoddemo

        SHARED

        "")

target_sources(
        jnimethoddemo

        PUBLIC
        ${SRC_LIST}
)

target_include_directories(
        jnimethoddemo

        PUBLIC
        ./
)

# 查找预编译库,并命名
find_library(
        log-lib

        log)

# 链接三方库
target_link_libraries(
        jnimethoddemo

        ${log-lib})

cmake 的语法、函数有很多,这里并不会一一赘述。在通过 cmake 构建出我们想要的 so 动态库之后,我们只需要在 Java 代码中加载 so,就能调用 c/c++ 代码了:

    static {
        System.loadLibrary("jnitestdemo");
    }

JNI 方法注册

从 Android Studio 提供的 demo 中我们可以看到,最最最基本的用法就是 Java 调用 c/c++ 函数。为了实现这个功能,我们需要实现方法注册,因为 Java 是不能直接调用 c/c++ 函数的,所以我们需要声明一个 Java 函数,并将这个 Java 函数和 c/c++ 函数映射在一起。在 Java 程序运行并调用函数时,就能通过映射关系找到 c/c++ 中对应的函数并进行执行。而 JNI 有两种注册方法方式:静态注册动态注册,这两种注册方法有什么区别呢?

静态注册

首先需要在 java 层定义一格 native 方法:

private static native String stringFromJNI();

然后可以通过 Alt + Insert 快捷键,在对应的 .cpp 文件中生成 native 方法:


extern "C"
JNIEXPORT jstring JNICALL
Java_com_xxxxxxx_android_dat_library_DatManager_stringFromJNI(JNIEnv *env, jclass clazz) {
   
    // TODO: implement stringFromJNI()
}

当然,你也可以手动编写一个对应的 native 方法,但是必须要满足 Java_<包名><类名><方法名>(__<参数>) 的规则,否则 java 方法和 native 方法会对应不上。因为在 java 层调用 stringFromJNI() 方法时,它会从对应的 so 库中寻找 Java_com_xxxxxxx_android_dat_library_DatManager_stringFromJNI 方法,如果找不到这个方法,则会报错;如果找到,则会为这两个函数建立一个映射关系。而且,由于只有当 stringFromJNI() 方法第一次执行时,才会去查找对应的本地函数,所以这个过程比较耗时。

注意点

如果方法名不正确,IDE 会报警告,但是能正常编译,代码运行后会报错:

java.lang.UnsatisfiedLinkError: No implementation found for java.lang.String com.xxxxxxx.android.dat.library.DatManager.stringFromJNI() (tried Java_com_xxxxxxx_android_dat_library_DatManager_stringFromJNI and Java_com_xxxxxxx_android_dat_library_DatManager_stringFromJNI__)

所以,建议这一步直接用插件快捷生成的方式执行,以免方法编写错误。

动态注册

想要使用动态注册,通常会通过实现 JNI_OnLoad 方法,然后在 JNI_OnLoad 方法中,通过 env->RegisterNatives() 来完成方法的动态注册,它的本质其实就是通过方法名、方法参数,将 java 方法和 native 方法创建映射关系。

首先需要在 java 层定义一格 native 方法,这一步和静态注册时一样的:

private static native String getVersionNative();

然后在 native 中手动实现:


// 首先需要实现 JNI_OnLoad,JNI_OnLoad 方法在 so 加载时就调用
JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {
   
    JNIEnv *env = nullptr;
    if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
   
        return JNI_ERR;
    }

    // 实现方法动态注册
    if (RegisterNativesDat(env) < 0) {
   
        return -1;
    }

    return JNI_VERSION_1_6;
}

// 注册 dat 相关方法
jint RegisterNativesDat(JNIEnv *env) {
   
    jclass clazzDatManager = env->FindClass("com/xxxxxxx/android/dat/library/DatManager");
    if (clazzDatManager == nullptr) {
   
        return JNI_ERR;
    }
    
    // 参数对应关系:java 方法名、java方法参数、对应的 native 方法名
    JNINativeMethod methods_datManager[] = {
   
            {
   "getVersionNative",         "()Ljava/lang/String;",                                                (void *) getVersion},
    };


    // 通过 RegisterNatives 方法手动完成 native 方法和 so 中的方法的绑定,这样虚拟机就可以通过这个函数映射表直接找到相应的方法
    return env->RegisterNatives(clazzDatManager, methods_datManager,
                                sizeof(methods_datManager) /
                                sizeof(methods_datManager[0]));
}

由于 JNI_OnLoad 函数中的方法注册在 so 加载的时候就会执行,并建立了映射关系。所以当首次执行 Java 中的 getVersionNative() 函数时,即可根据映射关系直接过去对应的 c/c++ 函数指针,运行效率相比静态注册高。

注意点

如果动态注册中,方法写错了,能正常编译,但是运行时会报错:

'JNI DETECTED ERROR IN APPLICATION: JNI NewGlobalRef called with pending exception java.lang.NoSuchMethodError: no static</
  • 23
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值