首先下载ndk,可以在单独下载ndk包,解压到本地目录,再将工程里配置ndk路径至解压的目录,或者直接在Android Studio里下载,下载解压成功后,自动配置路径,无需手动配置。Android Studio里下载方式如下:
ndk准备好后,接下来开始专注于工程本身。
首先创建一个Android Library Module
创建一个加法器功能的类:CalNum
public class CalNum {
//暴露给外界的接口
public float testAdd(float a, float b) {
return addFloat(a, b);
}
//通过jni,调用c/c++ 函数
private static native float addFloat(float a, float b);
}
上面的addFloat是本地方法,该怎么实现呢?我们知道C语言需要一个头文件(.h)声明函数原型,需要一个源文件(.c)实现函数功能,因此需要创建两个文件。
TestJni/testnum/src/main/java
TestJni是工程名,testnum是Module名,java目录下存放的是纯java代码。
头文件需要声明addFloat函数,jni函数名比较特殊,通过javah -jni命令生成。
进入Android Studio Terminal,cd 到 java 目录,执行如下命令:
javah -jni com.fish.testnum.CalNum
ps:经测试,这里无论怎么填,都默认生成所有支持平台的.so。
这时候我们开始make module,然而令人失望的是却是报错,原因是我们仅仅准备了jni相关文件,编译器并不知道如何去操作jni文件,而我们又知道Android.mk记录这编译相关东西,因此应当先让编译器找到Android.mk文件。
在module的build.gradle里,android层级内,指定Android.mk位置:
externalNativeBuild {
ndkBuild {
path "src/main/jni/Android.mk"
}
}
这里需要注意的是路径的确定
“src/main/jni/Android.mk”
build.gradle在app 目录下,而Android.mk 在/src/main/jni/ 目录下,因此需要通过上级目录索引到Android.mk
这个时候我们再make module,成功了!那么生成的.so文件在哪呢?首先定位到app build/intermediates 目录下,搜索".so"文件,经过筛选,找到如下目录:
build/intermediates/ndkBuild/debug/obj/local
该目录下文件为:
每个目录下有对应平台的.so库,如下:
我们应该注意到了,这里只是生成了4种平台下的.so库,我们明明记得一般是支持7种平台呢?没错,这里确实少了 armeabi,mips,mips64平台,因为在ndk17开始不再支持这三种平台,而我们这里使用的ndk版本是20。那么如果想所有平台都支持呢?那么使用的ndk版本需要低于17(经过测试,ndk16也不行,最好是15及其以下)。
要查看ndk支持的abi,定位到ndk目录下,执行ndk-which命令,即可输出该版本ndk支持的abi。
上面我们说了支持的平台不够多,但是我们还想减少支持的平台数呢,这时候需要在module build.gradle android { {defaultConfig xxx}} 添加如下代码:
ndk {
// Specifies the ABI configurations of your native
// libraries Gradle should build and package with your APK.
abiFilters "x86", "x86_64", "armeabi-v7a",
"arm64-v8a"
}
想要生成哪个平台,填其名字即可。
现在.so文件已经生成,另一个模块如何调用呢?记得我们创建模块的时候是创建了Android Library,也就是说我们module编译成了.jar文件,该.jar文件负责调用.so文件里的函数,并且.jar文件暴露给外界模块接口,外界模块间接调用了.so,整个流程下来就完成了一个最简单的jni编程、实例调用。那具体怎么配置module调用呢?有两种方法:
1、调用者工程内直接依赖Android Library module,优点是方便调试,前提是我们有Android Library module源码
2、 调用者工程内依赖.jar包,现成的第三方库一般以jar包形式提供。
下面分别简要说明两者的配置方式
sourceSets {
main {
jniLibs.srcDirs = ['libs']
}
}
至此,jar包和so库都准备好了,调用者就可以调用暴露出来的接口进行访问了。
btnNdk.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
CalNum calNum = new CalNum();
String toast = calNum.testAdd(20, 30) + " nukbuild";
Toast.makeText(MainActivity.this, toast, Toast.LENGTH_SHORT).show();
}
});
通过上面ndk-build方式可知,需要我们配置Android.mk Application.mk文件,比较繁琐。Google为此推出了新的编译方式-cmake,那么cmake需要怎么做呢?
首先下载cmake工具
其次,新建project的时候,会有一个c++支持选项,勾选即可。project创建完毕后,会发现比没勾选时多了几个文件:
1、src/main 目录下新建了jni文件夹,并且预先放置了一个cpp文件:
和ndk-build jni目录一致的,当然也可以放.c 和 .h文件。
2、app 目录下多了个CMakeLists.txt,该文件的作用和ndk-build时使用的.mk文件类似。
我们来看看该文件里边的语法:
externalNativeBuild {
cmake {
cppFlags ""
}
}
让编译器知道CMakeLists.txt 位置
externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}
注:第三点是手动新增的,新建的project并没有,编译会报错,加上第三点解决编译报错问题。
至此,cmake方式编译jni配置工作就完成了,只需要简单的勾选就可以支持ndk编程,是不是觉得比之前方便多了。也许你会问,创建工程时忘了添加c++支持,后面有需要编译jni怎么办呢?还是按照上面的方法,手动添加:
1、下载cmake工具
2、新建jni,编写.c/.h 、c++源文件
3、新建CMakeLists.txt,配置其中参数
4、配置build.gradle
ndk-build方式和cmake方式编写简单入门jni程序已经梳理完毕,总结几个比较关键的点:
1、两种方式需要哪些配置文件容易搞混乱(相对来说,具体配置文件语法都可以查得到,反而比较简单)
2、配置时路径容易搞糊涂,实际上只要厘清当前配置文件所在的位置、待指向的配置的文件所在的位置,相对位置就整明白了,进而填上相对索引即可访问
本文基于Android Studio 3.2 NDK 20