JNI是java语言提供的Java和C/C++相互沟通的机制,Java可以通过JNI调用本地的C/C++代码,本地的C/C++的代码也可以调用java代码。
JNI 是本地编程接口,Java和C/C++互相通过的接口,Java通过C/C++使用本地的代码的一个关键性原因在于C/C++代码的高效性。
NDK是一系列工具的集合。
它提供了一系列的工具,帮助开发者快速开发C(或C++)的动态库,并能自动将so和java应用一起打包成apk。
这些工具对开发者的帮助是巨大的。它集成了交叉编译器,并提供了相应的mk文件隔离CPU、平台、ABI等差异,开发人员只需要简单修改mk文件(指出“哪些文件需要编译”、“编译特性要求”等),就可以创建出so。
它可以自动地将so和Java应用一起打包,极大地减轻了开发人员的打包工作。
AndroidStudio甚至不需要配置mk文件,它将根据gradle中的相关配置,自动生成mk文件。也就说将配置过程移动到了gradle中集中管理
当然,如果一定要用mk手动处理,AS也是支持的
JNI是Java调用Native机制,是Java语言自己的特性全称为Java Native Interface,类似的还有微软.Net Framework上的p/invoke,可以让C#或Visual Basic.Net可以调用C/C++的API,所以说JNI和Android没有关系,在PC上开发Java的应用,如果运行在Windows平台使用JNI是是经常的,比如说读写Windows的注册表
NDK是Google公司推出的帮助Android开发者通过C/C++本地语言编写应用的开发包,包含了C/C++的头文件、库文件
Java通过JNI机制和C/C++沟通的具体步骤
在Android的NDK中,Java、C/C++、Dalvik VM关系如下:
C作为底层代码,在某些底层处理上,可能比Java更具备效率
由于JNI是JVM规范中的一部份,因此可以将我们写的JNI程序在任何实现了JNI规范的Java虚拟机中运行。
C语言体系已经有丰富的基础资源,利用jni使我们可以复用以前用C/C++写的大量代码。
为了应用的安全性,会将一些复杂的逻辑和算法通过本地代码(C或C++)来实现,然后打包成so动态库文件,并提供Java接口供应用层调用,这么做的目的主要就是为了提供应用的安全性,防止被反编译后被不法分子分析应用的逻辑。
然打包成so也不能说完全安全了,只是相对反编译Java的class字节码文件来说,反汇编so动态库来分析程序的逻辑要复杂得多,没那么容易被破解。比如百度开放平台提供的定位服务、搜索服务、LBS服务、推送服务的Android SDK,除了Java接口的jar包之外,还有一个.so文件,这个so就是实现了Java层定义的native接口的动态库
依托C++的跨平台特性,只需用C++编写一次逻辑,就可以将游戏打包发布到不同的平台(IOS、Android、WinPhone、黑莓、Linux、Windows)
当各种物联网设备接入互联网的同时,自然少不了人机交互的应用程序,当应用程序需要调用硬件特定的功能时,此时只能通过C或C++封装对应功能的JNI接口来供上层应用使用。
比如要用手机中的app控制家里的电灯、窗帘、冰箱、空调等一切智能的电子设备时,自然少不了应用要和底层硬件进行通讯,至于各种智能设备的运行控制,自然是由厂商来实现,他们只需提供操作设备相关功能的接口即可。
虽然厂商会封装好JNI接口,但我们也要了解下jni与java通讯的原理,以便我们在开发过程当中遇到问题时,能够快速定位到问题
开发JNI程序会受到系统环境的限制,因为用C/C++语言写出来的代码或模块,编译过程当中要依赖当前操作系统环境所提供的一些库函数,并和本地库链接在一起
而且编译后生成的二进制代码只能在本地操作系统环境下运行,因为不同的操作系统环境,有自己的本地库和CPU指令集,而且各个平台对标准C/C++的规范和标准库函数实现方式也有所区别
这就造成使用了JNI接口的JAVA程序,不再像以前那样自由的跨平台。如果要实现跨平台,就必须将本地代码在不同的操作系统平台下编译出相应的动态库
android作为linxu系统,使用的就是.so文件作为动态库
如果操作熟练,了解.h的生成规则,那么就不需要步骤2.3,直接手动写出.h文件并开始第4步即可,效果是一样的
以下实例我是在app中直接建的,但是最好方式是自己做一个新的module(android library)专门处理JNI操作,步骤是相同的:new module—–android library—-MyJni
在module中,编写JniTest.java类,源码如下:
使用native关键字说明这个函数是java和其他语言(c/c++)协作时用的,并用其他语言实现的
package test.com.MyJni;
public class HelloWorld {
public static native String sayHello(String name); // 1.声明这是一个native函数,由本地代码实现
static {
System.loadLibrary("HelloWorld"); // 2.加载实现了native函数的动态库,只需要写动态库的名字
}
public static void main(String[] args) {
String text = sayHello("yangxin"); // 3.调用本地函数
System.out.println(text);
}
}
或者
package test.com.MyJni;
public class JniTest {
//使用JNI的关键字native
//这个关键字决定我们那些方法能在我们的C文件中使用
//只须声明,不必实现
public native void yu();
public native double sum(double x,double y);
}
javac src/test/com/MyJni/HelloWorld.java -d ./bin
//注意:HelloWorld放在test.com.MyJni包下面
//-d 表示将编译后的class文件放到指定的目录下,这里我把它放到和src同级的bin目录下
需要注意的是,如果你操作熟练,了解.h的生成规则,那么就不需要步骤2、3,直接手动写出.h文件并开始第4步即可,效果是一样的
//默认生成的.h头文件名为:test_com_MyJni_HelloWorld.h(包名+类名.h)
javah -jni -classpath ./bin -d ./jni test.com.MyJni.HelloWorld
javah -d jni -classpath D:\Users\sf\AppData\Local\Android\sdk\platforms\android-21\android.jar;..\..\..\..\..\..\build\intermediates\classes\debug test.com.MyJni.JniTest
//也可以通过-o参数指定生成头文件名称:
javah -jni -classpath ./bin -o HelloWorld.h test.com.MyJni.HelloWorld
我们可以看看自动生成的头文件,具体的转换规则请参看《Native标准映射规则》
test_com_MyJni_HelloWorld.h:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include
/* Header for class test_com_MyJni_HelloWorld */
#ifndef _Included_test_com_MyJni_HelloWorld
#define _Included_test_com_MyJni_HelloWorld
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: test_com_MyJni_HelloWorld
* Method: sayHello
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_test_com_MyJni_HelloWorld_sayHello
(JNIEnv *, jclass, jstring);
#ifdef __cplusplus
}
#endif
#endif
和
/* DO NOT EDIT THIS FILE - it is machine generated */
#include
/* Header for class test_com_MyJni_JniTest */
#ifndef _Included_test_com_MyJni_JniTest
#define _Included_test_com_MyJni_JniTest
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: test_com_MyJni_JniTest
* Method: yu
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_test_com_MyJni_JniTest_yu
(JNIEnv *, jobject);
/*
* Class: test_com_MyJni_JniTest
* Method: sum
* Signature: (DD)D
*/
JNIEXPORT jdouble JNICALL Java_test_com_MyJni_JniTest_sum
(JNIEnv *, jobject, jdouble, jdouble);
#ifdef __cplusplus
}
#endif
#endif
HelloWorld.c:
// HelloWorld.c
#include "test_com_MyJni_HelloWorld.h"
#ifdef __cplusplus
extern "C"
{
#endif
/*
* Class: test_com_MyJni_HelloWorld
* Method: sayHello
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_test_com_MyJni_HelloWorld_sayHello(
JNIEnv *env, jclass cls, jstring j_str)
{
const char *c_str = NULL;
char buff[128] = { 0 };
c_str = (*env)->GetStringUTFChars(env, j_str, NULL);
if (c_str == NULL)
{
printf("out of memory.\n");
return NULL;
}
printf("Java Str:%s\n", c_str);
sprintf(buff, "hello %s", c_str);
(*env)->ReleaseStringUTFChars(env, j_str, c_str);
return (*env)->NewStringUTF(env, buff);
}
#ifdef __cplusplus
}
#endif
和
//必须的头文件jni.h
#include
//导入我们需要实现的本地方法
#include "test_com_MyJni_JniTest.h"
#include
/*
* Class: test_com_MyJni_JniTest
* Method: yu
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_test_com_MyJni_JniTest_yu
(JNIEnv *env, jobject obj){
LOGE("log string from ndk.");
printf("Hello World tom!!");
return;
}
/*
* Class: test_com_MyJni_JniTest
* Method: sum
* Signature: (DD)D
*/
JNIEXPORT jdouble JNICALL Java_test_com_MyJni_JniTest_sum(JNIEnv *env, jobject obj, jdouble a, jdouble b)
return a+b;
}
动态库文件名命名规则:lib+动态库文件名+后缀(操作系统不一样,后缀名也不一样)如:
我们接下来讲一下linux下的编译动态库的两种方法:
Android.mk文件和.c文件同级目录
我们实现好.c或者.cpp文件后,编写Android.mk文件,来生成动态库,一般使用NDK工具进行生成,首先是下载ndk包,然后设计全局变量,进入Android.mk文件夹中执行ndk编译命令即可
ndk编译命令使用参考:
http://www.cnblogs.com/lipeil/archive/2012/08/27/2659378.html
http://blog.csdn.net/laczff21/article/details/7542236
我们进入.c,.h, Android.mk所在的文件下面,然后执行ndk编译命令:
ndk-build
信息如下:
abc@abc:~/workspace/NativeTest/jni$ndk-build
AndroidNDK: WARNING: APP_PLATFORM android-19 is larger thanandroid:minSdkVersion 8 in/home/archermind/workspace/NativeTest/AndroidManifest.xml
[armeabi]Compile++ thumb: NativeClassJni <=com_example_nativetest_NativeClass.cpp
[armeabi]StaticLibrary : libstdc++.a
[armeabi]SharedLibrary : libNativeClassJni.so
[armeabi]Install : libNativeClassJni.so =>libs/armeabi/libNativeClassJni.so
Android.mk文件是GNU Makefile的一小部分,它用来对Android程序进行编译,Android.mk中的变量都是全局的,解析过程会被定义。
一个Android.mk文件可以编译多个模块,模块包括:APK程序、JAVA库、C\C++应用程序、C\C++静态库、C\C++共享库。
简单实例如下:
LOCAL_PATH := $(call my-dir) #表示是当前文件的路径
include $(CLEAR_VARS) #指定让GNU MAKEFILE该脚本为你清除许多 LOCAL_XXX 变量
LOCAL_MODULE:= helloworld #标识你在 Android.mk 文件中描述的每个模块
MY_SOURCES := foo.c #自定义变量
ifneq ($(MY_CONFIG_BAR),)
MY_SOURCES += bar.c
endif
LOCAL_SRC_FILES += $(MY_SOURCES) #包含将要编译打包进模块中的 C 或 C++源代码文件
include $(BUILD_SHARED_LIBRARY) #根据LOCAL_XXX系列变量中的值,来编译生成共享库(动态链接库)
更多字段参看(4.1.27.7)JNI/NDK开发指南(四)——JNI 实战全面解析
gcc -I$JAVA_HOME/include -I$JAVA_HOME/include/linux -fPIC -shared HelloWorld.c -o libHelloWorld.so
Error:Execution failed for task ':app:compileDebugNdk'.
> NDK not configured.
Download the NDK from http://developer.android.com/tools/sdk/ndk/.Then add ndk.dir=path/to/ndk in local.properties.
(On Windows, make sure you escape backslashes, e.g. C:\\ndk rather than C:\ndk)
工程中共有两个build.gradle配置文件,我们要修改的是在\app\build.gradle这个文件。为其在defaultConfig分支中增加上
sourceSets.main {
jni.srcDirs = []
// 默认情况下,你需要把C/C++源代码放在[module]/src/main/jni/路径下
//自定义源代码路径
jni.srcDirs 'src/main/java/test/com/MyJni'
//设置你的.so文件的实际路径,用于调用System.loadLibrary( libName );
//默认是再app/src/main/jniLibs中
jniLibs.srcDir "src/main/libs"
}
ndk {
moduleName "JniTest" //生成的so名字
ldLibs "log", "z", "m"
abiFilters "armeabi", "armeabi-v7a", "x86" //输出指定三种abi体系结构下的so库。目前可有可无。
}
以上配置代码指定的so库名称为JniTest,链接时使用到的库,对应android.mk文件中的LOCAL_LDLIBS,及最终输出指定三种abi体系结构下的so库。
Error:Execution failed for task ':app:compileDebugNdk'.
> com.android.ide.common.internal.LoggedErrorException: Failed to run command:
编译后的.so文件位于build文件夹的如图位置:
把.so文件放到相应的目录即可,默认路径如图所示的jniLibs
如果你不想把.so放在上面的默认路径,可以在buid.gradle中进行如下配置:
android {
// .. android settings ..
sourceSets.main {
jniLibs.srcDir 'src/main/myCppLibraries' // <-- 你的.so库的实际路径
}
}
调用和普通java函数一样,无非是需要提前加载下动态库
public class JniTest {
//加载so库
static {
System.loadLibrary( "MyJniTest" );
}
//使用JNI的关键字native
//这个关键字决定我们那些方法能在我们的C文件中使用
//只须声明,不必实现
public native void yu();
public native double sum(double x,double y);
}
import test.com.myjni.JniTest;
public class MainActivity extends AppCompatActivity {
TextView tv;
JniTest jniUtils=new JniTest();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tv=(TextView)findViewById(R.id.tv);
double a=1;
double b=2;
double sum=jniUtils.sum(a,b);
tv.setText(String.valueOf(sum));
}
}
Java在调用native(本地)方法之前,需要先加载动态库。如果在未加载动态之前就调用native方法,会抛出找不到动态链接库文件的异常。如下所示:
Exception in thread "main" java.lang.UnsatisfiedLinkError: com.study.jnilearn.HelloWorld.sayHello(Ljava/lang/String;)Ljava/lang/String;
at com.study.jnilearn.HelloWorld.sayHello(Native Method)
at com.study.jnilearn.HelloWorld.main(HelloWorld.java:9)
加载动态库的两种方式:
Android支持的.so文件都应该以“lib”开头,AndroidStudio自动生成的.so也是自动加上这个名字的
一般在类的静态(static)代码块中加载动态库最合适,因为在创建类的实例时,类会被ClassLoader先加载到虚拟机,随后立马调用类的static静态代码块。这时再去调用native方法就万无一失了
AndroidStudio默认jnilibs路径:module/src/main/jnilibs/armeabi/...
- libgnustl_shared.so
- ibprotobuf.so
- ibcrypto.so
- ibssl.so
- ibnet_manager.so
- ibfilehash.so
- ibmoa_jni.so
public class MOA_JNI {
static {
System.out.println("load libs.....");
System.loadLibrary("gnustl_shared");
System.loadLibrary("protobuf");
System.loadLibrary("crypto");
System.loadLibrary("ssl");
System.loadLibrary("net_manager");
System.loadLibrary("filehash");
System.loadLibrary("moa_jni");
}
public native int connectServer(int tunnel, int tunType, String ip, short port, boolean autoReconn);
public native int remoteCall(int tunnel, short severType, int operType, short flag,
byte[] byteArray, MoaResult result, int timeout);
...
}
System.loadLibrary("HelloWorld");
如果使用该方式,java会去java.library.path系统属性指定的目录下查找动态库文件,如果没有找到会抛出java.lang.UnsatisfiedLinkError异常
Exception in thread "main" java.lang.UnsatisfiedLinkError: no HelloWorld in java.library.path
at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1860)
at java.lang.Runtime.loadLibrary0(Runtime.java:845)
at java.lang.System.loadLibrary(System.java:1084)
at com.study.jnilearn.HelloWorld.(HelloWorld.java:13)
从异常中可以看出来,他是在java.library.path中查找该名称对应的动态库
那么这个路径是什么呢?
可以通过调用System.getProperties(“java.library.path”)方法获取查找的目录列表,其中在mac下找libHelloWorld.jnilib文件,linux下找libHelloWorld.so文件,windows下找libHelloWorld.dll文件
下面是本机mac os x 系统下的查找目录:
String libraryDirs = System.getProperty("java.library.path");
System.out.println(libraryDirs);
// 输出结果如下:
/Users/yhf/Library/Java/Extensions:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java:.
有两种方式可以让java从java.library.path找到动态链接库文件:
java -Djava.library.path=/Users/yangxin/Desktop
Linux/Unix环境下可以通过设置LD_LIBRARY_PATH环境变量,指定库的搜索目录
我们明白了调用native方法之前,首先要调用System.loadLibrary接口加载一个实现了native方法的动态库才能正常访问,否则就会抛出Java.lang.UnsatisfiedLinkError异常,找不到XX方法的提示。
现在我们想想,在Java中调用某个native方法时,JVM是通过什么方式,能正确的找到动态库中C/C++实现的那个native函数呢?
JVM查找native方法有两种方式:
public static native String sayHello(String name);
JNIEXPORT jstring JNICALL Java_com_study_jnilearn_HelloWorld_sayHello
(JNIEnv *, jclass, jstring);
背景:
在JNI_OnLoad的触发
效果:
/*
* JAVA方法与NDK函数的映射关系表
*/
static JNINativeMethod s_methods[] = {
{"connectServer", "(Ljava/lang/String;S)I", (void*)connectServer},
{"disconnectServer", "()I", (void*)disconnectServer},
{"remoteCall", "(SI[BLcom/sangfor/moa/MoaResult;I)I", (void*)remoteCall},
{"recvMessage", "(Lcom/sangfor/moa/MoaResult;)I", (void*)recvMessage},
{"isConnected", "()Z", (void*)isConnected},
{"setNdkLogLevel", "(I)I", (void*)setNdkLogLevel},
{"setNdkLogFile", "(Ljava/lang/String;I)I", (void*)setNdkLogFile},
{"getFileHash", "(Ljava/lang/String;)Ljava/lang/String;", (void*)getFileHash},
{"getStreamHash", "([B)Ljava/lang/String;", (void*)getStreamHash},
};
static jboolean native_methods_register(JNIEnv* env, const char* className)
{
jclass cls;
cls = env->FindClass(className);//获取RdpConn类引用
if (cls == NULL)
{
return false;
}
//获取JNINativeMethod的长度
jint num = sizeof(s_methods) / sizeof(s_methods[0]);
//注册java层的jni接口
jint ret = env->RegisterNatives(cls, s_methods, num);
if (ret != JNI_OK)
{
env->DeleteLocalRef(cls);
return false;
}
env->DeleteLocalRef(cls);
return true;
}
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved)
{
//A.2.1 注册RdpConn类中的本地接口,以避免每次呼叫都触发 .so文件内的函数查询
if (!native_methods_register(env, "com/sangfor/moa/MOA_JNI"))
{
return -1;
}
}
public class MOA_JNI {
/* connectServer - 连接服务器 */
public native int connectServer(String ip, short port);
/* disconnectServer - 与服务器断开连接 */
public native int disconnectServer();
/* RemoteCall - 向服务端查询的接口 */
public native int remoteCall(short severType, int operType, byte[] byteArray,
MoaResult result, int timeout);
/* recvMessage - 获取消息(服务器推送消息或本地消息)
* 若暂时没有任何消息,调用会被阻塞,直到有消息
*/
public native int recvMessage(MoaResult result);
/* isConnected - 查看通道连接状态
*/
public native boolean isConnected();
/* setNdkLogLevel - 设置底层NDK打日志的级别 */
public native int setNdkLogLevel(int level);
/* setNdkLogFile - 设置底层NDK打文件日志
* @filePath: 日志文件路径 . ""表示打印到console
* @sizeLimit:日志文件的大小限制(bytes)
*/
public native int setNdkLogFile(String filePath, int sizeLimit);
/* getFileHash - 获取文件的哈希值(七牛文件服务使用)
* @file - 文件全路径
* retrun - 计算出的哈希值(调用失败会返回 "null")
*/
public native String getFileHash(String file);
/* getStreamHash - 获取字节流的哈希值(七牛文件服务使用)
* @byteArray - 字节流
* retrun - 计算出的哈希值(调用失败会返回 "null")
*/
public native String getStreamHash(byte[] byteArray);
}
当我们在调用一个Java native方法的时候,方法中的参数是如何传递给C/C++本地函数中的呢?
Java方法中的参数与C/C++函数中的参数,它们之间是怎么转换的呢?
我们通过一个简单的示例来看下
class MyClass {}
public class HelloWorld {
//前面8个为基本数据类型,后面4个全部为引用类型
public static native void test(short s, int i, long l, float f, double d, char c,
boolean z, byte b, String str, Object obj, MyClass p, int[] arr);
public static void main(String[] args) {
String obj = "obj";
short s = 1;
long l = 20;
byte b = 127;
test(s, 1, l, 1.0f, 10.5, 'A', true, b, "中国", obj, new MyClass(), new int[] {});
}
static {
System.loadLibrary("HelloWorld");
}
}
//转化后
JNIEXPORT void JNICALL Java_test_com_MyJni_HelloWorld_test
(JNIEnv *env, jclass cls, jshort s, jint i, jlong l, jfloat f,
jdouble d, jchar c, jboolean z, jbyte b, jstring j_str, jobject jobj1, jobject job2, jintArray j_int_arr)
{
printf("s=%hd, i=%d, l=%ld, f=%f, d=%lf, c=%c, z=%c, b=%d", s, i, l, f, d, c, z, b);
const char *c_str = NULL;
c_str = (*env)->GetStringUTFChars(env, j_str, NULL);
if (c_str == NULL)
{
return; // memory out
}
(*env)->ReleaseStringUTFChars(env, j_str, c_str);
printf("c_str: %s\n", (char*)c_str);
}
从头文件函数的原型可以得知,test方法中形参的数据类型自动转换成了JNI中相应的数据类型,不难理解,在调用Java native方法将实参传递给C/C++函数的时候,会自动将java形参的数据类型自动转换成C/C++相应的数据类型,所以我们在写JNI程序的时候,必须要明白它们之间数据类型的对应关系
在Java语言中数据类型分为基本数据类型和引用类型,其中基本数据类型有8种:byte、char、short、int、long、float、double、boolean,除了基本数据类型外其它都是引用类型:Object、String、数组等。
JNIEnv 是线程相关的, 即 在 每个线程中 都有一个 JNIEnv 指针, 每个JNIEnv 都是线程专有的, 其它线程不能使用本线程中的 JNIEnv, 线程 A 不能调用 线程 B 的 JNIEnv;
JNIEnv只在当前线程中有效。本地方法不能将JNIEnv从一个线程传递到另一个线程中。相同的 Java 线程中对本地方法多次调用时,传递给该本地方法的JNIEnv是相同的。但是,一个本地方法可被不同的 Java 线程所调用,因此可以接受不同的 JNIEnv
JNIEnv 不能跨线程 :
– 当前线程有效 : JNIEnv 只在当前线程有效, JNIEnv 不能在 线程之间进行传递, 在同一个线程中, 多次调用 JNI层方法, 传入的 JNIEnv 是相同的;
– 本地方法匹配多JNIEnv : 在 Java 层定义的本地方法, 可以在不同的线程调用, 因此 可以接受不同的 JNIEnv;
Java类型 | 本地类型(JNI) | 描述 |
---|---|---|
boolean(布尔型) | jboolean | 无符号8个比特 |
byte(字节型) | jbyte | 有符号8个比特 |
char(字符型) | jchar | 无符号16个比特 |
short(短整型) | jshort | 有符号16个比特 |
int(整型) | jint | 有符号32个比特 |
long(长整型) | jlong | 有符号64个比特 |
float(浮点型) | jfloat | 32个比特 |
double(双精度浮点型) | jdouble 6 | 4个比特 |
void(空型) | void | N/A |
这些基本数据类型都是可以在Native层直接使用的
typedef union jvalue {
jboolean z;
jbyte b;
jchar c;
jshort s;
jint i;
jlong j;
jfloat f;
jdouble d;
jobject l;
} jvalue;
就是对是一种对函数返回值和参数的编码
类描述符是类的完整名称(包名+类名),将原来的 . 分隔符换成 / 分隔符
例如:在java代码中的java.lang.String类的类描述符就是java/lang/String
在实践中,我发现可以直接用该类型的域描述符取代,也是可以成功的
jclass intArrCls = env->FindClass("java/lang/String")
jclass intArrCls = env->FindClass("Ljava/lang/String;")
域 | Java 语言 |
---|---|
Z | boolean |
B | byte |
C | char |
S | short |
I | int |
J | long |
F | float |
D | double |
将参数类型的域描述符按照申明顺序放入一对括号中后跟返回值类型的域描述符,规则如下:
Java层方法 JNI函数签名
String test ( ) Ljava/lang/String;
int f (int i, Object object) (ILjava/lang/Object;)I
void set (byte[ ] bytes) ([B)V
...
class _jobject {};
class _jclass : public _jobject {};
class _jstring : public _jobject {};
class _jarray : public _jobject {};
class _jbooleanArray : public _jarray {};
class _jbyteArray : public _jarray {};
...
读取和返回的时候需要借助jni定义的中间数据类型并使用 env:JNIEnv函数表指针 进行对jvm中数据的操作,但是中间的变换可以使用C\C++代码
我们知道了JNI将JAVA数据和native数据的数据类型进行了转换,那么jni是如何对java传递下来的数据进行操作的呢?
基本类型很容易理解,就是对C/C++中的基本类型用typedef重新定义了一个新的名字,在JNI中可以直接访问
typedef unsigned char jboolean;
typedef unsigned short jchar;
typedef short jshort;
typedef float jfloat;
typedef double jdouble;
typedef int jint;
#ifdef _LP64 /* 64-bit Solaris */
typedef long jlong;
#else
typedef long long jlong;
#endif
typedef signed char jbyte;
JNI把Java中的所有对象当作一个C指针传递到本地方法中,这个指针指向JVM中的内部数据结构,而内部的数据结构在内存中的存储方式是不可见的,只能从JNIEnv指针指向的函数表中选择合适的JNI函数来操作JVM中的数据结构。
譬如访问java.lang.String对应的JNI类型jstring时,没有像访问基本数据类型一样直接使用,因为它在Java是一个引用类型,所以在本地代码中只能通过GetStringUTFChars这样的JNI函数来访问字符串的内容
package com.study.jnilearn;
public class Sample {
public native static String sayHello(String text);
public static void main(String[] args) {
String text = sayHello("yangxin");
System.out.println("Java str: " + text);
}
static {
System.loadLibrary("Sample");
}
}
JNIEXPORT jstring JNICALL Java_com_study_jnilearn_Sample_sayHello
(JNIEnv *env, jclass cls, jstring j_str)
{
const char *c_str = NULL;
char buff[128] = {0};
jboolean isCopy; // 返回JNI_TRUE表示原字符串的拷贝,返回JNI_FALSE表示返回原字符串的指针
c_str = (*env)->GetStringUTFChars(env, j_str, &isCopy);
printf("isCopy:%d\n",isCopy);
if(c_str == NULL)
{
return NULL;
}
printf("C_str: %s \n", c_str);
sprintf(buff, "hello %s", c_str);
(*env)->ReleaseStringUTFChars(env, j_str, c_str);
return (*env)->NewStringUTF(env,buff);
}
/**
* 读取JVM内部的字符串内容
* - Java默认使用Unicode编码,而C/C++默认使用UTF编码,所以在本地代码中操作字符串的时候,必须使用合适的JNI函数把jstring转换成C风格的字符串
* - GetStringUTFChars可以把一个jstring指针(指向JVM内部的Unicode字符序列)转换成一个UTF-8格式的C字符串
* env:JNIEnv函数表指针
* j_str:jstring类型(Java传递给本地代码的字符串指针)
* isCopy:取值JNI_TRUE和JNI_FALSE,如果值为JNI_TRUE,表示返回JVM内部源字符串的一份拷贝,并为新产生的字符串分配内存空间。如果值为JNI_FALSE,表示返回JVM内部源字符串的指针,意味着可以通过指针修改源字符串的内容,不推荐这么做,因为这样做就打破了Java字符串不能修改的规定。但我们在开发当中,并不关心这个值是多少,通常情况下这个参数填NULL即可
*/
GetStringUTFChars(env, j_str, &isCopy)
JNI的异常和Java中的异常处理流程是不一样的,Java遇到异常如果没有捕获,程序会立即停止运行。而JNI遇到未决的异常不会改变程序的运行流程,也就是程序会继续往下走,这样后面针对这个字符串的所有操作都是非常危险的,因此,我们需要用return语句跳过后面的代码,并立即结束当前方法
这两个函数是配对使用的,用了GetXXX就必须调用ReleaseXXX,而且这两个函数的命名也有规律,除了前面的Get和Release之外,后面的都一样
在这个例子中我们不必检查它的返回值,如果NewStringUTF创建java.lang.String失败,OutOfMemoryError这个异常会被在Sample.main方法中抛出。如果NewStringUTF创建java.lang.String成功,则返回一个JNI引用,这个引用指向新创建的java.lang.String对象
更多字符串内容,请查看(4.1.27.6)JNI/NDK开发指南(三)——从字符串处理了解JNI的函数机制
当Android的VM(Virtual Machine)执行到System.loadLibrary()函数时,首先会去执行C组件里的JNI_OnLoad()函数。
它的用途有二:
/**
* 系统接口,在库被加载时调用,在此接口中实现接口注册功能
*/
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved)
{
JNIEnv* env = NULL;
//初始化jni env环境 【A.1. 告诉VM此C组件使用那一个JNI版本。 预先校验是否支持】
if (!callback_environment_init(vm, &env))
{
return -1;
}
//A.2.1 注册RdpConn类中的本地接口,以避免每次呼叫都触发 .so文件内的函数查询
if (!native_methods_register(env, "com/sangfor/moa/MOA_JNI"))
{
return -1;
}
//A.2.2 初始化net_manager
if (!theCallManager().init())
{
return -1;
}
//A.2.3 初始化 theSocketManager : 初始化session + 初始化定时器(设置定时器定时检查网络通断情况)
if (!theSocketManager().init())
{
return -1;
}
//A.2.4 开启线程,执行 theSocketManager().run()---->在一个无限循环中执行相关任务
if (!theSocketManager().start())
{
return -1;
}
return JNI_VERSION_1_4;
}
/**
* 初始化虚拟机环境 【A.1. 告诉VM此C组件使用那一个JNI版本。 预先校验是否支持】
*@param [in] vm 虚拟机对象
*@param [out] env jni环境变量
*@return 成功:true, 失败:false
*/
static jboolean callback_environment_init(JavaVM* vm, JNIEnv **env)
{
jboolean ret = (jboolean)vm->GetEnv((void**)env, JNI_VERSION_1_4);
if (ret != JNI_OK)
{
return false;
}
return true;
}
-直接使用该JNI构造一个jstring对象返回
jstring str = env->newStringUTF("HelloJNI");
return str ;
jobjectArray ret = 0;
jsize len = 5;
jstring str;
string value("hello");
ret = (jobjectArray)(env->NewObjectArray(len, env->FindClass("java/lang/String"), 0));
for(int i = 0; i < len; i++)
{
str = env->NewStringUTF(value..c_str());
env->SetObjectArrayElement(ret, i, str);
}
return ret;
jclass m_cls = env->FindClass("com/ldq/ScanResult");
jmethodID m_mid = env->GetMethodID(m_cls,"" ,"()V");
jfieldID m_fid_1 = env->GetFieldID(m_cls,"ssid","Ljava/lang/String;");
jfieldID m_fid_2 = env->GetFieldID(m_cls,"mac","Ljava/lang/String;");
jfieldID m_fid_3 = env->GetFieldID(m_cls,"level","I");
jobject m_obj = env->NewObject(m_cls,m_mid);
env->SetObjectField(m_obj,m_fid_1,env->NewStringUTF("AP1"));
env->SetObjectField(m_obj,m_fid_2,env->NewStringUTF("00-11-22-33-44-55"));
env->SetIntField(m_obj,m_fid_3,-50);
return m_obj;
jclass list_cls = env->FindClass("Ljava/util/ArrayList;");//获得ArrayList类引用
if(listcls == NULL)
{
cout << "listcls is null \n" ;
}
jmethodID list_costruct = env->GetMethodID(list_cls , "" ,"()V"); //获得得构造函数Id
jobject list_obj = env->NewObject(list_cls , list_costruct); //创建一个Arraylist集合对象
//或得Arraylist类中的 add()方法ID,其方法原型为: boolean add(Object object) ;
jmethodID list_add = env->GetMethodID(list_cls,"add","(Ljava/lang/Object;)Z");
jclass stu_cls = env->FindClass("Lcom/feixun/jni/Student;");//获得Student类引用
//获得该类型的构造函数 函数名为 返回类型必须为 void 即 V
jmethodID stu_costruct = env->GetMethodID(stu_cls , "" , "(ILjava/lang/String;)V");
for(int i = 0 ; i < 3 ; i++)
{
jstring str = env->NewStringUTF("Native");
//通过调用该对象的构造函数来new 一个 Student实例
jobject stu_obj = env->NewObject(stucls , stu_costruct , 10,str); //构造一个对象
env->CallBooleanMethod(list_obj , list_add , stu_obj); //执行Arraylist类实例的add方法,添加一个stu对象
}
return list_obj ;
//获得jfieldID 以及 该字段的初始值
jfieldID nameFieldId ;
jclass cls = env->GetObjectClass(obj); //获得Java层该对象实例的类引用,即HelloJNI类引用
nameFieldId = env->GetFieldID(cls , "name" , "Ljava/lang/String;"); //获得属性句柄
if(nameFieldId == NULL)
{
cout << " 没有得到name 的句柄Id \n;" ;
}
jstring javaNameStr = (jstring)env->GetObjectField(obj ,nameFieldId); // 获得该属性的值
const char * c_javaName = env->GetStringUTFChars(javaNameStr , NULL); //转换为 char *类型
string str_name = c_javaName ;
cout << "the name from java is " << str_name << endl ; //输出显示
env->ReleaseStringUTFChars(javaNameStr , c_javaName); //释放局部引用
//构造一个jString对象
char * c_ptr_name = "I come from Native" ;
jstring cName = env->NewStringUTF(c_ptr_name); //构造一个jstring对象
env->SetObjectField(obj , nameFieldId , cName); // 设置该字段的值
jstring str = NULL;
jclass clz = env->FindClass("cc/androidos/jni/JniTest");
//获取clz的构造函数并生成一个对象
jmethodID ctor = env->GetMethodID(clz, "" , "()V");
jobject obj = env->NewObject(clz, ctor);
// 如果是数组类型,则在类型前加[,如整形数组int[] intArray,则对应类型为[I,整形数组String[] strArray对应为[Ljava/lang/String;
jmethodID mid = env->GetMethodID(clz, "sayHelloFromJava", "(Ljava/lang/String;II[I)I");
if (mid)
{
LOGI("mid is get");
jstring str1 = env->NewStringUTF("I am Native");
jint index1 = 10;
jint index2 = 12;
//env->CallVoidMethod(obj, mid, str1, index1, index2);
// 数组类型转换 testIntArray能不能不申请内存空间
jintArray testIntArray = env->NewIntArray(10);
jint *test = new jint[10];
for(int i = 0; i < 10; ++i)
{
*(test+i) = i + 100;
}
env->SetIntArrayRegion(testIntArray, 0, 10, test);
jint javaIndex = env->CallIntMethod(obj, mid, str1, index1, index2, testIntArray);
LOGI("javaIndex = %d", javaIndex);
delete[] test;
test = NULL;
}
static void event_callback(int eventId,const char* description) { //主进程回调可以,线程中回调失败。
if (gEventHandle == NULL)
return;
JNIEnv *env;
bool isAttached = false;
if (myVm->GetEnv((void**) &env, JNI_VERSION_1_2) < 0) { //获取当前的JNIEnv
if (myVm->AttachCurrentThread(&env, NULL) < 0)
return;
isAttached = true;
}
jclass cls = env->GetObjectClass(gEventHandle); //获取类对象
if (!cls) {
LOGE("EventHandler: failed to get class reference");
return;
}
jmethodID methodID = env->GetStaticMethodID(cls, "callbackStatic",
"(ILjava/lang/String;)V"); //静态方法或成员方法
if (methodID) {
jstring content = env->NewStringUTF(description);
env->CallVoidMethod(gEventHandle, methodID,eventId,
content);
env->ReleaseStringUTFChars(content,description);
} else {
LOGE("EventHandler: failed to get the callback method");
}
if (isAttached)
myVm->DetachCurrentThread();
}
线程中回调
把c/c++中所有线程的创建,由pthread_create函数替换为由Java层的创建线程的函数AndroidRuntime::createJavaThread。
static pthread_t create_thread_callback(const char* name, void (*start)(void *), void* arg)
{
return (pthread_t)AndroidRuntime::createJavaThread(name, start, arg);
}
static void checkAndClearExceptionFromCallback(JNIEnv* env, const char* methodName) { //异常检测和排除
if (env->ExceptionCheck()) {
LOGE("An exception was thrown by callback '%s'.", methodName);
LOGE_EX(env);
env->ExceptionClear();
}
}
static void receive_callback(unsigned char *buf, int len) //回调
{
int i;
JNIEnv* env = AndroidRuntime::getJNIEnv();
jcharArray array = env->NewCharArray(len);
jchar *pArray ;
if(array == NULL){
LOGE("receive_callback: NewCharArray error.");
return;
}
pArray = (jchar*)calloc(len, sizeof(jchar));
if(pArray == NULL){
LOGE("receive_callback: calloc error.");
return;
}
//copy buffer to jchar array
for(i = 0; i < len; i++)
{
*(pArray + i) = *(buf + i);
}
//copy buffer to jcharArray
env->SetCharArrayRegion(array,0,len,pArray);
//invoke java callback method
env->CallVoidMethod(mCallbacksObj, method_receive,array,len);
//release resource
env->DeleteLocalRef(array);
free(pArray);
pArray = NULL;
checkAndClearExceptionFromCallback(env, __FUNCTION__);
}
public void Receive(char buffer[],int length){ //java层函数
String msg = new String(buffer);
msg = "received from jni callback" + msg;
Log.d("Test", msg);
}
jclass stu_cls = env->GetObjectClass(obj_stu); //或得Student类引用
if(stu_cls == NULL)
{
cout << "GetObjectClass failed \n" ;
}
//下面这些函数操作,我们都见过的。O(∩_∩)O~
jfieldID ageFieldID = env->GetFieldID(stucls,"age","I"); //获得得Student类的属性id
jfieldID nameFieldID = env->GetFieldID(stucls,"name","Ljava/lang/String;"); // 获得属性ID
jint age = env->GetIntField(objstu , ageFieldID); //获得属性值
jstring name = (jstring)env->GetObjectField(objstu , nameFieldID);//获得属性值
const char * c_name = env->GetStringUTFChars(name ,NULL);//转换成 char *
string str_name = c_name ;
env->ReleaseStringUTFChars(name,c_name); //释放引用
cout << " at Native age is :" << age << " # name is " << str_name << endl ;
jbyte * arrayBody = env->GetByteArrayElements(data,0);
jsize theArrayLengthJ = env->GetArrayLength(data);
BYTE * starter = (BYTE *)arrayBody;
jbyte * olddata = (jbyte*)env->GetByteArrayElements(strIn, 0);
jsize oldsize = env->GetArrayLength(strIn);
BYTE* bytearr = (BYTE*)olddata;
int len = (int)oldsize;
jbyte *by = (jbyte*)pData;
jbyteArray jarray = env->NewByteArray(nOutSize);
env->SetByteArrayRegin(jarray, 0, nOutSize, by);
char* data = (char*)env->GetByteArrayElements(strIn, 0);
jstring WindowsTojstring(JNIEnv* env, char* str_tmp)
{
jstring rtn=0;
int slen = (int)strlen(str_tmp);
unsigned short* buffer=0;
if(slen == 0)
{
rtn = env->NewStringUTF(str_tmp);
}
else
{
int length = MultiByteToWideChar(CP_ACP, 0, (LPCSTR)str_tmp, slen, NULL, 0);
buffer = (unsigned short*)malloc(length*2+1);
if(MultiByteToWideChar(CP_ACP, 0, (LPCSTR)str_tmp, slen, (LPWSTR)buffer, length) > 0)
{
rtn = env->NewString((jchar*)buffer, length);
}
}
if(buffer)
{
free(buffer);
}
return rtn;
}
JNIEXPORT jstring JNICALL Java_com_explorer_jni_SambaTreeNative_getDetailsBy
(JNIEnv *env, jobject jobj, jstring pc_server, jstring server_user, jstring server_passwd)
{
const char *pc = env->GetStringUTFChars(pc_server, NULL);
const char *user = env->GetStringUTFChars(server_user, NULL);
const char *passwd = env->GetStringUTFChars(server_passwd, NULL);
const char *details = smbtree::getPara(pc, user, passwd);
jstring jDetails = env->NewStringUTF(details);
return jDetails;
}
函数 | Java数组类型 | 本地类型 | 说明 |
---|---|---|---|
GetBooleanArrayElements | jbooleanArray | jboolean | ReleaseBooleanArrayElements释放 |
GetByteArrayElements | jbyteArray | jbyte | ReleaseByteArrayElements释放 |
GetCharArrayElements | jcharArray | jchar | ReleaseShortArrayElements释放 |
GetShortArrayElements | jshortArray | jshort | ReleaseBooleanArrayElements释放 |
GetIntArrayElements | jintArray | jint | ReleaseIntArrayElements释放 |
GetLongArrayElements | jlongArray | jlong | ReleaseLongArrayElements释放 |
GetFloatArrayElements | jfloatArray | jfloat | ReleaseFloatArrayElements释放 |
GetDoubleArrayElements | jdoubleArray | jdouble | ReleaseDoubleArrayElements释放 |
GetObjectArrayElement | 自定义对象 | object | |
SetObjectArrayElement | 自定义对象 | object | |
GetArrayLength | 获取数组大小 | ||
NewArray | 创建一个指定长度的原始数据类型的数组 | ||
GetPrimitiveArrayCritical | 得到指向原始数据类型内容的指针,该方法可能使垃圾回收不能执行,该方法可能返回数组的拷贝,因此必须释放此资源。 | ||
ReleasePrimitiveArrayCritical | 释放指向原始数据类型内容的指针,该方法可能使垃圾回收不能执行,该方法可能返回数组的拷贝,因此必须释放此资源。 | ||
NewStringUTF | jstring类型的方法转换 | ||
GetStringUTFChars | jstring类型的方法转换 | ||
DefineClass | 从原始类数据的缓冲区中加载类 | ||
FindClass | 该函数用于加载本地定义的类。它将搜索由CLASSPATH 环境变量为具有指定名称的类所指定的目录和 zip文件 | ||
GetObjectClass | 通过对象获取这个类。该函数比较简单,唯一注意的是对象不能为NULL,否则获取的class肯定返回也为NULL | ||
GetSuperclass | 获取父类或者说超类 。 如果 clazz 代表类class而非类 object,则该函数返回由 clazz 所指定的类的超类。 如果 clazz指定类 object 或代表某个接口,则该函数返回NULL | ||
IsAssignableFrom | 确定 clazz1 的对象是否可安全地强制转换为clazz2 | ||
Throw | 抛出 java.lang.Throwable 对象 | ||
ThrowNew | 利用指定类的消息(由 message 指定)构造异常对象并抛出该异常 | ||
ExceptionOccurred | 确定是否某个异常正被抛出。在平台相关代码调用 ExceptionClear() 或 Java 代码处理该异常前,异常将始终保持抛出状态 | ||
ExceptionDescribe | 将异常及堆栈的回溯输出到系统错误报告信道(例如 stderr)。该例程可便利调试操作 | ||
ExceptionClear | 清除当前抛出的任何异常。如果当前无异常,则此例程不产生任何效果 | ||
FatalError | 抛出致命错误并且不希望虚拟机进行修复。该函数无返回值 | ||
NewGlobalRef | 创建 obj 参数所引用对象的新全局引用。obj 参数既可以是全局引用,也可以是局部引用。全局引用通过调用DeleteGlobalRef() 来显式撤消。 | ||
DeleteGlobalRef | 删除 globalRef 所指向的全局引用 | ||
DeleteLocalRef | 删除 localRef所指向的局部引用 | ||
AllocObject | 分配新 Java 对象而不调用该对象的任何构造函数。返回该对象的引用。clazz 参数务必不要引用数组类。 | ||
getObjectClass | 返回对象的类 | ||
IsSameObject | 测试两个引用是否引用同一 Java 对象 | ||
NewString | 利用 Unicode 字符数组构造新的 java.lang.String 对象 | ||
GetStringLength | 返回 Java 字符串的长度(Unicode 字符数) | ||
GetStringChars | 返回指向字符串的 Unicode 字符数组的指针。该指针在调用 ReleaseStringchars() 前一直有效 | ||
ReleaseStringChars | 通知虚拟机平台相关代码无需再访问 chars。参数chars 是一个指针,可通过 GetStringChars() 从 string 获得 | ||
NewStringUTF | 利用 UTF-8 字符数组构造新 java.lang.String 对象 | ||
GetStringUTFLength | 以字节为单位返回字符串的 UTF-8 长度 | ||
GetStringUTFChars | 返回指向字符串的 UTF-8 字符数组的指针。该数组在被ReleaseStringUTFChars() 释放前将一直有效 | ||
ReleaseStringUTFChars | 通知虚拟机平台相关代码无需再访问 utf。utf 参数是一个指针,可利用 GetStringUTFChars() 获得 | ||
NewObjectArray | 构造新的数组,它将保存类 elementClass 中的对象。所有元素初始值均设为 initialElement | ||
SetArrayRegion | 将基本类型数组的某一区域从缓冲区中复制回来的一组函数 | ||
GetFieldID | 返回类的实例(非静态)域的属性 ID。该域由其名称及签名指定。访问器函数的GetField 及 SetField系列使用域 ID 检索对象域。GetFieldID() 不能用于获取数组的长度域。应使用GetArrayLength()。 | ||
GetField | 该访问器例程系列返回对象的实例(非静态)域的值。要访问的域由通过调用GetFieldID() 而得到的域 ID 指定。 | ||
SetField | 该访问器例程系列设置对象的实例(非静态)属性的值。要访问的属性由通过调用SetFieldID() 而得到的属性 ID指定。 | ||
GetStaticFieldID | |||
GetStaticField | |||
SetStaticField | 同上,只不过是静态属性。 | ||
GetMethodID | 返回类或接口实例(非静态)方法的方法 ID。方法可在某个 clazz 的超类中定义,也可从 clazz 继承。该方法由其名称和签名决定。 GetMethodID() 可使未初始化的类初始化。要获得构造函数的方法 ID,应将 作为方法名,同时将void (V) 作为返回类型。 | ||
CallVoidMethod | |||
CallObjectMethod | |||
CallBooleanMethod | |||
CallByteMethod | |||
CallCharMethod | |||
CallShortMethod | |||
CallIntMethod | |||
CallLongMethod | |||
CallFloatMethod | |||
CallDoubleMethod | |||
GetStaticMethodID | 调用静态方法 | ||
CallMethod | |||
RegisterNatives | 向 clazz 参数指定的类注册本地方法。methods 参数将指定 JNINativeMethod 结构的数组,其中包含本地方法的名称、签名和函数指针。nMethods 参数将指定数组中的本地方法数。 | ||
UnregisterNatives | 取消注册类的本地方法。类将返回到链接或注册了本地方法函数前的状态。该函数不应在常规平台相关代码中使用。相反,它可以为某些程序提供一种重新加载和重新链接本地库的途径。 |
JNI使用改进的UTF-8字符串来表示不同的字符类型。Java使用UTF-16编码。UTF-8编码主要使用于C语言,因为它的编码用\u000表示为0xc0,而不是通常的0×00。非空ASCII字符改进后的字符串编码中可以用一个字节表示
JNI不会检查NullPointerException、IllegalArgumentException这样的错误,原因是:导致性能下降
在绝大多数C的库函数中,很难避免错误发生。
JNI允许用户使用Java异常处理。大部分JNI方法会返回错误代码但本身并不会报出异常。因此,很有必要在代码本身进行处理,将异常抛给Java。在JNI内部,首先会检查调用函数返回的错误代码,之后会调用ExpectOccurred()返回一个错误对象
jthrowable ExceptionOccurred(JNIEnv *env);
例如:一些操作数组的JNI函数不会报错,因此可以调用ArrayIndexOutofBoundsException或ArrayStoreExpection方法报告异常。
JNI/NDK开发指南
Android JNI局部引用表溢出:local reference table overflow (max=512)
(4.1.27.1)Android—简单的JNI实例
JNI与Android VM之间的关系