现在的位置: 首页 > 综合 > 正文

JNI总结

2013年02月09日 ⁄ 综合 ⁄ 共 10818字 ⁄ 字号 评论关闭

  公司要做一个云存储的产品,这个产品需要支持PC、MAC、ANDROID、IOS平台,使用C++开发了一个动态库,在各个平台编译后由其他的客户端使用,现在C++库都已经写好了,对于PC、MAC、IOS平台来说可以直接使用C++版本,但是在android上由于使用这个库的客户端采用java编码,因此就需要将C++库暴露出来的接口,使用jni编译成一个动态库使得在java中也可以调用。这个就是需求了。

  在这一个月的时间内,陆陆续续的了解JNI接口的文档,然后编写这些代码,现在工程是完成了,是时候来总结一下了。

  大纲:

  1、从HelloWorld开始

  2、主要使用到的函数总结

  3、引用类型

  4、异常处理机制

  5、多线程

  6、回调实现

  7、从C++中启动java class程序

  8、C++库封装问题

  9、android模拟环境

  在做所有的介绍之前,让我们先找找JNI的权威文档,在google中输入jni就得可以:http://docs.oracle.com/javase/1.5.0/docs/guide/jni/spec/jniTOC.html 当遇到任何难题时首先应该想起的就是到这里查找答案。接着让我们来找找有没有好用的中文资料,在百度文库中输入"JNI编程指南"可以找到一个PDF,总共78页,这些内容可以让你很快的入门。

 一、开发一个HelloWorld

  我们以HelloWorld为例来介绍下,在java之中调用C++的helloworld过程:

  1、在eclipse之中创建一个新的Project取名为JniHelloWorld,然后创建一个Helloworld.java,包取名为helloworld

  package helloworld;

  public class Helloworld {
    public native void helloworld();
    static {
      System.loadLibrary("JniHelloWorld");
    }
    public static void main(String[] args){
      Helloworld helloworld = new Helloworld();
      helloworld.helloworld();
    }
  }

  可以看到其中的创建了一个Helloworld类,有一个native申明的helloworld函数,这个函数就是JNI的本地函数,在java代码之中没有实现而是在C++代码之中实现。

  另外注意一个静态调用 System.loadLibrary("JniHelloWorld") 在这里我们加载使用C++实现的动态库JniHelloWorld.dll在其中有本地函数helloworld的实现代码。

  1.1 使用javac、javah来编译Helloworld.java文件和生成jni头文件

    先来配置下javac、javah工具,在jdk的安装目录下还有一个bin目录,如我的jdk安装目录为:D:\java 则在 D:\java\bin 目录下可以找到 javac.exe,javah.exe

    为了使用方便,可以将其 D:\java\bin 添加到环境变量 path 之中(我的电脑 右键菜单属性,左边栏高级系统设置,弹出的对话框中可以看到右下角有一个环境变量按钮,点击进去之后在用户变量与系统变量之中都有Path属性,只要把D:\java\bin 添加到path的后面即可,然后打开命令行,输入path查看设置是否正确如: D:\MinGW\bin;D:\java\bin; )

    现在使用javac来编译Helloworld.java为:打开命令行,进入工程目录如:E:\eclipse_java\maxthon\JniHelloWorld\src  然后执行如下命令:

    javac helloworld/Helloworld.java   生成一个Helloworld.class

    javah helloworld.Helloworld   生成一个helloworld_Helloworld.h 头文件,这个头文件在C++的工程之中使用

  2、在Vistual studio 2010 之中创建一个工程取名为 JniHelloWorld,在工程设置中选择:动态库DLL与空项目 选项

  接着设置项目属性:VC++属性 -》包含目录  将JNI的头文件目录添加到其中,如 D:\java\include D:\java\include\win32  再把javah生产的helloworld_Helloworld.h 头文件目录页添加到其中,如 E:\eclipse_java\maxthon\JniHelloWorld\src

  现在添加一个JniHelloworld.cpp 文件,内容如下:  

  #include "jni.h"
  #include "helloworld_Helloworld.h"
  #include <stdio.h>

  JNIEXPORT void JNICALL Java_helloworld_Helloworld_helloworld(JNIEnv * env, jobject obj)
  {
    printf("Jni hello world!\n");
  }

  编译后产生JniHelloWorld.dll,将其拷贝到java JniHelloWorld的工程目录如:E:\eclipse_java\maxthon\JniHelloWorld

  现在工作完成了,运行java工程应该就有输入:Jni hello world! 

