以前也简单用过JNI,但是只是简单用一下,好多都不明白。最近在看源码部分,有涉及到JNI调用的,所以这次打算彻底把它搞定。
先普及一下JNI的调用关系:JAVA------------------------>JNI------------------------------->Native.
我们需要从我们的入口代码写起,我们先来一段含有native函数的简单类:
package com.sahadev.regix; public class Hello { public static String getStringFromNative() { Bean createBean = createBean(); System.out.println(createBean.tag); return createBean.tag; } public static native Bean createBean(); }
这段代码就是说从Native中生成了一个Bean对象,然后将这个对象的tag属性返回给调用者。
所以这里有两个知识点可以学到:
1.如果生成一个自定义对象
2.如何在native中生成String字符串对象
Ok,既然Hello这个类有了createBean这个native方法,则我们需要使用Jdk提供的javah命令来生成C/C++所需要的头文件,Javah命令的说明如下:
用法: javah [options] <classes> 其中, [options] 包括: -o <file> 输出文件 (只能使用 -d 或 -o 之一) -d <dir> 输出目录 -v -verbose 启用详细输出 -h --help -? 输出此消息 -version 输出版本信息 -jni 生成 JNI 样式的标头文件 (默认值) -force 始终写入输出文件 -classpath <path> 从中加载类的路径 -bootclasspath <path> 从中加载引导类的路径 <classes> 是使用其全限定名称指定的 (例如, java.lang.Object)。
在我们正常使用的时候只需要简单的几个参数即可,我们以Hello这个类来举例说明:
javah -d E:\Kongfuzi\HelloJNI com.sahadev.regix.Hello
javah最基本的,不需要多说。-d 前面的用法已经说明,用于指定输出目录,我这里的输入目录是:E:\Kongfuzi\HelloJNI,最后是我们要对那个类操作的全路径名称,必须写全包名,最后需要注意的一点是,必须在com包名的上一级目录执行,一般是src目录,否则会出现如下错误:
E:\Kongfuzi\HelloJNI\src\com>javah -d E:\Kongfuzi\HelloJNI com.sahadev.regix.Hello 错误: 找不到 'com.sahadev.regix.Hello' 的类文件。
上面是个错误的例子,我现在位于src的下级目录com,注意不可以。
当这个命令执行完成之后,会在我们指定的目录生成一个以.h结尾的头文件:
打开它:
/* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class com_sahadev_regix_Hello */ #ifndef _Included_com_sahadev_regix_Hello #define _Included_com_sahadev_regix_Hello #ifdef __cplusplus extern "C" { #endif /* * Class: com_sahadev_regix_Hello * Method: createBean * Signature: ()Lcom/sahadev/regix/Bean; */ JNIEXPORT jobject JNICALL Java_com_sahadev_regix_Hello_createBean (JNIEnv *, jclass); #ifdef __cplusplus } #endif #endif
这是一段C++代码,上面的注释说,不要去编辑这个文件,这是机器自动生成的。看看它为我们自动生成了什么:
红色箭头指出的便是我们在Java文件中自己定义的。好,接下来我们来实现它:
我们在工程中添加名为jni的文件夹(如果安装了NDK支持的话,可以一键自动生成相关文件,这里就不多介绍了)。
然后在其中添加Android.mk、com_sahadev_regix_Hello.h,再新建一个名叫JNI_C++.cpp的文件:
打开JNI_C++.cpp:
写入如下代码:
//============================================================================ // Name : JNI_C++.cpp // Author : Sahadev // Version : // Copyright : Your copyright notice // Description : Hello World in C++, Ansi-style //============================================================================ #include <jni.h>//引入必须的JNI支持文件 #include "com_sahadev_regix_Main.h"//引入头文件 JNIEXPORT jobject JNICALL Java_com_sahadev_regix_Main_createBean(JNIEnv *env, jclass jc) { jclass jcl1 = env->FindClass("com/sahadev/regix/Bean"); //载入Bean类,注意写全包名,jclass 即为JAVA中的Class,代表类 jmethodID cMethodID = env->GetMethodID(jcl1, "<init>","(Ljava/lang/String;)V");//获取构造方法的ID,第一个参数是从哪个类获取,第二个参数<init>代表这是构造方法,第三个参数代表传入的参数是String,返回值为Void,这里的写法可以从JNI的文档中找到 return env->NewObject(jcl1, cMethodID,env->NewStringUTF("Hello,JNI!This is sahadev!"));//最后通过调用这个方法传入一个jString的字符串 }
我们将头文件引入,然后拷贝头文件中的native方法,添加参数变量,添加方法体。
上面的代码后面有一部分注释,这里补充一下:findClass和Java中的ClassLoader差不多,jclass等于Java中的Class,在这里调用方法的话需要先得到方法的ID,就像第二行使用了GetMethodID来获取,这三个参数分别代表:获取哪个类的方法,构造方法传<init>普通方法写方法名,第三个参数则代表参数个数类型返回值,最后通过NewStringUTF来生成jString,注意:在c++直接写出的字符串它并不是Java中的字符串,必须通过NewStringUTF方法来实现,最后通过调用NewObject方法来调用Bean的构造方法,并传入参数jString来生成一个Bean对象,并返回给调用者。具体资料请参见:JNI官方文档
好,我们的C++文件写好了,接下来就需要对它进行编译了,这里的编译工具不是什么GUN,而是Android官方提供的NDK-BUILD工具,如果是在Eclipse中集成了NDK环境,可以使用clean功能直接对它进行编译,我们这里只介绍手动编译:
首先我们需要编辑Android.mk文件:
LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := JNI_C++ LOCAL_SRC_FILES := JNI_C++.cpp include $(BUILD_SHARED_LIBRARY)
LOCAL_PATH即为调用命令的所在目录,你在哪个目录下使用cmd命令,这里就会返回它的路径地址
LOCAL_MODULE你生成的文件名称是什么,输出之后会自动在名称的前后加上lib和.so
LOCAL_SRC_FILES要对哪个文件进行编译。
http://developer.android.com/intl/zh-cn/ndk/guides/android_mk.html官方文档对上面的命令做了详细解释。
接下来,需要进入通过cmd命令进入工程所在的目录,然后使用命令ndk-build编译即可,如果ndk-build目录没有添加入本地变量,则使用全路径访问这个命令。我当时不知道这个命令如何使用,就进入了ndk-build所在的目录,想看看它的帮助说明,结果出现了这样的提示:
E:\Android_Sdk\android-ndk-r10e>ndk-build Android NDK: Could not find application project directory ! Android NDK: Please define the NDK_PROJECT_PATH variable to point to it. E:\Android_Sdk\android-ndk-r10e\build/core/build-local.mk:143: *** Android NDK: Aborting . Stop.
说没有发现应用的工程目录,于是我就去build/core/build-local.mk文件中的143行查看是什么原因:
ifndef NDK_PROJECT_PATH NDK_PROJECT_PATH := $(call find-project-dir,.,jni/Android.mk) endif ifndef NDK_PROJECT_PATH NDK_PROJECT_PATH := $(call find-project-dir,.,AndroidManifest.xml) endif ifndef NDK_PROJECT_PATH $(call __ndk_info,Could not find application project directory !) $(call __ndk_info,Please define the NDK_PROJECT_PATH variable to point to it.) $(call __ndk_error,Aborting) endif
根据上下文,应该是没有找到jni/Android.mk或者AndroidManifest.xml之类的文件,再根据上下文发现有这么一段说明:
# ==================================================================== # # If NDK_PROJECT_PATH is not defined, find the application's project # path by looking at the manifest file in the current directory or # any of its parents. If none is found, try again with 'jni/Android.mk' # # Note that we first look at the current directory to avoid using # absolute NDK_PROJECT_PATH values. This reduces the length of all # source, object and binary paths that are passed to build commands. # # It turns out that some people use ndk-build to generate static # libraries without a full Android project tree. # # If NDK_PROJECT_PATH=null, ndk-build make no attempt to look for it, but does # need the following variables depending on NDK_PROJECT_PATH to be explicitly # specified (from the default, if any): # # NDK_OUT # NDK_LIBS_OUT # APP_BUILD_SCRIPT # NDK_DEBUG (optional, default to 0) # Other APP_* used to be in Application.mk # # This behavior may be useful in an integrated build system. # # ====================================================================
也就是说如果你没有定义NDK_PROJECT_PATH的话,它就会根据manifest文件去寻找工程路径,这里的NDK_PROJECT_PATH你可以在环境变量中指定:
这样的话,就可以在任何地方直接使用ndk-build命令对工程目录进行编译了,设置完成之后请重启cmd:
好,编译完成之后我们就可以在我们工程目录的obj目录下发现编译好的.so等相关文件:
注意,我们在Android.mk文件中定义的LOCAL_MODULE是JNI_C++,而这里的输出文件会为它自动加上lib前缀与.so后缀。
如果集成了NDK环境,在Clean的时候会自动进行编译:
编译完成之后,就需要看看如何使用它了。
在使用的Java文件Hello中添加如下静态代码块:
static { System.loadLibrary("JNI_C++"); }
在这里的JNI_C++便是我们在.mk文件中定义好的,在使用它的时候,它会自动为我们加上lib前缀与.so后缀,以便访问我们的.so文件,注意,千万别自己加lib*.so,以下是Runtime.loadLibrary的方法说明:
Given the name "MyLibrary", that string will be passed to System.mapLibraryName. That means it would be a mistake for the caller to include the usual "lib" prefix and ".so" suffix.
在Activity中调用Hello这个类的getStringFromNative方法,这个方法便可以将从native中生成的字符串对象、Bean对象返回。
效果:
快试试,遇到什么问题欢迎留言。