NDK 开发入门

989 阅读3分钟

NDK 简介

Android NDK(Native Developer Kit) 是将 C/C++ 代码嵌入到原生 Android 应用中,在需要平台移植,对性能有着很高的要求,重复使用现有库,或者提供其自己的库供别人使用。

需要掌握的知识

  1. 原生共享库 xxx.so, NDK 根据原生代码 c/c++ 编译成 .so 库
  2. 编译成静态库 xxx.a, 将静态库链接到其他库
  3. java 本地接口(JNI) , JNI 是 Java 和 C++ 组件用以互相通信的接口。
  4. 应用二进制接口 (ABI),根据 android 平台生成 cpu 可执行文件,不同的 ABI 对应不同的架构:NDK 为 32 位 ARM、AArch64、x86 及 x86-64 提供 ABI 支持。值得说明的是 mips 和 mips64 在 ndk r17 中已经移除,再使用独立交叉工具链的时候不能指定 --arch=mips/mips64。以及 armeabi 也在 ndk r17 中被移除。 另外从 ndk r19 已经内置交叉编译工具链,不需要使用独立工具链。

NDK 简单使用(CMake 构建)

  1. 笔者 Android Studio(AS) 版本为 3.4.2, 因此如果创建一个支持 c/c++ 项目,内置会使用 CMake 来进行配置。新建工程结构如下:

  1. 在 main 下,新建 cpp 文件夹,新建一个 include 目录用来存放头文件。在 cpp 目录下新建源文件 hello-jni.cpp 编写本地函数给 java 层调用。首先我们在 java 中编写函数调用本地 c/c++ 函数。
public class MainActivity extends AppCompatActivity {

    static {
        System.loadLibrary("hellojni");
    }

    public native String messageFromJNI();

    public native void sendMessageToJNI(String msg);

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        TextView textView = findViewById(R.id.tv_message);
        textView.setText(messageFromJNI());


        sendMessageToJNI("Hello Jni, I come form Java.");
    }
}

接下来就是编写 JNI 函数,在之前老的 jdk 版本可以通过 javah 来生成,但是在 jdk10 已经移除了 javah 工具,通过 javac -h 来生成 JNI 头文件,使用如下的命令。

javac -h ./ xxx.java

其实当我们熟悉了定义 JNI 头文件规则后,就不需要用工具生成了。你只要记住它的规则就很简单了,规则表示如下:

JNIEXPORT 返回值 JINCALL Java_报名全路径(用下划线_隔开)_方法名(
JNIEnv *env,jobject instance);

举个例子:比如我的报名为 com.hxj.hellojni, 有一个本地方法
void messageFromJNI(),那么它本地方法就为:

extern "C"
JNIEXPORT void JNICALL
Java_com_hxj_hellojni_messageFromJNI(JNIEnv *env, jobject instance);

得到了头文件定义,就是在 hello-jni.cpp 中实现

#include "include/hello_jni.h" // 引入头文件

jstring Java_com_hxj_hellondk_MainActivity_messageFromJNI(JNIEnv *env, jobject instance) {
    // 调用 NewStringUTF 创建一个 Java 层可以使用的字符串.
    return env->NewStringUTF("Hello Java. I come from JNI");
}

void Java_com_hxj_hellondk_MainActivity_sendMessageToJNI(JNIEnv *env, jobject instance, jstring msg) {
    // 需要将 msg 转为 char*, 因为 jstring 并非真正的字符串类型,不能直接使用。
    const char* chars = env->GetStringUTFChars(msg, NULL);
    // 获取字符串长度
    int length = env->GetStringLength(msg);

    // 遍历打印字符, 通过 *(chars + i)  指针的方式获取字符
    for (int i = 0; i < length; ++i) {
        LOGD("i: %c ", *(chars + i));
    }
    
    // 一定要记得释放
    env->ReleaseStringUTFChars(msg, chars);
}
  1. 通过 NDK 提供编译工具链编打包

在Android Studio 2.2 以后,使用 NDK 和 CMake 将 C 及 C++ 代码编译到原生库中。这里先来看 CMake 方式,ndk-build 方式稍后讲述。

首先我们需要创建 CMake 构建脚本, 命名为 CMakeLists.txt,值得注意的是,这个名字不能写错。

# 配置 cmake 支持的最低版本
cmake_minimum_required(VERSION 3.6.0)

# 导入 include 头文件到编译环境中
include_directories(include)

# 将 include 目录下的源文件保存到 SRC_DIR 变量中.
aux_source_directory(include SRC_DIR)

