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 层来调用,怎么做呢?
/* com.blog.a.jni.HelloJni.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 :
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_blog_a_jni_HelloJni */
#ifndef _Included_com_blog_a_jni_HelloJni
#define _Included_com_blog_a_jni_HelloJni
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_blog_a_jni_HelloJni
* Method: stringFromJNI
* Signature: ()Ljava/lang/String;
*/
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 /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
复制代码
- 编写 Application.mk 文件,更多其他配置参考 developers:
# 用于此应用的 C++ 标准库。
# 默认情况下使用 system STL。其他选项包括 c++_shared、c++_static 和 none
APP_STL := c++_shared
# 要为项目中的所有 C++ 编译传递的标记。这些标记不会用于 C 代码
APP_CPPFLAGS := -frtti -fexceptions
# 架构,全部:APP_ABI := all
APP_ABI := armeabi-v7a arm64-v8a
# 声明构建此应用所面向的 Android API 级别,并对应于应用的 minSdkVersion
APP_PLATFORM := android-19
复制代码
- 编写 Android.mk 文件:
# 此变量表示源文件在开发树中的位置。
# 在上述命令中,构建系统提供的宏函数 my-dir 将返回当前目录(Android.mk 文件本身所在的目录)的路径。
LOCAL_PATH := $(call my-dir)
# 声明 CLEAR_VARS 变量,其值由构建系统提供
# CLEAR_VARS 变量指向一个特殊的 GNU Makefile,后者会为我们清除许多 LOCAL_XXX 变量
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 /* this */) {
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 /* this */) {
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 {
/* hello 库已经引用了 libmyJniTest.so,所以当 hello.so 加载后,myJniTest 自动就会被关联*/
// static { System.loadLibrary("myJniTest"); }
public native String stringFromJNI();
}
复制代码
好了,本节就先说到这里吧,下节我们接着说带参数的 java c++ 调用,关联 .a 库 和 .a 库怎么才能够生成 .so 库。demo 我已经上传 github 了,有需要的 clone 查看吧。
本文到这里就结束了。如果本文对你有用,来点个赞吧,大家的肯定也是 阿呆i 坚持写作的动力。