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

一边是C++代码,另一遍是JAVA代码,该使用什么方式进行交互呢?
答案是:JVM层。
Java → C++ 方向:
-
Java调用native方法
-
JVM查找对应的JNI函数
-
JNI桥梁进行数据类型转换(Java→C++)
-
调用C++业务逻辑
C++ → Java 方向:
-
C++返回业务结果
-
JNI桥梁进行数据类型转换(C++→Java)
-
JNI函数返回结果给JVM
-
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++程序媛~