前言
ndk开发是高级Android 开发必备的技能,也是很多Android开发工程师难以逾越的门槛,因此,学习ndk开发已经是高级Android必备技能,所以我尝试把自己ndk开发经验记录下来,不知之处,请大家多做指教。
导读
在Android OS上开发应用程序,Google提供了两种开发包:SDK和NDK。大家都知道SDK是基于Java开发的,ndk则是基于C/C++。不过,Android 平台从一开就已经支持了C/C++了。Google从一开始就说明Android也支持JNI编程方式,也就是第三方应用完成可以通过JNI调用自己的C动态度。于是NDK就应运而生了。
你可以从Google官方查阅到有许多关于SDK的优秀书籍、文章作为参考,但是Google提供的NDK资源,相对于SDK还是比较少的。本系列文章主要是用于,自己记录自学NDK的经验,并且希望能够帮助到哪些想学习NDK的朋友。
好了,下面我们一步一步的认识SDK吧。
- 了解什么是ndk
- 为什么使用NDK
- NDK到SO
- JNI的调用
什么是NDK
NDK 其中NDK的全拼是:Native Develop Kit。我们可以去官网查看Android NDK 具体的介绍,大致意思如下图:
英文意思如下:
The Android NDK is a toolset that lets you implement parts of your app in native code, using languages such as C and C++. For certain types of apps, this can help you reuse code libraries written in those languages.
翻译成中文意思如下:
Android NDK是一个工具集,可让您使用C和C ++等语言以本机代码实现应用程序的各个部分。对于某些类型的应用程序,这可以帮助您重用以这些语言编写的代码库。
简单的来说:
Android NDK 就是一套工具集合,允许你使用C/C++语言来实现应用程序的部分功能。
为什么使用NDK
至于为什么要使用ndk,只有你使用了,你才知道使用它的好处,我简单归纳了一下使用NDK的好处。
1、在平台之间移植其应用
2、重复使用现在库,或者提供其自己的库重复使用
3、在某些情况下提性能,特别是像游戏这种计算密集型应用
4、使用第三方库,现在许多第三方库都是由C/C++库编写的,比如Ffmpeg这样库。
5、不依赖于Dalvik Java虚拟机的设计
6、代码的保护。由于APK的Java层代码很容易被反编译,而C/C++库反编译难度大
NDK到SO
从上图这个Android系统框架来看,我们上层通过JNI来调用NDK层的,使用这个工具可以很方便的编写和调试JNI的代码。因为C语言的不跨平台,在Mac系统的下使用NDK编译在Linux下能执行的函数库——so文件。其本质就是一堆C、C++的头文件和实现文件打包成一个库。目前Android系统支持以下七种不用的CPU架构,每一种对应着各自的应用程序二进制接口ABI:(Application Binary Interface)定义了二进制文件(尤其是.so文件)如何运行在相应的系统平台上,从使用的指令集,内存对齐到可用的系统函数库。对应关系如下:
- ARMv5——armeabi
- ARMv7 ——armeabi-v7a
- ARMv8——arm64- v8a
- x86——x86
- MIPS ——mips
- MIPS64——mips64
- x86_64——x86_64
JNI的调用
知道了如何生成so库,下面就要开始jni的调用
1. 什么是jni
如果你不清楚jni,可以参考一下连接
简单解释就是:
JNI,全称为Java Native Interface,即Java本地接口,JNI是Java调用Native 语言的一种特性。通过JNI可以使得Java与C/C++机型交互。即可以在Java代码中调用C/C++等语言的代码或者在C/C++代码中调用Java代码。由于JNI是JVM规范的一部分,因此可以将我们写的JNI的程序在任何实现了JNI规范的Java虚拟机中运行。同时,这个特性使我们可以复用以前用C/C++写的大量代码JNI是一种在Java虚拟机机制下的执行代码的标准机制。代码被编写成汇编程序或者C/C++程序,并组装为动态库。也就允许非静态绑定用法。这提供了一个在Java平台上调用C/C++的一种途径,反之亦然。
Java调用C/C++在Java语言里面本来就有的,并非Android自创的,即JNI。JNI就是Java调用C++的规范。当然,一般的Java程序使用的JNI标准可能和android不一样,Android的JNI更简单。
2. 为什么需要JNI
因为在实际需求中,需要Java代码与C/C++代码进行交互,通过JNI可以实现Java代码与C/C++代码的交互。
3. JNI的优势
与其它类似接口Microsoft的原始本地接口等相比,JNI的主要竞争优势在于:它在设计之初就确保了二进制的兼容性,JNI编写的应用程序兼容性以及其再某些具体平台上的Java虚拟机兼容性(当谈及JNI时,这里并不特比针对Davik虚拟机,JNI适用于所有JVM虚拟机)。这就是为什么C/C++编译后的代码无论在任何平台上都能执行。不过,一些早期版本并不支持二进制兼容。二进制兼容性是一种程序兼容性类型,允许一个程序在不改变其可执行文件的条件下在不同的编译环境中工作。
4. JNI的三个角色
JNI下一共涉及到三个角色:C/C++代码、本地方法接口类、Java层中具体业务类。对应关系如下图所示:
JNI简要流程如下图:
5. JNI的命名规则
简单命名例子如下
JNIExport jstring JNICALL Java_com_example_hellojni_MainActivity_stringFromJNI( JNIEnv* env,jobject thiz )
函数说明:
jstring 是返回值类型
Java_com_example_hellojni 是包名
MainActivity 是类名
stringFromJNI 是方法名
其中JNIExport和JNICALL是不固定保留的关键字不要修改
6. 如何实现JNI
jni的开发步骤如下:
- 第1步:在Java中先声明一个native方法
- 第2步:编译Java源文件javac得到.class文件
- 第3步:通过javah -jni命令导出JNI的.h头文件
- 第4步:使用Java需要交互的本地代码,实现在Java中声明的Native方法(如果Java需要与C++交互,那么就用C++实现Java的Native方法。)
- 第5步:将本地代码编译成动态库(Windows系统下是.dll文件,如果是Linux系统下是.so文件,如果是Mac系统下是.jnilib)
- 第6步:通过Java命令执行Java程序,最终实现Java调用本地代码。
注意:javah 是JDK自带的一个命令,-jni参数表示将class 中用到native 声明的函数生成JNI 规则的函数
7. JNI结构
这张JNI函数表的组成就像C++的虚函数表。虚拟机可以运行多张函数表,举例来说,一张调试函数表,另一张是调用函数表。JNI接口指针仅在当前线程中起作用。这意味着指针不能从一个线程进入另一个线程。然而,可以在不同的咸亨中调用本地方法。
jni接口实例代码如下:
jdouble Java_pkg_Cls_f__ILjava_lang_String_2 (JNIEnv *env, jobject obj, jint i, jstring s)
{
const char *str = (*env)->GetStringUTFChars(env, s, 0);
(*env)->ReleaseStringUTFChars(env, s, str);
return 10;
}
函数参数说明:
env:一个接口指针
obj:在本地方法中声明的对象引用
i和s:用于传递的参数
关于obj、i和s的类型大家可以参考下面的JNI数据类型,JNI有自己的原始数据类型和数据引用类型如下,具体jni的参数说明可对照下表:
8. JNI原理
在计算机系统中,每一种编程语言都有一个执行环境(Runtime),执行环境用来解释执行语言中的语句,不同的编程语言的执行环境就好比神话世界中的"阴阳两界"一样,一般人不能同时生存在阴阳两界中,只有一些特殊的仙人——“黑白无常"才能自由穿梭在"阴阳两界”,而"黑白无常"往返于阴阳两界时手持生日薄,“黑白无常"按生死薄上记录的任命来"索魂”。
Java语言的执行环境是Java虚拟机(JVM),JVM其实是主机环境中的一个进程,每个JVM虚拟机都在本地环境中有一个JavaVM结构体,该结构体在创建Java虚拟机时被返回,在JNI环境中创建JVM的函数为JNI_CreateJavaVM。
JNI_CreateJavaVM(JavaVM **pvm, void **penv, void*args);
- 1、JavaVM
其中JavaVM是Java虚拟机在JNI层的代表,JNI全局仅仅有一个JavaVM结构中封装了一些函数指针(或叫函数表结构),JavaVM中封装的这些函数指针主要是对JVM操作接口。另外,在C和C++中的JavaVM的定义有所不同,在C中JavaVM是JNIInvokeInterface_类型指针,而在C++中有对JNIInvokeInterface_进行了一次封装,比C中少了一个参数,这也是为什么JNI代码更推荐使用C++来编写的原因。
下面我们来重点说一下JNIEnv
- 2、JNIEnv
JNIEnv是当前Java线程的执行环境,一个JVM对应一个JavaVM结构,而一个JVM中可能创建多个Java线程,每个线程对应一个JNIEnv结构,它们保存在线程本地存储TLS中。因此,不同的线程的JNIEnv是不同,也不能相互共享使用。JNIEnv结构也是一个函数表,在本地代码中通过JNIEnv的函数表来操作Java数据或者调用Java方法。也就是说,只要在本地代码中拿到了JNIEnv结构,就可以在本地代码中调用Java代码。
2.1JNIEnv是什么?
JNIEnv是一个线程相关的结构体,该结构体代表了Java在本线程的执行环境
2.2、JNIEnv和JavaVM的区别:
JavaVM:JavaVM是Java虚拟机在JNI层的代表,JNI全局仅仅有一个
JNIEnv:JavaVM 在线程中的代码,每个线程都有一个,JNI可能有非常多个JNIEnv;
2.3、JNIEnv的作用:
调用Java 函数:JNIEnv代表了Java执行环境,能够使用JNIEnv调用Java中的代码
操作Java代码:Java对象传入JNI层就是jobject对象,需要使用JNIEnv来操作这个Java对象
2.4、JNIEnv的创建与释放
2.4.1、JNIEnv的创建
JNIEnv 创建与释放:从JavaVM获得,这里面又分为C与C++,我们就依次来看下:
C 中——JNIInvokeInterface:JNIInvokeInterface是C语言环境中的JavaVM结构体,调用 (AttachCurrentThread)(JavaVM, JNIEnv*, void) 方法,能够获得JNIEnv结构体
C++中 ——_JavaVM:_JavaVM是C++中JavaVM结构体,调用jint AttachCurrentThread(JNIEnv** p_env, void* thr_args) 方法,能够获取JNIEnv结构体;
2.4.2、JNIEnv的释放
C 中释放:调用JavaVM结构体JNIInvokeInterface中的(DetachCurrentThread)(JavaVM)方法,能够释放本线程的JNIEnv
C++ 中释放:调用JavaVM结构体_JavaVM中的jint DetachCurrentThread(){ return functions->DetachCurrentThread(this); } 方法,就可以释放 本线程的JNIEnv
2.5、JNIEnv与线程
JNIEnv是线程相关的,即在每一个线程中都有一个JNIEnv指针,每个JNIEnv都是线程专有的,其他线程不能使用本线程中的JNIEnv,即线程A不能调用线程B的JNIEnv。所以JNIEnv不能跨线程。
JNIEnv只在当前线程有效:JNIEnv仅仅在当前线程有效,JNIEnv不能在线程之间进行传递,在同一个线程中,多次调用JNI层方便,传入的JNIEnv是同样的
本地方法匹配多个JNIEnv:在Java层定义的本地方法,能够在不同的线程调用,因此能够接受不同的JNIEnv
2.6、JNIEnv结构
JNIEnv是一个指针,指向一个线程相关的结构,线程相关结构,线程相关结构指向JNI函数指针数组,这个数组中存放了大量的JNI函数指针,这些指针指向了详细的JNI函数。
2.7、与JNIEnv相关的常用函数
2.7.1 创建Java中的对象
jobject NewObject(JNIEnv *env, jclass clazz,jmethodID methodID, …):
jobject NewObjectA(JNIEnv *env, jclass clazz,jmethodID methodID, const jvalue *args):
jobject NewObjectV(JNIEnv *env, jclass clazz,jmethodID methodID, va_list args):
第一个参数jclass class 代表的你要创建哪个类的对象,第二个参数,jmethodID methodID代表你要使用那个构造方法ID来创建这个对象。只要有jclass和jmethodID,我们就可以在本地方法创建这个Java类的对象。
2.7.2 创建Java类中的String对象
jstring NewString(JNIEnv *env, const jchar *unicodeChars,jsize len):
通过Unicode字符的数组来创建一个新的String对象。
env是JNI接口指针;unicodeChars是指向Unicode字符串的指针;len是Unicode字符串的长度。返回值是Java字符串对象,如果无法构造该字符串,则为null。
那有没有一个直接直接new一个utf-8的字符串的方法呢?答案是有的,就是jstring NewStringUTF(JNIEnv *env, const char *bytes)这个方法就是直接new一个编码为utf-8的字符串。
2.7.3 创建类型为基本类型PrimitiveType的数组
ArrayType NewArray(JNIEnv *env, jsize length);
指定一个长度然后返回相应的Java基本类型的数组
方法 返回值
NewArray Routines Array Type
NewBooleanArray() jbooleanArray
NewByteArray() jbyteArray
NewCharArray() jcharArray
NewShortArray() jshortArray
NewIntArray() jintArray
NewLongArray() jlongArray
NewFloatArray() jfloatArray
NewDoubleArray() jdoubleArray
用于构造一个新的数组对象,类型是原始类型。基本的原始类型如下:
方法 返回值
NewArray Routines Array Type
NewBooleanArray() jbooleanArray
NewByteArray() jbyteArray
NewCharArray() jcharArray
NewShortArray() jshortArray
NewIntArray() jintArray
NewLongArray() jlongArray
NewFloatArray() jfloatArray
NewDoubleArray() jdoubleArray
2.7.4 创建类型为elementClass的数组
jobjectArray NewObjectArray(JNIEnv *env, jsize length,
jclass elementClass, jobject initialElement);
造一个新的数据组,类型是elementClass,所有类型都被初始化为initialElement。
2.7.5 获取数组中某个位置的元素
jobject GetObjectArrayElement(JNIEnv *env,
jobjectArray array, jsize index);
返回Object数组的一个元素
2.7.6 获取数组的长度代码如下:
jsize GetArrayLength(JNIEnv *env, jarray array),获取array数组的长度.。
关于JNI的常用方法,我们会在后面一期详细介绍。文档可以参考oracle开发文献9. (九) JNI的引用
Java内存管理这块是完全透明的,new一个实例时,只知道创建这个类的实例后,会返回这个实例的一个引用,然后拿着这个引用去访问它的成员(属性、方法),完全不用管JVM内部是怎么实现的,如何为新建的对象申请内存,使用完之后如何释放内存,只需要知道有个垃圾回收器在处理这些事情就行了,然而,从Java虚拟机创建的对象传到C/C++代码就会产生引用,根据Java的垃圾回收机制,只要有引用存在就不会触发该该引用所指向Java对象的垃圾回收。
在JNI规范中定义了三种引用:局部引用(Local Reference)、全局引用(Global Reference)、弱全局引用(Weak Global Reference)。区别如下:
在JNI中也同样定义了类似与Java的应用类型,在JNI中,定义了三种引用类型:
- 局部引用(Local Reference)
- 全局引用(Global Reference)
- 弱全局引用(Weak Global Reference)
下面我们就依次来看下这三种引用
1、局部引用(Local Reference)
局部引用,也成本地引用,通常是在函数中创建并使用。会阻止GC回收所有引用对象。
最常见的引用类型,基本上通过JNI返回来的引用都是局部引用,例如使用NewObject,就会返回创建出来的实例的局部引用,局部引用值在该native函数有效,所有在该函数中产生的局部引用,都会在函数返回的时候自动释放(freed),也可以使用DeleteLocalRef函数手动释放该应用。之所以使用DeleteLocalRef函数:实际上局部引用存在,就会防止其指向对象被垃圾回收期回收,尤其是当一个局部变量引用指向一个很庞大的对象,或是在一个循环中生成一个局部引用,最好的做法就是在使用完该对象后,或在该循环尾部把这个引用是释放掉,以确保在垃圾回收器被触发的时候被回收。在局部引用的有效期中,可以传递到别的本地函数中,要强调的是它的有效期仍然只是在第一次的Java本地函数调用中,所以千万不能用C++全部变量保存它或是把它定义为C++静态局部变量。
2、全局引用(Global Reference)
全局引用可以跨方法、跨线程使用,直到被开发者显式释放。类似局部引用,一个全局引用在被释放前保证引用对象不被GC回收。和局部应用不同的是,没有俺么多函数能够创建全局引用。能创建全部引用的函数只有NewGlobalRef,而释放它需要使用ReleaseGlobalRef函数
3、弱全局引用(Weak Global Reference)
是JDK 1.2 新增加的功能,与全局引用类似,创建跟删除都需要由编程人员来进行,这种引用与全局引用一样可以在多个本地带阿妈有效,不一样的是,弱引用将不会阻止垃圾回收期回收这个引用所指向的对象,所以在使用时需要多加小心,它所引用的对象可能是不存在的或者已经被回收。
通过使用NewWeakGlobalRef、ReleaseWeakGlobalRef来产生和解除引用。
4、引用比较
在给定两个引用,不管是什么引用,我们只需要调用IsSameObject函数来判断他们是否是指向相同的对象。代码如下:
(*env)->IsSameObject(env, obj1, obj2)
如果obj1和obj2指向相同的对象,则返回JNI_TRUE(或者1),否则返回JNI_FALSE(或者0),
注意:
有一个特殊的引用需要注意:NULL,JNI中的NULL引用指向JVM中的null对象,如果obj是一个全局或者局部引用,使用(*env)->IsSameObject(env, obj, NULL)或者obj == NULL用来判断obj是否指向一个null对象即可。但是需要注意的是,IsSameObject用于弱全局引用与NULL比较时,返回值的意义是不同于局部引用和全局引用的。代码如下:
jobject local_obj_ref = (*env)->NewObject(env, xxx_cls,xxx_mid);
jobject g_obj_ref = (*env)->NewWeakGlobalRef(env, local_ref);
// ... 业务逻辑处理
jboolean isEqual = (*env)->IsSameObject(env, g_obj_ref, NULL);
自此,关于NDK与JNI基础已经讲解完毕,接下来我们开始愉快的hello world 编程之旅吧。