NDK简介
The NDK is a toolset that allows you to implement parts of your app using native-code languages such as C and C++. For certain types of apps, this can be helpful so you can reuse existing code libraries written in these languages, but most apps do not need the Android NDK.
NDK是一个工具集,允许你在你的工程里使用本地语言代码比如说C或C++,对于一些特定的工程,NDK对于重用已存在的本地库很有帮助,但对于大多数工程来说并不需要NDK。
特别注意,使用NDK并不能让大多数的工程提高效率,作为一个开发者,应该要平衡效益及缺点,尤其是当使用NDK并不能得到显著改善的时候,它往往会增加程序的复杂程度。一般来说,除非程序必须使用NDK才用它,一定不能因为自己偏好C或C++就使用NDK。
Android NDK是配合 Android SDK的工具,Google推出NDK的目的不是为了取代Android SDK ,当然也不可能完全取代,它只是作为Android SDK 的一个补充。用来编译应用的原生代码。
1、NDK是一系列工具的集合。
* NDK提供了一系列的工具,帮助开发者快速开发C(或C++)的动态库,并能自动将so和java应用一起打包成apk。这些工具对开发者的帮助是巨大的。
* NDK集成了交叉编译器,并提供了相应的mk文件隔离CPU、平台、ABI等差异,开发人员只需要简单修改mk文件(指出“哪些文件需要编译”、“编译特性要求”等),就可以创建出so。
* NDK可以自动地将so和Java应用一起打包,极大地减轻了开发人员的打包工作。
2、NDK提供了一份稳定、功能有限的API头文件声明。
Google明确声明该API是稳定的,在后续所有版本中都稳定支持当前发布的API。从该版本的NDK中看出,这些API支持的功能非常有限,包含有:C标准库(libc)、标准数学库(libm)、压缩库(libz)、Log库(liblog)、图形处理库(libjnigraphics)。
NDK环境搭建
以下只对NDK在Linux下进行说明,由于大家都android源码开发环境,所以并不需要单独再去下载NDK,只需要将make文件书写正确,直接执行mm即可。
Android.mk文件编写
Android.mk文件用于描述如何编译源码。但它作为为一个小型的Makefile文件,应尽量少申明变量。接下来看一个简单的例子。
----------------------------------------------------
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := hello-jni
LOCAL_SRC_FILES := hello-jni.c
include $(BUILD_SHARED_LIBRARY)
------------------------------------------------------
LOCAL_PATH := $(call my-dir)
Android.mk文件必须在开头位置定义LOCAL_PATH变量,它用于定位源码位置。在些例中,宏定义my-dir由NDK编译系统定义,用于指出当前文件夹的路径。
CLEAR_VARS是由NDK编译系统定义并指向一些特殊的GUN Makefile,它会为用户清理除LOCAL_PATH以外的许多LOCAL_XXX的变量,例如LOCAL_MODULE等等,这个步骤是必须的,因为所有的Makefile是在同一个GUN Make程序中执行,所以的变量都是全局的。
LOCAL_MODULE := hello-jni
LOCAL_MODULE变量必须定义用来声明模块名,这个name必须是唯一的并且不能包含空格,注意,系统会自动为生成的文件(也就是so文件)添加前缀和后缀。在本例中最终生成的so文件名为libhello-jni.so。如果将模块名申明为’libhello-jni’,那么最终生成的so文件名字将为libhello-jni.so,并不会再额外加一个前缀。
LOCAL_SRC_FILES := hello-jni.c
LOCAL_SRC_FILES变量必须包含即将打包生成so文件的C或C++文件。请注意,不需要把头文件列入其中。
Include $(BUILD_SHARED_LIBRARY)
BUILD_SHARED_LIBRARY是由NDK编译系统定义的,它指向一个GUN Makefile 脚本,主要管理从最近的申明“Include$(CLEAR_VARS)”处开始收集所有的变量信息(例如LOCAL_XXX),并决定如何编译等问题。
JNI数据类型
NDK是Google对JNI的一种良好封装,以便于开必者更好地使用JNI。关于数据类型NDK和JNI是完全一样的,所以此处开头为JNI数据类型。下边用一张图表来描述JNI中的基本数据类型。
基本数据类型只是在名字上有所变化,统一加了‘j’,JNI数据类型和标准的C或C++数据类型有较大区别,例如标准C语言中是没有boolean数据类型的,那么如何在C语言中为jboolean类型赋值呢?
由于jboolean在C语言中实际上是unsigned 8 bits,C语言中一般char类型只占一个字节,因此可以是这样理解。
Typedef unsigned char jboolean
所以可以用下列代码获取jboolean
-------------------------------------------------
unsigned char b = 1
jboolean bool = (jboolean)b
--------------------------------------------------
其它在标准C语言中没有的类型也可通过上边的方法得到。如果要得到java中的引用数据类型那就需要另一种方法了,无法像上边这样用“=”赋值再强制转型获得。下图展示了JNI引用类型和java中的引用类型的对应关系。
下边分别用代码展示如何获取jobject、jclass、jstring对象。
-----------------------------------------------------------------
JNIEXPORT jobject JNICALL
Java_com_ndk_NDKFirstActivity_getData(JNIEnv *env , jobject obj , jint ji)
{
//根据类名,获取类
jclass dataClass = (*env)->FindClass(env ,"com/ndk/Data");
if(dataClass == NULL)
{
return;
}
//根据类获取方法字段
jmethodID cid = (*env)->GetMethodID(env ,dataClass ,"<init>" ,"(I)V");
if(cid== NULL)
{
return;
}
LOGI("i init and return a construct in native c");
//根据类、构造方法字段初始化类对象
return (*env)->NewObject(env ,dataClass ,cid , ji);
}
获取jstring
(*env)->NewStringUTF(env, "Hello from JNI !");
----------------------------------------------------------------------------
获取jclass、jmethodID、jobject类似java中的反射,jclass可通过包名加类名获取,jmethodID代表着方法,本例获取的是构造方法,也是通过方法名获取,”<init>”即方法名,”(I)V”为方法签名,I代表着类型为int的型参,V代表方法。标准C语言中没有string类型,只有用char数组表示,在本例中可以用char数组生成jstring类型。值得注意的是,上面的代码也说明了可以在C语言中反调java中的函数。
在例子中声明的方法Java_com_ndk_NDKFirstActivity_getData有一定的命名规则,Java前缀,后接包名加类名,最后接方法名,中间以下划线间隔,后边的参数里env及obj均不是java本地方法中声明的型参,env可以理解成“整个JNI环境”,obj则是调用此本地方法的类的对象。
JNI调用流程
Dalvik虚拟机成功加载库之后,就会自动地寻找库里面的JNI_OnLoad函数,这个函数用途如下:
(1)告诉Dalvik虚拟机此C库使用哪一个JNI版本。如果你的库里面没有写明JNI_OnLoad()函数,VM会默认该库使用最老的JNI 1.1版本。但是新版的JNI做了很多的扩充,也优化了一些内容,如果需要使用JNI的新版功能,就必须在JNI_OnLoad()函数声明JNI的版本。如
(2)因为Dalvik虚拟机加载C库时,第一件事是调用JNI_OnLoad()函数,所以我们可以在JNI_OnLoad()里面进行一些初始化工作,如注册JNI函数等等。注册本地函数,可以加快java层调用本地函数的效率。
NDK代码示例
NDK使用流程一共以下几个步骤:
一:在java文件中声明native方法,并加载so库
二:用C或C++实现native方法
三:编写Android.mk文件
四:编译生成so库
五:在java工程目录加把生成的so库添加进来并修改相应的make文件
六:编译java工程,执行
Java文件代码:
-------------------------------------------------------------------------
public class HelloJni extends Activity
{
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
TextView tv = new TextView(this);
tv.setText( stringFromJNI() );
setContentView(tv);
}
public native String stringFromJNI();
static {
System.loadLibrary("hello-jni");
}
}
------------------------------------------------------------------------------
本例中声明了两个native方法,并把加载so文件语言“System.loadLibrary("hello-jni")”放在了static语句块中,static语句块会在onCreate方法之前执行。再看C文件对本地方法的实现
------------------------------------------------------------------------
#include <string.h>
#include <jni.h>
jstring
Java_com_example_hellojni_HelloJni_stringFromJNI( JNIEnv* env,
jobject thiz )
{
return (*env)->NewStringUTF(env, "Hello from JNI !");
}
------------------------------------------------------------------------
C语言中对本地方法的实现相对简单,方法命名以及env、thiz这两个对象之前已经和大家介绍过了,返回的jstring并不是用“=”赋值得来的,而是通过调用NewStringUTF方法得到。
Android.mk文件
------------------------------------------------------------------------------
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := hello-jni
LOCAL_SRC_FILES := hello-jni.c
include $(BUILD_SHARED_LIBRARY)
-------------------------------------------------------------------------------
由于本例中没有用到NDK提供的额外API,比如说Log库及图形库的一些函数,所以Android.mk相对简单,之前已经详细解释过了,如果要用到那些库,只需要添加一二语句即可。
接下来即是如何编译生成so库的问题了,把Android.mk文件以及实现本地方法的c文件单独放到一个文件夹中,此文件夹名一般约定俗成名为jni,进入文件夹,用mm编译即可。生成的so文件位于out\target\product\msm8660_surf\system\lib中,复制相应so文件到相关的java工程的libs目录的armeabi目录下。
接下来修改相关java工程的mk文件即可。在mk文件中添加LOCAL_JNI_SHARED_LIBRARIES := lib hello-jni即可。