編輯:關於Android編程
在研究了幾天JNI後,在自己生成的so庫中調用第三方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方法。這種方法更加靈活。
一、下圖是我的項目JniDemo目錄:
導入的兩個第三方庫是:libhello.so、libhello-jni.so
自己從源文件myhello.c編譯生成的庫是:libmyhello.so
java中調用native方法代碼如下:
package com.example.jnidemo; 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(); }
二、源文件的編寫
在使用javah生成頭文件後,要在源文件中使用include“xxxx.h”引入頭文件。如果頭文件不在jni根目錄下,還要在Android.mk中使用
LOCAL_C_INCLUDES:=(相對於jni目錄的)包含頭文件的目錄路徑
來聲明一下,否則報錯找不到頭文件。
然後實現自己聲明的native方法,再在其中調用第三方庫的函數。
具體代碼如下:
#include#include #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為例,如下:
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運行正常
6. 第三方a.so庫版本arm64-v8a,Application.mk中目標編譯版本APP-ABI:= arm64-v8a
報錯:erroradding symbols: File in wrong format
原因:目標編譯版本高於b.so,所以在解決32位的b.so的依賴時報錯,但64位的a.so編譯正常。
總結:
調用第三方so庫時要先查看文件ABI版本,根據32或64位的相應ABI版本去定義Application.mk中目標編譯版本APP-ABI。當然最好都是同一種版本,避免出現不識別、不兼容。
五、運行APP
補充:
我的demo是可以直接運行並調用第三方so庫的,但在實際項目中還是遇到了loadLibrary()找不到so庫的問題,報了下面的異常:
Couldn't load CloudService from loader dalvik.system.PathClassLoader[
DexPathList[[zip file "/data/app/com.example.demo-1.apk"],
nativeLibraryDirectories=[/data/app-lib/com.example.cameraframedatademo-1,
/vendor/lib,
/system/lib]]]: findLibrary returned null
解決方法: 既然是從DexPathList、nativeLibraryDirectories路徑中找不到so庫文件,那就手動push到對應目錄下。我這裡是把所有第三方庫都adb push 到了設備的system/lib
目錄下(如果需要且支持64位so庫,請push到system/lib64目錄下),這樣項目調用最終會在system/lib目錄下找到so庫(由於push到了系統文件夾下,其他應用也可以調用喽)。
本文將介紹Android中Resource Types的drawable、menu、layout。如需訪問官方原文,您可以點擊這些鏈接:《Drawable Resourc
近來項目有個需求,要做個和QQ空間類似的菜單欄透明度漸變和下拉刷新帶有阻尼回彈的效果。於是花點時間動手試了試,基本上達到了QQ空間的效果,截圖如下:通過觀察QQ空間的運行
一、簡述 最近項目組打算引入weex,並選定了一個頁面進行試水。頁面很簡單,主要是獲取數據渲染頁面,並可以跳轉到指定的頁面。跟之前使用RN 相比,weex 確實要簡單很
前言:經常會看到有一些app的banner界面可以實現循環播放多個廣告圖片和手動滑動循環。本以為單純的ViewPager就可以實現這些功能。但是蛋疼的事情來了