1.CMake基础:

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
 1.1 CMake 是什么:
(1).CMake是一个支持生成跨平台建构文件的工具
(2).CMake并不直接建构最终的软件,而是描述项目文件被编译的过程,生成标准的建构档(如 Unix 的 Makefile 或 VS 的 projects/workspaces),然后再以对应平台的建构方式使用。

1.2 CMake源文件:
(1).CMake编写的源⽂件以CMakeLists.txt 命名或以.cmake为扩展名
(2).CMake的源⽂件包括 命令和注释
(3).CMake源文件中所有有效的语句都是命令
可以是内置命令或者自定义的函数(function) 或 宏命令(macro)
(4).可以通过add_subdirectory()命令把子录的CMake源文件添加进来


1.3 CMake编译C/C++原理:
(1).CMake比Unix的make更为高级,使用起来要方便得多。
(2).终端cmake命令将CMakeLists.txt文件建构为make所需要的makefile文件,
最后用make命令编译源码生成可执行程序或共享库(so(shared object))
因此CMake在Linux终端执行步骤总的来说就两个:
1.cmake
2.make
(3).终端执行cmake后会生成很多编译中间文件以及makefile文件,
一般会新建一个build目录专门用来编译:
1.mkdir build
2.cd build
3.cmake ..
4.make

build的创建也可以在CMakeLists.txt中使用命令创建。
cmake指向CMakeLists.txt所在的目录,
cmake .. 表示当前CMakeLists.txt目录的上一级目录

对于一个庞大的工程,编写Makefile相当复杂,
有了CMake工具之后就可以读入所有源文件,自动生成Makefile等构建文件。

2.CMake注释:

1
2
3
4
5
(1).单行注释:#注释内容
(2).多行注释:可以使用括号来实现多行注释:
#[[多行注释
多行注释
多行注释]]

3.CMake变量:

1
2
3
4
5
6
7
8
9
10
11
   (1).CMake中所有的变量都是string类型。
(2).set()/unset():声明/移除一个变量
(3).声明变量:set(变量名 变量值)
set(var 123)
(4).引用变量:${变量名}
${var}
(5).打印变量:message("变量名 = ${变量名}")
message("var = ${var}")
示例:
set(var 123)
message("var=${var}")

4.CMake列表(LISTS)

1
2
3
4
5
6
7
8
9
10
11
   (1).列表也是字符串,可以把列表看做是一个特殊的变量,这个变量有多个值。
(2).语法格式:set(列表名 值1 值2 ... 值n) 或 set(列表名 “值1;值2;...值n”)
(3).声明列表:set(列表名 值1 值2 ... 值n) 或 set(列表名 “值1;值2;...值n”)
set(list_var 1 2 3 4 5) 或 set(list_var "1;2;3;4;5")
(4).引用列表:${列表名}
(5).打印列表:message("列表名 = ${列表名}")
message("list_var = ${list_var}")

示例:
set(list_aa 1 2 3 4 5)
message("list=${list_aa}")

5.CMake中变量的作用域

1
2
3
4
5
(1).全局层:cache变量,在整个项目范围可见,
一般在set定义变量式,指定CACHE参数就能定义cache变量
(2).目录层:在当前⽬录CMakeLists.txt中定义,
以及在该文件包含的其他CMake源文件中定义的变量
(3).函数层:在命令函数中定义的变量,属于函数作用域内的变量

6.CMake流程控制

(1).操作符:
优先级: () > 一元 > 二元 > 逻辑

image-20230718142650779

一元:

1
2
3
EXIST:如果指定的文件或目录存在,则为true
COMMAND:如果给定的name是可以调用的命令、宏或函数,则为true
DEFINED:如果定义了给定<name>的变量、缓存变量或环境变量,则为true.(变量的值无关紧要)

二元:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
EQUAL:相等(如果给定的字符串或变量的值是有效数字且等于右侧的数字,则为true)
LESS:小于(如果给定的字符串或变量的值是有效数字且小于右侧的数字,则为true)
LESS_EQUAL:小于或等于(如果给定的字符串或变量的值是有效数字且小于或等于右侧的数字,则为true)
GREATER:大于(如果给定的字符串或变量的值是有效数字且大于右侧的数字,则为true)
GREATER_EQUAL:大于或等于(如果给定的字符串或变量的值是有效数字且大于或等于右侧的数字,则为true)
STREQUAL:字符等于(如果给定的字符串或变量的值按字典顺序(lexicographically)等于右侧的字符串或变量,则为true)
STRLESS:字符小于(如果给定的字符串或变量的值按字典顺序(lexicographically)小于右侧的字符串或变量,则为true)
STRLESS_EQUAL:小于或等于(如果给定的字符串或变量的值按字典顺序(lexicographically)小于或等于右侧的字符串或变量,则为true)
STRGREATER:字符大于(如果给定的字符串或变量的值按字典顺序(lexicographically)大于右侧的字符串或变量,则为true)
STRGREATER_EQUAL:字符大于或等于(如果给定的字符串或变量的值按字典顺序(lexicographically)等于右侧的字符串或变量,则为true)
#VERSION_EQUAL也就是取整比较(任何非整数版本组件或版本组件的非整数结尾部分都会在该点有效截断字符串)
VERSION_EQUAL:等于(如果给定的字符串或变量的值等于右侧的值,则为true)
VERSION_LESS:小于(如果给定的字符串或变量的值等于右侧的值,则为true)
VERSION_LESS_EQUAL:小于或等于(如果给定的字符串或变量的值小于或等于右侧的值,则为true)
VERSION_GREATER:大于(如果给定的字符串或变量的值大于右侧的值,则为true)
VERSION_GRATER_EQUAL:大于或等于(如果给定的字符串或变量的值大于或等于右侧的值,则为true)
MATCHES:正则表达式匹配(如果给定的字符串或变量的值与给定的正则表达式匹配(matches),则为true)

逻辑:

1
2
3
NOT: !  如果条件不为ture,,则为true
AND: && 如果这两个条件都被单独被认为是true,则为true
OR: || 如果任一条件单独被认为是true,则为true

变量值参考网址:http://t.csdn.cn/IfYPc

(2).布尔常量值:

img

(3).条件命令 if():

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
     语法格式:
if (表达式)
COMMAND(ARGS...)
elseif(表达式)
COMMAND(ARGS...)
else(表达式)
COMMAND(ARGS...)
endif(表达式)

示例:
set(if_tap OFF)
set(elseif_tap ON)

if(${if_tap})
message("if")
elseif(${elseif_tap})
message("elseif")
else(${if_tap})
message("else")
endif(${if_tap})

elseif和else部分是可选的, 也可以使⽤多个elseif部分
缩进和空格对语句的解析没有影响

(4).循环命令 while():
语法格式:
while(表达式)
COMMAND(ARGS...)
endwhile(表达式)

示例:
set(a "")
while(NOT a STREQUAL "xxx")
set(a "${a}x")
message("a = ${a}")
endwhile()

break() 可以跳出整个循环
continue() 可以跳出当前循环

(5).循环遍历 foreach():
语法格式:
foreach(循环变量 参数1 参数2... 参数N)
COMMAND(ARGS...)
endforeach(循环变量)

遍历RANGE:
#循环范围从start到stop,循环增量为step
foreach(循环变量 RANGE start stop step)
COMMAND(ARGS...)
endforeach(循环变量)

遍历LISTS:
foreach(循环遍历 IN LISTS 列表)
COMMAND(ARGS...)
endforeach(循环变量)

示例:
foreach(item 1 2 3)
message("item = ${item}")
endforeach(item)

#RANGE:RANGE 4 表示从0到4
foreach(item RANGE 4)
message("item = ${item}")
endforeach(item)

#RANGE:打印 1 3 5
foreach(item RANGE 1 5 2)
message("item = ${item}")
endforeach(item)

#LISTS:
set(list_var 1 2 3)
foreach(item IN LISTS list_var)
message("item = ${item}")
endforeach(item)

foreach也支持 break() 和 continue() 命令跳出循环

7.CMake自定义函数命令

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
语法格式:
function(<name>[arg1 [arg3 [arg3...]]])
COMMAND(ARGS...)
endfunction(<name>)

调用格式:
name(参数列表)

示例:
function(func x y z)
message("call function func")
message("x = ${x}")
message("y = ${y}")
message("z = ${z}")
# ARGC 内置变量 参数个数
message("ARGC = ${ARGC}")
# ARGVn 内置变量 第 n 个参数,从0开始
message("arg1 = ${ARGV0}")
message("arg2 = ${ARGV1}")
message("arg3 = ${ARGV2}")
# ARGV 内置变量 参数列表
message("all args = ${ARGV}")
endfunction(func)

