NDK 是 Android 提供的一个开发工具包,是一组使我们能将 C 或 C++(“原生代码”)嵌入到 Android 应用中的工具。NDK 能够从 C/C++ 源代码构建原生共享库(.so)或原生静态库(.a),并支持将静态库关联到其他库。JNI 是 Java 和 C/C++ 组件用于相互通信的接口。这样一来,通过 NDK 和 JNI ,我们就能很方便的在 Android 应用中使用 C 和 C++ 代码。
Android Studio 编译原生库的默认构建工具是 CMake,CMake 可适用于跨平台项目。由于很多现有项目都使用 ndk-build 构建工具包,因此 Android Studio 也支持了 ndk-build,相比于 CMake,ndk-build 速度更快,但仅支持 Android。
ndk-build
上面说的都抽象,我们来看个栗子吧:
- 我定义了一个本地方法 stringFromJNI(),我想通过 c++ 代码来实现它,并通过 NDK 从这些 c++ 源码中构建出 .so 文件(命名随意),来供我的 java 层来调用,怎么做呢?
public class HelloJni {
public native String stringFromJNI();
}
复制代码
为了更直观,我先将整体的目录结构贴下:

- 首先我要知道 native 方法所在的 HelloJni.java 所对应的 .h 头文件,通过 javah 命令很容易得到。HelloJni.class 文件可以通过 javac 命令编译,在这里就直接拿 AS 自动编译好的了。
$ cd /Users/zuomingjie/gitSpace/BlogSample/app/build/intermediates/javac/debug/classes/
$ javah com.blog.a.jni.HelloJni
复制代码
- 这是 HelloJni.class 编译后的 .h 头文件,格式很讲究,Java 打头 + 类的全限定名 + 方法名,更多规范和 JNI 知识可查看 developer :
#include <jni.h>
#ifndef _Included_com_blog_a_jni_HelloJni
#define _Included_com_blog_a_jni_HelloJni
#ifdef __cplusplus
extern "C" {
#endif
JNIEXPORT jstring JNICALL Java_com_blog_a_jni_HelloJni_stringFromJNI
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
复制代码
- 编写我们的 native-lib.cpp(命名随意), 实现这个头文件,这个方法就是返回一个字符串:
#include <jni.h>
#include <string>
#include "HelloJni.h"
extern "C" JNIEXPORT jstring JNICALL
Java_com_blog_a_jni_HelloJni_stringFromJNI(
JNIEnv* env,
jobject ) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
复制代码
APP_STL := c++_shared
APP_CPPFLAGS := -frtti -fexceptions
APP_ABI := armeabi-v7a arm64-v8a
APP_PLATFORM := android-19
复制代码
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := myJniTest
LOCAL_SRC_FILES := cpp/native-lib.cpp
include $(BUILD_SHARED_LIBRARY)
复制代码
- 通过 ndk-build 命令构建 .so 文件(NDK 编译系统默认会在 $(APP_PROJECT_PATH)/jni 目录下寻找名为 Android.mk):
$ cd /Users/zuomingjie/gitSpace/BlogSample/app/src/main/java/com/blog/a/jni
$ ndk-build
复制代码
- 由于生成的 .so 文件在 jni 同级的 libs 目录下,我们可以直接拷贝至 jniLibs 目录,或者直接就在 gradle 文件中指定:
sourceSets {
main() { jniLibs.srcDirs = ['src/main/java/com/blog/a/libs'] }
}
复制代码
CMake
Android NDK 支持使用 CMake 编译我们的 C 和 C++ 代码,这也是在日常开发中最常见的方式。通过编写构建脚本 CMakeLists.txt,能非常方便的构建原生库或编译 .so/.a 文件。
这里还是看个栗子吧,为了直观,我先将整体的目录结构贴下:

这是我新创建的 CMakeLists.txt 文件,位置随意。其实当我们通过 AS 创建一个 Navive C++ 工程时,AS 会自动帮我们配置好 CMake 环境和生成 CMakeLists.txt 文件的:
cmake_minimum_required(VERSION 3.4.1)
# 将 native-lib.cpp 构建出 so共享库,并命名为 hello
add_library( # 构建的库的名字
hello
# 共享库
SHARED
# 库的原文件,这里与 CMakeLists.txt 同目录,直接就写 hello_lib.cpp 了
hello_lib.cpp )
# 通过 find_library 来找到需要关联的三方库
find_library( # Sets the name of the path variable.
log-lib
# 需要关联的 so 名字
log )
# 通过 link 可将源文件构建的库和三方库都加载进来
target_link_libraries( # 源文件库的名字
hello
# 三方库
${log-lib} )
复制代码
- 在 gradle 文件中配置,CMakeLists 文件位置要一致:
externalNativeBuild {
cmake {
path "src/main/java/com/blog/a/cpp/CMakeLists.txt"
version "3.10.2"
}
}
复制代码
- 创建 java 对应的加载类,在这里直接使用静态代码快加载 libhello.so,这个 so 就是我们 CMakeLists 文件中添加的,是的,不需要像上面那样导出 so 文件:
public class HelloCMakeJni {
static {
System.loadLibrary("hello");
}
public native String stringFromCmakeJNI();
}
复制代码
- 创建我们的 native-lib.cpp,并实现 stringFromCmakeJNI 的逻辑,注意这里的命名规范,就不展开说了:
#include <jni.h>
#include <string>
extern "C" JNIEXPORT jstring JNICALL
Java_com_blog_a_cpp_HelloCMakeJni_stringFromCmakeJNI(
JNIEnv* env,
jobject ) {
std::string hello = "Hello from C++ By cMaker";
return env->NewStringUTF(hello.c_str());
}
复制代码
- 最后就能在我们的 Activity 中调用了。并在 app/build/intermediates/cmake/debug/obj 目录下生成了 .so 文件,这里就不贴图了,直接可看 github BlogSample :
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.jni_show_layout)
findViewById<TextView>(R.id.sample_text).text = showJNIStr()
}
fun showJNIStr() = StringBuilder().apply {
append(HelloCMakeJni().stringFromCmakeJNI())
append("\n")
append(HelloJni().stringFromJNI())
}.toString()
复制代码
调用源 cpp 方法
在刚刚 CMake 的基础上,我想再让 native-lib.cpp 去调用其他的 cpp 方法,这很正常吧,一个 so 库一般不可能仅有一个方法吧,这里写个栗子吧:
为了直观,我先将整体的目录结构贴下:

