学习笔记,CMake的实战项目演示

289 阅读10分钟

CMake的配置与编译实战

前言

在现代软件开发中,构建系统是确保代码能够顺利编译和部署的关键环节。CMake 作为一个跨平台的构建工具,因其灵活性和强大的功能而被广泛应用于各类项目中。无论是简单的库、复杂的应用程序,还是移动开发、嵌入式系统,CMake 都能够提供高效的构建解决方案。

在前文中我们主要是回顾了 CMake 的基础编译命令,基础语法与逻辑处理,CMake 的常用命令、内置变量等概念,以及 Cmake 的项目管理方式。

那么应用到实战项目中我们到底要如何使用呢?这一期我们就根据几个项目来展示从易到难以及在不同平台下演示对应的配置方式。

本文的大纲:

  1. 在Android Studio (AS) 中如何链接本地 NDK 的C库功能,
  2. 在 AS 中如何链接第三方库的源码,如何打包为单个so文件或多个so文件
  3. 在 AS 中如何链接已有的so 动态库
  4. 在 Linux 系统中如何编译一个第三方 C 库,如何交叉编译?

接下来先从 Android 平台开始,因为它够简单不涉及到编译(NDK帮我们都做了),我们只是需要了解配置即可。

由于或多或少涉及到一些 NDK/JNI 的知识,由于我还没有讲到这一块,大家可以先看看知道具体的用处即可,即便不会 JNI/NDK 同样能看懂配置,今天的主题是 CMake 的配置,就不展开偏题了。

话不多说了,开始了。

一、在AS中的简单配置

我们可以在 AS 中创建项目的时候选择 Include C++ support 选项,这样项目创建的时候会帮我们创建好对应的 cpp 目录和 CMakeList.txt 配置文件。

如果是老项目,我们一样可以在 src/main 中创建 cpp 文件夹,并且创建对应的 CMakeList.txt 配置文件与对应的主cpp文件与对应的 JNI 桥接。

如果你还不了解 JNI 相关的知识点,我们先选择 Include C++ support 的方式创建项目,项目会自动帮我们桥接好对应的入口。

目录如下:

image.png

此时我们可以先演示从 C 中传递一个字符串显示的功能。

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

这就是自动生成的 JNI 桥接方法,那么在 CMakeList 的配置文件中:

cmake_minimum_required(VERSION 3.4.1)

project("native-lib")

# 指定so生成目录
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/libs/${ANDROID_ABI})

# 配置so库的信息
add_library(
        #这里生产的so库名称将为libnative-lib.so
        native-lib

        SHARED

        native-lib.cpp )

# 从系统库中查找依赖库
find_library(
        # 设置依赖库的名字,下面链接库的时候会用到
        log-lib

        # 查找log依赖库
        # {sdk-path}/ndk-bundle/sysroot/usr/include/android/log.h
        log )

# 配置库的依赖关系
target_link_libraries(
        native-lib

        ${log-lib} )

这里就很完美的应用到我们之前学习的知识点了,为什么用 find_library 是因为这是 Android 环境,NDK 内置了一些库我们可以直接使用,这里以 log 库为演示

这样我们生成的 native-lib 的 so 库就会链接到 log 库和我们自定义的 native-lib.cpp 文件。

此时就可以运行了:

image.png

咦?既然我们依赖了 Log 库,我们怎么没有用到呢?来了老弟。

我么都知道我们可以在 c/c++ 代码中用 print 函数输出 log 信息,但是这样在 AS 的 logcat 并不会显示,好在 Android 已经给我提供了相应的方法解决这个问题:使用log.h头文件,在前文中我们已经链接到了系统的 log 库,接下来我们就可以直接导入头文件使用啦。

#include <jni.h>
#include <string>
#include <android/log.h>

const char * LOG_TGA = "LOG_TGA";  //定义日志的TAG

extern "C" JNIEXPORT jstring

JNICALL
Java_com_example_testnative_MainActivity_stringFromJNI(
        JNIEnv *env,
        jobject /* this */) {
    std::string hello = "Hello from C++";

    //输出debug级别的日志信息
    __android_log_print(ANDROID_LOG_DEBUG, LOG_TGA, "hello form native log");

    return env->NewStringUTF(hello.c_str());
}

这样我们就可以打印出 LOG 啦!

image.png

