提及这个问题,就会有人问:为什么要用C++写成so让JAVA端调用,而不是让JAVA端自己写逻辑呢?
这是一个非常好的问题!它触及了系统架构设计的核心考量。用C++编写so让Java通过JNI调用,而不是全部用Java实现,主要是为了在性能、复用、能力这三个关键维度上取得平衡。
现在我们公司有个业务需要这样处理,之前是C++端写的处理逻辑,现在要将这部分功能放到Java服务台端调用,做成网络化软件,具体的原因这里不说了,我来说一下如何将so给Java调用吧!
之前我也写过类似的文章,是使用AndroidStudio将C++代码编译成so文件,当时是需要给平板端使用,现在不一样了,这次编译的so是要给Java后端使用,并运行在服务器中。这还是稍微有一点点差别的。
开发工具:无论是windows版本还是Ubuntu
系统,都是使用jdk-11.0.17 + VSCode进行开发。
当在windows环境下是需要编译成*.dll,在Ubuntu系统中需要打包成so的形式。
无论是在windows环境下还是Ubuntu环境下都可以共用一份代码,只需要针对Ubuntu环境做小小的改动就可以。
我来说一下我的代码中线程实现的核心功能:
1:固定心跳包线程
每间隔一秒需要轮巡一次数据,当五秒未收到消息时,发送心跳包检索设备是否在线。
2:多线程检测硬件设备做防抖处理
根据当前CPU的内核个数开启多线程,开启这个多线程主要是因为连接的硬件设备,在对硬件模型进行操作时有些传感器会来回抖动,导致会频繁触发某个点的开关,使用多线程做防抖操作,当上传的消息在500ms内未进行变化时,确定该点被触发。
注意:这里只为大家讲解如何实现多线程,至于硬件传感器是否使用合理的问题,不在这里做讨论了。
心跳包线程
1:启动线程
该线程用于监测所有设备的心跳,当连接30台设备性能是绰绰有余的~
|---------|-------------|
| 指标 | 预估数值 |
| CPU占用 | < 5% |
| 内存占用 | ~30MB |
| 线程上下文切换 | 可忽略 |
| 网络带宽 | 取决于每次访问的数量值 |
这种负载对于现代操作系统来说绰绰有余,所以完全可以用一个线程来监测。
当第一次添加设备编号时,才需要启动线程
cpp
//构造函数中初始化变量
m_bRunning = false; //默认,线程未启动
//函数调用
void NewMiddlewareOperation::AddNewModel(std::string sModelId, jstring jsModeId, JNIEnv* env, jobject obj)
{
auto pOperation = FindValidOperationPtr(sModelId, env, obj);
if (pOperation)
{
//当前模型已经存在,不需要添加了
return;
}
//存储设备id的映射,在回调JNI数据时使用
{
jstring jsCmdglobal = (jstring)env->NewGlobalRef(jsModeId);
// 释放局部引用
env->DeleteLocalRef(jsModeId);
gManager::instance().m_mapCacheJStringID.insert(std::make_pair(sModelId, jsCmdglobal));
}
// 使用智能指针创建对象,自动管理生命周期
m_mapModelPtr[sModelId] = std::make_shared<ModelOperationData>(sModelId);
// 启动心跳线程(仅启动一次)双重检查锁定
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);
}
}
}
函数解析:
参数sModelId:设备id已经由jstring转化成std::string类型
参数jsModelId:此时是未经转化的设备id
参数env:JNI环境指针,由调用接口自动传入
参数obj:Java对象引用,也是自动传入
疑问一:为什么我会将模型id的jstring类型以及std::string同时传入到函数中?
答疑:在C++代码中,每个设备id对应了一个操作处理类(类名:ModelOperationData)。我的存储容器是:
cpp
std::map<std::string, std::shared_ptr<ModelOperationData>> m_mapModelPtr; //模型处理类
在这里我才用了智能指针的方式,在Java端也可以正常使用,我已经测试过了~
此时你肯定会有疑问:为什么不直接吧jstring当做key值呢?还可以减少转换!切记,一定不要将jstring作为C++map的key值!
jstring是指针不是实际的字符串,生命周期由JVM管理,有可能导致悬空指针。
疑问二:为什么要存储全局的jstring数据?
答疑:在与Java后端进行回调时,当一拖多设备时,需要知道给回调的设备id,如果在回调时临时转换会导致性能开销问题。尤其是在高频唤醒中性能问题更明显!
而且临时转换时最容易丢失销毁代码:env->DeleteLocalRef(jstr);
为了提高回调效率,这里我将模型id进行了存储,即使是连接30台设备,与临时转换相比较而言这个消耗也是忽略不计的。
2:线程实现
cpp
void NewMiddlewareOperation::OnThreadHeartWorker()
{
JNIEnv* env = nullptr;
jint attachResult = gManager::instance().m_jvm->AttachCurrentThread((void**)&env, NULL);
if (attachResult != JNI_OK) {
fprintf(stderr, "FATAL: AttachCurrentThread failed, error code: %d\n", attachResult);
// 处理错误
return;
}
try
{
while (m_bRunning)
{
for (auto& pair : m_mapModelPtr) // 使用范围for循环,避免迭代器失效
{
// 通过智能指针获取原始指针
pair.second->processThreadDetection(env, gManager::instance().m_globalObj);
}
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
}
}
catch (const std::exception& e)
{
fprintf(stderr, "Heartbeat thread exception: %s\n", e.what());
}
// 分离线程
gManager::instance().m_jvm->DetachCurrentThread();
}
代码解析:
第一步:将线程附加到JVM中。
将当前原生线程附加到Java虚拟机中,获取当前线程的JNIEnv指针,并检查附加是否成功
第二步:while主循环逻辑
使用m_bRunning标志控制线程运行,遍历m_mapModelPtr映射表中所有的模型,并每间隔一秒执行一次循环。
第三步:线程结束时从JVM分离
我的这段代码中还有一些欠缺不足之处:异常处理不完善,我只给出了日志输出,具体的大家自行填充;没有验证JNIEnv的有效性;以及在进行循环时未进行mutex互斥量处理。
使用小结
到这里,简单的心跳包线程就可以在so中被启动,并能够间隔一秒进行一次数据回调了。
其实在代码中还有一些变量我没有详细说明,比如:gManager::instance().m_jvm以及gManager::instance().m_globalObj等等单例变量都没有解释,我会在后续文章中讲解如何用Java代码成功调用这个心跳包线程!
我是糯诺诺米团,一名C++程序媛~