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 项目配置 (以新项目为例)
-
创建项目:选择
Native C++
模板 (Android Studio 已帮你做基础配置) -
关键文件检查:
-
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 */
注释是提醒该参数的作用。
-
核心逻辑:
- 创建一个 C++
std::string
(hello
)。 - 使用
env->NewStringUTF()
函数将 C/C++ 中以\0
结尾的 UTF-8 编码的字符数组 (hello.c_str()
返回的const char*
) 转换为 JNI 的jstring
类型(对应 Java 的String
)。 - 返回这个
jstring
给 Java 调用者。
- 创建一个 C++
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. 运行与调试:见证奇迹的时刻
-
连接设备/启动模拟器:确保 ADB 识别到你的设备。
-
点击 Run (绿色三角) :Android Studio 会自动完成:
- 编译 Java/Kotlin 代码 ->
.dex
- 调用 CMake 编译 C/C++ ->
libnative-lib.so
(针对不同 ABI) - 打包 APK
- 安装 APK 到设备
- 启动应用
- 编译 Java/Kotlin 代码 ->
-
查看结果:应用启动后,屏幕上应该显示 "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;
}
关键点:
- 包含头文件:
#include <android/log.h>
- 定义 TAG 和宏:方便使用不同级别的日志 (
LOGD
,LOGE
等)。 - 链接
log
库:确保CMakeLists.txt
中已经通过target_link_libraries(native-lib ${log-lib})
链接了liblog.so
。 - 查看日志:在 Android Studio 的 Logcat 窗口,选择对应设备和你的 App 进程,按
LOG_TAG
(如 "JNI_DEMO") 或日志级别过滤。
7. 常见问题排查 (Hello JNI 版)
-
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
中。
- 最常见原因:函数名拼写错误! 仔细检查 C++ 函数名是否严格匹配
-
java.lang.UnsatisfiedLinkError: dlopen failed: library "libxxx.so" not found
- 确保
System.loadLibrary
调用发生在使用native
方法之前(通常放在static
块)。 - 检查设备 CPU 架构 (ABI)。
build.gradle
中abiFilters
是否包含了设备对应的 ABI (如arm64-v8a
,armeabi-v7a
)?初期可以注释掉abiFilters
让 Gradle 打包所有支持的 ABI。 - 库依赖问题:如果
libxxx.so
依赖其他库,需要确保它们也存在且路径正确。使用readelf -d libxxx.so
(Linux) 或objdump -p libxxx.so
(Windows) 查看依赖。
- 确保
-
编译错误 (C/C++)
- 仔细阅读 Android Studio 的 Build Output 窗口的错误信息,通常定位很准确。
- 常见错误:缺少
#include <jni.h>
,JNIEnv*
或jstring
等类型未定义,函数签名不匹配等。
-
应用崩溃 (无明确错误)
- 打开 Logcat,过滤日志级别为
Error
(E
) 或Fatal
(F
)。 - 查找
signal
信息 (如SIGSEGV
段错误),通常意味着 Native 层有非法内存访问(空指针、野指针、数组越界)。 - 使用
addr2line
或ndk-stack
工具将崩溃地址映射回源代码行号。在 Android Studio 中,崩溃时 Logcat 通常会自动解析出堆栈信息
- 打开 Logcat,过滤日志级别为