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
一、下圖是我的專案JniDemo目錄:
匯入的兩個第三方庫是:libhello.so、libhello-jni.so
自己從原始檔myhello.c編譯生成的庫是:libmyhello.so
java中呼叫native方法程式碼如下:
[java] view plain copy print ?
- package
- import android.app.Activity;
- import android.os.Bundle;
- import android.view.Menu;
- import android.view.MenuItem;
- import android.widget.TextView;
- public class DemoMain extends Activity {
- static {
- System.loadLibrary("myhello");
- System.loadLibrary("hello");
- System.loadLibrary("hello-jni");
- }
- TextView textView;
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_demo_main);
- textView = (TextView) findViewById(R.id.text);
- String str = getString() + "." + getJNIString();
- textView.setText(str);
- }
- public native String getString();
- public native String getJNIString();
- }
佈局就一個TextView元件,不再介紹。
二、原始檔的編寫
在使用javah生成標頭檔案後,要在原始檔中使用include“xxxx.h”引入標頭檔案。如果標頭檔案不在jni根目錄下,還要在Android.mk中使用
LOCAL_C_INCLUDES:=(相對於jni目錄的)包含標頭檔案的目錄路徑
來宣告一下,否則報錯找不到標頭檔案。
然後實現自己宣告的native方法,再在其中呼叫第三方庫的函式。
具體程式碼如下:
[cpp] view plain copy print ?
- #include <string.h>
- #include <jni.h>
- #include "com_example_jnidemo_DemoMain.h"
- #include "com_hello_hello_HelloActivity.h"
- #include "com_example_hellojni_HelloJni.h"
- jstring Java_com_example_jnidemo_DemoMain_getString(JNIEnv* env, jobject thiz) {
- return Java_com_hello_hello_HelloActivity_sayHello(env, thiz);//呼叫libhello.so中的函式
- }
- jstring Java_com_example_jnidemo_DemoMain_getJNIString(JNIEnv* env,
- jobject thiz) {
- return Java_com_example_hellojni_HelloJni_stringFromJNI(env, thiz);}//呼叫libhello-jni.so中的函式
三、android.mk,Application.mk配置
Android.mk用來配置各個模組如何編譯,如下:
- LOCAL_PATH := $(call my-dir) #my-dir就是該Android.mk所在目錄,本專案中即jni目錄
- include $(CLEAR_VARS) #清楚此行之前除了LOCAL_PATH外所有的變數,因為定義的多個模組中會有相同名稱的變數,目的是避免變數賦值衝突
- LOCAL_MODULE := hello-jni #指定一個當前模組名
- LOCAL_SRC_FILES := libhello-jni.so #要編譯的原始檔
- include $(PREBUILT_SHARED_LIBRARY) #編譯目標,PREBUILT_表示已經編譯好的,在使用NDK編譯時不會再次編譯,而是直接拷貝到libs目錄
- #預編譯.a靜態庫使用 PREBUILT_STATIC_LIBRARY
- include $(CLEAR_VARS)
- LOCAL_MODULE := hello
- LOCAL_SRC_FILES := libhello.so
- include $(PREBUILT_SHARED_LIBRARY)
- include $(CLEAR_VARS)
- LOCAL_MODULE := myhello #自己由原始檔編譯庫的模組名
- LOCAL_SRC_FILES := myhello.c #將被編譯的原始檔
- #【重要關鍵點】引入依賴的第三方so庫(使用模組名引入),使用\可以引入多個(注意:\符號後沒有空格或其他字元)
- LOCAL_SHARED_LIBRARIES := \
- hello-jni\
- hello
- include $(BUILD_SHARED_LIBRARY) #表示編譯成.so共享庫,即動態庫
Application.mk用來配置目標編譯ABI(應用二進位制介面),如arm64-v8a、armeabi、armeabi-v7a、mips、mips64、x86、x86_64。以armeabi-v7a為例,如下:
[plain] view plain copy print ?
- 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_LIBRARY的so庫,但不被連結還是找不到庫中API的。
解決方法:獲取so庫時最好要取得相應版本的庫(armeabi-v7a與armeabi都是32位,一般情況下應該互相相容,但不相容64位的arm64-v8a)。
(2)報錯 error adding symbols:File in wrong format
collect2:error: ld returned 1 exit status
(ld是連結操作)
錯誤原因:如果Application.mk中APP-ABI:=的目標編譯平臺版本為64位,而實際匯入的so庫版本是32位,就會不識別該so庫(wrong format)。
解決方法:在Application.mk(如果沒有,建立)中,把APP-ABI:=xxx的目標編譯版本降低點,如armeabi-v7a、armeabi這些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-v8a,Application.mk中目標編譯版本APP-ABI:= armeabi-v7a
報錯:Fileformat not recognized
原因:目標編譯版本是32位,比第三方64位的a.so庫低,識別不了64位a.so庫。
3.第三方a.so庫版本armeabi-v7a,Application.mk中目標編譯版本APP-ABI:= arm64-v8a
報錯:erroradding symbols: File in wrong format
原因:目標編譯版本是64位比32位的第三方so庫高,a.so或b.so被認為檔案格式錯誤。
4. 第三方a.so庫版本armeabi-v7a,Application.mk中目標編譯版本APP-ABI:= armeabi-v7a
結果:版本匹配,NDK編譯正常,armeabi-v7a相容armeabi版本的b.so,app執行正常
5. 第三方a.so庫版本armeabi,Application.mk中目標編譯版本APP-ABI:= armeabi-v7a
結果:版本匹配,NDK編譯正常,armeabi-v7a與armeabi互相相容,app執行正常