Android是由Google领导开发的操做系统,Android依靠其开放性,迅速普及,成为目前最流行的智能手机操做系统。html
图0-1 Android系统架构图java
图0-1是Android系统架构图。大多数程序位于最上层的Java Application层。Android经过把系统划分为几个层次从而使得开发者可使用平台无关的Java语言进行Android应用开发,没必要关心程序实际的硬件环境。 Google不只为开发者提供了SDK开发套件,为了能让开发者使用C/C++编写的本地化的共享库,利用编译后的共享库更高效的完成计算密集型的操做来提升应用的性能,或者移植重用已有的C/C++组件,提升开发效率,Android 1.5以后,又推出了NDK(Native Development Kit)。有了NDK,开发者可以在Android平台上使用JNI(Java Native Interface)技术,实现应用程序中调用本地二进制共享库。 因为Android系统不一样于以往的JNI使用环境而是在嵌入式硬件环境下,Android NDK提供了一套交叉编译工具链,和构建程序的工具方便开发者在桌面环境下编译目标平台的二进制共享库。 目前NDK提供了对ARMv5TE,ARMv7-A,x86和MIPS指令集平台的支持,同时在本地接口的支持上,目前如下本地接口支持linux
由上面的介绍,咱们能够知道,实际上NDK开发是以JNI技术为基础的,所以要求开发者必需要掌握基本的JNI技术,这样才能进行有效的NDK开发。android
JNI(Java Native Interface)是Java SDK 1.1时正式推出的,目的是为不一样JVM实现间提供一个标准接口,从而使Java应用可使用本地二进制共享库,扩充了原有JVM的能力,同时Java程序仍然无需再次编译就能够运行在其余平台上,即保持了平台独立性又能使用平台相关的本地共享库提高性能。在Java开发中的位置以下图所示。JNI做为链接平台独立的Java层(如下简称Java层)与与平台相关的本地环境(如下简称Native层)之间的桥梁。数组
图1-1 JNI在Java开发中的位置缓存
实际上在Android内部就大量的使用了JNI技术,尤为是在Libraries层和Framework层。数据结构
Google在其文档提到了NDK不能让大多数应用获益,其增长的复杂度远大于得到的性能的代价。Google建议当须要作大量的cpu密集同时少许存储操做或者重用C/C++代码时能够考虑使用NDK。 本文的余下部分将具体介绍Android平台下经过NDK的支持的如何进行JNI的开发。架构
本节经过一个简单的例子,介绍NDK开发流程以及JNI的基本使用。 笔者假定你已经下载了NDK,且有Android SDK开发的经验。 在NDK开发包中就有若干的NDK示例。其中hello-jni
是一个简单的实例。该实例从native层传递字符串到java层,并显示在界面上。(你能够在Eclipse里选择 新建Anroid项目 ,以后选择 “Create project from existing source”,并定位到NDK目录中的 Sample/hello-jni ,这样就能够将示例代码导入到Eclipse中。) HelloJni的Java代码以下:oracle
package com.example.hellojni; import android.app.Activity; import android.view.View; import android.widget.Button; import android.widget.TextView; import android.os.Bundle; import android.view.View.OnClickListener; public class HelloJni extends Activity { /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); Button btn = (Button)findViewById(R.id.btn); final TextView txtv = (TextView)findViewById(R.id.txtv); btn.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { txtv.setText(stringFromJNI());//调用native函数 } }); } /* A native method that is implemented by the * 'hello-jni' native library, which is packaged * with this application. * 声明含有native关键词的函数,就能够在类中使用了。 */ public native String stringFromJNI(); /* * 该函数并无在共享库中实现,可是仍然能够声明。 * 没有实现的native函数也能够在类中声明,native方法仅在首次调用时才开始搜索。 * 若没有找到该方法,会抛出java.lang.UnsatisfiedLinkError异常 */ public native String unimplementedStringFromJNI(); /* this is used to load the 'hello-jni' library on application * startup. The library has already been unpacked into * /data/data/com.example.HelloJni/lib/libhello-jni.so at * installation time by the package manager. * 使用静态方式再建立类时就载入共享库,该共享库(后面会介绍)在程序安装后 * 位于/data/data/com.example.HelloJni/lib/libhello-jni.so */ static { System.loadLibrary("hello-jni"); } }
Java代码中调用native函数很简单。大体分为如下几步:app
JNI的使用的一个关键点是 1) 如何找到共享库 2)如何将Java代码中的声明的native方法和实际的C/C++共享库中的代码相关联,即JNI函数注册。 第一个问题能够交给NDK构建工具 ndk-build
解决:一般是将编译好的so共享库放在 libs/armeabi/libXXX.so
以后会有更详细的介绍。第二个问题能够将在第二节中系统讲述,如今咱们只简单的说一下如何作。
简易实用的方法是经过利用Java提供的 javah
工具生成和声明的native函数对应的头文件。具体操做是以下:
bin/
目录,应该是编译好的。jni
子目录,若是没有则建立一个(咱们如今使用的自带的实例代码,所以能够)。javah -jni com.example.hellojni.HelloJNI -classpath bin/classes -o jni/hello-jni.h
确认javah所在路径已经在的$PATH路径下jni
目录下生成一个名为 my_jni_header.h
的头文件。上一步骤咱们获得了与Java源文件对应的头文件,所以只要编写 my_jni_header.c
,实现头文件里面的声明的源代码。生成的内容以下:
/* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class com_example_hellojni_HelloJni */ #ifndef _Included_com_example_hellojni_HelloJni #define _Included_com_example_hellojni_HelloJni #ifdef __cplusplus extern "C" { #endif /* * Class: com_example_hellojni_HelloJni * Method: stringFromJNI * Signature: ()Ljava/lang/String; */ JNIEXPORT jstring JNICALL Java_com_example_hellojni_HelloJni_stringFromJNI (JNIEnv *, jobject); /* * Class: com_example_hellojni_HelloJni * Method: unimplementedStringFromJNI * Signature: ()Ljava/lang/String; */ JNIEXPORT jstring JNICALL Java_com_example_hellojni_HelloJni_unimplementedStringFromJNI (JNIEnv *, jobject); #ifdef __cplusplus } #endif #endif
能够看到生成的头文件中的函数和示例项目 hello-jni
中的 hello-jni.c
正好对应。据此也可知咱们生成的头文件是正确的。 hello-jni.c
源代码以下:
#include <string.h> #include <jni.h> #include <stdio.h> /* This is a trivial JNI example where we use a native method * to return a new VM String. See the corresponding Java source * file located at: * * apps/samples/hello-jni/project/src/com/example/HelloJni/HelloJni.java */ jstring Java_com_example_hellojni_HelloJni_stringFromJNI( JNIEnv* env, jobject thiz ) { char msg[100]; sprintf(msg,"Hello from JNI."); return (*env)->NewStringUTF(env, msg); }
通过以上两步,咱们已经获得了C/C++共享库的源代码,如今须要使用交叉编译工具将其编译成目标机器上的二进制共享库。NDK工具提供了一个简单的构建系统,开发者之须要编写 Android.mk
,以后在项目根目录下执行命令 ndk-build
就能够完成交叉编译过程。
LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := hello-jni LOCAL_SRC_FILES := hello-jni2.c include $(BUILD_SHARED_LIBRARY)
Android.mk
能够看做是小型的makefile,关于 Android.mk
的更多细节,限于篇幅,这里不作详细介绍请参考NDK自带文档,里面有完整的介绍。 输出的信息相似下面:
Gdbserver : [arm-linux-androideabi-4.4.3] libs/armeabi/gdbserver Gdbsetup : libs/armeabi/gdb.setup Compile thumb : hello-jni <= hello-jni.c SharedLibrary : libhello-jni.so Install : libhello-jni.so => libs/armeabi/libhello-jni.so
上面的信息告诉咱们生成好的so文件路径为 libs/armeabi/libhello-jni.so
。至此一个简单的NDK程序的已经制做完成。 总结一下大体过程是:
javah
工具生成头文件ndk-build
完成共享库的编译上一节咱们经过一个简单的实例,对NDK开发有了一个感性的认识。可是你也许会发现Java层上声明的native函数与native上面的实现之间的关联是经过javah生成头文件完成的,这个方法显得很笨拙。 实际上这种静态注册的方法是经过函数名(Java_com_example_hellojni_HelloJni_stringFromJNI
)来创建联系。这种作法有诸多弊端:
Android内部实现上,在使用JNI时很显然并无这样作,它采用了更加规范的 动态注册
的方法进行两个层次上的关联。
如下代码是上面的 hell-jni.c
的动态注册版,代码中使用的是自定义的native函数名称。
#include <string.h> #include <jni.h> #include <stdio.h> jstring getHelloString(); static JNINativeMethod gMethods[] = { { "stringFromJNI", "()Ljava/lang/String;", (void *)getHelloString } }; static int nMethods = 1; static JNIEnv *env = NULL; jstring getHelloString() { char msg[100]; sprintf(msg,"Hello from JNI."); return (*env)->NewStringUTF(env, msg); } jint JNI_OnLoad(JavaVM *vm,void *reserved){ jint result = -1; jclass clz = NULL; if ((*vm)->GetEnv(vm,(void**) &env, JNI_VERSION_1_4) != JNI_OK){ return -1; } clz = (*env)->FindClass(env,"com/example/hellojni/HelloJni"); if((*env)->RegisterNatives(env,clz,gMethods,nMethods) < 0) { return -1; } return JNI_VERSION_1_4;//根据JNI规范,JNI_OnLoad必须返回版本号常量不然出错。 }
根据Java的官方文档1,当VM载入共享库时,会寻找 jint JNI_OnLoad(JavaVM *vm, void *reserved)
函数,若是存在则再载入共享库以后调用该函数。所以咱们能够在该函数中完成native函数的注册工做。 JNI_OnLoad
函数的参数有两个,最主要就是JavaVM
结构。 JavaVM
是存储VM信息的数据结构。更多信息将在后面讲到,这里咱们只须要知道,经过JavaVM指针咱们能够获得另外一个JNI核心结构—— JNIEnv
, JNIEnv
表明了整个JNI环境的数据结构,实际是一个函数表,其中存储了JNI的各类相关操做的函数指针,后文会详细介绍,在这里咱们只须要知道在JNIEnv结构有如下的方法,经过调用就能够实现动态注册。
RegisterNatives
用来注册一组native函数,其中使用到了 JNINativeMethod
结构,具体定义以下3:
typedef struct { char *name; //Java代码中声明的native函数的名称 char *signature; //对应Java代码层native函数的签名,下面会介绍 void *fnPtr; //共享库中函数指针 } JNINativeMethod;
这里就涉及到了 函数签名
Java容许函数重载,所以在注册时就要具体区分出来,不然会出现混乱,于是这里就要使用一种方法将每一个Java中的方法标上惟一的标记。这种方法就是 函数签名 。函数签名应该属于JVM内部的规范,不具备可读性。规定4以下:
类型标识 | JAVA类型 |
---|---|
Z | boolean |
B | byte |
C | char |
S | short |
I | int |
J | long |
F | float |
D | double |
L/java/lang/String; | String |
[I | int[] |
[L/java/lang/object; | Object[] |
V | void |
表1 类型标示对应表
每一个函数签名大体格式 (<参数签名>)返回值类型签名
引用类型的参数签名形式为 L<包名>
JAVA函数 | 函数签名 |
---|---|
String f() | ()L/java/lang/String; |
void f(String s,AClass cls,long l) | (L/java/lang/String;L/com/example/AClass;J)V |
String f(byte[]) | ([B)V |
表2 一些签名示例 函数看起来很难懂,咱们能够利用 javap
工具查看类中的函数签名那个信息,具体用法:
$PROJECT/bin/classes
下($PROJECT表明Android程序项目根目录,并假定java文件已经编译好,存在bin目录)javap -s com.example.helljni.HelloJni
其中com.example.hellojni.HelloJni
是类的完整名称这一节中,经过动态注册版的hello-jni代码示例,简要介绍如何在JNI中实现更灵活的动态注册方法关联Java层中native函数和Native层中的实现函数。JNI规范中规定VM在载入共享库以后,要调用 JNI_OnLoad
函数,通常能够在共享库中实现该方法并完成动态注册。 初步接触了 JavaVM
结构和 JNIEnv
结构,并了解了 JNIEnv
的两个“函数成员”FindClass
和 registerNatives
。以后还看到了JNI中保存关联信息的JNINativeMethod
结构以及了解了Java的 函数签名 。
Java层和Native层之间如同两个说着不一样语言的国家同样,若是要互相交流就必需要懂得对方的语言。在Native层中是如何表示Java层的数据类型呢?
JAVA数据类型 | NATIVE层数据类型 | 符号属性(UNSIGNED/SIGNED) | 长度(BIT) |
---|---|---|---|
boolean | jboolean | unsigned | 8 |
byte | jbyte | unsigned | 8 |
char | jchar | unsigned | 16 |
short | jshort | signed | 16 |
int | jint | signed | 32 |
long | jlong | signed | 64 |
float | jfloat | signed | 32 |
double | jdouble | signed | 64 |
表3 基本数据类型转换表
JAVA引用类型 | NATIVE类型 |
---|---|
全部object | jobject |
java.lang.Class | jclass |
java.lang.String | jstring |
Object[] | jobjectArray |
boolean[] | jbooleanArray |
byte[] | jbyteArray |
char[] | jcharArray |
short[] | jshortArray |
int[] | jintArray |
long[] | jlongArray |
float[] | jfloatArray |
double[] | jdoubleArray |
java.lang.Throwable | jthrowable |
表4 引用数据类型转换表 Native层中将除String之外的类都做为 jobject
处理,对于数组类型,只有基本数据类型的数组是单独表示,其余类型的都以 jobjectArray
类型存储。
http://java.sun.com/docs/books/jni/html/functions.html JavaVM指针指向了一个表明整个VM的实例,同时对全部native线程都有效。主要有如下几个接口可使用5:
DestroyJavaVM
卸载整个VM实例AttachCurrentThread
将当前的native线程attach到VM实例中,当线程加入到VM线程后,该线程就能够调用诸如访问Java对象、调用Java方法等JNI函数DetachCurrentThread
与 AttachCurrentThread
相反GetEnv
既能够用来检查当前线程是否已经attach到VM实例,还能够获得当前线程的JNIEnv结构。JNIEnv接口包含了JNI的主要功能的函数接口,注意JNIEnv是与线程相关的结构,JNIEnv接口实际是指向内部的一个函数集合,要在Native层操纵某个具体的类,或者调用方法,则须要 JNIEnv
。在native函数的动态注册方法这一节就使用 JNIEnv
的函数进行了native函数的注册。 JNIEnv
是指向一个函数表的指针的指针。 其具体定义以下6
typedef const struct JNINativeInterface *JNIEnv; const struct JNINativeInterface ... = { NULL, NULL, NULL, NULL, GetVersion, DefineClass, FindClass, FromReflectedMethod, FromReflectedField, ToReflectedMethod, GetSuperclass, IsAssignableFrom, ToReflectedField, ....//还有不少,这里略去 };
下图是 JNIEnv
的一个简单图示7
JNIEnv能提供的功能很是多,大致能够分为如下几类5:
限于篇幅,在此没法一一讲解用法。仅说明较经常使用的几个。更多详细信息请参考Sun出版的JNI开发者指南(地址)
经过JNIEnv提供的如下方法就能够调用对象的方法
//调用对象方法的函数原型 NativeType Call<type>Method(JNIEnv *env, jobject obj,jmethodID methodID, ...); NativeType Call<type>MethodA(JNIEnv *env, jobject obj,jmethodID methodID, jvalue *args); NativeType Call<type>MethodV(JNIEnv *env, jobject obj,jmethodID methodID, va_list args); //对对象成员操做的函数原型 NativeType Get<type>Field(JNIEnv *env, jobject obj,jfieldID fieldID); void Set<type>Field(JNIEnv *env, jobject obj, jfieldID fieldID,NativeType value); //取得methodID,fieldId的函数原型 jmethodID GetMethodID(JNIEnv *env, jclass clazz,const char *name, const char *sig); jfieldID GetFieldID(JNIEnv *env, jclass clazz,const char *name, const char *sig);
前三个函数为一组调用对象方法的函数,区别仅在于传递参数的方式不一样。其中NativeType
表示Java方法返回值对应的Native类型,具体转换见表3,表4。 <type>
是Void
/ Boolean
/ Int
/ Long
/ Object
等Java基本数据类型。调用这一组函数时,既须要传递对象的信息,还要传递方法的标识以及Java类中的方法的参数。 jobject
变量既能够经过在Native层中调用 CallObjectMethod
获得,也能够经过后面提到的建立对象实例获得。 methodId
则能够经过 GetMethodID
取得。 jclass
参数能够由前文提到的env->FindClass
函数取得。 相似地,还有 CallStatic<type>Method
、GetStatic<type>Field
、 SetStatic<type>Field
在此再也不赘述。
因为String特别经常使用,且存在比较复杂的编码问题,JNI特地将String类做为一个独立的Native层中的数据类型jstring处理。同其余Object操做相似,jstring也是经过 JNIEnv
来管理的。主要的操做函数有:
jstring NewString(JNIEnv *env, const jchar *unicodeChars,jsize len); void ReleaseStringChars(JNIEnv *env, jstring string,const jchar *chars); const jchar * GetStringChars(JNIEnv *env, jstring string,jboolean *isCopy); jsize GetStringLength(JNIEnv *env, jstring string); jstring NewStringUTF(JNIEnv *env, const char *bytes); void ReleaseStringUTFChars(JNIEnv *env, jstring string,const char *utf); const char * GetStringUTFChars(JNIEnv *env, jstring string,jboolean *isCopy); jsize GetStringUTFLength(JNIEnv *env, jstring string);
函数的功能能够从名称大体了解到,其中 New
开头的都是将JNI中将String按照编码分为两种,一种是Unicode编码(UTF-16),一种是UTF-8编码 须要注意的是Native层中并无垃圾自动回收机制,所以申请字符串资源,用完以后要进行释放操做,不然会引发内存泄露。 使用过程当中还要注意:Unicode字符串不是“0结尾”的,所以不要依赖\u0000
进行字符串的操做。 常见的错误还包括调用 NewStringUTF
传入的参数 bytes
必须是 Modified UTF-8
格式的,不然会出现乱码。8
Native层能够经过操做jarray数据来处理Java层的数组类型。JNI中将基本类型Java数组和引用类型数组分开处理。 下面是几个Java数组的例子。
int[] iarr; //基本类型数组 float[] farr;//基本类型数组 Object[] oarr;//引用类型数组,数组元素是Object int[][] arr2;//引用类型数组,数组元素是 int[]
下表是基本类型数组操做的函数小结
JNI函数 | 描述 |
---|---|
Get<Type>ArrayRegion | 将基本类型数组的数据复制到预先申请好的C数组中或者反方向操做操做 |
Set<Type>ArrayRegion | |
Get<Type>ArrayElements | 得到/释放指向基本类型数组的数据的指针 |
Release<Type>ArrayElements | |
GetArrayLength | 返回数组的长度 |
New<Type>Array | 新建一个指定长度的数组 |
GetPrimitiveArrayCritical | 得到/释放指向基本类型数据的指针 |
ReleasePrimitiveArrayCritical |
表5 基本数据类型数组的操做函数
下面以一个简单的代码片断做为说明9。假设某段Java代码中声明了如下的native函数
native int[][] get2DArray(int size);//返回 int[size][size]大小的二维数组
Native层能够用如下代码实现
jobjectArray get2DArray(jint size){ jobjectArray result; int i; jclass intArrCls = (*env)->FindClass(env, "[I"); if (intArrCls == NULL) { return NULL; /* exception thrown */ } result = (*env)->NewObjectArray(env, size, intArrCls, NULL); if (result == NULL) { return NULL; /* out of memory error thrown 可能遇到空间不足*/ } for (i = 0; i < size; i++) { jint tmp[256]; /* make sure it is large enough! */ int j; jintArray iarr = (*env)->NewIntArray(env, size); if (iarr == NULL) { return NULL; /* out of memory error thrown */ } for (j = 0; j < size; j++) { tmp[j] = i + j; } (*env)->SetIntArrayRegion(env, iarr, 0, size, tmp); (*env)->SetObjectArrayElement(env, result, i, iarr); (*env)->DeleteLocalRef(env, iarr); } return result; }
上述代码展现了 NewObjectArray
、 NewIntArray
、 SetObjectArrayElement
、SetIntArrayRegion
等函数的用法,代码可读性很高,这里不作进一步解释。
Java做为高级语言,具备垃圾自动回收管理机制,内存管理相对轻松。而C/C++则没有这样的机制,所以在Native层对象实例可能被垃圾回收。这里就涉及到了JNI的对象引用的管理。 JNI支持三种引用类型—— LocalReference
/ GlobalReference
/WeakGlobalReference
,每一种引用类型的生命周期是不一样的。 大多数JNI函数使用的是 LocalReference ,即在函数中调用的”New”操做返回的都是对象的 LocalReference
。 LocalReference
只在函数执行代码范围内有效,只要JNI函数一返回,引用就会被释放。相对地, GlobalReference
能够在多个函数之间共享,直到开发者本身调用释放函数才会被垃圾回收。另外一方面 WeakGlobalReference
则具备 引用缓存 功能——一方面它能够像 GlobalReference
同样跨函数共享引用,另外一方面它不会阻碍引用的垃圾回收过程。但JNI文档中建议开发者使用 GlobalReference
和 LocalReference
替代WeakGlobalReference
,由于该引用随时均可能会被垃圾回收,即便是在调用了IsSameObject
断定引用有效以后仍然可能会失效10。 有关引用的操做有
//GlobalReference jobject NewGlobalRef(JNIEnv *env, jobject obj); void DeleteGlobalRef(JNIEnv *env, jobject globalRef); //LocalReference void DeleteLocalRef(JNIEnv *env, jobject localRef); jobject NewLocalRef(JNIEnv *env, jobject ref); //WeakLocalReference jweak NewWeakGlobalRef(JNIEnv *env, jobject obj); void DeleteWeakGlobalRef(JNIEnv *env, jweak obj); //通用的引用操做 jobject AllocObject(JNIEnv *env, jclass clazz); jobject NewObject(JNIEnv *env, jclass clazz,jmethodID methodID, ...); jclass GetObjectClass(JNIEnv *env, jobject obj); jobjectRefType GetObjectRefType(JNIEnv* env, jobject obj); jboolean IsSameObject(JNIEnv *env, jobject ref1,jobject ref2);
本文大体介绍了Android NDK的相关技术以及NDK的基础——JNI的使用,其中简述了NDK的开发流程、函数注册的两种方式、JNI技术的基本内容,其中包括了Java层和Native层之间的数据转换和互操做方法。不难发现,JNI技术扩展了原有Java技术的能力。
1 Java Native Interface Specificationhttp://docs.oracle.com/javase/6/docs/technotes/guides/jni/spec/invocation.html
2http://docs.oracle.com/javase/6/docs/technotes/guides/jni/spec/functions.html#wp16027
3http://docs.oracle.com/javase/6/docs/technotes/guides/jni/spec/functions.html#wp17734
4 深刻理解Android:卷I pp28-29
5 Java Native Interface: Programmer’s Guide and Specificationhttp://java.sun.com/docs/books/jni/html/functions.html
6 JNIEnv定义http://docs.oracle.com/javase/6/docs/technotes/guides/jni/spec/functions.html#wp23720
7 http://java.sun.com/docs/books/jni/html/objtypes.html#5190
8 Android Developers JNI Tipshttp://developer.android.com/guide/practices/design/jni.html#UTF\_8\_and\_UTF\_16\_strings
9 代码改编自The Java Native Interface Programmer’s Guide and Specificationhttp://java.sun.com/docs/books/jni/html/objtypes.html#27791
10 http://docs.oracle.com/javase/6/docs/technotes/guides/jni/spec/functions.html#weak