NDK 知识梳理(2) - 使用 CMake 进行 NDK 开发之如何编写 CMakeLists.txt 脚本

一、前言

在前一篇文章 NDK 知识梳理(1) - 使用 CMake 进行 NDK 开发之初体验 中,我们一起学习了如何在Android Studio中使用CMake来进行NDK开发,而编写CMakeLists.txt构建脚本是其中一个重要的环节,今天我们就来一起学习CMakeLists.txt的一些应用,介绍它在下面三种场景的用法:

  • 从原生代码构建一个原生库
  • 添加Android NDKAPI
  • 引入第三方so

二、从原生代码构建一个原生库

2.1 指定 CMake 最低版本

cmake_minimum_required用于指定CMake的最低版本信息,不加入会收到警告。

cmake_minimum_required(VERSION 3.4.1)

2.2 从原生代码构建一个原生库

add_library()用于指示CMake从原生代码构建一个原生库,通俗地说,就是从.cpp经过编译得到.so文件。正如我们在 NDK 知识梳理(1) - 使用 CMake 进行 NDK 开发之初体验 中看到的那样,我们通过add_libraryCMake根据native-lib.cpp源文件构建一个名为native-lib的共享库:

# Specifies a library name, specifies whether the library is STATIC or
# SHARED, and provides relative paths to the source code. You can
# define multiple libraries by adding multiple add.library() commands,
# and CMake builds them for you. When you build your app, Gradle
# automatically packages shared libraries with your APK.

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

             # Sets the library as a shared library.
             SHARED

             # Provides a relative path to your source file(s).
             src/main/cpp/native-lib.cpp )

对于add_library()括号中的内容,可以分为三个部分:

(1) 指定原生库的名字

add_library的第一个参数,决定了最终生成的共享库的名字,例如我们将共享库的名字定义为native-lib,那么最终生成的so文件将在前面加上lib前缀,也就是libnative-lib.so,但是我们在代码中加载该共享库的时候,仍然应当使用native-lib,也就是像下面这样:

static {
    System.loadLibrary(“native-lib”);
}

(2) 静态库 or 共享库

通过第二个参数,我们可以指定根据源文件编译出来的是静态库还是共享库,分别对应STATIC/SHARED关键字,这里简单提一下两者的区别:

  • 静态库:以.a结尾。静态库在程序链接的时候使用,链接器会将程序中使用到函数的代码从库文件中拷贝到应用程序中。一旦链接完成,在执行程序的时候就不需要静态库了。
  • 共享库:以.so结尾。在程序的链接时候并不像静态库那样在拷贝使用函数的代码,而只是作些标记。然后在程序开始启动运行的时候,动态地加载所需模块。

(3) 指定源文件

指定编译的源文件,这里是一个和CMakeLists.txt相关的相对路径,如果我们有多个源文件,那么就在后面添加文件的路径即可。

2.3 关联多个源文件的例子

下面,我们对 NDK 知识梳理(1) - 使用 CMake 进行 NDK 开发之初体验 中的计算器的例子进行优化,把加法和减法的操作放在另一个.cpp文件中实现,以演示关联多个.cpp文件的例子,整个目录的结构变为:

  • addition_subtraction.cpp
int addition(int a, int b) {
    return a + b;
}

int subtraction(int a, int b) {
    return a - b;
}


  • addition_subtraction.h
#ifndef CMAKEOLDDEMO_ADDITION_SUBTRACTION_H
#define CMAKEOLDDEMO_ADDITION_SUBTRACTION_H

//加法
int addition(int a, int b);

//减法
int subtraction(int a, int b);

#endif
  • calculator.cpp
#include <jni.h>
#include "../include/addition_subtraction.h"

extern "C"
JNIEXPORT jint JNICALL
Java_com_demo_lizejun_cmakeolddemo_NativeCalculator_addition(JNIEnv *env, jobject instance, jint a, jint b) {
    return addition(a, b);
}


extern "C"
JNIEXPORT jint JNICALL
Java_com_demo_lizejun_cmakeolddemo_NativeCalculator_subtraction(JNIEnv *env, jobject instance,  jint a, jint b) {
    return subtraction(a, b);
}

那么我们需要将add_library改写为:

cmake_minimum_required(VERSION 3.4.1)

add_library(calculator SHARED src/main/cpp/calculator/calculator.cpp src/main/cpp/calculator/addition_subtraction.cpp)

include_directories(src/main/cpp/include/)

三、添加 NDK API

Android系统当中,预制了一些标准的NDK库,这些库函数的目的就是让开发者能够在原生方法中实现之前在Java层开发的一些功能,我们可以通过 NDK 库 查找所需要的API

因为这些库已经预制在系统当中了,所以如果我们要调用这些库中的函数,那么不需要将其打包到APK当中,所需要做的就是向CMake提供希望使用的库名称,并将其关联到自己的原生库,最后在原生代码中引入相应的头文件,调用方法就可以了。

