技术的更新换代真的很快,以前做NDK开发用的是ndk-build,最近要用到ndk,查了一下资料,几年前已经改用CMake了,其实之前有学习过这个,但是时间一长,又给忘了,所以,好记性不如烂笔头,这次得做个笔记了。
创建一个新项目,起名为“NdkDemo”
点击AndroidStudio右上角的SDK Manager图标,安装NDK和CMake,如下
打开项目结构(Ctrl + Shift + Alt + S),并设置NDK位置,如下:
创建jni目录:右击app > New > Folder > JNI Folder,此时会在app/src/main目录下生成jni目录,最新官方文档教程中使用的是cpp目录,所以也可以手动创建名为cpp的目录,创建jni也可以,在Android视图下,jni目录会显示成cpp,如下:
对于强迫症,还是想知道为什么会这样?
在AndroidStudio3.5版本以及更新的版本中,对于native有了新的项目结构。
旧版本native源文件存放于app/src/main/jni/目录,CMakeLists.txt存放于app/目录
新版本native源文件和CMakeLists.txt都存放于app/src/main/cpp/目录
所以,我们尽量使用新版本的方式吧!
创建C++文件:右击cpp > New > C/C++ Source File,在弹出的对话框中输入文件名为demo(可以随意取名),Type选择".cpp",这样就创建出了一个demo.cpp的文件,用于写C++代码。
创建CMakeLists.txt文件:右击cpp > New > File,在弹出来的输入框中输入:CMakeLists.txt (注:必须用这个名字),并输入如下代码:
# 设置构建native library所需的CMake最低版本。
cmake_minimum_required(VERSION 3.4.1)
#创建一个库(多次调用add_library即可创建多个库)
add_library( # 设置库的名称
demo-lib
# 将库设置为共享库(即so文件)
SHARED
# 指定源文件的相对路径
demo.cpp )
CMakeLists.txt就是一个配置文件,指定了要编译的源文件,而且配置了要生成一个名为demo-lib的库,它的文件名为libdemo-lib.so。
AndroidStudio是中构建apk文件是使用gradle来完成的,所以我们还要告诉gradle在构建的时候要编译so文件并打包到apk中,只需要把CMakeLists.txt配置文件告诉gradle即可,方法如下:
右击app > Link C++ Project with Gradle,在弹出来的对话框中指定CMakeLists.txt文件的位置,如下:
此时会在app目录下的build.gradle文件中生成如下内容:
android {
externalNativeBuild {
cmake {
path file('src/main/jni/CMakeLists.txt')
}
}
}
如果这个时候在Build窗口中报出一个构建失败的错误,如下:
大概意思是说执行native build失败了,没关系,因为我们的demo.cpp文件中什么也没写。
在MainActivity中声明一个native方法,如下:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
external fun getString(): String
}
注:这里使用的是kotlin语言,方法使用external修饰,如果是java则用native修饰。
在demo.cpp文件中实现对应的jni函数(getString方法),如下:
#include <jni.h>
#include <string>
extern "C"
JNIEXPORT jstring JNICALL
Java_com_even_app_ndkdemo_MainActivity_getString(JNIEnv *env, jobject thiz) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
此函数的功能为返回一个字符串:“Hello from C++”。此时如果发现代码有红色报错,没关系,点击右上角的同步Gradle按钮即可消除,如下:
加载动态连接库(即so文件),并调用native方法,如下:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Toast.makeText(this, getString(), Toast.LENGTH_SHORT).show()
}
external fun getString(): String
companion object {
init {
System.loadLibrary("demo-lib")
}
}
}
运行查看效果,如下:
OK,大告成功,如上图所示,拿到了从C++函数中返回的字符串:“Hello from C++”。
完成上面的例子后,如果还想再增加第二个native方法时,可以使用自动生成功能,如在MainActivity中添加一个add方法,如下:
external fun add(x: Int, y: Int): Int
此时会报错,把光标定位到add上,按Alt + Enter,选择创建jni函数即可,如下:
此时就会自动在demo.cpp中增加对应的jni函数,如下:
extern "C"
JNIEXPORT jint JNICALL
Java_com_even_app_ndkdemo_MainActivity_add(JNIEnv *env, jobject thiz, jint x, jint y) {
// TODO: implement add()
}
在创建第一个jni函数的时候无法使用此方式来自动生成,这应该算是AndroidStudio的一个Bug吧,只有手动写了一个jni函数之后,第二个才会出现创建jni函数的命令。
File > New > New Project > Native C++,此时可能会报一个错,如下:
错误说的是NDK没有配置,这应该也算AndroidStudio的一个Bug吧,我已经安装有NDK,有好几个版本(包含最新版本),AndroidStudio应该自动给我选一个,用最新的也行。上面描述说的意思是首选的NDK版本是21.0.6113669,我并没有安装这个版本,所以它提示我们安装,其实没有必要安装这个版本的,按Ctrl + Shift + Alt + S打开项目结构,在弹出来的对话框中设置一个已经安装的NDK位置即可。然后就可以直接运行查看效果了。
如果是要做一个新项目,这样的方式创建NDK项目非常方便。
使用向导创建的NDK项目还会在app下的build.gradle中多一个cppFlag的配置,如下:
android {
defaultConfig {
externalNativeBuild {
cmake {
cppFlags ""
}
}
}
}
这个应该是用于指定C++的版本,cppFlags为空,这是因为我在创建这个项目的向导中选择的C++版本时选择了默认,如下:
点击下拉箭头,可以看到我们当前设置的NDK的版本所支持的C++版本,不同的NDK版本可能会支持不同的C++版本,截图如下:
假如我们选择C++17,则cppFlag属性如下:
android {
defaultConfig {
externalNativeBuild {
cmake {
cppFlags "-std=c++17"
}
}
}
}
直接在AndroidStudio中运行项目时,AndroidStudio会根据手机的CPU类型来生成对应的CPU架构的so文件。点击Build > Analyze APK,在弹出来的对话框中选择app\build\outputs\apk\debug\app-debug.apk,这样就能看到apk文件的内部结构了,如下:
因为我的手机CPU是arm64-v8a结构的,所以直接运行时它只生成和手机对应的so文件,如果想要生成所有的,可以点击Build > Build Bundle(s) / APK(s) > Build APK(s),这样的话也会生成debug.apk,但是这个apk并不针对哪一个手机,所以会生成所有的so,如下:
这里说的所有的so是指特定NDK版本支持的so CPU类型,从NDK 17开始,不再支持armeabi,所以上图中没有看到armeabi的so。为什么不支持armeabi了呢?因为现在的手机基本上都支持armeabi-v7a了。
如果只想配置生成特定的so类型,可以在app下的build.gradle中设置如下:
android {
defaultConfig {
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'
}
}
}
从apk中提取so文件很容易,把apk扩展名改成rar,然后用压缩软件解压即可。
通过生成apk,从apk中获取是比较容易记的,从build目录中就需要记一下目录结构,有时候会记不住,但是也没关系,记不住的时候就从apk中取so嘛。
在Build Variants面板中选择release版本,默认是debug版本,不知道有什么区别,但是选release肯定是比较正规嘛,准没错。然后执行菜单命令:Build/Build Bundle(s)/Apk(s)/Build Apk(s),这样就会生成一个release版本的apk,是一个没有签名的apk(如果gradle文件中没有配置签名的话),生成apk的同时也会生成对应的so文件,在如下位置:
NDK入门:https://developer.android.google.cn/ndk/guides
代码实验室:https://codelabs.developers.google.com/codelabs/android-studio-cmake/#0
Github上提供的NDK Demo:https://github.com/android/ndk-samples
关于CMake:https://developer.android.com/ndk/guides/cmake.html
配置CMake:https://developer.android.google.cn/studio/projects/configure-cmake#add-ndk-api
添加native代码:https://developer.android.google.cn/studio/projects/add-native-code
JNI相关:https://developer.android.google.cn/training/articles/perf-jni
JNI接口规范:https://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/jniTOC.html
jniLibs目录在main目录下,如:
以前我们都是把so文件放到jniLibs目录中来使用,现在好了,打包时直接编译源码为so并打包到apk。人有时候就是想使用原来的方式,网上找了一下,在CMakeLists.txt中加入下面这句代码:
# 设置so文件输出到jniLibs目录中
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/../jniLibs/${ANDROID_ABI})
在AndroidStudio中直接运行应用,确实会在jniLibs目录中生成对应的so文件了,但是报一个错误,如下:
More than one file was found with OS independent path ‘lib/armeabi-v7a/libdemo-lib.so’. If you are using jniLibs and CMake IMPORTED targets, see https://developer.android.com/studio/preview/features#automatic_packaging_of_prebuilt_dependencies_used_by_cmake
提示中有一个链接,但是点击这个连接之后找不到相关的内容,应该是Google重新编辑网页,删除掉了,这个链接是跳转到AndroidStudio4.1的新特性的,在这里面会有“Automatic packaging of prebuilt dependencies used by CMake”的相关内容,但是已经被编辑掉了,在Google中搜索这段英文时能找到链接,但是要点快照,出来的网页就会有需要的内容,这里把原文搬过来,并配上翻译如下:
Automatic packaging of prebuilt dependencies used by CMake
自动打包CMake使用的预构建依赖项
Prior versions of the Android Gradle Plugin required that you explicitly package any prebuilt libraries used by your CMake external native build by using jniLibs:
早期版本的Android Gradle插件要求您使用以下命令显式打包CMake外部本机内部版本使用的所有预构建库 jniLibs:
sourceSets {
main {
// The libs directory contains prebuilt libraries that are used by the
// app's library defined in CMakeLists.txt via an IMPORTED target.
// libs目录包含预构建的库,这些库由CMakeLists.txt中通过导入目标定义的应用程序库使用。
jniLibs.srcDirs = ['libs']
}
}
With Android Gradle Plugin 4.0, the above configuration is no longer necessary and will result in a build failure:
使用Android Gradle Plugin 4.0时,不再需要上述配置,并且会导致构建失败:
What went wrong:
Execution failed for task ‘:app:mergeDebugNativeLibs’.
A failure occurred while executing com.android.build.gradle.internal.tasks.Workers$ActionFacade
More than one file was found with OS independent path ‘lib/x86/libprebuilt.so’
External native build now automatically packages those libraries, so explicitly packaging the library with jniLibs results in a duplicate. To avoid the build error, simple remove the jniLibs configuration from your build.gradle file.
现在,外部本机构建会自动打包这些库,因此将jniLibs结果与库明确打包在一起。为避免生成错误,只需jniLibs从build.gradle文件中删除配置即可。
使用AndroidStudio的向导创建的NDK项目,默认就已经导入了Logcat库,如下:
如上图,画红圈的即为在C中引入Logcat相关设置,这是向导自动创建的,也可以删掉注释,简化一下,如下:
在C源文件中添加如下代码:
#include <android/log.h>
#define LOG_TAG "JNI"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
然后就可以调用LOGD或LOGI来打印日志了,如下:
extern "C"
JNIEXPORT jint JNICALL
Java_cn_android666_nativehello_MainActivity_add(JNIEnv *env, jobject thiz, jint x, jint y) {
LOGI("x = %d", x); // 使用语法和printf()函数是一样的
LOGD("y = %d", y);
LOGI("Hello World!");
return x + y;
}