/ Android

采用C++开发Android APP

作为一名算法开发人员,一定会有大量项目是用C/C++开发完成的。随着移动设备功能日益强大,总会有这么一天,你需要把曾经的C++代码移植到Android系统中运行。这正是我最近面临的状况。作为一名从未写过JAVA的业余程序员,如何解决这个问题?

问题分析

遇到困难先不要慌,镇静下来仔细分析一下情况。首先,需要移植的工程从功能上来分析大致可以分为三大模块:

  • 调用摄像头进行人脸检测;
  • 根据检测到的信息进行进一步计算;
  • 将结果输出,控制后端的硬件。

第一部分调用了第三方视觉库OpenCV。第二和第三部分是自己编写的C++代码。考虑到Android系统本身提供了人脸检测的模块可供调用,第一部分的问题算不上棘手。在之前的项目中,第二部分内容的原理已经非常清晰,大不了用JAVA再实现一遍。第三部分因为平台与控制硬件发生了变化,有同事可以提供支持,在此可以不予考虑。所以我需要完成的事项就变为:

  • 在Android系统下实现摄像头调用与人脸检测;
  • 尝试将第二个模块移植到Android系统。

制定移植计划

这两项事情对于我来说有一定的难度,主要原因是我从未写过JAVA代码,没有做过Android下的APK开发,更别提将C++代码移植到Android运行。但是任务已经安排了下来,只能想办法克服这些困难,尽力而为。虽然没有写过JAVA代码,但是C++的基础应该可以借鉴;没做过Android下的APK开发,但是iOS下的APP开发经验应该会提供一部分帮助;C++代码移植万一不可行,就重新用JAVA写一遍,毕竟原理已经了然于胸,可以根据需求做相应的功能裁剪,尽可能减少工作量,以保证按期交付。所以我需要做的事项变成了:

  • 尽快入门Android下的APP开发;
  • 尝试寻找C++移植方法,如果不行,则采用JAVA重新实现。

Android应用开发入门

由于整个移植过程只有一周的时间,不可能做到真正的入门,最快的方法就是从Google提供的官方sample code入手。于是打开浏览器,访问Google开发者中国站:http://developer.android.google.cn. 从官方介绍来看,Google提供了一款类似Xcode的工具:Android Studio。于是一边下载安装Android Studio,一边浏览官方提供的示例代码,尤其是人脸检测相关部分。经过一番检索,发现Android下的人脸识别有两种实现途径:

  • Android系统提供了Camera API,可以实现人脸检测[1]
  • Google另外提供了Mobile Vision API,可以实现包括人脸检测在内的众多强大的功能[1:1]

幸运的是,Google针对Mobile Vision API还提供了7份示例代码,其中包括一份名为FaceTracker人脸检测代码。发现这段代码让我又惊又喜,这起码节省了2天的时间。二话不说下载用Android Studio导入,编译运行,一切都很完美。

C++代码支持情况

我使用的Android Studio版本为2.2.3,在建立工程的时候有一个可选项Include C++ Support,这表明Android Studio是支持C++的。

浏览官方站点也发现Android提供NDK来将C++代码编译为JNI库[1:2],然后可在Java环境下调用。于是尝试建立一个支持C++的工程,将自己的C++代码添加到默认生成的native-lib.cpp文件中,编译运行,OK。可是,如何让现有基于JAVA的工程来支持C++?

为了弄清楚这一点,我选择从分析两种工程之间的差异入手。分别新建了一个JAVA工程,一个C++工程。将两个项目目录结构展开,并排放在一起比较:

左侧为JAVA工程,右侧为C++工程。仔细比较发现,支持C++的工程主要发生了如下变化:

  • build.gradleMainActivity的文件内容发生了变化。
  • src目录下多了一个cpp目录,内有一个native-lib.cpp文件;
  • src目录下多了一个CMakeLists.txt文件;

接下来详细看看变化的细节。

build.gradle

build.gradle文件中,android{...}代码块中增加了如下内容:

externalNativeBuild {
    cmake {
        path "CMakeLists.txt"
    }
}

defaultConfig{...}代码块中增加了如下内容:

externalNativeBuild {
    cmake {
        cppFlags ""
    }
}

从内容可以看出,这是在告诉gradle采用CMake的方式对C++代码进行编译,并且给出了CMake需要的文件CMakeLists.txt

CMakeLists.txt

CMakeLists.txt文件中内容很长,且有非常详细的注释,这里摘取其中重要的内容如下:

# ...
cmake_minimum_required(VERSION 3.4.1)

# ...
add_library( # Sets the name of the library.
             native-lib

             # Sets the library as a shared library.
             SHARED

             # Provides a relative path to your source file(s).
             # Associated headers in the same location as their source
             # file are automatically included.
             src/main/cpp/native-lib.cpp )

# ...
find_library( # Sets the name of the path variable.
              log-lib

              # Specifies the name of the NDK library that
              # you want CMake to locate.
              log )

# ...
target_link_libraries( # Specifies the target library.
                       native-lib

                       # Links the target library to the log library
                       # included in the NDK.
                       ${log-lib} )

从注释可以看出,CMakeLists.txt做了以下事项:

  • 通过add_library()的方式添加C++源代码文件native-lib.cpp,采用SHARED方式编译,生成native-lib库文件;
  • 通过find_library()的方式包含要链接的外部库文件log
  • 通过target_link_libraries()的方式链接native-lib库文件。

native-lib.cpp正是C++源代码。

native-lib.cpp

作为C++源代码所在,native-lib.cpp内容如下:

#include <jni.h>
#include <string>

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

在其中终于看到熟悉的std::string hello = "Hello from C++"; 显然,这里声明了一个名为Java_com_yinguobing_robin_cpp_1application_MainActivity_stringFromJNI的函数,该函数返回一个类型为jstring的字符串,字符串内容为Hello from C++。那这个函数又是在哪里调用的呢?

MainActivity

在iOS APP开发过程中,程序的入口一般为UIViewController,同样,放在src/java路径下的这个MainActivity一定是JAVA程序的入口了。仔细比较该文件,发现其中增加了如下内容:

// Used to load the 'native-lib' library on application startup.
static {
    System.loadLibrary("native-lib");
}

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

正如注释所述,System.loadLibrary("native-lib")负责加载native-lib库文件;而stringFromJNI()正是在native-lib.cpp中实现的C++函数。

C++代码移植

既然原理已经清楚,接下来的事情就很简单了。整个代码移植的过程大概分以下几步:

  • build.gradle中添加支持C++外部编译的代码;
  • 建立CMakeList.txt文件,并添加要编译的源代码文件;
  • 在程序的入口文件中添加要调用的C++函数原型;
  • 在C++源代码中更新被调用函数的名称。

以上几步完成,你用C++实现的函数即可在Java环境下进行调用了。

最后附上Android官方文档:向您的项目添加 C 和 C++ 代码, 地址是 https://developer.android.google.cn/studio/projects/add-native-code.html

参考


  1. Android NDK ↩︎ ↩︎ ↩︎

Yin Guobing

Yin Guobing

BOE技术研发工程师🔬,业余码农😳,蓝猫铲屎官🐈。曾独立开发了一款iOS APP并上线🎉。现居北京,正在为了理想中的生活而奋斗..

Read More