二、JNI的编译与运行

如何区分安卓项目是java工程还是native工程?

  1. build文件中是否有externalNativeBuild声明,有则表示属于native工程:
arduino 复制代码
//native工程的入口
externalNativeBuild{
	cmake{
        path"src/main/cpp/CMakeLists.txt"
        version "3.10.2"
    }
}
  1. 检查代码中是否有 native修饰的本地函数声明。

如何区分生成的lib库是静态库还是动态库

在as中打开output中编译的apk,看lib目录下生成的native库是以何种后缀结尾:

  1. .so结尾是动态库。(.so理解为插件化)
  2. .a结尾则是静态库。(.a理解为组件化)

CMake编译

Android Studio中CMake的编译过程

  1. 程序开始编译时,AS会从build.gradle找externalNativeBuild的cmake配置信息。从配置信息中找到CMakeLists.txt 文件的位置、版本号。
    as4.0版本之前的CMakeLists.txt 文件位置是在app-module 下;4.0及之后的版本则是在 /main/cpp文件夹下存放的。
  2. 将第一步骤得到的信息 交给Android SDK目录里的CMake.exe可执行文件。
  3. CMakeLists.txt 文件中的cmake_minimum_required(VERSION 3.10.2) 定义了编译所需的CMake最低版本号,必须高于这个最低版本号。
  4. CMake会按照CMakeLists.txt文件中的配置信息来进行库的编译。

CMakeLists文件基本配置介绍

perl 复制代码
//要求CMake的最小版本
cmake_minimum_required(VERSION 3.10.2)

# Declares and names the project.
# 当前项目的名称
project("MyFecDemo")

#配置要被编译入库中的代码文件
add_library( # Sets the name of the library.
             # 本地lib库的名称
             native-lib

             # Sets the library as a shared library.
        	 # 设置本地库的权限 SHARED属于动态库,STATIC属于静态库
             SHARED

             # Provides a relative path to your source file(s).
             native-lib.cpp 
             # 如果有新增的文件,需要使用到,那么必须在此继续注册,如新增native-lib02.cpp
             native-lib02.cpp
             # 支持添加多个.cpp、.a、.so文件。
           )

#查找三方库
find_library( # Sets the name of the path variable.
              # 给找到的库取的别名为 log-lib ,log-lib是一个变量名。
              log-lib

              # Specifies the name of the NDK library that
              # you want CMake to locate.
              # 从系统环境变量配置的系统库路径中查找liblog.so库
              # 库名叫做log,但是我们编译出来的so库文件会被默认在库名前加上'lib'前缀。
              # 所以编译器去查找log库时,实际找的是liblog.so文件。
              log )

#将查找到的三方库链接到当前库
target_link_libraries( # Specifies the target library.
                      //被链接到 native-lib库中去。
                       native-lib

                       # Links the target library to the log library
                       # included in the NDK.
                      #将find_library得到的库的别名通过 ${} 取出来,然后将这个库链接到native-lib中。
                       ${log-lib} )

可以看第一章节的CMake编译部分复习巩固。

JNI函数语法介绍

以下列函数为例:

c 复制代码
extern "C" //表示以C的语法来编译
JNIEXPORT jstring JNICALL   //jstring 表示返回值为jstring->string类型。
Java_com_xunua_MyFecDemo_NativeLibManager_getString(JNIEnv *env, jobject thiz) {//参数的顺序不可以随意修改。 JNIEnv *env 一定永远是每个jni函数的第一个参数。
    string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}

C++与java的值传递

C++字符串是C++的对象。JAVA字符串是JAVA的对象。两者是在不同区域的。JAVA的对象在方法区。C++的对象在栈区。

所以如果要将C++栈区的String字符串传递给Java,那么就需要一个转换器;首先将C++的字符串转成C的字符数组,之后再通过转换器将C的字符数组存储到Java的方法区,再将这个java方法区的字符串引用返回给到java代码。

代码中的hello.c_str()是将C++的字符串转成C的字符数组(因为C++的String结构比较复杂,所以转成C的字符数组结构更简单,在内存中读取/转换更简单);这里的转换器就是JNI代码中的env,"env->NewStringUTF"它将C的字符数组转为Java字符串后存储到Java的方法区,然后返回Java方法区中该字符串的引用。接着通过jni函数将JAVA字符串的引用返回给到java代码。

extern"C"

C++高级语言,使用gcc编译器。与c使用的编译器不一样的。所以添加extern"C"的目的是为了告知编译时使用C的编译器来编译。

