Android NDK 开发之 CMake 必知必会

news2024/9/22 13:30:54

Android Studio 从 2.2 版本起开始支持 CMake ,可以通过 CMake 和 NDK 将 C/C++ 代码编译成底层的库,然后再配合 Gradle 的编译将库打包到 APK 中。

这意味就不需要再编写 .mk 文件来编译 so 动态库了。

CMake 是一个跨平台构建系统,在 Android Studio 引入 CMake 之前,它就已经被广泛运用了。

Google 官方网站上有对 CMake 的使用示范,可以参考 官方指南。

总结官网对 CMake 的使用,其实也就如下的步骤:

  1. add_library 指定要编译的库,并将所有的 .c.cpp 文件包含指定。
  2. include_directories 将头文件添加到搜索路径中
  3. set_target_properties 设置库的一些属性
  4. target_link_libraries 将库与其他库相关联

如果你对上面的步骤还是不了解,那么接下来就更深入了解 CMake 相关内容吧~~~

CMake 的基本操作

以 Clion 作为工具来讲解 CMake 的基本使用。

 

CMake 编译可执行文件

一个打印 hello world 的 cpp 文件,通过 CMake 将它编译成可执行文件。

在 cpp 的同一目录下创建 CMakeLists.txt 文件,内容如下:

# 指定 CMake 使用版本
cmake_minimum_required(VERSION 3.9)
# 工程名
project(HelloCMake)
# 编译可执行文件
add_executable(HelloCMake main.cpp )

其中,通过 cmake_minimum_required 方法指定 CMake 使用版本,通过 project 指定工程名。

add_executable 就是指定最后编译的可执行文件名称和需要编译的 cpp 文件,如果工程很大,有多个 cpp 文件,那么都要把它们添加进来。

定义了 CMake 文件之后,就可以开始编译构建了。

CMake 在构建工程时会生成许多临时文件,避免让这些临时文件污染代码,一般会把它们放到一个单独的目录中。

操作步骤如下:

# 在 cpp 目录下创建 build 目录
mkdir build
# 调用 cmake 命令生成 makefile 文件
cmake ..
# 编译
make

在 build 目录中可以找到最终生成的可执行文件。

这就是 CMake 的一个简单操作,将 cpp 编译成可执行文件,但在 Android 中,大多数场景都是把 cpp 编译成库文件。

【学习地址】:FFmpeg/WebRTC/RTMP/NDK/Android音视频流媒体高级开发
【文章福利】:免费领取更多音视频学习资料包、大厂面试题、技术视频和学习路线图,资料包括(C/C++,Linux,FFmpeg webRTC rtmp hls rtsp ffplay srs 等等)有需要的可以点击1079654574加群领取哦~

  

CMake 编译静态库和动态库

同样还是一个 cpp 文件和一个 CMake 文件,cpp 文件内容为打印字符串的函数:

#include <iostream>
void print() {
    std::cout << "hello lib" << std::endl;
}

同时,CMake 文件也要做相应更改:

cmake_minimum_required(VERSION 3.12)
# 指定编译的库和文件,SHARED 编译动态库
add_library(share_lib SHARED lib.cpp)
# STATIC 编译静态库
# add_library(share_lib STATIC lib.cpp)

通过 add_library 指定要编译的库的名称,以及动态库还是静态库,还有要编译的文件。

最后同样地执行构建,在 build 目录下可以看到生成的库文件。

到这里,就基本可以使用 CMake 来构建 C/C++ 工程了。

CMake 基本语法

熟悉了上面的基本操作之后,就必然会遇到以下的问题了:

  • 如果要参与编译的 C/C++ 文件很多,难道每个都要手动添加嘛?
  • 可以把编译好的可执行文件或者库自动放到指定位置嘛?
  • 可以把编译好的库指定版本号嘛?

带着这些问题,还是要继续深入学习 CMake 的相关语法,最好的学习材料就是 官网文档 了。

为了避免直接看官方文档时一头雾水,这里列举一些常用的语法命令。

注释与大小写

在前面就已经用到了 CMake 注释了,每一行的开头 # 代表注释。

另外,CMake 的所有语法指令是不区分大小写的。

变量定义与消息打印

通过 set 来定义变量:

# 变量名为 var,值为 hello
set(var hello) 