二、主要使用到的函数总结

  Jni的本地代码对java VM的访问通过jni 接口,而jni接口是通过一个接口指针传递的,也就是JNIEnv* ,也就是每个本地函数的第一个参数,JNIEnv* 指针指向一个函数列表,通过这些列表中的函数可以实现各种功能。

  2.1 字符串

    java中的字符串以unicode方式保存,但是通过JNI接口也可以获得UTF格式的字符串。

    GetStringUTFChars、ReleaseStringUTFChars 这两个函数用于获取UTF格式字符串,以及释放该字符串(之所以会有释放函数是由于java之中采用unicode编码,要返回UTF编码字符串在GetStringUTFChars函数中会创建一个临时存储空间用于存放这个UTF格式字符串,这个创建的空间需要显示释放)

    获取字符串长度: GetStringLength 

    创建字符串对象:NewStringUTF

    获取字符串块:GetStringUTFRegion

  2.2 数组

    JNI之中将数据分为基本类型与对象类型,基本累就也就是 int、long、char 等这些预定义类型,而对象类型也就是引用类型,如字符串、数组、各种类对象等。对于每种基本类型JNI都预定义了相应的函数如:Get/Release<Type>ArrayElement 用于获取与释放成员,另外使用 Get/Set<Type>ArrayRegion用于获取与设置多个成员。另外使用New<Type>Array可用于创建基本类型数组。

    在 JNI之中,对象使用Object表示,获取对象数组成员为:Get/SetObjectArrayElement 由于在java之中对象都是引用,也就不存在创建新的缓存因此也就没有Release函数了。使用NewObjectArray用于创建对象数组。

    另外不管是基本类型还是对象类型,都可以使用GetArrayLength函数来获取数组大小。

  2.3 其他

三、关于引用

   在写JNI本地代码时,比较容易出错的地方就是引用。对于所有的基本类型,直接在java与本地代码之间拷贝,而对于对象类型则使用引用来传递。在虚拟机(VM)之中每一个从java传递到本地代码的对象都有一个记录,这些记录着的对象将不会被回收,只有当这些记录被释放后垃圾收集器才能回收这些对象。

  JNI之中有三种引用类型,局部引用、全局引用、弱引用。

  局部引用只在本地函数内部有效,在函数返回时会被自动释放,因此在绝大多数情况下可以依靠该机制进行垃圾回收,但是完全依赖这种方式有时会有问题,比如:1)在创建了一个大型的java对象,若在使用完成后没有及时释放则会一直占用着内存直到函数返回,2)大量的局部引用本身也会占用虚拟机内存空间,比如进行一个大数据量的循环,可能产生巨量的局部引用,而每个引用本身也需要占用内存,最终可能导致内存不够的情况发生。因此对于局部引用明智的作法就是在不使用的时候总是显示删除。

  局部引用只在当前线程与当前函数内部有效,而全局引用与弱引用则是全局与跨线程有效的,他们可以通过一个局部引用创建。而全局引用与弱引用的区别在于:全局引用在释放前会阻止引用的对象被垃圾回收,而弱引用则不会(弱引用即使没有显示释放,所引用的对象也可能被垃圾回收)。

  释放局部引用:DeleteLocalRef;从局部引用创建与释放全局引用:New/DeleteGlobalRef;从局部引用创建弱引用:New/DeleteWeakGlobalRef;

  现在有一个问题,既然弱引用所引用的对象可能被垃圾回收,那么怎么知道所引用的对象是否被回收呢,JNI提供了一个函数 env->IsSameObject(weakObject,NULL) 若返回 JNI_TRUE 则表示已经被回收。

