終極篇 C++算法到安卓的移植——AS調用VS的so庫

目的:windows平臺下的c++算法,需要移植到安卓系統上。平時用慣了Visual Studio,再在其他軟件上重新寫算法,調試算法,實在頭疼。所以我用VS的c++移動開發功能創建動態共享庫,將算法內容放入,並實現JNI和JAVA接口部分;最後用Android Studio調用成功。

 

吐槽微軟的仿真器以及VS自帶的google emulator for android,搞了很久,還是有問題,不能直接用(本着放在一起調試方便,竟然沒搞出來。如果有朋友在這一塊調試好了,記得發文章,還是很期待的),索性重點不在這裏,乾脆放棄,使用Android Studio做測試。(本來算法功能測試已經在windows平臺測試的差不多了)

 

吐槽開始。。。

三週了,從未接觸java,android,對於一個C++死忠粉各種沒信心,只能各種查資料,找度娘,找論壇,都以爲沒戲了,終於給我搞成功了!!

在此特別感謝CSDN的Mr_L_Y,他對我的幫助無以言謝。這位大神貢獻的資料可以查看:

VS2019 C++的跨平臺開發——Android .so開發

https://blog.csdn.net/luoyu510183/article/details/94590497

VS2019 OpenCV的Windows工程到安卓的移植

https://blog.csdn.net/luoyu510183/article/details/102710080

 

其實有這兩篇文章足以移植VS中創建的C++移動開發的SO庫,但是想想這麼久的辛苦,還是記錄一下自己的成果。

 

本文使用的軟件版本如下:

  • VisualStudio 2019 (創建c++移動開發的動態庫)
  • AndroidStudio 3.5 (調用VS創建的動態庫,並在模擬器中顯示結果)
  • NDK-r16B
  • Android SDK 21
  • Opencv 4.1.1(android)

 

 

第一部分:創建c++算法的so庫,供安卓調用

1. 軟件準備:安裝Visual Studio中的“使用C++的移動開發”,不需要在可選項中選擇模擬器

2. 打開VS,新建項目,選擇”動態共享庫(Android)“,命名爲SharedObject

3. 配置opencv

由於算法中使用了opencv,具體配置可以參考文章

Visual Studio + android + opencv 跨平臺生成動態庫文件https://blog.csdn.net/Merria28/article/details/102517646

在這裏特別講一下配置的問題,java不需要區分debug和release,所以在附加依賴項或者庫依賴項中的所有配置是一樣的。需要注意的是,opencv的第三方依賴庫x86_64和x86中比arm64和arm的庫文件少一個libtegra_hal.a,配置的時候不要添加就可以了。

附加庫目錄需要指定到配置文件夾:

OpenCV-android-sdk\sdk\native\3rdparty\libs\armeabi-v7a

參考MR_L_Y的文章,使用了$(PlatformShortName)代替了具體的每種配置,但是我的編譯不過,就自己手動改成具體的配置內容了。Visual Studio中的ARM(對應安卓下的armeabi-v7a文件夾下的lib),ARM64(對應arm64-v8a),x86(對應x86),x64(對應x86_64)。

 

4. 添加自己的任意算法庫頭文件和源文件到項目中

我這裏的頭文件OpenCVFunc.h內容如下:

#pragma once
float TestOpencv(float* buf, int len);
float TestMath();

源文件內容如下:

#include "OpenCVFunc.h"

#include <math.h>
#include <opencv2/opencv.hpp>

float TestOpencv(float* buf, int len)
{
	cv::Mat mat = cv::Mat(len, 1, CV_32FC1, buf);
	auto sum = cv::sum(mat);
	return sum.val[0];
}

float TestMath()
{
	return sqrt(2.0f);
}

 

5. 導出上面頭文件中的函數

在項目默認生成中的SharedObject19.cpp文件中添加,完整代碼如下:

#include "SharedObject19.h"


#define LOGI(...) ((void)__android_log_print(ANDROID_LOG_INFO, "SharedObject19", __VA_ARGS__))
#define LOGW(...) ((void)__android_log_print(ANDROID_LOG_WARN, "SharedObject19", __VA_ARGS__))

extern float TestOpencv(float* buf, int len);
extern float TestMath();

