0

0

如何使用c++和JNI (Java Native Interface) 进行Android NDK开发? (性能优化)

尼克

尼克

发布时间:2026-01-17 16:19:45

|

506人浏览过

|

来源于php中文网

原创

jnienv 必须每次调用时重新获取,因其是线程局部的;javavm 可全局缓存,但 jnienv* 跨线程复用会导致崩溃;正确做法是通过 getenv 或 attachcurrentthread 获取,并在 native 线程结束时 detach。

如何使用c++和jni (java native interface) 进行android ndk开发? (性能优化)

为什么 JNIEnv* 必须在每个 JNI 函数调用中重新获取,不能跨线程缓存?

Android 的 JNI 环境(JNIEnv*)是线程局部的,不是全局可复用的指针。很多开发者误以为在 Java_com_example_NativeLib_init 中保存一次 JNIEnv*,后续就能直接用——这会导致崩溃或随机内存错误,尤其在回调、异步线程或 onDraw 频繁调用场景下。

正确做法是:每次进入 JNI 函数时,通过 JavaVM->GetEnv()AttachCurrentThread() 获取当前线程有效的 JNIEnv*。如果线程未附加(如 native 线程刚创建),必须先 AttachCurrentThread;用完后,若该线程由 native 创建,建议显式 DetachCurrentThread(避免线程资源泄漏)。

  • JavaVM* 可安全全局缓存(在 JNI_OnLoad 中获取并存为静态变量)
  • 不要在 C++ 成员变量或 static 指针中长期持有 JNIEnv*
  • Android 12+ 对未 detach 的线程更敏感,可能触发 ANR 或 java.lang.OutOfMemoryError: pthread_create failed

如何避免频繁 FindClassGetMethodID 导致的性能瓶颈

FindClass 是 JNI 中开销最大的操作之一,它要遍历类路径、解析字节码、触发类加载——在循环或高频回调(如音频处理、传感器采样)中反复调用,会显著拖慢吞吐量,甚至引发 GC 压力。

解决方案是「缓存 class 引用和方法 ID」,但必须注意生命周期管理:

立即学习Java免费学习笔记(深入)”;

Q.AI视频生成工具
Q.AI视频生成工具

支持一分钟生成专业级短视频,多种生成方式,AI视频脚本,在线云编辑,画面自由替换,热门配音媲美真人音色,更多强大功能尽在QAI

下载
  • 使用 GetObjectClassFindClass + NewGlobalRef 缓存 jclass(否则类卸载后引用失效)
  • GetMethodID/GetFieldID 结果是纯数值,可直接缓存为 static const,无需 global ref
  • 缓存时机放在 JNI_OnLoad 或首次调用时(加锁保护),而非每次函数入口
  • 避免缓存 java.lang.String 等系统类——它们不会被卸载,FindClass 结果可直接复用,但加 NewGlobalRef 更稳妥
static jclass g_StringClass = nullptr;
static jmethodID g_StringInit = nullptr;
<p>JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM<em> vm, void</em> reserved) {
JNIEnv* env;
if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR;
}
jclass localClass = env->FindClass("java/lang/String");
if (localClass == nullptr) return JNI_ERR;
g_StringClass = reinterpret_cast<jclass>(env->NewGlobalRef(localClass));
g_StringInit = env->GetMethodID(g_StringClass, "<init>", "([BLjava/nio/charset/Charset;)V");
env->DeleteLocalRef(localClass);
return JNI_VERSION_1_6;
}</p>

如何用 GetDirectBufferAddress 替代 GetByteArrayElements 避免内存拷贝?

当 Java 层传入 ByteBuffer.allocateDirect() 时,底层是 native 内存;而 GetByteArrayElements 总是触发 JVM 内部拷贝(即使传的是 direct buffer),在图像处理、音视频编解码等大数据量场景下,每帧多一次 memcpy 就是几十 MB/s 的浪费。