四、异常处理

  JNI的本地代码可以抛出任何异常,也可以处理从java代码中产生的异常,若产生的异常没有得到处理,最终将传递到虚拟机。通常情况下,函数直接通过返回码来表示异常。但是有如下两种情况下需要调用ExceptionOccurred函数来检查是否有异常产生:1)调用Java函数时;2)某些数组访问函数,不返回错误码而是直接产生如 ArrayIndexOutOfBoundsException、ArrayStoreException。

  当产生异常时,本地代码应该在调用其他JNI函数前处理这些异常。处理异常的相关函数为:ExceptionOccurred - 判断是否有异常发生, ExceptionDescribe - 打印异常堆栈信息, ExceptionClear - 清除异常,下面是一个示例代码:  

  ///这里调用java的一个函数,执行完成后需要检查是否产生了异常
  env->CallVoidMethod(obj,mid);
  exc = env->ExceptionOccurred();
  if(exc)
  {
    ///获取异常类信息,然后调用其的toString方法以获取相应的描述
    jclass jcls = env->GetObjectClass(exc);
    jmethodID mid = env->GetMethodID(jcls,"toString","()Ljava/lang/String;");
    jstring exp_msg = (jstring)env->CallObjectMethod(exc,mid);
    const char* pmsg = env->GetStringUTFChars(exp_msg,NULL);
    printf("EXCEPTION:%s\n",pmsg);
    env->ReleaseStringUTFChars(exp_msg,pmsg);

    ///这里也可以再JNI本地代码中产生一个异常,重新抛出
    jclass newExcCls;
    env->ExceptionDescribe();
    env->ExceptionClear();
    newExcCls = env->FindClass("java/lang/IllegalArgumentException");
    if(newExcCls == NULL) return;
    env->ThrowNew(newExcCls,"throw from c code");
}

五、多线程支持

  在多线程之中若需要使用到jni相关的函数,则需要注意获取jni接口指针的问题,也就是本地函数的第一个参数JNIEnv*指针。由于本地函数的JNIEnv*指针不能保存起来使用,因此需要通过其他方式。

  JNI函数中提供了一个GetEnv函数可以用于获取这个接口指针,原型如下:

  jint GetEnv(JavaVM *vm, void **env, jint version);

  但是这里的JavaVM指针又要从哪里来呢?答案就是使用 GetJavaVM 函数原型如下:

  jint GetJavaVM(JNIEnv *env, JavaVM **vm); 

  示例代码:  

  static JavaVM *jvm;

  jint start(JNIEnv * env, jobject obj, jstring host)
  {
    int status = env->GetJavaVM(&jvm);
    if(status != 0) {
      printf("Start get java vm error!\n");
      return MX_CS_ERROR_CODE_CANT_GET_JVM;
    }
    return status;
  }

  jint doint()
  {
    JNIEnv *env;
    if(jvm->AttachCurrentThread((void**)&env, NULL) !=0)
    {
      printf("get env error\n");
      return;
    }
    ......
    jvm->DetachCurrentThread();
  }

  ///注:使用GetJavaVM函数将虚拟机示例保存到一个全局的静态变量之中,然后再任何地方都可以使用AttachCurrentThread来后去JNIEnv* 指针。

