我们在前面的文章中大致了解了NDK的概念以及NDK的大致组成部分
现在该让我们亲手实现一下NDK了
本文中我将使用Cmake 和 ndk-build来分别实现NDK
一. 实现NDK的大致步骤
实现NDK的完整步骤如下:
- 创建Android ndk project,使用
Native
关键字定义Java
方法(即需要调用的native方法) - 使用
javac
编译上述Java
源文件 (即.java文件
)最终得到.class
文件 - 通过
javah
命令编译.class
文件,最终导出JNI
的头文件(.h
文件) - 用
C++
实现在Java
中声明的Native
方法 Activity
加载so库
,使用native
方法- 生成
.so
库文件 - 执行代码
我们可以发现实际上实现NDK的步骤和实现JNI的步骤基本是完全一样的
3,4步骤在cmake
和ndk-build
中自动为我们做好了,我们没必要手动完成,所以可以省去3,4步骤
生成.so
文件的操作也是自动完成的,我们也可以省去
所以改变后我们需要手动完成的步骤如下
- 创建Android ndk project,使用
Native
关键字声明native
方法 - 用
C++
实现Native
方法 Activity
中写加载so库
,调用native
方法的代码- 执行代码
下面我将按照上面的步骤一步步的实现NDK
实现NDK的前提是我们得先配置NDK环境
但是因为环境变量的配置网上很多,所以这里我省略了环境变量的配置的阐述
二.实现NDK的相关配置
1. 使用Android studio自动创建一个Android NDK 项目
Android studio很贴心的为我们提供了自动化创建一个Android NDK 项目的傻瓜式操作
我们只要点击Android studio
中的
File -> new -> new project
然后选择下图中的
nativeC++
,然后和普通的Android project进行一样的操作就行了
2. 手动创建 Android NDK Project
虽然Android studio 提供了自动创建 NDK 项目的功能,可以自动进行NDK所需的配置
但是为了更好的了解 NDK ,也为了需要修改NDK的相关文件时知道怎么修改, 我们还是有必要了解怎么进行手动配置
因为我们已经知道了 NDK 的编译工具分为 cmake
和 ndk-build
,所以下面我将分别说明三类配置
- 通用配置
- cmake 配置
- ndk-build 配置
注意: 通用配置是一定要配置的,cmake和ndk-build配置只要选择一个就够了
2.1 通用配置
-
设置 NDK 的路径
在项目的
local.properties
中进行如下的设置注意:这里的
ndk.dir
要设置成你自己的ndk路径
ndk.dir=/Users/fczhao/Library/Android/sdk/ndk/20.0.5594570
-
设置是否支持以前的版本
因为我们要操作的Android ndk project 中有许多有悠久的历史,所以使用的还是好多年前的ndk包,为了让 gradle 支持以前的 ndk ,我们可以对gradle进行设置
当然如果你的NDK使用的是现在的版本,可以不进行配置
在项目的
gradle.properties
中进行如下的设置
android.useDeprecatedNdk=true
2.2 cmake 配置
完成cmake
配置时,不要忘了配置通用配置
在使用cmake时,我们必须要配置四个文件,在下图中我圈出来了即是这四个文件
同时我也将在下面分别说明他们进行了什么改变
注意:下面我分析的代码是Android studio自动生成的代码
-
在使用 NDK 的 **
module
**的build.gradle
中进行如下的配置注意:
这里我们的module叫
app
,所以在**app
**里面进行配置同时是在
module
中配置,而不是在 project 中的 build.gradle 中配置
android {
......
defaultConfig {
......
externalNativeBuild {
cmake {
cppFlags ""
}
}
}
//extenalNativeBuild 是 gradle 的一个属性
//可以指定 NDK 的配置,里面主要有 cmake 和 ndk-build两个属性
//cmake 和 ndk-build 中的属性可以指定 cmake 和 ndk-build 的版本,路径等等
externalNativeBuild {
cmake {
//指定了 CmakeLists.txt 文件的路径
//指定了 cmake的版本
path "src/main/cpp/CMakeLists.txt"
version "3.10.2"
}
}
}
在main
目录下面创建 cpp
目录 , 里面创建两个文件**CmakeLists.txt
和 xxx.cpp
**
-
CMakeLists.txt
这个文件主要配置 cmake ,上面代码中的path指向的就是这个文件的路径
下面的是最基础的配置,即如果要使用cmake,那么一定要在 cmakeList.txt 进行配置的
# 注意:native-lib是Android studio自动生成的,要是喜欢,可以改成其他的名字
# 设置cmake的最小版本
cmake_minimum_required(VERSION 3.4.1)
# add_library
# 可以创建一个so库,同时可以进行下面几个配置
# 1.设置编译后 so文件的名字
# 2.把它设为 静态库(Static) 或 共享库/动态库(shared)
# 3.提供这个库的源码所在的 相对路径
# 也可以定义多个库,Gradle会自动将共享库与APK打包在一起
add_library(
# 设置编译后 so 库 的名字,这里名字设置为 native-lib
# .so 文件可以和 .cpp 文件的名字不同,即我这里可以不用 native-lib 这个名字
native-lib
# 设置生成的文件为共享库,即 so 文件,这里生成 native-lib.so
# NDK中的library分为 静态库(static)和共享库(shared)
# 如果要生成静态库,这里设置为 STATIC ,生成后文件为 native-lib.a
SHARED
# 源文件的相对路径,即要被编译的文件路径
# 有这个,我们其实可以把 xxx.cpp 放在任何位置,但是一般我们都放在 main/cpp下面
# 这个cpp文件的路径可以有多个,即多个cpp文件编译到成一个so文件
native-lib.cpp )
# find_library
# 从系统库中搜索指定的依赖并设置它的别名
# 这里我们从系统查找 log 库,并把他的别名设置为 log-lib
# 由于默认情况下CMake在搜索路径中包含系统库,因此只需要指定要添加的依赖库的名称即可
# 在完成编译之前,CMake会检查该库是否存在
find_library(
# 设置该依赖库的别名
log-lib
# 指定您要CMake查找的的依赖库的名称
log )
# target_link_libraries
# 配置库的依赖关系
# 可以依赖多个库,比如可以依赖下面的几个
# 1. 自己在项目中定义的库
# 2. 依赖的第三方库
# 3. 系统库
target_link_libraries( # 指定需要这些依赖的源文件
native-lib
# 依赖库,可以是多个
${log-lib} )
-
xxx.cpp
这个文件就是我们要进行调用的 C++ 代码这里是 native-lib.cpp , 里面的大致代码如下
#include <jni.h>
#include <string>
//导入C++的头文件
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_ndk_1test_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
xxx.cpp 中的一些注意事项:
extern "C" JNIEXPORT jstring JNICALL Java_com_example_ndk_1test_MainActivity_stringFromJNI
看起来很长,实际上我们可以把它分成三部分:
-
extern "C"
这个表示使用C语言的方式来进行编译
每个方法都需要这个来定义,但是如果我们有多个方法时,可以使用**
extern "C"{...}
**来定义extern "C"{ JNIEXPORT jstring JNICALL Java_com_example_myapplication_MainActivity_stringFromJNI(...) {...} JNIEXPORT jstring JNICALL Java_com_example_myapplication_MainActivity_sayHelloWorld(...) {...} }
-
JNIEXPORT jstring
这个声明了该方法的返回类型,这里的返回类型为 string
JNI中的返回类型和Java,C++中的不太一样,具体的可以去网上查一下
-
JNICALL Java_com_example_ndk_1test_MainActivity_stringFromJNI
这里实际上是标志了我们要JNI要调用方法(或者说我们要用C++覆写的native方法)的具体路径
这个实际上也是由三个部分组成
a. Java 标识了这个是Java方法,
J
一定要大写b. 包名,com_example_ndk_1test_MainActivity是我们的包名
c. 方法名 stringFromJNI是我们要覆写的方法名
如果仔细观察,会发现路径中有 _
和 _1
这两个东西,那这两个东西到底是什么呢,实际上在Java包名转换成这个的过程中有下面两个规则
a. 包中的 .
会替换为_
b. 包中的_
会替换为_1
所以我们原来的包名是com.example.ndk_test.MainActivity.stringFromJNI
转换后变成了com_example_ndk_1test_MainActivity_stringFromJNI
2.3 ndk-build 配置
在完成ndk-build
时,不要忘记配置通用配置
在ndk-build中,我们要修改的文件如图所示:
- 红色部分是我们要进行修改的
- 黄色部分是我们要删除的,因为黄色部分是
Android studio
自动生成的关于cmake
实现的部分,我们这里要使用ndk-build
,拿来没用,所以要删除
下面我将依次分析红色部分的内容:
-
module
中的build.gradle
在这里我们声明我们要使用
ndk-build
代码如下:
android { ... defaultConfig { ... // 这里注释掉,因为这里是Android studio 自动生成的关于cmake的代码 // externalNativeBuild { // cmake { // cppFlags "" // } // } } ... externalNativeBuild { // cmake { // path "src/main/cpp/CMakeLists.txt" // version "3.10.2" // } // ndkBuild path是 Android.mk文件的路径 ndkBuild{ path 'src/main/jni/Android.mk' } } }
-
创建
jni
文件夹,里面创建Android.mk,Application.mk,xxx.cpp
-
Android.mk
# 这个是源文件的路径,call my-dir表示了返回当前Android.mk所在的目录 LOCAL_PATH := $(call my-dir) # 清除许多 LOCAL_XXX 变量 # 注意:不会清除 LOCAL_PATH include $(CLEAR_VARS) # LOCAL_MODULE 变量存储要构建的模块的名称 # 这里最终会生成叫 libnative-lib.so 的文件 LOCAL_MODULE := native-lib # 源文件名字 # 可以有多个源文件,使用空格隔开 LOCAL_SRC_FILES := native-lib.cpp # 指定编译出什么二进制文件 # 这里编译出共享库,即:.so文件 # 编译出静态库可以使用: BUILD_STATIC_LIBRARY include $(BUILD_SHARED_LIBRARY)
-
Application.mk
# 定义生成的二进制文件要生成的CPU架构 # 这里指定生成 arm64-v8a 可以用的二进制文件 APP_ABI := arm64-v8a # 定义可以使用该二进制文件的Android版本 # 注意:比 android:minSdkVersion 值大时,会有警告 APP_PLATFORM := android-21 # 默认情况下,ndk-build 假定 Android.mk 文件位于项目根目录的相对路径 jni/Android.mk 中。 # 要从其他位置加载 Android.mk 文件,将 APP_BUILD_SCRIPT 设置为 Android.mk 文件的绝对路径。 APP_BUILD_SCRIPT # 没有这个会出现error: no template named 'pair'的问题 APP_STL := c++_shared
-
native-lib.cpp
这里的cpp实际上和上面
cmake
中的是完全一样的,这里我也特地把他命名为cmake中的,具体要注意的事情参考上面cmake
中的即可到了这一步(完成
Android.mk
,Application.mk
,xxx.cpp
的配置后),我们就可以生产.so
文件了命令行进入
jni
目录下,输入ndk-build
即可可能输入后,会有一些问题,关于这些问题,我将在后面的博客中进行阐述
-
MainActivity
MainActivity中的设置也是和cmake中的一样的,ndk-build 中我也特地把生成
.so
文件命名为cmake中的
三. 使用cmake来实现ndk
现在我们已经知道了cmake实现NDK的一些东西了,下面让我们尝试自己动手实现一下
cmake为我们减少了很多的复杂操作,所以我们只需要三步就可以实现NDK
- 声明Native方法
- 实现Native方法,经过一系列操作,最终生成.so 文件
- 加载.so文件,调用Native方法
下面我将具体的讲述每一个步骤是怎么实现的:
1 在Activity中定义一个native
方法
public class MainActivity extends AppCompatActivity {
//定义Native方法
public native String stringFromJNI();
}
2. 用C++实现Native方法
注意:是在**cmakeLists.txt
中add_library
指定的位置实现**
如在上面我们指定了 native-lib.cpp
会被我们编译
那么我们只能在native-lib.cpp
中实现Native方法,否则我们写的 C++ 代码没有任何作用
add_library(
......
native-lib.cpp )
并且注意覆写的方法名是否正确,如上文的
extern "C" JNIEXPORT jstring JNICALL Java_com_example_myapplication_MainActivity_stringFromJNI
但是我们可以发现这个方法名太长了,所以Android studio 也提供了很简便的方法来自动生成方法名
我们在Activity中的Native方法,按下option + enter
然后就会自动生成下面的代码
extern "C"
JNIEXPORT jstring JNICALL
Java_com_example_myapplication_MainActivity_stringFromJNI(JNIEnv *env, jobject thiz) {
// TODO: implement stringFromJNI()
}
然后我们在里面写我们需要实现的C++代码即可
这里我们返回一个 "Hello Wrold ,this is from C++ "
extern "C"
JNIEXPORT jstring JNICALL
Java_com_example_myapplication_MainActivity_stringFromJNI(JNIEnv *env, jobject thiz) {
std::string hello = "Hello Wrold ,this is from C++ ";
return env->NewStringUTF(hello.c_str());
}
3. 在Activity中调用 相应的库(共享库或者静态库)
//加载so文件
static {
System.loadLibrary("native-lib");
}
4. 调用Native方法
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView tv = findViewById(R.id.sample_text);
//调用这个方法
tv.setText(stringFromJNI());
}
最后运行这个Android project
因为我们在上面的Activity中把stringFromJNI的值设置给了TextView,所以可以显示Hello Wrold ,this is from C++
5. .so
文件
这个过程看起来挺简单的,但是如果我们看了上面的步骤:
-
声明Native方法
-
实现Native方法,经过一系列操作,最终生成.so 文件
-
加载.so文件
-
调用Native方法
会发现这样一个问题,我们在这个过程中的需要的
.so
文件去哪里了呢?实际上,如果我们仔细寻找,可以在**
app/build/intermediates/cmake
**目录下找到,如下图:因为我是在app这个module中使用了ndk,所以这里是app,app改成你使用了ndk的module就行了
5.1 验证Activity调用的是 .so
文件
但是为了更好的了解NDK的实现过程,以及确定Activity的确是调用 .so 库,而不是直接调用 C++ 代码
所以我们可以使用这样的思路来验证:
使编译的.cpp
文件与生成的.so
文件不同名,Activity加载.cpp
名字的文件,看是否可以运行
我们使用下面的步骤来进行验证
-
修改
cmakeLists.txt
我们上面已经知道,
cmakeLists.txt
决定了cmake的编译所以我们修改生成
.so
文件的相关代码,实现.cpp
与.so
不同名add_library( # 设置编译后 so 库 的名字,这里名字设置为 native-test # 最后会生成 libnative-test.so # 这里我们让 .cpp 与 .so 不同名 native-test native-lib.cpp ) target_link_libraries( # 这里必须与 .so 文件同名 native-test ... )
然后我们到cmake目录下去找,看到生成了
libnative-test.so
然后Activity还是上面的代码,加载
native-lib
文件static { System.loadLibrary("native-lib"); }
运行代码,会发现有这样的报错:
java.lang.UnsatisfiedLinkError: ...... couldn't find "libnative-lib.so"
这里我们就可以完全明白了 Activity中调用的是
.so
文件,而不是直接调用C++代码我们把代码改成
native-test
后,运行成功static { System.loadLibrary("native-test"); }
四. 使用ndk-build
实现NDK
这里我将使用ndk-build
来实现NDK,ndk-build
中的很多注意事项,实际上在上面的配置和cmake实现NDK过程中已经很清楚的说明了,所以已经说过的提示,这里将不赘述
1. 使用Android studio
创建project
因为Android studio
中现在默认使用cmake
,所以自动创建的是已经配置好的cmake环境,我们这里使用ndk-build
,需要对project
进行一些改变,如下:
- 删除
cpp
目录下所有文件 - 建立
jni
目录 jni
目录下建立三个文件,Android.mk,Application.mk,xxx.cpp
,这里的xxx.cpp为了方便,也为了保留native-lib.cpp
中的JNI
语句方便以后我们参考,我们可以直接把cpp
目录下的native-lib.cpp
弄过来- 其他的配置将在下面说明
2. gradle.properties
因为ndk-build
常在历史很久的项目中使用,所以一般使用的都是几年前的NDK
,为了让Android studio 支持以前的NDK,我们必须写这个
android.useDeprecatedNdk = true
3. module中的
build.gradle
android {
...
defaultConfig {
...
// 这里注释掉,因为这里是Android studio 自动生成的关于cmake的代码
// externalNativeBuild {
// cmake {
// cppFlags ""
// }
// }
}
...
externalNativeBuild {
// cmake {
// path "src/main/cpp/CMakeLists.txt"
// version "3.10.2"
// }
// ndkBuild path是 Android.mk文件的路径
ndkBuild{
path 'src/main/jni/Android.mk'
}
}
}
4. jni
目录下的配置
Android.mk
# 这个是源文件的路径,call my-dir表示了返回当前Android.mk所在的目录
LOCAL_PATH := $(call my-dir)
# 清除许多 LOCAL_XXX 变量
# 注意:不会清除 LOCAL_PATH
include $(CLEAR_VARS)
# LOCAL_MODULE 变量存储要构建的模块的名称
# 这里最终会生成叫 libnative-lib.so 的文件
LOCAL_MODULE := native-lib
# 源文件名字
# 可以有多个源文件,使用空格隔开
LOCAL_SRC_FILES := native-lib.cpp
# 指定编译出什么二进制文件
# 这里编译出共享库,即:.so文件
# 编译出静态库可以使用: BUILD_STATIC_LIBRARY
include $(BUILD_SHARED_LIBRARY)
jni
目录下的Application.mk
# 定义生成的二进制文件要生成的CPU架构
# 这里指定生成使用CPU架构都可以用的二进制文件
APP_ABI := all
# 定义可以使用该二进制文件的Android版本
# 注意:比 android:minSdkVersion 值大时,会有警告
APP_PLATFORM := android-21
# 默认情况下,ndk-build 假定 Android.mk 文件位于项目根目录的相对路径 jni/Android.mk 中。
# 要从其他位置加载 Android.mk 文件,将 APP_BUILD_SCRIPT 设置为 Android.mk 文件的绝对路径。
APP_BUILD_SCRIPT
# 没有这个会出现error: no template named 'pair'的问题
APP_STL := c++_shared
jni
目录下的native-lib.cpp
#include <jni.h>
#include <string>
//因为只声明了这个方法,所以这里只实现了stringFromJNI方法
//native方法的实现按照这个的格式即可
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_myapplication_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello , this is from C11 ";
return env->NewStringUTF(hello.c_str());
}
JNI语法中的一些注意点在上面的cmake
部分已经强调了,这里不再赘述
5. 加载so
库 调用native
方法
public class MainActivity extends Activity {
// 加载 so 库
// 这里的名字一定和 Android.mk 中 LOCAL_MODULE 相同
// 如:LOCAL_MODULE := native-lib
static {
System.loadLibrary("native-lib");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 调用native方法
TextView tv = findViewById(R.id.sample_text);
tv.setText(stringFromJNI());
}
//声明native方法
public native String stringFromJNI();
}
6. so
库
命令行进入jni
目录,输入ndk-build
即可
fczhao@iMac ~/SelfCode/Android/MyApplication/app/src/main/jni ndk-build ✔ 509 13:09:44
Android NDK: WARNING: APP_PLATFORM android-28 is higher than android:minSdkVersion 1 in /Users/fczhao/SelfCode/Android/MyApplication/app/src/main/AndroidManifest.xml. NDK binaries will *not* be compatible with devices older than android-28. See https://android.googlesource.com/platform/ndk/+/master/docs/user/common_problems.md for more information.
[arm64-v8a] Compile++ : native-lib <= native-lib.cpp
[arm64-v8a] SharedLibrary : libnative-lib.so
[arm64-v8a] Install : libnative-lib.so => libs/arm64-v8a/libnative-lib.so
[arm64-v8a] Install : libc++_shared.so => libs/arm64-v8a/libc++_shared.so
[x86_64] Compile++ : native-lib <= native-lib.cpp
[x86_64] SharedLibrary : libnative-lib.so
[x86_64] Install : libnative-lib.so => libs/x86_64/libnative-lib.so
[x86_64] Install : libc++_shared.so => libs/x86_64/libc++_shared.so
[armeabi-v7a] Compile++ thumb: native-lib <= native-lib.cpp
[armeabi-v7a] SharedLibrary : libnative-lib.so
[armeabi-v7a] Install : libnative-lib.so => libs/armeabi-v7a/libnative-lib.so
[armeabi-v7a] Install : libc++_shared.so => libs/armeabi-v7a/libc++_shared.so
[x86] Compile++ : native-lib <= native-lib.cpp
[x86] SharedLibrary : libnative-lib.so
[x86] Install : libnative-lib.so => libs/x86/libnative-lib.so
[x86] Install : libc++_shared.so => libs/x86/libc++_shared.so
我们可以看到他们生成了:x86
,x86_64
,arm64-v8a
,armeabi-v7a
的``so`文件
可以看见创建了libs
, obj
,两个目录来存放生成的so
文件
最后运行Android studio project即可