碰到了一个需求。大致要求是需要通过jni底层的C/C++代码调用上层java的函数。为把整个思路列的清楚点,把具体步骤的过程罗列了出来。
花了点时间一些Jni简单的用法总结了一下:
https://blog.csdn.net/DFSAE/article/details/105443135
https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/intro.html
This chapter introduces the Java Native Interface (JNI). The JNI is a
native programming interface. It allows Java code that runs inside a
Java Virtual Machine (VM) to interoperate with applications and
libraries written in other programming languages, such as C, C++, and
assembly.
关于官方的反射说明
Reflection SupportProgrammers can use the JNI to call Java methods or
access Java fields if they know the name and type of the methods or
fields. The Java Core Reflection API allows programmers to introspect
Java classes at runtime. JNI provides a set of conversion functions
between field and method IDs used in the JNI to field and method
objects used in the Java Core Reflection API.
这部分的内容可以参考另一篇:
https://blog.csdn.net/DFSAE/article/details/105443135
上述是JNI调用最简单的过程,简单的JAVA调用C/C++都可以满足了。回到前面的需求来,本以为这个搜索内容会一搜一大把的,结果搜出来的画风是这样的一类代表:
https://www.cnblogs.com/xitang/p/4174619.html
emmmm…这个过程的调用逻辑是JAVA——> JNI接口——> C/C++的jni接口函数——> 在函数里调用JAVA方法。这个和需求不符合,需求要求C/C++主动调用JAVA,而不是通过JAVA上层往下进行控制。
但是看看中间的核心段:
jclass dpclazz = (*env)->FindClass(env,"XXX/XXX/XXX/XXX"); //找到某个类
jmethodID method1 = (*env)->GetMethodID(env,dpclazz,"helloFromJava","()V"); //找到helloFromJava
(*env)->CallVoidMethod(env,obj,method1); //调用
这里的大概逻辑就是先找到类,再找到方法,在调用。javap -p -s javafile可以查看signature。但如果直接照搬就有有几个难以解决的变量,一个是env, 另一个是obj。这两变量都是接口的参数,所以肯定不能直接拿过来用的,所以这里其实就需要后面搞明白这几个变量到底代表什么意思。所以突破口也在这里。
但要先解决问题,改了搜索关键字,就变成了类似于这样(相似的还有很多)
https://www.cnblogs.com/jiangjh/p/10991365.html
这种例子中看到JNIEnv这中变量可以从JavaVM中获取。但是上个demo是自己create出来的,但我既然已经运行了调用了JNI接口了,这应该说明最起码JavaVM应该已经是现成的了。问题是通过哪个接口获得JavaVM。
于是我在jni.h里翻了翻,列了几个可能的项,最后尝试了一下jint GetJavaVM(JavaVM **vm)这个方法可以获取到JavaVM。接着就是在新的进程里创建JNIEnv了(因为jni调用的函数和我程序肯定是在不同的线程中)。一开始很开心看到有GetEnv以为搞出来了,结果把所有的版本一试发现都报一个JNI_EDETACHED(thread detached from the VM)的错误。最后发现可以用这个函数解决,
g_JavaVM->AttachCurrentThread((void **)(&env), NULL)
在AttachCurrentThread需要调DetachCurrentThread释放就OK了
最后问题解决考了这个:
https://www.jianshu.com/p/e576c7e1c403
关于写法jni.h里有这么一段:
/*
* We use inlined functions for C++ so that programmers can write:
* env->FindClass("java/lang/String")
* in C++ rather than:
* (*env)->FindClass(env, "java/lang/String")
* in C.
*/
jni.h里主要有以下部分… 常见类型定义
union jvalue
struct _jfieldID;
typedef struct _jfieldID *jfieldID;
struct _jmethodID;
typedef struct _jmethodID *jmethodID;
/* Return values from jobjectRefType */
jobjectRefType
/*possible return values for JNI functions*/ //jni函数返回值
/* used in ReleaseScalarArrayElements*/
/*
* used in RegisterNatives to describe native method name, signature,
* and function pointer.
* RegisterNatives 函数中用来表述本地方法名,signature和函数指针
*/
JNINativeMethod
/*JNI Native Method Interface.*/
//本地方法JNIEnv_(C++),C是JNINativeInterface_ ,所以也能看的出来为什么两种的写法不一样
struct JNIEnv_ {
....一堆JNI操作方法
}
struct JavaVMInitArgs; //未使用
typedef struct JavaVMAttachArgs; //未使用
/*JNI Invocation Interface.*/ //这个主要定义在JAVAVM_
struct JNIInvokeInterface_;
struct JavaVM_;
{
JNIInvokeInterface_ *functions;
jint DestroyJavaVM();
jint AttachCurrentThread(void **penv, void *args);
jint DetachCurrentThread();
jint GetEnv(void **penv, jint version);
jint AttachCurrentThreadAsDaemon(void **penv, void *args);
}
//四个独立接口方法
_JNI_IMPORT_OR_EXPORT_ jint JNICALL
JNI_GetDefaultJavaVMInitArgs(void *args);
_JNI_IMPORT_OR_EXPORT_ jint JNICALL
JNI_CreateJavaVM(JavaVM **pvm, void **penv, void *args);
_JNI_IMPORT_OR_EXPORT_ jint JNICALL
JNI_GetCreatedJavaVMs(JavaVM **, jsize, jsize *);
/* Defined by native libraries. */
JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM *vm, void *reserved);
JNIEXPORT void JNICALL
JNI_OnUnload(JavaVM *vm, void *reserved);
//JNI版本宏
从上述大概分为javaVM, JNIEnv,JNINativeMethod还有些调用。
关于JAVA和本地类型详细关系以及一些签名的含义可以参考:
https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/types.html
函数说明参考:
https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/functions.html
大概有个心里准备后,开始参考官方文档关于整个JNI的结构设计可以参考官方文档
https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/design.html
JNI interface pointer
一开始的JNI interface pointer看得我一脸懵逼,但从文档后面的描述看env被称为接口指针,所以大概就能理解了,其实就是包含了JNI一系列的操作接口。
double Java_pkg_Cls_f__ILjava_lang_String_2 (
JNIEnv *env, /* interface pointer */
jobject obj, /* "this" pointer */
jint i, /* argument #1 */
jstring s) /* argument #2 */
The JNI interface pointer is the first argument to native methods. The JNI interface pointer is of type JNIEnv. The second argument differs depending on whether the native method is static or nonstatic. The second argument to a nonstatic native method is a reference to the object. The second argument to a static native method is a reference to its Java class.
这段话大概是说,第一个env其实就是jni的接口指针指向本地方法,从前面jni.h的头文件的JNIEnv_结构体的成员方法也看得出来。第二个参数obj取决于本地方法是否静态。如果是非静态的本地方法就关联到一个object;如果是静态的就关联到一个JAVA类。(其实还是不明白这个this指代的是什么,会不会指的是JAVA调用JNI方法的那个类的this指针???)
回到前面的关于jni interface pointer的说明,这个是JNIEnv的结构图:
The JNI interface pointer is only valid in the current thread. A native method, therefore, must not pass the interface pointer from one thread to another. A VM implementing the JNI may allocate and store thread-local data in the area pointed to by the JNI interface pointer.
Native methods receive the JNI interface pointer as an argument. The VM is guaranteed to pass the same interface pointer to a native method when it makes multiple calls to the native method from the same Java thread. However, a native method can be called from different Java threads, and therefore may receive different JNI interface pointers.
JNIEnv的接口指针仅在当前线程生效,所以在本地方法中,不能够跨线程传递。VM执行JNI会分配和存储线程数据的JNIEnv指针。
这里其实就解释了前面所谓的JNI调用里的env指针为什么不能在该调用之外的地方使用,所以其实这里的调用应该是个多线程的概念。最后给的提示是VM会分配,所以还是要先抓住VM。同时JNI接口指针是区分线程的,但他们都指向到一个JNI函数表里。
关于前面的那个方法CallVoidMethod(env,obj,method1)中用到的obj能不能用的问题
The JNI divides object references used by the native code into two categories: local and global references. Local references are valid for the duration of a native method call, and are automatically freed after the native method returns. Global references remain valid until they are explicitly freed.
Objects are passed to native methods as local references. All Java objects returned by JNI functions are local references. The JNI allows the programmer to create global references from local references. JNI functions that expect Java objects accept both global and local references. A native method may return a local or global reference to the VM as its result.
object有两种类型,一个是局部的,一个是全局的。局部在本地调用过程中有效,全局需要显示释放。object作为局部引用传递到本地方法。JNI允许开发者创建从局部引用中创建全局引用。JNI函数期望JAVA对象接收全局和局部应用。本地调用会返回局部或者全局的应用做为结果返回给VM。所以object也是有作用域之分,也是保存在线程栈上,所以跨线程也是存在问题的。
o implement local references, the Java VM creates a registry for each transition of control from Java to a native method. A registry maps nonmovable local references to Java objects, and keeps the objects from being garbage collected. All Java objects passed to the native method (including those that are returned as the results of JNI function calls) are automatically added to the registry. The registry is deleted after the native method returns, allowing all of its entries to be garbage collected.
为了执行局部引用,VM为每次从JAVA到本地方法的控制传输创建注册。注册映射表将会固定JAVA object避免被垃圾回收。所有的Java object通过本地方法(包括那些作为JNI调用的返回)将会被自动添加到注册表中。注册表在本地调用返回后将自动删除,然后垃圾回收。这里直接是了这种局部引用在native方法中的生命周期,但没解释在线程中是否也是会自动释放。
关于JAVA VM的一些概念
文档里对JAVA VM的介绍不是很多。介绍了一些对JAVA VM的操作。
The JNI_CreateJavaVM() function loads and initializes a Java VM and returns a pointer to the JNI interface pointer. The thread that called JNI_CreateJavaVM() is considered to be the main thread.
这个函数允许加载和初始化一个JAVA vm并且返回jni接口指针,哪里调用哪里就被视为VM的主线程。
The JNI interface pointer (JNIEnv) is valid only in the current thread. Should another thread need to access the Java VM, it must first call AttachCurrentThread() to attach itself to the VM and obtain a JNI interface pointer. Once attached to the VM, a native thread works just like an ordinary Java thread running inside a native method. The native thread remains attached to the VM until it calls DetachCurrentThread() to detach itself.
JNIEnv仅在当前线程下有小,如果另一个线程要用到要通过java VM,一定要调用AttachCurrentThread函数来关联到他的线程并且获得JNIEnv指针。一旦关联到VM,本地线程就像一普通的JAVA线程一样运行在本地调用里。本地线程将会一直保持直到调用DetachCurrentThread()去分离。
A native thread attached to the VM must call DetachCurrentThread() to detach itself before exiting. A thread cannot detach itself if there are Java methods on the call stack.
线程退出前必须要调用DetachCurrentThread方法。
The JNI_DestroyJavaVM() function unloads a Java VM.
The VM waits until the current thread is the only non-daemon user thread before it actually unloads. User threads include both Java threads and attached native threads. This restriction exists because a Java thread or attached native thread may be holding system resources, such as locks, windows, and so on. The VM cannot automatically free these resources. By restricting the current thread to be the only running thread when the VM is unloaded, the burden of releasing system resources held by arbitrary threads is on the programmer.
用JNI_DestroyJavaVM函数写在JAVA VM。VM在真正卸载前将等待直到当前线程是唯一非守护用户线程。用户线程包括JAVA下城和关联的本地线程。此限制存在是因为Java线程或附加的本机线程可能打开系统资源。VM不能自动释放这些资源。
所以解决上述过程步骤:
1. 找到VM
因为既然可以调用JNI,那VM应该已经存在了。
搜一下JAVAEnv 相关接口说明
Java VM Interface
GetJavaVM
jint GetJavaVM(JNIEnv *env, JavaVM **vm);
Returns the Java VM interface (used in the Invocation API) associated with the current thread. The result is placed at the location pointed to by the second argument, vm.
LINKAGE:
Index 219 in the JNIEnv interface function table.
PARAMETERS:
env: the JNI interface pointer.
vm: a pointer to where the result should be placed.
RETURNS:
Returns “0” on success; returns a negative value on failure.
该函数可以返回与当前线程关联的Java VM接口。
最后其实是用这个接口从一个本地调用拿到JAVA VM的指针的,但看文档,觉得十分奇怪,不知道这里的关联到该线程的JAVA VM接口是否可以在别的线程也可以使用????好像通篇说过来感觉是JAVA VM接口会ATTACH到某线程???所以暂时就按这个思路去做。
2.需要获得一个本线程的
JNIEnvAttachCurrentThread
jint AttachCurrentThread(JavaVM *vm, void
**p_env, void *thr_args); Attaches the current thread to a Java VM. Returns a JNI interface pointer in the JNIEnv argument. Trying to
attach a thread that is already attached is a no-op. A native thread
cannot be attached simultaneously to two Java VMs. When a thread is
attached to the VM, the context class loader is the bootstrap loader.
通过这个函数可以attach到VM,然后得到当前线程的JavaEnv。
3.需要获得Object
GetObjectClass
jclass GetObjectClass(JNIEnv *env, jobject obj);
Returns the class of an object.
从对象中返回类
4.获得类
FindClass
jclass FindClass(JNIEnv *env, const char *name);
In JDK release 1.1, this function loads a locally-defined class. It searches the directories and zip files specified by the CLASSPATH environment variable for the class with the specified name.
Since Java 2 SDK release 1.2, the Java security model allows non-system classes to load and call native methods. FindClass locates the class loader associated with the current native method; that is, the class loader of the class that declared the native method. If the native method belongs to a system class, no class loader will be involved. Otherwise, the proper class loader will be invoked to load and link the named class.
Since Java 2 SDK release 1.2, when FindClass is called through the Invocation Interface, there is no current native method or its associated class loader. In that case, the result of ClassLoader.getSystemClassLoader is used. This is the class loader the virtual machine creates for applications, and is able to locate classes listed in the java.class.path property.
The name argument is a fully-qualified class name or an array type signature .
用这个方法可以找到类以及是否存在,然后执行需要捕捉下异常,详细见文档
5.获得方法
GetMethodID
jmethodID GetMethodID(JNIEnv *env, jclass clazz,const char
*name, const char *sig); Returns the method ID for an instance (nonstatic) method of a class or interface. The method may be defined in one of the clazz’s superclasses and inherited by clazz. The method is determined by its name and signature.
GetMethodID() causes an uninitialized class to be initialized.
To obtain the method ID of aconstructor, supply as the method name and void (V) as the return type.
该方法可以在上述的类中找到对应方法
6.调用方法
CallMethod Routines, CallMethodA Routinesm,CallMethodV Routines
Routines
NativeType CallMethod(JNIEnv *env, jobject obj,jmethodID methodID, …);
NativeType CallMethodA(JNIEnv *env, jobject obj,jmethodID methodID, const jvalue *args);
NativeType CallMethodV(JNIEnv *env, jobject obj,jmethodID methodID, va_list args);
Methods from these three families of operations are used to call a Java instance method from a native method.They only differ in their mechanism for passing parameters to the methods that they call.
These families of operations invoke an instance (nonstatic) method on a Java object, according to the specified method ID. The methodID argument must be obtained by calling GetMethodID().
When these functions are used to call private methods and constructors, the method ID must be derived from the real class of obj, not from one of its superclasses.
这个具体根据需要的反射函数来找对应调用方法。
7.最后如果是attach出来的,需要分离这个线程(这次调用)的资源
DetachCurrentThread
jint DetachCurrentThread(JavaVM *vm);
Detaches the current thread from a Java VM. All Java monitors held by this thread are released. All Java threads waiting for this thread to die are notified.
The main thread can be detached from the VM.
关于object
在Object上翻了车,其实一开始没有理解Object是什么意思,只看到了有作用域的限制。于是想当然就Allocal了一个,然后就导致了JNI调用的函数在另一个对象去了。
所以Object其实是指的这个对象的实例。其实也就是指代java调用这边new出来的这个对象。
所以如果希望在一个对象内需要在JNI调用的地方将用NewGlobalRef()局部的传入的object保存为全局引用,然后在其他线程中也就可以使用了。同时如果用完了,需要用DeleteGlobalRef()删除。而GetObjectClass直接从全局的这个object中获取
关于GetMethodID调用失败
现在原因暂不清除,还在调试猜测原因