Android-NDK-008-使用AS进行NDK开发

239 阅读4分钟

使用 Android Studio(AS)进行 NDK(Native Development Kit)开发是 Android 应用开发中的一个常见需求,尤其是当需要用 C 或 C++ 编写性能要求较高的代码时。NDK 提供了一种在 Android 设备上运行本地代码的方式,可以直接操作硬件和执行更高效的计算。以下是使用 Android Studio 进行 NDK 开发的完整指南。

1. 准备工作

1.1 安装 Android NDK 和 CMake

首先,确保你已经安装了 Android NDK 和 CMake。你可以通过 Android Studio 安装这些工具:

  1. 打开 Android Studio。
  2. 点击 File > Settings > Appearance & Behavior > System Settings > Android SDK
  3. SDK Tools 标签下,勾选 NDK (Side by side)CMake,然后点击 OK 安装。

1.2 配置 Gradle 构建文件

确保在项目的 build.gradle 文件中启用了 NDK 支持。通常,Android Studio 会为你自动设置,但你可以手动检查或修改。

kotlin
复制代码
android {
    compileSdkVersion 34 // 或者你所使用的 SDK 版本

    defaultConfig {
        applicationId = "com.example.myapplication"
        minSdkVersion = 21
        targetSdkVersion = 34
        versionCode = 1
        versionName = "1.0"

        externalNativeBuild {
            cmake {
                path = "src/main/cpp/CMakeLists.txt"  // 指定 CMake 构建文件的路径
            }
        }
    }

    externalNativeBuild {
        cmake {
            version = "3.10.2"  // CMake 版本(根据需要调整)
        }
    }
}

2. 创建 Native C/C++ 代码

2.1 创建 C++ 源文件

  1. 在你的 Android 项目中,右键点击 app 文件夹,选择 New > Directory,然后创建一个 cpp 目录。
  2. cpp 目录下,创建一个 C++ 源文件,例如 native-lib.cpp

示例 native-lib.cpp

cpp
复制代码
#include <jni.h>
#include <string>

extern "C" JNIEXPORT jstring JNICALL
Java_com_example_myapplication_MainActivity_stringFromJNI(
    JNIEnv* env,
    jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}

2.2 创建 CMakeLists.txt 文件

src/main/cpp 目录下,创建一个 CMakeLists.txt 文件,用于配置 CMake 构建过程。Android Studio 使用 CMake 来管理 C++ 构建。

示例 CMakeLists.txt

cmake
复制代码
cmake_minimum_required(VERSION 3.10.2)

project("myapplication")

# 设置 C++ 标准
set(CMAKE_CXX_STANDARD 17)

# 查找系统的 Android NDK
find_library(log-lib log)

# 将 C++ 源文件加入到构建中
add_library(native-lib SHARED
            native-lib.cpp)

# 链接系统库
target_link_libraries(native-lib
                      ${log-lib})

3. 在 Kotlin/Java 中使用 JNI

为了在 Kotlin 或 Java 中调用 C/C++ 编写的本地方法,你需要通过 JNI(Java Native Interface)进行连接。

3.1 声明本地方法

MainActivity.kt 中,声明一个与 C++ 中的本地方法相对应的方法,并使用 external 关键字。

kotlin
复制代码
package com.example.myapplication

import android.os.Bundle
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {

    // 声明本地方法
    external fun stringFromJNI(): String

    // 加载本地库
    companion object {
        init {
            System.loadLibrary("native-lib")
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // 获取本地方法返回的字符串
        val textView: TextView = findViewById(R.id.sample_text)
        textView.text = stringFromJNI()
    }
}

4. 同步项目并构建

  1. 点击 Sync Now 按钮同步项目。
  2. 点击 BuildRun 按钮构建并运行项目。你可以在设备上看到由 C++ 代码生成的字符串 "Hello from C++" 显示在屏幕上。

5. 调试 Native 代码

5.1 设置调试器

如果你希望调试 C++ 代码,可以在 Android Studio 中设置调试器:

  1. native-lib.cpp 文件中设置断点。
  2. 使用 Android Studio 的 Debug 模式运行应用程序。
  3. 调试器会自动连接到设备,并在 C++ 代码的断点处停下。

5.2 配置 NDK 调试

你还需要在 build.gradle 中启用调试支持:

kotlin
复制代码
android {
    defaultConfig {
        externalNativeBuild {
            cmake {
                // 配置调试信息
                cppFlags "-g"
            }
        }
    }
}

6. 管理和调试多个 ABI 架构

你可以为不同的架构(如 armeabi-v7aarm64-v8ax86_64 等)配置 .so 文件。这对于支持多架构的应用非常有用。

配置支持的 ABI 架构:

kotlin
复制代码
android {
    defaultConfig {
        externalNativeBuild {
            cmake {
                abiFilters("armeabi-v7a", "arm64-v8a", "x86_64")
            }
        }
    }
}

Gradle 会根据设备架构生成对应的 .so 文件,并在应用运行时加载适合的版本。

7 引入第三方so库

7.1 将 so 库和头文件拷贝到对应的目录

/app/src/main/jniLibs/arm/libxxx.so

7.2 修改 CMakeLists.txt 文件

第三方so库 这里和之前在第二步中介绍的创建一个新的原生库类似, 区别在于最后一个参数,我们通过IMPORTANT标志告知CMake只希 望将库导入到项目中。

  • 目标库的路径 这里有几点需要说明: 1、CMAKE_SOURCE_DIR}表示的是CMakeLists.txt所在的路径,我们指定第三方so所在路径时,应当以这个常量为起点来说。 2、我们应当为每种ABI接口提供单独的软件包,那么,我们就可以在jinLibs下建立多个文件夹,每个文件夹对应一种ABI接口类型,之后再通过${ANDROID_ABI}来泛化这一层目录的结构,这样将有助于充分利用特定的CPU架构。 3、三方的库关联到原生库 这里和将NDK库关联到原生库的原理是一样的。 4、为了确保 CMake 可以在编译时定位 我们的 头文件,我们需要将 include_directories() 命令添加到 CMake 构建脚本中并指定头文件的路径
