Android JNI

JNI允许Java与C/C++交互,通过声明native函数、实现C代码、生成SO库和加载库来调用。JNI函数注册分为静态和动态两种,静态注册通过固定命名规则,动态注册则更灵活,通过JNI_OnLoad指定映射。JNIEnv是JNI的核心,用于操作Java对象和调用Java方法。文章还介绍了函数签名的重要性和JNI函数的常见结构。
摘要由CSDN通过智能技术生成

1.JNI

JNI(Java Native Interface)指Java本地调用,作用是使Java与C/C++具有交互能力。JNI相当于翻译官,使两种语言能够互相理解相互调用。

NDK(Native Development kit)本地开发工具包,允许使用原生语言(C/C++)来实现应用程序的部分功能。

通过JNI技术可以实现Java调用C程序、C程序调用Java代码。

041f5748d75045f8b70b062952b6cc82.png

JNI的使用步骤:

①java声明native函数

②jni实现对应的c函数

③编译生成so库

④java加载so库,并调用native函数

为什么要使用JNI技术呢?因为当需要进行大量数据计算时,原生代码的计算速度远远快于Java:

e7390d4d059046e4a9def383bcaab1eb.png

 

2.JNI函数注册

开发中使用JNI,一般是定义Java native方法,并写好对应的C方法实现。那么在Java代码中调用Java native方法时,虚拟机是怎么知道并调用so库里对应的C方法的呢?其实Java native方法与C方法的对应关系是通过注册实现的。

JNI函数分为静态注册和动态注册两种方式。

①静态注册

静态注册就是通过固定的命名规则映射Java和Native函数。即C中按照JNI规范书写函数名:Java_包名_类名_方法名。

通过javac和javah编译出头文件,然后实现对应的cpp文件,这就是静态注册的方式。这种调用方式是由于JVM按照默认的映射规则来匹配对应的native函数,如果没匹配就会报错。

举例:

//Java层代码JniSdk.java

public class JniSdk {

    static {

        System.loadLibrary("test_jni");

    }

    public native String showJniMessage();

}

//Native层代码test_jni.cpp

extern "C" JNIEXPORT jstring JNICALL Java_com_example_dragon_androidstudy_jnidemo_JniSdk_showJniMessage (JNIEnv* env, jobject job) {

    return env->NewStringUTF("hello world");

}

在这里,java层的showJniMessage函数对应的就是c语言那边的Java_com_example_jnidemo_JniSdk_showJniMessage函数。这个映射是JVM实现的。

在调用showJniMessage函数时JVM会从JNI库寻找对应的函数并调用。寻找时的规则:由于JniSdk.java的包名是com.example.jnidemo,那么showJniMessage方法完整的路径就是com.example.jnidemo.JniSdk.showJniMessage。而.在C里面有特殊的函数,所以JVM就将它替换成了_,并在前面加了Java_标识,就变成了上面的方法。

e77fd49bf5f24f35a7e22be41da81770.png

静态注册的优缺点:

系统默认方式,使用简单;

