1. 程式人生 > >Android平臺下JNI呼叫第三方so庫

Android平臺下JNI呼叫第三方so庫

 首先說一下在網上查詢資料時,對於呼叫第三方so庫,有人說有兩種方法:

1.    對於so庫的API符合JNI格式(即使用javah指令生成的標頭檔案中那種格式),可以在Java程式碼中宣告它對應的native方法,直接調  用。

    比如,jni方法名為: jstringJNICALL Java_com_example_hellojni_HelloJni_stringFromJNI(JNIEnv *,jobject);    (即字首Java+

包名+類名+方法名)

    那麼這個方法名就是在java中宣告的native方法名:publicnative String  stringFromJNI();


2.    對於so庫的API不符合JNI格式,需要自己編寫c/c++原始檔,在該原始檔實現自己的JNI格式native函式,在JNI函式中呼叫第三方so庫的函式,再在java中呼叫自己實現的JNI格式的native方法。這種方法更加靈活。



jni函式的呼叫請參考我的另一篇部落格http://blog.csdn.NET/u013403478/article/details/52068095

,這裡主要介紹第三方so庫的配置、載入。




一、下圖是我的專案JniDemo目錄:


   

匯入的兩個第三方庫是:libhello.solibhello-jni.so

自己從原始檔myhello.c編譯生成的庫是:libmyhello.so


java中呼叫native方法程式碼如下:

[java]  view plain  copy  print ?
  1. package
     com.example.jnidemo;  
  2.   
  3. import android.app.Activity;  
  4. import android.os.Bundle;  
  5. import android.view.Menu;  
  6. import android.view.MenuItem;  
  7. import android.widget.TextView;  
  8.   
  9. public class DemoMain extends Activity {  
  10.   
  11.     static {  
  12.         System.loadLibrary("myhello");  
  13.         System.loadLibrary("hello");  
  14.         System.loadLibrary("hello-jni");  
  15.     }  
  16.     TextView textView;  
  17.   
  18.     @Override  
  19.     protected void onCreate(Bundle savedInstanceState) {  
  20.         super.onCreate(savedInstanceState);  
  21.         setContentView(R.layout.activity_demo_main);  
  22.   
  23.         textView = (TextView) findViewById(R.id.text);  
  24.         String str = getString() + "." + getJNIString();  
  25.         textView.setText(str);  
  26.     }  
  27.   
  28.     public native String getString();  
  29.   
  30.     public native String getJNIString();  
  31. }  

佈局就一個TextView元件,不再介紹。




二、原始檔的編寫

在使用javah生成標頭檔案後,要在原始檔中使用include“xxxx.h”引入標頭檔案。如果標頭檔案不在jni根目錄下,還要在Android.mk中使用

       LOCAL_C_INCLUDES:=(相對於jni目錄的)包含標頭檔案的目錄路徑

來宣告一下,否則報錯找不到標頭檔案。

然後實現自己宣告的native方法,再在其中呼叫第三方庫的函式。


具體程式碼如下:

[cpp]  view plain  copy  print ?
  1. #include <string.h>  
  2. #include <jni.h>  
  3. #include "com_example_jnidemo_DemoMain.h"  
  4. #include "com_hello_hello_HelloActivity.h"  
  5. #include "com_example_hellojni_HelloJni.h"  
  6.   
  7. jstring Java_com_example_jnidemo_DemoMain_getString(JNIEnv* env, jobject thiz) {  
  8.   
  9.     return Java_com_hello_hello_HelloActivity_sayHello(env, thiz);//呼叫libhello.so中的函式  
  10. }  
  11.   
  12. jstring Java_com_example_jnidemo_DemoMain_getJNIString(JNIEnv* env,  
  13.         jobject thiz) {  
  14.   
  15.     return Java_com_example_hellojni_HelloJni_stringFromJNI(env, thiz);}//呼叫libhello-jni.so中的函式  





三、android.mkApplication.mk配置

Android.mk用來配置各個模組如何編譯,如下:


[plain]  view plain  copy  print ?
  1. LOCAL_PATH := $(call my-dir) #my-dir就是該Android.mk所在目錄,本專案中即jni目錄  
  2. include $(CLEAR_VARS)   #清楚此行之前除了LOCAL_PATH外所有的變數,因為定義的多個模組中會有相同名稱的變數,目的是避免變數賦值衝突  
  3.   
  4. LOCAL_MODULE    := hello-jni    #指定一個當前模組名  
  5. LOCAL_SRC_FILES := libhello-jni.so  #要編譯的原始檔  
  6. include $(PREBUILT_SHARED_LIBRARY)  #編譯目標,PREBUILT_表示已經編譯好的,在使用NDK編譯時不會再次編譯,而是直接拷貝到libs目錄  
  7.                                                                                 #預編譯.a靜態庫使用 PREBUILT_STATIC_LIBRARY  
  8.   
  9. include $(CLEAR_VARS)  
  10.   
  11. LOCAL_MODULE    := hello  
  12. LOCAL_SRC_FILES := libhello.so  
  13. include $(PREBUILT_SHARED_LIBRARY)  
  14.   
  15. include $(CLEAR_VARS)  
  16.   
  17. LOCAL_MODULE    := myhello  #自己由原始檔編譯庫的模組名  
  18. LOCAL_SRC_FILES := myhello.c    #將被編譯的原始檔  
  19.   
  20. #【重要關鍵點】引入依賴的第三方so庫(使用模組名引入),使用\可以引入多個(注意:\符號後沒有空格或其他字元)  
  21. LOCAL_SHARED_LIBRARIES := \  
  22. hello-jni\  
  23. hello  
  24. include $(BUILD_SHARED_LIBRARY) #表示編譯成.so共享庫,即動態庫  

