目的: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的移植方法;如果是自己写的,源码结构比较简单的直接参考我这里的方法会更方便一点。)
本文使用的软件版本如下:
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
#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文件生成就结束了。
开始之前,先放一下需要修改的文件,内容不多,需要注意细节:
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