在 Android Studio 中使用 C/C++ 链接库:以 OpenCV 3.4.1 为例

2020/02/13 Android C++

这里总结一下在 Android Studio 中调用已有的 C/C++ 库的流程和技巧,以 Android Studio 3.5.3 和 OpenCV 3.4.1 为例。

流程主要参考:


0. 环境配置

首先需要在 Android Studio 中安装几个组件:

  • NDK: 用于在 Android 中使用 C/C++;
  • LLDB:调试工具;
  • CMake:编译 C/C++ 代码。如果使用 ndk-build 的话可以不装,不过我们还是使用比较熟需的 CMake 比较方便。

安装方法不难,直接参考:官方文档:Install and configure the NDK and CMake

1. 创建支持 C/C++ 的工程

注意:这里是从创建一个空项目开始。如果你已经有了一个项目,并且要添加 C++ 库的话,这个就稍微麻烦一点,需要自行添加相关的几个文件了,可以参见下面比较这两种工程之间的不同。

笔者使用的是 Android Studio 3.5.3。它在创建一个新的支持 C++ 的项目时,和过去版本稍有一点不同。选择 File -> New -> New Project 后,直接从 Phone and Tablet 中向下滚动,选择最后一个选项 Native C++(见下图)。而老版本的 Android Studio中并没有这个选项,而是要新建普通的 Empty Activity,随后才出现一个选项是 Support C/C++。此后,建立你的项目就行了,最后一页会有一个选项是支持 C++ 11 或 14。

这个新的支持 C++ 的项目和普通的纯 java 的工程的区别是:

  • 多出了一个 cpp 文件夹在路径 /your-project/app/src/main/cpp 中,它里面已经包含了 CMakeLists.txt 和 native-lib.cpp 文件,其中前者就是我们常见的 C/C++ 中使用的 cmake 文件,后者包含了 C/C++ 代码和 java 代码的接口函数。这个文件夹就是将来放置所有 C++ 代码的。
  • build.gradle (Module app) 文件也会和普通的有所区别:增加了支持 cmake 的一些语句。详情参见本文最上面给出的几个链接。
  • MainActivity.java 文件中添加了加载链接库文件的语句:
      // Used to load the 'native-lib' library on application startup.
      static {
          System.loadLibrary("native-lib");
      }
    

    以及 C++ 代码的接口函数: ```java /**

    • A native method that is implemented by the ‘native-lib’ native library,
    • which is packaged with this application. */ public native String stringFromJNI(); ```

2. 添加 C/C++ 库文件到工程

注意:这一章写的是使用第三方库的头文件和库文件。如果你有全部的 .h 和 .cpp 源码,那么直接放在 cpp 文件夹中就行了,然后直接进行下一步。

流程如下:

  • 首先,在 /your-project/app/src/main/ 中新建一个名为 jniLibs 的文件夹,用于放置库文件。注意这个文件名和路径必须是固定的,修改路径或者修改文件名都不行。
  • 拷贝库文件到 jniLabs 文件夹。对于 OenCV 来说,从官网下载 OpenCV for Android 源代码文件并解压。之后,将其中的 /OpenCV-android-sdk/sdk/native/libs 路径下的全部文件拷贝到 jniLibs 路径下。大概有 4-7 个文件夹,每个文件夹是对应的不同 Android 平台上;

整个工程大概是这样的:

3. 增加链接库到 CMakeLists.txt

在 CMakeLists.txt 中添加如下内容:


cmake_minimum_required(VERSION 3.4.1) # 这句应该已经有了

# 下面开始是新增内容
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=gnu++11")

# 简单定义俩变量
set(pathToOpenCV ~/dev/OpenCV-android-sdk-3.4.1)
set(pathToProject ~/dev/AndroidToFTest/)

# 增加头文件
include_directories(${pathToOpenCV}/sdk/native/jni/include)

# 使用已有的库来生成一个新的名为 lib_opencv 的库,这里指定是导入的
add_library(lib_opencv SHARED IMPORTED)

# 这一句也是固定的。这里的 ${ANDROID_ABI} 代表不同的 Android 平台,参见:https://developer.android.com/ndk/guides/abis
set_target_properties(lib_opencv PROPERTIES IMPORTED_LOCATION ${pathToProject}/app/src/main/jniLibs/${ANDROID_ABI}/libopencv_java3.so)

# 接着应当是本来就有的内容:生成 native-lib 库并添加到工程
...

# 最后记得链接 lib_opencv 到工程
target_link_libraries( # Specifies the target library.
        native-lib

        # Links the target library to the log library
        # included in the NDK.
        ${log-lib} 
        
        # 这个是新加的,上面是本来就有的
        lib_opencv
        )