extern "C" {
	
	float ExternTestOpencv(float* buf, int len) //這個用來導出給Android JNI使用
	{
		return TestOpencv(buf, len);
	}

	float ExternTestMath()//這個用來導出給Android JNI使用
	{
		return TestMath();
	}

	//C++導出給Java類使用的命名規範
	//Java_packagename_classname_functionname
	//第一個傳參總是JNIEnv* env
	//第二個傳參 如果是static成員函數就是jclass type,
	//		    如果是非static成員函數就是jobject thiz,
	//第三個傳參纔是真正的參數
	JNIEXPORT jfloat JNICALL
		Java_com_jniexample_JNIInterface_CVTestSum(JNIEnv* env, jclass type, jfloatArray buf) //這個用來導出給Java使用
	{
		auto len = env->GetArrayLength(buf);
		jboolean notcopy = JNI_FALSE;
		float* fptr = env->GetFloatArrayElements(buf, &notcopy);//從Java內存轉換到native指針
		return TestOpencv(fptr, len);
	}
	JNIEXPORT jfloat JNICALL
		Java_com_jniexample_JNIInterface_TestSum(JNIEnv* env, jclass type, jfloatArray buf)//這個用來導出給Java使用
	{
		auto len = env->GetArrayLength(buf);
		jboolean notcopy = JNI_FALSE;
		float* fptr = env->GetFloatArrayElements(buf, &notcopy);
		float sum = 0;
		for (size_t i = 0; i < len; i++)
		{
			sum += fptr[i];
		}
		return sum;
	}
	JNIEXPORT jfloat JNICALL
		Java_com_jniexample_JNIInterface_TestMath(JNIEnv* env, jclass type)//這個用來導出給Java使用
	{
		return TestMath();
	}


	//此簡單函數返回平臺 ABI,此動態本地庫爲此平臺 ABI 進行編譯。
	const char * SharedObject19::getPlatformABI()
	{
	#if defined(__arm__)
	#if defined(__ARM_ARCH_7A__)
	#if defined(__ARM_NEON__)
		#define ABI "armeabi-v7a/NEON"
	#else
		#define ABI "armeabi-v7a"
	#endif
	#else
		#define ABI "armeabi"
	#endif
	#elif defined(__i386__)
		#define ABI "x86"
	#else
		#define ABI "unknown"
	#endif
		LOGI("This dynamic shared library is compiled with ABI: %s", ABI);
		return "This native library is compiled with ABI: %s" ABI ".";
	}

	void SharedObject19()
	{

	}

	SharedObject19::SharedObject19()
	{
	}

	SharedObject19::~SharedObject19()
	{
	}


}

需要提示的是,我們算法庫只有兩個算子TestOpencv和TestMath,但是在導出部分我卻編寫了4個算子用於外部導出,他們分別是:

float ExternTestOpencv(float* buf, int len)

float ExternTestMath()

JNIEXPORT jfloat JNICALL

Java_com_jniexample_JNIInterface_CVTestSum(JNIEnv* env, jclass type, jfloatArray buf)

JNIEXPORT jfloat JNICALL

Java_com_jniexample_JNIInterface_TestMath(JNIEnv* env, jclass type)

其中有個函數:

JNIEXPORT jfloat JNICALL

Java_com_jniexample_JNIInterface_TestSum(JNIEnv* env, jclass type, jfloatArray buf)

這個函數是用來檢測Java_com_jniexample_JNIInterface_CVTestSum結果是否正常的,實現方式不一樣而已。正常情況下是不需要的。

上面這四個函數需要特別說明一下,前兩個是用來導出給Android JNI使用的,後兩個是用來導出給Java使用的。具體使用的位置,在第二大部分會詳細介紹。

 

6. 編譯生成so文件

安卓在調用的時候最好提供全部配置的庫文件。Visual Studio中的ARM(對應安卓下的armeabi-v7a文件夾下的lib),ARM64(對應arm64-v8a),x86(對應x86),x64(對應x86_64)。

這裏我使用了x86進行測試,其他配置的庫文件先不管。

還有一個小問題,Mr_L_Y大牛在他的文章“”中最後提示部分的第四點提到 “在使用Opencv4.1.1的安卓native sdk後,如果項目屬性裏選擇的是 llvm-libc++ static,那麼會出現編譯錯誤,undefined reference to `strtof_l'. 具體原因我也不清楚,但是由於Opencv使用libc++_shared,所以使用static本身也不合理。” 我這裏發現ARM64和x64下使用llvm-libc++ static編譯通不過,都會提示undefined reference to `strtof_l'這個錯誤,我在具體應用的時候改成了libc++_shared,就會編譯通過。

到這裏,so文件生成就結束了。

 

第二部分,使用Android Studio調用VS2019生成的動態共享庫