灵活性差(如果修改了java native函数所在类的包名或类名,需手动修改C函数名称;

②动态注册

使用静态注册方式时,每次使用JNI都要先声明native,然后编译成class,再生成头文件,最后再按照特定的规则去实现native函数。这一整套流程下来不仅繁琐,很多步骤还是没必要的(没必要生成.h文件,只要在.c文件里面根据对应的规则声明函数即可)。而且JVM在根据静态注册匹配的规则调用函数时效率也比较低。因此有了动态注册的方法。

动态注册就是不用默认的映射规则,直接由开发者告诉JVM java中的native函数对应的是C文件里的哪个函数。

动态注册一般是通过重写JNI_OnLoad函数,用jint RegisterNative(j class clazz, const JNINativeMethod* methods, jint nMethods)函数将Java中定义的native函数和C/C++中定义的函数进行映射。即在JNI_OnLoad中指定Java Native函数与C实现函数的对应关系。

所以动态注册在调用Native方法之前就已经知道它在JNI中对应的函数指针,这需要用到一个结构体来描述两者之间的关系:

typedef struct {

    const char* name; //对应java中native的函数名

    const char* signature; //java中native函数的函数签名

    void* fnPtr; //JNI中对应的函数指针

} JNINativeMethod;

举例:

//Java层定义Native函数

public class JniSdk {

    static {

        System.loadLibrary("test_jni");

    }

    public static native int numAdd(int a, int b);

    public native void dumpMessage();

}

//.cpp中动态注册

/*定义函数映射关系。需要注册的函数列表放在JNINativeMethod类型的数组中。参数:1.java中用native关键字声明的函数名;2.签名(传进来参数类型和返回值类型的说明) ;3.C/C++中对应函数的函数名(地址)*/

static JNINativeMethod g_methods[] = {

        {"numAdd", "(II)I", (void*)add},

        {"dumpMessage","()V",(void*)dump},

}; 

jint JNI_OnLoad(JavaVM *vm, void *reserved) {

    j_vm = vm;

   JNIEnv *env = NULL;

    //获取JNIEnv

    if (vm->GetEnv((void**)&env, JNI_VERSION_1_2) != JNI_OK) {

        LOGI("on jni load , get env failed");

        return JNI_VERSION_1_2;

    }

    //指定类的路径(把.换成/),通过FindClass方法来找到对应的类

    jclass clazz = env->FindClass("com/example/jnidemo/JniSdk");

    //注册函数,参数分别为java类、所要注册的函数数组、注册函数的个数(也可以用sizeof(g_methods)/sizeof(g_methods[0])

    jint ret = env->RegisterNatives(clazz, g_methods, 2);

    if (ret != 0) {

        LOGI("register native methods failed");

    }

    return JNI_VERSION_1_2;

}

在调用System.loadlibrary函数时,JVM会回调上面的JNI_OnLoad函数,就是在这个函数里通过env->RegisterNatives进行的动态注册,其中env是jni函数实现的核心。

动态注册原理图:

bee6f91eb27a4952a0320f9c4f03cac8.png

8cb117a248e0454487ece3b758d6a87b.png

动态注册的优缺点:

函数名看着舒服一些,但是需要在C代码中维护Java Native函数与C函数的对应关系;

灵活性稍高(如果修改了java native函数所在类的包名或类名,仅调整Java native函数的签名信息)

 

4.函数签名

函数签名可以理解成一个函数的唯一标识,一个签名对应着一个函数的签名,它们是一一对应的关系。

函数的签名是针对函数的参数以及返回值组成的。它遵循如下格式(参数类型1;参数类型2;参数类型3.....)返回值类型。例如上面的numAdd函数一样,它在java层的函数声明是:

int numAdd(int a, int b)

这里面有两个参数都是int,并且返回值也是int。所以的函数签名是(II)I。而dumpMessage函数没有任何参数,并且返回值也是空,所以它的签名是()V。

注:如果自己手动写这种签名的话很容易出错,有一个工具可以很方便的列出每个函数的签名。可以先通过javac命令编译出class文件。然后再通过javap -s -p xxx.class命令列出这个class文件所有的函数签名。

 

5.JNIEnv

java函数和jni函数一旦建立映射关系后,在java层调用native函数就变得很简单了。但是一般程序并不仅仅是这么简单的需求,大多数的时候还需要jni函数调用java层的函数,比如进行后台文件操作后将结果通知到上层,这个时候就需要jni调用java的函数了。这时就要JNIEnv出场了,JNIEnv可以说是贯穿了整个JNI技术的核心。

JNIEnv代表了Java环境,通过JNIEnv*就可以对Java端的代码进行操作,如:创建Java对象NewObject或NewString、调用Java对象的方法CallMethod、获取Java对象的属性GetField等。

通过JNIEnv *env调用Java层代码,如获得某个字段、获取某个函数、执行某个函数等:

//获得某类中定义的字段id

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

    return functions->GetFieldID(this, clazz, name, sig);

}

//获得某类中定义的函数id

jmethodID GetMethodID(jclass clazz, const char* name, const char* sig) {

    return functions->GetMethodID(this, clazz, name, sig);

}

这里与Java的反射比较类似,参数:clazz表示类的class对象;name表示字段名、函数名;sig:如果是字段表示字段类型的描述符,如果是函数表示函数结构的描述符。

举例:

//Java代码

public class Hello{

     public int property;

