Android JNI 使用方法总结及原理分析

一、前言

JNIJava 本地接口,是 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_initJava_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<void *>(
            android_location_GnssLocationProvider_class_init_native)},
    {"native_is_supported", "()Z", reinterpret_cast<void *>(
            android_location_GnssLocationProvider_is_supported)},
    {"native_is_agps_ril_supported", "()Z",
            reinterpret_cast<void *>(android_location_GnssLocationProvider_is_agps_ril_supported)},
    {"native_is_gnss_configuration_supported", "()Z",
            reinterpret_cast<void *>(
                    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<JNIEnv*>(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;
}

最终通过调用 JNIEnvRegisterNatives 函数来完成JNI的注册。因此我们在 Android 环境下去编译自定义的模块时当需要此函数时,需要做如下两件事:

① #include "JNIHelp.h"
② Android.mk 中添加 LOCAL_SHARED_LIBRARIES +=libnativehelper

当然在有些地方使用的是 AndroidRuntime::registerNativeMethods 这种方式,其实这个函数底层调用的还是 jniRegisterNativeMethods 函数,并最终回归到 JNIEnvRegisterNatives 函数。

那么当使用 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

jobjectjclass 通常作为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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,290评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,399评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,021评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,034评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,412评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,651评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,902评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,605评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,339评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,586评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,076评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,400评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,060评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,851评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,685评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,595评论 2 270

推荐阅读更多精彩内容