OSG for Android新手教程系列(四)——JNI与NDK的使用

  上一篇教程中,我针对一个最简单的HelloWorld示例进行了详细讲解,从宏观上讲明了OSG for Android项目的开发方法。这里给出链接:http://blog.csdn.net/dongzhong1990/article/details/51736868

  如果阅读过上一篇教程,也许会对一些细节上的问题还不是很清楚。例如,示例中osgNativeLib.cpp中那些奇怪命名方法的函数是怎样产生的,为什么要这样写。本篇教程将对该问题进行详细解答。

--------------------------------------------------

  众所周知,在开发Android项目时,我们通常使用的是Java语言。然而OSG却是一个基于C++平台的三维渲染引擎。于是,在这里就有了一个小小的冲突。那么,为了能够在Android平台上使用基于C++的OSG,我们应该怎么做呢?通常情况下,我们也许会产生以下两种想法:第一,将OSG重新用Java编写一遍;第二,通过某些方法,使Java可以调用C++;第三,算了,不用Java了。

  我们分别看看这三种方法。第一种方法,如果是对于一些小型的框架引擎,这也不失为一种办法。但是很多情况下,框架引擎代码量都是比较庞大的,如果对每个框架引擎都重新更换语言编写,那么工作量会十分巨大,而且很容易出现错误。第二种方法,简单可行高效,通过这种方法可以快速地将不同语言编写的框架引擎融合到同一个项目中,同时这种方法也是现阶段对不同平台融合的首选方法。第三种方法,算了,不考虑。

  那么,现在我们所要考虑的就是如何使Java与C++之间进行沟通。正如前几篇教程所提到的那样,在Java程序中使用C/C++需要使用JNI,即Java Native Interface。而Google公司又为Android开发提供了NDK,即Native Development Kit,作为Android项目中使用C/C++的工具集。本篇教程将对JNI和NDK的使用方式进行详细讲解,使学习者能够更加深入的理解Android调用OSG函数的内部机制。

  本文所用的NDK为r10d版本,下载链接:http://developer.android.com/tools/sdk/ndk/index.html。如果链接不上,可以百度一下,会有不少网盘中分享安装包。

  与上篇教程一样,本文默认项目已经配置完毕。具体配置方式,参考本系列教程(二),链接:http://blog.csdn.net/dongzhong1990/article/details/51736868

  文章将分别从Java层和C/C++层进行阐述。讲解需要的代码都保存在了CODE平台上,链接:https://code.csdn.net/dongzhong1990/osgandroidhelloworld/tree/master


一、Java层相关实现


  在Android中调用OSG的C/C++函数,与普通的Android项目的区别并不是很大。在Java层中所需要做出的修改都集中在了osgNativeLib.java这个文件中。代码如下:

osgNativeLib.java:

package osg.dong.osghelloworld;

public class osgNativeLib
{
	static
	{
		System.loadLibrary("osgNativeLib");
	}
	
	public static native void init(int width, int height);
	public static native void step();
}


  从代码中可以看到,osgNativeLib类中定义了两个带有native修饰符的方法,分别是init和step。这里的native修饰符就是用于指定该方法调用了本地语言的方法,即C/C++语言。也就是说,只要是在定义方法前加了native修饰符,那就是可以与C/C++层沟通的方法。而在Java层的其他地方,调用这些native方法,和调用普通的Java方法是没有任何区别的。

  同时我们也看到,在Java层中,并没有native方法的具体实现代码,仅仅只有一个声明。这是因为这些方法的具体实现都是在C/C++层中完成的。也就是说,这些方法的实际功能是在C/C++层上的,而在Java层上,这些方法仅仅是作为一个调用接口使用的。

  在osgNativeLib中,除了两个native方法外,还存在一个static静态块。在这个静态块中,系统载入了一个函数库,这个函数库的名称为“osgNativeLib”,如果阅读过本系列教程之前两篇的话,应该就知道,这个osgNativeLib的名字是在Android.mk配置文件中定义的。那么,这个库到底是怎么形成的呢,这里暂时先放一下,留个疑问,下文我会详细讲解。

  在Android项目中调用OSG函数,在Java层的实现就是这么简简单单的几句代码而已。真正的核心所在是在C/C++层和两层之间的沟通上。


二、C/C++层相关实现


  在上一篇教程中,我们遗留了一个疑问,那就是osgNativeLib.cpp文件中,有两个命名方式以、修饰符和参数类型都很奇怪的函数,这两个函数是怎么得来的?在这里我进行详细解答。

  其实,osgNativeLib.cpp里面的函数是可以通过jni的javah命令自动生成的。我们在编写好osgNativeLib.java中的native方法后,先build一下项目,这时候基本上会报错。我们不用管错误,build项目的目的是为了得到osgNativeLib.class文件,这个文件就是osgNativeLib.java经过编译后得到的class文件。Android项目中所有的class文件都会保存在<项目根目录>/bin/classes路径下。

  打开cmd,进入到<项目根目录>/bin/classes路径下,输入命令:javah osg.dong.osghelloworld.osgNativeLib。注意!这里需要将osgNativeLib类所在的包全名写上。之后,你就会在classes文件夹下发现一个文件名为osg_dong_osghelloworld_osgNativeLib.h的C/C++头文件。该文件代码如下:

osg_dong_osghelloworld_osgNativeLib.h:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class osg_dong_osghelloworld_osgNativeLib */

#ifndef _Included_osg_dong_osghelloworld_osgNativeLib
#define _Included_osg_dong_osghelloworld_osgNativeLib
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     osg_dong_osghelloworld_osgNativeLib
 * Method:    init
 * Signature: (II)V
 */
JNIEXPORT void JNICALL Java_osg_dong_osghelloworld_osgNativeLib_init
  (JNIEnv *, jclass, jint, jint);

/*
 * Class:     osg_dong_osghelloworld_osgNativeLib
 * Method:    step
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_osg_dong_osghelloworld_osgNativeLib_step
  (JNIEnv *, jclass);

#ifdef __cplusplus
}
#endif
#endif


  其中,我们看到了熟悉的身影。就是下面这两个函数声明:

JNIEXPORT void JNICALL Java_osg_dong_osghelloworld_osgNativeLib_init
  (JNIEnv *, jclass, jint, jint);
JNIEXPORT void JNICALL Java_osg_dong_osghelloworld_osgNativeLib_step
  (JNIEnv *, jclass);

  这两个函数声明就是我们所需要的。这两个函数分别对应了osgNativeLib.java中init方法和step方法。其命名规则是:Java_ + <包全名(“.”替换为“_”)> + _<函数名>。并在所有原有参数之前增加两个参数:JNIEnv*和jclass。其他的原有参数类型,因为是从Java转换到C/C++,所以会有一定的改变。这里给出Java、JNI和C/C++参数类型的对应规则:


  


  其中基本类型的变量可以直接转换。但是对象类型的转换相比而言就稍微麻烦些,具体的方法已经有很多人做了实现,在此我就不再赘述。

  好了,现在我们就知道了Java层中的native方法在C/C++层中的声明形式。现在我们就需要对这些声明了的函数进行定义。新建一个osgNativeLib.cpp文件,将下面代码复制进去:

extern "C" {
	JNIEXPORT void JNICALL Java_osg_dong_osghelloworld_osgNativeLib_init(JNIEnv *, jclass, jint, jint);
	JNIEXPORT void JNICALL Java_osg_dong_osghelloworld_osgNativeLib_step(JNIEnv *, jclass);

}

  同时,也不要忘了添加头文件,#include "jni.h"。而osg_dong_osghelloworld_osgNativeLib.h这个文件我们就可以删除了。记住,我们需要的只是其中的内容,而不是这个文件。

  剩下的事情就和标准的C/C++没什么区别了。在定义函数的时候,仍然需要加上JNIEXPORT和JNICALL,表示这是JNI调用的函数。

  接下来就可以尽情的“享用”OSG啦。


三、NDK的作用

  假设我们已经将OSG for Android项目的OSG部分实现完成,编写了好多的c、cpp、h文件。那么,我们现在可以运行项目了吗?稍安勿躁,我们还缺了一步。Android还是不能直接认识c、cpp和h的,我们需要将他们转换为Android可以认识的东西,那怎么办呢?这时候就需要NDK出场了。NDK的作用就是将这些C/C++文件编译成为Android可以认识的.so(动态库)和.a(静态库),至于到底是生成了哪种,则是在配置文件Android.mk文件末尾处的那句include $(BUILD_SHARED_LIBRARY)或者include $(LOCAL_STATIC_LIBRARY)决定的。同时,也别忘记了要把所有的.c文件和.cpp文件都要加到LOCAL_SRC_FILES里去,不少新手会容易犯这个错误,甚至是熟练开发人员也会偶尔遗忘这一点,要注意。

  对于我们来说,这时候需要做的就是build项目。NDK会自动地根据Android.mk文件的配置对C/C++文件进行编译,无需我们额外做的更多。生成的.so文件、.a文件可以在项目路径下的obj文件夹中找到,文件名是lib+<库名>+.so/.a。例如,本文示例生成的就是libosgNativeLib.so。而在osgNativeLib.java文件中静态块内载入的库就是这个。


--------------------------------------------------------

  

  本篇教程主要讲解了OSG for Android项目中使用到的JNI和NDK用法。在下一篇教程中,我将对OSG的基本常识进行讲解,以帮助OSG“纯新人”更好的理解OSG for Android项目,敬请关注。

发布了21 篇原创文章 · 获赞 63 · 访问量 6万+

猜你喜欢

转载自blog.csdn.net/dongzhong1990/article/details/51760925