一、前言
JNI 即 Java 本地接口,是 Java 调用 Native 语言的一种特性。Java 调用 C/C++ 是 Java 语言里面本来就有的,并非是 Android 自创。在新版的 Android Studio 中通过 CMake 工具来构建编译,过程比较简单不再赘述。本文着重记录笔者在实际应用中所遇到的问题。
二、类型转换
Java | Native | Signature |
---|---|---|
byte | jbyte | B |
char | jchar | C |
double | jdouble | D |
float | jfloat | F |
int | jint | I |
short | jshort | S |
long | jlong | J |
boolean | jboolean | Z |
void | void | V |
对象 | jobject | L+classsname+; |
Class | jclass | Ljava/lang/Class; |
String | jstring | Ljava/lang/String; |
Throwable | jthrowable | Ljava/lang/Throwable; |
Object[] | jobjectArray | [L+classname+; |
byte[] | jbyteArray | [B |
char[] | jcharArray | [C |
double[] | jdoubleArray | [D |
float[] | jfloatArray | [F |
int[] | jintArray | [I |
short[] | jshortArray | [S |
long[] | jlongArray | [J |
boolean | jbooleanArray | [Z |
Java 中是有重载方法的,即方法名相同但是参数不同,因此仅仅通过方法名是无法找到 Java 中对应的具体方法,Signature ,方法签名便是起这个作用的;
假如在 Java 中定义如下:
public native void reportData(String data,String tag);
则它在 JNI 中的方法签名为 (Ljava/lang/String;Ljava/lang/String;)V,括号内为 native
方法中的参数对应的签名, V 代表方法返回 void ;
三、静态注册
Java 通过不同的方式来找到相应的 Native 方法,这是由注册native函数的具体方法来决定;
例如当Java调用native方法 native_init 时,就会从 JNI 中寻找 Java_com_Example_jnidemo_native_init 函数,没有就报错,如果找到就会为 native_init 和 Java_com_Example_jnidemo_native_init 建立关联,其本质就是保存 JNI 的函数指针,这样再次调用 native_init 方法时直接使用这个函数指针就可以了。静态注册就是根据方法名将Java方法和JNI函数建立关联,当然这种方式有一些缺点:
- JNI 层的函数名过长
- 声明Native方法的类需要使用javah生成头文件(CMake方式不用)
- 初次调用Native方法时需要建立关联,影响效率
四、动态注册
通过查看 Framework 层代码,发现在系统的 Native 层几乎用的都是动态注册的方式,所谓 动态注册 就是在调用 Native 方法之前就已经知道它在 JNI 中对应的函数指针,这需要用到一个结构体用来描述两者之间的关系:
libnativehelper/include_jni/jni.h
typedef struct {
const char* name;
const char* signature;
void* fnPtr;
} JNINativeMethod;
name: Java方法的名字
signature:Java方法的签名信息
fnPtr: JNI中对应的方法指针
动态注册的步骤是:在 JNI 中实现 JNI_OnLoad 函数,
这里以 GnssLocationProvider.cpp 为例,其路径为:
/framework/base/services/core/jni/com_android_server_location_GnssLocationProvider.cpp
1、构建关系数组
static const JNINativeMethod sMethods[] = {
/* name, signature, funcPtr */
{"class_init_native", "()V", reinterpret_cast(
android_location_GnssLocationProvider_class_init_native)},
{"native_is_supported", "()Z", reinterpret_cast(
android_location_GnssLocationProvider_is_supported)},
{"native_is_agps_ril_supported", "()Z",
reinterpret_cast(android_location_GnssLocationProvider_is_agps_ril_supported)},
{"native_is_gnss_configuration_supported", "()Z",
reinterpret_cast(
android_location_gpsLocationProvider_is_gnss_configuration_supported)},
......
};
2、注册JNI函数
int register_android_server_location_GnssLocationProvider(JNIEnv* env) {
jniRegisterNativeMethods(
env,
"com/android/server/location/GnssBatchingProvider",
sMethodsBatching,
NELEM(sMethodsBatching));
jniRegisterNativeMethods(
env,
"com/android/server/location/GnssGeofenceProvider",
sGeofenceMethods,
NELEM(sGeofenceMethods));
jniRegisterNativeMethods(
env,
"com/android/server/location/GnssMeasurementsProvider",
sMeasurementMethods,
NELEM(sMeasurementMethods));
jniRegisterNativeMethods(
env,
"com/android/server/location/GnssNavigationMessageProvider",
sNavigationMessageMethods,
NELEM(sNavigationMessageMethods));
return jniRegisterNativeMethods(
env,
"com/android/server/location/GnssLocationProvider",
sMethods,
NELEM(sMethods));
}
定义在 libnativehelper/include/nativehelper/JNIHelp.h
#ifndef NELEM
# define NELEM(x) ((int) (sizeof(x) / sizeof((x)[0])))
#endif
3、实现 JNI_OnLoad 函数
GnssLocationProvider.cpp 比较特殊,此函数的实现是在同级目录下的 onLoad.cpp 中:
extern "C" jint JNI_OnLoad(JavaVM* vm, void* /* reserved */)
{
JNIEnv* env = NULL;
jint result = -1;
if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
ALOGE("GetEnv failed!");
return result;
}
ALOG_ASSERT(env, "Could not retrieve the env!");
register_android_server_broadcastradio_BroadcastRadioService(env);
register_android_server_broadcastradio_Tuner(vm, env);
register_android_server_PowerManagerService(env);
register_android_server_SerialService(env);
register_android_server_InputApplicationHandle(env);
register_android_server_InputWindowHandle(env);
register_android_server_InputManager(env);
register_android_server_LightsService(env);
register_android_server_AlarmManagerService(env);
register_android_server_UsbDeviceManager(env);
register_android_server_UsbMidiDevice(env);
register_android_server_UsbAlsaJackDetector(env);
register_android_server_UsbHostManager(env);
register_android_server_vr_VrManagerService(env);
register_android_server_VibratorService(env);
register_android_server_SystemServer(env);
register_android_server_location_GnssLocationProvider(env);
register_android_server_connectivity_Vpn(env);
register_android_server_connectivity_tethering_OffloadHardwareInterface(env);
register_android_server_devicepolicy_CryptoTestHelper(env);
register_android_server_ConsumerIrService(env);
register_android_server_BatteryStatsService(env);
register_android_server_hdmi_HdmiCecController(env);
register_android_server_tv_TvUinputBridge(env);
register_android_server_tv_TvInputHal(env);
register_android_server_PersistentDataBlockService(env);
register_android_server_HardwarePropertiesManagerService(env);
register_android_server_storage_AppFuse(env);
register_android_server_SyntheticPasswordManager(env);
register_android_server_GraphicsStatsService(env);
register_android_hardware_display_DisplayViewport(env);
register_android_server_net_NetworkStatsService(env);
#ifdef USE_ARC
register_android_server_ArcVideoService();
#endif
return JNI_VERSION_1_4;
}
4、关于 jniRegisterNativeMethods 函数
通过上面的实例中可以看出框架是通过调用 jniRegisterNativeMethods 函数完成 JNI 注册,这个函数被定义在 libnativehelper/JNIHelp.cpp 中:
extern "C" int jniRegisterNativeMethods(C_JNIEnv* env, const char* className,
const JNINativeMethod* gMethods, int numMethods)
{
JNIEnv* e = reinterpret_cast(env);
......
if ((*env)->RegisterNatives(e, c.get(), gMethods, numMethods) < 0) {
char* tmp;
const char* msg;
if (asprintf(&tmp, "RegisterNatives failed for '%s'; aborting...", className) == -1) {
// Allocation failed, print default warning.
msg = "RegisterNatives failed; aborting...";
} else {
msg = tmp;
}
e->FatalError(msg);
}
return 0;
}
最终通过调用 JNIEnv 的 RegisterNatives 函数来完成JNI的注册。因此我们在 Android 环境下去编译自定义的模块时当需要此函数时,需要做如下两件事:
① #include "JNIHelp.h"
② Android.mk 中添加 LOCAL_SHARED_LIBRARIES +=libnativehelper
当然在有些地方使用的是 AndroidRuntime::registerNativeMethods 这种方式,其实这个函数底层调用的还是 jniRegisterNativeMethods 函数,并最终回归到 JNIEnv 的 RegisterNatives 函数。
那么当使用 AndroidRuntime::registerNativeMethods 时也需要做如下两件事:
① #include "android_runtime/AndroidRuntime.h"
② Android.mk 中添加 LOCAL_SHARED_LIBRARIES +=libandroid_runtime
5、JNI_OnLoad 函数
在Java代码中,通过 loadLibrary 要求虚拟机来装载 so 文件,形式如下:
public class JNItest {
static {
System.loadLibrary("jnitest");
}
}
在代码运行时会在 /system/lib 目录下查找 libjnitest.so 文件,加载入虚拟机中,与此同时产生一个 Load 事件,这个事件触发后,程序默认会在载入的.so文件的函数列表中查找 JNI_OnLoad 函数并执行,与"Load"事件相对,当载入的.so文件被卸载时,Unload 事件被触发,此时,程序默认会去在载入的.so文件的函数列表中查找 JNI_OnUnload 函数并执行,然后卸载.so文件。需要注意的是,JNI_OnLoad 与 JNI_OnUnload 这两个函数在.so组件中并不是强制要求。
调用链路如下:
System.loadLibrary()
-> Runtime:loadLibrary0()
-> Runtime:nativeLoad()
-> [Runtime] Runtime.c:Runtime_nativeLoad()
-> [native] Openjdkjvm.cc:JVM_NativeLoad()
-> [Runtime] art::JavaVMExt:LoadNativeLibrary() //已加载则直接返回success,
//否则new SharedLibrary 添加
->[native] android::OpenNativeLibrary() //dlopen
-> [Runtime] SharedLibrary:FindSymbol("JNI_OnLoad", nullptr);
-> [Runtime] SharedLibrary:FindSymbolWithoutNativeBridge()
-> dlsym("JNI_OnLoad");
-> so文件中的JNI_OnLoad函数
System.java : /libcore/ojluni/src/main/java/java/lang/System.java
Runtime.java: /libcore/ojluni/src/main/java/java/lang/Runtime.java
Runtime.c: /libcore/ojluni/src/main/native/Runtime.c
Openjdkjvm.cc: /libcore/ojluni/src/main/native/Openjdkjvm.cc
JavaVMExt: /art/runtime/java_vm_ext.cc
SharedLibrary: /art/runtime/java_vm_ext.cc
五、引用类型
在 JNI 规范中定义了三种引用:局部引用(Local Reference)、全局引用(Global Reference)、弱全局引用(Weak Global Reference)。
1、局部引用(Local Reference)
局部引用,也称本地引用,通常是在函数中创建并使用,函数返回时自动释放,当然可以使用DeleteLocalRef函数手动释放,存在某些场景下 native 函数在返回前占用了大量内存
2、全局引用(Global Reference)
全局引用可以跨方法、跨线程使用,直到被开发者显式释放。类似局部引用,一个全局引用在被释放前保证引用对象不被GC回收。和局部应用不同的是,只能通过 NewGlobalRef 和 ReleaseGlobalRef 两个函数来创建和释放全局引用
3、弱全局引用(Weak Global Reference)
是JDK 1.2 新增加的功能,与全局引用类似,创建跟删除都需要由编程人员来进行,这种引用与全局引用一样可以在多个本地 Native 有效,不一样的是,弱引用将不会阻止垃圾回收期回收这个引用所指向的对象,所以在使用时需要多加小心,它所引用的对象可能是不存在的或者已经被回收,因此在使用前需要使用 JNIEnv 的 IsSameObject 函数来判断
六、SO文件生成及使用
1、流程图
2、Android Studio平台
上图中我从 so 文件的来源分类,第一类是在 Android Studio 平台使用 CMake 来编译构建 JNI 进而生成 so 文件,这其中不需要我们多做其他的什么工作,默认是生成所有架构的 so 文件,当然也可以通过在 build.gradle 文件中配置属性来生成指定的架构:
android{
......
ndk{ // add this
abiFilters "armeabi-v7a", "arm64-v8a"
}
}
Jar 包生成
Android 工程在使用 so 库时,是通过 JNI 的方式进行调用,因此在提供给别人使用 so 库的同时应当还要提供相应的 Jar 包供使用方调用。同时静态注册的这种方式,还存在JNI函数的命名与Java端native声明函数所在的类的包名必须一致的限制,所以必须提供对应的Jar包。配置方式如下:
// module 的 build.gradle 中 与 android 节点同级
task makeJar(type: Copy) {
delete 'libs/gms.jar' //删除已经存在的jar包
from('build/intermediates/compile_library_classes/debug/')//从该目录下加载要打包的文件,注意这个目录,不同版本的AndroidStudio是不一样的,比如在3.0版本是build/intermediates/bundles/release/,要自己去查一下。
into('libs/')//jar包的保存目录
include('classes.jar')//设置过滤,只打包classes文件
rename('classes.jar', 'gms.jar')//重命名,gms.jar 根据自己的需求设置
}
makeJar.dependsOn(build)
Terminal 中执行命令:
./gradlew makeJar
3、纯C/C++,第三方平台
如果将一个C/C++项目,编译成 so 库提供给 Android 使用是怎样的呢?我们知道 JNI 的好处是使得Android 可以使用优秀的 C/C++ 代码,提高性能。但弊端是失去了跨平台的性质,C/C++ 的代码必须编译成不同架构的 so 库来适配设备的差异。CMake 的编译方式请参见这里,这里对 Android.mk 的方式来做说明:
Application.mk:
APP_ABI := all #所有架构
APP_PLATFORM := android-16
Android.mk:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := range
LOCAL_SRC_FILES := PublicCommon.c\
RangeDecoder.c\
RangeEncoder.c
LOCAL_LDLIBS += -llog
LOCAL_SHARED_LIBRARIES := libcutils
include $(BUILD_SHARED_LIBRARY)
bush.sh
#!/bin/bash
PWD=`pwd`
NDK=/home/zsk/Android/Sdk/ndk/20.0.5594570
BUILDER=${NDK}/ndk-build
ABS=${PWD}/Android.mk
NAM=${PWD}/Application.mk
${BUILDER} NDK_PROJECT_PATH=${PWD} APP_BUILD_SCRIPT=${ABS} NDK_APPLICATION_MK=${NAM}
七、JNI相关知识
1、JNIEnv 与 JavaVM
- JavaVM ,是Java虚拟机在JNI层的代表,JNI全局仅仅有一个JavaVM结构中封装了一些函数指针(或叫函数表结构),JavaVM中封装的这些函数指针主要是对JVM操作接口。
- JNIEnv ,是一个线程相关的结构体,该结构体代表了Java在本线程的执行环境
- 区别: JavaVM 全局只有一个,JNIEnv 每个线程都有一个
关于具体的在JNI中各种反射查找类以及各种类型数据的填充,可以参考Gnss的部分源码:
frameworks/base/services/core/jni/com_android_server_location_GnssLocationProvider.cpp
2、jobject 与 jclass
jobject 与 jclass 通常作为JNI函数的第二个参数,当所声明Native方法是 静态方法 时,对应参数 jclass,因为静态方法不依赖对象实例,而依赖于类,所以参数中传递的是一个jclass类型。相反,如果声明的Native方法时 非静态方法 时,那么对应参数是 jobject,这两个数据类型定义在 jni.h 头文件中
参考
[ 1 ] : https://blog.csdn.net/flydream0/article/details/7371692
[ 2 ] : https://www.cnblogs.com/heixiang/p/10987838.html
[ 3 ] : https://www.jianshu.com/p/87ce6f565d37
[ 4 ] : https://www.jianshu.com/p/b4431ac22ec2