调用:fun(1 2 3)

8.CMake自定义宏命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
语法格式:
macro(<name>[arg1 [arg3 [arg3...]]])
COMMAND(ARGS...)
endmacro(<name>)

调用格式:
name(实参列表)

示例:
marco(ma x y z)
message("call macro ma")
message("x = ${x}")
message("y = ${y}")
message("z = ${z}")
endmacro(ma)

调用:ma(1 2 3)

函数命令有自己的作用域
宏的作用域和调用者的作用域是一样的
作用域:
全局层:cache变量,在整个项目范围可见,一般在set定义变量时,指定CACHE参数就能定义为cache变量。
目录层:在当前目录CMakeLists.txt中定义,以及在该文件包含的其他cmake源文件中定义的变量。
函数层:在命令函数中定义的变量,属于函数作用域内的变量。

9.CMake常用变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
CMake预设了一些常用变量,这些变量通常会在编写CMakeLists.txt文件时使用到:
CMAKE_MAJOR_VERSION:cmake 主版本号
CMAKE_MINOR_VERSION:cmake 次版本号
CMAKE_C_FLAGS:设置 C 编译选项
CMAKE_CXX_FLAGS:设置 C++ 编译选项
PROJECT_SOURCE_DIR:工程的根目录
PROJECT_BINARY_DIR:运行 cmake 命令的目录
CMAKE_CURRENT_SOURCE_DIR:当前CMakeLists.txt 所在路径
CMAKE_CURRENT_BINARY_DIR:目标文件编译目录
EXECUTABLE_OUTPUT_PATH:重新定义目标二进制可执行文件的存放位置
LIBRARY_OUTPUT_PATH:重新定义目标链接库文件的存放位置
UNIX:如果为真,表示为UNIX-like的系统,包括AppleOSX和CygWin
WIN32:如果为真,表示为 Windows 系统,包括 CygWin
APPLE:如果为真,表示为 Apple 系统
CMAKE_SIZEOF_VOID_P:表示void*的大小(例如为4或者8),可以使用其来判断当前构建为32位还是64位
CMAKE_CURRENT_LIST_DIR:表示正在处理的CMakeLists.txt文件所在目录的绝对路径
CMAKE_ARCHIVE_OUTPUT_DIRECTORY:用于设置ARCHIVE目标的输出路径
CMAKE_LIBRARY_OUTPUT_DIRECTORY:用于设置LIBRARY目标的输出路径
CMAKE_RUNTIME_OUTPUT_DIRECTORY:用于设置RUNTIME目标的输出路径

10.CMake常用命令

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
(1) project命令:
命令语法:project(<projectname> [languageName1 languageName2 ...])
命令简述:用于指定项目的名称
使用范例:project(Main)

(2) cmake_minimum_required命令:
命令语法:cmake_minimum_requried(VERSION major[.minor[.patch)
命令简述:用于指定需要的CMake的最低版本
使用范例:cmake_minimum_requried(VERSION 2.8.3)

(3) aux_source_directory命令:
命令语法:aux_source_directory(<dir> <variable>)
命令简述:用于包含源文件目录,dir目录下的所有源文件的名字保存在变量variable中
使用范例:aux_source_directory(${CMAKE_CURRENT_SOURCE_DIR}/src DIR_SRCS)

(4) add_executable命令:
命令语法:add_executable(<name> [WIN32] [MACOSX_BUNDLE][EXCLUDE_FROM_ALL] source1 source2 … sourceN)
命令简述:用于指定从一组源文件source1 source2 ... sourceN 编译出一个可执行文件且命名为name
使用范例:add_executable(Main $(DIR_SRCS))

(5) add_library命令:
命令语法:add_library([STATIC | SHARED | MODULE] [EXCLUDE_FROM_ALL] source1source2 … sourceN)
命令简述:用于指定从一组源文件 source1 source2 ... sourceN编译出一个库文件且命名为name
使用范例:add_library(Lib $(DIR_SRCS))

(6) add_dependencies命令:
命令语法:add_dependencies(target-name depend-target1 depend-target2 …)
命令简述:用于指定某个目标(可执行文件或者库文件)依赖于其他的目标。
这里的目标必须是add_executable、add_library、add_custom_target命令创建的目标

(7) add_subdirectory命令:
命令语法:add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL])
命令简述:用于添加一个需要进行构建的子目录
使用范例:add_subdirectory(Lib)

(8) target_link_libraries命令:
命令语法:target_link_libraries(<target> [item1 [item2 […]]][[debug|optimized|general] ] …)
命令简述:用于指定target需要链接item1 item2 ...。这里target必须已经被创建,链接的item可以是已经存在的target(依赖关系会自动添加)
使用范例:target_link_libraries(Main Lib)

(9) set命令:
命令简述:用于设定变量 variable 的值为 value。如果指定了 CACHE 变量将被放入 Cache(缓存)中。
命令语法:set(<variable> <value> [[CACHE <type><docstring> [FORCE]] | PARENT_SCOPE])
使用范例:set(ProjectName Main)

(10) unset命令:
命令语法:unset(<variable> [CACHE])
命令简述:用于移除变量 variable。如果指定了 CACHE 变量将被从 Cache 中移除。
使用范例:unset(VAR CACHE)

(11) message命令:
命令语法:message([STATUS|WARNING|AUTHOR_WARNING|FATAL_ERROR|SEND_ERROR] “message todisplay”…)
命令简述:用于输出信息
使用范例:message(“Hello World”)

(12) include_directories命令:
命令语法:include_directories([AFTER|BEFORE] [SYSTEM] dir1 dir2 …)
命令简述:用于设定目录,这些设定的目录将被编译器用来查找 include 文件
使用范例:include_directories(${PROJECT_SOURCE_DIR}/lib)

(13) find_path命令:
命令语法:find_path(<VAR> name1 [path1 path2 …])
命令简述:用于查找包含文件name1的路径,如果找到则将路径保存在VAR中(此路径为一个绝对路径),如果没有找到则结果为<VAR>-NOTFOUND.默认情况下,VAR会被保存在Cache中,这时候我们需要清除VAR才可以进行下一次查询(使用unset命令)
find_path(LUA_INCLUDE_PATH lua.h ${LUA_INCLUDE_FIND_PATH})
if(NOT LUA_INCLUDE_PATH)
message(SEND_ERROR "Header file lua.h not found")
endif()

(14) find_library命令:
命令语法:find_library(<VAR> name1 [path1 path2 …])
命令简述:用于查找库文件 name1 的路径,如果找到则将路径保存在 VAR 中(此路径为一个绝对路径),
如果没有找到则结果为 <VAR>-NOTFOUND。
一个类似的命令 link_directories 已经不太建议使用了

(15) add_definitions命令:
命令语法:add_definitions(-DFOO -DBAR …)
命令简述:用于添加编译器命令行标志(选项),通常的情况下我们使用其来添加预处理器定义
使用范例:add_definitions(-D_UNICODE -DUNICODE)

(16) file命令:
命令简述:此命令提供了丰富的文件和目录的相关操作(这里仅说一下比较常用的)
使用范例:
# 目录的遍历
# GLOB 用于产生一个文件(目录)路径列表并保存在variable 中
# 文件路径列表中的每个文件的文件名都能匹配globbing expressions(非正则表达式,但是类似)
# 如果指定了 RELATIVE 路径,那么返回的文件路径列表中的路径为相对于 RELATIVE 的路径
file(GLOB variable [RELATIVE path][globbing expressions]...)