那么除了 Android 内置的 log 库,还有哪些库呢?多的去了,比如我们的图片 Bitmap 处理,也可以用同样的方式处理。

我们在 CMakeList 的配置中加入 Bitmap 的依赖

cmake_minimum_required(VERSION 3.4.1)

project("native-lib")

# 指定so生成目录
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/libs/${ANDROID_ABI})

# 配置so库的信息
add_library(
        #这里生产的so库名称将为libnative-lib.so
        native-lib

        SHARED

        native-lib.cpp )

# 从系统库中查找依赖库
find_library(
        # 设置依赖库的名字,下面链接库的时候会用到
        log-lib

        # 查找log依赖库
        # {sdk-path}/ndk-bundle/sysroot/usr/include/android/log.h
        log )

# 配置库的依赖关系,
target_link_libraries(
        native-lib

        android
        #这里可以用 log-lib 的方式去 find_library 链接到,可以直接用 jnigraphics 的便捷方式都可以!
        jnigraphics
        ${log-lib} )

在 MainActivity 中添加处理图片的方法:

    external fun nativeProcessBitmap(bitmap: Bitmap)

点击 alt + enter 即可自动生成对应的JNI处理:

然后我们从网上 COPY 一个彩色图片换为灰色的处理算法:

#include <jni.h>
#include <string>
#include <android/log.h>
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <android/bitmap.h>

#define MAKE_RGB565(r, g, b) ((((r) >> 3) << 11) | (((g) >> 2) << 5) | ((b) >> 3))
#define MAKE_ARGB(a, r, g, b) ((a&0xff)<<24) | ((r&0xff)<<16) | ((g&0xff)<<8) | (b&0xff)

#define RGB565_R(p) ((((p) & 0xF800) >> 11) << 3)
#define RGB565_G(p) ((((p) & 0x7E0 ) >> 5)  << 2)
#define RGB565_B(p) ( ((p) & 0x1F  )        << 3)

#define RGB8888_A(p) (p & (0xff<<24) >> 24 )
#define RGB8888_R(p) (p & (0xff<<16) >> 16 )
#define RGB8888_G(p) (p & (0xff<<8)  >> 8 )
#define RGB8888_B(p) (p & (0xff) )

#define RGBA_A(p) (((p) & 0xFF000000) >> 24)
#define RGBA_R(p) (((p) & 0x00FF0000) >> 16)
#define RGBA_G(p) (((p) & 0x0000FF00) >>  8)
#define RGBA_B(p)  ((p) & 0x000000FF)
#define MAKE_RGBA(r, g, b, a) (((a) << 24) | ((r) << 16) | ((g) << 8) | (b))


extern "C"
JNIEXPORT void JNICALL
Java_com_example_testnative_MainActivity_nativeProcessBitmap(JNIEnv *env, jobject instance, jobject bitmap) {
    if (bitmap == NULL) {
        return;
    }

    AndroidBitmapInfo bitmapInfo;
    memset(&bitmapInfo, 0, sizeof(bitmapInfo));
    // Need add "jnigraphics" into target_link_libraries in CMakeLists.txt
    AndroidBitmap_getInfo(env, bitmap, &bitmapInfo);
    // Lock the bitmap to get the buffer
    void *pixels = NULL;
    int res = AndroidBitmap_lockPixels(env, bitmap, &pixels);
    // From top to bottom
    int x = 0, y = 0;
    for (y = 0; y < bitmapInfo.height; ++y) {
        // From left to right
        for (x = 0; x < bitmapInfo.width; ++x) {
            int a = 0, r = 0, g = 0, b = 0;
            void *pixel = NULL;
            // Get each pixel by format
            if (bitmapInfo.format == ANDROID_BITMAP_FORMAT_RGBA_8888) {
                pixel = ((uint32_t *) pixels) + y * bitmapInfo.width + x;
                int r, g, b;
                uint32_t v = *((uint32_t *) pixel);
                r = RGB8888_R(v);
                g = RGB8888_G(v);
                b = RGB8888_B(v);
                int sum = r + g + b;
                *((uint32_t *) pixel) = MAKE_ARGB(0xff, sum / 3, sum / 3, sum / 3);
            } else if (bitmapInfo.format == ANDROID_BITMAP_FORMAT_RGB_565) {
                pixel = ((uint16_t *) pixels) + y * bitmapInfo.width + x;
                int r, g, b;
                uint16_t v = *((uint16_t *) pixel);
                r = RGB565_R(v);
                g = RGB565_G(v);
                b = RGB565_B(v);
                int sum = r + g + b;
                *((uint16_t *) pixel) = MAKE_RGB565(sum / 3, sum / 3, sum / 3);
            }
        }
    }
    AndroidBitmap_unlockPixels(env, bitmap);

}