关键判断逻辑:只对 direct buffer 使用 GetDirectBufferAddress,且必须配合 GetDirectBufferCapacity 校验长度。普通 heap byte array 仍需走 GetByteArrayRegionGetByteArrayElements(后者需记得 ReleaseByteArrayElements)。

  • IsDirect 判断是否为 direct buffer
  • GetDirectBufferAddress 返回 void*,不增加引用计数,无需释放
  • 切勿对非 direct buffer 调用该函数——返回 nullptr,解引用即 crash
  • Android 8.0+ 上,direct buffer 的地址可安全用于 DMA 或 GPU 映射(如 Vulkan VK_EXTERNAL_MEMORY_ANDROID_HARDWARE_BUFFER_BIT_ANDROID
JNIEXPORT void JNICALL Java_com_example_NativeLib_processDirectBuffer(JNIEnv* env, jobject thiz, jobject buffer) {
    if (!env->IsDirect(buffer)) {
        jclass ex = env->FindClass("java/lang/IllegalArgumentException");
        env->ThrowNew(ex, "buffer must be a direct ByteBuffer");
        return;
    }
    void* addr = env->GetDirectBufferAddress(buffer);
    jlong capacity = env->GetDirectBufferCapacity(buffer);
    if (addr == nullptr || capacity <= 0) return;
    // 直接操作 addr,无拷贝
    process_image_data(static_cast<uint8_t*>(addr), static_cast<size_t>(capacity));
}

NDK 中启用 -O3 -flto -march=armv8-a+simd 后为何部分设备闪退?

高级优化标志能提升浮点密集型计算(如 FFT、卷积)30%+ 性能,但会破坏 ABI 兼容性:例如 -march=armv8-a+simd 生成的指令在不支持 NEON 的旧设备(如部分 ARM Cortex-A7)上非法,直接 SIGILL;-flto 可能导致符号重排,与 Java 层 native 方法名映射失败(UnsatisfiedLinkError)。

安全做法是分档构建 + 运行时检测:

  • ABI 分离:为 arm64-v8a 启用 SIMD,armeabi-v7a 仅用 -O2 + -mfpu=neon
  • 运行时 CPU 特性检测:用 android_getCpuFeatures()(需 android/ndk-version.h)判断是否支持 ANDROID_CPU_ARM_FEATURE_NEON,再 dispatch 到不同实现
  • -flto 必须配套 full LTO 链接(CMake 中设 set(CMAKE_INTERPROCEDURAL_OPTIMIZATION ON)),否则函数内联不生效,还可能因弱符号问题导致崩溃
  • 禁用 -fstack-protector-strong 在某些低端芯片上引发栈对齐异常(尤其 inline asm 场景)

最易忽略的一点:所有 JNI 函数签名必须声明为 extern "C",否则 -flto 可能因 C++ name mangling 导致 Java 找不到 native 方法。

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
string转int
string转int

在编程中,我们经常会遇到需要将字符串(str)转换为整数(int)的情况。这可能是因为我们需要对字符串进行数值计算,或者需要将用户输入的字符串转换为整数进行处理。php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

1010

2023.08.02

c语言const用法
c语言const用法

const是关键字,可以用于声明常量、函数参数中的const修饰符、const修饰函数返回值、const修饰指针。详细介绍:1、声明常量,const关键字可用于声明常量,常量的值在程序运行期间不可修改,常量可以是基本数据类型,如整数、浮点数、字符等,也可是自定义的数据类型;2、函数参数中的const修饰符,const关键字可用于函数的参数中,表示该参数在函数内部不可修改等等。

562

2023.09.20

javascriptvoid(o)怎么解决
javascriptvoid(o)怎么解决

javascriptvoid(o)的解决办法:1、检查语法错误;2、确保正确的执行环境;3、检查其他代码的冲突;4、使用事件委托;5、使用其他绑定方式;6、检查外部资源等等。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

186

2023.11.23

java中void的含义
java中void的含义

本专题整合了Java中void的相关内容,阅读专题下面的文章了解更多详细内容。

134

2025.11.27

堆和栈的区别
堆和栈的区别

堆和栈的区别:1、内存分配方式不同;2、大小不同;3、数据访问方式不同;4、数据的生命周期。本专题为大家提供堆和栈的区别的相关的文章、下载、课程内容,供大家免费下载体验。

443

2023.07.18

堆和栈区别
堆和栈区别

堆(Heap)和栈(Stack)是计算机中两种常见的内存分配机制。它们在内存管理的方式、分配方式以及使用场景上有很大的区别。本文将详细介绍堆和栈的特点、区别以及各自的使用场景。php中文网给大家带来了相关的教程以及文章欢迎大家前来学习阅读。

605

2023.08.10

class在c语言中的意思
class在c语言中的意思

在C语言中,"class" 是一个关键字,用于定义一个类。想了解更多class的相关内容,可以阅读本专题下面的文章。

870

2024.01.03

python中class的含义
python中class的含义

本专题整合了python中class的相关内容,阅读专题下面的文章了解更多详细内容。

30

2025.12.06

C# ASP.NET Core微服务架构与API网关实践
C# ASP.NET Core微服务架构与API网关实践

本专题围绕 C# 在现代后端架构中的微服务实践展开,系统讲解基于 ASP.NET Core 构建可扩展服务体系的核心方法。内容涵盖服务拆分策略、RESTful API 设计、服务间通信、API 网关统一入口管理以及服务治理机制。通过真实项目案例,帮助开发者掌握构建高可用微服务系统的关键技术,提高系统的可扩展性与维护效率。

3

2026.03.11

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Kotlin 教程
Kotlin 教程

共23课时 | 4.3万人学习

C# 教程
C# 教程

共94课时 | 11.1万人学习

Java 教程
Java 教程

共578课时 | 80.6万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号