安卓开发进阶之快速上手JNI 其一初窥门径

1、什么是JNI?

      JNI 全称是 Java Native Interface,即java本地接口,换句话说,就是连接JAVA代码和本地代码的桥梁(本地代码是指的使用C或C++编写的代码),本地代码一般是更底层的逻辑,比如操作系统,硬件驱动之类的代码,这种底层代码一般都是C或C++编写的,所以JAVA程序要想实现更高的性能或访问底层资源就绕不开JNI这个桥梁,如下图:

前期准备

需要在Android Studio中配置好NDK和CMAKE工具,如下图:

NDK指Native Development Kit(原生开发工具包),是Android平台的一个开发工具集,用于在Android应用中使用C和C++代码。它包含了API、交叉编译器、链接程序、调试器、构建工具。

CMake是一个跨平台的构建工具。

2、JNI的注册方式

以c++为例,前面说了,JAVA程序中想要调用C++里面的函数时,或者C++函数调用JAVA里面的函数时,都需要使用JNI,而JNI的注册分为静态和动态两种(注册:简单理解就是把java函数和C++函数建立映射关系),先讲比较好理解的静态注册,我们直接从一个简单的例子开始讲:

求两数之和,java层传入两数,c++层计算后返回给java层

2.1、静态注册

2.1.1  编写java层函数

java层先构造一个本地函数,本地函数以native关键字修饰,如下:

public class MyJniTest {
    
    public static native int addInt(int num1, int num2);
    
}

2.1.2  编写jni层的函数

我们在src\main目录下创建一个jni目录,然后在里面编写JNI方法

下面是JNI方法的标准格式:

JNIEXPORT 返回值 JNICALL  Java_包名_类名_函数名(参数列表)

我们先创建jni的头文件,也就是.h文件,假设叫static_register_JNI.h

无需手动敲代码,可以直接对着java层的本地函数按alt+enter,选create JNI fuction,自动创建jni方法,或者使用javah命令生成(参考后面ndkbuild中的做法),生成的头文件如下:

#include <jni.h>

//
// Created by ZhouPeng on 2024/4/15.
//

#ifndef JNI_APPLICATION_STATICJNIREGISTER_H
#define JNI_APPLICATION_STATICJNIREGISTER_H

extern "C" {

JNIEXPORT jint
JNICALL
Java_com_example_jniapplication_MyJniTest_addInt(JNIEnv *env, jclass clazz, jint num1, jint num2);

}

#endif //JNI_APPLICATION_STATICJNIREGISTER_H

上面的JNIEnv:即 Java Native Interface Environment,Java 本地编程接口环境。JNIEnv 内部定义了很多函数用于简化我们的 JNI 编程。JNI 把 Java 中的所有对象或者对象数组当作一个 C 指针传递到本地方法中,这个指针指向 JVM 中的内部数据结构(对象用jobject来表示,而对象数组用jobjectArray或者具体是基本类型数组),而内部的数据结构在内存中的存储方式是不可见的。只能从 JNIEnv 指针指向的函数表中选择合适的 JNI 函数来操作JVM 中的数据结构。

jclass:我的理解是对应Java中的Class

jint: JNI的数据类型,对应Java中的int类型

extern "c":表示可以使用C和C++进行混合开发

我们在头文件里面定义了jni函数,现在我们需要去实现这个函数,需要创建对应的源码文件static_register_JNI.cc


#include "static_register_JNI.h"
#include "add.h"
//
// Created by ZhouPeng on 2024/4/15.
//

extern "C" {

JNIEXPORT jint
JNICALL
Java_com_example_jniapplication_MyJniTest_addInt(JNIEnv *env, jclass clazz, jint num1, jint num2) {
    // TODO: implement addInt()

    int temp1 = num1;
    int temp2 = num2;
    int result = add(temp1,temp2);
    return result;

   }

}

我们看到这个函数其实很简单,我们调用了一个add函数完成求和,其中jnit这个数据类型是jni层才有的数据类型,对应java和c++中的int型,数据类型这块后续会讲。

2.1.3 编写c++层函数

然后我们看下add这个c++方法的实现,也是先创建头文件add.h,内容很简单

using namespace std;