下面,我们就介绍一个调用Android Native API的例子。我们给Calculator加上一个新的接口,而在其本地方法中调用Android NDK的方法来打印一串Java层传过来的字符。

(1) 增加接口

我们在NativeCalculator.java中增加一个接口logByNative

public class NativeCalculator {

    private static final String SELF_LIB_NAME = "calculator";

    static {
        System.loadLibrary(SELF_LIB_NAME);
    }

    public native int addition(int a, int b);

    public native int subtraction(int a, int b);
    
    public native void logByNative(String tag, String log);

}

(2) 在 CMakeLists.txt 引入 Android NDK 的 log 库并把它和 calculator 关联

cmake_minimum_required(VERSION 3.4.1)

add_library(calculator SHARED src/main/cpp/calculator/calculator.cpp src/main/cpp/calculator/addition_subtraction.cpp)

include_directories(src/main/cpp/include/)

find_library(log-lib log)

target_link_libraries(calculator ${log-lib})

对比于之前,我们增加了下面这两句:

find_library(log-lib log)

target_link_libraries(calculator ${log-lib})

它们的作用分别是:

  • find_library:将一个变量和Android NDK的某个库建立关联关系。该函数的第二个参数为Android NDK中对应的库名称,而调用该方法之后,它就被和第一个参数所指定的变量关联在一起。
    在这种关联建立以后,我们就可以使用这个变量在构建脚本的其它部分引用该变量所关联的NDK库。
  • target_link_libraries:把NDK库和我们自己的原生库calculator进行关联,这样,我们就可以调用该NDK库中的函数了。

(3) 在 Calculator.cpp 引入头文件并调用打印 Log 的函数

#include <jni.h>
#include "../include/addition_subtraction.h"
#include <android/log.h>

extern "C"
JNIEXPORT jint JNICALL
Java_com_demo_lizejun_cmakeolddemo_NativeCalculator_addition(JNIEnv *env, jobject instance, jint a, jint b) {
    return addition(a, b);
}


extern "C"
JNIEXPORT jint JNICALL
Java_com_demo_lizejun_cmakeolddemo_NativeCalculator_subtraction(JNIEnv *env, jobject instance,  jint a, jint b) {
    return subtraction(a, b);
}

extern "C"
JNIEXPORT void JNICALL
Java_com_demo_lizejun_cmakeolddemo_NativeCalculator_logByNative(JNIEnv *env, jobject instance, jstring tag_, jstring log_) {
    const char *tag = env->GetStringUTFChars(tag_, 0);
    const char *log = env->GetStringUTFChars(log_, 0);
    __android_log_write(ANDROID_LOG_DEBUG, tag, log);
    env->ReleaseStringUTFChars(tag_, tag);
    env->ReleaseStringUTFChars(log_, log);
}

这里需要做的就是两步:

  • 引入头文件
#include <android/log.h>
  • 调用NDK库中的方法
 __android_log_write(ANDROID_LOG_DEBUG, tag, log);

(4) 在 Java 中调用,观察结果

public class MainActivity extends AppCompatActivity {

    private NativeCalculator mNativeCalculator;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mNativeCalculator = new NativeCalculator();
        Log.d("Calculator", "11 + 12 = " + (mNativeCalculator.addition(11,12)));
        Log.d("Calculator", "11 - 12 = " + (mNativeCalculator.subtraction(11,12)));
        mNativeCalculator.logByNative("Calculator", "Log By Native");
    }

}

最终的打印结果为:


四、引入第三方的.so

最后一部分,我们举一个通过第三方.so库来实现乘除法的例子,为了得到一个.so库,我们通过新建一个工程,然后将它编译出的.apk文件解压,取出其中的.so文件。

这里获得第三方so的原理其实和我们之前一直谈到的其实是一样的,我们只是借助了Android Studio来模拟了这个流程。

3.1 获得第三方 so 库

新建工程的目录结构为:


  • multiplication_division.h
#ifndef SOMAKER_MULTIPLICATION_DIVISION_H
#define SOMAKER_MULTIPLICATION_DIVISION_H

int multiplication(int a, int b);

int division(int a, int b);

#endif
  • multiplication_division.cpp
int multiplication(int a, int b) {
    return a * b;
}

int division(int a, int b) {
    return a / b;
}


  • CMakeLists.txt
cmake_minimum_required(VERSION 3.4.1)

add_library(multiplication_division SHARED src/main/cpp/multiplication_division.cpp)

include_directories(src/main/cpp/include/)

build.gradleandroid节点下,增加构建任务:

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

这个工程编译完毕之后,去app/build/outputs/apk/目录下将编译出来的APK文件解压,得到libmultiplication_division.so库,我们将它作为第三方的so库导入到计算器的例子当中。

3.2 引入第三方 so 库

(1) 将 so 库和头文件拷贝到对应的目录

