Note:这篇文章是基于Android Studio 2.3版本的,对于很多功能2.2开始就已经支持,但是存在一些bug,例如CMake中的add_custom_command用echo打印调试信息再2.2版本中无法查看,直到2.3才修复。其他的更新暂时没有体会。
当然在进行下面的步骤前,需要先在Android Studio中的SDK Manager安装好LLDB、CMake还有NDK。
从没有勾选C++ Support的空项目开始
在新建项目的时候,是可以勾选添加C++ Support选项的,它会自动产生CMakeLists.txt以及在build.gradle中添加对CMakeLists.txt的引用。但是为了理解Android Studio中gradle结合CMake是如何构建NDK项目的,我们还是从零开始。
当最终项目完成之后,目录树应该是下面的样子(去除了与构建结构无直接关联的目录及文件,带星号的是我们即将添加的部分):
.
├── app
│ ├── build.gradle
│ ├── CMakeLists.txt
│ └── src
│ └── main
│ ├── AndroidManifest.xml
│ ├──*cpp
│ │ ├── native-math.cpp
│ │ └── native-opencv.cpp
│ └── java
│ └── com/huang/opencvtest
│ └── MainActivity.java
├──*distribution
│ ├── include
│ └── libs
├──*mathlib
│ ├── build.gradle
│ ├── CMakeLists.txt
│ └── src
│ └── main
│ ├── AndroidManifest.xml
│ └── cpp
│ ├── add.cpp
│ └── add.h
├──*openCVLibrary320
│ ├── build.gradle
│ └── src
│ └── main
│ ├── AndroidManifest.xml
│ └── java/org/opencv
├── gradle.properties
├── local.properties
├── build.gradle
└── settings.gradle
我们要添加自己定义的一个简单的数学C++库,以及OpenCV4Android预编译库。
首先将视图切换到Project,今后操作一直在Project视图中进行。
构建的结构
顶级的build.gradle
在我们一般的使用下并不需要去设置,顶级的构建文件中我们仅仅需要改settings.gradle
。settings.gradle
描述了这一个项目在构建的时候需要包含哪一些模块。最开始只有app
模块,因此里面只有一句话include ':app'
。
系统在构建的时候,就会根据这句话,去寻找模块名为app
的目录下面的build.gradle
文件,并将其纳入构建结构树中。最终形成一课完整的构建结构树,将所有的部分联系在一起,编译成最终的Android应用。
每个模块中的我们称之为局部build.gradle
,在这里面,定义了该模块为application或者library或其他,一般我们考虑这两个选项。这里面定义了许多构建这个模块时要用到的参数,在后续我们添加NDK支持的时候需要往里面添加一些参数,其中对CMakeLists.txt的路径就是在这里指定的。
添加自定义的C++库mathlib
创建源文件
- 我的项目名称为OpenCVTest,所以右键这个项目点击
New->Module
,然后选Android Library
,输入库的名称MathLib
,然后Finish
,系统就会生成对应的模块,并构建好初始的目录树。系统将库命名为MathLib
,但是目录树中还是小写的mathlib
。这个时候系统会自动在顶级settings.gradle
添加对于这个新模块的include
语句。并且在模块目录下构建好了初始的build.gradle
。 - 现在我们开始创建自己的C++库,首先右键
mathlib
目录下的src/main
,然后选择New->Directory
,输入cpp
并确定。这个目录就是我们要创建的库的源文件的位置。 - 右键
add
,点击New->C/C++ Source File
,输入add.cpp
,并选中Create an associated header
。 - 在
.cpp
文件中定义好一个简单的加法函数,并在.h
文件中添加好对应声明。
库本身的定义就到此为止。
将源文件关联到构建系统中
我们用CMake来构建C++库,然后CMake又要和gradle结合,在Android Studio里面协作管理C++和Java的代码。
我们在模块mathlib
的根目录下创建一个名为CMakeLists.txt
的文件,写入
cmake_minimum_required(VERSION 3.4.1)
add_library(add SHARED
src/main/cpp/add.cpp)
set(distribution_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../distribution)
set_target_properties(add PROPERTIES
LIBRARY_OUTPUT_DIRECTORY
${distribution_DIR}/libs/${ANDROID_ABI})
target_include_directories(add
PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src/main/cpp)
#add_custom_command(TARGET add POST_BUILD
# COMMAND ${CMAKE_COMMAND} -E
# copy ${distribution_DIR}/libs/${ANDROID_ABI}/libadd.so
# ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/libadd.so
# COMMAND ${CMAKE_COMMAND} -E
# echo "output libadd.so to ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}"
# COMMENT "Copying add to output directory")
上面的CMake指令就是将源文件添加,并构件为一个Library即库,然后将创建后的libadd.so
复制到项目根目录的distribution
中,并最终被主模块app
引用。
Note:在这里需要注意的是,target_include_directories
,它对创建的库设置include
路径,针对目标来设置,可以避免与其他库的冲突,并且此时对自定义的库设置好了此路径后,后续导入这个库就不需要再次设置了。但对于预构建的库,就需要设置,稍后会有详细讲解。
Note:这里有个问题。被注释的部分,是将libadd.so
复制到${CMAKE_LIBRARY_OUTPUT_DIRECTORY}
中,被复制到这个目录的.so
文件会被自动添加到.apk
文件中去。但是在稍后为OpenCV的库添加的android.sourceSets.jniLibs.srcDirs
会让这个特性失效,因此这里还是注释掉,稍后统一为库设置路径,让系统复制到.apk
文件中。
这个时候,CMakeLists.txt
还是独立的,并没有与Android Studio的构建系统联系起来。
接下来我们在模块mathlib
的build.gradle
中的defaultConfig{}
中添加如下语句:
externalNativeBuild {
cmake {
arguments '-DANDROID_PLATFORM=android-19',
'-DANDROID_TOOLCHAIN=clang', '-DANDROID_STL=gnustl_static'
targets 'add'
}
}
这里arguments
是编译参数,而targets
则是相比于add_subdirectory
更高权限的方法。一般来说可以把它删去,即默认构建所有目标。
Note:之所以在defaultConfig{}
中设置,是因为在Android中,有许多不同的所谓风味,即免费版和付费版等,defaultConfig{}
的设置可以在不同风味中覆盖,即它是缺省设置。
然后在android{}
最后添加如下语句,将CMakeLists.txt
关联起来。
externalNativeBuild {
cmake {
path 'CMakeLists.txt'
}
}
这样,我们的自定义C++库就添加完成了。
在主模块中使用自定义C++库
C++库已经创建好了,接下来就要在主模块中使用它了。
为了使用自定义C++库,我们需要一个中间人,它从Android本身的Java程序中获取请求,然后使用我们的C++库中的函数计算得到结果,并将数据传回Android本身的Java程序中。
扮演这个角色的,也是一个C++源文件。
我们在主模块app
的根目录下创建一个CMakeLists.txt
文件,再在src/main
下创建一个目录cpp
,其中再创建native-math.cpp
。CMakeLists.txt
用来将主模块中包括native-math.cpp
在内的主模块中的库作为一个顶级库。
我们先将native-math.cpp
空着,先写CMakeLists.txt
。
在CMakeLists.txt
中写入
cmake_minimum_required(VERSION 3.4.1)
set(distribution_DIR ${CMAKE_SOURCE_DIR}/../distribution)
# set add lib
add_library(lib_add SHARED IMPORTED)
set_target_properties(lib_add PROPERTIES IMPORTED_LOCATION
${distribution_DIR}/libs/${ANDROID_ABI}/libadd.so)
include_directories(lib_add
${distribution_DIR}/include)
add_library(native-math SHARED
src/main/cpp/native-math.cpp)
set_target_properties(native-math PROPERTIES
LIBRARY_OUTPUT_DIRECTORY ${distribution_DIR}/libs/${ANDROID_ABI})
target_link_libraries(native-math
android
lib_add
log)
我们将之前创建的libadd.so
使用导入,指定其路径,并在这里命名为lib_add
。
然后使用target_link_libraries
将包括默认的android
在内的lib_add
链接到native-math
中。
在模块app
的局部build.gradle
中,像之前一样添加好对应的语句:
defaultConfig{}
中:
externalNativeBuild {
cmake
arguments '-DANDROID_PLATFORM=android-19',
'-DANDROID_TOOLCHAIN=clang', '-DANDROID_STL=gnustl_static'
}
}
ndk {
abiFilters 'armeabi-v7a'
}
其中abiFilters
的作用是,在生成.so
库时,只生成对应结构的文件,以至于最后的.apk
文件也只有对应架构的.so
库。
然后在android{}
中:
externalNativeBuild {
cmake {
path 'CMakeLists.txt'
}
}
sourceSets {
main {
jniLibs.srcDirs = ['../distribution/libs']
}
}
其中的sourceSets.main.jniLibs.srcDirs
就是指定jni
库的了路径,让系统自动把.so
文件复制到.apk
文件中。
在native-math.cpp
中已经可以使用jni
接口了。
单独构建好mathlib
之后,在native-math.cpp
中把接口按照规定的格式写好即可。
#include <jni.h>
#include <add.h>
extern "C" {
jint Java_com_huang_opencvtest_MainActivity_addFromCpp(JNIEnv *env, jobject thiz, jint a, jint b){
return add(a,b);
}
}
函数的名称按照与Android中对应的.java
路径的格式来写。
接着在src/main/java/*/MainActivity.java
中的MainActivity
类下面,加载库,以及设置好对应的方法声明:
static {
System.loadLibrary("native-math");
}
private native int addFromCpp(int a, int b);
然后就可以在onCreate
方法中使用这个C++库定义的函数,在Java中对应的函数了。
最后别忘了在项目中添加模块的依赖关系才可以正常运行这个Android App。右键项目OpenCVTest
,选择Open Module Settings
。选择app->Dependencies
,添加Module dependency
,选择mathlib
,确定即可。
然后到此完成,可以运行App了。
添加OpenCV库的支持
导入OpenCV进项目
- 从OpenCV的官网将OpenCV4Android 3.2下载下来,解压到某个目录。
- 点击Android Studio的
File->New->Import Module
,然后选择路径为OpenCV-android-sdk/sdk/java
,确定。并在导入之后,修改build.gradle
中的SDK版本。 - 在
Open Module Settings
中添加模块的依赖关系,使app
依赖openCVLibrary320
。
现在已经可以在.java
文件中看得到OpenCV的自动补全了。
配置OpenCV的C++预构建库
把包含文件夹OpenCV-android-sdk/sdk/native/jni/include
和预构建库文件夹OpenCV-android-sdk/sdk/native/libs
也复制到项目的distribution
中。
由于之前已经在添加C++库时修改了app
的build.gradle
,所以这个步骤现在不需要再执行了。
由于OpenCV是预构建库,所以没有编译的过程,因此模块openCVLibrary320
中不需要添加CMakeLists.txt
等。我们直接在app
模块中根目录下的CMakeLists.txt
导入OpenCV的库即可。
add_library(lib_opencv_java3 SHARED IMPORTED)
set_target_properties(lib_opencv_java3 PROPERTIES
IMPORTED_LOCATION ${distribution_DIR}/libs/${ANDROID_ABI}/libopencv_java3.so)
include_directories(lib_opencv_java3
${distribution_DIR}/include)
需要注意的是.so
使用SHARED
,.a
使用STATIC
。
Note:与上面自定义的库相对,这里是导入预构建的库,因此需要设置一下include
目录,对预构建的库不能用target_include_directories
,因此在这里我们使用include_directories
来设置include
目录。同样地,在这里设置好了以后,后续链接这个库的时候,就不需要再次设置这个目录了。
然后先像之前C++库一样,在src/main/cpp
下新建文件native-opencv.cpp
,先空着。
然后在文件最后再写上以下部分:
add_library(native-opencv SHARED
src/main/cpp/native-opencv.cpp)
set_target_properties(native-opencv PROPERTIES
LIBRARY_OUTPUT_DIRECTORY ${distribution_DIR}/libs/${ANDROID_ABI})
target_link_libraries(native-opencv
android
lib_opencv_java3
log)
由于我们统一将include
目录设置为distribution/include
,libs
目录设置为distribution/libs
,因此在app
的build.gradle
中我们不需要再做出改动。
现在就已经可以在native-opencv.cpp
中使用OpenCV的函数了。
#include <jni.h>
#include <opencv2/opencv.hpp>
#include <vector>
using namespace cv;
using namespace std;
extern "C" {
void Java_com_huang_opencvtest_MainActivity_nativeProcessFrame(JNIEnv *env, jobject thiz, jlong addrGray, jlong addrRGBA){
Mat& gray = *(Mat *) addrGray;
Mat& rgba = *(Mat *) addrRGBA;
vector<KeyPoint> v;
Ptr<ORB> orb = ORB::create();
orb->detect(gray, v, cv::Mat());
for (int i = 0; i < v.size(); ++i) {
const KeyPoint& kp = v[i];
circle(rgba, Point(kp.pt.x, kp.pt.y), 10, Scalar(255,0,0,255));
}
}
}
现在就可以在src/main/java/*/MainActivity.java
中按照同样的方法,载入库,写上方法声明。最后,如下所示。
static {
System.loadLibrary("native-opencv");
System.loadLibrary("native-math");
}
private native int addFromCpp(int a, int b);
private native void nativeProcessFrame(long addrGray, long addrRGBA);
然后就可以继续完成整个app
了。
用手机检测并画出ORB特征点
具体的代码在我的GitHub仓库中。
值得注意的问题
在这两天自己对这篇文章所述的进行探索和尝试的过程中,也慢慢对Android Studio的结构有了更深的理解。期间也碰到了不少问题,参考了不少资料才得以解决,有些方案不是最好的,但是还是解决了问题。
- 自己编译的
libadd.so
库自动加入.apk
文件的问题。一开始只是按照官方的Sample,把创建好的文件放到项目根目录下的distribution
中,并且在最开始接触的时候,觉得build.gradle
中的sourceSets
那个设置很烦,而且想纯用CMake解决这个问题,所以搜索了很多资料。后来参考了Google Code论坛,发现可以在CMakeLists.txt
中,添加一个自定义命令,将编译后的.so
复制到${CMAKE_LIBRARY_OUTPUT_DIRECTORY}
中,这样系统就会自动添加这些.so
库了。但是这样就会引发下一个问题。 - 预构建库(如OpenCV)的
.so
库文件载入.apk
文件的问题。对于预构建库,不存在编译过程,那么之前那个编译后的自定义命令就无效了。然后在参考Martin的文章的时候,遇到一个问题,就是OpenCV的.so
库一直不会被复制到.apk
中,但是修改成一样的配置文件,仍然不行,但是将这个博主的项目下载下来构建后发现.apk
中有lib_opencv_java3.so
文件。这就很奇怪,于是我仔细对比了两个项目,发现在这个博主的项目中,在Android
视图看项目后,存放OpenCV库的文件夹被认为是某种Assets Folder
,但是项目中没有找到任何配置文件描述这一特性。在参考了Stack Overflow问答,并重复实验多次后,我发现:在src/main
下新建一个JNI Folder
,并且命名为jniLibs
,在build.gradle
中会自动生成一段sourceSets { main { jni.srcDirs = ['src/main/jniLibs', 'src/main/jniLibs/'] } }
,即使把这段删除,然后在其中放入OpenCV的.so
库,不需要任何配置,系统就会自动复制其中的.so
进入.apk
,很奇怪。换成别的名字都不行。但是对于其他名字,不删除这一段,就可以。结论就是jniLibs
这个名字本身有某种特殊性,系统会自动将其中的.so
文件作为库,复制到.apk
中。但是如这篇文章所述,这样会使本应将${CMAKE_LIBRARY_OUTPUT_DIRECTORY}
中.so
库复制到.apk
中的过程失效。在试过很多中办法之后,最后妥协,干脆把所有自定义或者预构建的库都使用sourceSets.main.jniLibs.srcDirs
的方法来处理(注意这里jni.srcDirs
和jniLibs.srcDirs
使用起来目前没有发现有区别,两个都行)。 - OpenCV Manager的问题。虽然还不清楚这个具体是怎么弄的,但是在直接使用OpenCV的时候可能会出现提示找不到OpenCV Manager的问题,然后闪退。参考了Stack Overflow问答之后,就可以把这个问题解决了。
- 尝试在
.cpp
使用ORB特征检测器检测ORB特征的时候,编译时提示abstract class
等错误提示,一开始以为是库没有正确加载,搞了半天,结果参考了OpenCV问答之后,发现是自己使用方法错误。应该是接口变了,不能直接这样定义ORB orb;
,而应该使用Ptr<ORB> orb = ORB::create()
来定义。 - 之前在Android Studio 2.2中,在
CMakeLists.txt
中想要用echo
命令输出某些信息时,发现输出不了,但是刚好在2017/03/04我更新了最新版的Android Studio 2.3后,貌似这个Bug被修复了,可以在Gradle Console中看到输出结果了。 - 应用对摄像头权限的问题。由于初涉Android,不懂权限的申请,但是现在还是要手动在
Settings->Apps
中对应用添加摄像头权限才可以显示图像。 - OpenCV中图像旋转了90°的问题。图像永远与摄像头实际方向相差90°,经过搜索,这应该是OpenCV本身的问题。网上有不完美修复的办法,例如OpenCV问答中的通过旋转和镜像来处理,最终的图像虽然方向对了,但是是被压扁了的,这肯定不行。但是后来在Martin的文章中提供的项目中找到了解决办法,就是在
app
的AndroidManifest.xml
中强制手机横屏,即可。 - 正如前面构建步骤中所述,自定义C++库中,
include
目录是需要用target_include_directories
来设置的,并且这个设置是一次性的,即自定义C++库构建之后,被其他模块链接的时候不需要再次设置这个目录。但是预构建库则不同,我们需要用include_directories
来设置include
目录。
还有一些不是遇到的问题,但是是在我寻找上述问题答案的过程中,找到的可以作为参考的资料:
- Why there are two versions of OpenCV 3.x and 2.4.xx ? - OpenCV Q&A Forum
- OpenCV 3.0 | OpenCV
参考资料
- Issue 214664 - android - We need to package the standard shared libraries in to the APK - Android Open Source Project - Issue Tracker - Google Project Hosting
- 使用Android Studio 2.2和Cmake (CMakeLists)让OpenCV 飞起来 - Martin的博客 - 博客频道 - CSDN.NET
- How to create jniLibs folder on Android Studio? - Stack Overflow
- Importing Library Having Native Code in Android studio | Cumulations
- OpenCV Android without OpenCV Manager - Stack Overflow
- Image Registration (OpenCV 3.0.0) - OpenCV Q&A Forum
- How can i change orientation without ruin camera settings? - OpenCV Q&A Forum
- Why there are two versions of OpenCV 3.x and 2.4.xx ? - OpenCV Q&A Forum
- OpenCV 3.0 | OpenCV
- soaph/OpenCVTest: OpenCV 3.2, Android Studio 2.3, with CMake