在 MainActivity 中获取 Bitmap 展示 Bitmap 的逻辑如下:


class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.sampleText.text = stringFromJNI()

        //获取Bitmap,处理Bitmap,展示Bitmap
        val bitmap = BitmapFactory.decodeResource(resources, R.drawable.chenxiao)
        binding.ivImg.setImageBitmap(bitmap)
        binding.button.setOnClickListener {
            binding.ivImg.setImageBitmap(handleBitmap(bitmap))
        }
    }

    /**
     * Native处理图片。
     */
    private fun handleBitmap(bitmap: Bitmap) : Bitmap {
        val bmp = bitmap.copy(Bitmap.Config.ARGB_8888, true);
        nativeProcessBitmap(bmp);
        return bmp
    }


    external fun stringFromJNI(): String

    external fun nativeProcessBitmap(bitmap: Bitmap)

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

效果如下:

Screen_recording_20250509_175320.gif

二、在AS中导入第三方库的配置

那有同学可能会说了,你这都是用的 Android 的内置库,配置太简单了,复杂点的怎么搞?

2.1 源码集成

确实 Android 内置的库都是一些基本的功能,如果是我们自己写的 C 代码或者第三方的代码源码我们怎么集成呢?

例如在前文中我们讲到 CMake 的时候自己写了一个 Log 打印加上当前时间的格式化工具类,我们可以修改一下拿过来集成试试。

需要注意的是之前的 C 代码的 Log 是用 cout 函数打印的,但是这里的 cout 无法在 Android Studio 的 安卓项目中显示,所以我修改了一下,不直接打印而是格式化字符串返回。

print.h:

#include <string>
std::string log(const std::string& message);  // 返回字符串类型

print.cpp:

#include "../include/print.h"
#include <sstream>  // 必须添加

std::string log(const std::string& message) {
    time_t now = time(0);
    tm *localTime = localtime(&now);

    char buffer[80];
    strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", localTime);

    std::stringstream ss;
    ss << "[" << buffer << "] " << message;  // 流式拼接字符串
    return ss.str();
}

print 模块的 CMakelist.txt 的内容:

cmake_minimum_required(VERSION 3.4.1)

project(print)

add_library(print STATIC src/print.cpp)

我们直接 add_library 即可,在主项目的 CMakeList.txt 中我们直接依赖这个 print 项目即可。

依赖后的项目如下:

image.png

在 CMake 的文章中 【传送门】 我们介绍过它依赖三方库的几种方式。

不管是源码路径依赖,还是 add_subdirectory 的方式都可行,我这里使用的是 add_subdirectory 的方式,完全是按照之前定义的规律来。

cmake_minimum_required(VERSION 3.4.1)

project("native-lib")


# 配置so库的信息
add_library(
        #这里生产的so库名称将为libnative-lib.so
        native-lib
        SHARED
        native-lib.cpp
)

# 从系统库中查找依赖库
find_library(
        # 设置依赖库的名字,下面链接库的时候会用到
        log-lib
        # 查找log依赖库
        # {sdk-path}/ndk-bundle/sysroot/usr/include/android/log.h
        log
)

# 使用相对目录直接引入项目
add_subdirectory(print)

# 配置库的依赖关系
target_link_libraries(
        native-lib
        print
        android
        #这里可以用 log-lib 的方式去 find_library 链接到,可以直接用 jnigraphics 的便捷方式都可以!
        jnigraphics
        ${log-lib}
)


target_include_directories(native-lib PUBLIC
        "${CMAKE_CURRENT_SOURCE_DIR}/print/include"
)

我们在 native-lib.cpp 中测试一下使用:

#include "print.h"

const char * LOG_TGA = "LOG_TGA";  //定义日志的TAG

