深入浅出 Android JNI (二)

5 阅读7分钟

1. 磨刀不误砍柴工:配置开发环境

1.1 安装必备组件

打开 Android Studio (建议最新稳定版) ,通过 SDK Manager 安装:

  • NDK (Native Development Kit) :核心工具链,编译 C/C++
  • CMake:主流跨平台构建系统,管理 Native 编译过程
  • LLDB:强大的 Native 代码调试器
    miro.medium.com/max/1400/1*…

1.2 项目配置 (以新项目为例)

  1. 创建项目:选择 Native C++ 模板 (Android Studio 已帮你做基础配置)

  2. 关键文件检查

    • app/build.gradle:确认 NDK 版本和 CMake 路径

    android {
        ...
        defaultConfig {
            ...
            externalNativeBuild {
                cmake {
                    cppFlags "" // 可添加编译选项如:-std=c++17
                    // 指定ABI (Application Binary Interface),初期可只选一个加快编译
                    // abiFilters 'arm64-v8a', 'armeabi-v7a', 'x86', 'x86_64'
                }
            }
        }
        externalNativeBuild {
            cmake {
                path "src/main/cpp/CMakeLists.txt" // CMake 构建脚本路径
                version "3.22.1" // 指定CMake版本 (与安装的一致)
            }
        }
    }
    
    • local.properties:确保指定了 NDK 路径

      sdk.dir=/path/to/your/android/sdk
      ndk.dir=/path/to/your/ndk/version // 例如 ndk.dir=/Users/name/Android/sdk/ndk/25.1.8937393
      

2. 庖丁解牛:Hello JNI 项目结构解析

一个最简单的 JNI 项目包含以下核心文件:

app/
├── src/
│   ├── main/
│   │   ├── java/
│   │   │   └── com/example/myapp/
│   │   │       └── MainActivity.java  // Java 入口,声明并调用 native 方法
│   │   ├── cpp/
│   │   │   ├── CMakeLists.txt        // CMake 构建脚本,告诉 CMake 如何编译你的 C/C++
│   │   │   └── native-lib.cpp        // 你的 C/C++ JNI 实现代码
│   │   └── res/                      // 资源文件
├── build.gradle                      // 项目构建配置
└── local.properties                  // SDK & NDK 路径配置

3. 实战:编写你的第一个 Hello JNI

3.1 Java 侧:声明与调用 Native 方法 (MainActivity.java)

package com.example.myapp;

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

public class MainActivity extends AppCompatActivity {

    static {
        // 1. 加载名为 "native-lib" 的共享库
        // 实际加载的文件名是 libnative-lib.so (Linux/Android 规则)
        System.loadLibrary("native-lib");
    }

    // 2. 声明 native 方法
    // 这个方法的具体实现在 C/C++ 层 (native-lib.cpp)
    public native String stringFromJNI();

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

        // 3. 调用 native 方法获取字符串
        String helloFromC = stringFromJNI();

        // 4. 将结果显示在 TextView 上
        TextView tv = findViewById(R.id.sample_text);
        tv.setText(helloFromC); // 显示 "Hello from C++!"
    }
}

关键点解析:

  • System.loadLibrary("native-lib"):

    • 静态初始化块中调用,确保类加载时库就被加载。
    • 参数 "native-lib" 对应最终生成的共享库文件名 libnative-lib.so (lib 前缀和 .so 后缀是系统自动添加的)。
  • public native String stringFromJNI();:

    • native 关键字表明这是一个本地方法,没有方法体。
    • 它的具体实现将由 C/C++ 代码提供。

3.2 C++ 侧:实现 Native 方法 (native-lib.cpp)

#include <jni.h>   // 必须包含!提供 JNI 类型和函数声明
#include <string>  // 使用 C++ 字符串

// 关键:符合 JNI 规范的函数命名
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_myapp_MainActivity_stringFromJNI(
        JNIEnv* env,        // JNI 环境指针,所有 JNI 函数的入口
        jobject /* this */)  // 调用该方法的 Java 对象实例 (本例是 MainActivity 实例)
{
    // 1. 创建一个 C++ 字符串
    std::string hello = "Hello from C++!";

    // 2. 将 C++ std::string 转换为 JNI 的 jstring
    // 使用 JNIEnv 提供的函数 NewStringUTF
    // 注意:NewStringUTF 要求 UTF-8 编码的字符数组
    return env->NewStringUTF(hello.c_str());
}