# 获取当前目录下的所有的文件(目录)的路径并保存到 ALL_FILE_PATH 变量中
file(GLOB ALL_FILE_PATH ./*)
# 获取当前目录下的 .h 文件的文件名并保存到ALL_H_FILE 变量中
# 这里的变量CMAKE_CURRENT_LIST_DIR 表示正在处理的 CMakeLists.txt 文件的所在的目录的绝对路径(2.8.3 以及以后版本才支持)

NDK(Native Development Kit)是一个用于在Android平台上开发C/C++原生代码的工具集。在NDK中,静态库(Static Library)和动态库(Dynamic Library)是两种不同的库文件形式。

静态库(SHARED)是将代码和依赖的库函数编译链接成一个可执行文件时静态地被调用的库。它的特点是将所有代码和依赖性都包含在文件内部,使得可执行文件独立性强,不依赖于其他外部库。静态库在编译时会将所有函数和数据都复制到可执行文件中,因此可执行文件的大小会增加。每次更新或修改静态库中的代码时,都需要重新编译和链接可执行文件。

动态库(STATIC)是独立于可执行文件的共享库,它在程序运行时被动态加载和链接到内存中。动态库可以被多个可执行文件共享,从而提供了代码的重用性和节省了可执行文件的大小。当应用程序需要使用动态库时,操作系统会在运行时加载它们,以提供所需的函数和资源。这样可以在不重新编译和链接可执行文件的情况下更新和修改动态库。

总结:

1
2
3
4
1. 静态库在编译时将代码和依赖性复制到可执行文件中,使得可执行文件独立性强,但会增加可执行文件的大小。
2. 动态库在程序运行时被动态加载和链接到内存中,可以被多个可执行文件共享,提供了代码的重用性和节省了可执行文件的大小。
3. 静态库需要重新编译和链接可执行文件才能更新和修改,而动态库可以在不重新编译和链接可执行文件的情况下更新和修改。
4. 静态库适合于对独立性要求较高的场景,而动态库适合于代码重用和共享的场景。

后缀或格式在NDK中,静态库和动态库的文件后缀或格式通常使用以下常见的命名约定:

1
2
3
4
5
6
7
8
9
10
11
12
13
1. 静态库的文件后缀或格式可以是:
- Linux系统上:.a
- Windows系统上:.lib
- macOS系统上:.a
此外,静态库还可以被压缩或归档成.tar.gz或.zip等文件格式。

2. 动态库的文件后缀或格式可以是:
- Linux系统上:.so (Shared Object)
- Windows系统上:.dll (Dynamic Link Library)
- macOS系统上:.dylib (Dynamic Library)

这些是常见的命名约定,但并不是绝对的规定,具体的后缀或格式可能会因编译器、操作系统或项目设置的不同而有所变化。
在使用NDK开发时,可以根据具体需求和平台要求选择适当的文件后缀或格式来命名静态库和动态库。

CMakeLists.txt 文件详解

【1】 设置cmake最小版本

1
2
# 设置cmake最小版本
cmake_minimum_required(VERSION 3.18.1)

【2】指定项目

1
2
# 指定项目
project("jniproject")

【3】导入库目录

1
2
# 导入头文件目录
include_directories("C:/Program Files/Java/jdk-18.0.1.1/include")

相当于 -I,如果C/C++中没有指定头文件的具体路径,在cmake中用include_directories指定头文件的目录也是可以的。

【4】生成一个动态库

1
2
3
# 生成一个动态库(windows:dll,android:so)
# 可以引入多个文件,多个文件用空格或者换行隔开
add_library(jniproject SHARED src/main/cpp/native-lib.cpp src/main/cpp/Test.cpp)

add_library可以指定多个源码文件,但是如果文件比较多的话也是一件麻烦的事情。
这里有两种方法可以指定目录中所有的文件:

1
2
3
4
# 指定一个或多个目录中所有对应的文件,并将名称保存到 DIR_SRCS 变量(可以指定多个目录)
file(GLOB DIR_SRCS src/main/cpp/*.cpp)
message("DIR_SRCS:=============${DIR_SRCS}")
add_library(jniproject SHARED ${DIR_SRCS})

或者

1
2
3
4
5
# 查找目录所有源文件 并将名称保存到 DIR_SRCS 变量
# 不能查找子目录
aux_source_directory(src/main/cpp/ DIR_SRCS)#src/main/cpp源文件路径
message("DIR_SRCS:=============${DIR_SRCS}")
add_library(jniproject SHARED ${DIR_SRCS})

【5】引入其它cmakelist

1
2
# 添加 child 子目录下的cmakelist
add_subdirectory(child)

【6】指定动态库或静态库路径,或预处理

有两种方法:

1
2
3
4
5
# 设置一个变量
# CMAKE_CXX_FLAGS c++参数
# CMAKE_C_FLAGS c参数
# CMAKE_C_FLAGS = --sysroot=XX
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -L${CMAKE_SOURCE_DIR}/src/main/cpp/${ANDROID_ABI}")

或者

1
2
3
4
5
6
# 引入静态库或者动态库
# 第一个参数:库名称
# 第二个参数:SHARED:动态库 STATIC:静态库
# IMPORTED 表示以导入的方式添加进来(预编译静态/动态库)
add_library(Test STATIC IMPORTED)
set_target_properties(Test PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/src/main/cpp/${ANDROID_ABI}/libTest.a)

【7】生成可执行文件,在Android上不适用

1
2
# 生成可执行文件(生成exe文件, 在VS工具上可用)
add_executable (jniproject1 native-lib.cpp)

【8】查找一个ndk自带库

1
2
# log-lib 是变量名称  log是动态库名称 将liblog.so或liblog.a的路径赋值给log-lib
find_library(log-lib log)

将日志库log的路径赋值到变量log-lib中。

【9】链接ndk自带的库

1
2
3
4
5
# 链接ndk自带的库
target_link_libraries(
jniproject # 链接 libjniproject.so
Test # 链接 libTest.so
${log-lib}) # 链接 liblog.so

【10】Cmake基本配置

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# 设置cmake最小版本
cmake_minimum_required(VERSION 3.18.1)

# 指定项目
project("jniproject")

# 导入头文件目录
# include_directories("C:/Program Files/Java/jdk-18.0.1.1/include")

# 生成一个动态库(windows:dll,android:so)
# 可以引入多个文件,多个文件用空格或者换行隔开
# add_library(jniproject SHARED src/main/cpp/native-lib.cpp src/main/cpp/Test.cpp)

# 指定一个或多个目录中所有对应的文件,并将名称保存到 DIR_SRCS 变量(可以指定多个目录)
# file(GLOB DIR_SRCS src/main/cpp/*.cpp)
# message("DIR_SRCS:=============${DIR_SRCS}")
# add_library(jniproject SHARED ${DIR_SRCS})

# 查找目录所有源文件 并将名称保存到 DIR_SRCS 变量
# 不能查找子目录
aux_source_directory(src/main/cpp/ DIR_SRCS)
message("DIR_SRCS:=============${DIR_SRCS}")
add_library(jniproject SHARED ${DIR_SRCS})

# 添加 child 子目录下的cmakelist
# add_subdirectory(child)

# 设置一个变量
# CMAKE_CXX_FLAGS c++参数
# CMAKE_C_FLAGS c参数
# CMAKE_C_FLAGS = --sysroot=XX
# set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -L${CMAKE_SOURCE_DIR}/src/main/cpp/${ANDROID_ABI}")

# 引入静态库或者动态库
# 第一个参数:库名称
# 第二个参数:SHARED:动态库 STATIC:静态库
# IMPORTED 表示以导入的方式添加进来(预编译静态/动态库)
add_library(Test STATIC IMPORTED)
set_target_properties(Test PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/src/main/cpp/${ANDROID_ABI}/libTest.a)

# 输出 Android_ABI 的值
message("ANDROID_ABI =================: ${ANDROID_ABI}")

# 生成可执行文件(生成exe文件, 在VS工具上可用)
# add_executable (jniproject1 native-lib.cpp)

# log-lib 是变量名称 log是动态库名称 将liblog.so或liblog.a的路径赋值给log-lib
find_library(log-lib log)

# 链接ndk自带的库
target_link_libraries(
jniproject # 链接 libjniproject.so
Test # 链接 libTest.so
${log-lib}) # 链接 liblog.so

原文链接:https://www.jianshu.com/p/f65dec960e8e

一、Makefile命令规则
1
2
Makefile的命令规则如下:
目标:依赖

假设有一个Test.cpp文件,代码如下:

1
2
3
4
5
6
7
8
#include <iostream>

using namespace std;

int main() {
count << "===Test===" << endl;
return 0;
}

我们可以使用gcc或g++来 预处理汇编编译链接,也可以利用Makefile来执行这些命令。

在Makefile中填入配置:

1
2
3
# 生成预处理文件
Test.i:Test.cpp
g++ -E Test.cpp -o Test.i

Test.i 是目标,Test.cpp 是依赖,整体的意思是:Test.i是依赖于Test.cpp生成的。
左右两边用冒号隔开,也就是Makefile命令规范:

1
目标:依赖

下一行是 g++ 命令,Test.i的生成是根据这条 g++ 命令生成的。g++前面必须含有分割符,而且必须是tab分割,不能是空格,否则在执行Makefile时不被识别。

执行该Makefile文件:

image-20230717161647472

Makefile中可能由多个目标组成,make命令会找到Makefile中第一个目标执行。也可以指定一个目标执行:

1
make 目标

现在Makefile中只有一个目标,所以可以执行:

1
make Test.i
二、Makefile实现 预处理汇编编译链接
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 生成预处理文件
Test.i:Test.cpp
g++ -E Test.cpp -o Test.i

# 生成汇编文件
Test.s:Test.i
g++ -S Test.i -o Test.s

# 生成目标文件
Test.o:Test.s
g++ -c Test.s -o Test.o

# 生成链接文件(可执行文件)
Test:Test.o
g++ Test.o -o Test。

定义四个目标,分别是:Test.iTest.sTest.oTest

分别执行这四个目标:

1
2
3
4
make Test.i
make Test.s
make Test.o
make Test

可以完成 预处理汇编编译链接
但是,完成这写指令,还可以一步完成,我们的目标是执行 make 命令完成所有的步骤。

只需要改变一下Makefile中的目标顺序即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 生成链接文件(可执行文件)
Test:Test.o
g++ Test.o -o Test

# 生成目标文件
Test.o:Test.s
g++ -c Test.s -o Test.o

# 生成汇编文件
Test.s:Test.i
g++ -S Test.i -o Test.s

# 生成预处理文件
Test.i:Test.cpp
g++ -E Test.cpp -o Test.i

输入make命令一次性执行完所有的目标:

image-20230717161803969

注意:顺序要正确,必须形成逐步依赖关系。

三、makefile编译多个文件

Student.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#ifndef _STUDENT_H_
#define _STUDENT_H_

#include <string>

using namespace std;

class Student {

private:
string name;
int age;

public:
void setName(string name);
string getName();
void setAge(int age);
int getAge();
};

#endif

Student.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "Student.h"

void Student::setName(string name) {
this->name = name;
}
string Student::getName() {
return name;
}
void Student::setAge(int age) {
this->age = age;
}
int Student::getAge() {
return age;
}

Test.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
#include "Student.h"

using namespace std;

int main() {

Student* stu = new Student();
stu->setName("zhangsan");
stu->setAge(13);
cout << "姓名:" << stu->getName() << ", 年龄:" << stu->getAge() << endl;
return 0;
}

Makefile:

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
# 生成链接文件(可执行文件)
Test:Test.o Student.o
g++ Test.o Student.o -o Test

# 生成目标文件Test.o
Test.o:Test.s
g++ -c Test.s -o Test.o

# 生成目标文件Student.o
Student.o:Student.s
g++ -c Student.s -o Student.o

# 生成汇编文件Test.s
Test.s:Test.i
g++ -S Test.i -o Test.s

# 生成汇编文件Test.s
Student.s:Student.i
g++ -S Student.i -o Student.s

# 生成预处理文件Test.i
Test.i:Test.cpp
g++ -E Test.cpp -o Test.i

# 生成预处理文件Student.i
Student.i:Student.cpp
g++ -E Student.cpp -o Student.i

执行make命令之后,生成 预处理文件汇编文件目标文件可执行文件

img

四、makefile多文件管理

在编译期间,在同一个目录下生成多个文件,文件多了有点乱,这时,就需要分类管理,将不同格式的文件放入不同的文件夹。

修改Makefile配置:

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
35
36
37
38
39
40
41
42
# 定义变量INFO_DIR,指定预处理文件和汇编文件的存放目录
INFO_DIR = ./info

# 定义变量INC_DIR,指定头文件的存放目录
INC_DIR = ./include

# 定义变量SRC_DIR,指定c/c++文件的存放目录
SRC_DIR = ./src

# 定义变量BIN_DIR,指定可执行文件的存放目录
BIN_DIR = ./bin

# 定义变量OBJ_DIR,指定可执行文件的存放目录
OBJ_DIR = ./obj

# 生成链接文件(可执行文件)
Test:${OBJ_DIR}/Test.o ${OBJ_DIR}/Student.o
g++ ${OBJ_DIR}/Test.o ${OBJ_DIR}/Student.o -o ${BIN_DIR}/Test

# 生成目标文件Test.o
${OBJ_DIR}/Test.o:${INFO_DIR}/Test.s
g++ -c ${INFO_DIR}/Test.s -o ${OBJ_DIR}/Test.o

# 生成目标文件Student.o
${OBJ_DIR}/Student.o:${INFO_DIR}/Student.s
g++ -c ${INFO_DIR}/Student.s -o ${OBJ_DIR}/Student.o

# 生成汇编文件Test.s
${INFO_DIR}/Test.s:${INFO_DIR}/Test.i
g++ -S ${INFO_DIR}/Test.i -o ${INFO_DIR}/Test.s

# 生成汇编文件Test.s
${INFO_DIR}/Student.s:${INFO_DIR}/Student.i
g++ -S ${INFO_DIR}/Student.i -o ${INFO_DIR}/Student.s

# 生成预处理文件Test.i
${INFO_DIR}/Test.i:${SRC_DIR}/Test.cpp
g++ -E ${SRC_DIR}/Test.cpp -o ${INFO_DIR}/Test.i

# 生成预处理文件Student.i
${INFO_DIR}/Student.i:${SRC_DIR}/Student.cpp
g++ -E ${SRC_DIR}/Student.cpp -o ${INFO_DIR}/Student.i

目录结构如下:

img

1
2
3
4
5
6
bin、include、info、obj、src文件夹需要手动创建;
bin文件夹存放可执行文件;
include文件夹存放头文件;
info文件夹存放预处理文件和汇编文件;
obj文件夹存放目标文件;
src文件夹存放c或cpp文件;
五、clean命令

在Makefile中添加:

1
2
clean:
del bin obj info

执行clean命令,可以删除 bin、obj、info文件夹中所有的文件。

1
make clean
六、Makefile通配符
1
2
3
4
5
6
7
%.o:表示一个xx.o文件
$@:表示目标文件
$<:表示第一个依赖文件
$^:所有不重复的依赖文件,以空格分开
$*:不包含扩展名的target文件名称
$+:所有的依赖文件,以空格分开,并以出现的先后为序,可能包含重复的依赖文件
$?:所有时间戳比target文件晚的依赖文件,并以空格分开

由于 -o 后面输出的必然是目标文件,所以 -o 后面的输出文件可以替换为 $@:

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
35
36
37
38
39
40
41
42
43
44
45
# 定义变量INFO_DIR,指定预处理文件和汇编文件的存放目录
INFO_DIR = ./info

# 定义变量INC_DIR,指定头文件的存放目录
INC_DIR = ./include

# 定义变量SRC_DIR,指定c/c++文件的存放目录
SRC_DIR = ./src

# 定义变量BIN_DIR,指定可执行文件的存放目录
BIN_DIR = ./bin

# 定义变量OBJ_DIR,指定可执行文件的存放目录
OBJ_DIR = ./obj

# 生成链接文件(可执行文件)
${BIN_DIR}/Test:${OBJ_DIR}/Test.o ${OBJ_DIR}/Student.o
g++ ${OBJ_DIR}/Test.o ${OBJ_DIR}/Student.o -o $@

# 生成目标文件Test.o
${OBJ_DIR}/Test.o:${INFO_DIR}/Test.s
g++ -c ${INFO_DIR}/Test.s -o $@

# 生成目标文件Student.o
${OBJ_DIR}/Student.o:${INFO_DIR}/Student.s
g++ -c ${INFO_DIR}/Student.s -o $@

# 生成汇编文件Test.s
${INFO_DIR}/Test.s:${INFO_DIR}/Test.i
g++ -S ${INFO_DIR}/Test.i -o $@

# 生成汇编文件Test.s
${INFO_DIR}/Student.s:${INFO_DIR}/Student.i
g++ -S ${INFO_DIR}/Student.i -o $@

# 生成预处理文件Test.i
${INFO_DIR}/Test.i:${SRC_DIR}/Test.cpp
g++ -E ${SRC_DIR}/Test.cpp -o $@

# 生成预处理文件Student.i
${INFO_DIR}/Student.i:${SRC_DIR}/Student.cpp
g++ -E ${SRC_DIR}/Student.cpp -o $@

clean:
del bin obj info
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
#$<、$^ 和 % 结合使用,可以最大程度上简化配置:
# 定义变量INFO_DIR,指定预处理文件和汇编文件的存放目录
INFO_DIR = ./info

# 定义变量INC_DIR,指定头文件的存放目录
INC_DIR = ./include

# 定义变量SRC_DIR,指定c/c++文件的存放目录
SRC_DIR = ./src

# 定义变量BIN_DIR,指定可执行文件的存放目录
BIN_DIR = ./bin

# 定义变量OBJ_DIR,指定可执行文件的存放目录
OBJ_DIR = ./obj

# 生成链接文件(可执行文件)
${BIN_DIR}/Test:${OBJ_DIR}/Test.o ${OBJ_DIR}/Student.o
g++ $^ -o $@

# 生成所有目标文件 %:任意字符通配符 $<:表示第一个依赖文件
${OBJ_DIR}/%.o:${INFO_DIR}/%.s
g++ -c $< -o $@

# 生成所有汇编文件 %:任意字符通配符 $<:表示第一个依赖文件
${INFO_DIR}/%.s:${INFO_DIR}/%.i
g++ -S $< -o $@

# 生成所有预处理文件 %:任意字符通配符 $<:表示第一个依赖文件
${INFO_DIR}/%.i:${SRC_DIR}/%.cpp
g++ -E $< -o $@

clean:
del bin obj info

以上配置显然已经简单了很多。

另外,头文件已经放在include目录,cpp文件已经放在了src目录,所以需要指定头文件位置,由两种方法可以指定:

【方法一】在cpp中修改头文件位置

1
#include "Student.h"

改成

1
#include "../include/Student.h"

【方法二】g++命令后面指定头文件路径

加入 -I $(INC_DIR) 导入头文件

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
35
36
37
# 定义变量INFO_DIR,指定预处理文件和汇编文件的存放目录
INFO_DIR = ./info

# 定义变量INC_DIR,指定头文件的存放目录
INC_DIR = ./include

# 定义变量SRC_DIR,指定c/c++文件的存放目录
SRC_DIR = ./src

# 定义变量BIN_DIR,指定可执行文件的存放目录
BIN_DIR = ./bin

# 定义变量OBJ_DIR,指定可执行文件的存放目录
OBJ_DIR = ./obj

# -I 指定头文件
CXXFLAGS=-I $(INC_DIR)


# 生成链接文件(可执行文件)
${BIN_DIR}/Test:${OBJ_DIR}/Test.o ${OBJ_DIR}/Student.o
g++ $(CXXFLAGS) $^ -o $@

# 生成所有目标文件 %:任意字符通配符 $<:表示第一个依赖文件
${OBJ_DIR}/%.o:${INFO_DIR}/%.s
g++ $(CXXFLAGS) -c $< -o $@

# 生成所有汇编文件 %:任意字符通配符 $<:表示第一个依赖文件
${INFO_DIR}/%.s:${INFO_DIR}/%.i
g++ $(CXXFLAGS) -S $< -o $@

# 生成所有预处理文件 %:任意字符通配符 $<:表示第一个依赖文件
${INFO_DIR}/%.i:${SRC_DIR}/%.cpp
g++ $(CXXFLAGS) -E $< -o $@

clean:
del bin obj info

g++出现了多次,也可以使用变量代替,并且新增TAGERT和OBJS变量:

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
35
36
37
38
39
40
41
42
43
44
45
# 定义变量INFO_DIR,指定预处理文件和汇编文件的存放目录
INFO_DIR = ./info

# 定义变量INC_DIR,指定头文件的存放目录
INC_DIR = ./include

# 定义变量SRC_DIR,指定c/c++文件的存放目录
SRC_DIR = ./src

# 定义变量BIN_DIR,指定可执行文件的存放目录
BIN_DIR = ./bin

# 定义变量OBJ_DIR,指定可执行文件的存放目录
OBJ_DIR = ./obj

# -I 指定头文件
CXXFLAGS=-I $(INC_DIR)

# CC 指定编译器 gcc 、 g++
CC=g++

#最终生成的目标,
TAGERT=Test

# 目标文件
OBJS=${OBJ_DIR}/Test.o ${OBJ_DIR}/Student.o

# 生成链接文件(可执行文件)
${BIN_DIR}/${TAGERT}:${OBJS}
${CC} $(CXXFLAGS) $^ -o $@

# 生成所有目标文件 %:任意字符通配符 $<:表示第一个依赖文件
${OBJ_DIR}/%.o:${INFO_DIR}/%.s
${CC} $(CXXFLAGS) -c $< -o $@

# 生成所有汇编文件 %:任意字符通配符 $<:表示第一个依赖文件
${INFO_DIR}/%.s:${INFO_DIR}/%.i
${CC} $(CXXFLAGS) -S $< -o $@

# 生成所有预处理文件 %:任意字符通配符 $<:表示第一个依赖文件
${INFO_DIR}/%.i:${SRC_DIR}/%.cpp
${CC} $(CXXFLAGS) -E $< -o $@

clean:
del bin obj info

以上配置还存在一定的弊端,OBJS变量指定了目标文件,此时的目标文件是需要指定具体目标文件的。

七、wildcard函数查找

$(wildcard pattern):pattern定义了文件名的格式,wildcard取出其中存在的文件。

1
2
# wildcard查找当前目录下所有cpp文件
SRCS=$(wildcard $(SRC_DIR)/*.cpp)

此时 SRCS 就是 src目录下所有cpp格式的文件。

八、patsubst函数替换

$(patsubst pattern,replacement,$(var)):从var中将符合patern格式的内容,替换为replacement。

1
2
3
4
5
6
# wildcard查找当前目录下所有cpp文件
SRCS=$(wildcard $(SRC_DIR)/*.cpp)

# notdir 去除掉绝对路径,只保留名字
# patsubst 把字串 $(notdir $(SRCS)) 符合模式[%.cpp]的单词替换成[%.o]
OBJS=$(patsubst %.cpp,$(OBJ_DIR)/%.o,$(notdir $(SRCS)))

patsubst 可以解决指定明确目标文件的弊端,改进弊端之后的配置如下:

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# 定义变量INFO_DIR,指定预处理文件和汇编文件的存放目录
INFO_DIR = ./info

# 定义变量INC_DIR,指定头文件的存放目录
INC_DIR = ./include

# 定义变量SRC_DIR,指定c/c++文件的存放目录
SRC_DIR = ./src

# 定义变量BIN_DIR,指定可执行文件的存放目录
BIN_DIR = ./bin

# 定义变量OBJ_DIR,指定可执行文件的存放目录
OBJ_DIR = ./obj

# -I 指定头文件
CXXFLAGS=-I $(INC_DIR)

# CC 指定编译器 gcc 、 g++
CC=g++

#最终生成的目标,
TAGERT=Test

# wildcard查找当前目录下所有cpp文件
SRCS=$(wildcard $(SRC_DIR)/*.cpp)

# notdir 去除掉绝对路径,只保留名字
# patsubst 把字串 $(notdir $(SRCS)) 符合模式[%.cpp]的单词替换成[%.o]
OBJS=$(patsubst %.cpp,$(OBJ_DIR)/%.o,$(notdir $(SRCS)))

# wildcard *.cpp 当前目录下所有c文件
SRCS=$(wildcard $(SRC_DIR)/*.cpp)

# 生成链接文件(可执行文件)
${BIN_DIR}/${TAGERT}:${OBJS}
${CC} $(CXXFLAGS) $^ -o $@

# 生成所有目标文件 %:任意字符通配符 $<:表示第一个依赖文件
${OBJ_DIR}/%.o:${INFO_DIR}/%.s
${CC} $(CXXFLAGS) -c $< -o $@

# 生成所有汇编文件 %:任意字符通配符 $<:表示第一个依赖文件
${INFO_DIR}/%.s:${INFO_DIR}/%.i
${CC} $(CXXFLAGS) -S $< -o $@

# 生成所有预处理文件 %:任意字符通配符 $<:表示第一个依赖文件
${INFO_DIR}/%.i:${SRC_DIR}/%.cpp
${CC} $(CXXFLAGS) -E $< -o $@

clean:
del bin obj info
九、Makefile的变量

Makefile的变量分为两种:及时变量延时变量

1
2
3
4
【1】 `:=`:即时变量,该变量的值即刻确定,在定义时就被确定了;
【2】`=`:延时变量,该变量的值,在使用时才确定;
【3】`?=`:延时变量,第一次定义才起效,如果前面被定义过了就忽略这句;
【4】`+=`:附加,它是即时变量还是延时变量取决于前面的定义;
十、函数遍历

$(foreach val,list,text):对于list(通常用空格隔开)里的每一个变量执行text操作

1
2
3
4
5
6
7
8
9
# 定义一个list
LIST=a b c

# 用f代表A中的各个变量,执行第三个参数的操作。
# foreach 遍历关键字,f:LIST中的每个变量,
RESULT=$(foreach f,$(LIST),$(f).result)

all:
@echo RESULT=$(RESULT)

输出结果:

1
RESULT=a.result b.result c.result
十一、filter函数过滤

$(filter pattern...,text):在text里面取出符合pattern格式的值

$(filter-out pattern...,text):在text里面取出不符合pattern格式的值

1
2
3
4
5
6
7
8
9
10
11
12
# 定义变量C 
C= a b c d/

# 在变量C中取出符合%/的值
D=$(filter %/,$(C))

# 在变量C中取出不符合%/的值
E=$(filter-out %/,$(C))

all:
@echo D=$(D)
@echo E=$(E)

输出结果:

1
2
D=d/
E=a b c
十二、CXXFLAGS补充

上面已经定义了 CXXFLAGS 变量:

1
CXXFLAGS=-I $(INC_DIR)

我们还可以对它进行补充,比如:

1
2
# -I 指定头文件 
CXXFLAGS=-I $(INC_DIR) -Wall -O2 -std=c++11 -frtti -fexceptions

可以指定的FLAG有:

1
2
3
4
5
6
7
8
9
【1】-Werror:会把所有警告当成错误
【2】-I: 该选项用于指定编译程序时依赖的头文件路径
【3】-On: 这是一个优化选项,如果在编译时指定该选项,则编译器会根据n的值(n取0到3之间)
对代码进行不同程度的优化,其中-O0 表示不优化,n的值越大,优化程度越高
【4】-L: 库文件依赖选项,该选项用于指定编译的源程序依赖的库文件路径,库文件可以是静态链接库,也可以是动态链接库
【5】-Wall: 允许发出gcc能提供的所有有用的警告,也可以用-W(warning)来标记指定的警告
【6】-std=c++11:C++11标准
【7】-frtti 和 -fexceptions:关闭 exceptions、rtti 可以减小程序的占用的空间和提升程序的运行效率,
关闭后可能带来一些不兼容问题,使用 typeid 运算符必须开启 RTTI

原文:NDK<第五篇>:Makefile(windows环境) - 简书 (jianshu.com)

1. 下载编辑器VScode

vscode安装

2. 下载编译器MinGW并解压

mingw下载

如果你因为网络环境限制无法下载

不限速下载,请笑纳^-^:https://wwn.lanzouh.com/iLOip031ku6b 密码:1234

  • 在C盘中解压文件

img

理论上你可以在任何地方解压,但注意路径不能包含中文,至于特殊字符请自行测试

3. 将MinGW添加至环境变量

  • 进入mingw64下的bin文件夹,复制当前路径,Win + i唤起系统设置,输入高级系统设置并进入,点击环境变量,选择path,编辑,新建,粘贴路径,按下三个确定

img

4. 配置VScode插件

  • 打开VScode安装插件 ChineseC/C++ ,等待安装完毕后重启VScode

img

  • 切换C/C++插件至 1.8.4 版本

img

因最新版本不会自动生成launch.json文件,给后续优化带来不便,故退回旧版本。

5. 运行代码

  • 新建文件夹,修改为英文名称并进入,右键 通过Code打开 若在安装时未勾选相关选项,可能没有这个选项,请自行在VScode内操作打开文件夹

  • 新建一个文件,英文命名且扩展名为 .c

  • 编写相关代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    #include <stdio.h>
    #include <stdlib.h>
    int main()
    {
    printf("Hello World!\n");
    printf("你好世界!\n");
    system("pause"); // 防止运行后自动退出,需头文件stdlib.h
    return 0;
    }
  • VScode菜单栏,点击运行,启动调试,稍等程序运行,输出结果在下方终端,上方调试面板,点击最右边的 橙色方框 停止程序运行

img

6. 调整和优化

请根据自己的需要进行优化

代码运行后 .vscode 文件夹会自动生成在你的源文件目录下

.vscode 文件夹下的 task.jsonlaunch.json 用来控制程序的运行和调试

  • 将程序运行在外部控制台【推荐】

    ​ 打开.vscode 文件夹下的 launch.json 文件,找到 "externalConsole": false,false 改为 true 并保存

    img

  • 解决中文乱码问题【推荐】

​ 打开.vscode 文件夹下的 task.json 文件,找到 "${fileDirname}\\${fileBasenameNoExtension}.exe" 在后面加上英文 逗号 然后回车到下一行,粘贴下面文本 "-fexec-charset=GBK" 并保存

img

  • 收纳生成的 exe 可执行文件【可选】

​ 1.打开.vscode 文件夹下的 task.json 文件,找到 "${fileDirname}\\${fileBasenameNoExtension}.exe"

​ 2.修改成 "${fileDirname}\\coin\\${fileBasenameNoExtension}.exe" 并保存,同理,launch.json 下也有相同的字段,需要你修改

​ 3.在源文件同目录下新建 coin 文件夹,程序运行后,可执行文件将会生成在里面(其中 coin 可修改成你喜欢的英文名字)

这样 .c 文件一多起来的时候,就不会出现 .exe.c 相互穿插在目录中^-^

img

7. 提示

  • 若源代码文件夹含有中文路径,将会无法编译程序。
  • 若你的Windows用户名使用了中文,可能无法运行。
  1. 什么是反射?
    Java的反射(reflection)机制是指在程序的运行状态中,可以构造任意一个类的对象,可以了解任意一个对象所属的类,可以了解任意一个类的成员变量和方法,可以调用任意一个对象的属性和方法。这种动态获取程序信息以及动态调用对象的功能称为Java语言的反射机制。反射被视为动态语言的关键。[摘自百度百科]

  2. 反射的用途
    反射通常由需要检查或修改Java虚拟机中运行的应用程序的运行时行为的程序使用。这是一个相对高级的功能,只应由对语言基础有很深了解的开发人员使用。考虑到这一警告,反射是一种强大的技术,可以使应用程序执行原本不可能的操作。

  3. 反射的优缺点
    优点:
    a. 可扩展性
    应用程序可以通过使用其完全限定的名称创建可扩展性对象的实例来使用外部用户定义的类。
    b. 类浏览器和可视化开发环境
    类浏览器需要能够枚举类的成员。可视化开发环境可以受益于利用反射中可用的类型信息来帮助开发人员编写正确的代码。
    c. 调试器和测试工具
    调试器需要能够检查类的私有成员。测试工具可以利用反射来系统地调用在类上定义的可发现的集合API,以确保测试套件中的代码覆盖率很高。
    缺点:
    反射功能强大,但不应随意使用。如果可以在不使用反射的情况下执行操作,那么最好避免使用它。通过反射访问代码时,应牢记以下注意事项。
    a. 性能开销
    由于反射涉及动态解析的类型,因此无法执行某些Java虚拟机优化。因此,反射操作的性能要比非反射操作慢,因此应避免在对性能敏感的应用程序中经常调用的代码段中。
    b. 安全限制
    反射需要运行时许可,而在安全管理器下运行时可能不存在。对于必须在受限的安全上下文(例如Applet)中运行的代码,这是一个重要的考虑因素。
    c. 暴露内部细节
    由于反射允许代码执行在非反射代码中是非法的操作,例如访问私有字段和方法,因此使用反射可能会导致意外的副作用,这可能会使代码无法正常工作并可能破坏可移植性。反射性代码破坏了抽象,因此可能会随着平台的升级而改变行为。
    以上内容引用百度百科或翻译自java官方文档,使大家对于Java反射有一个基本的认识,接下来我们开始学习java反射的基本使用。

  4. Java反射基本使用

    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
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
         1. 首先我们创建一个类,然后分别使用正常手段和反射方式获取类的实例
    public class Person {
    String name;
    private int age;

    // 无参构造
    public Person ()
    {

    }

    // 有参构造
    public Person(String name, int age) {
    this.name = name;
    this.age = age;
    }


    public String getName() {
    return name;
    }

    public void setName(String name) {
    this.name = name;
    }

    public int getAge() {
    return age;
    }

    public void setAge(int age) {
    this.age = age;
    }

    // public 方法
    public void printName()
    {
    System.out.println("printName = " + name);
    }

    // private 方法
    private void printAge()
    {
    System.out.println("printAge = " + age);
    }
    }

    这个类共有两个属性,两个构造方法,set,get方法以及一个公共的printName方法,一个私有的printAge方法,下面我们就分别使用正常方法和反射方法获取该类的对象

    一. 正常的方式
    Person person = new Person(); // 这种方式就不多说了,大家都会的
    二. 反射方式
    // 使用反射获取类对象的步骤如下
    // 1. 获取类
    // 2. 调用newInstance方法实例化对象
    // 获取类的方式有三种
    // 1. 类名.class
    // 2. 对象名.getClass();
    // 3. Class.forName("类的全路径名");
    // 以下例子讲解了反射的各种用法,其中包括:
    // 1. 获取类
    // 2. 获取类加载器(这个后面再说,先了解)
    // 3. 获取方法列表(public、private)
    // 4. 获取属性列表(public、private)
    // 5. 获取构造器列表
    // 6. 获取指定的方法,属性
    具体请看以下代码:


    public class main {


    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {

    // 1. 正常的手段
    Person person = new Person();

    // 2. 反射

    // 第一种拿到类的方式
    Class cls = Person.class;
    // 第二种拿到类的方式
    Class cls1 = person.getClass();
    // 第三种拿到类的方式
    Class cls2 = Class.forName("Person");


    // 获取实例, 使用Class.newInstance方法来实例化对象
    Person person1 = (Person) cls.newInstance();
    person1.setName("jiangc");
    person1.printName();

    // 类加载器,获取类加载器的方式,Class.getClassLoader
    ClassLoader classLoader = person1.getClass().getClassLoader();
    System.out.println(classLoader);

    System.out.println("------------------------获取公共方法,包括从父类继承 通过getMethods方法-----------------------------------------------------");
    // 获取公共方法,包括从父类继承 通过getMethods方法
    Method[] methods = cls.getMethods();
    // 打印一下
    for (int i = 0; i < methods.length; i++) {
    System.out.println(methods[i].getName() + "()");
    }
    System.out.println("------------------------获取类的所有方法,包括私有方法,只能是本类的 通过getDeclareMethods方法----------------------------------------------------");
    // 获取类的所有方法,包括私有方法,只能是本类的 通过getDeclareMethods方法
    Method[] declaredMethods = cls.getDeclaredMethods();
    for (Method declaredMethod : declaredMethods) {
    System.out.println(declaredMethod.getName() + "()");
    }
    System.out.println("------------------------获取指定方法 通过getMethod方法, 第一个参数是方法名,后面是可变参数,是参数的类型-----------------------------------------------------");
    // 获取指定方法 通过getMethod方法, 第一个参数是方法名,后面是可变参数,是参数的类型
    Method setName = cls.getMethod("setName", String.class);
    // 执行方法 通过invoke 第一个是对象,后面是方法的参数
    setName.invoke(person1, "lalala");
    person1.printName();
    // 其中注意的一个点,类似于int类型的在传入类型的时候使用int.class 这种方式,如下所示
    Method setAge = cls.getMethod("setAge", int.class);
    setAge.invoke(person1, 18);
    System.out.println("age = " + person1.getAge());

    // 访问私有方法有一些不同, 如果直接invoke会报错,因为没有访问权限,在调用私有方法的时候需要先调用setAccessible方法,打开访问权限
    Method printAge = cls.getDeclaredMethod("printAge");

    printAge.setAccessible(true);

    printAge.invoke(person1);
    System.out.println("-------------------获取字段 使用getFields 方法----------------------------------------------------------");

    // 获取字段 使用getFields 方法
    Field[] fields = cls.getFields();
    for (Field field : fields) {
    System.out.println(field.getName());
    }
    System.out.println("-------------------这里只有一个public修饰的字段,使用getDeclareFields方法可以获取私有的字段----------------------------------------------------------");

    // 这里只有一个public修饰的字段,使用getDeclareFields方法可以获取私有的字段
    Field[] declaredFields = cls.getDeclaredFields();
    for (Field declaredField : declaredFields) {
    System.out.println(declaredField.getName());
    }
    System.out.println("--------------------获取指定字段,和方法类似,这里使用的是getField方法, 传入字段名---------------------------------------------------------");
    // 获取指定字段,和方法类似,这里使用的是getField方法, 传入字段名
    Field name = cls.getField("name");
    System.out.println("打印:");
    System.out.println("字段的名字 :" + name.getName());
    // 打印字段的值
    System.out.println("字段的值:" + name.get(person1));
    // 修改public字段的值
    // 修改字段的值
    name.set(person1, "yayaya");
    System.out.println(name.get(person1));
    Object o = name.get(person1);

    // 修改私有字段的值
    System.out.println("修改私有字段的值, 和私有方法一样,无论访问还是修改,都需要调用setAccessible方法来打开权限");
    // 获取私有字段
    Field age = cls.getDeclaredField("age");

    age.setAccessible(true);
    System.out.println("age原本的值:" + age.get(person1));
    // 修改age
    age.set(person1, 27);
    System.out.println("修改之后的值:" + age.get(person1));
    // 大家看到,前面通过反射拿到的对象都是调用的newInstance方法,
    //这个方法我们没有传参数,Person有一个有参构造,那么我们怎么通过有参构造来实例化对象呢
    // 使用Constructors()方法获取所有的构造方法
    System.out.println("-----------------------------------------------------------------------------");
    System.out.println("通过getConstructors方法获取构造方法的列表");
    Constructor<Person>[] constructors = (Constructor<Person>[]) cls.getConstructors();
    for (Constructor<Person> constructor : constructors) {
    System.out.println(constructor);
    }

    // 获取指定名称的构造方法
    System.out.println("获取指定构造器实例化对象");
    Constructor<Person> constructor = (Constructor<Person>) cls.getConstructor(String.class, int.class);
    Person xixixi = (Person) constructor.newInstance("xixixi", 23);
    xixixi.printName();
    // 其他获取私有的啊什么的大家自己去尝试

    // 以上讲解了如何使用反射获取类的public和private的方法、属性,列表,获取public和private的指定方法,属性,以及调用方法,还有获取指定构造器实例化的使用方式。反射的知识就讲到这里。



    }
    }
  5. 反射在代理模式中的使用
    首先我们先看一下代理模式,什么是代理模式,举个例子:代购的公司,房产中介公司,都可以看作代理,例如说你要买海外的东西,有两种方式:1. 自己直接联系海外商家,直接买,2. 找代购,把自己的需求告诉代购,代购全权代理,你不需要和商家打交道,这就是代理
    我们先来看一下代理模式的类的关系图:
    这里是代理模式的类图

说一下这个类的关系,Subject类主要定义了一些公共方法,然后真实类和代理类去实现这个类,同时代理类拥有真实类的引用,从而调用真实类的方法。

代理模式有两种:1. 静态代理 2. 动态代理
我们先看静态代理

首先,想象一个场景,有一个房产中介aProxy,他可以为客户提供租房服务,客户aClient找他租房,中介aProxy找房东,中介为客户提供全套服务,包括合同,打扫等服务,下面我们根据代理模式的类图来实现一下

1.公共类,提供接口

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/**
* 公寓类,定义公共方法(对应抽象类:Subject)
*/
public interface Apartment {
// 租房接口,提供一个参数:租房面积
public void renting(int area);
}

3. 房东提供租房
/**
* 房产中介(对应上面类图中的真实类:Goodscompanies)
*/
public class Intermediary implements Apartment{
@Override
public void renting(int area) {
System.out.println("房屋出租面积为:" + area + "的房屋");

}
}
4. 中介代理房东

/**
* 代理人(对应上面的Proxy)
*/
public class ProxyPeople implements Apartment{
// 真实对象的引用
private Intermediary intermediary;


/**
* 代理自己的方法(增强方法)
*/
private void conclude_contract()
{
System.out.println("代理签合同");
}


/**
* 代理自己的方法(增强方法)
*/
private void sweep()
{
System.out.println("代理打扫房间");
}
@Override
public void renting(int area) {

conclude_contract();
// 调用真实的对象的方法
intermediary.renting(area);
sweep();
}
}

这时候,客户想买一个新房子,而这个租房子的代理人没有能力提供买房子的服务(大家这里不要抠细节,我们假设这个中介就只能处理租房的事情),而另外一个中介可以提供买房的信息,所以,客户买房子就只能换一个代理这里记为:ProxyRealty,提供新房的实体其实是房地产开发商,所以关系是,买房的代理通过开放商向客户提供买房服务,请看下面的代码:

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
/**
* 房产类接口
*/
public interface Brealty {
public void renting(int area);
}

/**
* 开发商
*/
public class realty implements Brealty{
@Override
public void renting(int area) {
System.out.println("客户买了:" + area + "面积的房子");
}
}

/**
* 提供买房服务代理类
*/
public class ProxyRealty implements Brealty{

// 提供房子的实体类
private realty re;

public ProxyRealty(realty re) {
this.re = re;
}

private void see_apartment()
{
System.out.println("带着顾客看房子...");
}

private void sign_contract ()
{
System.out.println("和客户签合同");
}

private void pay()
{
System.out.println("交首付");
}

private void handing_room()
{
System.out.println("交房");
}
@Override
public void renting(int area) {
see_apartment();
re.renting(200);
sign_contract();
pay();
handing_room();
}
}

大家想一下,假如这时候客户买完房子了,想装修,这时候通过网络又找到另外一个代理人,全权交给这个人帮自己装修,代码和上面类似,这里就不写了,大家有感觉了吗?这就是静态代理,大家可以对照类图和代码例子理解一下。


title3.1Launcher编译之Aidegen

Aidegen大概意思就是它是一个自动生成项目配置文件,针对不同的主流ide都是可以的,比如 Android Studio or IntelliJ project等,就不需要我们自己针对不同ide来搞对应项目

使用示例:

1
2
3
4
5
6
7
$source build/envsetup.sh && lunch <TARGET>
//例1.可以直接使用module name来进行对应的启动
$aidegen Settings -i s //这儿i是IDE的意思s代表Android Studio
//AIDEGen 会主动帮你把对应的模块编译一遍,顺带把梳理出的依靠用 Python 生成一个个的 dependency,最终直接帮你把 AS 拉起,项目主动打开
//例2.可以直接使用module path来进行对应的启动
$ aidegen packages/apps/Settings frameworks -i s
//studio中修改代码后,直接cd到模块内mm单编,成功后cd到源码make -j4整编查看效果
option long option Description
-d –depth The depth of module referenced by source.
-i –ide Launch IDE type, j=IntelliJ s=Android Studio e=Eclipse c=CLion v=VS Code
-p –ide-path Specify user’s IDE installed path.
-n –no_launch Do not launch IDE.
-r –config-reset Reset all AIDEGen’s saved configurations.
-s –skip-build Skip building jars or modules.
-v –verbose Displays DEBUG level logging.
-a –android-tree Generate whole Android source tree project file for IDE.
-e –exclude-paths Exclude the directories in IDE.
-l –language Launch IDE with a specific language,j=java c=C/C++ r=Rust
-h –help Shows help message and exits.

遇到的问题:

image-20230516100504865

image-20230516100541279

解决方案:(原因是本地AndroidStudio没有配置路径)

1
2
3
4
5
6
7
sudo gedit .bashrc
//最后一行加上
export PATH=$PATH:/home/Android/Sdk/platform-tools
export PATH=$PATH:/snap/android-studio/125/android-studio/bin
PATH=$PATH:/snap/android-studio/125/android-studio/bin
//保存后,执行立即生效
source .bashrc

自定义系统服务首先要创建aidl文件、aidl的stub实现类以及服务的Manager类,总共涉及到三个文件,首先我们先定义文件的名称:(以下3个类为了方便可以现在androidStudio中编译)
(1)ILanceManager.aidl
(2)LanceManager.java
(3)LanceManagerService.java

1.编写AIDL文件(Stub) ILanceManager.aidl

frameworks/base/core/java/android/os/ILanceManager.aidl;

1
2
3
4
5
6
package android.os;
// Declare any non-default types here with import statements
/** *@hide */
interface ILanceManager {
String request(String msg);
}

注册AIDL(编译配置)frameworks/base/Android.dp

1
2
3
4
5
filegroup{ 
srcs: [...
"core/java/android/os/ILanceManager.aidl",
...
]}

有的aosp版本为frameworks/base/Android.mk

1
2
3
4
LOCAL_SRC_FILES+=...
core/java/android/app/IActivityManager.aidl \
core/java/com/lance/service/ILanceManager.aidl \
...

2.创建系统服务(extends Stub)LanceManagerService.java

frameworks/base/core/java/com/android/server/LanceManagerService.java

1
2
3
4
5
6
7
8
9
10
11
package com.android.service;
import android.app.ILanceManager;
import android.os.RemoteException;
import android.util.Log;
public class LanceManagerService extends ILanceManager.Stub {
@Override
public String request(String msg) throws RemoteException {
Log.e("tyl","request:"+msg);
return "LanceManagerService:"+msg;
}
}

注册系统服务:frameworks/base/core/java/android/content/Context.java 添加上下文

1
2
3
#line 3784 新增上下文
public static final String LANCE_SERVICE = "lance";//需要小写
#line 3384 将LANCE_SERVICE添加到value中

frameworks/base/services/java/com/android/server/SystemServer.java 中的startOtherServices方法中添加系统服务

1
2
import com.android.service.LanceManagerService;
ServiceManager.addService(Context.LANCE_SERVICE, new LanceManagerService());

3.创建客户端代理 LanceManager.java

frameworks/base/core/java/android/os/LanceManager.aidl;

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
package android.os;
import android.util.Log;

public class LanceManager {
private ILanceManager service;
/**
*@hide
*/
public LanceManager(ILanceManager iLanceManager){
this.service=iLanceManager;
}
/**
*@hide
*/
public void require(String msg){
try {
String value = service.request(msg);
Log.e("tyl","require="+value);
} catch (RemoteException e) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
throw e.rethrowFromSystemServer();
}
}
}
}

