背景
現在越來越多應用包含一些第三方C/C++算法庫, 比如圖像處理, 人臉檢測, 語音識別等等. 第三方提供的算法庫都是C/C++動態(tài)庫(.so), 不同的提供商提供的接口存在差異, 主要分為以下兩種:
- 提供Java接口和so庫
這種類型調用很簡單, 把so庫放到打包到apk或者Android系統中, 通過Java接口調用即可, JNI部分代碼提供商都寫好了.
- 只提供C/C++接口和so庫
此類型調用稍微麻煩些, 需要自己寫編譯規(guī)則和JNI代碼
由于一般算法庫既可能集成在App中, 也有可能集成在Android系統中, 所以大部分算法提供商都是只提供C/C++接口, 這樣就更省事, 只是在App集成稍微麻煩些,下面就講一下這兩種方式如何集成和調用.
App中調用C/C++算法庫
由于提供的算法庫中并沒有JNI部分代碼, 所以我們需要自己寫JNI代碼, 并在native方法中調用算法提供的接口, 最終會產生兩個so庫, 一個包含我們的native代碼, 另一個就是算法庫, 集成步驟如下:
1.編寫JNI代碼
這部分可參看資料很多, 本文不做介紹,直接略過,可參考我之前寫的文章:Android JNI 函數注冊的兩種方式(靜態(tài)注冊/動態(tài)注冊)
2.編寫Android.mk和Application.mk
so庫是通過NDK編譯產生的, 我們需要編寫編譯規(guī)則,其中主要注意的有兩點, 1. 算法商提供的so是以預置的方式(prebuild)進行編譯的. 2.我們寫的native代碼也是編譯成so文件, 并且要依賴于算法so庫.
假設算法提供的so庫名字為libalgo.so
, 提供的頭文件為algo.h
, 可供調用的方法為 const char* getVersion()
, 我們寫的代碼在test.cpp
中 , jni目錄結構如下:
jni
│─Android.mk
│─Application.mk
│─test.cpp
│
├─include
│── algo.h
│
├─lib
│── libalgo.so
Android.mk代碼如下:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := algo
LOCAL_SRC_FILES := lib/libalgo.so
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
#預編譯算法庫
include $(PREBUILT_SHARED_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := test
LOCAL_SRC_FILES := test.cpp
#依賴的算法庫
LOCAL_SHARED_LIBRARIES := libalgo
#編譯為動態(tài)庫
include $(BUILD_SHARED_LIBRARY)
Application.mk
#編譯arm架構32位so庫, 64位為 arm64-v8a
APP_ABI := armeabi-v7a
APP_PLATFORM := android-14
test.cpp部分代碼如下:
#include "jni.h"
#include "algo.h"
int test(JNIEnv *env,jclass obj)
{
const char* algoVersion = getVersion();
//其他代碼....
return 0;
}
3. 編譯運行
在jni目錄下執(zhí)行ndk-build即可生成兩個so庫, 分別為libtest.so和libalgo.so, 將so打包到apk并在java代碼中加載so庫
System.loadLibrary("test");
加載后調用對應java native方法即可.
注: NDK編譯時候LOCAL_MODULE
的名字不需要帶lib前綴, NDK會自動補上, 同時Java 加載時也不需要寫lib前綴,并且我們只需加載libtest.so, 當加載libtest.so時, 會自動加載libalgo.so
Android系統中通過C/C++調用算法庫
對于手機廠商(ODM/OEM)來說, 集成算法都是在直接集成到Android系統中, 這樣效率會比較高, 集成的位置一般是HAL層, 比如Camera美顏, 降噪等算法基本都集成在Camera HAL層.Camera HAL層都是C/C++代碼, 直接調用相應算法庫即可, 但由于Android系統中編譯方式是由Android.mk來組成, 并且和NDK中的Android.mk有差異, 因此主要不同點在于mk文件的編寫, 步驟如下:
同樣假設算法商提供的so庫名字為libalgo.so
, 提供的頭文件為algo.h
, 可供調用的方法為 const char* getVersion();
1. 在Android源碼中預編譯算法庫
比如我經常在高通平臺HAL層集成Camera相關算法, 則在 hardware/qcom/camera/QCamera2/HAL/ 文件夾下新建目錄, 用于存放算法庫和頭文件,以及Android.mk, 假設目錄為 hardware/qcom/camera/QCamera2/HAL/algo/
目錄內容如下:
algo
│─Android.mk
│
├─include
│── algo.h
│
├─lib
│── libalgo.so
Android.mk內容
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
#默認不會加上lib前綴
LOCAL_MODULE := libalgo
#不管是release還是debug版本, 都編譯這個模塊
LOCAL_MODULE_TAGS := optional
#so文件路徑以及名字
LOCAL_SRC_FILES := lib/libalgo.so
LOCAL_MODULE_STEM := $(LOCAL_MODULE)
#需要指定文件后綴
LOCAL_MODULE_SUFFIX := $(suffix $(LOCAL_SRC_FILES))
#編譯32位算法庫
LOCAL_MULTILIB := 32
#表明預編譯的是動態(tài)庫
LOCAL_MODULE_CLASS := SHARED_LIBRARIES
include $(BUILD_PREBUILT)
可以看到源碼編譯和NDK編譯Android.mk的內容有較大差異, 主要表現在如下方面:
-
LOCAL_MODULE
默認不會加上lib前綴 - 要指定文件后綴
- 要說明編譯的文件類型(動態(tài)庫, 靜態(tài)庫,apk等等)
- 預編譯使用
BUILD_PREBUILT
而不是PREBUILT_SHARED_LIBRARY
2. 在調用的模塊Android.mk中加入依賴庫文件
預編譯完成后, 要告訴調用模塊, 需要依賴這個so庫和頭文件, 同樣以在Camera HAL層調用為例,
修改 hardware/qcom/camera/QCamera2/HAL/Android.mk
, 修改內容如下:
#部分代碼省略
LOCAL_C_INCLUDES := \
$(LOCAL_PATH)/../util \
$(LOCAL_PATH)/wrapper
# 引入調用算法庫的頭文件
LOCAL_C_INCLUDES += \
$(LOCAL_PATH)/algo/include
#部分代碼省略
LOCAL_SHARED_LIBRARIES += libqdMetaData libqservice libbinder
# 表明當前HAL模塊依賴我們要調用的算法庫
LOCAL_SHARED_LIBRARIES += libalgo
#部分代碼省略
#在最末尾出引入我們預編譯的Android.mk, 不然默認系統不會執(zhí)行我們寫的預編譯用的Android.mk
include $(LOCAL_PATH)/algo/Android.mk
3. 調用算法庫
調用算法庫方法和App中一樣, 引入頭文件調用對應函數即可. 比如在 QCamera2HWI.cpp 中進行調用的話, 修改如下:
// 部分代碼省略...
#include <binder/Parcel.h>
#include <binder/IServiceManager.h>
#include <utils/RefBase.h>
#include <QServiceUtils.h>
#include "QCamera2HWI.h"
#include "QCameraMem.h"
// 引入算法庫對應的頭文件
#include "algo.h"
// 在startPreview函數中進行調用
int QCamera2HardwareInterface::startPreview()
{
ATRACE_CALL();
int32_t rc = NO_ERROR;
// 部分代碼省略...
// 調用
const char* algoVersion = getVersion();
}
3. 編譯運行
完成上述內容后, 直接編譯系統刷機運行即可, 也可以模塊編譯, 即上面的例子中, 可以使用如下命令編譯(前提是之前已經全部編譯過系統了): mmm hardware/qcom/camera/QCamera2/HAL/
執(zhí)行完成后, 有兩個so庫需要push到手機對應位置 camera.xxx.so
和 libalgo.so
, camera.xxx.so
是Camera HAL層編譯完生成的so, xxx代表使用的平臺比如 msm8953, 另一個就是我們集成的算法庫, push后需重啟手機生效.
如果編譯的時候出現一些找不到函數的一些錯誤, 可以看看這篇文章: 一些算法集成遇到的問題
Android.mk注意事項
集成過程中免不了要寫寫mk代碼, Android.mk編寫過程中有些坑, 在此給大家說下我遇到過的坑.
-
include其他Android.mk時, include語句要放在mk文件末尾
編寫Android.mk文件經常需要引入其他目錄或者子目錄mk文件, 一般做法有兩種:
# include指定目錄mk include $(LOCAL_PATH)/Denoise/Android.mk # include 當前文件夾子目錄下的mk include $(call all-subdir-makefiles)
在mk中使用這兩個mk語句都需要放在文件夾末尾,因為mk中的變量是全局的,如果放在文件中間,會改變當前的一些變量值,比如
LOCAL_PATH
值變?yōu)槟鉯nclude的目錄了,這樣目錄就會出錯,導致編譯失敗. -
include $(call all-subdir-makefiles)
只會引入當前目錄下一級子目錄的mk, 更深層次mk不會被引入這個是系統定義的函數, 默認只會引入當前目錄下一級的Android.mk, 更深層次的mk需要自己手動引入, 或者每個目錄都放一個mk, 其中只寫
include $(call all-subdir-makefiles)
這個語句. 對于在已經賦值過的變量, 追加值使用 +=, := 會覆蓋之前的值
-
使用
#
注釋代碼時,如果使用了\
, 會把所有代碼都注釋掉,而不是注釋一行下面的
#
注釋會將所有變量都注釋掉,而不是注釋QCamera2Factory.cpp這一行.
LOCAL_SRC_FILES := \
# QCamera2Factory.cpp \
QCamera2Hal.cpp \
QCamera2HWI.cpp \
QCameraMem.cpp \
總結
- NKD和Android源碼中的Android.mk(makefile)的語法有差異, NDK編譯只是針對C/C++代碼,而Android源碼中的Android.mk是用來編譯整個系統的.
- NDK編譯的mk中,
LOCAL_MODULE
會自動加上前綴lib, 預編譯使用PREBUILT_SHARED_LIBRARY
, Android系統中LOCAL_MODULE
不會被添加前綴, 預編譯使用BUILD_PREBUILT
, 且要指定后綴名, 編譯文件的類型 - 兩種編譯方式都需使用
LOCAL_SHARED_LIBRARIES
來表明依賴的so庫, 并且加載動態(tài)庫時會自動加載對應依賴的動態(tài)庫.