android调用C++通用方式

想象你的 Android App 是一座繁忙的城市。

Kotlin/Java 写的业务层,是城市的办公楼:流程清晰、办事方便,适合做 UI、网络、业务逻辑。

C/C++ 写的原生库,是城市边缘的一座重工业工坊:机器轰鸣、效率极高,擅长干"重活"------音视频编解码、图像处理、加密、数学运算、AI 推理、游戏物理。

但办公楼和工坊语言不通:

一个说"JVM 的普通话",一个说"CPU 的方言"。

**JNI(Java Native Interface)**就是那扇"传话窗口",而 NDK 是给你建工坊、修道路、装吊车的一整套工具。

所谓"安卓应用直接调用原生库",本质就是:

在 APK 中打包 .so(共享库),在运行时由系统加载它,然后用 JNI 在 Kotlin/Java 与 C/C++ 之间传递数据、调用函数。

下面我们按"修路---建窗---运货---验收"的顺序,搭一套从 0 到可上线的调用体系。

一、先把概念摆平:我们说的"原生库"一般是 .so

在 Android 上,最常见的原生库形式是 ELF 共享库:

libxxx.so:共享库文件

放在 APK 的特定目录:lib//libxxx.so

由系统动态链接器加载(dlopen 机制)

1.1 ABI:同一座工坊要给不同"口音"的工人准备不同工具

Android 设备 CPU 架构不同,需要不同 ABI 的 .so:

arm64-v8a(主流)

armeabi-v7a(一些老设备)

x86_64(模拟器常见)

x86(更老)

同一个库通常要产出多份:每个 ABI 一份 .so。

二、两条主路线:JNI(最常见) vs 直接 dlopen(高级但小众)

把"调用原生库"分成两种玩法:

2.1 路线 A:JNI(官方正统路线,99% 项目用它)

Kotlin/Java 声明 external 方法

C/C++ 实现 JNI 函数

Java 调用像普通函数一样调用 native 方法

优点:标准、稳定、工具链完善

缺点:需要写 JNI glue code("胶水层")

2.2 路线 B:dlopen/dlsym 动态加载(像自己拿钥匙开工坊门)

你可以在 native 层 dlopen("libxxx.so")

用 dlsym 找函数指针再调用

优点:可按需加载、插件化

缺点:更底层,容易踩 ABI/符号/安全策略坑;Java 层仍通常通过 JNI 进入 native

结论:如果你不是做插件化/热插拔,先选 JNI。

三、最常见工程做法:Kotlin/Java 调 C/C++(JNI)一步步走

3.1 先在 Java/Kotlin 写"敲门口令":external + System.loadLibrary

就像你告诉门卫:"我一会要进工坊找某个师傅干活。"

object NativeBridge {

init {

System.loadLibrary("mylib") // 对应 libmylib.so

}

external fun add(a: Int, b: Int): Int

}

AI写代码

kotlin

运行

调用:

val r = NativeBridge.add(1, 2)

AI写代码

kotlin

运行

注意:loadLibrary 里写的是逻辑名 mylib,文件名是 libmylib.so

3.2 再在 C/C++ 写"接线员":JNI 函数实现

你需要一个 native 文件,例如 native-lib.cpp:

#include <jni.h>

extern "C"

JNIEXPORT jint JNICALL

Java_com_example_app_NativeBridge_add(JNIEnv* env, jobject thiz, jint a, jint b) {

return a + b;

}

这里的方法名像一串"暗号":

Java_包名_类名_方法名

它要与 Kotlin/Java 的声明匹配。

3.3 用 CMake 把工坊搭起来:生成 .so

在 CMakeLists.txt 里:

cmake_minimum_required(VERSION 3.22.1)

project("myapp")

add_library(mylib SHARED native-lib.cpp)

find_library(log-lib log)

target_link_libraries(mylib ${log-lib})

AI写代码

cmake

Gradle(Kotlin DSL 举例)里启用 externalNativeBuild:

android {

defaultConfig {

ndk {

abiFilters += listOf("arm64-v8a", "armeabi-v7a")

}

externalNativeBuild {

cmake {

cppFlags += "-std=c++17"

}

}

}

externalNativeBuild {

cmake {

path = file("src/main/cpp/CMakeLists.txt")

}

}

}

AI写代码

kotlin

运行

这样构建时 Gradle 会帮你产出对应 ABI 的 libmylib.so 并打进 APK。

四、另一种常见方式:你已经有第三方 .so,我只想"直接用"

有时候你拿到的是别人给的 libfoo.so(可能还带头文件 .h),你只想在 App 中调用它。

这就像工坊已经建好了,你只需要:

把工坊搬进城市(把 .so 放进工程并打包)

在 JNI 胶水层里"转接电话"(调用库函数)

Java/Kotlin 通过胶水层调用

4.1 如何把现成 .so 放进 APK?

把文件放到:

app/src/main/jniLibs//libfoo.so

例如:

app/src/main/jniLibs/arm64-v8a/libfoo.so

app/src/main/jniLibs/armeabi-v7a/libfoo.so

Gradle 默认会把 jniLibs 打包进去。

4.2 CMake 如何链接已有 .so?

在 CMake 中把它当作"导入库":

add_library(foo SHARED IMPORTED)

set_target_properties(foo PROPERTIES

IMPORTED_LOCATION

CMAKESOURCEDIR/../jniLibs/{CMAKE_SOURCE_DIR}/../jniLibs/CMAKESOURCEDIR/../jniLibs/{ANDROID_ABI}/libfoo.so)