- 编写头文件 method2.h,并定义方法 getHelloWorld:
#ifndef _GET_HELLO_WORLD_H_
#define _GET_HELLO_WORLD_H_
extern const char* getHelloWorld();
#endif
复制代码
- 编写 method2.cpp,实现 getHelloWorld 方法,这里仅仅返回一个 “HelloWorld” :
#include "method2.h"
extern const char* getHelloWorld() {
return "HelloWorld";
}
复制代码
- 我们在 native-lib.cpp 中来调用这个方法:
#include <jni.h>
#include <string>
#include "method2.h"
extern "C" JNIEXPORT jstring JNICALL
Java_com_blog_a_cpp_HelloCMakeJni_stringFromCmakeJNI(
JNIEnv* env,
jobject ) {
std::string hello = getHelloWorld();
return env->NewStringUTF(hello.c_str());
}
复制代码
- 最后还需要在 CMakeLists.txt add_library中添加 method2.cpp:
add_library( # Sets the name of the library.
hello
# Sets the library as a shared library.
SHARED
method2.cpp
hello_lib.cpp )
复制代码
关联第三方so库
在实际场景下,我们的 c++ 源库可能还会引用一些其他的三方库,这该怎么做呢?举个例子吧:
在我们上面 CMake 的基础上,再关联上我们 libs 目录下 ndk-build 出的 so文件,为了直观,我先将整体的目录结构贴下:

- 首先将 so 库的待提供方法所对应的 .h 文件,放在 include 目录下,目的是,在源库 cpp 内需要时调用:
# 这样我们就能在源库内使用 so .h 内提供的方法了
#include "xxx.h"
复制代码
- 配置 CMakeLists.txt 文件,引入我们的 libmyJniTest.so, 这里是重点:
# 将我们的 .so 关联到我们的 hello_lib.cpp
include_directories(include)
# 导入三方库
add_library(myJniTest
SHARED
IMPORTED)
# 设置关联的 so 库名称、目标位置
# ${CMAKE_SOURCE_DIR} 是 CMakeLists.txt 所在目录
set_target_properties(myJniTest
PROPERTIES IMPORTED_LOCATION
${CMAKE_SOURCE_DIR}/../libs/${ANDROID_ABI}/libmyJniTest.so )
# 通过 link 可将源文件构建的库和三方库都加载进来
target_link_libraries( # 源文件库的名字
hello
# 引用的三方库
myJniTest
# Links the target library to the log library
# included in the NDK.
# 三方库
${log-lib} )
复制代码
- 通过上面两步,我们的 hello 库内就引入了 libmyJniTest.so 文件了。所以档 HelloJni 注释掉 loadLibrary ,依然能正常使用,因为在加载 libhello.so 时,就算加载了 libmyJniTest.so.
public class HelloJni {
public native String stringFromJNI();
}
复制代码
好了,本节就先说到这里吧,下节我们接着说带参数的 java c++ 调用,关联 .a 库 和 .a 库怎么才能够生成 .so 库。demo 我已经上传 github 了,有需要的 clone 查看吧。
本文到这里就结束了。如果本文对你有用,来点个赞吧,大家的肯定也是 阿呆i 坚持写作的动力。