(2) 修改 CMakeLists.txt 文件

cmake_minimum_required(VERSION 3.4.1)

add_library(calculator SHARED src/main/cpp/calculator/calculator.cpp src/main/cpp/calculator/addition_subtraction.cpp)

include_directories(src/main/cpp/include/)

add_library(multiplication_division SHARED IMPORTED)

set_target_properties(multiplication_division PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI}/libmultiplication_division.so )

find_library(log-lib log)

target_link_libraries(calculator multiplication_division ${log-lib})

这里相比于之前,修改了以下三句:

add_library(multiplication_division SHARED IMPORTED)

set_target_properties(multiplication_division PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI}/libmultiplication_division.so )

target_link_libraries(calculator multiplication_division ${log-lib})

这三句话的作用分别为:

  • 添加第三方so
    这里和之前在第二步中介绍的创建一个新的原生库类似,区别在于最后一个参数,我们通过IMPORTANT标志告知CMake只希望将库导入到项目中。

  • 指定目标库的路径
    这里有几点需要说明:

  • ${CMAKE_SOURCE_DIR}表示的是CMakeLists.txt所在的路径,我们指定第三方so所在路径时,应当以这个常量为起点。

  • 按理来说,我们应当为每种ABI接口提供单独的软件包,那么,我们就可以在jinLibs下建立多个文件夹,每个文件夹对应一种ABI接口类型,之后再通过${ANDROID_ABI}来泛化这一层目录的结构,这样将有助于充分利用特定的CPU架构。

  • 将第三方的库关联到原生库
    这里和将NDK库关联到原生库的原理是一样的。

(3) 声明新的接口,并在 Calculator.cpp 引入第三方的头文件,调用函数

package com.demo.lizejun.cmakeolddemo;

public class NativeCalculator {

    private static final String SELF_LIB_NAME = "calculator";

    static {
        System.loadLibrary(SELF_LIB_NAME);
    }

    public native int addition(int a, int b);

    public native int subtraction(int a, int b);

    public native void logByNative(String tag, String log);

    public native int multiplication(int a, int b);

    public native int division(int a, int b);

}
#include <jni.h>
#include "../include/addition_subtraction.h"
#include <android/log.h>
#include "../include/multiplication_division.h"

extern "C"
JNIEXPORT jint JNICALL
Java_com_demo_lizejun_cmakeolddemo_NativeCalculator_addition(JNIEnv *env, jobject instance, jint a, jint b) {
    return addition(a, b);
}


extern "C"
JNIEXPORT jint JNICALL
Java_com_demo_lizejun_cmakeolddemo_NativeCalculator_subtraction(JNIEnv *env, jobject instance,  jint a, jint b) {
    return subtraction(a, b);
}

extern "C"
JNIEXPORT void JNICALL
Java_com_demo_lizejun_cmakeolddemo_NativeCalculator_logByNative(JNIEnv *env, jobject instance, jstring tag_, jstring log_) {
    const char *tag = env->GetStringUTFChars(tag_, 0);
    const char *log = env->GetStringUTFChars(log_, 0);
    __android_log_write(ANDROID_LOG_DEBUG, tag, log);
    env->ReleaseStringUTFChars(tag_, tag);
    env->ReleaseStringUTFChars(log_, log);
}

extern "C"
JNIEXPORT jint JNICALL
Java_com_demo_lizejun_cmakeolddemo_NativeCalculator_multiplication(JNIEnv *env, jobject instance, jint a, jint b) {
    return multiplication(a, b);
}

extern "C"
JNIEXPORT jint JNICALL
Java_com_demo_lizejun_cmakeolddemo_NativeCalculator_division(JNIEnv *env, jobject instance, jint a, jint b) {
    return division(a, b);
}

之前的步骤完成之后就很简单了,我们只需要引入该so库对应的头文件,再调用它提供的方法就可以了。

(4) 在 Java 中调用本地方法

public class MainActivity extends AppCompatActivity {

    private NativeCalculator mNativeCalculator;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mNativeCalculator = new NativeCalculator();
        Log.d("Calculator", "11 + 12 = " + (mNativeCalculator.addition(11,12)));
        Log.d("Calculator", "11 - 12 = " + (mNativeCalculator.subtraction(11,12)));
        mNativeCalculator.logByNative("Calculator", "Log By Native");
        Log.d("Calculator", "11 * 12 = " + (mNativeCalculator.multiplication(11,12)));
        Log.d("Calculator", "11 / 12 = " + (mNativeCalculator.division(11,12)));
    }

}

运行程序,最终打印的结果为:


四、小结

这一篇文章,我们简要地总结了CMakeLists.txt在几种场景下应该如何编写。在学习的过程中,感觉之前学的C/C++都忘光了,头文件、静态库/动态库、extern关键字,都不记得了,打算先好好复习一下相关的知识再继续NDK的学习了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容