六、回调实现

  在异步模式开发的程序之中,回调是非常常见的,若使用JNI来实现回调可以采用如下方法:

  1)在java中申明一个接口类,定义好各个回调函数的格式,然后定义实现该接口的具体处理类 如:  

  public interface FileSyncCallback {
    public int on_return(String user_id,String path,int err);
  }

  class FileSyncNotifyCallback implements FileSyncCallback {
    public int on_return(String user_id,String path,int err)
    {
      String string =String.format("Return:user_id(%s),path(%s),err(%d)",

        user_id,path,err);
      System.out.println(string);
    }
  }

  2)将FileSyncNotifyCallback的对象传递传递到本地函数之中如:

  FileSyncNotifyCallback callback = new FileSyncNotifyCallback();

  fileSync.put_file_nonblock(user_id,"zcs", callback);

  3)本地代码之中,获取callback参数的类型,然后查找on_return函数ID,再调用即可

七、从C++之中启动JVM

  从C++中启动JVM网络上也有很多代码,这里有一种简单的实现,代码如下:

  #include "jni.h"

  #include <stdio.h>
  #include <stdlib.h>
  #include "windows.h"

  #define USER_CLASSPATH "E:/eclipse_java/maxthon/JniTest/src"

  int main(int argc,char* argv)
  {
    JNIEnv* env;
    JavaVM *jvm;
    jint res;
    jclass cls;
    jmethodID mid;
    jstring jstr;
    jclass stringClass;
    jobjectArray args;

    JavaVMInitArgs vm_args;
    JavaVMOption option[1];
    option[0].optionString = "-Djava.class.path="USER_CLASSPATH;
    vm_args.version = JNI_VERSION_1_6;
    vm_args.options = option;
    vm_args.nOptions = 1;
    vm_args.ignoreUnrecognized = JNI_TRUE;

    ///
    HINSTANCE hJvm = LoadLibrary(L"D:/java/jdk7/jre/bin/client/jvm.dll");
    if(hJvm == NULL)
    {
      printf("LoadLibrary error %d\n",GetLastError());
      return -1;
    }
    typedef jint (WINAPI *PFunCreateJavaVM)(JavaVM **, void **, void *);

    PFunCreateJavaVM funCreateJVM = (PFunCreateJavaVM)GetProcAddress(hJvm,"JNI_CreateJavaVM");
    if(funCreateJVM == NULL)
    {
      FreeLibrary(hJvm);
      printf("Cant get CreateJVM func\n");
      return -1;
    }

    res = (*funCreateJVM)(&jvm,(void**)&env,&vm_args);

    FreeLibrary(hJvm);
    if(res <0)
    {
      printf("Cant create java VM\n");
      exit(1);
    }

    cls = env->FindClass("MyJni/MyJni");
    if(cls == NULL)
    {
      printf("Cant get class:MyJni/MyJni\n");
      goto destory;
    }

    mid = env->GetStaticMethodID(cls,"main","([Ljava/lang/String;)V");
    if(mid == NULL)
    {
      printf("Cant get methid:main\n");
      goto destory;
    }

    jstr = env->NewStringUTF("from C!");
    if(jstr == NULL)
    {
      printf("Cant New String\n");
      goto destory;
    }

    stringClass = env->FindClass("java/lang/String");
    args = env->NewObjectArray(1,stringClass,jstr);
    if(args == NULL)
    {
      printf("Cant create Object Array\n");
      goto destory;
    }

    env->CallStaticVoidMethod(cls,mid,args);
    goto destory;

    destory:
    if(env->ExceptionOccurred())
    {
      env->ExceptionDescribe();
    }
    jvm->DestroyJavaVM();

  }

  注意事项:1)这个工程需要使用到jni.h 因此也就需要添加jdk的引用目录 如:D:\java\include;D:\java\include\win32;

  2)class文件目录也就是程序开头定义的 USER_CLASSPATH,注意的是若只写出了java文件,需要使用javac将其编译成class文件才能使用;

  3)动态加载 jvm.dll,这个库目录也是在jdk安装目录之中 如:D:/java/jdk7/jre/bin/client/jvm.dll

八、库封装问题

