JNI 性能优化
源:JNI Tips - Android Developers
JNI 调用存在固有开销,但通过正确的优化技巧,可以将性能提升数倍甚至数十倍。本文深入讲解 JNI 性能优化的方方面面。
JNI 性能概览
调用开销
每次 JNI 调用的开销主要来自:
| 开销来源 | 耗时 | 说明 |
|---|---|---|
| 方法查找 | ~10ns | 首次调用需查找方法ID |
| 参数编组 (Marshalling) | 可变 | Java→C 类型转换 |
| 栈帧切换 | ~5ns | JVM→Native 切换 |
| 返回值转换 | 可变 | C→Java 类型转换 |
TIP
单次 JNI 调用的总开销约为 50-100ns(空调用),实际开销取决于参数复杂度。
性能指导原则
- 减少JNI 边界跨越 - 批量操作优于多次调用
- 避免数据拷贝 - 使用 DirectByteBuffer
- 缓存频繁使用的引用 - 类、方法、字段 ID
- 选择合适的数组访问方法 - 根据场景权衡
减少 JNI 调用次数
批量操作
kotlin
@OptIn(ExperimentalForeignApi::class)
@CName("Java_com_example_Slow_processArray")
fun slowProcessArray(
env: CPointer<JNIEnvVar>,
thiz: jobject,
array: jintArray
) {
val jniEnv = env.pointed.pointed!!
val length = jniEnv.GetArrayLength!!(env, array)
// ❌ 每个元素都调用一次 JNI
for (i in 0 until length) {
memScoped {
val element = allocArray<jintVar>(1)
jniEnv.GetIntArrayRegion!!(env, array, i, 1, element)
// 处理单个元素
val processed = element[0] * 2
jniEnv.SetIntArrayRegion!!(env, array, i, 1, cValuesOf(processed).ptr)
}
}
}
// 性能:1000元素 ≈ 1mskotlin
@OptIn(ExperimentalForeignApi::class)
@CName("Java_com_example_Fast_processArray")
fun fastProcessArray(
env: CPointer<JNIEnvVar>,
thiz: jobject,
array: jintArray
) {
val jniEnv = env.pointed.pointed!!
// ✅ 一次获取整个数组
val elements = jniEnv.GetIntArrayElements!!(env, array, null)
val length = jniEnv.GetArrayLength!!(env, array)
// 批量处理
for (i in 0 until length) {
elements!![i] *= 2
}
// 一次性写回
jniEnv.ReleaseIntArrayElements!!(env, array, elements, 0)
}
// 性能:1000元素 ≈ 0.05ms(20倍提升)合并多个操作
kotlin
@OptIn(ExperimentalForeignApi::class)
// ❌ 多次调用
fun slowApproach(image: Bitmap) {
applyGrayscale(image) // JNI 调用1
applyBlur(image, 5) // JNI 调用2
adjustBrightness(image, 1.2f) // JNI 调用3
}
// ✅ 单次调用
@CName("Java_com_example_ImageProcessor_applyFilters")
fun applyFilters(
env: CPointer<JNIEnvVar>,
thiz: jobject,
pixels: jintArray,
width: jint,
height: jint,
blurRadius: jint,
brightness: jfloat
) {
// 一次性应用所有滤镜
// 灰度 → 模糊 → 亮度调整
}DirectByteBuffer 零拷贝
使用 DirectByteBuffer
Direct存Buffer 直接操作堆外内存,避免数据拷贝:
kotlin
@OptIn(ExperimentalForeignApi::class)
@CName("Java_com_example_Direct_processDirect")
fun processDirect(
env: CPointer<JNIEnvVar>,
thiz: jobject,
buffer: jobject // DirectByteBuffer
): jlong {
val jniEnv = env.pointed.pointed!!
// 获取 DirectByteBuffer 的 native 地址
val addr = jniEnv.GetDirectBufferAddress!!(env, buffer)
val capacity = jniEnv.GetDirectBufferCapacity!!(env, buffer)
if (addr != null && capacity > 0) {
val bytePtr = addr.reinterpret<ByteVar>()
// ✅ 零拷贝:直接操作内存
var checksum: Long = 0
for (i in 0 until capacity) {
checksum += bytePtr[i.toInt()].toUByte().toLong()
}
return checksum
}
return -1
}性能对比
| 方法 | 10MB 数据处理时间 | 说明 |
|---|---|---|
ByteArray + GetByteArrayElements | 8ms | 需要拷贝 |
DirectByteBuffer | 0.5ms | 零拷贝 |
创建 DirectByteBuffer
kotlin
val buffer = ByteBuffer.allocateDirect(1024 * 1024) // 1MB
nativeProcess(buffer)kotlin
@OptIn(ExperimentalForeignApi::class)
@CName("Java_com_example_Factory_createBuffer")
fun createBuffer(
env: CPointer<JNIEnvVar>,
thiz: jobject,
size: jlong
): jobject? {
val jniEnv = env.pointed.pointed!!
// 分配 native 内存
val memory = nativeHeap.allocArray<ByteVar>(size.toInt())
// 创建 DirectByteBuffer
return jniEnv.NewDirectByteBuffer!!(env, memory, size)
// 注意:内存生命周期需要手动管理
}缓存策略
缓存类和方法 ID
频繁查找类和方法会严重影响性能:
kotlin
@OptIn(ExperimentalForeignApi::class)
object JNICache {
// 缓存的类引用
var stringClass: jclass? = null
var integerClass: jclass? = null
var arrayListClass: jclass? = null
// 缓存的方法 ID
var valueOfMethod: jmethodID? = null
var addMethod: jmethodID? = null
var toStringMethod: jmethodID? = null
fun initialize(env: CPointer<JNIEnvVar>) {
val jniEnv = env.pointed.pointed!!
// 查找并缓存 String 类
val localStringClass = jniEnv.FindClass!!(env, "java/lang/String")
stringClass = jniEnv.NewGlobalRef!!(env, localStringClass) as jclass
jniEnv.DeleteLocalRef!!(env, localStringClass)
// 缓存 valueOf 方法
val localIntClass = jniEnv.FindClass!!(env, "java/lang/Integer")
integerClass = jniEnv.NewGlobalRef!!(env, localIntClass) as jclass
valueOfMethod = jniEnv.GetStaticMethodID!!(
env, integerClass, "valueOf", "(I)Ljava/lang/Integer;"
)
jniEnv.DeleteLocalRef!!(env, localIntClass)
// 缓存 ArrayList.add
val localListClass = jniEnv.FindClass!!(env, "java/util/ArrayList")
arrayListClass = jniEnv.NewGlobalRef!!(env, localListClass) as jclass
addMethod = jniEnv.GetMethodID!!(
env, arrayListClass, "add", "(Ljava/lang/Object;)Z"
)
jniEnv.DeleteLocalRef!!(env, localListClass)
}
fun cleanup(env: CPointer<JNIEnvVar>) {
val jniEnv = env.pointed.pointed!!
stringClass?.let { jniEnv.DeleteGlobalRef!!(env, it) }
integerClass?.let { jniEnv.DeleteGlobalRef!!(env, it) }
arrayListClass?.let { jniEnv.DeleteGlobalRef!!(env, it) }
}
}
// 使用缓存
@CName("Java_com_example_Example_useCached")
fun useCached(env: CPointer<JNIEnvVar>, thiz: jobject, value: jint) {
val jniEnv = env.pointed.pointed!!
// ✅ 直接使用缓存的 ID,无需每次查找
val integer = jniEnv.CallStaticObjectMethod!!(
env,
JNICache.integerClass,
JNICache.valueOfMethod,
value
)
}性能影响
| 操作 | 未缓存 | 已缓存 | 提升 |
|---|---|---|---|
| FindClass | ~500ns | - | - |
| GetMethodID | ~300ns | - | - |
| 使用缓存的 ID | - | ~50ns | 16x |
数组访问优化
三种数组访问方法对比
kotlin
@OptIn(ExperimentalForeignApi::class)
// 方法1:GetIntArrayElements(推荐用于大数组)
fun method1_GetElements(env: CPointer<JNIEnvVar>, array: jintArray) {
val jniEnv = env.pointed.pointed!!
val isCopy = nativeHeap.alloc<jbooleanVar>()
val elements = jniEnv.GetIntArrayElements!!(env, array, isCopy.ptr)
val length = jniEnv.GetArrayLength!!(env, array)
// 处理(可能是副本,也可能是直接访问)
for (i in 0 until length) {
elements!![i] *= 2
}
jniEnv.ReleaseIntArrayElements!!(env, array, elements, 0)
nativeHeap.free(isCopy)
}
// 方法2:GetIntArrayRegion(推荐用于小数组)
fun method2_GetRegion(env: CPointer<JNIEnvVar>, array: jintArray) {
val jniEnv = env.pointed.pointed!!
val length = jniEnv.GetArrayLength!!(env, array)
memScoped {
val buffer = allocArray<jintVar>(length)
// 拷贝到 native 缓冲区
jniEnv.GetIntArrayRegion!!(env, array, 0, length, buffer)
// 处理
for (i in 0 until length) {
buffer[i] *= 2
}
// 拷贝回去
jniEnv.SetIntArrayRegion!!(env, array, 0, length, buffer)
}
}
// 方法3:GetPrimitiveArrayCritical(最快,但有限制)
fun method3_GetCritical(env: CPointer<JNIEnvVar>, array: jintArray) {
val jniEnv = env.pointed.pointed!!
val length = jniEnv.GetArrayLength!!(env, array)
val isCopy = nativeHeap.alloc<jbooleanVar>()
// Critical 区域开始
val elements = jniEnv.GetPrimitiveArrayCritical!!(env, array, isCopy.ptr)
?.reinterpret<IntVar>()
// ⚠️ 在 Critical 区域内不能调用任何其他 JNI 函数
// ⚠️ 也不能长时间阻塞
for (i in 0 until length) {
elements!![i] *= 2
}
// Critical 区域结束
jniEnv.ReleasePrimitiveArrayCritical!!(env, array, elements, 0)
nativeHeap.free(isCopy)
}性能对比(1M 元素数组)
| 方法 | 耗时 | 优点 | 缺点 |
|---|---|---|---|
GetIntArrayElements | 2.5ms | 灵活,可调用JNI | 可能拷贝 |
GetIntArrayRegion | 3.0ms | 明确拷贝,安全 | 始终拷贝 |
GetPrimitiveArrayCritical | 0.8ms | 最快,可能零拷贝 | 严格限制 |
选择建议
kotlin
@OptIn(ExperimentalForeignApi::class)
fun chooseMethod(array: jintArray, needJNICalls: Boolean, arraySize: Int) {
when {
// 小数组(<1000元素)- 使用 Region
arraySize < 1000 -> method2_GetRegion(env, array)
// 需要调用其他 JNI 函数 - 使用 Elements
needJNICalls -> method1_GetElements(env, array)
// 大数组 + 简单处理 - 使用 Critical
else -> method3_GetCritical(env, array)
}
}字符串优化
字符串传递策略
kotlin
@OptIn(ExperimentalForeignApi::class)
// ❌ 频繁创建/销毁字符串
@CName("Java_com_example_Slow_processStrings")
fun slowProcessStrings(env: CPointer<JNIEnvVar>, thiz: jobject, count: jint): jobjectArray? {
val jniEnv = env.pointed.pointed!!
val stringClass = jniEnv.FindClass!!(env, "java/lang/String")
val result = jniEnv.NewObjectArray!!(env, count, stringClass, null)
for (i in 0 until count) {
// 每次都创建新字符串
val str = "Item $i".toJString(env)
jniEnv.SetObjectArrayElement!!(env, result, i, str)
jniEnv.DeleteLocalRef!!(env, str) // 必须删除局部引用
}
return result
}
// ✅ 使用 StringBuilder 模式
@CName("Java_com_example_Fast_processStrings")
fun fastProcessStrings(env: CPointer<JNIEnvVar>, thiz: jobject, count: jint): jobjectArray? {
val jniEnv = env.pointed.pointed!!
// 在 native 侧构建所有字符串
val strings = (0 until count).map { "Item $it" }
// 一次性创建数组和字符串
val stringClass = jniEnv.FindClass!!(env, "java/lang/String")
val result = jniEnv.NewObjectArray!!(env, count, stringClass, null)
strings.forEachIndexed { i, str ->
val jstr = str.toJString(env)
jniEnv.SetObjectArrayElement!!(env, result, i, jstr)
jniEnv.DeleteLocalRef!!(env, jstr)
}
return result
}内存管理优化
避免局部引用泄漏
kotlin
@OptIn(ExperimentalForeignApi::class)
// ❌ 局部引用泄漏
@CName("Java_com_example_Bad_leakReferences")
fun leakReferences(env: CPointer<JNIEnvVar>, thiz: jobject, array: jobjectArray) {
val jniEnv = env.pointed.pointed!!
val length = jniEnv.GetArrayLength!!(env, array)
for (i in 0 until length) {
val element = jniEnv.GetObjectArrayElement!!(env, array, i)
// ❌ 未删除局部引用,循环中积累大量引用
// 处理 element...
}
// 可能耗尽局部引用表
}
// ✅ 正确管理局部引用
@CName("Java_com_example_Good_manageReferences")
fun manageReferences(env: CPointer<JNIEnvVar>, thiz: jobject, array: jobjectArray) {
val jniEnv = env.pointed.pointed!!
val length = jniEnv.GetArrayLength!!(env, array)
for (i in 0 until length) {
val element = jniEnv.GetObjectArrayElement!!(env, array, i)
// 处理 element...
// ✅ 立即删除局部引用
jniEnv.DeleteLocalRef!!(env, element)
}
}性能测试工具
基准测试框架
kotlin
@OptIn(ExperimentalForeignApi::class)
object JNIBenchmark {
fun measure(name: String, iterations: Int, block: () -> Unit): Long {
// 预热
repeat(100) { block() }
// 测量
val start = kotlin.system.getTimeNanos()
repeat(iterations) { block() }
val end = kotlin.system.getTimeNanos()
val avgNs = (end - start) / iterations
println("$name: ${avgNs}ns per iteration")
return avgNs
}
}
// 使用
fun benchmarkArrayAccess() {
val array = IntArray(10000) { it }
JNIBenchmark.measure("GetIntArrayElements", 1000) {
nativeProcessWithElements(array)
}
JNIBenchmark.measure("GetIntArrayRegion", 1000) {
nativeProcessWithRegion(array)
}
JNIBenchmark.measure("DirectByteBuffer", 1000) {
val buffer = ByteBuffer.allocateDirect(10000 * 4)
nativeProcessDirect(buffer)
}
}JNI 性能优化的核心是减少边界跨越、避免数据拷贝、合理缓存。遵循这些原则,性能提升10-100倍是常见的。