当需要引用变量时,在变量名外面加上 ${} 符合来引用变量。

# 引用 var 变量
${var}

还可以通过 message 在命令行中输出打印内容。

set(var hello) 
message(${var})

数学和字符串操作

数学操作

CMake 中通过 math 来实现数学操作。

# math 使用,EXPR 为大小
math(EXPR <output-variable> <math-expression>)
math(EXPR var "1+1")
# 输出结果为 2
message(${var})

math 支持 +, -, *, /, %, |, &, ^, ~, <<, >> 等操作,和 C 语言中大致相同。

字符串操作

CMake 通过 string 来实现字符串的操作,这波操作有很多,包括将字符串全部大写、全部小写、求字符串长度、查找与替换等操作。

具体查看 官方文档。

set(var "this is  string")
set(sub "this")
set(sub1 "that")
# 字符串的查找,结果保存在 result 变量中
string(FIND ${var} ${sub1} result )
# 找到了输出 0 ,否则为 -1
message(${result})

# 将字符串全部大写
string(TOUPPER ${var} result)
message(${result})

# 求字符串的长度
string(LENGTH ${var} num)
message(${num})

另外,通过空白或者分隔符号可以表示字符串序列。

set(foo this is a list) // 实际内容为字符串序列
message(${foo})

当字符串中需要用到空白或者分隔符时,再用双括号""表示为同一个字符串内容。

set(foo "this is a list") // 实际内容为一个字符串
message(${foo})

文件操作

CMake 中通过 file 来实现文件操作,包括文件读写、下载文件、文件重命名等。

具体查看 官方文档

# 文件重命名
file(RENAME "test.txt" "new.txt")

# 文件下载
# 把文件 URL 设定为变量
set(var "http://img.zcool.cn/community/0117e2571b8b246ac72538120dd8a4.jpg")

# 使用 DOWNLOAD 下载
file(DOWNLOAD ${var} "/Users/glumes/CLionProjects/HelloCMake/image.jpg")

在文件的操作中,还有两个很重要的指令 GLOBGLOB_RECURSE

