我們知道在jni中執行一個java函數需要調用幾行代碼才行,如
jclass objClass = (*env).GetObjectClass(obj);
jmethodID methodID = (*env).GetMethodID(objClass, methodName, methodSig);
jobject result = (*env).CallObjectMethod(obj, methodID, ...);
這樣使用起來很不方便,尤其當需要大量的調用java函數就會產生大量的上述代碼,由此我產生了一個開發封裝這些操作的工具類,以便大量簡化我們的開發。
簡單封裝其實可以看到整個過程基本是固定不變的:先獲取Class,然後獲取method,然後在執行call。所以可以簡單的先封裝成一系列工具函數,如:
jobject callObjMethod(JNIEnv *env, jobject obj, const char *methodName, const char *methodSig, ...){
va_list args;
jclass objClass = (*env).GetObjectClass(obj);
jmethodID methodID = (*env).GetMethodID(objClass, methodName, methodSig);
va_start(args,methodSig);
jobject result = (*env).CallObjectMethodV(obj, methodID, args);
va_end(args);
return result;
}
jint callIntMethod(JNIEnv *env, jobject obj, const char *methodName, const char *methodSig, ...){
va_list args;
jclass objClass = (*env).GetObjectClass(obj);
jmethodID methodID = (*env).GetMethodID(objClass, methodName, methodSig);
va_start(args,methodSig);
jint result = (*env).CallIntMethodV(obj, methodID, args);
va_end(args);
return result;
}
jboolean callBooleanMethod(JNIEnv *env, jobject obj, const char *methodName, const char *methodSig, ...){
va_list args;
jclass objClass = (*env).GetObjectClass(obj);
jmethodID methodID = (*env).GetMethodID(objClass, methodName, methodSig);
va_start(args,methodSig);
jboolean result = (*env).CallBooleanMethodV(obj, methodID, args);
va_end(args);
return result;
}
這樣當我們要通過jni執行某個java函數的時候,就一行代碼就可以搞定了,比如String.length():
jint len = callIntMethod(env, str, "length", "()I")
這樣就可以大大減少了代碼量,而且代碼也更易讀了。
優化通過上面可以看到這些函數大部分代碼都非常類似,只有一行代碼和返回值有區別,所以我考慮使用函數模版來進行優化,如下:
template <typename T>
T callMethod(JNIEnv *env, jobject obj, const char *methodName, const char *methodSig, ...){
va_list args;
jclass objClass = (*env).GetObjectClass(obj);
jmethodID methodID = (*env).GetMethodID(objClass, methodName, methodSig);
va_start(args,methodSig);
T result;
if(typeid(T) == typeid(jobject)){
result = (*env).CallObjectMethodV(obj, methodID, args);
}
if(typeid(T) == typeid(jdouble)){
result = (*env).CallDoubleMethodV(obj, methodID, args);
}
...
va_end(args);
return *result;
}
這樣只要調用callMethod<return type>即可,願望很美好,但是上面代碼實際上是無法通過編譯。
因為模版函數實際上是在編譯時,根據調用的類型,拷貝生成多個具體類型的函數以便使用。
所以如果有這樣的調用callMethod<jobject>(...),在編譯時就會拷貝成一個如下的函數:
jobject callMethod(JNIEnv *env, jobject obj, const char *methodName, const char *methodSig, ...){
va_list args;
jclass objClass = (*env).GetObjectClass(obj);
jmethodID methodID = (*env).GetMethodID(objClass, methodName, methodSig);
va_start(args,methodSig);
jobject result;
if(typeid(jobject) == typeid(jobject)){
result = (*env).CallObjectMethodV(obj, methodID, args);
}
if(typeid(jobject) == typeid(jdouble)){
result = (*env).CallDoubleMethodV(obj, methodID, args);
}
...
va_end(args);
return *result;
}
注意這行代碼:
if(typeid(jobject) == typeid(jdouble)){
result = (*env).CallDoubleMethodV(obj, methodID, args);
}
雖然實際上是無法執行的代碼,但是編譯時還是會進行檢查,由於將jdouble類型的賦值給jobject類型的result,所以編譯不通過,類型無法轉換。而且這裡用強轉static_cast等方法都不行。
我考慮兩種方法來解決這個問題,一種是保證編譯不報錯,因為運行時不會執行的代碼,只要通過編譯就可以。另外一種是不同的類型編譯不同的代碼。
void指針在c++中void指針可以被賦值任何類型指針,且void指針強轉為任何類型指針在編譯時不會報錯。代碼如下:
template <typename T>
T callMethod(JNIEnv *env, jobject obj, const char *methodName, const char *methodSig, ...){
va_list args;
jclass objClass = (*env).GetObjectClass(obj);
jmethodID methodID = (*env).GetMethodID(objClass, methodName, methodSig);
va_start(args,methodSig);
T* result = new T();
if(typeid(T) == typeid(jobject)){
jobject objec = (*env).CallObjectMethodV(obj, methodID, args);
void *p = &objec;
result = (T*)p;
}
if(typeid(T) == typeid(jdouble)){
jdouble doub = (*env).CallDoubleMethodV(obj, methodID, args);
void *p = &doub;
result = (T*)p;
}
va_end(args);
return *result;
}
當然利用void指針很不安全,雖然可以通過編譯,但是執行時如果類型不同會直接造成crash。所以並不建議這種方式。
模版函數特例化將差異代碼部分封裝到另一個模版函數中,並且對每種類型進行特例化,這樣還可以去掉if-else判斷,代碼如下:
template <typename K>
K call2Result(JNIEnv *env, jobject obj, jmethodID methodID, va_list args){
return *(new K());
}
template <>
jobject call2Result(JNIEnv *env, jobject obj, jmethodID methodID, va_list args){
return (*env).CallObjectMethodV(obj, methodID, args);
}
template <>
jdouble call2Result(JNIEnv *env, jobject obj, jmethodID methodID, va_list args){
return (*env).CallDoubleMethodV(obj, methodID, args);
}
...
template <typename T>
T callMethod(JNIEnv *env, jobject obj, const char *methodName, const char *methodSig, ...){
va_list args;
jclass objClass = (*env).GetObjectClass(obj);
jmethodID methodID = (*env).GetMethodID(objClass, methodName, methodSig);
va_start(args,methodSig);
T result = call2Result<T>(env, obj, methodID, args);
va_end(args);
return result;
}
這樣在編譯時,如果返回值是jobject類型的,當編譯到call2Result時,就會直接調用jobject call2Result(...)這個函數,就不再涉及類型轉換的問題。
這樣去掉了if判斷,但是由於沒有通用的函數,所以所有使用的類型都需要特例化,如果某個類型未特例化,代碼執行可能就會有問題。而在jni中,與java對應的類型其實就那麼十幾種,所以我們只要全部實現一遍call2Result即可。
undefined reference to使用模版函數出現這個問題,是因為沒有將模版函數的實現寫在頭文件中,只將模版函數的聲明在頭文件中,而在源文件中實現的。
所以我們應該將模版函數的實現也寫進頭文件中,而模版函數特例化則可以在源文件中實現,但是注意要include頭文件。
返回值是void類型因為void的特殊性,所以如果當成泛型來處理會有很多問題,這裡把返回值是void類型的單獨實現一個函數即可。
總結上面我們僅僅是實現了調用普通函數的工具,根據這個思路我們還可以實現調用靜態函數、獲取成員變量、賦值成員變量等,這樣當我們在進行jni開發的時候,如果需要對java對象或類進行操作,只需要一行代碼就可以了。
源碼關注本公眾號(BennuCTech),發送「JNIObjectTools」獲取源碼。
實現一個通用的中英文排序工具
Android中使控制項保持固定寬高比的幾種方式
RecyclerView局部刷新機制——payload