公司要做一个云存储的产品,这个产品需要支持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,现在可以启动安卓项目了。