You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

JNI回调Java时字符串生命周期及同步问题求助

JNI字符串生命周期与异步处理崩溃问题分析

崩溃原因定位

你遇到的随机崩溃,核心原因是JNI字符串引用/底层内存的生命周期与Java侧异步处理的时机不匹配:

  • 若C侧通过GetStringUTFChars获取了Java字符串的底层字符指针,未复制就直接传递给Java侧(或让Java侧异步访问),随后调用ReleaseStringUTFChars释放内存,那么Java侧异步处理时,该指针已失效,会触发内存访问错误。
  • 若C侧在Java侧异步处理完成前就调用DeleteLocalRef释放jstring本地引用(或让JNI方法返回,本地引用自动回收),且Java侧操作依赖JNI层的引用而非Java堆的String对象,也会导致引用失效。

必须提前复制字符串的场景

以下两种情况必须复制字符串内容:

  1. C侧将GetStringUTFChars返回的字符指针传递给Java侧,由Java侧后续(异步)访问该指针指向的内存。
  2. C侧需要在JNI方法返回后立即释放jstring相关JNI引用,而Java侧无法保证在释放前完成所有字符串操作。

正确的同步与处理方案

方案1:在JNI层复制字符串,传递Java侧String对象(最安全)

在JNI方法内,将输入的jstring复制为新的JavaString对象,再传递给Java侧做异步处理。此时Java侧持有新字符串的强引用,不受C侧原JNI引用释放的影响。

C侧代码示例:

JNIEXPORT void JNICALL Java_com_example_RTCClient_onCandidates(JNIEnv *env, jobject thiz, jstring sdp_jstr) {
    // 提取原字符串的UTF-8内容
    const char *sdp_raw = (*env)->GetStringUTFChars(env, sdp_jstr, NULL);
    if (sdp_raw == NULL) {
        return; // 内存分配失败,直接返回
    }

    // 创建新的Java String对象(复制内容,Java侧持有强引用)
    jstring sdp_copy = (*env)->NewStringUTF(env, sdp_raw);
    // 释放原字符串的临时引用
    (*env)->ReleaseStringUTFChars(env, sdp_jstr, sdp_raw);

    // 获取Java侧异步处理方法的ID
    jclass cls = (*env)->GetObjectClass(env, thiz);
    jmethodID handle_sdp_mid = (*env)->GetMethodID(env, cls, "handleRemoteSdpAsync", "(Ljava/lang/String;)V");
    if (handle_sdp_mid != NULL) {
        // 调用Java侧异步方法,传入复制后的字符串
        (*env)->CallVoidMethod(env, thiz, handle_sdp_mid, sdp_copy);
    }

    // 释放JNI本地引用
    (*env)->DeleteLocalRef(env, sdp_copy);
    (*env)->DeleteLocalRef(env, cls);

    // 此时可以安全释放原jstring的本地引用(或等待JNI方法返回自动回收)
    (*env)->DeleteLocalRef(env, sdp_jstr);
}

Java侧代码示例:

public class RTCClient {
    public native void onCandidates(String sdp);

    // 接收复制后的字符串,异步处理
    public void handleRemoteSdpAsync(String sdpCopy) {
        new Thread(() -> {
            try {
                // 耗时的SDP处理操作
                setRemoteDescription(sdpCopy);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }).start();
    }

    private void setRemoteDescription(String sdp) {
        // 实际处理SDP的逻辑,比如传入WebRTC的PeerConnection
    }
}

方案2:使用全局引用(需注意内存泄漏)

如果不想复制字符串,可以将原jstring转为JNI全局引用,Java侧处理完成后主动通知C侧释放该全局引用。这种方式避免了内存复制,但必须严格保证全局引用被释放,否则会导致内存泄漏。

C侧代码示例:

// 全局引用存储(可关联到Java对象的附加数据,避免全局变量的线程安全问题)
static jstring g_sdp_global_ref = NULL;

JNIEXPORT void JNICALL Java_com_example_RTCClient_onCandidates(JNIEnv *env, jobject thiz, jstring sdp_jstr) {
    // 创建全局引用,延长生命周期
    g_sdp_global_ref = (*env)->NewGlobalRef(env, sdp_jstr);
    if (g_sdp_global_ref == NULL) {
        return;
    }

    // 调用Java侧异步处理方法
    jclass cls = (*env)->GetObjectClass(env, thiz);
    jmethodID handle_mid = (*env)->GetMethodID(env, cls, "handleSdpWithGlobalRef", "()V");
    if (handle_mid != NULL) {
        (*env)->CallVoidMethod(env, thiz, handle_mid);
    }

    (*env)->DeleteLocalRef(env, cls);
    // 原jstring本地引用可安全释放
    (*env)->DeleteLocalRef(env, sdp_jstr);
}

// Java侧处理完成后调用此方法释放全局引用
JNIEXPORT void JNICALL Java_com_example_RTCClient_releaseSdpGlobalRef(JNIEnv *env, jobject thiz) {
    if (g_sdp_global_ref != NULL) {
        (*env)->DeleteGlobalRef(env, g_sdp_global_ref);
        g_sdp_global_ref = NULL;
    }
}

Java侧代码示例:

public class RTCClient {
    public native void onCandidates(String sdp);
    public native void releaseSdpGlobalRef();

    private String originalSdp;

    public void onCandidates(String sdp) {
        this.originalSdp = sdp;
        nativeOnCandidates(sdp);
    }

    public void handleSdpWithGlobalRef() {
        new Thread(() -> {
            try {
                // 这里直接使用原字符串(全局引用保证了Java对象不被GC)
                setRemoteDescription(originalSdp);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                // 处理完成后释放全局引用
                releaseSdpGlobalRef();
            }
        }).start();
    }

    private native void nativeOnCandidates(String sdp);
    private void setRemoteDescription(String sdp) {
        // 处理SDP逻辑
    }
}

关键注意事项

  • JNI本地引用仅在当前线程的JNI调用栈中有效,不能跨线程传递或释放。
  • GetStringUTFChars返回的指针不能长期持有,必须在使用完后调用ReleaseStringUTFChars,否则会导致内存泄漏或野指针。
  • 异步处理时,Java侧必须持有字符串的强引用(或通过JNI全局引用保证对象存活),直到所有操作完成。

内容的提问来源于stack exchange,提问作者user3499851

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.23 16:34:56