使用CMake进行NDK开发

CMake简单入门

Posted by XYH on May 24, 2019

使用CMake进行NDK开发简记。

前言

最近在开发项目的时候,部分网络请求的加密规则由于一直写在Java代码中,很容易被反编译找到关键加密规则导致被爬取数据,所以在评估了加密方案之后决定把加密规则由C++实现,通过NDK编译之后提供给Java层使用。

NDK编译的方式有:

本文记录CMake编译。

CMake

关于CMake的介绍

开始使用CMake

安装CMake

在开始项目前,确保已安装如下程序:

image

其中LLDB可以用于调试C++代码。

指定NDK目录。

image

编写C++文件

借鉴官方例子。

image

CMakeLists.txt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#设置编译 native library 需要最小的 cmake 版本
cmake_minimum_required(VERSION 3.4.1)

#将指定的源文件编译为名为 libnative-lib.so 的动态库
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).
             native-lib.cpp )

#查找本地 log 
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} )

native-lib.cpp

1
2
3
4
5
6
7
8
9
10
11
#include <jni.h>
#include <string>

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

Gradle配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
android {
    compileSdkVersion 28

    defaultConfig {
        applicationId "com.example.cmake"
        minSdkVersion 17
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
        externalNativeBuild {
            cmake {
                cppFlags ""
            }
        }
        ndk {
            //指定ABI为armeabi-v7a
            abiFilters "armeabi-v7a"
        }
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    externalNativeBuild {
        cmake {
            path "src/main/cpp/CMakeLists.txt"
        }
    }
}

Activity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        sample_text.text = stringFromJNI()
    }

 
    external fun stringFromJNI(): String

    companion object {
        
        init {
            System.loadLibrary("native-lib")
        }
    }
}

至此,C++代码已经正常执行。

CMake编译so

这个时候直接make project可以在目录工作目录\build\intermediates\cmake\debug下看到各ABI对应的so文件。

image

CMake指令

注:指令与大小写无关,官方建议使用大写,不过 Android 的 CMake 指令是小写的,下面为了便于阅读,采取小写的方式。

project

语法:project( [CXX] [C] [Java]) 这个指令是定义工程名称的,并且可以指定工程支持的语言(当然也可以忽略,默认情况表示支持所有语言),不是强制定义的。例如:project(HELLO) 定义完这个指令会隐式定义了两个变量: <projectname>_BINARY_DIR<projectname>_SOURCE_DIRMESSAGE指令有用到这两个变量;

另外 CMake 系统还会预定义了 PROJECT_BINARY_DIRPROJECT_SOURCE_DIR 变量,它们的值和上面两个的变量对应的值是一致的。不过为了统一起见,建议直接使用PROJECT_BINARY_DIRPROJECT_SOURCE_DIR,即使以后修改了工程名字,也不会影响两个变量的使用。

set

语法:set(VAR [VALUE]) 这个指令是用来显式地定义变量,多个变量用空格或分号隔开 例如:

set(SRC_LIST main.c test.c)

注意,当需要用到定义的 SRC_LIST 变量时,需要用${var}的形式来引用,如:${SRC_LIST} 不过,在 IF 控制语句中可以直接使用变量名。

message

语法:message([SEND_ERROR | STATUS | FATAL_ERROR] “message to display” … ) 这个指令用于向终端输出用户定义的信息,包含了三种类型: SEND_ERROR,产生错误,生成过程被跳过; STATUS,输出前缀为—-的信息; FATAL_ERROR,立即终止所有 CMake 过程;

add_executable

语法:add_executable(executable_file_name [source]) 将一组源文件 source 生成一个可执行文件。 source 可以是多个源文件,也可以是对应定义的变量 如:

add_executable(hello main.c)

cmake_minimun_required(VERSION x.x.x)

用来指定 CMake 最低版本为x.x.x,如果没指定,执行 cmake 命令时可能会出错。

add_subdirectory

语法:add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL]) 这个指令用于向当前工程添加存放源文件的子目录,并可以指定中间二进制和目标二进制存放的位置。EXCLUDE_FROM_ALL参数含义是将这个目录从编译过程中排除。

另外,也可以通过 SET 指令重新定义 EXECUTABLE_OUTPUT_PATHLIBRARY_OUTPUT_PATH 变量来指定最终的目标二进制的位置(指最终生成的 hello 或者最终的共享库,不包含编译生成的中间文件)

set(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin)

set(LIBRARY_OUTPUT_PATH ${PROJECT_BINARY_DIR}/lib)

add_library

语法:add_library(libname [SHARED | STATIC | MODULE] [EXCLUDE_FROM_ALL] [source]) 将一组源文件 source 编译出一个库文件,并保存为 libname.so (lib 前缀是生成文件时 CMake自动添加上去的)。其中有三种库文件类型,不写的话,默认为 STATIC:

  • SHARED: 表示动态库,可以在(Java)代码中使用 System.loadLibrary(name) 动态调用;
  • STATIC: 表示静态库,集成到代码中会在编译时调用;
  • MODULE: 只有在使用 dyId 的系统有效,如果不支持 dyId,则被当作 SHARED 对待;
  • EXCLUDE_FROM_ALL: 表示这个库不被默认构建,除非其他组件依赖或手工构建

将compress.c 编译成 libcompress.so 的共享库 add_library(compress SHARED compress.c)

add_library 命令也可以用来导入第三方的库: add_library(libname [SHARED | STATIC | MODULE | UNKNOWN] IMPORTED) 如,导入 libjpeg.so

add_library(libjpeg SHARED IMPORTED)

导入库后,当需要使用 target_link_libraries 链接库时,可以直接使用该库。

find_library

语法:find_library( name1 path1 path2 …) VAR 变量表示找到的库全路径,包含库文件名 。例如:

find_library(libX X11 /usr/lib) find_library(log-lib log) #路径为空,应该是查找系统环境变量路径

set_target_properties

语法: set_target_properties(target1 target2 … PROPERTIES prop1 value1 prop2 value2 …) 这条指令可以用来设置输出的名称(设置构建同名的动态库和静态库,或者指定要导入的库文件的路径),对于动态库,还可以用来指定动态库版本和 API 版本。 如,

set_target_properties(hello_static PROPERTIES OUTPUT_NAME “hello”)

include_directories

语法:include_directories([AFTER | BEFORE] [SYSTEM] dir1 dir2…) 这个指令可以用来向工程添加多个特定的头文件搜索路径,路径之间用空格分割,如果路径中包含了空格,可以使用双引号将它括起来,默认的行为是追加到当前的头文件搜索路径的 后面。

语法:target_link_libraries(target library library2…) 这个指令可以用来为 target 添加需要的链接的共享库,同样也可以用于为自己编写的共享库添加共享库链接。 如:

#指定 compress 工程需要用到 libjpeg 库和 log 库 target_link_libraries(compress libjpeg ${log-lib})