如何区分安卓项目是java工程还是native工程?
- build文件中是否有externalNativeBuild声明,有则表示属于native工程:
arduino
//native工程的入口
externalNativeBuild{
cmake{
path"src/main/cpp/CMakeLists.txt"
version "3.10.2"
}
}
- 检查代码中是否有 native修饰的本地函数声明。
如何区分生成的lib库是静态库还是动态库
在as中打开output中编译的apk,看lib目录下生成的native库是以何种后缀结尾:
- .so结尾是动态库。(.so理解为插件化)
- .a结尾则是静态库。(.a理解为组件化)
CMake编译
Android Studio中CMake的编译过程
- 程序开始编译时,AS会从build.gradle找externalNativeBuild的cmake配置信息。从配置信息中找到CMakeLists.txt 文件的位置、版本号。
as4.0版本之前的CMakeLists.txt 文件位置是在app-module 下;4.0及之后的版本则是在 /main/cpp文件夹下存放的。 - 将第一步骤得到的信息 交给Android SDK目录里的CMake.exe可执行文件。
- CMakeLists.txt 文件中的cmake_minimum_required(VERSION 3.10.2) 定义了编译所需的CMake最低版本号,必须高于这个最低版本号。
- 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
- java字符串为什么在方法区:JVM方法区主要存储:class的字节码、字符串、常量池。
- java调用的JNI方法都需要加 extern "C",后面如果使用动态注册的话就可以不加extern "C"。
- 在C语言的JNI函数中,有定义返回值类型的JNI函数,如果不return返回值,那么也可以编译通过,但是运行执行该函数时会报错崩溃。
- 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实现的函数名去对应:
- 首先必须在Java代码中实现库的加载loadlibrary,用于生成本地函数的映射表。
- Java层调用Native函数时,Java层声明的函数名会主动的拼接 包名+当前类型+函数名。然后通过拼接后的字符串去映射表中找到对应的JNI函数地址。