Application.mk用來配置目標編譯ABI(應用二進位制介面),如arm64-v8aarmeabiarmeabi-v7amipsmips64x86x86_64。以armeabi-v7a為例,如下:

[plain]  view plain  copy  print ?
  1. APP_ABI := armeabi-v7a  #表示 編譯目標 ABI(應用二進位制介面)  


四、在終端使用NDK編譯jni目錄


如果看到所有庫都install到了libs目錄,沒有報錯,就編譯成功了.


    補充:下面結合我遇到過的編譯錯誤,解析一下原因及解決手段:


        前提說明:在自己的編譯生成的動態庫中依賴了第三方so庫(編譯時第三方庫不會再次編譯,而是直接拷貝到libs中,所以一般是在編譯依賴了第三方庫的自己的動態庫時報的錯)

(每次用NDK重新編譯,最好刪除之前生成的編譯結果so庫和obj目錄)


(1)報錯error:undefined reference to'Java_com_example_hellojni_HelloJni_stringFromJNI'

collect2:error: ld returned 1 exit status

    網上有人說LOCAL_ALLOW_UNDEFINED_SYMBOLS:= true就可以編譯過,但這是治標不治本,執行時依然報錯。


錯誤原因:so庫在生成時,如果Application.mk宣告一個變數APP-ABI:=xxx,會生成不同平臺下的so庫,而且編譯時64位平臺的so庫無法在32位平臺上被連結,這才報了這個解決依賴連結時找不到庫中方法的問題,所以雖然Android.mk中指明瞭是PREBUILT_SHARED_LIBRARYso庫,但不被連結還是找不到庫中API


解決方法:獲取so庫時最好要取得相應版本的庫(armeabi-v7aarmeabi都是32位,一般情況下應該互相相容,但不相容64位的arm64-v8a)。

(2)報錯 error adding symbols:File in wrong format

collect2:error: ld returned 1 exit status

(ld是連結操作)


錯誤原因:如果Application.mkAPP-ABI:=的目標編譯平臺版本為64位,而實際匯入的so庫版本是32位,就會不識別該so庫(wrong format)。


解決方法:Application.mk(如果沒有,建立)中,把APP-ABI:=xxx的目標編譯版本降低點,如armeabi-v7aarmeabi這些32位等等,使之與實際匯入so庫匹配



整理思路:(可以在終端中,使用$file xxx.so指令檢視動態庫是32位還是64位。)


接下來我通過對比不同so庫與編譯目標ABI來進行解析:


     前提準備:在自己由原始檔編譯的動態庫中,假設依賴呼叫了兩個第三方so a.so(準備了各個ABI版本)和b.so(只有版本為armeabi的)。


1. 第三方a.so庫版本arm64-v8a,(不建立Application.mk)預設目標編譯版本(預設是armeabi版本):

    

    報錯:Fileformat not recognized

    原因:預設的目標編譯版本為32位,比第三方a.so庫的64位低,識別不了a.so庫。


2. 第三方a.so庫版本arm64-v8aApplication.mk中目標編譯版本APP-ABI:= armeabi-v7a


    報錯:Fileformat not recognized

    原因:目標編譯版本是32位,比第三方64位的a.so庫低,識別不了64a.so庫。


3.第三方a.so庫版本armeabi-v7aApplication.mk中目標編譯版本APP-ABI:= arm64-v8a


    報錯:erroradding symbols: File in wrong format

    原因:目標編譯版本是64位比32位的第三方so庫高,a.sob.so被認為檔案格式錯誤。


4. 第三方a.so庫版本armeabi-v7aApplication.mk中目標編譯版本APP-ABI:= armeabi-v7a


    結果:版本匹配,NDK編譯正常,armeabi-v7a相容armeabi版本的b.soapp執行正常


5. 第三方a.so庫版本armeabiApplication.mk中目標編譯版本APP-ABI:= armeabi-v7a


    結果:版本匹配,NDK編譯正常,armeabi-v7aarmeabi互相相容,app執行正常