add_library(mylib SHARED native-lib.cpp)

target_link_libraries(mylib foo)

AI写代码

cmake

然后在 native-lib.cpp 里 #include "foo.h" 并调用 foo 的 API(前提是你有头文件和正确的函数声明)。

4.3 Java/Kotlin 依旧只需要 load 你的桥接库

通常你只 loadLibrary("mylib"),它会间接依赖 libfoo.so

但如果依赖关系复杂,可能需要显式 loadLibrary("foo") 再 load mylib(视链接方式而定)。

五、数据怎么过桥?------"运货"才是 JNI 的难点

调用 native 最难的不在"能不能调用",而在"怎么安全高效地传数据"。

把 Java/Kotlin 到 C++ 的数据传递想象成过海关:

你能带什么(类型支持)

你怎么申报(转换)

你带的货要不要打包(拷贝)

你离开时要不要把箱子带回去(释放引用)

5.1 基础类型:最省事

int ↔ jint

long ↔ jlong

float/double ↔ jfloat/jdouble

boolean ↔ jboolean

5.2 字符串:像"需要翻译的文件"

Java 字符串是 UTF-16,C++ 常用 UTF-8 char*。JNI 提供:

GetStringUTFChars(转 UTF-8,可能拷贝)

用完必须 ReleaseStringUTFChars

示意:

JNIEXPORT void JNICALL

Java_xxx_print(JNIEnv* env, jobject, jstring s) {

const char* c = env->GetStringUTFChars(s, nullptr);

// use c

env->ReleaseStringUTFChars(s, c);

}

AI写代码

cpp

运行

5.3 数组/ByteBuffer:性能关键

如果你要传图像、音频、模型输入等大块数据,尽量用:

byte\[\]:简单但可能拷贝

DirectByteBuffer:可零拷贝拿到指针(常用于高性能)

DirectByteBuffer 像"集装箱直达码头":减少搬运。

六、库加载与依赖:为什么有时会崩在 UnsatisfiedLinkError?

这类错误像"门没开、钥匙不对、工坊不在"。

常见原因清单:

ABI 不匹配:你只打了 arm64,设备是 armeabi-v7a(或反过来)

库名不对:loadLibrary("mylib") 但实际是 libMyLib.so 或没打包

依赖库缺失:libmylib.so 依赖 libfoo.so,但 foo 没放进 APK

符号找不到:链接时没把某个函数导出/名称被 C++ name mangling 改了

Android 版本限制:某些旧 NDK API 或使用了不允许的系统私有库

排查建议:

adb logcat 看具体缺哪个 so 或符号

用 readelf -d libmylib.so 看依赖项(NEEDED)

确认 APK 内 lib// 是否真的有对应文件

七、线程与回调:原生层能不能"反过来调用 Java"?能,但要按规矩办

有时你希望 native 在某个事件发生时回调 Java,比如:

音频解码完一帧

网络库收到了数据

计算完成通知 UI

可以做到,但要注意:

native 线程如果不是从 Java 进来的,需要 AttachCurrentThread 才能用 JNIEnv

回调要避免频繁跨 JNI(会有开销)

UI 操作要切回主线程(Android 的基本法)

把它理解为:工坊里的人要打电话回办公楼,必须先去总机登记分机号。

八、为什么有些项目更喜欢用 JNA/反射来"调用原生库"?在 Android 上通常不推荐

JNA(Java Native Access)在桌面 Java 生态较常见,但在 Android 上:

体积与性能成本更高

兼容性与安全策略更复杂

大部分 Android 项目仍以 JNI 为主

所以如果你目标是可控、可上线、可维护:JNI 是主路。

九、发布与安全:把工坊"合法运营"的注意事项

只带必要 ABI:减少包体(arm64-v8a 通常必带)

符号与崩溃定位:保留 unstripped so 或单独保留符号文件用于 native 崩溃解析

混淆与安全:Java 层 Proguard/R8 + native 层符号剥离(但要留调试符号映射)

第三方库许可证:很多 .so 涉及开源协议,需合规

隐私合规:原生库如果访问设备信息/权限,依旧受 Android 隐私政策约束

十、把整个流程一句话串起来(记忆版)

Kotlin/Java 用 System.loadLibrary 把 .so 载入内存 → 用 external 声明门口的"窗口" → C/C++ 用 JNI 实现这个窗口并在里面调用真正的原生库函数 → 通过 CMake/Gradle 把各 ABI 的 .so 打包进 APK → 运行时按 ABI 自动选对库加载执行。

相关推荐
AiNightVision1 小时前
NMC存算一体与AI ISP
人工智能·算法·车载系统·自动驾驶·无人机·视频·智能硬件
imaol11 小时前
网络通信-网络编程
linux·udp
白远山1 小时前
自助健身小程序源码:架构拆解、核心链路与本地部署实战
java·架构·uni-app·需求分析
国科安芯2 小时前
商业航天星载数据管理单元的存储容错与接口集成方案研究
嵌入式硬件·架构·ecc·商业航天·抗辐射
道尔柯南2 小时前
【Linux】线程概念与控制
linux
wuminyu2 小时前
Markword在紧凑对象头上的实现原理剖析
java·linux·c语言·jvm·c++
酸菜。3 小时前
DW_apb_wdt总结
linux
虚无的纽扣3 小时前
【Linux】一篇文章带你吃透进程控制:fork、wait、exit到底在做什么
linux·ubuntu
raindayinrain3 小时前
Linux 网络编程--套接字选项
linux·网络·套接字选项