     public int fun(int param, int[] arr){

          return 100;

     }

}

//JNI C/C++代码

JNIEXPORT void Java_Hello_test(JNIEnv* env, jobject obj){

    jclass myClazz = env->GetObjectClass(obj);

    jfieldId fieldId_prop = env -> GetFieldId(myClazz, "property", "I");

    jmethodId methodId_fun = env -> GetMethodId(myClazz, "fun", "(I[I)I");

}

 

Java语言的执行环境是Java虚拟机JVM,JVM其实是主机环境中的一个进程,每个JVM虚拟机都在本地环境中有一个JavaVM结构体,该结构体在创建JVM虚拟机时被返回。JNI全局仅仅有一个,JavaVM是Java虚拟机在JNI层的代表,一个JVM对应一个JavaVM结构。一个JVM中可能创建多个Java线程,每个线程对应一个JNIEnv结构,它们保存在线程本地存储TLS中。JNIEnv是一个线程相关的函数表结构体,该结构体代表了Java在本线程的执行环境。不同线程的JNIEnv是不同,也不能相互共享使用。在本地代码中通过JNIEnv的函数表来操作Java数据或调用Java方法。

也就是说,JNIEnv是JVM内部维护的一个和线程相关的代表JNI环境的结构体。这个结构体和线程相关,并且C函数里面的线程与java函数中的线程是一一对应关系,也就是说如果在java里的某个线程调用jni接口,不管调用多少个JNI接口,传递的JNIEnv都是同一个对象。因为这个时候java只有一个线程,对应的JNI也只有一个线程,而JNIEnv是跟线程绑定的,因此也只有一个。

 

6.JNI函数

常见的JNI方法:

#include <jni.h>

#include <string>

extern "C" JNIEXPORT jstring JNICALL

Java_com_test_testnativec_MainActivity_stringFromJNI(JNIEnv* env, jobject /* this */) {

    std::string hello = "Hello from C++";

    return env->NewStringUTF(hello.c_str());

}

JNI方法结构与Java方法类似,同样包含方法名、参数、返回类型,只不过多了一些修饰词、特定参数类型而已。

①extern "C"

作用:避免编绎器按照C++的方式去编绎C函数。

如果删掉extern “C”,重新生成so,运行app,结果会直接闪退:UnsatisfiedLinkError: No implementation found for java.lang.String com.test.testnativec.MainActivity.stringFromJNI()

通过反编译so文件会发现,去掉extern “C” 后函数名字竟然被修改了:

//保留extern "C"

000000000000ea98 T 

Java_com_test_testnativec_MainActivity_stringFromJNI

//去掉extern "C"

000000000000eab8 T 

_Z40Java_com_test_testnativec_MainActivity_stringFromJNIP7_JNIEnvP8_jobject

所以,如果希望编译后的函数名不变,应通知编译器使用C的编译方式编译该函数(即:加上关键字:extern “C”)。

②JNIEXPORT、JNICALL

JNIEXPORT用来表示该函数是否可导出(即:方法的可见性);JNICALL用来表示函数的调用规范(如:__stdcall)。

可以通过点击JNIEXPORT、JNICALL关键字跳转到jni.h中的定义,如下图:

18ef8cc7cded42af9ea8828721812d7c.png

通过查看jni.h中的源码,原来JNIEXPORT、JNICALL是两个宏定义。

宏可以这样理解:宏JNIEXPORT代表的就是右侧的表达式:__attribute__ ((visibility ("default")));或者也可以说:JNIEXPORT是右侧表达式的别名。宏可表达的内容很多,如:一个具体的数值、一个规则、一段逻辑代码等。

attribute___((visibility ("default"))) 描述的是“可见性”属性visibility。default表示外部可见,类似于public修饰符 (即:可以被外部调用);hidden表示隐藏,类似于private修饰符 (即:只能被内部调用)。

如果想使用hidden隐藏方法,可这么写:

#include <jni.h>

#include <string>

extern "C" __attribute__ ((visibility ("hidden"))) jstring JNICALL

Java_com_test_testnativec_MainActivity_stringFromJNI(JNIEnv* env, jobject /* this */) {

    std::string hello = "Hello from C++";

    return env->NewStringUTF(hello.c_str());

}