int add(int a,int b);//添加函数声明

然后创建源码文件add.cc,内容也很简单

#include "add.h"

using namespace std;

int add(int a,int b) {

   return a+b;

}

到此,我们代码都写好,下面需要选择编译方式了,有两种编译方式,ndkbuild和cmake,后面会讲这两种的编译方法;

2.2、动态注册

动态注册步骤就简洁多了,直接开写jni的源码文件dynamic_register_JNI.cc,代码中的注释已经写的很详细了,数据类型和函数签名会在后面讲到,函数如下:

#include "add.h"
#include <jni.h>

//jni对应的函数
jint addNumber(JNIEnv *env,jclass clazz,jint a,jint b){

    int temp1 = a;
    int temp2 = b;
    //调用对应的c++函数
    int result = add(temp1,temp2);
    return  result;
}

//三个参数,java层的函数名, java层函数的签名, C层函数指针
//获取签名方法: javap -s -p DynamicRegister.class
//建立java函数与jni函数的映射关系
static const JNINativeMethod methods[]={
        {"addInt","(II)I",(void*)addNumber},
};

//java层函数load时,便会自动调用该函数
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved){
    //获得 JniEnv
    JNIEnv *jniEnv{nullptr};
    if (vm->GetEnv((void **) &jniEnv, JNI_VERSION_1_6) != JNI_OK) {
        return -1;
    }
    //FindClass,反射,通过类的名字反射
    jclass mainActivityCls = jniEnv->FindClass("com/example/jniapplication/MyJniTest");
    //注册方法,
    jint ret=jniEnv->RegisterNatives(mainActivityCls,methods,sizeof(methods)/sizeof(methods[0]));
    //注册 如果小于0则注册失败
    if (ret != 0) {
        return -1;
    }
    return JNI_VERSION_1_6;
}

methods[]:这个数组就是建立java函数与jni函数的映射关系的,有了映射关系才知道调用谁。
JNI_OnLoad:这个函数会在java函数加载时自动调用,通过类名反射拿到java对象。
动态注册的模版是固定的,分为三部分:jni函数的实现,函数映射关系数组,JNI_OnLoad函数实现

3、编译方式

编译方式有ndkbuild和cmake,推荐使用cmake,更方便简洁

3.1 ndkbuild编译

我们以静态注册为例,使用ndkbuild编译的流程如下

3.1.1 在写好所有源码的基础上,make-project一下项目或者使用javac命令生成.class文件,如下图所示:

如果代码是在app模块下的话,生产的.class文件在下面这个路径下

app\build\intermediates\javac\debug\classes

如果是在其他模块,则是在 其他模块\build\classes\java\main目录下