# GLOB 的使用
file(GLOB ROOT_SOURCE *.cpp)
# GLOB_RECURSE 的使用
file(GLOB_RECURSE CORE_SOURCE ./detail/*.cpp)

其中,GLOB 指令会将所有匹配 *.cpp 表达式的文件组成一个列表,并保存在 ROOT_SOURCE 变量中。

GLOB_RECURSE 指令和 GLOB 类似,但是它会遍历匹配目录的所有文件以及子目录下面的文件。

使用 GLOBGLOB_RECURSE 有好处,就是当添加需要编译的文件时,不用再一个一个手动添加了,同一目录下的内容都被包含在对应变量中了,但也有弊端,就是新建了文件,但是 CMake 并没有改变,导致在编译时也会重新产生构建文件,要解决这个问题,就是动一动 CMake,让编译器检测到它有改变就好了。

预定义的常量

在 CMake 中有许多预定义的常量,使用好这些常量能起到事半功倍的效果。

  • CMAKE_CURRENT_SOURCE_DIR
    • 指当前 CMake 文件所在的文件夹路径
  • CMAKE_SOURCE_DIR
    • 指当前工程的 CMake 文件所在路径
  • CMAKE_CURRENT_LIST_FILE
    • 指当前 CMake 文件的完整路径
  • PROJECT_SOURCE_DIR
    • 指当前工程的路径

比如,在 add_library 中需要指定 cpp 文件的路径,以 CMAKE_CURRENT_SOURCE_DIR 为基准,指定 cpp 相对它的路径就好了。

# 利用预定义的常量来指定文件路径
add_library( # Sets the name of the library.
             openglutil
             # Sets the library as a shared library.
             SHARED
             # Provides a relative path to your source file(s).
             ${CMAKE_CURRENT_SOURCE_DIR}/opengl_util.cpp
             )

平台相关的常量

CMake 能够用来在 Window、Linux、Mac 平台下进行编译,在它的内部也定义了和这些平台相关的变量。

具体查看 官方文档 。

列举一些常见的:

  • WIN32
    • 如果编译的目标系统是 Window,那么 WIN32 为 True 。
  • UNIX
    • 如果编译的目标系统是 Unix 或者类 Unix 也就是 Linux ,那么 UNIX 为 True 。
  • MSVC
    • 如果编译器是 Window 上的 Visual C++ 之类的,那么 MSVC 为 True 。
  • ANDROID
    • 如果目标系统是 Android ,那么 ANDROID 为 1 。
  • APPLE
    • 如果目标系统是 APPLE ,那么 APPLE 为 1 。

有了这些常量做区分,就可以在一份 CMake 文件中编写不同平台的编译选项。

if(WIN32){
    # do something
}elseif(UNIX){
    # do something
}

函数、宏、流程控制和选项 等命令

具体参考cmake-commands ,这里面包括了很多重要且常见的指令。

简单示例 CMake 中的函数操作:

function(add a b)
    message("this is function call")
    math(EXPR num "${a} + ${b}" )
    message("result is ${aa}")
endfunction()

add(1 2)

其中,function 为定义函数,第一个参数为函数名称,后面为函数参数。

在调用函数时,参数之间用空格隔开,不要用逗号。

宏的使用与函数使用有点类似:

macro(del a b)
    message("this is macro call")
    math(EXPR num "${a} - ${b}")
    message("num is ${num}")
endmacro()

del(1 2)

在流程控制方面,CMake 也提供了 if、else 这样的操作:

set(num 0)
if (1 AND ${num})
    message("and operation")
elseif (1 OR ${num})
    message("or operation")
else ()
    message("not reach")
endif ()

其中,CMake 提供了 ANDORNOTLESSEQUAL 等等这样的操作来对数据进行判断,比如 AND 就是要求两边同为 True 才行。

另外 CMake 还提供了循环迭代的操作:

set(stringList this is string list)
foreach (str ${stringList})
    message("str is ${str}")
endforeach ()

CMake 还提供了一个 option 指令。

可以通过它来给 CMake 定义一些全局选项:

option(ENABLE_SHARED "Build shared libraries" TRUE)

if(ENABLE_SHARED)
    # do something
else()
    # do something   
endif()

可能会觉得 option 无非就是一个 True or False 的标志位,可以用变量来代替,但使用变量的话,还得添加 ${} 来表示变量,而使用 option 直接引用名称就好了。

CMake 阅读实践

明白了上述的 CMake 语法以及从官网去查找陌生的指令意思,就基本上可以看懂大部分的 CMake 文件了。

这里举两个开源库的例子:

  • github.com/g-truc/glm
    • glm 是一个用来实现矩阵计算的,在 OpenGL 的开发中会用到。
    • CMakeLists.txt 地址在 这里
  • github.com/libjpeg-tur…
    • libjpeg-turbo 是用来进行图片压缩的,在 Android 底层就是用的它。
    • CMakeLists.txt 地址在 这里

这两个例子中大量用到了前面所讲的内容,可以试着读一读增加熟练度。

为编译的库设置属性

接下来再回到用 CMake 编译动态库的话题上,毕竟 Android NDK 开发也主要是用来编译库了,当编译完 so 之后,我们可以对它做一些操作。

通过 set_target_properties 来给编译的库设定相关属性内容,函数原型如下:

set_target_properties(target1 target2 ...
                      PROPERTIES prop1 value1
                      prop2 value2 ...)

比如,要将编译的库改个名称:

set_target_properties(native-lib PROPERTIES OUTPUT_NAME "testlib" )

更多的属性内容可以参考 官方文档。

不过,这里面有一些属性设定无效,在 Android Studio 上试了无效,在 CLion 上反而可以,当然也可能是我使用姿势不对。

比如,实现动态库的版本号:

set_target_properties(native-lib PROPERTIES VERSION 1.2 SOVERSION 1 )

对于已经编译好的动态库,想要把它导入进来,也需要用到一个属性。

比如编译的 FFmpeg 动态库,

# 使用 IMPORTED 表示导入库
add_library(avcodec-57_lib SHARED IMPORTED)
# 使用 IMPORTED_LOCATION 属性指定库的路径
set_target_properties(avcodec-57_lib PROPERTIES IMPORTED_LOCATION
                        ${CMAKE_CURRENT_SOURCE_DIR}/src/main/jniLibs/armeabi/libavcodec-57.so )

链接到其他的库

如果编译了多个库,并且想库与库之间进行链接,那么就要通过 target_link_libraries

target_link_libraries( native-lib
                       glm
                       turbojpeg
                       log )

在 Android 底层也提供了一些 so 库供上层链接使用,也要通过上面的方式来链接,比如最常见的就是 log 库打印日志。

如果要链接自己编译的多个库文件,首先要保证每个库的代码都对应一个 CMakeLists.txt 文件,这个 CMakeLists.txt 文件指定当前要编译的库的信息。

然后在当前库的 CMakeLists.txt 文件中通过 ADD_SUBDIRECTORY 将其他库的目录添加进来,这样才能够链接到。

ADD_SUBDIRECTORY(src/main/cpp/turbojpeg)
ADD_SUBDIRECTORY(src/main/cpp/glm)

添加头文件

在使用的时候有一个容易忽略的步骤就是添加头文件,通过 include_directories 指令把头文件目录包含进来。

这样就可以直接使用 #include "header.h" 的方式包含头文件,而不用 #include "path/path/header.h" 这样添加路径的方式来包含。

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/69359.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

12月7日 补充数据库链式操作,只生成sql语句,不执行sql的几种函数以及thinkphp中验证器

补充知识&#xff1a; 生成sql语句但是不会执行sql语句 public function fetchSqlTest(){$result Db::name(user)->fetchSql(true)->select();//dump($result);//使用正常select查询默认中select(true&#xff09;改为false即可返回sql语句$result Db::name(user)->…

UNIX/LINUX操作系统内核结构

通用操作系统的现状和分类 UNIX操作系统的根本特点&#xff1a;分时多用户、开放性 基本功能特征 系统结构 操作系统核心 系统调用的集合及实现系统调用的内部算法就形成操作系统核心 系统调用 以函数形式提供给核外的命令和上层应用系统使用的一组程序&#xff0c;涵…

观察者模式

目录 一、观察者模式解决了哪些问题&#xff1a; 二、体检的小栗子 三、什么是观察者模式 四、代码实现 一、观察者模式解决了哪些问题&#xff1a; 对象之间的一对多依赖关系应该在不使对象紧密耦合的情况下定义。应该确保当一个对象改变状态时&#xff0c;自动更新无限数…

5-5:显示系统的通知

通知列表 显示评论、点赞、关注三种类型的通知。 通知详情 分页显示某一类主题所包含的通知。 未读消息 在页面头部显示所有的未读消息数量 通知存在message表里面 修改MessageMapper.java package com.nowcoder.community.dao;import com.nowcoder.community.entity.Messa…

毕业2年,跳槽到我们公司拿18K,这就是00后卷王带来的压迫感吗?

内卷&#xff0c;是现在热度非常高的一个词汇&#xff0c;随着热度不断攀升&#xff0c;隐隐到了“万物皆可卷”的程度。 在程序员职场上&#xff0c;什么样的人最让人反感呢? 是技术不好的人吗?并不是。技术不好的同事&#xff0c;我们可以帮他。 是技术太强的人吗?也不…

AI 也会写代码了,但我并不担心

如果你比较关注人工智能&#xff0c;可能会注意到最近圈子的人都在刷屏一个 AI 玩意&#xff0c;叫 ChatGPT 。我一直关注的和菜头在他的公众号槽边往事上就接连写了几篇文章。为什么和菜头是男的&#xff0c;读者却要叫他“婶婶”怎么把AI逼到生气水文与干货如果看完上面的第一…

一行代码 网页变灰

背景 想必大家都感受到了&#xff0c;很多网站、APP 在昨天都变灰了。 先来感受一下变灰后的效果。 让网页变为黑白配色&#xff0c;是个常见的诉求。而且往往是突如其来的诉求&#xff0c;是无法预知的。 当发生这样的需求时&#xff0c;我们需要迅速完成变更发布。 这种…

机器学习 决策树 随机森林

文章目录参考决策树指标基尼系数基础公式公式理解引入划分后的公式划分后公式的理解信息熵、信息增益如何理解信息熵两种指标的对比总结参考 b站视频 【决策树、随机森林】附源码&#xff01;&#xff01;超级简单&#xff0c;同济大佬手把手带你学决策树决策树模型及案例&…

收藏 外贸企业必备的跨境电商B2B出口报关完整指南

跨境企业想要将货物出口到国外&#xff0c;报关是重要步骤之一&#xff0c;它是货物合法进出国境的通行证。许多初入跨境电商行业的B2B企业在报关上由于无人指引走了许多弯路&#xff0c;浪费了时间。 一.跨境出口模式 根据海关总署规定&#xff0c;跨境电商出口的监管模式分为…

QDataStream(数据流)

QDataStream类实现将QIODevice的二进制数据串行化。一个数据流就是一个二进制编码信息流&#xff0c;独立于主机的操作系统&#xff0c;CPU或字节顺序。QDataStream可以实现c的基本数据类型的串行化。 构造函数&#xff1a; 常用函数 &#xff1a; atEnd()是否到达结束位置re…

基于jsp+ssm的旅游活动报名系统-计算机毕业设计

项目介绍 本系统主要采JSP语言进行编写&#xff0c;本系统数据库采用数据处理功能强大的MySQL来建立 管理员可以通过结伴休闲平台进行旅行社管理、个人活动管理、旅行社活动管理、客户管理、用户管理等。个人用户则可通过结伴休闲平台对进行发布活动、结伴活动等。旅行社用户…

STC15 - ExternalRAM_enable()/InternalRAM_enable()

文章目录STC15 - ExternalRAM_enable()/InternalRAM_enable()概述实验环境MCU的规格区别IAP15F2K61S2的使用限制实验的组合公用测试代码1. 没有焊接外部RAM ExternalRAM_enable()2. 没有焊接外部RAM InternalRAM_enable()3. 焊接外部RAM ExternalRAM_enable()4. 焊接外部RAM…

世界杯狂欢季,家居品牌仅靠4位UP增长2000wB站声量

时隔四年&#xff0c;2022年卡塔尔世界杯于11月21日正式开始&#xff0c;直至目前比赛进度已过半程&#xff0c;这场世界赛事备受各国瞩目&#xff0c;全网有关于“足球”的运动信息接踵而至。 飞瓜数据&#xff08;B站版&#xff09;显示&#xff0c;B站平台带有“世界杯”标…

React源码之概念篇

React主要的三部分 这是从ReactDom.render后触发的步骤 调度协调渲染 设计理念 首先在React中需要在浏览器刷新一次就需要执行如下的步骤&#xff1a; JS脚本执行->样式布局->样式绘制&#xff0c;如果js执行时间超过了16.6ms&#xff0c;那么就没有时间留给样式布局…

[附源码]计算机毕业设计良辰之境影视评鉴系统Springboot程序

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

【viper】go 配置管理神器viper使用详解

目录一、viper简介二、基本使用1.viper基本配置2.读取配置3.自定义配置源4.注册别名4.获取环境变量5.获取命令行参数6.多实例使用7.监听配置变化三、读取远程配置四、保存配置一、viper简介 viper是一个应用程序解决方案&#xff0c;它支持在应用程序内使用&#xff0c;特性如…

基于元胞自动机的人员疏散模拟器研究(Matlab代码实现)

&#x1f468;‍&#x1f393;个人主页&#xff1a;研学社的博客 &#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜…

[前端面试题]:数组去重的几种方法

总结一些日常需要用到的一些api&#xff0c;也是在一些面试中会经常出现的题目&#xff0c;今天分享的是数组去重的几个不同的方法&#xff0c; 同时文章也被收录到我的《JS基础》专栏中&#xff0c;欢迎大家点击收藏加关注。 给大家推荐一个实用面试题库 1、前端面试题库 &am…

魔兽世界开服架设教程—魔兽国服停服时间已确定!玩家该何去何从

《魔兽世界》国服停止运营时间定在&#xff0c;2023年1月24日0点起 近期随着暴雪和网易公司先后发出的公告&#xff0c;《魔兽世界》国服关服时间已经十分确定了&#xff0c;国服停运时间定在明年的1月24号0点起正式关服。随着国服将关闭&#xff0c;魔兽私服即将崛起。毕竟在游…

【Java面试】并发

文章目录线程有那些状态&#xff1f;一个程序来显示状态切换过程正常执行流程阻塞执行流程等待执行流程说说线程池的核心参数wait和sleep的区别Lock和synchronized的区别Lock中Condition的使用说说Java中的悲观锁与乐观锁乐观锁悲观锁Hashtable和ConcurrentHashMap的区别&#…