公司项目需要用到第三方qt插件,由于业务是偏底层的,基本上用不到jar包,因此只能通过jni的方式调用。没学过c++,十多天的摸爬滚打一路过来不容易啊!今天总算跑通了。网上关于jni的资料相关博客有很多,我这里就不重复了,推荐两个博主的文章,我主要把碰到比较困难的问题总结一下。
问题1:
首先是参数传递的问题。思路很清晰,java这边定义一个native方法,然后这个方法在c++(qt)那方实现,很容易就能想到参数传递以及返回值是关键,只要搞清楚这两点剩下的逻辑在qt实现就行了。基本传递数据类型比较简单,对象类型通过上面两个系列的文章也能搞定,碰到的第一个问题是复杂对象类型数据的传递。比如类A的参数有int id,B b,C c,即有的字段是对象类型的。甚至再多嵌套几重也是一样的想清楚了还是比较简单的。举个栗子:
//真正实现的时候字段肯定是private,这里只是方便演示(好吧,我摊牌了,少写两行代码-_-..)
//private字段通过get set方法一样能拿到数据。
public class A{
public int id;
//这里如果是human数组或者list也可以,就不搞那么复杂了
public Human human;
public Giraffe giraffe;
//此方法的功能:传入a对象,计算Human 和Giraffe 的身高差
public native static float div(A a);
public A(Human human, Giraffe giraffe) {
this.human = human;
this.giraffe = giraffe;
}
}
class Human{
public String name;
public float tall;
}
class Giraffe{
public String name;
public float tall;
}
将A.java生成头文件com_zone_jni_A.h后看到最主要的地方:
/*
* Class: com_zone_jni_A
* Method: div
* Signature: (Lcom/zone/jni/A;)F
*/
JNIEXPORT jfloat JNICALL Java_com_zone_jni_A_div
(JNIEnv *, jobject, jobject);
新建个Sources文件main.cpp实现这个方法。
#include "com_zone_jni_A.h"
#include <iostream>
/*
* Class: com_zone_jni_A
* Method: div
* Signature: (Lcom/zone/jni/A;)F
*/
JNIEXPORT jfloat JNICALL Java_com_zone_jni_A_div
(JNIEnv *env, jclass, jobject jAObject){
//获取jAObject对象中的class对象
jclass aClass = env->GetObjectClass(jAObject);
//获取aClass对象中名称为'id'签名为I的字段id
jfieldID fieldId = env->GetFieldID(aClass, "id", "I");
//得到jAObject对象的id字段的值
jint getId = env->GetIntField(jAObject, fieldId);
std::cout << "id---" << getId << std::endl;
//查找HumanClass
jclass jHumanClass = env->FindClass("com/zone/jni/Human");
//字段名称:tall 签名:Lcom/zone/jni/Human; ###注意 ";" 是属于签名的
jfieldID id_human = env->GetFieldID(aClass, "human", "Lcom/zone/jni/Human;");
jobject getHuman = env->GetObjectField(jAObject, id_human);
jfieldID id_human_tall = env->GetFieldID(jHumanClass, "tall", "F");
jfloat humanTall = env->GetFloatField(getHuman, id_human_tall);
//这里用不上name字段,只是说明一下jstring和c++的string是不同的需要做转换
jstring humanName = (jstring)env->GetObjectField(getHuman, id_human);
const char *cHumanName = env->GetStringUTFChars(humanName, nullptr);
std::cout << "HumanName: " << cHumanName << std::endl;
//查找GiraffeClass
jclass jGiraffeClass = env->FindClass("com/zone/jni/Giraffe");
//字段名称:tall 签名:Lcom/zone/jni/Giraffe;
jfieldID id_giraffe = env->GetFieldID(aClass, "giraffe", "Lcom/zone/jni/Giraffe;");
jobject getGiraffe = env->GetObjectField(jAObject, id_giraffe);
jfieldID id_giraffe_tall = env->GetFieldID(jGiraffeClass, "tall", "F");
jfloat giraffeTall = env->GetFloatField(getGiraffe, id_giraffe_tall);
return giraffeTall - humanTall;
}
java测试:
问题2:
常见的崩溃和错误:
一、崩溃
二、错误
Exception in thread "main" java.lang.UnsatisfiedLinkError:
java.lang.NoSuchFieldError: id
java.lang.NoSuchMethodError: id
.......
错误的解决不要怀疑,一定是代码的问题。
崩溃大多数原因也和代码相关(是指一些粗心或者意外写错的代码,学java的,指针,内存溢出那些没算在这一类),还有一些比较复杂,原因也千奇百怪,比如内存溢出、软件版本等问题。
因此排查问题的第一步就是:检查代码!检查代码!检查代码!确定c++调用java的类字段方法要一模一样。
从上到下说来看:这三个类我都放在同一个包下,com.zone.jni。
在c++中FindClass或者需要用到签名的地方着重检查时不时对应的,如果修改了包名这些用到的地方一定要同步修改。
再看到字段,java定义了id字段:public int id;小写 i,c++如果大写也会出错,像这样:
方法名也要一样不用多说了。
再下来到方法调用传参的地方,c++用到了的参数(对象)就必须赋值,例如下面这样:
用到了A对象中的human、giraffe等值,而又传的空对象过去,肯定是不行的。
对于jvm崩溃的一些复杂的情况后面再讨论。
问题3:
Can't find dependent libraries问题
当你的dll文件明明放在指定文件夹或者项目文件下,不管用绝对路径还是相对路径都报错找不到libraries的时候就要检查一下依赖了,如下图。利用工具Dependency Walker
工具链接:
链接:https://pan.baidu.com/s/1CNuLHsSHM8JsjnIqgbiMAg
提取码:iek1
如QtConverLibTest.dll依赖的其他dll:
黄色问好表示缺少该依赖,拿QT5Cored.dll来说,它下面可能还依赖了很多dll,要一层一层导入并且被依赖的dll要在依赖的dll之前被导入。
图示QT5Cored.dll依赖的ICUDT52.DLL 、ICUUC52.DLL、ICUIN52.DLL在它之前被导入。
当然要是这样一行一行写下来如果依赖的库多了,写二三十行都很常见,那么怎么办呢,不可能每次都写那么多load吧。那就是修改环境变量,把dependency路径加在PATH环境变量中,和设置jdk环境变量一样的方法,很简单,不会的另行百度。设置了环境变量后就只需要加载那一个你需要的dll了。
问题4:
Debug下正常, Release崩溃的问题
debug顺利的调试通了,千万别高兴的太早,这个问题足足困扰了我两天半,虽然最后解决了,但还是不清楚具体原因。为了找问题基本上是一段代码一段代码的debug,定位到了几个地方却发现代码没问题。网上有很多种说法,大多都是说内存问题、野指针、数组越界、初始化等等。起初怀疑是QString.arg()有问题,还有jstring转char*,但调查后都没看出什么所以然,最后是通过将LPCWSTR类型的变量改为了char*,只能感叹c++中字符串的操作比起java来好复杂。
参考:
问题5:
LInux下文件权限的问题。
如果你的代码中用了QProcess *process = new QProcess();调用本地的应用程序那么一定要检查应用程序的权限及你生成的动态库文件的权限。最好直接将权限设置为755,以防检查问题的时候没看到这点,耽误很久来查问题。
问题6:
其他
1、其实这一点算不上什么问题,只是需要注意:不同平台不同环境要对应,32位jdk使用32位dll,64位jdk使用64位dll,mac环境下生成.dylub文件,linux生成.so文件。
2、关于测试。要是linux和mac机器上没有idea等编辑器,只需要装好一个jdk也是可以测试的。毕竟java号称一次编译到处运行可不是白来的。只需要将测试类打成jar包即可。生成jar包可以参考:
其实只需要记住一句话:jar -cvf xxx.jar com/
普通是java类打成jar包默认是没有指定主类的(SpringBoot等项目可以在maven中配置),要是碰到如下问题:
需要用压缩工具打开jar包解压出MANIFEST.MF文件,然后修改
如下图:
加上这句话Main-Class: xxx (根据自己的主类而定)
最后将此文件复制进jar包替换即可。生成了jar包就可以在各个环境下测试了。
3、此外就是对linux和mac环境的基本操作要熟练。