关键点解析:

  • 函数签名 (extern "C" JNIEXPORT jstring JNICALL):

    • extern "C":告诉 C++ 编译器使用 C 语言的命名和调用约定(防止 C++ 的名称修饰(mangling)导致 JVM 找不到函数)。
    • JNIEXPORT:宏定义,确保函数在动态库中可见(可被 JVM 加载)。
    • JNICALL:宏定义,指定函数调用约定(通常不需要开发者关心细节)。
    • jstring:返回值类型,对应 Java 的 String
  • 函数名 (Java_com_example_myapp_MainActivity_stringFromJNI):

    • 这是 JNI 最严格的规则!  必须严格按照此格式命名:
      Java_ + 包名(点替换为下划线) + _ + 类名 + _ + 方法名

    • 本例中:

      • 包名:com.example.myapp -> com_example_myapp
      • 类名:MainActivity
      • 方法名:stringFromJNI
    • 注意大小写!  必须与 Java 中的包名、类名、方法名完全一致(包括大小写)。一个常见的错误是 MainActivity 写成 mainactivity

  • 参数 (JNIEnv* env, jobject thiz):

    • JNIEnv* env最重要的参数!  指向 JNI 函数表的指针。所有与 Java 交互的操作(创建对象、调用方法、处理异常等)都通过 env->xxxFunction() 调用。
    • jobject thiz:指向调用此 native 方法的 Java 对象实例(本例是 MainActivity 的一个实例)。如果 stringFromJNI 是 static 方法,则此参数类型为 jclass,指向 MainActivity.class。代码中 /* this */ 注释是提醒该参数的作用。
  • 核心逻辑:

    1. 创建一个 C++ std::string (hello)。
    2. 使用 env->NewStringUTF() 函数将 C/C++ 中以 \0 结尾的 UTF-8 编码的字符数组 (hello.c_str() 返回的 const char*) 转换为 JNI 的 jstring 类型(对应 Java 的 String)。
    3. 返回这个 jstring 给 Java 调用者。

4. 粘合剂:CMakeLists.txt 详解

CMakeLists.txt 告诉 CMake 如何编译你的 C/C++ 代码并生成 .so 库。

# 1. 设置 CMake 的最低版本要求
cmake_minimum_required(VERSION 3.22.1)

# 2. 定义项目名称 (通常与库名无关)
project("myapp")

# 3. 创建并命名一个库 (SHARED 表示生成共享库 .so)
add_library(
        native-lib          # 库的名称:生成 libnative-lib.so
        SHARED              # 类型:SHARED(动态库), STATIC(静态库)
        native-lib.cpp      # 源文件列表 (多个文件用空格隔开)
)

# 4. 查找 Android 系统提供的 Log 库,并命名为 log-lib
find_library(
        log-lib             # 为找到的库设置一个变量名 (log-lib)
        log                 # Android 系统库 liblog.so 的名字
)

# 5. 将目标库 (native-lib) 与找到的系统库 (log-lib) 链接
target_link_libraries(
        native-lib         # 目标库 (我们自己的 native-lib)
        ${log-lib}         # 链接 liblog.so (用于 __android_log_print)
        # 可以链接其他库,如 android (图形), OpenSLES (音频) 等
)

关键点解析:

  • add_library(native-lib SHARED native-lib.cpp): 定义我们要构建一个名为 native-lib 的共享库(.so),源代码是 native-lib.cpp。库名 native-lib 必须与 Java 中 System.loadLibrary("native-lib") 指定的名字一致。
  • find_library(log-lib log): 在 NDK 的系统库路径中查找名为 log (对应 liblog.so) 的库,并将其路径存储在变量 log-lib 中。
  • target_link_libraries(native-lib ${log-lib}): 将我们自己的 native-lib 与系统提供的 log 库链接起来。这样我们才能在 native-lib.cpp 中使用 __android_log_print 等日志函数。