重新编译、运行,结果闪退了。原因是函数Java_com_test_testnativec_MainActivity_stringFromJNI已被隐藏,而在java中调用该函数时,找不到该函数,所以抛出了异常。

宏JNICALL右边是空的,说明只是个空定义。宏JNICALL代表的是右边定义的内容,那么java代码也可直接使用右边的内容(空)替换调JNICALL(即去掉JNICALL关键字),编译后运行,调用so仍然是正确的。

 

7.demo测试

①搭建环境

启动Android Studio --> 打开SDK Manager --> SDK Tools:

227c7930e27d4f399272d80885f63f35.png

选择NDK、CMake、LLDB(调试Native时会使用),选择Apply进行安装,等安装成功后,NDK开发所依赖的环境也就齐全了。

②Native C++项目创建(HelloWord案例)

新建项目,选择Native C++:

ee9417d9d84e4b70810622b82fa6f8e4.png

新创建的项目,默认已包含完整的native示例代码、cmake配置:

673757243a83429585abead93165d375.png

 cf386dce77aa4152b77fef6bf4dfc9ca.png

这样就可以自己定义Java native方法,并在cpp目录中写native实现了,很方便。

可以直接使用工程默认生成的native-lib.cpp,简单调整一下native实现方法的代码:

10bfce23e4d14f7fa219d68504b64cd8.png

因Native C++工程默认已配置好了CMakeLists.txt和gradle,所以可直接运行工程看效果。

JNI交互效果已经看到了,说明CMake编译成功了。

③现在分析一下CMake生成的库文件与apk中的库文件

Android工程编译时,会执行CMake编译,在 工程/app/build/.../cmake/ 中会产生对应的so文件:

837b268f52874fa89db938a67f94967e.png

继续编译安卓工程,会根据build中的内容生成*.apk安装包文件。反编译apk安装包文件,查找so库文件。原来在apk安装包中,so库都被存放在lib目录中:

022d02d297b5427e82799e4a3aa240b1.png

那CMake是如何编译生成so库的呢?其实CMake是基于CMakeLists.txt文件和gradle配置实现编译Native类的。

看一下CMakeLists.txt文件:

#cmake最低版本要求

cmake_minimum_required(VERSION 3.4.1)

# 配置库生成路径。CMAKE_CURRENT_SOURCE_DIR指cmake库的源路径,通常是build/.../cmake/;/../jniLibs/指与CMakeList.txt所在目录的同级目录:jniLibs (如果没有会新建);ANDROID_ABI指生成库文件时,采用gradle配置的ABI策略(即:生成哪些平台对应的库文件)

set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/../jniLibs/${ANDROID_ABI})

#添加库

add_library(

        # 库名

        native-lib

        # 类型:SHARED 是指动态库,对应的是.so文件,STATIC 是指静态库,对应的是.a文件

        SHARED

        # native类路径

        native-lib.cpp)

# 查找依赖库

find_library( 

        # 依赖库别名

        log-lib

        # 希望加到本地的NDK库名称,log指NDK的日志库

        log)

# 链接库,建立关系( 此处就是指把log-lib 链接给 native-lib使用 )

target_link_libraries( 

        # 目标库名称(native-lib 是要生成的so库)

        native-lib

        # 要链接的库(log-lib 是上面查找的log库)

        ${log-lib})

再看一下app的gradle又是如何配置CMake的呢?

apply plugin: 'com.android.application'

android {

    compileSdkVersion 29

    buildToolsVersion "29.0.1"

    defaultConfig {

        applicationId "com.qxc.testnativec"

        minSdkVersion 21

        targetSdkVersion 29

        versionCode 1

        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

        //定义cmake默认配置属性

        externalNativeBuild {

            cmake {

                cppFlags ""

            }

        }

    }

    //定义cmake对应的CMakeList.txt路径(重要)

    externalNativeBuild {

        cmake {

            path "src/main/cpp/CMakeLists.txt"

        }

    }

    sourceSets {

        main {

            jniLibs.srcDirs = ['jniLibs']//指定lib库目录

        }

    }

}

接着,重新build就会在cpp相同目录级别位置生成jniLibs目录,so库也在其中了:

bf3c2281f538496eafdf45010daefa53.png

 

 

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值