以前学了一点NDK相关的知识,这篇文章主要总结回顾一下。NDK是一种帮助我们开发C/C++代码的工具,NDK的开发包里包含很多交叉编译的工具。一般情况下我们开发Android应用,只需要用到Java代码,调用Android API即可,因此NDK在我们应用开发中很少接触,下面从几个方面详细介绍一下NDK。
一、NDK使用场景
NDK到底在什么情况下使用呢?一般用于以下三种情形:
- 提高效率:C/C++编写的代码较Java编写的代码效率高,例如需要对大量数据进行排序时
- 使用现有第三方C/C++库:大部分开源库都是用C/C++编写的,例如:人脸识别、音视频处理开源库、图形图像处理开源库,如OpenCV等。
- 底层程序设计,便于移植:用C/C++代码编写的库可以方便地在其他嵌入式平台上再次使用。例如想开发一个在Android和IOS平台都能使用的库,就需要用到C/C++来编写。
二、什么是交叉编译
一般我们编写的程序很容易就在电脑上编译执行,这是因为我们的电脑的内存及CPU都能提供充足的空间和处理能力,但是在一般的嵌入式设备中开发就没那么简单了。由于嵌入式设备只有有限的空间和处理资源,因此很多情况下我们无法在嵌入式设备上编译程序,由此就有了交叉编译的概念。
交叉编译就是在一个平台上生成另一个平台上可执行的代码,例如我们的电脑是X86平台,手机是arm平台,那么我们可以在电脑的x86平台上编译出来我们的应用程序,安装到我们手机的arm平台来运行,ndk开发包中就提供了这样的交叉编译工具。
三、什么是JNI
JNI(Java Native Interface,本地调用)提供了若干的API实现了Java和其他语言的通信(主要是C&C++),JNI标准已经成为了Java平台的一部分。
JNI的实现流程如下:
四、什么是链接库
“库”是成熟的、可复用的代码,我们可以将“库”理解为一个文件,这个文件可以在编译时由编译器直接链接到可执行程序中,也可以在运行时由操作系统的runtime enviroment根据需要动态加载到内存中。
很多时候我们程序员写代码都会依赖很多底层的库,库的存在不仅节约了很多时间,也保证了我们开发的稳定性,很多时候我们没有那么多的精力和能力来将所有的代码从头开始写,因此库的存在是很有意义的。
本质上来说库是一种可执行代码的二进制形式,可以被操作系统载入内存执行。
库有两种:静态库(.a、*.lib)和动态库(.so、.dll)。windows上对应的是.lib和.dll。linux上对应的是.a和.so。
我们知道,将一个程序编译成可执行程序有如下几个步骤:
所谓静态、动态是指链接。静态库与动态库的区别在于链接阶段如何处理库:
- 静态链接库:将汇编生成的目标文件.o与引用到的库一起链接打包到可执行文件中。它的特点是体积大、可移植性好,对库的链接是编译时完成的。
- 动态链接库:在编译时只编译自己的资源,运行时动态查找所需资源。它的特点是体积小、可共享,但可能出现所需文件找不到的情况。
五、动手试试吧
首先下载ndk开发包,可以去官网上下载相应的版本:下载地址,下载好以后配置环境,这里就不赘述了。
1.eclipse下ndk的使用##
以前用NDK都是在eclipse下,主要有以下个步骤:
(1)新建Android项目,在MainActivity中添加native方法,例如:
public static native String getStringFromC();
(2)在项目工程下创建jni目录,并借助javah命令生成C头文件到jni目录下,在cmd中敲如下指令即可生成对应头文件:
生成的头文件可以看见其中与native函数有关的函数定义:
/*
* Class: com_example_hellondk_MainActivity
* Method: getStringFromC
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_example_hellondk_MainActivity_getStringFromC
(JNIEnv *, jclass);
(3)在jni目录下写.c文件来写具体的实现代码,并在该C文件中引入(2)中生成的头文件
#include<stdio.h>
#include<stdlib.h>
#include "com_example_hellondk_MainActivity.h"//引入(2)中生成的头文件
JNIEXPORT jstring JNICALL Java_com_example_hellondk_MainActivity_getStringFromC
(JNIEnv * env, jclass jclass){ // 函数的具体实现
return(*env)->NewStringUTF(env,"Get Hello string from C!");
}
(4)在jni下创建Android.mk文件,指定编译哪个C文件
LOCAL_MODULE : = hello
LOCAL_SRC_FILES : = hello.c
(5)执行ndk-build命令,生成obj目录并生成libhello.so(在obj目录下)
(6)在MainActivity中写静态代码块:
static{
System.loadLibrary("hello");//导入生成的库libhello.so
}
最终我们的MainActivity.java如下,调用名为“hello”的动态库里的函数getStringFromC(),获得其返回字符串显示在页面的TextView中:
最后我们运行一下我们的应用程序可以看到,我们的java层成功地调用了我们的动态库中的获取字符串函数并将我们设置的字符串显示了出来。
注:我们可以编译不同平台的.so文件,通过在jni目录下建立Application.mk文件来控制。
当Application.mk文件中加入:APP_ABI := all时,生成所有四个版本的so文件,如下图所示。
若只想生成arm平台下的so文件,可将Application.mk文件中APP_ABI := all注释,如下图:
2.Android Studio下ndk的使用##
用AndroidStudio也来试试吧!我使用的Android Studio版本是1.5,gradle版本是2.8
(1)首先新建一个工程,配置好NDK路径:File->Project Structure
这样设置ndk路径的效果跟在工程的local.properties中添加如下配置语句是一样的,这样设置成功后会自动在该文件下生成这句配置语句。
ndk.dir=E\:\\androidStudio\\android-ndk-r13
(2)在app模块的build.gradle文件中配置ndk相关信息,如下图,此处配置了生成的库的名字,也可以在这里进行更多的配置,比如编译平台的选择、log功能的配置等等。
(3)在工程目录下的 gradl.properties 文件里加上android.useDeprecatedNdk=true
(4)与在eclipse下的MainActivity代码一样,调用native的getHelloStringFromC()方法,这时候这个方法会报错,因为还没有与这个方法对应的本地方法
解决办法很简单,在app>src>main目录下新建jni目录
在报错的本地方法调用函数上按Alt+Enter,便会在jni目录下自动生成对应的.c文件
修改hello.c中的代码即可,所有的都是自动生成的,是不是很方便?
若想生成对应的头文件,也很简单,借助javah命令,在Terminal中输入如下语句即可:
javah -d [生成头文件的存放路径] -jni [调用对应native方法的源文件路径]
3.ndk的使用小结##
从上面eclipse和Android Studio的使用可以看出,二者原理上并无差别,只是Android Studio的自动化程度更高,用起来更加方便,但是目前对NDK的支持仍处于试验阶段,可能不同版本的使用过程中会出现这样或者那样的错误。工具只是一种辅助,自己可以根据自己的使用习惯选择合适的工具。有关NDK使用的进阶学习可以查看我的另一篇博客:Anroid NDK初体验(下)