開始之前,先放一下需要修改的文件,內容不多,需要注意細節:

1. 打開android studio 創建新項目,選擇Native C++,語言選擇Java,其他隨意。我這裏創建的項目名稱爲NativeCplusplus

2. 將算法so庫導入到安卓項目中——libSharedObject19.so放入當前項目

放入位置app/libs/x86/libSharedObject19.so 以及app/libs/x86/libopencv_java4.so

由於安卓模擬器默認用的是x86的,所以使用x86庫文件;其他配置的庫文件如果要放,每個配置文件夾下都必須有着兩個so文件,否則會編譯報錯。可以選擇放幾個配置文件夾:armeabi-v7a, arm64-v8a, x86, x86_64app/build.gradle文件

3. 修改app/build.gradle文件

 

4. 創建java類,這裏面用到的函數對應第一部分中SharedObject.cpp中的類似如下形式的函數:

JNIEXPORT jfloat JNICALL

Java_com_jniexample_JNIInterface_CVTestSum(JNIEnv* env, jclass type, jfloatArray buf)

添加的java類函數直接可以在MainActivity.java中調用並顯示結果。

先將Android改爲Project,在app/src/java文件夾上右擊,NEW-JavaClass

然後實現JNIInterface.java的內容:

5. 修改CmakeLists.txt

這部分修改的內容主要針對的是導出的供JNI使用的函數,對應第一部分中SharedObject.cpp中的類似如下形式的函數:float ExternTestOpencv(float* buf, int len)。修改cmake文件後,這部分函數就可以在native-lib.cpp中使用了。然後才能在MainActivity.java中使用並顯示結果。

6. 修改native-lib.cpp文件

可以使用so中導出的供JNI使用的函數,即第5步講到的float ExternTestOpencv(float* buf, int len)這種函數。

 

7. 在顯示結果之前,需要添加顯示的方式和位置。

我們通過文本和按鈕的方式在文件app\src\main\res\layout\activity_main.xml中實現。雙擊打開該文件,添加文本和按鈕。

 

8. 在MainActivity.java中調用java函數,即調用native-lib.cpp和JNIInterface.java中的函數。這部分內容是通過上一步創建的UI界面顯示的。

至此,代碼和顯示設計都完成了。下一步編譯運行。

 

 

9. 編譯apk:Build-->Build Bundle(s) / APK(s)-->Build APK(s)

10. 分析apk:Build-->Analyze APK...

 

11. 點擊運行按鈕,在模擬器上運行。(也可以選擇在安卓設備上運行)

這裏我沒有設備,只能在模擬器上運行,第一次使用需要創建一個模擬器,點擊菜單欄上的AVD Manager圖標,如下圖所示。選擇左下角的Create Virtual Device。一切按默認或者推薦選擇設置即可。需要注意的是,x86的模擬器比arm的模擬器快很多,儘量選x86的。(所以我想用vs_emulator.exe,據說該模擬器sudo更快。調試更方便。)

設置好虛擬設備之後,可以點擊右側的綠色按鈕運行一下效果。體驗之後你就懂了。。。

這時就可以點擊菜單欄上的運行按鈕,查看自己的運行效果了。

這是我的效果:

 

最後,我要引用Mr_L_Y的警示,因爲不注意就會入坑:

1.AndroidStudio中的虛擬機默認是使用的x86的安卓系統,所以應該用x86編譯下的.so文件。

 

2.apk安裝後一運行就提示xxx已停止工作,就是安卓裏面的崩潰,一般情況下是.so找不到,需要使用logcat自己排查問題。

 

3.apk點擊那個按鍵後xxx已停止工作,崩潰在xxxxx函數沒有實現,一般錯誤是那兩個導出給安卓的函數名不正確,認真檢查。

 

4.在使用Opencv4.1.1的安卓native sdk後,如果項目屬性裏選擇的是 llvm-libc++ static,那麼會出現編譯錯誤,undefined reference to `strtof_l'. 具體原因我也不清楚,但是由於Opencv使用libc++_shared,所以使用static本身也不合理。

 

5.在進行大項目移植時,請先建立最小的opencv項目測試成功後再開始。

 

6.一定要會使用logcat

 

7.事已至此,請靜下來學習一點Java和Android的開發知識,不要什麼都直接去百度,最後拼湊出一個剛好能使用的項目。

 

安卓的官方文檔:https://developer.android.com/studio/projects/add-native-code.html

https://developer.android.com/ndk/guides

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章