本篇是应一个读者的请求,而且这种需求应该还是蛮多的:刚入职或者刚进实验室的新人,接手一套C++算法工程,现在老板让你移植到Android上。全部用Java重写,当然是不现实的。本文将介绍一种尽可能简单的移植方法。
本文使用的软件版本如下:
首先请参考 VS2017的C++开发心得(八)DLL动态链接——Opencv的使用建立一个简单的OpenCV VS解决方案,如下:
一个简单的cv::sum的使用。
所谓的移植,就是移除过去OpenCV对于Windows底层的依赖。
一听到Windows的底层依赖,你是不是感觉自己都是用的C++的标准库在编程,没有用到任何Windows的接口,应该不会有任何依赖。举个一个简单例子,比如你的项目在使用Opencvxxx.dll的时候,会用到LoadLibrary这个函数,而这个函数就是由Windows的user32.dll实现的。
在有全部源码的情况下可以使用安卓的NDK进行重新编译源码,然后要把所有使用的第三方库文件.dll和.lib,替换为安卓端的.so和.a。
第一种老实的做法,在步骤1.中建立的安卓项目下面把所有的.cpp和.h文件按照原来的项目结构拷贝过去。
第二种取巧的做法,直接修改“Opencv411Template.vcxproj”和“Opencv411Template.sln”为安卓项目。
(第三种不推荐的做法,在AndroidStudio的JNI中导入所有的.cpp和.h进行编译,用AndroidStudio开发C++实在不推荐)
这里简单介绍第二种做法,尝试之前请备份整个项目。
首先用文本工具打开“Opencv411Template.vcxproj”(准备的Opencv Windows项目)和“SharedObject7.vcxproj”(步骤1.中建立的安卓项目)。
先看下Windows的项目文件:
上图红色框内的就是需要进行编译的文件。除了这部分其他全部替换为“SharedObject7.vcxproj”(下图)中的内容就行,是不是很简单:
把两个红框标注的内容合并起来,记得还要把 "SharedObject7.cpp" "SharedObject7.h""pch.h" 这三个文件拷贝到Opencv411Template的对应目录下,合并结果如下:
接下来修改.sln文件,用右方的红框内的内容替换到左边:
重新打开Opencv411Template项目看看:
已经变成安卓项目,接下来的工作就是替换Windows工程的Opencv4.1.1为安卓的opencv-4.1.1-android-sdk。这里和Windows的主要区别在于加载的库不一样。
先简单说下Android库的头文件目录和库文件目录:
头文件位于:\opencv-4.1.1-android-sdk\OpenCV-android-sdk\sdk\native\jni\include
库文件位于:
\opencv-4.1.1-android-sdk\OpenCV-android-sdk\sdk\native\3rdparty\libs
\opencv-4.1.1-android-sdk\OpenCV-android-sdk\sdk\native\libs
\opencv-4.1.1-android-sdk\OpenCV-android-sdk\sdk\native\staticlibs
头文件很简单直接添加目录就行,没有变化。
关键是库文件,Windows工程只链接一个dll(或者静态编译几个.lib)。这里先必须链接一个libopencv_java4.so,其次根据你的需求要添加多个静态库进行编译。静态库的链接,懂的自然懂,还不懂的,就添加所有.a进去,多十几M的空间大家都还能接受。
下面是VS2019中安卓库的链接操作。首先把上面的三个路径添加到链接器的附加库目录里面,如下,注意红框的选择:
最后就是添加附加库:
由于Opencv安卓的库名没有Debug和Release的区别,也没有平台区别,而是用文件夹名称来区分库。这是比较好的命名方式,方便使用宏定义路径。
完整的库列表:
-lopencv_java4
-lopencv_calib3d
-lopencv_core
-lopencv_dnn
-lopencv_features2d
-lopencv_flann
-lopencv_highgui
-lopencv_imgcodecs
-lopencv_imgproc
-lopencv_ml
-lopencv_objdetect
-lopencv_photo
-lopencv_stitching
-lopencv_video
-lopencv_videoio
-lcpufeatures
-lIlmImf
-littnotify
-llibjasper
-llibjpeg-turbo
-llibpng
-llibprotobuf
-llibtiff
-llibwebp
-lquirc
-ltbb
-ltegra_hal
-lz
-ldl
-lm
-llog
然后信心满满的去编译,一堆错误:
不要怕,只是几个C++的设置而已。
再次编译项目:
成功了,注意从这里开始我都只示范Debug和ARM64配置下的编译.so的Android加载流程。
修改导出文件SharedObject7.cpp如下(这个文件用来调用之前Windows项目下的函数接口,然后通过两种方式导出给JNI和Java类使用):
#include "SharedObject7.h"
#define LOGI(...) ((void)__android_log_print(ANDROID_LOG_INFO, "SharedObject7", __VA_ARGS__))
#define LOGW(...) ((void)__android_log_print(ANDROID_LOG_WARN, "SharedObject7", __VA_ARGS__))
extern float TestOpencv(float* buf, int len); //假设这是过去Opencv工程的导出接口
extern "C" {
float ExternTestOpencv(float* buf, int len)//这个用来导出给Android JNI使用
{
return TestOpencv(buf,len);
}
//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;
}
/*此简单函数返回平台 ABI,此动态本地库为此平台 ABI 进行编译。*/
const char * SharedObject7::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 SharedObject7()
{
}
SharedObject7::SharedObject7()
{
}
SharedObject7::~SharedObject7()
{
}
}
至此,C++端的准备完成。
AS我也不是什么专家,就简单贴下步骤:
创建完成,切换到项目视图:
这个类用来对接.so中的两个导出函数:Java_com_jniexample_JNIInterface_CVTestSum和Java_com_jniexample_JNIInterface_TestSum。
所以,接下来就要创建这两个函数:
接着把生成.so放到工程下面:
请按照Opencv的文件夹结构来存放.so(我这里只演示了arm64-v8a的版本,其他架构文件名参考下图):
在JNI中导入即在AndroidStudio的.cpp文件中使用.so中的导出函数。
首先修改native-lib.cpp(自动生成的文件):
然后修改CMakeLists.txt(AS是使用的Cmake,所以多多少少要会点Cmake的语法):
以上两步就是jni对外部的.so库的使用和链接。
先双击activity_main.xml进行UI编辑:
增加两个button用于调用两个导出的Java函数,增加一个Textview用来显示计算结果:
修改UI的后端代码:
package cn.com.inxpar.nativeproject;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
import com.jniexample.JNIInterface;
import org.w3c.dom.Text;
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
// Used to load the 'native-lib' library on application startup.
static {
System.loadLibrary("native-lib");
System.loadLibrary("SharedObject7");
}
private TextView sumText;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Example of a call to a native method
TextView tv = findViewById(R.id.sample_text);
tv.setText(stringFromJNI());
sumText=findViewById(R.id.textView);
findViewById(R.id.button).setOnClickListener(this);
findViewById(R.id.button2).setOnClickListener(this);
}
float[] a=new float[]{1,2,2,3,3,4};
float sum=0;
@Override
public void onClick(View v) {
switch (v.getId())
{
case R.id.button:
sum= JNIInterface.CVTestSum(a);
sumText.setText("OpenCV Sum:"+Float.toString(sum));
break;
case R.id.button2:
sum= JNIInterface.TestSum(a);
sumText.setText("Raw Sum:"+Float.toString(sum));
break;
default:
break;
}
}
/**
* A native method that is implemented by the 'native-lib' native library,
* which is packaged with this application.
*/
public native String stringFromJNI();
}
编译apk,分析apk:
如果分析的apk里面有这几个.so库那就对了:
最后在arm64的安卓设备上运行:
看,是不是很简单。
1.AndroidStudio中的虚拟机默认是使用的x86的安卓系统,所以应该用x86编译下的.so文件。
2.apk安装后一运行就提示xxx已停止工作(这就是安卓里面的崩溃),一般情况下是.so找不到,需要使用logcat自己排查问题。
3.app运行后点击某个按键后提示xxx已停止工作,logcat显示崩溃在xxxxx函数没有实现,一般错误是那两个导出给安卓的函数名不正确(是否为静态,传参是否正确),认真检查。
4.VS2019在使用Opencv4.1.1的安卓native sdk后,如果项目属性里选择的是 llvm-libc++静态库,那么会出现编译错误:undefined reference to `strtof_l'. 具体原因我也不清楚,但是由于Opencv使用libc++_shared,所以这里使用static本身也不合理,改成llvm-libc++共享库后就可以编译成功。
5.在进行大项目移植时,请先建立最小的opencv项目测试成功后再开始。
6.一定要会使用logcat
7.事已至此,请静下来学习一点Java和Android的开发知识,不要什么都直接去百度,最后拼凑出一个刚好能使用的项目。
安卓的官方文档:https://developer.android.com/studio/projects/add-native-code.html
https://developer.android.com/ndk/guides