温馨提示:这篇文章已超过412天没有更新,请注意相关的内容是否还可用!
摘要:,,本文详细解析了Android中的JNI(Java Native Interface)技术。JNI是Java与本地代码(如C/C++)之间的桥梁,它允许Java应用程序调用本地代码以提高性能或访问特定系统资源。本文介绍了JNI的基本原理、使用方法和注意事项,帮助开发者更好地理解和应用JNI技术,优化Android应用程序的性能和功能。
一、JNI简介

JNI 是Java Native Interface的缩写,表示Java本地调用,通过JNI技术可以实现Java调用C程序和C程序调用Java代码。
二、JNI函数注册

2.1 静态注册:
- 静态注册的方式我们平时用的比较多。我们通过javac和javah编译出头文件,然后再实现对应的cpp文件的方式就是属于静态注册的方式。这种调用的方式是由于JVM按照默认的映射规则来匹配对应的native函数,如果没匹配,则会报错。
- 优缺点: 系统默认方式,使用简单; 灵活性差(如果修改了java native函数所在类的包名或类名,需手动修改C函数名称(头文件、源文件))
//Java 层代码JniSdk.java public class NativeDemo { static { System.loadLibrary("jnisdk"); } public native String showJniMessage(); } //Native层代码 jnidemo.cpp extern "C" JNIEXPORT jstring JNICALL Java_com_dong_example_JnidemoClass_showJniMessage (JNIEnv* env, jobject job) { return env->NewStringUTF("hello world"); }
JNIEXPORT :在Jni编程中所有本地语言实现Jni接口的方法前面都有一个"JNIEXPORT",这个可以看做是Jni的一个标志,至今为止没发现它有什么特殊的用处。 jstring :这个学过编程的人都知道,当然是方法的返回值了,对应java的String类型,无返回值就是void JNICALL :这个可以理解为Jni 和Call两个部分,和起来的意思就是 Jni调用XXX(后面的XXX就是JAVA的方法名)。 Java_NativeDemo_sayHello:这个就是被上一步中被调用的部分,也就是Java中的native 方法名:包名+类名+方法名。 JNIEnv * env:这个env可以看做是Jni接口本身的一个对象,jni.h头文件中存在着大量被封装好的函数,这些函数也是Jni编程中经常被使用到的,要想调用这些函数就需要使用JNIEnv这个对象,例如:env->GetObjectClass()。 jobject obj:代表着native方法的调用者,本例即new NativeDemo();但如果native是静态的,那就是NativeDemo类。
2.2 动态注册
- 动态注册不再按照特定的规则去实现native函数,只要在.c文件里面根据对应的规则声明函数即可,所以我们可以不用默认的映射规则,直接由我们告诉JVM,java的native函数对应的是C文件里面的哪个函数。
- 优缺点: 函数名看着舒服一些,但是需要在C代码中维护Java Native函数与C函数的对应关系; 灵活性稍高(如果修改了java native函数所在类的包名或类名,仅调整Java native函数的签名信息)
//Java 层代码JniSdk.java public class JniSdk { static { System.loadLibrary("jnisdk"); } public static native int numAdd(int a, int b); public native void dumpMessage(); } //Native层代码 jnidemo.cpp 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; if (vm->GetEnv((void**)&env, JNI_VERSION_1_6) != JNI_OK) { return -1; } jclass clazz = env->FindClass("com/dong/example/JnidemoClass"); //第一个参数clazz:是对应的类名的完整路径,(把.换成/) //第一个参数g_methods:是定义的全局变量 //第一个参数2:是g_methods的数组长度,也可以用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_6; }
上面的JNI_OnLoad函数是在我们通过System.loadlibrary函数的时候,JVM会回调的一个函数,我们就是在这里做的动态注册的事情,通过env->RegisterNatives注册。
2.3 这里主要讲解一下g_methods对象,下面是JNINativeMethods结构体的定义
typedef struct { const char* name; //对应java中native的函数名 const char* signature; //java中native函数的函数签名 void* fnPtr; //C这边实现的函数指针 } JNINativeMethod;
三、函数签名
3.1 什么是函数签名:
所谓函数签名,简单点的理解可以理解成一个函数的唯一标识,一个签名对应着一个函数的签名。这个是一一对应的关系。有些人可能会问:函数名不能作为标识么?答案当然是否定的
3.2 为什么需要函数的签名:
我们知道,java是支持函数重载的。一个类里面可以有多个同名但是不同参数的函数,所以函数名+参数名才唯一构成一个函数标识,因此我们需要针对参数做一个签名标识。这样jni层才能唯一识别到一个函数
3.3 如何获取函数的签名
函数的签名是针对函数的参数以及返回值进行组成的。它遵循如下格式(参数类型1;参数类型2;参数类型3…)返回值类型。例如我们上面的numAdd函数一样。他在java层的函数声明是:
public static native int numAdd(int a, int b);
两个参数都是int,并且返回值也是int,所以的函数签名是(II)I。
public native void dumpMessage();
而dumpMessage函数没有任何参数,并且返回值也是空,所以它的签名是()V
3.6 函数类型对应的签名的映射关系:
类型标识 Java类型 Z boolean B byte C char S short I int J long F float D double L/java/language/String String [I int[] [Ljava/lang/object Object[] V void
四、JNIEnv
4.1 JNIEnv介绍
JNIEnv贯穿了整个JNI技术的核心,java层调用native函数主要通过映射关系的建立,但jni函数调用java层的函数就要通过JNIEnv了
4.2 何为JNIEnv
JNIEnv是JVM内部维护的一个和线程相关的代表JNI环境的结构体,这个结构体是和线程相关的。并且C函数里面的线程与java函数中的线程是一一对应关系。也就是说,如果在java里的某个线程调用jni接口,不管调用多少个JNI接口,传递的JNIEnv都是同一个对象。因为这个时候java只有一个线程,对应的JNI也只有一个线程,而JNIEnv是跟线程绑定的,因此也只有一个
4.2 通过JNIEnv调用java对象方法,通过JNIEnv调用方法大致可以分为以下两步:
a、获取到对象的class,并且通过class获取成员属性
b、通过成员属性设置获取对应的值或者调用对应的方法
c、注意如果jni方法是通过static方式调用的话,这边的jobject表示的是jclass对象,需要进行强转,并不表示一个独立的对象
public class JniSdk { private int mIntArg = 5; public int getArg() { return mIntArg; } } void dump(JNIEnv *env, jobject obj) { LOGI("this is dump message call: %p", obj); jclass jc = env->GetObjectClass(obj); jmethodID jmethodID1 = env->GetMethodID(jc,"getArg","()I"); jfieldID jfieldID1 = env->GetFieldID(jc,"mIntArg","I"); jint arg1 = env->GetIntField(obj,jfieldID1); jint arg = env->CallIntMethod(obj, jmethodID1); LOGI("show int filed: %d, %d",arg, arg1); }
4.3 跨线程如何调用java方法
- 上面可以直接调用的原因是java调用到jni层的时候始终都在同一个线程,因此再jni层可以直接操作从java层传递下来的JNIEnv对象来实现各种操作。但是如果是在JNI层创建的一个额外的线程想调用Java方法呢?这个时候又该如何操作呢?
- 一个java线程和一个jni线程共同拥有一个JNIEnv,如果java线程调用native函数的时候,JVM还没有为这两个线程建立起映射关系,那么就会新创建一个JNIEnv并且传递到jni线程
- 如果之前已经有创建过映射关系。那么就直接采用原来的JNIEnv 。如上面所描述的那样,两个JNIEnv的对象是相同的
- 反之也一样,如果jni调用java线程的话,那么需要向JVM申请获取到已经映射的JNIEnv,如果之前未映射过的话。那么就重新创建一个,这个方法就是AttachCurrentThread。
JNIEnv *g_env; void *func1(void* arg) { //进入另一个新线程 //使用全局保存的g_env,进行操作java对象的时候程序会崩溃 jmethodID jmethodID1 = g_env->GetMethodID(jc,"getArg","()I"); jint arg = g_env->CallIntMethod(obj, jmethodID1); //通过这种方法获取的env,然后再进行获取方法进行操作不会崩溃 JNIEnv *env; j_vm->AttachCurrentThread(&env,NULL); } void dumpArg(JNIEnv *env, jobject call_obj, jobject arg_obj) { //打印线程 LOGI("on dump arg function, env :%p", env); g_env = env; pthread_t *thread; pthread_create(thread,NULL, func1, NULL); }
上面表示JNIEnv跟每个线程是捆绑的,无法在线程B访问到线程A的JNIEnv,所以通过保存g_env的方式去使用是不行的。而是应该要通过AttachCurrentThread方法进行获取新的JNIEnv,然后再进行调用。
五、销毁
- java创建的对象是由垃圾回收器来回收和释放内存的,但java的那种方式在jni那边是行不通的。
- 在JNI层,如果使用ObjectA = ObjectB的方式来保存变量的话,这种是没办法保存变量的,随时会被回收,我们必须要通过env->NewGlobalRef和env->NewLocalRef的方式来创建,还有一个env->NewWeakGlobalRef(这种很少使用)
- 两种的生命周期的情况如下:
- NewLocalRef创建的变量再函数调用结束后会被释放掉
- NewGlobalRef创建的变量除非手动delete掉,否则会一直存在
六、JNIEnv操作Java端的代码,主要方法:
函数名称 作用 NewObject 创建Java类中的对象 NewString 创建Java类中的String对象 NewArray 创建类型为Type的数组对象 GetField 获得类型为Type的static的字段 SetField 创建Java类中的对象 GetStaticField 创建Java类中的对象 SetStaticField 设置类型为Type的static的字段 CallMethod 调用返回值类型为Type的static方法 CallStaticMethod 调用返回值类型为Type的static方法 七、Java 、C/C++中的常用数据类型的映射关系表
JNI中定义的别名 Java类型 C/C++类型 jint / jsize int int jshort short short jlong long long / long long (__int64) jbyte byte signed char jboolean boolean unsigned char jchar char unsigned short jfloat float float jdouble double double jobject Object _jobject* jbyteArray byte[] signed byte[] jcharArray char[] unsigned char[] jdoubleArray double[] unsigned double[] jfloatArray float[] unsigned float[] jintArray int[] unsigned int[] jshortArray short[] unsigned short[] jlongArray long[] unsigned long[] jbooleanArray boolean[] unsigned bool[] 八、函数类型对应的签名的映射关系:
Java类型 字段描述符(签名) 备注 int I int的首字母、大写 float F float的首字母、大写 double D double的首字母、大写 short S short的首字母、大写 long L long的首字母、大写 char C char的首字母、大写 byte B byte的首字母、大写 boolean Z 因B已被byte使用,所以JNI规定使用Z object L + /分隔完整类名 String 如: Ljava/lang/String array [ + 类型描述符 int[] 如:[I void V i无返回值类型 Method (参数字段描述符…)返回值字段描述符 int add(int a,int b) 如:(II)I byte[] [B 相比普通类型多了“[”来表示数组 char[] [C 同上… double[] [D … float[] [F … int[] [I … short[] [S … long[] [J … boolean[] [Z … 九、JNI 打印日志
9.1 Cmake文件中有log模块引用,不然编译不通过
# 编译一个库 add_library( native-lib # 库的名字 SHARED # 动态库(.so库) native-lib.cpp # 需要编译的C++文件 ) # 相当于定义一个变量log-lib,引用安卓的打印模块 find_library( log-lib log ) # 将变量log-lib连接到so库(我这边的so库名字是native-lib)中,这样这个库就能使用日志了 target_link_libraries( native-lib ${log-lib} )
9.2 然后在cpp文件中加入:
#include #define TAG "kang" #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__); #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__); #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__);
9.3 使用日志打印:
extern "C" JNIEXPORT jstring JNICALL Java_com_example_jnidemo_MainActivity_stringFromJNI(JNIEnv* env,jobject) { std::string hello = "Hello from C++"; LOGD("jni打印(LOGD)") LOGE("jni打印(LOGE)") LOGI("jni打印(LOGI)") return env->NewStringUTF(hello.c_str()); }
十、完整源码
10.1,静态注册:
Native层:NativeLib.java
package com.bob.nativelib; public class NativeLib { static { System.loadLibrary("nativelib"); } public native String stringFromJNI(); }
C++层:nativelib.cpp
#include #include #include #define TAG "kang" #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__); #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__); #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__); extern "C" JNIEXPORT jstring JNICALL Java_com_bob_nativelib_NativeLib_stringFromJNI( JNIEnv* env, jobject /* this */) { std::string hello = "Hello from C++"; LOGE("Hello from C++"); return env->NewStringUTF(hello.c_str()); }
java调用层
//静态注册 public static void main(String[] args) { NativeLib nativeLib=new NativeLib(); System.out.println(nativeLib.stringFromJNI()); }
10.2,动态注册:C语言
native层:JNITools.java
package com.bob.nativelib; public class JNITools { static { System.loadLibrary("dynamicnativelib"); } public static native int add(int a,int b); }
C层:dynamicnativelib.c
#include "jni.h" //日志打印 #include #define TAG "kang" #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__); #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__); #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__); //加 jint addNumber(JNIEnv *env,jclass clazz,jint a,jint b){ return a+b; } //三个参数,java层函数名,java 层方法签名,C 层方法指针 //获取签名方法: javap -s -p DynamicRegister.class static const JNINativeMethod methods[]={ {"add","(II)I",(void*)addNumber}, }; //java层load时,便会自动调用该方法 JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved){ JNIEnv* env = NULL; //获得 JniEnv int r = (*vm)->GetEnv(vm,(void**)&env,JNI_VERSION_1_6); if(r != JNI_OK){ return -1; } //FindClass,反射,通过类的名字反射 jclass mainActivityCls = (*env)->FindClass(env, "com/bob/nativelib/JNITools");//注册 如果小于0则注册失败 //注册方法 r = (*env)->RegisterNatives(env,mainActivityCls,methods,sizeof(methods)/sizeof(methods[0])); if(r != JNI_OK){ return -1; } return JNI_VERSION_1_6; }
java调用层:
//动态注册 public static void main(String[] args) { //动态注册c库 JNITools jniTools=new JNITools(); System.out.println(String.valueOf(jniTools.add(100,100))); }
10.3,动态注册:C++语言
native层:JNITools2.java
package com.bob.nativelib; public class JNITools2 { static { System.loadLibrary("dynamicnativelib2"); } public static native int add(int a,int b); }
C++层:dynamicnativelib2.cpp
#include //加 jint addNumber(JNIEnv *env,jclass clazz,jint a,jint b){ return a+b; } //三个参数,java层函数名,java 层方法签名,C 层方法指针 //获取签名方法: javap -s -p DynamicRegister.class static const JNINativeMethod methods[]={ {"add","(II)I",(void*)addNumber}, }; //java层load时,便会自动调用该方法 JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved){ //获得 JniEnv JNIEnv *jniEnv{nullptr}; if (vm->GetEnv((void **) &jniEnv, JNI_VERSION_1_6) != JNI_OK) { return -1; } //FindClass,反射,通过类的名字反射 jclass mainActivityCls = jniEnv->FindClass("com/bob/nativelib/JNITools2");//注册 如果小于0则注册失败 //注册方法 jint ret=jniEnv->RegisterNatives(mainActivityCls,methods,sizeof(methods)/sizeof(methods[0])); if (ret != 0) { return -1; } return JNI_VERSION_1_6; }
java调用层:
//动态注册 public static void main(String[] args) { //动态注册c++库 JNITools2 jniTools2=new JNITools2(); System.out.println(String.valueOf(jniTools2.add(100,100))); }
10.4 jin调用java层
native层:TestCallBack.java
package com.bob.nativelib; public class TestCallBack { static { System.loadLibrary("jnitojava"); } //回调方法 里面调用了add public native void callBackAdd(); public int add(int x,int y){ return x+y; } }
C++层:jnitojava.cpp
#include #include #include #define TAG "kang" #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__); extern "C" JNIEXPORT void JNICALL Java_com_bob_nativelib_TestCallBack_callBackAdd(JNIEnv* env,jobject) { //1得到字节码 包名:com.bob.nativelib jclass jclazz = env->FindClass("com/bob/nativelib/TestCallBack"); //2得到方法 jmethodID jmethodIds = env->GetMethodID(jclazz,"add","(II)I"); //3实例化 jobject object = env->AllocObject(jclazz); //4调用方法 jint result= env->CallIntMethod(object,jmethodIds,100,1); //5打印结果 LOGE("result:%d",result); }
java调用层:
public static void main(String[] args) { TestCallBack testCallBack=new TestCallBack(); testCallBack.callBackAdd(); System.out.println(String.valueOf(testCallBack.add(600,600))); }
10.5,完整的CMakeLists.txt
原来的JNI项目是需要自己手动配置的,有了CMake就简单多了,会自动帮我们配置项目,第11节将讲述CMake的优势
# For more information about using CMake with Android Studio, read the # documentation: https://d.android.com/studio/projects/add-native-code.html # Sets the minimum version of CMake required to build the native library. cmake_minimum_required(VERSION 3.10.2) # Declares and names the project. project("nativelib") # Creates and names a library, sets it as either STATIC # or SHARED, and provides the relative paths to its source code. # You can define multiple libraries, and CMake builds them for you. # Gradle automatically packages shared libraries with your APK. # 编译一个库 add_library( # Sets the name of the library. nativelib # Sets the library as a shared library. SHARED # Provides a relative path to your source file(s). nativelib.cpp) add_library( # 库的名字 dynamicnativelib # 动态库(.so库) SHARED # 需要编译的C++文件 dynamicnativelib.c) add_library( # 库的名字 dynamicnativelib2 # 动态库(.so库) SHARED # 需要编译的C++文件 dynamicnativelib2.cpp) add_library( # 库的名字 jnitojava # 动态库(.so库) SHARED # 需要编译的C++文件 jnitojava.cpp) # Searches for a specified prebuilt library and stores the path as a # variable. Because CMake includes system libraries in the search path by # default, you only need to specify the name of the public NDK library # you want to add. CMake verifies that the library exists before # completing its build. find_library( # Sets the name of the path variable. log-lib # Specifies the name of the NDK library that # you want CMake to locate. log ) # Specifies libraries CMake should link to your target library. You # can link multiple libraries, such as libraries you define in this # build script, prebuilt third-party libraries, or system libraries. # 将变量log-lib连接到so库(我这边的so库名字是native-lib)中,这样这个库就能使用日志打印功能了 target_link_libraries( # Specifies the target library. nativelib # Links the target library to the log library # included in the NDK. ${log-lib} ) target_link_libraries( # Specifies the target library. dynamicnativelib # Links the target library to the log library # included in the NDK. ${log-lib} ) target_link_libraries( # Specifies the target library. dynamicnativelib2 # Links the target library to the log library # included in the NDK. ${log-lib} ) target_link_libraries( # Specifies the target library. jnitojava # Links the target library to the log library # included in the NDK. ${log-lib} )
10.5:总结
c库和c++库还是有些区别的,下面要注意的几点,不然编译不通过
a,头部引用
C语言 #include "jni.h" C++ #include
b,JNIEnv指针引用和FindClass函数参数有区别
//C语言,FindClass,反射,通过类的名字反射 jclass mainActivityCls = (*env)->FindClass(env, "com/bob/nativelib/JNITools"); //C++语言,FindClass,反射,通过类的名字反射 jclass mainActivityCls = env->FindClass("com/bob/nativelib/JNITools2");
十一,CMake
10.1 CMake的优势:
- 可以直接的在C/C++代码中加入断点,进行调试
- java引用的C/C++中的方法,可以直接ctrl+左键进入
- 对于include的头文件或者库,也可以直接进入
- 不需要配置命令行操作,手动的生成头文件,不需要配置android.useDeprecatedNdk=true属性
10.2 传统JNI方式步骤:
- 新建jni目录,写好C/C++代码,注册JNI时我们使用了javah -jni对JAVA类进行操作,自动生成了jni目录以及对应的头文件,然后根据头文件写了C/C++代码。
- 在jni目录下创建且配置好Android.mk和Application.mk两个文件。
- build.gradle文件中根据情况进行配置,可不进行配置使用默认值。
- 通过ndk-build操作,我们能得到对应的so文件,放置在相应位置,java代码中即可调用C/C++代码,运行程序。
10.3 CMake方式步骤:
- 新建cpp目录,写好C/C++代码。
- 创建且配置CMakeLists.txt文件。
- build.gradle文件中根据情况进行配置,CMakeLists.txt文件的路径必须配置。
- java代码中即可调用C/C++代码,运行程序。
- project的build.gradle文件中,gradle版本不能低于2.2,否则会报错。
10.4 CMake和传统JNI的主要区别
- 以前的jni目录改成cpp,名字更换了,下面还是存放C/C++文件。
- 之前对C/C++文件的编译配置Android.mk、Application.mk文件放在jni目录下,现在改成CMakeLists.txt文件。事实上这些文件的位置是可任意存放的,只需要配置好就行。但最好还是按照默认习惯放置。
- NewGlobalRef创建的变量除非手动delete掉,否则会一直存在
还没有评论,来说两句吧...