extern "C" JNIEXPORT jstring
JNICALL
Java_com_example_testnative_MainActivity_stringFromJNI(
        JNIEnv *env,
        jobject /* this */) {
    std::string hello = "Hello from C++";

    //输出debug级别的日志信息
    __android_log_print(ANDROID_LOG_DEBUG, LOG_TGA, "hello form native log");

    std::string combinedStr = std::string("测试Print库的信息 - ") + hello;
    std::string logStr = log(combinedStr);

    return env->NewStringUTF(logStr.c_str());
}

此时返回的字符串前面应该是有时间格式化的,我们看看效果:

image.png

它的编译处理流程,通过 add_library(print STATIC ...) 会生成 libprint.a(编译中间产物)

其实我们在 debug 包中看到编译后的产物也是可以看到临时文件,他是可忽略的。

image.png

但在最终 APK 中,libprint.a 不会独立存在,其代码会被 完全合并 到 libnative-lib.so

image.png

如果我们选择使用 SHARED 的方式 add_library 那么就会生成两个 so 文件

image.png

这都是我们在 CMake 的文章中讲到过的,这里复习并实战演示一下。

那么在AS 中导入第三方源码的测试流程就结束了。

那么可能有同学又要问了,我的 Native 同学已经帮我打包好了so,或者我用的第三方作者已经提供了 so 文件,那么我在 AS 中如何集成呢?

2.2 动态库集成

这可能是更多开发者遇到的问题,因为其实大部分情况下我们都是直接使用别人已经编译好的 so。其实集成起来也很简单。

下面我们用一个著名的第三方开源库 fdk-aac 的动态库加头文件来演示。

我们导入 fdk-aac 的 so 文件以及对应的头文件

image.png

这里我只找到了 armeabi-v7a 的包,所以我这里只演示它,需要我们在项目中设定 ndk 为 armeabi-v7a 才行哦。

还是之前的 CMakeLists.txt 我们直接添加进去,不同的是之前我们用的是 add_subdirectory 直接引入项目,这里我们使用的动态库不是一个标准的项目我们就使用的是第一种方案直接路径指定。

cmake_minimum_required(VERSION 3.4.1)

project("native-lib")

#配置动态链接库对应头文件的目录
include_directories(${PROJECT_SOURCE_DIR}/include)

# 配置so库的信息
add_library(
        #这里生产的so库名称将为libnative-lib.so
        native-lib
        SHARED
        native-lib.cpp
        fdkcodec.cpp
)

# 添加 fdk-aac 库,使用 ANDROID_ABI 变量构建路径
add_library(fdk-aac
        SHARED
        IMPORTED)

set_target_properties(
        fdk-aac
        PROPERTIES IMPORTED_LOCATION
        ${PROJECT_SOURCE_DIR}/../jniLibs/${ANDROID_ABI}/libfdk-aac.so)

# 从系统库中查找依赖库
find_library(
        # 设置依赖库的名字,下面链接库的时候会用到
        log-lib
        # 查找log依赖库
        # {sdk-path}/ndk-bundle/sysroot/usr/include/android/log.h
        log
)


# 使用相对目录直接引入项目
add_subdirectory(print)

# 配置库的依赖关系
target_link_libraries(
        native-lib
        fdk-aac
        print

        android
        #这里可以用 log-lib 的方式去 find_library 链接到,可以直接用 jnigraphics 的便捷方式都可以!
        jnigraphics
        ${log-lib}
)


target_include_directories(native-lib PUBLIC
        "${CMAKE_CURRENT_SOURCE_DIR}/print/include"
)

都加上了详细的注释,这样我们就可以运行啦。

打包之后的效果是:

image.png

我们通过 FDKAACUtil 类就可以调用对应的 JNI 类了,这里就不展开了。

三、在Linux中编译第三方库

之前我们都是在 Android Studio 中通过内置的 gradle 帮我们编译完成了,那么如果我们想自己编译项目怎么完成呢?

比如我们想要编译一个 OpenCV 的开源库,我们在 Linux 中想要编译怎么操作?

其实是和我们之前学习的 Cmake 一样的套路。下载源码,创建build文件夹,预编译(配置参数),然后编译。

只是第三方库比较复杂需要额外的依赖条件之类的,编译的过程慢一些,本质是一样的,这里我演示一下:

我们可以先预先安装一下一些基本的依赖库

# 基础编译工具
sudo apt-get update
sudo apt-get install -y build-essential cmake git pkg-config