3.1.2  我们在anroid studio的terminal窗口,使用javah命令生成jni的.h头文件(这里其实和前面使用快捷键生成头文件类似,都是一种自动生成的方法,你也可以不用这种方法,手动写也行

1、先 cd app/src/main

2、然后使用javah命令,如下

javah  -d  jni  -classpath  ..\..\build\intermediates\javac\debug\classes com.example.jniapplication.MyJniTest

上面这段命令中,

-d jni  表示在当前目录下创建一个 jni 文件夹

-classpath 表示后面的参数是class文件的路径,路径和完整类名之间有个空格

该命令会在jni目录下生成一个头文件com_example_jniapplication_MyJniTest.h

3、然后我们复制一下头文件里面的jni方法,创建一个源文件去实现它,就是前面的

static_register_JNI.cc这个文件

4、配置Application.mk文件,在jni目录创建该文件,内容如下:

#//生成哪些架构的so动态库,all表示所有的,包括armeabi-v7a、arm64-v8a、x86、x86_64,多个之间空格隔开
APP_ABI := all
#针对android sdk28生成动态库,与项目保持一致即可
APP_PLATFORM := android-28

5、配置Android.mk文件,在jni目录创建该文件,内容如下:

#每个Android.mk文件必须以定义LOCAL_PATH为开始,它用于在开发tree中查找源文件;
#宏my-dir则由Build System提供,返回包含Android.mk的目录路径。
LOCAL_PATH := $(call my-dir)
#CLEAR_VARS 变量由Build System提供,并指向一个指定的GNU Makefile,由它负责清理很多LOCAL_xxx。
#例如:LOCAL_MODULE, LOCAL_SRC_FILES, LOCAL_STATIC_LIBRARIES等等。但不清理LOCAL_PATH,
#这个清理动作是必须的,因为所有的编译控制文件由同一个GNU Make解析和执行,其变量是全局的,所以清理后才能避免相互影响
include $(CLEAR_VARS)
#LOCAL_MODULE模块必须定义,生成.so文件的文件名,以表示Android.mk中的每一个模块。名字必须唯一且不包含空格。
LOCAL_MODULE:=test
#C/C++所需的源文件路径,多个文件之间用空格隔开
LOCAL_SRC_FILES:=static_register_JNI.cc add.cc
#BUILD_STATIC_LIBRARY:编译为静态库
#BUILD_SHARED_LIBRARY:编译为动态库
#BUILD_EXECUTABLE:编译为Native C可执行程序
include $(BUILD_SHARED_LIBRARY)

6、执行ndkbuild命令,如果配置了环境变量就执行在terminal窗口输入ndk-build

没有配置就是使用完整路径,看sdk在哪里

我的是这样的 C:\Users\ZhouPeng\AppData\Local\Android\Sdk\ndk\20.0.5594570\ndk-build

之后会在libs目录下生成四个文件夹'armeabi-v7a', 'x86', 'arm64-v8a', 'x86_64',每个文件夹下有个.so文件

7、配置build.gradle文件,配置支持的so库架构,对应不同架构的芯片;

在android {  下设置jni依赖目录

8、将前面生成的.so文件复制到app模块的libs目录下,然后找个地方加载这个so库,注意加载时文件名不用写lib的前缀;

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

9、然后java层正常调用方法即可。

3.2 cmake编译

我们以动态注册为例,使用cmake编译流程如下

3.2.1 编译写jni动态加载的函数,就是前面的dynamic_register_JNI.cc这个源码文件,这里就不再赘述了。

3.2.2 配置CMakeLists.txt文件,在jni目录下创建该文件,内容如下:

#表示项目支持的最低cmake工具版本是3.22.1
cmake_minimum_required(VERSION 3.22.1)


# Declares and names the project.
project("JNI Application")


#设置so库的输出路径
#set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/libs/${ANDROID_ABI})


# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.

add_library( # Sets the name of the library.
        #.so文件的文件名
        dynamic_test

        # Sets the library as a shared library.
        SHARED

        # Provides a relative path to your source file(s).
        #c++源码文件
        dynamic_register_JNI.cc add.cc)

# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.

find_library( # Sets the name of the path variable.
        log-lib

        # Specifies the name of the NDK library that
        # you want CMake to locate.
        log)

# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.

target_link_libraries( # Specifies the target library.
        #.so文件的文件名
        dynamic_test

        # Links the target library to the log library
        # included in the NDK.
        ${log-lib})

3.2.3 配置build.gradle文件,在android {  设置cmake文件的路径和版本

    externalNativeBuild {
        cmake {
            path file('src/main/jni/CMakeLists.txt')
            version '3.22.1'
        }
    }

3.2.4  找个地方加载so库,然后正常调用即可,编译生成的so库在下面这个目录下,比ndkbuild好的地方是,步骤少,cmake编译无需再把.so文件复制到某个目录下再加载了,可以直接加载。

4、JNI中的数据类型和类型签名

4.1 数据类型

JNI作为java和c++的中间层,有自己的数据类型,基本类型如下:

常见的引用类型:

4.2 类型签名

在JNI动态注册时我们需要使用类型签名,什么是类型签名呢?我们知道Java中的函数可以重载,即一个函数可以有不同数量和类型的参数以及返回值,所以我们动态注册在映射时,光靠函数名称肯定是不行的,所以还需要指定参数和返回值类型的,所以类型签名其实就是用来表示这个函数的参数和返回值的。

以下面这个例子为例,第一个"addInt"是Java函数名称,后面的"(II)I",括号里面的是参数,括号外面的是返回值,I代表是int类型的数据,II代表两个int类型的数据,是不是很简单。

函数签名对应关系如下:

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值