# 生成指定 abi 共享库,其命名为 libHelloJni.so
add_library(
        hellojni

        SHARED

        ${SRC_DIR}
        hello-jni.cpp
)

# 将 android 日志库 log 链接到 libhellojni.so 共享中.
target_link_libraries(
        hellojni
        log
)

  1. Gradle 中使用 CMake 变量

项目 app 下的 build.gradle 中加入下面的配置

android {
  defaultConfig {
       .... 省略....

        externalNativeBuild {
            cmake {
                // 只指定了 'armabi-v7a', 其他的还有 'arm64-v8a','x86_86', 'x86'
                // 对应cpu为 arm 架构和 x86 架构,
                // 值得注意的是,如果你的 ndk 版本为 17 及以上,则不再支持 mips, mips64
               abiFilters  'armeabi-v7a'
            }
        }
    }  
    
 // 指定 cmkae 查找编译脚本版本和路径
 externalNativeBuild {
        cmake {
            version '3.10.2'
            path "src/main/cpp/CMakeLists.txt"
        }
    }
}


CMake 做了什么? 我们可以从构建信息中得到答案。

// 调用 ndk 提供的交叉编译工具 clang++ 
/xxx/Android/sdk/ndk-bundle/toolchains/llvm/prebuilt/darwin-x86_64/bin/clang++
// 生成的目标平台, 这里是 cpu 为 arm 架构的 android 系统
--target=armv7-none-linux-androideabi21
// 交叉编译工具链, 这里不难看出,ndk 19 以后提供了 llvm 已经包含了直接使用的编译工具链
--gcc-toolchain=/xxx/sdk/ndk-bundle/toolchains/llvm/prebuilt/darwin-x86_64
// 指定指定平台的依赖库和头文件, 以及自定义库中需要的头文件,比如我们上面的 include 目录下。
--sysroot=/xxx/sdk/ndk-bundle/toolchains/llvm/prebuilt/darwin-x86_64/sysroot 
-Dhellojni_EXPORTS -I/Users/ciggomac/Desktop/HelloNDK/app/src/main/cpp/include -g -DANDROID -fdata-sections -ffunction-sections -funwind-tables -fstack-protector-strong -no-canonical-prefixes -mfpu=vfpv3-d16 -fno-addrsig -march=armv7-a -mthumb -Wa,--noexecstack -Wformat -Werror=format-security -stdlib=libc++  -O0 -fno-limit-debug-info  -fPIC -MD -MT 

当正确构建完成后,我们可以找到我们的 apk 文件,压缩后打开或者使用 AS 打开。

使用 nm 命令查看共享库中包含那些函数, 可以看到我们编写的两个函数。

nm -D xxx.so

ndk-build 构建使用

ndk-build 脚本可用于编译采用 NDK 基于 Make 的编译系统的项目。它有两个脚本文件 Android.mk 和 Application.mk 构成。

  1. 工程目录结构

  1. 前面 JNI 接口已经编写了,所以这里只说明 Android.mk 的配置
# 编译系统提供的宏函数 my-dir 将返回当前目录(Android.mk 文件本身所在的目录)的路径。
LOCAL_PATH := $(call my-dir)

# CLEAR_VARS 变量指向一个特殊的 GNU Makefile,后者会清除许多 LOCAL_XXX 变量,
# 例如 LOCAL_MODULE、LOCAL_SRC_FILES 和 LOCAL_STATIC_LIBRARIES。
include $(CLEAR_VARS)

# 编译的模块的名称
LOCAL_MODULE := hellojni

# log 日志库
LOCAL_LDLIBS := -llog -landroid -lEGL -lGLESv1_CM

# 将 include 添加编译时头文件搜索目录
LOCAL_C_INCLUDES := include

# 编译到模块中的 C 和/或 C++ 源文件列表。
LOCAL_SRC_FILES := hello_jni.cpp

# 编译系统生成扩展名为 .so 的库文件
include $(BUILD_SHARED_LIBRARY)

Application.mk

// 指定编译的平台,这个文件不是必须的,可以在 app 下 build.gradle 指定 abiFilters
// 代替
APP_ABI := all

  1. Gradle 中配置 ndkBuild path
android {
  defaultConfig {
       .... 省略....

        externalNativeBuild {
            ndkBuild {
                // 只指定了 'armabi-v7a', 其他的还有 'arm64-v8a','x86_86', 'x86'
                // 对应cpu为 arm 架构和 x86 架构,
                // 值得注意的是,如果你的 ndk 版本为 17 及以上,则不再支持 mips, mips64
               abiFilters  'armeabi-v7a'
            }
        }
    }  
}

代码比较简单,自行按着步骤来配置一遍。