Android NDK 新人友好入门教程

原文地址:https://www.techyourchance.com/android-ndk-tutorial/

Android NDK 教程

Android Native Development Kit (NDK) 是 Android SDK 内部的一套工具,它允许将 C/C++ 代码和库集成到 Android 应用程序中。术语“native”来源于与 Java/Kotlin 代码不同,后者编译成 JVM 字节码,而 C/C++ 代码直接编译成特定硬件架构的本地可执行代码。这使得编译后的产物更具性能,但可移植性较差。

Android NDK 的使用场景

当你将 NDK 代码集成到 Android 应用程序中时,它会增加复杂性,增加生成的 APK 的大小,导致构建时间变长,并使调试变得困难。因此,在大多数情况下,你不应该使用 NDK。

然而,NDK 在以下情况下非常有用:

  1. 你需要重用现有的 C/C++ 代码或库。
  2. 你希望为敏感功能实现更好的代码混淆,从而使其更难被逆向工程。
  3. 你需要非常高的性能,这是在 JVM 上无法实现的。

如果你恰好面临上述要求之一,那么请继续阅读。

设置 Android NDK

要使用 Android NDK,你首先需要使用 AndroidStudio 的 SDK Manager 安装 NDK 和 CMake 工具。CMake 是一个用于 C/C++ 代码的特殊构建工具。

然后,你需要在项目中添加一个新的 CMakeLists.txt 文件。这是 CMake 的配置文件(类似于 Gradle 的 build.gradle)。虽然你可以将此文件放在任何位置,但标准方法是将其放在相应 Gradle 模块的 src 目录中:

[图片上传失败...(image-915f91-1736003773367)]

目前,先将新添加的 CMakeLists.txt 文件留空。我们稍后会回来处理它。

最后,在相应模块的 build.gradle 文件中添加以下代码(如果你将其放在不同的目录中,请更改 CMakeLists.txt 的路径):

android {
    ...
    externalNativeBuild {
        cmake {
            path file('CMakeLists.txt')
        }
    }
}

此时,Gradle 已经知道你的项目使用了 NDK,并将在构建过程中执行 CMake。

将 C/C++ 源代码添加到 Android 项目中

你可以自由地将本地源代码放在项目目录的任何位置。标准方法是在相应模块的主源集中添加 cpp 目录,并将其作为应用程序本地部分的根目录。

为了本教程的目的,我们将使用 NDK 计算第 n 个斐波那契数。因此,让我们添加 fibonacci.cpp 文件:

int computeFibonacci(int n) {
    if (n <= 1) {
        return n;
    }
    return computeFibonacci(n - 1) + computeFibonacci(n - 2);
}

尽管像上面这样的简单函数不需要头文件,但为了学习体验,让我们添加 fibonacci.h 文件:

#ifndef FIBONACCI_H
#define FIBONACCI_H

int computeFibonacci(int n);

#endif // FIBONACCI_H

现在我们有了实现所需功能的 C++ 代码。

将 Android 代码与本地代码集成

下一步是在标准 Android 代码和新添加的本地代码之间建立“桥梁”。这是一个涉及通信通道两端特殊修改的两步过程。

首先,让我们添加 NdkBridge 类:

package com.techyourchance.android.ndk

import ...

class NdkBridge {

    suspend fun computeFibonacci(argument: Int): FibonacciResult {
        return withContext(Dispatchers.Default) {
            computeFibonacciNative(argument)
        }
    }

    private external fun computeFibonacciNative(argument: Int): FibonacciResult

    private companion object {
        init {
            System.loadLibrary("my-native-code")
        }
    }
}

这个类将成为“桥梁”在 Android 端的入口。这里需要注意几点:

  • 这个类的完整包名很重要,稍后会引用它。
  • 我使用协程来避免从 UI 线程调用本地函数。你可以使用任何你喜欢的多线程方法。
  • 特殊的 external 函数 computeFibonacciNative 是对应于 my-native-code 库中相应函数的占位符,当 NdkBridge 类初始化时加载它。
  • 尽管 computeFibonacciNative 将在本地代码中实现,但其返回值是 Android 端定义的数据结构。理论上,返回值可以只是 Int,但我决定为了学习目的使其更具趣味性。

JVM 与本地功能集成的机制称为 JNI – Java Native Interface。SdkManager 类中的占位符函数 computeFibonacciNative 表明这个类期望在 my-native-code 库中找到相应的 JNI 函数。为了满足这一要求,让我们添加 jni-facade.cpp 文件并在其中实现所需函数:

#include <jni.h>
#include <fibonacci.h>

extern "C"
JNIEXPORT jobject JNICALL
Java_com_techyourchance_android_ndk_NdkBridge_computeFibonacciNative(
        JNIEnv *env,
        jobject thiz,
        jint n
) {

    // 计算结果并在传递回 Java 之前将其转换为 jint
    jint result = static_cast<jint>(computeFibonacci(n));

    // 构造一个在 Java 代码中定义的 FibonacciResult 对象
    jclass resultClass = env->FindClass("com/techyourchance/android/ndk/FibonacciResult");
    jmethodID constructor = env->GetMethodID(resultClass, "<init>", "(II)V");
    jobject resultObj = env->NewObject(resultClass, constructor, n, result);

    return resultObj;
}

这个类是“桥梁”在 native 端的入口。简要解释一下上述代码:

  • 第一个包含语句是使用 JNI 功能所必需的。
  • 第二个包含语句将 computeFibonacci 本地函数带入作用域。
  • extern "C" 是 C++ 的一个特殊指令,用于禁用所谓的“名称修饰”。基本上,它告诉编译器在编译产物中保留此函数的名称。
  • JNIEXPORT 是一个指令,使此函数对 Android 端可见。
  • jobject 是函数的返回类型(在此情况下为 Java 对象)。
  • JNICALL 表示此函数将通过 JNI 调用。
  • Java_com_techyourchance_android_ndk_NdkBridge_computeFibonacciNative 是 NdkBridge 类中相应占位符函数的完全限定名(包、类、函数名)。此名称允许系统将占位符函数映射到此 JNI 实现。
  • JNI 函数的第一个参数始终是 JNIEnv 指针。此对象可以用于访问各种 JNI 功能。
    第二个参数始终是调用者 Java 对象的引用(在此情况下为 NdkBridge 的实例)。
    第三个及后续函数参数对应于从 Java 代码传递的参数(在此情况下,只有一个 jint)。
  • C++ int 类型和 Java int 类型不是同一种类型。JNI 将 Java int 类型指定为 jint。由于 computeFibonacci 函数返回 C++ int,我们使用 static_cast<jint> 调用将其转换为 jint。
  • 由于此 JNI 函数的返回类型是在 Android 代码中定义的 Java 对象,JNIEnv 用于查找该类,识别其构造函数并创建一个新实例。这种方法与 Java 运行时反射非常相似,但由本地端调用。

我知道这些一开始可能会有点令人困惑。正如我们之前讨论的,使用 NDK 增加了代码库的复杂性,所以现在你就能明白我之前的意思了。

配置 CMake 以构建本地库

现在我们已经在通信桥梁的两端有了所有必要的源代码,但如果你尝试使用 NdkBridge,你的应用程序将会崩溃。错误信息会说找不到名为 my-native-code 的库。这就是 NdkBridge 初始化时加载的库。那么,问题出在哪里呢?

答案很简单:还记得我们添加但留空的 CMakeLists.txt 文件吗?此文件应该包含构建所需本地库的指令。让我们添加它们:

# 我们需要在这里设置一些最低版本
cmake_minimum_required(VERSION 3.18.1)

# 声明一个新的本地库
add_library(
        # 设置库的名称
        my-native-code
        # 将库指定为共享库 (.so)
        SHARED
)

# 这个辅助变量指向本地源代码的位置
set(NATIVE_SRC_DIR src/main/cpp)

# 将特定的源文件添加到库中
target_sources(
        my-native-code
        PRIVATE
        ${NATIVE_SRC_DIR}/jni-facade.cpp
        ${NATIVE_SRC_DIR}/fibonacci/fibonacci

我认为这里的注释应该不言自明。

请注意,新本地库的名称应与 NdkBridge 加载的名称相同(即如果你更改了它,请在两个地方都更改)。

上述 CMakeLists.txt 配置对应于以下本地源代码的结构:

structure

此时,你应该能够编译你的应用程序并使用 NdkBridge 在本地代码中计算第 n 个斐波那契数。

结论

本教程应该能让你快速开始使用 Android NDK。然而,我必须警告你,它只涵盖了这一复杂功能的非常基础的部分。因此,如果你需要在 Android 应用程序中使用 NDK,我建议花时间更深入地理解其工作原理。

项目源码已上传至 Github:https://github.com/LittleFogCat/MyNDKStudy

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

友情链接更多精彩内容