终极篇 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库,但是想想这么久的辛苦,还是记录一下自己的成果。

(这里插一句,如果是整个大项目的移植,比如团队项目,直接参考Mr_L_Y的移植方法;如果是自己写的,源码结构比较简单的直接参考我这里的方法会更方便一点。)

 

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

  • 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

终极篇 C++算法到安卓的移植——AS调用VS的so库_第1张图片

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 
#include 

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, ¬copy);//从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, ¬copy);
		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

终极篇 C++算法到安卓的移植——AS调用VS的so库_第2张图片

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文件

终极篇 C++算法到安卓的移植——AS调用VS的so库_第3张图片

 

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

终极篇 C++算法到安卓的移植——AS调用VS的so库_第4张图片

然后实现JNIInterface.java的内容:

终极篇 C++算法到安卓的移植——AS调用VS的so库_第5张图片

5. 修改CmakeLists.txt

这部分修改的内容主要针对的是导出的供JNI使用的函数,对应第一部分中SharedObject.cpp中的类似如下形式的函数:float ExternTestOpencv(float* buf, int len)。修改cmake文件后,这部分函数就可以在native-lib.cpp中使用了。然后才能在MainActivity.java中使用并显示结果。

终极篇 C++算法到安卓的移植——AS调用VS的so库_第6张图片

6. 修改native-lib.cpp文件

可以使用so中导出的供JNI使用的函数,即第5步讲到的float ExternTestOpencv(float* buf, int len)这种函数。

终极篇 C++算法到安卓的移植——AS调用VS的so库_第7张图片

 

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

我们通过文本和按钮的方式在文件app\src\main\res\layout\activity_main.xml中实现。双击打开该文件,添加文本和按钮。

终极篇 C++算法到安卓的移植——AS调用VS的so库_第8张图片

 

8. 在MainActivity.java中调用java函数,即调用native-lib.cpp和JNIInterface.java中的函数。这部分内容是通过上一步创建的UI界面显示的。

终极篇 C++算法到安卓的移植——AS调用VS的so库_第9张图片

终极篇 C++算法到安卓的移植——AS调用VS的so库_第10张图片

至此,代码和显示设计都完成了。下一步编译运行。

 

 

9. 编译apk:Build-->Build Bundle(s) / APK(s)-->Build APK(s)

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

终极篇 C++算法到安卓的移植——AS调用VS的so库_第11张图片

 

11. 点击运行按钮,在模拟器上运行。(也可以选择在安卓设备上运行)

这里我没有设备,只能在模拟器上运行,第一次使用需要创建一个模拟器,点击菜单栏上的AVD Manager图标,如下图所示。选择左下角的Create Virtual Device。一切按默认或者推荐选择设置即可。需要注意的是,x86的模拟器比arm的模拟器快很多,尽量选x86的。(所以我想用vs_emulator.exe,据说该模拟器sudo更快。调试更方便。)

设置好虚拟设备之后,可以点击右侧的绿色按钮运行一下效果。体验之后你就懂了。。。

这时就可以点击菜单栏上的运行按钮,查看自己的运行效果了。

终极篇 C++算法到安卓的移植——AS调用VS的so库_第12张图片

这是我的效果:

终极篇 C++算法到安卓的移植——AS调用VS的so库_第13张图片

 

最后,我要引用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

 

 

你可能感兴趣的:(android,算法移植)