回顾
JNI 要点
- Java 要 load 库
- Java 要把方法声明为 native 的
- C 语言实现 Java 的 native 方法时,函数名有固定形式,可以用 javah 生成头文件得到函数名
- Java 调用 C 函数时,JVM 会自动传两个参数下去: 分别是 JNIEnv * 和 jobject 类型的
层:Java -> JNI -> Native
- 分三层
- 最上面是 Java 层
- 中间是JIN层,C语言编写,以.so形式存在
- 最下是Native层,C语言编写,以.so形式存在
- 举例
- Java(MediaScanner) —> JIN(libmedia_jin.so) —> Native(libmedia.so)
- jni 层和 Native 层虽然都是 C/C++ 写的,但还是有区别的:
- Native 层完全不知道有 Java 层的存在,是完完全全的C/C++语言写程序。
- JNI 层的C/C++函数的第一个参数都是 JNIEnv* env,函数可以通过这个参数跟Java层交互。
JNIEnv
- Java 传给 C 的第一个参数是 JNIEnv* 类型的,下面我们就看看 JNIEnv 是什么
JNIEnv 是什么
- JNIEnv 是个结构体,这个结构体里,全都是函数指针,一共有300个左右。
- 这些函数指针形如:
jclass (*FindClass)(JNIEnv*, const char*);
jint (*CallIntMethodV)(JNIEnv*, jobject, jmethodID, va_list);
void (*CallVoidMethodA)(JNIEnv*, jobject, jmethodID, jvalue*);
void (*SetIntField)(JNIEnv*, jobject, jfieldID, jint);
jbyteArray (*NewByteArray)(JNIEnv*, jsize);
const char* (*GetStringUTFChars)(JNIEnv*, jstring, jboolean*);
- JNIEnv* 是 java 虚拟机传的
- 也就是说是 java 虚拟机初始化了一个 JNIEnv 结构体,把300个函数指针都初始化了。
- 但函数指针指向的都是C的函数,所以这是Java虚拟机干的事儿。Java虚拟机作为Java世界和C世界的桥梁,这也是体现之处。
- 当你愉快的调用
(*env)->FindClass(env, "java/lang/String")
时,虽然这是一个C函数,但它是 java 虚拟机初始化的。 - 总之, JNIEnv 是 Java 虚拟机为 C 层准备的一系列函数的集合。
C 和 C++ 中 JNIEnv 的区别
- jni 层函数的第一个参数都是 JNIEnv* env, 无论C还是C++
- 但是这个 JNIEnv 对于C和C++是不同的,看一下 jni.h 中的定义(来自 NDK r13b):
#if defined(__cplusplus)
/* 在C++中,JNIEnv 等同于 _JNIEnv,而_JNIEnv是一个结构体,一会儿下面会有代码 */
typedef _JNIEnv JNIEnv;
typedef _JavaVM JavaVM;
#else
/* 在C语言中,JNIEnv 是个指向结构体的指针,指向的是 JNINativeInterface 类型的结构体 */
typedef const struct JNINativeInterface* JNIEnv;
typedef const struct JNIInvokeInterface* JavaVM;
#endif
...
// 里面定义的全是函数指针
struct JNINativeInterface
{
jclass (*FindClass)(JNIEnv*, const char*);
}
...
// 里面定义的全是函数指针
struct _JNIEnv
{
// C++ 的 JNIEnv 是个 wrapper,其函数调用还是 JNINativeInterface 的函数
const struct JNINativeInterface_ *functions;
jclass FindClass(const char *name) {
return functions->FindClass(this, name);
}
}
- 区别1:对于C语言,使用
(*env)->
来调用函数;对于 C++ 用env->
。 - 区别2:对于C语言,使用
(*env)->xxx(env, ...)
来调用函数;对于 C++ 用env->()
,即 C 语言第一个参数还要把 env 写上去。
动态注册
什么是动态注册
- 目前我们知道,JNI 层的函数名,一定得叫
Java_包名_类名_函数名
- 这是因为,当 Java 调用 native 方法时,会去 so 库里搜索,搜索的函数名就是这么约定的。
- 如果不按这种规则命名,也有方法能让 Java 找到对应的 C/C++ 函数,就是动态注册
- 动态注册是 JNI 层主动告诉 Java 层函数对应关系,让 Java 层不必去库里搜索。
- 动态注册除了有缩短函数名,不借助 javah 工具这两个好处,还有提升效率的好处,因为可以避免搜索。
动态注册的原理
- Java 在调用 C/C++ 之前,肯定要加载 C/C++ 的库,即
System.loadLibrary()
- 在加载库的时候,JVM 会去被加载的库中寻找函数
JNI_OnLoad(JavaVM* jvm, void* reserved)
,如果找到了就调用它。它是一个 C/C++ 函数,工作在 JNI 层。 - 我们就在这个函数里实现动态注册。即我们在 JNI 代码里实现这个函数,在函数体内完成动态注册。
JNI_OnLoad()
- 在实现这个函数之前,先来看看它的参数:
JNI_OnLoad(JavaVM* jvm, void* reserved)
- 第二个参数从名字就能看出来,是保留参数,暂不讨论。
- 第一个参数是
JavaVM*
类型的。而普通的 JNI 函数的第一个参数都是JNIEnv*
类型的。
JavaVM 和 JNIEnv
- JavaVM 是进程相关的,JNIEnv 是线程相关的
- 在 Android 里,每个进程只有一个 JavaVM(DalvikVM) 的实例
- Android 中每当一个 Java 线程第一次要调用本地 C/C++ 代码时,Dalvik 虚拟机实例会为该 Java 线程产生一个 JNIEnv* 指针;
- Java 每条线程在和 C/C++ 相互调用时,JNIEnv* 是相互独立的,互不干扰,这就提升了并发执行时的安全性;
- 当本地的 C/C++ 代码想获得当前线程所想要使用的 JNIEnv 时,可以使用 Dalvik VM 对象的
JavaVM* jvm->GetEnv()
方法,该方法即会返回当前线程所在的 JNIEnv*。
JNI_OnLoad(JavaVM* jvm, void* reserved)
会把虚拟机对象传递到 JNI 层,这几乎是整个 JNI so 库唯一一次获得 JVM 指针的机会。
所以一般正经的 Native Code 的 jni 层,都会实现JNI_OnLoad()
函数,并且在函数里用全局变量把 JavaVM* 保存下来,以便以后使用。
关于Android
- 可以不准确的理解为:一个 Android app 就是一个 Android Linux 上的进程
- 在 Android 里,可以简单的理解为:一个进程对应一个 Dalvik 虚拟机。Java 的 dex 字节码和 c/c++ 的 so 库同时运行这个进程之内。
- Dalvik 虚拟机当然已经实现了JNI标准,所以在 Dalvik 虚拟机加载 so 库时,会先调用
JNI_Onload()
动态注册的实现
- Java 虚拟机已经准备好了函数,专门用于动态注册:
(*env)->RegisterNatives(env, clazz, gMethods, numMethods)
- 我们在
JNI_OnLoad()
里调用它即可 - 其中 gMethods 是一个数组,数组里放的都是 JNINativeMethod 类型的变量
- JNINativeMethod 这个结构体如下:
typedef struct{
//Java 中native函数名,不用包含路径,比如 processFile
const char* name;
//Java 函数的签名信息,用字符串表示,是参数类型和返回值类型的组合
const char* signature;
//JNI 层对应的函数的指针
void* fnPtr;
}JNINativeMethod;
- 这个结构体就是把 Java 层的函数名和 JNI 层的函数指针对应起来的
- 所以,把实现了的 Java 层的函数的指针和 Java 层函数名对应写好,封装到这个结构体里
- 再把所有这样的结构体放到数组 gMethods 里
- 就可以用 RegisterNatives() 进行函数注册了
- 因为 JNINativeMethod 结构体里只写 Java 里的函数名,不包含路径,所以不知道这个函数是哪个类的
- 所以 RegisterNatives() 需要一个参数 clazz,这是 jclass 类型的,代表着java类
- 注册过的函数就有了对应关系了,当 Java 层调用 native 函数时,直接就能找到 JNI 层的对应的函数指针了
- JNINativeMethod 这个结构体里之所以需要一个函数签名信息,是因为 Java 支持函数重载
- 只有把参数返回值都确定了,才能找到对应的到底是哪个 Java 层函数
动态注册小结
- 动态注册是 C/C++ 世界告诉某个 Java 虚拟机:我的这个函数,是给这个 Java 类用的,它对应该类的这个 native 方法。
- 注意: C/C++ 是把这种对应关系告诉了虚拟机,而不是告诉某个 Java 类,即不是告诉了 Java 程序员。跟静态注册一样,只有虚拟机需要关心 java 层的方法和 jni 层的函数之间的对应关系,而不是 Java 程序员需要关心。
- 注意:C/C++ 里的一个函数,对应的是 Java 里的某个类的某个方法。Java里必须有类。
- 因为动态注册的过程,是 C/C++ 跟虚拟机对话的过程,所以 C/C++ 必须先拿到那个虚拟机才能进行注册。在 C/C++ 世界,Java 虚拟机的代表就是这两个结构体变量:JavaVM, JNIEnv。
Android 动态注册的一般流程
- 先把实现了 Java 层 native 函数的那些函数的指针都封装到 JNINativeMethod 数组里
static JNINativeMethod gMethods[]={
{
"processFile",
"(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V",
(void*)android_meida_MediaScanner_processFile
},
{
"native_init",
"()V",
(vodi*)android_meida_MediaScanner_native_init
}
};
- 通过方便函数
AndroidRuntime::registerNativeMethods(env,"android/media/MediaScanner",gMethods,ELEN(gMethods))
来注册 - 而不是直接使用
(*env)->RegisterNatives(env,clazz,gMethods,numMethods)
- 因为方便函数的第二个参数直接传字符串就可以了,不用先通过字符串生成一个 jclass 类型再传
在JNI层操作Java层的东西
调用Java层的方法
- 通过
jclass clazz = env->FindClass("含有路径的类名");
找到类 - 通过
jmethodID mid = env->GetMethodID(clazz,"方法名","方法签名信息");
找到Java层方法的ID
- 注意 jmethodID 是一个专门记录 Java 层方法的类型
- 类似的还有一个 jfieldID
- 通过
env->CallxxxMethod(jobj,mid,param1,param2...);
调用 Java 层的方法
- CallxxxMethod 中的 xxx 是 Java 方法的返回值类型,比如 CallVoidMethod,CallIntMethod
- 第一个参数是指调用哪个对象的方法,就是 Java 中
.
前面的那个对象 - 第二个参数 Java 中的 MethodID
- 后面的参数就是 Java 方法的参数了,其类型都要是 java 中能处理的类型,比如 jstring,jint,jobject
get和set Java层的field
- 通过
jclass clazz = env->FindClass("含有路径的类名");
找到类 - 通过
jfieldID fid = env->GetFieldID(clazz,"成员名","成员类型标示");
找到Java层成员变量的ID - 通过
GetxxxField(env,obj,fid);
/SetxxxField(env,obj,fid,value);
来get/set相应的成员变量
从无到有: The Invocation API
- 以上方法都是 Java 主动调用了 native 代码之后,native 代码拿着 JNIEnv 去操作 Java 层的东西
- 如果现在连 JVM 都没有,C/C++ 世界能不能使用 Java 世界的代码呢?
- 就像 JAVA 可以通过 loadLibrary 把 C 库加载到内存,然后使用其中的方法一样。C/C++C 也可以先启动一个 JVM,然后使用 Java 的方法。
- 这要用到 The Invocation API。
hello world
- 目的: 一个 C/C++ 写的可执行程序,在代码里使用已经编译好的 class 文件里的功能。
- 下面的例子来自JNI官方文档第5章,本文介绍具体怎么把这个例子运行起来。
1. JAVA Code
public class JavaApp{
public static void javaMethod(){
System.out.println("I have done a lot of things by Java!");
}
}
2. 编译 java:
javac JavaApp.java
直接生成 JavaApp.class 没什么好说的。
3. CPP Code
#include <jni.h>
int main()
{
JavaVM *jvm; /* denotes a Java VM */
JNIEnv *env; /* pointer to native method interface */
JavaVMInitArgs vm_args; /* JDK/JRE 6 VM initialization arguments */
vm_args.version = JNI_VERSION_1_6;
vm_args.ignoreUnrecognized = false;
JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args);
jclass cls = env->FindClass("JavaApp");
jmethodID mid = env->GetStaticMethodID(cls, "javaMethod", "()V");
env->CallStaticVoidMethod(cls, mid);
jvm->DestroyJavaVM();
}
4. 编译CPP
- 加上-I选项,让gcc能找到 jni.h 和 jni_md.h:
-I/usr/lib/jvm/java-8-openjdk-amd64/include -I/usr/lib/jvm/java-8-openjdk-amd64/include/linux
- 加上连接器选项,让ld能找到 libjvm.so:
-L/usr/lib/jvm/java-8-openjdk-amd64/jre/lib/amd64/server CppApp.cpp -ljvm
注意: gcc 在做链接时会严格的按照从左到右的顺序,如果 -lxxx 这个库的左边并没有任何东西需要它,那么这个 -lxxx 会被忽略。 因为我们 CppApp.cpp 编译出来的 .o 是需要 libjvm.so 的,所以我们的 -ljvm 一定要出现在 CppApp.cpp 的右边
参考: http://stackoverflow.com/questions/16860021/undefined-reference-to-jni-createjavavm-linux - 最后,完整的编译命令:
gcc -I/usr/lib/jvm/java-8-openjdk-amd64/include -I/usr/lib/jvm/java-8-openjdk-amd64/include/linux -L/usr/lib/jvm/java-8-openjdk-amd64/jre/lib/amd64/server -o app CppApp.cpp -ljvm
5. 运行
- 直接运行 app 会报错链接不上 libjvm.so 这个库。
- 因为安装完 openJDK8,操作系统并不能找到 libjvm.so。 可以运行
sudo ldconfig -v | grep jvm
验证一下,果然啥也没有。 - 我们需要自己配置一下,利用 ldconfig 工具。
- 具体做法是: 在目录
/etc/ld.so.conf.d/
里添加一个文件: openJDK.conf,这个文件里写上:/usr/lib/jvm/java-8-openjdk-amd64/jre/lib/amd64/server
。 这就是 libjvm.so 所在的目录。 然后运行一下 ldconfig 即可。因为 ldconfig 运行的时候,会去加载目录/etc/ld.so.conf.d/
里的所有 .conf 文件。 - 搞定后直接运行
./app
,会打印出 Java 层的输出,大功告成。
总结
- 至此,我们已经成功运行了该例子,现在来总结一下Native程序到底是怎么调到java的功能的。
- 想使用 java code 实现的功能,首先我们得搞一个 JVM 出来,这就是:
JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args);
- 这个函数是在 libjvm.so 里实现的,此函数是 JNI 标准的一部分,所以随JDK发布。
- 这个函数能创建一个 JVM 对象,该对象肯定也是活在我们的 Native 进程里的。
- 这个函数中创建 JVM 的同时,把 JNIEnv 也返回给我们了,即第二个参数。
- 我们拿着 JNIEnv 就可以随便搞了,像获取 Java 类,获取 methodID,调用 java method 的什么的,都不是新鲜事了。
其他零星
jstring 要手动释放
- JNI层的jstring要手动释放,这和jstring内部实现有关
- 用
char *cString = env->GetStringUTFChars(jstring javaString, NULL)
能从jstring类型得到C语言的字符串(char*) - 用
jstring javaString = env->NewStringUTF(const char* cString)
能从C语言的字符串得到jstring的类型 - 以上两种方法调用之后,都要调用
env->ReleaseStringUTFChars(jstring javaString, char* cString)
来释放 - 否则会导致JVM内存泄露
JNI类型签名
- Java 类型对应到 C/C++ 里都有个对应的类型标示。 比如 Java 的 long,在C/C++里用 “J” 做类型标示。
函数签名信息的格式是:
(参数1类型标示参数2类型标示...参数n类型标示)返回值类型标示
可以用 javap 工具生成函数签名信息
logcat
- jni c 语言层往 logcat 里写 log
要点:
1. #include <android/log.h>
2. Android.mk 里:LOCAL_LDLIBS := -llog
3. __android_log_print(ANDROID_LOG_DEBUG, "YourTag", "Your log here %s", a_c_string_var);
4. 上述函数能把log写入logcat,第一个参数是log级别,第二个是Tag,第三个是log的内容。并且第三个参数可以按照print()的方式进行格式化字符串。
参考文档
- 《深入理解Android卷一》 第二章
- JNI 官方文档第5章: http://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/invocation.html