其实就是 CMakeLists.txt 中常见的做法。上面添加的代码流程是:

  • 首先用已有的 OpenCV 库再新生成一个 lib_opencv 动态库;
  • 再添加 lib_opencv 到 native-lib 中。

这样的好处是,最终在 MainActivity.java 中就只需要加载这一个最终的库就够了,而不是一个库加载一次。

如果你使用的不是第三方库,而是有自己实现的全部的 .h 和 .cpp 源文件,那么就容易一些,只需要 add_library() 就够了,可以删除 set_target_properties 命令。例如:

add_library(your_lib SHARED XXX.cpp)

最后再记得将 your_lib 链接到 native-lib 中。

此时再运行 Build -> Make Project 来建立工程后,就会发现,刚刚在 CMakeLists.txt 中增加的头文件已经被加入到了 cpp 下面的 includes 路径中(只是 Android Studio 是这么显示的,实际并没有这个文件夹)。此时你在你的 C++ 文件中就可以使用各种提示了。

4. 修改 build.gradle 文件 (Optional)

这一步是可选的。可以在你的 build.gradle (Module: app) 文件中增加一些内容,例如 cppFlags 等。不加肯定也能通过。

externalNativeBuild {
            cmake {
                cppFlags "-std=c++11 -frtti -fexceptions" // 根据情况设置
                abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a' // 根据情况设置
            }
        }

5. 修改接口函数

上面已经提到了,C++ 代码的接口函数被定义到了 ` native-lib.cpp` 文件中,类似这样:

extern "C" JNIEXPORT jstring JNICALL
Java_com_example_androidcpptest_MainActivity_stringFromJNI(
        JNIEnv *env,
        jobject /* this */) {
    std::string hello = tryToPrint("Hello from C++");
    return env->NewStringUTF(hello.c_str());
}

而在 MainActivity.java 中包含了对应的函数声明:

    /**
     * A native method that is implemented by the 'native-lib' native library,
     * which is packaged with this application.
     */
   public native String stringFromJNI();

注意,该函数名的格式是固定的,就是 Java_各级包名_类名_函数名(参数...),其中“函数名”之前的都是固定的,函数名和参数当然可以改了,不过记得修改 MainActivity.java 中的对应的函数声明。另外,可以实现多个接口函数,只要满足这种格式要求并增加函数声明就行,不过记得将接口函数都放在extern "C" { ... } 的范围内。

接口函数的语法其实就是 Java 内调用 C++ 代码的语法,需要的话可以搜索相关内容。一个典型的接口函数的例子来自本文开头给出的链接 [2]。这个函数作用是将输入的的彩色图片转换成灰度图。

// 注意这里的函数名格式:Java_各级包名_类名_函数名(参数...),需严格按照这种格式,否则会出错
// 输入数组 buf 是一个 int[] buf 数组,w 和 h 是宽度和高度。
JNIEXPORT jintArray JNICALL Java_com_example2_apple_myapplication_MainActivity_gray(
    JNIEnv *env, jobject instance, jintArray buf, jint w, jint h)
{
    jint *cbuf = env->GetIntArrayElements(buf, JNI_FALSE);
    if (cbuf == NULL)
    {
        return 0;
    }
    // 将输入数组还原成一个 cv::Mat,方便进行操作。
    Mat imgData(h, w, CV_8UC4, (unsigned char *)cbuf);
    uchar *ptr = imgData.ptr(0);
    for (int i = 0; i < w * h; i++)
    {
        //计算公式:Y(亮度) = 0.299*R + 0.587*G + 0.114*B
        // 对于一个int四字节,其彩色值存储方式为:BGRA
        int grayScale = (int)(ptr[4 * i + 2] * 0.299 + ptr[4 * i + 1] * 0.587 + ptr[4 * i + 0] * 0.114);
        ptr[4 * i + 1] = grayScale;
        ptr[4 * i + 2] = grayScale;
        ptr[4 * i + 0] = grayScale;
    }
    int size = w * h;
    jintArray result = env->NewIntArray(size);
    // 上面我们是对 Mat 做了修改,而 Mat 指向的内存空间其实就是 cbuf 的空间,因此 cbuf 中
    // 的结果就是最终结果。
    // 这里创建一个新的数组并返回。
    env->SetIntArrayRegion(result, 0, size, cbuf);
    env->ReleaseIntArrayElements(buf, cbuf, 0);
    return result;
}

Search

    Table of Contents