九、安卓模拟环境

  总体上有几个步骤:

  准备工作:

    1)安装Cygwin Terminal工具,在windows下模拟linux环境,其中有很多linux下的工具

    2)安装安卓ndk(native development kit 本地开发包),现在的版本是android-ndk-r8b,ndk的作用就是编译在安卓下java可以调用的动态库,文档位置:安装目录下的 docs 目录

    3)安装安装sdk,这个是安卓开发工具包,里面还自带了一个在PC上可以运行的模拟器,开发出来的程序不需要再移动设备上就可以运行,文档位置:安装目录下的 docs 目录

    4)给eclipse安装安卓项目开发插件adt,安装地址:http://dl-ssl.google.com/android/eclipse/

  编译动态库:

    1)编译JNI动态库,首先在ecipse之中创建一个android项目,在项目目录下,建立一个jni目录,将jni的本地代码放入其中,然后写一个 Android.mk 文件用于编译jni本地代码的动态库,如:      

    LOCAL_PATH := $(call my-dir)

    include $(CLEAR_VARS)
    TARGET_PLATFORM := android-5
    LOCAL_MODULE := filesyncjni
    LOCAL_C_INCLUDES := ../src D:/Program_File/android-ndk-r8b/sources/cxx-stl/gnu-libstdc++/4.6/include D:/Program_File/android-  ndk-r8b/sources/cxx-stl/gnu-libstdc++/4.6/libs/armeabi/include
    LOCAL_SRC_FILES := android_filesync_FileSync.cpp
    LOCAL_LDLIBS := D:/Program_File/android-ndk-r8b/sources/cxx-stl/gnu-libstdc++/4.6/libs/armeabi/libgnustl_static.a -L$(SYSROOT)/usr/lib -L. -L./libs/armeabi -llog -lc -lstdc++
    include $(BUILD_SHARED_LIBRARY)

    接着就可以使用NDK中的工具 ndk-build 进行编译了,如:假设已经设置好了NDK的环境变量为ndk的安装目录,则进入安卓项目的jni目录执行:$NDK/ndk-build ,顺利的话就可以产生可以再安卓模拟器之中使用的动态库了。

    2)若JNI动态库还需要依赖另外的库,则需要注意:编写依赖库的makefile文件,这个文件用于在ndk之中编译动态库,注意:g++编译器必须使用ndk中自带的,编译时候选择的platforms时为了兼容性,尽量选择较早的平台,如:

    NDKROOT=D:/Program_File/android-ndk-r8b   

    CPP=arm-linux-androideabi-g++ --sysroot=${NDKROOT}/platforms/android-5/arch-arm
    #STRIP=arm-linux-androideabi-strip  使用strip工具可以较少编译出来动态库的大小

    3)模拟器设置

    在SDK的安装目录下,可以看到AVD Manager.exe、SDK Manager.exe,其中SDK Manager.exe用于SDK的升级即安装使用,另外的AVD Manager.exe 就是模拟器管理工具。

    假设sdk安装目录为:D:\Program_Files\Android\android-sdk 并且已经设置好sdcard的位置为 d:/mysdcard.img

    启动模拟器:进入 D:\Program_Files\Android\android-sdk\tools 目录,执行:./emulator -avd avd2.3 -sdcard d:/mysdcard.img  启动之后的窗口不要关闭,否则模拟器也会相应关闭。(使用avd表示启动相应的模拟器,sdcard表示启用sdcard)

    打开shell命令行,当模拟器启动完成之后:进入 D:\Program_Files\Android\android-sdk\platform-tools 目录,执行 ./adb.exe shell 当成功时界面上将出现#号,等待命令输入,这时候执行ls,可以查看所有目录及文件,cd sdcard 目录则可以查看sdcard目录中的文件。

    4)创建的安卓项目,默认是没有权限访问sdcard与网络的,因此若需要访问则要申请权限:打开项目下的 AndroidManifest.xml 文件加入     

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.INTERNET"/>

    Ok,现在可以启动安卓项目了。

 

 

抱歉!评论已关闭.