1. 项目概述:为什么要在UniApp插件里调用C++的so库?
最近在做一个UniApp项目,需要处理一些高强度的图像计算。用JavaScript写了个算法,在模拟器上跑得还行,一到真机上就卡成幻灯片。这让我不得不重新思考技术选型。UniApp的跨端能力确实强,但涉及到密集计算、硬件加速或者复用已有成熟C/C++库时,它的JavaScript运行环境就显得力不从心了。这时候,原生插件就成了打通性能瓶颈的桥梁。
而更进一步,当我们需要极致的性能、复用历史遗留的C++算法库,或者与底层硬件(如相机、传感器)深度交互时,仅仅调用Java/Kotlin原生代码可能还不够,我们需要直接与C/C++编译的共享库(so库)对话。这听起来有点“硬核”,像是在App里搞嵌入式开发,但实际场景比想象中更常见:比如集成OpenCV做图像识别、调用FFmpeg进行音视频编解码、使用加密算法库、或者运行一个用C++写的物理引擎。
这个项目的核心,就是解决如何在UniApp的Android原生插件中,安全、高效地调用由C++语言编译生成的so库。它不是一个简单的API调用,而是一套从UniApp的Vue页面,到Android的Java/Kotlin层,再到JNI(Java Native Interface)桥接,最终抵达C++核心逻辑的完整技术栈串联。理解这个流程,不仅能解决眼前的性能问题,更能让你对移动端混合开发有更深层的掌控力。
2. 整体架构与核心思路拆解
在动手写代码之前,我们必须把整个调用链的架构想清楚。这就像盖房子先画蓝图,方向错了,后面全是坑。
2.1 技术栈分层与职责
整个调用过程可以清晰地分为四层:
UniApp JavaScript层:这是我们的业务界面和交互入口。通过
uni.requireNativePluginAPI调用我们封装好的原生插件,并传递参数、接收回调。这一层开发者最熟悉,关注点在于业务逻辑和用户体验。Android原生插件层(Java/Kotlin):这是UniApp与Android系统交互的桥梁。我们在这里创建一个Module,实现特定的接口,供JS调用。它的核心职责是接收JS请求,通过JNI调用C++函数,并处理返回结果。这一层是本次开发的重点。
JNI(Java Native Interface)粘合层:这是Java和C++两种语言通信的“翻译官”。它定义了Java层声明的
native方法与其对应的C/C++函数之间的映射关系。我们会编写一个C++头文件(.h)和实现文件(.cpp),实现数据类型的转换(如jstring到std::string)和函数调用。C++核心逻辑层:这是我们最终要调用的、用C++编写的核心算法或功能库,通常以动态链接库(.so文件)的形式存在。它可能是我们自己编写的,也可能是第三方提供的(如OpenCV的libopencv_java4.so)。这一层完全用C++编写,追求极致的执行效率。
2.2 方案选型:为什么是JNI而不是其他?
可能有人会问,Android不是有NDK(Native Development Kit)吗?直接写C++不行吗?这里需要明确几个概念:
- NDK:是一套工具集,让我们能在Android应用中使用C和C++代码。JNI是NDK提供的、实现Java与C/C++交互的核心机制。可以说,我们要在Android插件中调用C++,JNI是必经之路。
- 直接调用so库:我们无法在Java中直接“打开”一个so库并调用其中的函数。必须通过JNI定义好接口,由Java的
System.loadLibrary加载so库,然后调用声明为native的Java方法,JNI机制会自动找到并执行so库中对应的C++函数。
选择JNI方案,主要基于以下几点考量:
- 标准化与兼容性:JNI是Java平台调用本地代码的标准方式,得到了Android系统的完整支持,兼容性最好。
- 安全性:通过JNI定义的接口是可控的,可以有效地在Java和C++之间进行数据校验和错误处理,避免直接内存操作带来的崩溃风险。
- 开发工具链成熟:Android Studio对NDK和JNI开发的支持已经非常完善,有代码提示、调试工具(LLDB)等,降低了开发难度。
注意:JNI开发的一个核心挑战是“类型转换”和“内存管理”。Java和C++有着完全不同的数据类型系统和内存管理模型(Java自动垃圾回收 vs. C++手动管理)。在JNI层,我们必须小心翼翼地处理
jstring,jintArray,jobject等JNI类型与C++的std::string,int*, 自定义结构体之间的转换,稍有不慎就会导致内存泄漏或程序崩溃。
3. 开发环境与工具链准备
工欲善其事,必先利其器。调用C++ so库的开发环境比纯UniApp或纯Android开发要复杂一些,需要配置好几个关键部件。
3.1 核心工具安装与验证
Android Studio:这是开发Android原生插件的基石。确保安装时勾选了Android SDK、NDK(Side by side)和CMake。NDK版本不宜过新或过旧,建议选择LTS长期支持版本(如r25c),稳定性更有保障。你可以在
File -> Settings -> Appearance & Behavior -> System Settings -> Android SDK -> SDK Tools中查看和安装。UniApp开发环境(HBuilderX):用于编写和调试UniApp前端代码。确保你的HBuilderX安装了最新的App开发基座和真机运行插件。
C++编译工具链:主要依赖NDK。NDK内部包含了Clang编译器和一系列针对不同CPU架构(armeabi-v7a, arm64-v8a, x86, x86_64)的交叉编译工具链。我们不需要单独配置,Android Studio的CMake或ndk-build会自动调用它们。
3.2 创建UniApp原生插件项目结构
UniApp的原生插件有固定的目录结构。我们通常在HBuilderX中先创建一个普通的UniApp项目,然后在其中创建原生插件模块。
- 在UniApp项目根目录下,创建
nativeplugins文件夹(如果不存在)。 - 在
nativeplugins下,创建你的插件文件夹,例如MyCppPlugin。 - 在
MyCppPlugin中,创建android文件夹。最终的插件Android工程就放在这里。 - 用Android Studio打开这个
android文件夹(注意:是打开这个文件夹,而不是导入项目)。然后将其转换为一个标准的Android Library模块。- 在Android Studio中,
File -> New -> New Module...,选择Android Library。 - 模块名称建议与插件名一致,如
my-cpp-plugin。 - 包名建议为
com.xxx.plugin.my_cpp_plugin。 - 语言选择Kotlin(推荐,代码更简洁)或 Java。
- 最低API级别:这需要与你so库支持的架构以及UniApp项目的最低要求对齐。如果so库使用了较新的CPU指令集,可能需要提高最低API级别。通常设为21(Android 5.0)是一个兼容性较好的起点。
- 在Android Studio中,
3.3 配置Gradle构建脚本
这是让项目支持编译C++代码的关键。我们需要修改模块下的build.gradle.kts(Kotlin DSL) 或build.gradle(Groovy) 文件。
// 在 android {} 块内添加或修改 android { compileSdk 34 // 使用与你SDK匹配的版本 defaultConfig { minSdk 21 targetSdk 34 versionCode 1 versionName "1.0" // 1. 指定CMake参数(可选,用于传递宏定义等给C++代码) externalNativeBuild { cmake { cppFlags "-std=c++17" // 指定C++标准,如C++17 arguments "-DANDROID_STL=c++_shared" // 指定C++运行时库,shared便于多个so共用 // abiFilters 也可以在这里设置,但更推荐在defaultConfig或productFlavors中设置 } } // 2. 指定需要打包的ABI架构(非常重要!必须与你的so库架构匹配) ndk { abiFilters.addAll(setOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")) // 通常只需要前两个:armeabi-v7a (32位ARM) 和 arm64-v8a (64位ARM) // 如果你的so库只提供了特定架构,就在这里指定,可以减小APK体积。 } } // 3. 链接CMakeLists.txt文件 externalNativeBuild { cmake { path = file("src/main/cpp/CMakeLists.txt") // CMake构建脚本的路径 version = "3.22.1" // 指定CMake版本,建议使用Android Studio推荐的版本 } } // 4. 配置打包选项,防止压缩so文件(某些情况下需要) packagingOptions { jniLibs { useLegacyPackaging = true // 如果遇到so库加载失败,可以尝试开启此项 } // 也可以排除不需要的so文件 // exclude 'lib/armeabi-v7a/libunused.so' } }同时,在dependencies块中,确保添加了UniApp原生插件所需的依赖(具体版本请查询官方文档或示例):
dependencies { // UniApp原生插件核心依赖 implementation 'com.alibaba:fastjson:1.2.83' implementation 'com.github.bumptech.glide:glide:4.16.0' // 可能还需要其他工具库 }4. 编写C++核心代码与JNI接口
这是整个流程中最具技术挑战性的一环。我们将从C++库的创建开始。
4.1 创建C++源文件与CMakeLists.txt
在Android Library模块的
src/main目录下,创建cpp文件夹。所有C++源代码和头文件都将放在这里。在
cpp目录下,创建我们的C++头文件和源文件。例如,我们创建一个简单的图像处理函数:native-lib.h(头文件)
#ifndef UNIAPP_CPP_PLUGIN_NATIVE_LIB_H #define UNIAPP_CPP_PLUGIN_NATIVE_LIB_H #include <jni.h> // 必须包含JNI头文件 #include <string> // 声明一个C++函数:将两个整数相加 extern "C" int addTwoNumbers(int a, int b); // 声明一个C++函数:处理字符串(示例:转换为大写) extern "C" std::string processString(const std::string& input); #endif //UNIAPP_CPP_PLUGIN_NATIVE_LIB_Hnative-lib.cpp(源文件)
#include "native-lib.h" #include <algorithm> // 用于std::transform #include <cctype> // 用于std::toupper int addTwoNumbers(int a, int b) { return a + b; } std::string processString(const std::string& input) { std::string result = input; std::transform(result.begin(), result.end(), result.begin(), [](unsigned char c) { return std::toupper(c); }); return result; }extern "C"是关键!它告诉C++编译器以C语言的方式编译和链接这些函数,防止C++的名称修饰(name mangling)导致JNI找不到对应的函数。在
cpp目录下,创建CMakeLists.txt文件。这个文件指导CMake如何构建我们的C++库。# 设置CMake的最低版本要求 cmake_minimum_required(VERSION 3.18.1) # 定义项目名称和使用的编程语言 project("mycppplugin") # 创建并命名一个库,设置为STATIC(静态库.a)或SHARED(动态库.so) # 我们这里创建SHARED动态库,最终会生成 libnative-lib.so add_library( native-lib # 库的名称,生成的文件会是 libnative-lib.so SHARED native-lib.cpp # 列出所有源文件 ) # 指定头文件搜索路径(如果需要包含其他头文件) # target_include_directories(native-lib PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include) # 链接其他库到你的库上(例如,链接log库用于Android日志输出) find_library( log-lib log ) target_link_libraries( native-lib ${log-lib} )
4.2 编写JNI粘合层代码
JNI层是Java和C++之间的“桥梁”。我们需要在C++侧实现Java中声明的native方法。
在Java/Kotlin中声明native方法: 首先,在Android插件模块的Java/Kotlin类中,声明需要由C++实现的方法。通常我们会创建一个专门的类来管理这些JNI调用。
package com.example.my_cpp_plugin class JniBridge { // 1. 加载动态库。库名“native-lib”对应CMake中定义的库名,系统会自动添加“lib”前缀和“.so”后缀。 init { System.loadLibrary("native-lib") } // 2. 声明外部原生方法。这些方法的实现将在C++的so库中。 external fun nativeAdd(a: Int, b: Int): Int external fun nativeProcessString(input: String): String // 可以添加一些Java/Kotlin层的包装方法,便于调用 fun addNumbers(a: Int, b: Int): Int { return nativeAdd(a, b) } fun toUpperCase(input: String): String { return nativeProcessString(input) } }生成JNI函数签名头文件: Java的
native方法需要与C++中一个特定签名的函数对应。我们可以使用javac和javah(旧)或javac -h(新)来生成头文件。更简单的方法是先编写C++侧的JNI函数,保持签名一致。JNI函数的签名格式是固定的:Java_包名_类名_方法名。- 包名:
com_example_my_cpp_plugin(点换成下划线) - 类名:
JniBridge - 方法名:
nativeAdd - 完整函数名:
Java_com_example_my_cpp_plugin_JniBridge_nativeAdd
- 包名:
在C++中实现JNI函数: 在
native-lib.cpp中,我们实现上述签名的函数。#include <jni.h> #include "native-lib.h" extern "C" JNIEXPORT jint JNICALL Java_com_example_my_cpp_plugin_JniBridge_nativeAdd(JNIEnv *env, jobject /* this */, jint a, jint b) { // 调用我们之前写好的纯C++函数 int result = addTwoNumbers(static_cast<int>(a), static_cast<int>(b)); return static_cast<jint>(result); } extern "C" JNIEXPORT jstring JNICALL Java_com_example_my_cpp_plugin_JniBridge_nativeProcessString(JNIEnv *env, jobject /* this */, jstring input) { // 1. 将jstring转换为C++的std::string const char *cstr = env->GetStringUTFChars(input, nullptr); if (cstr == nullptr) { return nullptr; // 内存不足异常已抛出 } std::string cppInput(cstr); env->ReleaseStringUTFChars(input, cstr); // 必须释放! // 2. 调用纯C++函数处理字符串 std::string cppResult = processString(cppInput); // 3. 将C++的std::string转换回jstring并返回给Java return env->NewStringUTF(cppResult.c_str()); }关键点解析:
JNIEnv* env:这是JNI环境指针,是所有JNI函数调用的入口。通过它,我们可以调用一系列函数来操作Java对象(创建、访问、修改)、转换数据类型、抛出异常等。每个线程都有自己独立的JNIEnv,不能跨线程使用。jobject this:如果native方法不是static的,这个参数就代表调用该方法的Java对象实例。如果是static方法,这个参数是jclass,代表Java类。JNIEXPORT和JNICALL:这是用于定义函数调用约定的宏,确保函数能被Java虚拟机正确识别和调用。- 内存管理:对于
GetStringUTFChars这类函数获取的资源,必须使用对应的ReleaseStringUTFChars来释放,否则会导致内存泄漏。这是JNI编程中最容易出错的地方之一。
5. 构建与集成:生成so库并打包
代码写好了,接下来需要把它编译成so库,并集成到我们的Android插件中。
5.1 编译生成so库
在Android Studio中,有几种方式可以触发C++代码的编译:
- 同步与构建:完成上述CMakeLists.txt和C++代码编写后,点击Android Studio工具栏的
Sync Project with Gradle Files按钮。同步成功后,直接点击Build -> Make Module ‘my-cpp-plugin’。 - 查看输出:构建成功后,你可以在模块的
build/intermediates/cmake/目录下找到为各个ABI架构生成的libnative-lib.so文件,路径类似于debug/obj/arm64-v8a/libnative-lib.so。
5.2 集成第三方预编译so库
更多时候,我们不是自己编写C++库,而是集成第三方提供的预编译so库(如OpenCV)。步骤有所不同:
放置so文件:在模块的
src/main目录下创建jniLibs文件夹(这是Android Studio默认寻找so库的目录)。在jniLibs内,再为每个ABI架构创建子文件夹,如arm64-v8a,armeabi-v7a。将第三方提供的对应架构的so文件放入相应文件夹。src/main/jniLibs/ ├── arm64-v8a │ └── libthird_party.so └── armeabi-v7a └── libthird_party.so配置CMakeLists.txt链接预编译库:如果需要在自己的JNI代码中调用第三方库的函数,则需要在CMakeLists.txt中声明并链接它。
# 添加预编译库的导入 add_library( third_party # 给这个库起一个在CMake中使用的名字 SHARED IMPORTED ) # 设置导入库的路径。${CMAKE_SOURCE_DIR} 指向CMakeLists.txt所在目录。 set_target_properties( third_party PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI}/libthird_party.so ) # 在链接你自己的库时,链接这个第三方库 target_link_libraries( native-lib ${log-lib} third_party ) # 链接第三方库包含头文件:将第三方库的C++头文件(.h)放到
cpp/include之类的目录下,并在CMakeLists.txt中用target_include_directories指定头文件路径,在你的JNI代码中#include它们。
5.3 在UniApp插件Module中调用JNI层
现在,我们需要在UniApp原生插件的入口Module中,实例化我们写的JniBridge,并对外提供JS可调用的方法。
创建UniApp Module: 在Android插件的源码目录(通常是
src/main/java/com/...)下,创建一个类实现UniModule或UniDestroyableModule接口。package com.example.my_cpp_plugin import io.dcloud.feature.uniapp.annotation.UniJSMethod import io.dcloud.feature.uniapp.bridge.UniJSCallback import io.dcloud.feature.uniapp.common.UniModule class MyCppPluginModule : UniModule() { // 初始化JNI桥接类 private val jniBridge = JniBridge() @UniJSMethod(uiThread = false) // uiThread = false 表示该方法在非UI线程执行,适合耗时操作 fun add(option: Map<String, Any>, callback: UniJSCallback) { val a = option["a"] as? Int ?: 0 val b = option["b"] as? Int ?: 0 val result = jniBridge.addNumbers(a, b) callback.invoke(UniJSCallbackResult(data = result)) } @UniJSMethod(uiThread = false) fun toUpper(option: Map<String, Any>, callback: UniJSCallback) { val input = option["input"] as? String ?: "" val result = jniBridge.toUpperCase(input) callback.invoke(UniJSCallbackResult(data = result)) } // 可以定义常量供JS端读取 override fun getExportedConstants(): Map<String, Any>? { return mapOf("version" to "1.0.0") } }注册Module: 在插件目录的
android文件夹下,需要创建一个assets目录(如果不存在),并在其中创建dcloud_uniplugins.json文件,用于向UniApp引擎注册你的原生模块。{ "nativePlugins": [ { "type": "module", "name": "MyCppPlugin", // JS中调用时的模块名 "class": "com.example.my_cpp_plugin.MyCppPluginModule" // 模块类的完整路径 } ] }
6. UniApp前端调用与联调
后端打通了,前端调用就相对简单了。
6.1 JS端调用插件
在UniApp的Vue页面或JS文件中,你可以这样调用我们刚刚封装好的插件:
<template> <view class="content"> <button @click="callNativeAdd">调用C++加法</button> <button @click="callNativeToUpper">调用C++字符串处理</button> <text>结果:{{ result }}</text> </view> </template> <script> export default { data() { return { result: '' } }, methods: { async callNativeAdd() { // 1. 引入原生插件 const myCppPlugin = uni.requireNativePlugin('MyCppPlugin') // 2. 调用插件方法 const res = await new Promise((resolve, reject) => { myCppPlugin.add({ a: 10, b: 20 }, (ret) => { // ret 是一个对象,通常包含 code, data, msg 等字段 if (ret.code === 0 || ret.code === undefined) { // 成功 resolve(ret.data) } else { reject(new Error(ret.msg || '调用失败')) } }) }) this.result = `加法结果:${res}` uni.showToast({ title: `结果:${res}`, icon: 'none' }) }, async callNativeToUpper() { const myCppPlugin = uni.requireNativePlugin('MyCppPlugin') const res = await new Promise((resolve, reject) => { myCppPlugin.toUpper({ input: 'hello uniapp and c++!' }, (ret) => { if (ret.code === 0 || ret.code === undefined) { resolve(ret.data) } else { reject(new Error(ret.msg)) } }) }) this.result = `大写结果:${res}` } } } </script>6.2 真机调试与问题定位
这是最考验耐心的一步。由于涉及三层调用(JS->Java->C++),任何一层出错,表现可能都是App闪退或无响应。
查看Android Logcat:这是最重要的调试工具。在Android Studio的
Logcat窗口,选择你的设备和应用进程,过滤DEBUG或ERROR级别日志。JNI层的崩溃通常会在这里产生带有signal,abort,SIGSEGV(段错误) 等关键词的日志,并附带堆栈信息,能精确指向C++代码出错的行数。使用JNI调试工具:
adb logcat | grep -E “(DEBUG|JNI)”:在终端过滤JNI相关日志。- 检查JNI函数签名:签名错误是导致
UnsatisfiedLinkError的常见原因。可以使用javap -s命令查看编译后的Java类中native方法的签名,与C++中的函数签名进行严格比对。cd /path/to/your/class/files javap -s com.example.my_cpp_plugin.JniBridge
在C++代码中添加日志:使用Android NDK提供的
<android/log.h>在C++代码中打印日志,这对于追踪C++逻辑流非常有用。#include <android/log.h> #define LOG_TAG "MyCppPlugin" #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__) #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) // 在函数中使用 JNIEXPORT jint JNICALL Java_..._nativeAdd(...) { LOGD("nativeAdd called with a=%d, b=%d", a, b); int result = a + b; LOGD("nativeAdd result=%d", result); return result; }在Logcat中过滤
MyCppPlugin标签即可看到这些日志。使用LLDB进行Native调试:对于复杂的C++逻辑问题,可以配置Android Studio进行Native代码调试。这需要在
Run/Debug Configuration中,选择你的应用,在Debugger标签页下选择Dual(Java + Native) 或Native。然后在C++代码中打上断点。这需要设备或模拟器支持,且配置过程稍复杂,但对于解决疑难杂症至关重要。
7. 常见问题、性能优化与进阶技巧
踩过无数坑之后,我总结了一些高频问题和优化建议。
7.1 常见问题排查速查表
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
App启动时崩溃,Logcat报java.lang.UnsatisfiedLinkError: dlopen failed: library “libxxx.so” not found | 1. so库未正确打包进APK。 2. so库放置路径不对。 3. so库依赖的其他so库缺失。 4. so库的ABI与设备不匹配。 | 1. 解压生成的APK,查看lib/目录下是否有对应ABI的so文件。2. 确认so库放在 src/main/jniLibs/或通过CMake正确编译到libs/目录。3. 使用 `readelf -d libxxx.so |
调用native方法时崩溃,报UnsatisfiedLinkError: No implementation found for... | 1. JNI函数签名不匹配(最常见)。 2. 库名加载错误( System.loadLibrary参数不对)。3. C++函数未用 extern “C”包裹,导致名称修饰。 | 1. 使用javap -s核对Java方法签名,与C++函数名(包括包名、类名)逐字符比对。2. 确认 loadLibrary的参数是去掉lib前缀和.so后缀的库名。3. 检查C++实现文件,确保JNI函数声明在 extern “C”块内。 |
调用后App无响应或闪退,Logcat有SIGSEGV(段错误) | 1. C++代码中存在空指针访问、数组越界、野指针。 2. JNI层内存泄漏(Get后未Release)。 3. 多线程访问JNIEnv不当。 | 1. 使用AddressSanitizer等内存检测工具编译调试版本。 2. 仔细检查所有 GetXXX调用是否有配对的ReleaseXXX。3. 确保每个线程通过 JNIEnv* env = ...->GetEnv()获取自己的JNIEnv,不要跨线程使用。 |
| 性能不如预期 | 1. JNI调用本身有开销(特别是频繁调用小函数)。 2. 数据在Java和C++间频繁拷贝(如大数组、字符串)。 | 1. 采用“批处理”策略,一次JNI调用处理大量数据,避免多次往返。 2. 对于大型数据,考虑使用 ByteBuffer(NIO Buffer) 或MemoryFile在Java堆外分配内存,C++端直接操作,避免拷贝。 |
| 第三方so库崩溃 | 1. 第三方so库与当前Android API级别或NDK版本不兼容。 2. 初始化第三方库的上下文(Context)未正确传递或设置。 | 1. 联系库的提供者,获取兼容的版本或编译指南。 2. 仔细阅读第三方库的文档,确保按照要求进行初始化和资源释放。通常需要在Java层提供一个初始化方法,调用第三方库的初始化函数。 |
7.2 性能优化与最佳实践
减少JNI调用次数:JNI调用是有成本的。设计接口时,应尽量将多个操作合并到一次JNI调用中。例如,不要在一个循环里每次迭代都调用JNI函数,而是将数据打包(如数组)一次性传入C++处理,再一次性返回结果。
谨慎处理字符串和数组:
GetStringUTFChars/GetStringChars和Get<PrimitiveType>ArrayElements可能会在内存中创建副本。对于大字符串或大数组,这很耗时。- 考虑使用
GetStringCritical和GetPrimitiveArrayCritical,它们尝试返回指向原始数据的指针,但在此期间必须不能进行任何可能导致垃圾回收的JNI调用或阻塞操作。 - 对于只读的大数据,使用
GetStringRegion和Get<PrimitiveType>ArrayRegion直接拷贝到预先分配好的C++缓冲区,有时比获取指针更可控。
管理好局部引用(Local Reference):JNI函数创建的Java对象(如
NewStringUTF,NewObject)是局部引用,在本地方法返回后会自动释放。但如果在一个本地方法内创建了大量局部引用(例如在长循环中),可能会超出JVM的局部引用表限制,导致FatalError。此时需要使用env->DeleteLocalRef(ref)手动删除不再需要的局部引用,或者使用env->EnsureLocalCapacity(capacity)确保有足够的容量。处理多线程:
- JNIEnv不能跨线程:每个线程必须通过
JavaVM*(可以在JNI_OnLoad中保存全局变量)来获取自己的JNIEnv:jint JNI_GetEnv(JavaVM* vm, void** env, jint version)。 - 全局引用(Global Reference):如果需要在多个线程或多个本地方法调用间共享一个Java对象,需要将其从局部引用提升为全局引用 (
env->NewGlobalRef) 或弱全局引用 (env->NewWeakGlobalRef)。使用完毕后必须用env->DeleteGlobalRef删除,否则会导致内存泄漏。
- JNIEnv不能跨线程:每个线程必须通过
异常处理:C++代码中可以通过JNI函数检测和抛出Java异常。
env->ExceptionCheck():检查是否有待处理的Java异常。env->ExceptionDescribe():将异常信息打印到logcat。env->ExceptionClear():清除当前线程的待处理异常。- 在调用可能抛出异常的JNI函数(如调用Java方法)后,或者在C++代码发生错误时,应检查异常并妥善处理(如清理资源并返回),避免异常传播导致虚拟机崩溃。
7.3 进阶:封装与复用
当你成功集成一个复杂的C++库(如OpenCV)后,可以考虑将其封装得更易用。
创建更友好的Java/Kotlin API:在JNI桥接类之上,再封装一个业务逻辑层。例如,对于OpenCV的图像处理,可以提供一个
ImageProcessor类,内部调用JNI方法,但对外暴露Bitmap processImage(Bitmap src)这样符合Android开发者习惯的API。使用AIDL或直接内存共享处理大量数据:对于实时视频流、大型图像等场景,在Java和C++间来回拷贝数据帧是无法接受的。可以探索以下方案:
- Android NDK Camera2 API:直接在Native层访问相机数据。
- OpenGL ES纹理共享:如果处理结果用于渲染,可以在Native层将处理结果直接输出到OpenGL纹理,供GLSurfaceView或TextureView使用。
- MemoryFile/SharedMemory:在Java层创建一块共享内存区域,C++端通过文件描述符映射同一块内存,实现零拷贝数据交换。这是较高级的用法,需要对Linux共享内存有了解。
将插件发布为独立组件:将调试好的Android原生插件模块(包含Java/Kotlin代码、JNI代码、CMake配置、so库)整体打包,方便在其他UniApp项目中复用。你需要提供清晰的文档,说明插件的配置方法、API列表以及so库的ABI要求。
整个流程走下来,你会发现,在UniApp中调用C++ so库,本质上是在搭建一座连接JavaScript灵活世界与C++高性能世界的稳固桥梁。这座桥的每个部件——环境配置、JNI签名、内存管理、异常处理——都必须精心设计和施工。虽然初期会感到繁琐,但一旦打通,你将获得混合开发中无可比拟的性能优势和生态整合能力,能够将那些经过千锤百炼的C++宝藏库,无缝融入到你的跨端应用之中。