add_library( # 指定目标导入库. 
            imported-lib
            # 设置导入库的类型(静态或动态)    为    shared 
library.
            SHARED
            # 告知    CMake imported-lib 是导入的库 
            IMPORTED )
set_target_properties( # 指定目标导入库 
                      imported-lib
                      # 指定属性(本地导入的已有库) 
                      PROPERTIES IMPORTED_LOCATION
                      # 指定你要导入库的路径. 
                      # ${CMAKE_SOURCE_DIR} 
                      imported-
lib/src/${ANDROID_ABI}/libimported-lib.so )
#为了确保    CMake 可以在编译时定位到我们的    头文件,我们需要使用 
include_directories() 命令,并包含    头文件的路                   
include_directories( imported-lib/include/ )
#要将预构建库关联到我们的原生库,请将其添加到    CMake 构建脚本的 
target_link_libraries() 命令中
target_link_libraries(  # 指定了三个库,分别是native-lib、 
imported-lib和log-lib.
                       native-lib 
                       imported-lib
                       # log-lib是包含在    NDK 中的一个日志库 
                       ${log-lib} )

在代码中引入第三方库的头文件,调用函数

8. 常见问题和解决方案

8.1 JNI 错误

  • 找不到本地方法:确保本地方法在 C++ 代码中实现,并且 JNI 函数签名正确。你可以使用 javap -s 工具查看 Java 方法的 JNI 签名。

8.2 CMake 构建失败

  • 确保 CMake 配置文件正确,并且相关依赖库已正确链接。

8.3 Android 构建脚本错误

  • 检查 Gradle 配置中的 externalNativeBuildndk 配置,确保 CMake 和 NDK 版本正确。

10 资料文献

首推 Android NDK 官方文档(developer.android.google.cn/ndk/guides?… NDK 开发又觉得新建的 Hello World 项目过于简单时。建议把googlesamples - android-ndk(github.com/googlesampl…) 项目拉下来。里面有多个实例参考,比官方文档完整很多。

  • Q1:怎么指定 C++标准?

A:在 build_gradle 中,配置 cppFlags -std

externalNativeBuild { 
cmake {
   cppFlags "-frtti -fexceptions -std=c++14" 
   arguments '-DANDROID_STL=c++_shared'
} 
}
  • Q2:add_library 如何编译一个目录中所有源文件? A: 使用 aux_source_directory 方法将路径列表全部放到一个变量中。
# 查找所有源码    并拼接到路径列表
aux_source_directory(${CMAKE_HOME_DIRECTORY}/src/api 
SRC_LIST)
aux_source_directory(${CMAKE_HOME_DIRECTORY}/src/core 
CORE_SRC_LIST)
list(APPEND SRC_LIST ${CORE_SRC_LIST})
add_library(native-lib SHARED ${SRC_LIST})
  • Q3:怎么调试 CMakeLists.txt 中的代码? A:使用 message 方法
cmake_minimum_required(VERSION 3.4.1) 
message(STATUS "execute CMakeLists") 

然后运行后在 .externalNativeBuild/cmake/debug/{abi}/cmake_build_output.txt 中查 看 log。

  • Q4:什么时候 CMakeLists.txt 里面会执行?

测试了下,好像在 sync 的时候会执行。执行一次后会生成 makefile 的文件缓存之类的东西放在 externalNativeBuild 中。所以如果 CMakeLists.txt 中没有修改的话再次同步好像是不会重新执行的。(或者删除.externalNativeBuild 目录)。

真正编译的时候好像只是读取 .externalNativeBuild 目录中已经解析好的makefile 去编译。不会再去执行 CMakeLists.txt

  • gcc/clang编译器的编译命令

编译命令: gcc/clang -g -O2 -o log ffmpeg_log.c -I -L -l(第一竖线是大写的i,第三个竖线是小写的L) 示例clang -g -O2 -o log ffmpeg_log.c -I …/ffmpeg -L …/ffmpeg/libavutil -lavutil

解析: -g 输出文件中的调试信息 -O2 对输出文件做指令优化(默认是-O1是不对指令进行优化,-O2编译器会按照自己的理解优化指令,让指令运行的更快)-o 输出文件的名字 -o后面跟的.c文件就是要编译的文件的名字 -I 指定头文件的位置 -L 指定库文件的位置 -l 指定引用的库文件名字 示例命令是使用ffmpeg的日志系统。

总结

通过 Android Studio 使用 NDK 开发原生代码,可以显著提高应用的性能,特别是处理需要高效计算的任务时。你只需创建 C++ 代码并通过 JNI 与 Kotlin 或 Java 进行交互,然后使用 CMake 来构建本地库。借助 Android Studio 提供的调试功能,可以轻松地开发、调试和优化本地代码。