注册客户端服务

frameworks/base/core/java/android/app/SystemServiceRegistry.java

1
2
3
4
5
6
7
8
9
10
11
12
13
import android.os.ILanceManager;
import android.os.LanceManager;
static {
//.....................
registerService(Context.LANCE_SERVICE, LanceManager.class,
new CachedServiceFetcher<LanceManager>() {
@Override
public LanceManager createService(ContextImpl ctx) throws ServiceNotFoundException {
IBinder b = ServiceManager.getServiceOrThrow(Context.LANCE_SERVICE);
ILanceManager service = ILanceManager.Stub.asInterface(b);
return new LanceManager(service);
}});
}

配置白名单 build/core/tasks/check_boot_jars/package_whitelist.txt(package_allowed_list.txt)

1
2
com\.android\..*//ILanceManager.aidl和LanceManager.java的路径,这里默认系统已有;
com\.enjoy\..* //如自定义文件夹则手动添加,例如enjoy为文件名;

3.添加SELinux权限

3.1 service_contexts

a. system/sepolicy/private/service_contexts文件
b. system/sepolicy/prebuilts/api/28.0/private/service_contexts文件
c. system/sepolicy/prebuilts/api/27.0/private/service_contexts文件
d. system/sepolicy/prebuilts/api/26.0/private/service_contexts文件
添加内容如下(注意这文件内容及格式都需要一致,要不然编译通不过

1
lance                                     u:object_r:lance_service:s0

3.2 service.te (未编译其他版本的报错,其他文件的其他版本未编译也make成功了)

a. system/sepolicy/public/service.te文件
b. system/sepolicy/prebuilts/api/28.0/public/service.te文件
c. system/sepolicy/prebuilts/api/27.0/public/service.te文件
d. system/sepolicy/prebuilts/api/26.0/public/service.te文件
添加内容如下(注意这文件内容及格式都需要一致,要不然编译通不过

1
type lance_service, app_api_service, ephemeral_app_api_service, system_server_service, service_manager_type;

3.3 untrusted_app.te

a. system/sepolicy/private/untrusted_app文件
b. system/sepolicy/prebuilts/api/28.0/private/untrusted_app文件
c. system/sepolicy/prebuilts/api/27.0/private/untrusted_app文件
d.system/sepolicy/prebuilts/api/26.0/private/untrusted_app文件
添加内容如下(注意这文件内容及格式都需要一致,要不然编译通不过

1
allow untrusted_app_all lance_service:service_manager find;

3.4

添加完之后,使用make update-api更新

image-20230510111816106

上面报错是因为是系统api,所以在ILanceManager和LanceManager的方法中加上 /** @hide */

make update-api编译通过后执行make -j20

4.调用自定义服务

4.1利用java的双亲委派机制,在自己的工程中引入这些自定义服务相关联的类,这样就可以直接使用这些类了,不需要用到反射

1
2
3
//activity中执行
LanceManager service = (LanceManager) getSystemService("lance");
service.require("hhh");
1
2
3
4
5
6
7
8
package android.os;
//只保留LanceManager这个类即可,其他类不需要;
//包名需与源码一致
public class LanceManager这个类即可,其他类不需要; {
public String require(String msg){
return null;
}
}

我这里报错:No virtual method require(Ljava/lang/String;)Ljava/lang/String; in class Landroid/os/LanceManager; or its super classes (declaration of ‘android.os.LanceManager’ appears in /system/framework/framework.jar!classes2.dex)大致意思是已经有一个相同包名的类出现,不确定是不是因为android11加入了什么检查机制,目前没找到解决方案;

4.2 修改sdk的方式调用(两种方案:)

1.AOSP Make SDK替换原本的SDK,

2.把AOSP中编译的LanceManager添加到原本的SDK中;

下面路径拿到LanceManager.java和Context.java

1
2
3
out/target/common/obj/JAVA_LIBRARIES/framwrork_intermediates/class.jar(改成zip后解压)/android/os/LanceManager.java

out/target/common/obj/JAVA_LIBRARIES/framwrork_intermediates/class.jar(改成zip后解压)/android/content/Context.java

打开Studiod的SDK路径/platforms/android-30(这里和studio的demo版本对应)将android.jar备份1份防止出错,将android.jar改为android.zip,再对应Context.java和LanceManager.java的包名复制进去后重新改为jar文件;