5. 运行与调试:见证奇迹的时刻

  1. 连接设备/启动模拟器:确保 ADB 识别到你的设备。

  2. 点击 Run (绿色三角) :Android Studio 会自动完成:

    • 编译 Java/Kotlin 代码 -> .dex
    • 调用 CMake 编译 C/C++ -> libnative-lib.so (针对不同 ABI)
    • 打包 APK
    • 安装 APK 到设备
    • 启动应用
  3. 查看结果:应用启动后,屏幕上应该显示  "Hello from C++!" 。恭喜!你成功实现了 Java 调用 C++ 代码!

6. 调试利器:Native 层日志输出

在 native-lib.cpp 中添加日志,方便调试:

#include <jni.h>
#include <string>
#include <android/log.h> // 引入 Android 日志头文件

// 定义日志标签 (TAG)
#define LOG_TAG "JNI_DEMO"
// 定义日志级别和宏 (简化__android_log_print调用)
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)

extern "C" JNIEXPORT jstring JNICALL
Java_com_example_myapp_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */) {

    std::string hello = "Hello from C++!";
    LOGD("Creating jstring from: %s", hello.c_str()); // 调试日志

    jstring result = env->NewStringUTF(hello.c_str());

    if (result == nullptr) {
        LOGE("Failed to create jstring!"); // 错误日志
        // 可以考虑抛出异常回 Java
    }
    return result;
}

关键点:

  1. 包含头文件#include <android/log.h>
  2. 定义 TAG 和宏:方便使用不同级别的日志 (LOGDLOGE 等)。
  3. 链接 log 库:确保 CMakeLists.txt 中已经通过 target_link_libraries(native-lib ${log-lib}) 链接了 liblog.so
  4. 查看日志:在 Android Studio 的 Logcat 窗口,选择对应设备和你的 App 进程,按 LOG_TAG (如 "JNI_DEMO") 或日志级别过滤。

7. 常见问题排查 (Hello JNI 版)

  1. java.lang.UnsatisfiedLinkError: No implementation found for...

    • 最常见原因:函数名拼写错误!  仔细检查 C++ 函数名是否严格匹配 Java_包名_类名_方法名 规则(包名点换下划线,大小写一致)。
    • 检查 System.loadLibrary("xxx") 中的 xxx 是否与 CMakeLists.txt 中 add_library(xxx ...) 的 xxx 一致 (不包括 lib 和 .so)。
    • 确保 .so 文件被打包进 APK 并安装到设备上。检查 build/outputs/apk/debug/ 下的 APK,用解压软件查看 lib/ 目录下是否有对应 ABI 的 libxxx.so
    • 检查 CMakeLists.txt 配置是否正确,源文件是否包含在 add_library 中。
  2. java.lang.UnsatisfiedLinkError: dlopen failed: library "libxxx.so" not found

    • 确保 System.loadLibrary 调用发生在使用 native 方法之前(通常放在 static 块)。
    • 检查设备 CPU 架构 (ABI)。build.gradle 中 abiFilters 是否包含了设备对应的 ABI (如 arm64-v8aarmeabi-v7a)?初期可以注释掉 abiFilters 让 Gradle 打包所有支持的 ABI。
    • 库依赖问题:如果 libxxx.so 依赖其他库,需要确保它们也存在且路径正确。使用 readelf -d libxxx.so (Linux) 或 objdump -p libxxx.so (Windows) 查看依赖。
  3. 编译错误 (C/C++)

    • 仔细阅读 Android Studio 的 Build Output 窗口的错误信息,通常定位很准确。
    • 常见错误:缺少 #include <jni.h>JNIEnv* 或 jstring 等类型未定义,函数签名不匹配等。
  4. 应用崩溃 (无明确错误)

    • 打开 Logcat,过滤日志级别为 Error (E) 或 Fatal (F)。
    • 查找 signal 信息 (如 SIGSEGV 段错误),通常意味着 Native 层有非法内存访问(空指针、野指针、数组越界)。
    • 使用 addr2line 或 ndk-stack 工具将崩溃地址映射回源代码行号。在 Android Studio 中,崩溃时 Logcat 通常会自动解析出堆栈信息