C++多线程打包成so给JAVA后端(Ubuntu)<2>

简单的线程启动已经在<1>中介绍了,核心还是C++的线程调用方式,只是多了JVM的绑定,那么今天就为大家介绍如何让Java端启动这个心跳包线程吧!

一边是C++代码,另一遍是JAVA代码,该使用什么方式进行交互呢?

答案是:JVM层。

Java → C++ 方向:

  1. Java调用native方法

  2. JVM查找对应的JNI函数

  3. JNI桥梁进行数据类型转换(Java→C++)

  4. 调用C++业务逻辑

C++ → Java 方向:

  1. C++返回业务结果

  2. JNI桥梁进行数据类型转换(C++→Java)

  3. JNI函数返回结果给JVM

  4. Java代码接收并使用结果

其实对于第一次写so方法的程序员来讲,是很迷糊的,JVM是怎么查找到JNI函数的呢?

我刚开始写的时候也很迷糊,这是一个核心机制问题!

JVM查找JNI函数的过程确实很巧妙,通常有两种方式:静态注册(默认方式)、动态注册。

为了更快理解,主要讲解静态注册方式。

静态注册JVM

1.普通函数调用

cpp 复制代码
JNIEXPORT void JNICALL Java_com_jc_JCMiddlewareJNI_initSo(JNIEnv *env, jobject obj)
{
	fprintf(stderr, "<soLog>Java_com_jc_JCMiddlewareJNI_initSo\n");
}

使用静态注册时,命名规则一定是按照上面的格式严格执行。

需要注意的是:包名中的"."用下划线"_"代替。比如我的包名是:com.jc就变成了com_jc

注册函数写好后,我们的C++代码就可以在这个函数中实现了。当我们使用AndroidStudio写NativeC++代码时,前缀都是会自动生成的,如果怕自己写错可以生成一个模板在后面改就可以了。

2.初始化入口函数

当Java程序通过System.loadLibrary("YourLib");加载你写的本地so文件时,在库被加载之后,任何其他JNI函数被调用之前,JVM会自动调用JNI_OnLoad函数。

对于C++开发人员来说,相当于theApp/main的存在。

cpp 复制代码
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) 
{
	//具体代码...
	return JNI_VERSION_1_8; //返回版本号
}

在这个函数中,可以做以下几种事情:

a. 缓存 jclass、jmethodID、jfileIdID等全局变量。

有人会问为什么要缓存这些变量呢?

这是一个关于JNI性能优化的核心问题,缓存这些引用的主要作用是避免每次调用本地方法时都进行耗时的查询操作,从而极大地提升性能,同时也能简化代码结构并保证线程安全。

当C++代码频繁将数据回调给Java端时,势必会消耗性能,如果在回传数据时频繁地查询类型以及函数名时,操作非常耗时,并且会重复冗余的工作,这完全是不必要的。

b. 初始化全局的JavaVM*指针。

这是JNI开发中实现高级功能的基础,能够让本地代码在任何线程、任何时间与Java虚拟机进行交互。

尤其是在本地创建的线程中尤为重要,想要让so里面自己启动线程,那么初始化JavaVM*指针一定是最重要的。

c. 动态注册本地方法。

d. 初始化本地库所需的其他全局资源。

既然如何,我们需要丰富下JNI_OnLoad函数~

cpp 复制代码
static JavaVM* jvm = nullptr;
static jclass gJcClass = nullptr;
static jmethodID gLogMid = nullptr;

JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) 
{
    // 保存 JavaVM 指针
    jvm = vm;
    
    // 获取 JNIEnv
    JNIEnv* env;
    if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_8) != JNI_OK) {
        return JNI_ERR;
    }
    
    // 查找并缓存 Java 类
    jclass localRef = env->FindClass("com/jc/JCMiddlewareJNI");
    if (!localRef) {
        env->ExceptionClear();  // 清除可能的异常
        return JNI_ERR;
    }
    
    // 创建全局引用
    gJcClass = (jclass)env->NewGlobalRef(localRef);
    env->DeleteLocalRef(localRef);
    if (!gJcClass) {
        return JNI_ERR;
    }
    
    // 缓存方法 ID
    gLogMid = env->GetStaticMethodID(gJcClass, "SoCallbackLogBack", "(Ljava/lang/String;)V");
    if (!gLogMid) {
        env->ExceptionClear();
        return JNI_ERR;
    }
    
    // 在单例中设置指针
    gManager::instance().setJNIReferences(jvm, gJcClass, gLogMid);
    
    return JNI_VERSION_1_8;
}

JNIEXPORT void JNI_OnUnload(JavaVM* vm, void* reserved) {
    JNIEnv* env;
    if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_8) == JNI_OK) {
        if (gJcClass) {
            env->DeleteGlobalRef(gJcClass);
            gJcClass = nullptr;
        }
    }
    jvm = nullptr;
    gLogMid = nullptr;
    gManager::instance().clearJNIReferences();
}

代码解析:

这是一个完整的JNI库生命周期管理代码。

  • 全局变量声明:用于缓存JNI资源。

  • JNI_OnLoad:库加载时的初始化函数。

  • JNI_OnUnload:库卸载时的清理函数。

static JavaVM* jvm = nullptr;

作用:缓存Java虚拟机实例的指针,在当前使用时,主要用于后续启动线程。

通过jvm->AttachCurrentThread()让本地线程与JVM交互。

static jclass gJcClass = nullptr;

作用:缓存Java类的全局引用,用于后续进行数据回调。

static jmethodID gLogMid = nullptr;

作用:缓存Java静态方法的ID,对应的方法是:SoCallbackLogBack(String msg)。

在这里我只列举了回调日志的方法,大家可以根据自身项目的情况,进行扩展。

调用C++功能

1.初始化心跳包线程

为了大家方便理解,在"Java_com_jc_JCMiddlewareJNI_initSo"中进行心跳包线程的启动。

至于这个函数,大家可以将它与JNI_OnLoad以及JNI_OnUnload放到一块,方便统一管理,比如我将这些JVM的接口函数统一放到了native-lib.cpp中。

代码如下:

cpp 复制代码
#include <jni.h>
#include<jni_md.h>
#include <string>
#include "NewMiddlewareOperation.h" //C++操作类

static NewMiddlewareOperation* g_pOperation = nullptr;

static JavaVM* jvm = nullptr;
static jclass gJcClass = nullptr;
static jmethodID gLogMid = nullptr;

JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
    //详细代码如上所示
}

JNIEXPORT void JNI_OnUnload(JavaVM* vm, void* reserved) {
    //详细代码如上所示
}

extern "C" 
{
	JNIEXPORT void JNICALL Java_com_jc_JCMiddlewareJNI_initSo(JNIEnv *env, jobject obj)
	{
		//创建C++操作类,在构造函数中启动线程
		if (g_pOperation == nullptr)
		{
			g_pOperation = new NewMiddlewareOperation(env, obj);
		}
	}
	//在这里写于JAVA交互的普通函数...
}

NewMiddlewareOperation是C++操作类,在这里实现关于C++的核心逻辑功能。

在这里,有人会疑问:文章<1>中不是在"AddNewModel"中启动的心跳包线程吗?为什么你这次讲是在构造函数中?

其实都可以的,无论是写在哪里,心跳包的线程都只会启动一次,写在构造函数中,是为了让大家更容易理解。

cpp 复制代码
NewMiddlewareOperation::NewMiddlewareOperation(JNIEnv* env, jobject obj)
	:m_bRunning(false)
{
	//构造函数中保存全局引用
	gManager::instance().m_globalObj = env->NewGlobalRef(obj);

	
	// 启动心跳线程(仅启动一次)双重检查锁定
	if (!m_bRunning)
	{
		std::string sPrintLog = "<soLog>create heart thread";
		gManager::instance().SoCallBackPrintLog(sPrintLog, env, obj);
		m_bRunning = true; //启动线程
		m_thread = std::thread(&NewMiddlewareOperation::OnThreadHeartWorker, this);
		// 3. 验证线程是否成功创建
		if (m_thread.joinable())
		{
			m_bRunning = true;
		}
		else
		{
			std::string log = "<soLog>thread full: un joinable";
			gManager::instance().SoCallBackPrintLog(log, env, obj);
		}
	}
}

到这里,Java端加载C++的initSo函数就能够启动心跳包线程了!具体的线程方法文章<1>中有讲解,这里就不再详细描述了。

到这里就能够在C++的so文件中完美启动一个线程并定时给Java端发送消息~

大家有什么问题,或者是我的逻辑有问题的都可以留言告诉我哟,后续再为大家分享如何启动多线程的方式!

我是糯诺诺米团,一名C++程序媛~

相关推荐
yuanmenghao14 小时前
自动驾驶中间件iceoryx - 内存与 Chunk 管理(一)
c++·vscode·算法·链表·中间件·自动驾驶·柔性数组
艾莉丝努力练剑14 小时前
【QT】初识QT:背景介绍
java·运维·数据库·人工智能·qt·安全·gui
Grassto14 小时前
Go 在哪里找第三方包?Module 查找顺序详解
开发语言·后端·golang
橘颂TA14 小时前
【剑斩OFFER】算法的暴力美学——面试题 01.02 :判定是否互为字符串重排
c++·算法·leetcode·职场和发展·结构与算法
小鸡脚来咯14 小时前
后端开发vue速成
开发语言·前端·javascript
HABuo14 小时前
【Linux进程(二)】操作系统&Linux的进程状态深入剖析
linux·运维·服务器·c语言·c++·ubuntu·centos
-西门吹雪14 小时前
c++线程之再研究研究多线程
开发语言·c++
一线大码14 小时前
后端分层架构规范和标准包结构
java·后端
南屿欣风14 小时前
Maven 聚合工程打包报错:Unable to find main class 快速解决
java·maven