一、前期基础知识储备
Android NDK 是一套允许您使用 C 和 C++ 等语言,以原生代码实现部分应用功能的工具集。在开发某些类型的应用时,这有助于您重复使用以这些语言编写的代码库(C++等语言在执行速度、稳定性、安全性上有很大的优势)。
public class MyActivity extends Activity {
/**
* Native method implemented in C/C++
*/
public native void computeFoo();
}
可以在 Android Studio 2.2 或更高版本 中使用 NDK 将 C 和 C++ 代码编译到原生库中,然后使用 Android Studio 的集成编译系统 Gradle 将原生库打包到 APK 中。Java 代码随后可以通过 Java 原生接口 (JNI) 框架调用原生库中的函数。
Android Studio 编译原生库的默认编译工具是 CMake。由于很多现有项目都使用 ndk-build 编译工具包,因此 Android Studio 也支持 ndk-build。不过,如果您要创建新的原生库,则应使用 CMake。
本文主要分析NDK的方式,暂不涉及CMake的方式处理。
二、上代码,具体实现
在实际开发中,我们使用原生代码的场景常见的有两种,一是有C++原生代码编译好的so库但是没有原生代码,二是两者都有。第一种情况就不好做延伸了,因为没有原生代码所以无法做功能扩展;第二种情况是本文主要讨论的,以博主开发的应用为例,谷歌2019年8月1日起,所有发布到谷歌应用市场的应用,既要求有32位,也要求要64位,32位64位主要针对的就是原生代码提供的so库。
如上图,armeabi-v7a下存放的so库为32位,arm64-v8a下存放的是64位so库,在以前的开源项目来说,往往只有32位的,没有64位的,所以就需要自己手动创建新的64位so库。
如上图,我们现在有C++原生代码的情况下,只需要生成一份新的64位so库即可。步骤如下:
1)gradle.properties文件下加入android.useDeprecatedNdk=true;
2)app的build.gradle文件下加入三处声明;
android {
compileSdkVersion 28
defaultConfig {
... ...
... ...
ndk {
moduleName "nativefaceswap"
abiFilters 'armeabi-v7a','arm64-v8a'
}
}
sourceSets {
main {
jni.srcDirs = []
}
}
externalNativeBuild {
ndkBuild {
path "src/main/jni/Android.mk"
}
}
}
①第一处ndk的moduleName声明,与Android.mk文件中声明的so库名一致;同时指定过滤器,编译出指定架构的so库,本例中需要32位和64位so库,所以过滤器指明架构为armeabi-v7a和arm64-v8a。
②第二处sourecSets的声明,指明了so库放置的位置,Android Studio默认放置so库的文件夹为jniLibs,若想改为其他目录则需要在这里进行声明以更改。
③第三处externalNativeBuild的声明,指明了Android.mk文件的位置,用以编译时进行定位。
3)如果你的C++文件来自于开源项目,那么需要手动进行修改;
如上图,在main目录下新建jni文件夹,然后里面放入至少三个文件——Android.mk, Application.mk, cpp文件;
如上图,然后打开Android.mk文件,要修改几处地方:
第一处,由于项目依赖到了OpenCV,所以需要将OpenCv的位置指定为自己电脑下的位置,而不再使用原开源项目的位置;
第二处,LOCAL_MODULE,这里决定了包名,可以不进行修改,但注意和build.gradle中声明的保持一致。
4)最后点击"make project"开始编译项目,然后在build目录下寻找,最后新建jniLibs文件夹,然后拷贝到jniLibs文件夹内。
如此一来,就编译出了新的arm64-v8a文件内的so库。
三、上述过程中可能遇到的一些错误
1.编译时,打包时遇到包重复的问题
More than one file was found with OS independent path 'lib/arm64-v8a/libnativefaceswap.so'
More than one file was found with OS independent path 'lib/armeabi-v7a/libnativefaceswap.so'
解决方法:在build.gradle下添加:
packagingOptions {
pickFirst 'lib/armeabi-v7a/libnativefaceswap.so'
pickFirst 'lib/arm64-v8a/libnativefaceswap.so'
}
上述声明是告诉AS,进行打包,选取第一次出现的so库进行打包。
2.调用JNI时出错,报使用的Native方法没有实现的错误
java.lang.UnsatisfiedLinkError: No implementation found for
错误的可能原因:
1)so 文件路径问题
① 要么你就在main/下新建一个JniLibs文件夹,再把SDK里带的so文件目录诸如armeabi等等文件夹放进去就可以了。
② 要不你就把放so文件的目录诸如armeabi直接放在放jar文件的目录libs里头,然后修改build.gradle文件的内容,添加如下
android {
sourceSets{
main{ jniLibs.srcDirs=['Libs']
}
}
2)jni 调用类的路径一定要一致。也就是说 .so中函数声明涉及到的package name和class name与调用它的package name和class name不符。因此我们要改变我们工程中的package name和class name。使其与.so文件中函数签名提示的一致,在这个类中加入native方法的声明。
举例来说,我要使用FaceSwap类下的Native方法:
public native void portraitSwapNative(long addrImg1,
long addrImg2,
int[] landmarksX1,
int[] landmarksY1,
int[] landmarksX2,
int[] landmarksY2,
long addrResult);
那么我使用的C++原生代码就需要可以定位到这里,以下是FaceSwap类的位置信息:
所以对应使用的C++原生代码中定义的Native方法就需要进行对应的修改;
更多时候,我们是直接使用已有的so库而不想去修改原生代码,所以这里推荐使用的方法为,在自己项目目录下新建一个同名的目录,然后将代码移植过去,这样是成本最小的改动,如图:
如图,我们在主项目的目录下新建一级目录,命名依照C++代码内的声明,这样直接使用已有的so库就不会有问题了。
使用CMake编译时个人碰到的一些问题和解决方法
android Ndk开发大坑--创建新项目时编译报错 executing external native build for cmake
Android Studio 报错 executing external native build for cmake xxx CMakeLists.txt
这个问题的出错情况可能有多种原因导致,我的情况是在第一篇文章中找到答案。
补充一些知识,JNI相关的内容,开发中不常使用,容易忘记。
SDK:software development kit。软件开发工具包。
被软件开发工程师用于为特定的软件包、软件框架、硬件平台、操作系统等建立应用软件的开发工具的集合。
NDK 其中NDK的全拼是:Native Develop Kit。
Android NDK 是一套允许您使用原生代码语言(例如C和C++) 实现部分应用的工具集。在开发某些类型应用时,这有助于您重复使用以这些语言编写的代码库。NDK本身其实就是一个交叉工作链,包含了Android上的一些库文件,然后,NDK为了方便使用,提供了一些脚本,使得更容易的编译C/C++代码。
总之,在Android的SDK之外,有一个工具就是NDK,用于进行C/C++的开发。一般情况,是用NDK工具把C/C++编译为.so文件,然后在Java中调用。
上层通过JNI来调用NDK层的,使用这个工具可以很方便的编写和调试JNI的代码。
因为C语言的不跨平台,在Mac系统的下使用NDK编译出在Linux下能执行的函数库——so文件。其本质就是一堆C/C++的头文件和实现文件打包成一个库。
JNI,全称为Java Native Interface,即Java本地接口。通过JNI可以实现Java代码与C/C++代码的交互。
由于JNI是JVM规范的一部分,因此可以将我们写的JNI的程序在任何实现了JNI规范的Java虚拟机中运行。
Java调用C/C++在Java语言里面本来就有的,并非Android自创的,即JNI。JNI就是Java调用C++的规范。
当然,一般的Java程序使用的JNI标准可能和android不一样,Android的JNI更简单。
JNI下一共涉及到三个角色:C/C++代码、本地方法接口类、Java层中具体业务类。
#include <jni.h>
#include <string>
extern "C"
JNIEXPORT jstring
JNICALL
Java_com_example_ndkdemo_NDKTools_getStringFromNDK(
JNIEnv *env, jobject /* this */) {
std::string hello = "Hello from TEST C++";
return env->NewStringUTF(hello.c_str());
}
上面的Cpp文件中的函数解释:
jstring 是返回值类型;
com_example_hellojni是包名;
MainActivity 是类名;
stringFromJNI 是方法名;其中JNIExport和JNICALL是不固定保留的关键字不要修改。两个参数,JNIEnv* 代表了Java环境,通过这个JNIEnv* 指针,就可以对Java端的代码进行操作。
jobject 代表这个native方法的类实例或这个类的class对象。
传统JNI开发流程:
- 第1步:在Java中先声明一个native方法;
- 第2步:编译Java源文件javac得到.class文件,点击
Build
中的Make Project
或者Rebuild Project
进行编译来获取中间文件; - 第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调用本地代码。
CMake开发流程:
- 第1步:创建源文件,在
main
目录下新建一个目录cpp,右键点击此目录,然后选择 New > C/C++ Source File,比如命名为native-lib; - 第2步:跟build同级目录创建CMake构建脚本,CMake构建脚本是一个纯文本的文件,而且这个名字必须是是CMakeLists.txt; 模块的根目录并选择
New
——>File;
- 第3步:向CMake脚本文件写入数据;
- 第4步:向Gradle 关联到原生库;右键点击想要关联到原生库的Module,并从菜单中选择
Link C++ Project with Gradle;
externalNativeBuild {
cmake {
path 'CMakeLists.txt'
}
}
- 第5步: 编写native-lib.cpp;
- 第6步:
NDKTools.java下
添加库引用;
public class NDKTools {
static {
System.loadLibrary("native-lib");
}
public static native String getStringFromNDK();
}
CMake的运转流程
- 1、Gradle 调用外部构建脚本
CMakeLists.txt;
- 2、CMake 按照构建脚本的命令将 C++ 源文件
native-lib.cpp
编译到共享的对象库中,并命名为libnative-lib.so
,Gradle 随后会将其打包到APK中; - 3、运行时,应用的NDKTools会使用
System.loadLibrary()
加载原生库。应用就是可以使用库的原生函数stringFromJNI()
。
CMake文件解析
下面解析一份最基础的文件:
cmake_minimum_required(VERSION 3.4.1)
add_library( # Sets the name of the library.
native-lib
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
src/main/cpp/native-lib.cpp )
find_library( # Defines the name of the path variable that stores the
# location of the NDK library.
log-lib
# Specifies the name of the NDK library that
# CMake needs to locate.
log )
target_link_libraries( # Specifies the target library.
native-lib
# Links the log library to the target library.
${log-lib} )
CMakeLists.txt
我们看到这里主要是分为四个部分:
- cmake_minimum_required(VERSION 3.4.1):指定CMake的最小版本
- add_library:创建一个静态或者动态库,并提供其关联的源文件路径,开发者可以定义多个库,CMake会自动去构建它们。Gradle可以自动将它们打包进APK中。
- 第一个参数——native-lib:是库的名称;
- 第二个参数——SHARED:是库的类别,是
动态的
还是静态的;
- 第三个参数——src/main/cpp/native-lib.cpp:是库的源文件的路径;
- find_library:找到一个预编译的库,并作为一个变量保存起来。由于CMake在搜索库路径的时候会包含系统库,并且CMake会检查它自己之前编译的库的名字,所以开发者需要保证开发者自行添加的库的名字的独特性。
- 第一个参数——log-lib:设置路径变量的名称;
- 第一个参数—— log:指定NDK库的名子,这样CMake就可以找到这个库;
- target_link_libraries:指定CMake链接到目标库。开发者可以链接多个库,比如开发者可以在此定义库的构建脚本,并且预编译第三方库或者系统库。
- 第一个参数——native-lib:指定的目标库;
- 第一个参数——${log-lib}:将目标库链接到NDK中的日志库。
参考文章《Android JNI学习(二)——实战JNI之“hello world”》《Android JNI 篇 - 从入门到放弃》
.so文件
so是shared object的缩写,见名思义就是共享的对象,机器可以直接运行的二进制代码。大到操作系统,小到一个专用软件,都离不开so。so主要存在于Unix和Linux系统。
我们通过C/C++开发的软件,如果以动态链接库的形式输出,那么在Android中它的输出就是一个.so文件。
相比于.a,.so文件是在运行时,才会加载的。所以,当我们将.so文件放入工程时,一定有一段Java代码在运行时,load了这个native库,并通过JNI调用了它的方法。
所以,当我们使用JNI开发时,我们就是在开发一个.so文件。不论我们是开发一个工程,还是开发一个库,只要当我们使用C++开发,都会生成对应的.so文件。
所以JNI开发的核心是,我们生成so的过程,和使用so的过程。
两种NDK使用方式的区别《AndroidStudio CMake和传统JNI的区别》
CMake优势:
- 可以直接的在C/C++代码中加入断点,进行调试
- java引用的C/C++中的方法,可以直接ctrl+左键进入
- 对于include的头文件或者库,也可以直接进入
- 不需要配置命令行操作,手动的生成头文件,不需要配置android.useDeprecatedNdk=true属性
中文的CMake手册