1、前言
jni是java调用原生语言来进行开发的一座桥梁,原生语言一般是指c,c++语言,即jni机制可以让java语言调用c,c++语言,也可以让c,c++语言调用java语言。这样的相互调用,互相结合,主要是出现在对性能要求较高的应用上。在android中,由于它的开发语言也是java,所以也可以利用原生语言进行开发,对jni的了解和使用有助于我们在做应用的时候,对于时间性能要求较高的代码段,可以用原生语言来开发,然后java通过jni机制来调用它,就可以达到很好的效果。
总的来说,jni是一种机制,一种可以然java和原生语言互相调用的机制。
学习jni开发需要有c++或者c基础。
2、基本环境要求
利用jni机制开发,对于开发环境是有特别要求的,因为c,c++语言的开发也需要环境的支持。
单纯的android开发:jdk,sdk,eclipse,adt。
如果要在eclipse上进行jni开发,那么需要:ndk,cygwin,ant。
本文接下来的例子都是使用eclipse的方法进行开发,使用android studio的朋友,由于编译器自身的集成,利用jni机制进行开发会更加方便,但是为了更深入的了解jni机制,本文使用的是eclipse。
对于android的基本开发环境的安装就不提了,jni需要的开发环境这里就提一下,方便完全没基础的朋友安装环境。
2.1 ant的安装
ant是一个命令行构建工具,它主要是驱动目标进行任务的进行。在jni中一般是用来驱动命令。它的安装可以去ant下载官网下载。一般下载zip压缩包即可。然后解压安装到我们指定的目录(自由的目录位置)。最关键的一步是添加环境变量。
假如你的ant根目录是E:\apache-ant-1.9.7。那么你只需要复制这个文件夹下的bin路径,添加到高级环境变量的path变量中。注意与path变量本身存在的值必须添加分号进行分割。
比如:
随后我们用命令行工具输入ant -version命令。如果安装成功就会输出ant的版本号。
2.2cygwin的安装
由于android原生语言开发环境包(NDK)是基于类unix系统运行的,它包含了许多shell脚本,而它们又是不可以直接在window系统运行的,因此需要安装cygwin模拟类unix系统的环境。cygwin的下载地址
由于笔者的安装环境以及完成,所以并不能图文并茂德截图讲解。为了方便大家的安装,笔者决定直接使用参考资料的截图。
读者只需要按照笔者的截图顺序去做就可以安装成功了,cygwin一定要安装成功,否则会导致ndk运行的失败,这也是为什么笔者花这么多图的原因。
2.3 NDK的安装
NDK即android原生语言开发环境包。下载地址:NDK的下载
我们下载好压缩包解压到我们指定的目录,然后需要添加环境变量,比如笔者的NDK安装在:E:\android-ndk-r11b。那么只需要在path变量里面添加此路径即可,记得用分号分隔。
随后我们在命令行窗口输入ndk-build命令。如果有以下输出说明安装成功。
2.4 在eclipse里面配置jni开发环境
需要指定NDK的目录路径。笔者的如下:
3、实践过程进行总结
实践才是检验真理的唯一标准,通过上面的环境配置,我们就可以搭建一个可以开发jni的android环境了,接下来就一步一步引导大家去开发一个jni实例。
首先新建一个空白的android项目。
我们新建一个类,专门处理native函数。笔者命名为CppUtils。内容如下:
public class CppUtils { static { System.loadLibrary("cppUtils"); } /** * 从CPP获取字符串 * * @return */ public native String getStringFromCPP(); }
这里是简单的从c++原生代码中获取字符串。
3.1 System.loadLibrary
java在java.lang.System包提供了两个静态方法用于加载共享库(一种含原生语言实现的可供android程序调用的库),分别是load,loadLibrary两个方法,由于我们在程序启动的时候就需要加载共享库,因此放在静态代码块中加载。这两个方法的参数是共享库名称。注意共享库为了跨平台使用,它的文件名称会包含一些前缀,而共享库文件的后缀是so。比如我们加载cppUtils共享库,其实他的全名是:libcppUtils.so。
3.2 native标签
native标签用于告诉java编辑器,它的方法是由原生语言实现的,因此不需要去实现它。native方法用分号结束。即如果你希望一个方法用原生语言实现,那么你就给它声明为native方法。
3.3 生成原生语言头文件
由于原生语言头文件需要根据字节码文件来进行分析,所以,在生成头文件之前,我们必须对项目进行build。之后打开我们项目的bin/classes的文件夹,笔者的文件夹如下图:
接着我们就要针对CppUtils.class进行分析生成头文件,在我们对编写原生语言头文件的时候,最好借助工具生成,而不是手写,这样出错的概率才会更低,否则很容易发生jni桥无法将java函数与原生方法联系起来的错误。生成头文件的方法,就是使用命令行工具。比如笔者这里就是,先进入自己项目要分析的java文件的目录下,然后生成头文件。生成头文件的命令如下:
javah -classpath bin/classes com.example.jnibolg.CppUtils
完整的操作过程看下图:
然后回到eclipse中,刷新下我们的项目,我们会发现多了一个以h结尾的文件,这个就是机器生成的头文件。
关于原声函数的实现以及祥光头文件,我们需要放在jni文件夹中,因此,我们接下来需要在项目中建立jni文件夹,并将相关文件放进去。
这样我们的原生文件就生成了。
3.4 分析头文件
接下来我们需要分析头文件的内容,有助于帮助我们了解整个实现过程。首先我们看头文件的代码:
/* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class com_example_jnibolg_CppUtils */ #ifndef _Included_com_example_jnibolg_CppUtils #define _Included_com_example_jnibolg_CppUtils #ifdef __cplusplus extern "C" { #endif /* * Class: com_example_jnibolg_CppUtils * Method: getStringFromCPP * Signature: ()Ljava/lang/String; */ <pre name="code" class="cpp">JNIEXPORT jstring JNICALL Java_com_example_jnibolg_CppUtils_getStringFromCPP (JNIEnv *, jobject);
#ifdef __cplusplus}#endif#endif 首先我们可以看到jni.h头文件被包含了,这个头文件包含了jni机制为了实现从java对象到原生语言的映射的规则。因此,我们一切java调用原生函数,或者原声函数调用java,都必须通过它来实现。
其次我们关注这个函数声明:
JNIEXPORT jstring JNICALL Java_com_example_jnibolg_CppUtils_getStringFromCPP (JNIEnv *, jobject);
这个函数声明说明了,它实现的是jnibolg包下的CppUtils类的getStringFromCPP方法,返回的是jstring类型,这是一个jni类型,映射到java的string类型。这里不详细解析jni类型映射,以免变得复杂,主要以实现一个例子了解整个流程为主。
JNIEnv 是一个指针,指向jni对象。通过它,就可以调用jni.h头文件包含的所有函数。即,它就是一个指向jni对象的指针。
jobject表示当前函数所实现方法所属的java对象,这里指的是CppUtils的一个实例。
有了头文件,那么接下来,我们需要用到NDK了,这就需要获取NDK的支持。
3.5 获取NDK的支持
右击我们的项目,找到android tools,点击add native support,这样就可以获取NDK开发包的支持了。点击之后,会需要你填写一个名称,这个名称将会用作共享库的名称,同时这个名字也是我们CppUtils中System.loadLibrary所包含的文件名称。确定之后,我们看jni文件夹,会生成NDK支持的文件。
其中cppUtils.cpp是我们要进行实现的C++文件。Android.mk是NDK的makefile文件,通过他,可以将原生语言的实现生成为共享库。我们之前安装的cygwin目的就是支持NDK的系统构建。我们打开我们NDK的目录,笔者的如下:
可以发现NDK有很多makefile文件,这些文件都是用于帮助构成共享库的。
3.6 分析mk文件的内容
我们打开Android.mk文件,内容如下:
LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := cppUtils LOCAL_SRC_FILES := cppUtils.cpp include $(BUILD_SHARED_LIBRARY)
LOCAL_PATH,这是一个用于定位源文件的宏,在Android.mk文件中,它必须是第一个变量。
include $(CLEAR_VARS),作用是清除命名冲突,因为Android构建系统在单次执行中会构建多个文件和模块,为了避免LOCAL_<NAME>模式的变量名冲突,必须包含这条指令。
LOCAL_MODULE,指的是生成的共享库的名称,为了适应不同的架构,生成的共享库会含有lib前缀。
LOCAL_SRC_FILES,指的是生成共享库的源文件,多个源文件之间用空格隔开。
include $(BUILD_SHARED_LIBRARY)指令表示生成一个共享库。
关于共享库的生成,可以有更复杂的组织,比如多个共享库依赖某个静态库.....这里不详细讲解,只为了让大家理解整个流程。我们要知道的就是,基本的生成流程都是按照上面这几条指令顺序来的。
3.7实现原生代码
我们打开cppUtils.cpp文件,会发现是空的。如果包含了jni.h头文件指令,我们把他删掉,因为我们即将要实现的头文件已经包含了jni,h头文件,所以我们的源文件无需再次包含。
接着我们将头文件需要实现的函数声明复制过来,为了避免出错,强烈建议复制过来。然后修改成下面这个样子。
#include "com_example_jniblog_CppUtils.h" JNIEXPORT jstring JNICALL Java_com_example_jniblog_CppUtils_getStringFromCpp (JNIEnv * env, jobject jthis) { return env->NewStringUTF("来自C++"); }
3.8调用native函数
做完了上面这些,就可以函数调用了。调用和正常的java调用没有差别的。比如这里就是:
private TextView text; CppUtils cppUtils = new CppUtils(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); text = (TextView) findViewById(R.id.text); text.setText("从C++获取字符串:" + cppUtils.getStringFromCpp()); }
3.9 总结
从整个流程下来,我们可以清晰的知道每一步要怎么做为什么这么做这么做的意义,虽然并没有深入讲解,但是对于整个jni流程来说,是一个很好的了解。
作者:qq_25722767 发表于2016/9/15 21:41:33 原文链接
阅读:80 评论:0 查看评论