# 常用依赖库
sudo apt-get install -y libjpeg-dev libpng-dev libtiff-dev
sudo apt-get install -y libavcodec-dev libavformat-dev libswscale-dev libv4l-dev
sudo apt-get install -y libxvidcore-dev libx264-dev libgtk-3-dev

image.png

然后就是下载 opencv 的源代码了:

image.png

下载半天失败了,网络不行,这里我直接把之前下载的源码SSH过来吧:

image.png

我们就可以先创建 build 文件并且进行预编译生成对应的 makefile:

image.png

预编译完成:

image.png

这里我是直接编译的,其实可以指定一些简单的参数,比如生成静态库还是动态库之类的。

cmake -D CMAKE_BUILD_TYPE=RELEASE \
      -D BUILD_SHARED_LIBS=ON \
      -D BUILD_STATIC_LIBS=OFF \
      -D OPENCV_GENERATE_PKGCONFIG=ON \
      ..

Makefile已经有了,直接全量编译:

image.png

然后就是等待编译完成,我虚拟机的垃圾配置第一次全编等待 45 分钟左右才编译完成:

动态库和静态库:

image.png

可执行文件:

image.png

这是默认编译为当前环境的,如果我们想编译为其他平台的,我们就可以用到交叉编译,例如 ARM 的树莓派。

我们可以先安装对应的交叉编译链:

sudo apt-get install gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf

我们创建一个工具链文件 toolchain.cmake:

set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR arm) # 根据目标平台设置

# 设置交叉编译器
set(CMAKE_C_COMPILER /usr/bin/arm-linux-gnueabihf-gcc)
set(CMAKE_CXX_COMPILER /usr/bin/arm-linux-gnueabihf-g++)

那么在预编译阶段我们的参数就需要改一下:

cmake -D CMAKE_BUILD_TYPE=RELEASE \
      -D CMAKE_TOOLCHAIN_FILE=/path/to/toolchain.cmake
      -D BUILD_SHARED_LIBS=ON \
      -D BUILD_STATIC_LIBS=OFF \
      -D OPENCV_GENERATE_PKGCONFIG=ON \
      ..

预编译完成之后就是和上面一样的编译流程

make -j$(nproc)

此时编译出来的就是对应平台可用的产物了:

image.png

image.png

总结

在这篇文章中,我们深入探讨了 CMake 在 Android 开发和 Linux 环境中如何配置与编译的实战应用。通过逐步演示,我们可以了解到从简单的 JNI 桥接到复杂的第三方库集成,CMake 的强大功能和灵活性。

在 Android 项目中通过使用 find_library 查找并链接 Android 内置的库,如 log 和 jnigraphics,能够有效利用 NDK 提供的功能。

在 Android 项目中想要集成第三方库或自定义库,可以通过通过 add_library 和 add_subdirectory 的方式集成自定义 C++ 库。也可以通过动态库和静态库的方式集成,可以通过不同的依赖方式配置 CMake 以支持不同类型的库,确保代码的灵活性和可复用性。

在 Linux 系统中我们描述了如何在 Linux 中使用 CMake 编译 OpenCV 等第三方库,以及编译不同平台的方法,交叉编译的概念。

Cmake 的本质就是提高开发效率,简化构建过程。希望通过不同的示例我们可以掌握 CMake 的基本用法和平台适配能力,以及依赖方式的不同用法与对应的编译步骤。

在 Android Studio 中的配置,我们会接触到一些 NDK 相关的知识点,JNI相关语法之类的,我这里没有过多的深入。在 Linux 系统中想要编译出 Android 系统中可用的动态库也需要交叉编译用到 Android NDK相关的地方,由于我们还没有复习到这一块,所以我都是尽量跳过,请大家见谅后期会出 NDK 相关的专题文章给大家补上。

如果你看文章感觉前言不接后尾,可能是没看过前文,推荐从前文 Cmake 的使用详情那一篇文章看起,本篇文章本质上就是前文的各场景下的示例而已。

那么文章到这里就到尾声了,如果在阅读过程中遇到任何代码或理解有误解的地方,或者在代码、注释中发现错误和遗漏,都可以在评论区指出并进行修正。

如果您觉得本文对您有启发和帮助,请给我一个点赞以示支持!您的反馈才是我最大的动力。

Ok,完结撒花啦。