如果不给jni函数声明extern"C",那么程序在运行该函数时,会崩溃,报错信息为:No implementation found for java.lang.String.xxx.xxxx.func2() 在C++中没有发现该方法的实现。

上述错误的几种出现原因:①对应的jni函数上没有添加extern "C"。 ②在java代码中没有添加static{ System.loadLibrary("libname")} 加载库。 ③没有在build文件中声明externalNativeBuild属性。 ④apk内的lib文件夹下没有对应库(没有被编译进去apk中)

不添加 extern"C" **导致 No implementation found for java.lang.String.xxx.xxxx.func2() 错误的原因:

**因为C不支持重载,所以Java调用JNI函数时,编译器是根据JNI的函数名去匹配的。但是C++支持重载,所以Java调用JNI函数时,编译器会根据JNI的函数名+参数一起去匹配,所以出现找不到对应JNI函数的异常。

JNIEXPORT

JNIEXPORT jstring JNICALL

JNIEXPORT是一个宏,它其实是__attribute__ ((visibility ("default")))。

arduino 复制代码
#define JNIEXPORT  __attribute__ ((visibility ("default")))

attribute ((visibility ("default"))) :
attribute:方法属性

visibility:可见性

"default":表示默认值(等价于为public,允许我们的java层去访问)。 取值:"hidden"则表示外部不可见,等价于private。

jstring:表示当前jni函数的返回值类型为string。

JNICALL:在源码中是jni的空实现,可以没有。

JNI函数的参数类型

scss 复制代码
//Java声明的非静态本地函数(native void)在JNI函数中的参数列表
(JNIEnv *env, jobject thiz) 
  • 参数的顺序不可以随意修改。
  • *JNIEnv env 一定永远是每个jni函数的第一个参数。JNIEnv 其实是 _JNIEnv 结构体的别名。
    这个 _JNIEnv 结构体内部定义了许多的函数,这些函数的作用基本上都是为了用于获取Java那边的内容。
    Java主线程中调用jni函数是,env都是同一个。
    如果在子线程或者其他不同的线程调用,那么jni函数收到的env不是同一个,属于不同线程。
    所以在不同现场执行jni函数时,需要在jni函数中切换线程去处理(后面补充)。
  • thiz 表示Java中定义该函数的实例对象

静态本地函数(static native void) 的jni函数中参数列表有些变化,会变为:

kotlin 复制代码
(JNIEnv *env, jclass clazz) 

其中第二个参数由jobject变成了jclass。

  • cLazz ****指的不再是Java中定义该函数的实例对象,而是指的定义该函数的Java类(非对象)。

C++主动访问JAVA对象(C++反射)

C++中访问java对象其实是通过反射去实现访问的。 Java层的反射API其实本质上是调用C++的native函数去实现的,所以C++中的反射可以直接调用native函数去实现,会比Java的反射要更加直接,减少了一层调用链。

比如在Java层中定义了两个成员,并声明一个native函数:

arduino 复制代码
public String mName="Test";
public int num=0;

public native void func3();

JNI层实现func3(),并在函数中做反射去修改mName、num。

ini 复制代码
extern "C"
JNIEXPORT void JNICALL
Java_com_xunua_MyFecDemo_MainActivity_func3(JNIEnv *env, jobject thiz) {
    jclass clazz=env->GetObjectClass(thiz);
    //拿到java中两个成员的id
    jfieldID mName = env->GetFieldID(clazz,"mName","Ljava/lang/String;");
    jfieldID num = env->GetFieldID(clazz,"num","I");
    //修改他们的值
    env->SetObjectField(thiz,mName,env->NewStringUTF("哈哈哈,被JNI改了"));
    env->SetIntField(thiz,num,50);
}

JNI实现回调Java方法

在java层创建普通函数,JNI通过反射来调用该Java函数,从而实现回调:

arduino 复制代码
/**
 * 创建普通函数,用于native来调用当前函数。实现回调。
 * @param code返回的状态码
 */
public void onCallback(int code){
    Toast.makeText(this, "被native回调了,code:"+code, Toast.LENGTH_SHORT).show();
}

因为要模拟JNI去回调Java层的回调函数,所以Java层还需要再写一个native函数来模拟触发JNI函数来回调:

csharp 复制代码
public native void func4();

JNI函数实现func4:

scss 复制代码
extern "C"
JNIEXPORT void JNICALL
Java_com_xunua_MyFecDemo_MainActivity_func4(JNIEnv *env, jobject thiz) {
    //获取Java对象
    jclass clazz=env->GetObjectClass(thiz);
    //获取函数ID
    jmethodID onCallbackId = env->GetMethodID(clazz,"onCallback","(I)V");//通过函数签名信息 "(I)V" 来确保调用的函数准确性。
	env->CallVoidMethod(thiz,onCallbackId,123);
}

当java层调用native函数func4时,JNI层会主动去调起Java层的onCallback回调函数,并返回状态码。

这样就完成了一次模拟JNI回调Java函数的过程。

调用流程:Java.func4() -> JNI.func4() -> java.onCallBack()

签名信息Sig

上述的调用GetMethodID获取函数ID、GetFieldID获取成员变量ID时,最末尾的实参传入了sig字符串 ,那个就是对象的签名信息 ,jni就是通过签名信息来判断成员的数据类型、函数的入参与返回值;用以准确的查找/匹配正确的目标成员。签名信息在第一章的JNI函数签名sig信息章节有讲过。

FAQ-01

  1. java字符串为什么在方法区:JVM方法区主要存储:class的字节码、字符串、常量池。
  2. java调用的JNI方法都需要加 extern "C",后面如果使用动态注册的话就可以不加extern "C"。
  3. 在C语言的JNI函数中,有定义返回值类型的JNI函数,如果不return返回值,那么也可以编译通过,但是运行执行该函数时会报错崩溃。
  4. jni方法中参数无法传接口。因为java和jni是不同的东西,jni没有对java接口的支持。但是JNI可以通过反射去实现回调Java函数。

纯Java项目中编译JNI

我们在Android项目中因为有Android Studio帮助我们去处理了JNI的头文件函数生成、SO的编译等过程,所以我们在Android Studio进行JNI开发的过程时会相对轻松。所以这次我们要在Java项目中去实现JNI开发,在这个过程中,我们需要手动使用javah生成C++头文件,手动调用GCC指令来编译so库。

参考上一篇文章:Java中实现JNI(不借助Android Studio)

Native函数的调用流程

Java层中执行static{System.loadlibrary("")}加载so库时,会在虚拟机中生成一个函数映射表:

一般情况下,JNI(Java Native Interface)库中的函数是通过JNI函数表进行映射的。这个JNI函数表存储了Java中的方法和本地方法之间的映射关系。在Java本地库加载后,JNI函数表会被填充,以便Java虚拟机可以通过JNI调用本地方法。

虚拟机本身是一个C++的可执行文件,叫做libart.so(所有的安卓系统源码会被编译成libart.so);每次程序调用Java函数时都会走到libart.so

在调用函数时,会通过dlopen("libart.so") 去hook虚拟机的.so,接着查找对应的函数,如果是普通函数,则从Java中找;如果为native函数,则通过映射表找。

接着虚拟机从映射表中根据函数名去找到对应函数地址并执行。

因为Java声明的native函数名是很简短的,但是在JNI中定义该函数时,函数名是有很多前缀的。那么Java层声明的Native函数名 是如何 与JNI实现的函数名去对应:

  1. 首先必须在Java代码中实现库的加载loadlibrary,用于生成本地函数的映射表。
  2. Java层调用Native函数时,Java层声明的函数名会主动的拼接 包名+当前类型+函数名。然后通过拼接后的字符串去映射表中找到对应的JNI函数地址。
相关推荐
数据猎手小k22 分钟前
AndroidLab:一个系统化的Android代理框架,包含操作环境和可复现的基准测试,支持大型语言模型和多模态模型。
android·人工智能·机器学习·语言模型
你的小101 小时前
JavaWeb项目-----博客系统
android
风和先行1 小时前
adb 命令查看设备存储占用情况
android·adb
AaVictory.2 小时前
Android 开发 Java中 list实现 按照时间格式 yyyy-MM-dd HH:mm 顺序
android·java·list
似霰3 小时前
安卓智能指针sp、wp、RefBase浅析
android·c++·binder
大风起兮云飞扬丶3 小时前
Android——网络请求
android
干一行,爱一行3 小时前
android camera data -> surface 显示
android
断墨先生3 小时前
uniapp—android原生插件开发(3Android真机调试)
android·uni-app
无极程序员5 小时前
PHP常量
android·ide·android studio
萌面小侠Plus6 小时前
Android笔记(三十三):封装设备性能级别判断工具——低端机还是高端机
android·性能优化·kotlin·工具类·低端机