公司項目需要用到第三方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來好複雜。
參考:
https://blog.csdn.net/Qsir/article/details/78645062
問題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包可以參考:
https://blog.csdn.net/zhanaolu4821/article/details/103006983
其實只需要記住一句話:jar -cvf xxx.jar com/
普通是java類打成jar包默認是沒有指定主類的(SpringBoot等項目可以在maven中配置),要是碰到如下問題:
需要用壓縮工具打開jar包解壓出MANIFEST.MF文件,然後修改
如下圖:
加上這句話Main-Class: xxx (根據自己的主類而定)
最後將此文件複製進jar包替換即可。生成了jar包就可以在各個環境下測試了。
3、此外就是對linux和mac環境的基本操作要熟練。