当编译过程比较复杂的时候, 我们就可以使用 shell 脚本来简化这个过程, 同时版本升级, 重复构建的基础上 shell 脚本也能够重复利用, 下面直接开始看看 shell 脚本是如何使用的, 先从最简单的开始

hello shell

1
2
3
4
#!/bin/bash

# echo 打印输出
echo "Hello Shell!"

#!/bin/bash 第一行一般都是用来声明是用什么解释器来执行, 这里是 Bash , 然后 echo 是用来打印的, 一个简单的 hello shell 脚本就完成了, 然后通过 cmd 执行 ./1.sh 就可以了

变量

  • 变量的设置规则

    1. 命名跟 Java 规范一样, 不能以数字开头等
    2. shell 中默认都是字符串类,如果需要用到其他类型,需要额外处理
    3. 变量用等号连接,不能有空格
    4. 变量的值如果有空格,需要用单引号或者双引号包括

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      
      #!/bin/bash
      
      # 变量
      str1="str11"
      str2=str1 # 赋值
      str3=32 # 数字会被默认当作字符串处理
      
      echo "str1 = ${str1}"
      echo "str2 = $str2"
      echo "str3 = $str3"
      
      # 输出环境变量 CMAKE_PATH
      echo "CMAKE_PATH = ${CMAKE_PATH}"
      
      # 指定一个命令的执行结果返回给变量 (pwd 是当前的工作目录)
      soPath=`pwd`
      echo "sopath = ${soPath}"

代码都比较简单, 语法也基本是固定格式, 运行后, 结果如下

获取参数

  1. $n 来获取参数,$0 代表程序本身,$1-$9代表第一个参数到第九个参数,十以上的参数要用大括号 ${10}
  2. $* 代表的是命令中的所有参数,但是会把参数看成一个整体 "$0,$1,$2,$3,$4...$n"
  3. $@ 代表命令中的所有参数,但是会把参数区分对待 "$0","$1","$2"..."$n"
  4. $# 代表参数中的个数

     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
    
    #!/bin/bash
    
    echo "$0=$0"
    echo "\$1=$1"
    echo "\$2="$2
    
    echo "\$*="$*
    echo "\$@="$@
    echo "\$#="$#
    
    #每次逻动(删除)第一个参数
    shift
    
    echo "\$1=$1"
    echo "\$@="$@
    
    echo "========================================"
    for i in $*
    do
    echo $i
    done
    
    echo "========================================"
    for i in $@
    do
    echo $i
    done
    
    echo "========================================"
    for i in "$@"
    do
    echo $i
    done
    
    echo "========================================"
    for i in "$*"
    do
    echo $i
    done

还有一些,读取控制台上的输入赋值给变量

1
2
3
# 获取用户及时输入的参数
#read -p "please input your name!" name
#echo "name = $name"

read 表示读取用户输入,然后将其存储出 name 中

1
2
read -sp "please input your password" pwd
echo "password=$pwd"

这里和上面的区别就是, -sp 会将密码隐藏起来,并赋值给 pwd,执行结果如下

1
2
read -t 5 -n 1 -p "please input [y|n]" input
echo "input = $input" 

这里代表的意思是:

  • read 命令来读取用户输入
  • -t 5:设置一个超时时间为 5 秒。如果用户在 5 秒内没有输入内容,read 命令将会自动结束,并且 input 变量会为空
  • -n 1:指定读取一个字符的输入(即用户输入一个字符后 read 命令会立即结束,而不需要等待用户按下 Enter 键)
  • -p "please input [y|n]: ":在读取用户输入前,显示提示信息 "please input [y|n]: "

预定义变量

指的是 bash 中已经定义好的,我们可以直接拿过来用

  1. $?:返回的是上一个执行命令的返回值,执行成功返回 0 ,执行失败返回非0
  2. $$:获取当前角本的进程号
  3. $!:获取最后一个后台执行的进程号

条件判断和数字使用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# 条件判断 test expression 或者 []
# 测试的范围:整数,字符串,文件

num1=1
num2=2
# 计算和, 注意默认情况下,shell 当作是字符串
num3=$num1+$num2
echo $num3

# 数字的计算, 使用 expr 命令, 或者 $(())
num4=$(($num1+$num2))
echo $num4

# 执行的是 expr 命令,注意需要有空格
num5=`expr $num1 + $num2`
echo $num5

执行的结果如下

条件判断

1
2
3
4
1. [ str ] 测试字符串是否不为空
2. test -n str 测试字符串是否不为空或者是 [ -n str ] 
3. test -z str 测试字符串是否为空或者是 [ -z str ] 
4. [ str1 = str2 ] 是否相等

数字:

1
2
3
4
5
6
1. [ num1 -eq num2 ] 测试是否相等
2. [ num1 -ne num2 ] 不等
3. [ num1 -ge num2 ] >=
4. [ num1 -gt num2 ] >
5. [ num1 -le num2 ] <=
6. [ num1 -lt num2 ] <

文件:

1
2
3
4
5
6
7
1. test -d file  目录
2. test -f file   普通文件
3. test -e file  存在
4. test -L file  链接
5. test -r file   可读
6. test -w file  可写
7. test -x file   可执行

接下来让我们编写一个根据用户输入编译类型来编译出对应的构建产物,这在实际的场景中也挺场景的

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#!/bin/bash

# 判断有没有输入 ( $1 是取的第一个参数)
if [ -z $1 ]
then
        echo 请输入需要编译的 type 【Release or Debug】
        exit # 结束掉执行
fi # 结束 if 语句

# 定义变量,指定编译类型
CMAKE_BUILD_TYPE=""
if [ "Debug" = $1 ]; then
        CMAKE_BUILD_TYPE="Debug"
elif [ "Release" = $1 ]; then
        CMAKE_BUILD_TYPE="Release"
else echo 请输入需要编译的 type 【Release or Debug】
        exit

fi

echo "${CMAKE_BUILD_TYPE} 正在开始编译中, 请稍后"

函数

在上面的例子中,提示的错误信息都是一样的, 这里可以用一个函数封装起来

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/bin/bash

# 函数需要使用的话, 需要先定义
function error(){
        echo 请输入需要编译的 type 【Release or Debug】
        echo "方法参数的输入 \$1 = ${1}"  #函数需要传参数 arm $1 参数不是代表的脚本参数,而是方法传递过来的参数

                exit
}

if [ -z $1 ]
then error exit # 函数使用
fi 

CMAKE_BUILD_TYPE=""
if [ "Debug" = $1 ]; then
        CMAKE_BUILD_TYPE="Debug"
elif [ "Release" = $1 ]; then
        CMAKE_BUILD_TYPE="Release"
else error arm  # 函数使用, 并传递参数

fi

echo "${CMAKE_BUILD_TYPE} 正在开始编译中, 请稍后"

这里将相同的逻辑部分, 都封装了在 error 中,其余的基本不变, 需要注意的是 error 内部 $1 接受到的参数区别,不是脚本传过来的参数,而是方法传递过来的参数, 运行后如下

循环和switch

 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
echo "=========================================="
for((i=0;i<10;i++))
do
echo $i
done

echo "=========================================="
sum=0
i=1
while(($i<=100))
do
        sum=$(($sum+$i))
        i=$(($i+1))
done
echo "sum = $sum"

echo "=========================================="
case $1 in # 读取终端输入的第一个参数
start) # 命中 start 条件
        echo "start service"
        ;;
stop) # 命中 stop
        echo "stop service"
        ;;
*) # 所有条件都没有命中
        echo "invalid command"
        echo "Usage:{start|stop}"
esac # esac 是 case 语句的结束标志

执行后结果如下

输出日志到文件中

临时重定向

临时重定向是指一次性地将命令的输出重定向到某个文件或设备。常用的重定向符号有:

  • >:将标准输出(stdout,文件描述符1)重定向到一个文件,会覆盖文件的内容
  • >>:将标准输出重定向到一个文件,会追加到文件末尾
  • 2>:将标准错误(stderr,文件描述符2)重定向到一个文件,会覆盖文件内容
  • 2>>:将标准错误重定向到一个文件,会追加到文件末尾

    1
    2
    3
    4
    5
    6
    
    # 临时重定向,> 代表写入到文件中,会被覆盖
    echo "log error" > log.txt
    echo "log error 2" > log.txt
    
    # >> 代表的是追加
    echo "log error 3" >> log.txt

执行结果后, 会生成 log.txt, 文件信息是这样的

永久重定向

永久重定向是使用 exec 命令改变当前 Shell 环境中文件描述符的默认目的地,直到 Shell 会话结束或被再次更改。永久重定向对所有后续命令生效,而不仅仅是单个命令

1
2
3
4
5
6
7
8
9
# 永久重定向 echo 标准输入,0标准输出,1标准错误,2追加
exec 1>log.txt # 
exec 2>>log.txt

echo "h1"
echo "h2"
echo "h3"

随便输入
  1. exec >log.txt

    • 将标准输出重定向到 log.txt,这意味着之后所有的标准输出都会写入到 log.txt,并覆盖其中已有的内容。
  2. exec >>log.txt

    • 将标准输出重定向追加到 log.txt,这意味着之后所有的标准错误都会追加写入到 log.txt

所以, 执行后,log.txt 文件是这样的

交叉编译

在上一篇文章中,我们编译了 libmath.so 库, 是在 Linux 中编译的,只能在 Linux 中使用,为了在 Android 平台上能够使用

那么就需要使用交叉编译了,这里我们刚刚已经学习了 shell 脚本编写,接下来使用 shell 脚本,来完成此次的交叉编译

要完成交叉编译,需要安装 NDK,并且配置环境变量,有这么以下几个步骤

  1. 下载 NDK

    1
    2
    3
    4
    5
    
    # 在 Linux 中的下载 NDK,在控制台中输入
    wget -c https://dl.google.com/android/repository/android-ndk-r27-linux.zip?hl=zh-cn
    # 然后进行解压 
    unzip 文件名
    # 解压后,放置到 lib/ndk 目录下
  2. 配置NDK环境变量

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    vim ~/.bashrc
    # 在文件末尾追加以下代码:
    export NDK_PATH=/lib/ndk/android-ndk-r27
    export PATH=$PATH:$NDK_PATH
    
    # 然后更新一下环境变量
    source ~/.bashrc
    
    # 再试试 ndk-build出现如下说明就安装好了
    Android NDK: Could not find application project directory !
    Android NDK: Please define the NDK_PROJECT_PATH variable to point to it.
    /usr/lib/ndk/android-ndk-r27/build/core/build-local.mk:151: *** Android NDK: Aborting    .  Stop.

然后使用上一篇文章中的源代码,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
# 构建生成 so 库
cmake_minimum_required(VERSION 3.22)

# 给工程取一个名字
PROJECT (Math)

MESSAGE(STATUS "this is PROJECT_SOURCE dir "${PROJECT_SOURCE_DIR})

# 指定头文件在哪个目录
INCLUDE_DIRECTORIES (${PROJECT_SOURCE_DIR}/include)

# 搜集 src 目录下的所有 .cpp 文件(源文件)
# SRC_LIST 代表 src 目录下的所有源文件 
AUX_SOURCE_DIRECTORY (${PROJECT_SOURCE_DIR}/src SRC_LIST)


# 指定 so 的生成目录 lib
SET (LIBRARY_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/lib)

MESSAGE (STATUS "src_list : "${SRC_LIST})

# 指定生成动态库 .so  math -> libmath.so  默认生成的是静态库 
ADD_LIBRARY (math SHARED ${SRC_LIST})

然后编写脚本文件,其中最重要的是 NDK 相关的一些配置

1
2
3
4
cmake -DANDROID_NDK=${NDK_PATH} \ #ndk安装目录
        -DCMAKE_TOOLCHAIN_FILE=${NDK_PATH}/build/cmake/android.toolchain.cmake \ # NDK 交叉编译工具链
        -DANDROID_ABI="armeabi-v7a" \ # 指定生成库支持的架构
        -DANDROID_NATIVE_API_LEVEL=26 # 指定支持的最低版本

完整内容如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/bin/bash

echo "NDK_PATH = ${NDK_PATH}"

# 检查 NDK 路径是否正确
if [ ! -d "$NDK_PATH" ]; then
  echo "NDK 路径不存在: $NDK_PATH"
  exit 1
fi

# 创建并进入构建目录 (目的是为了将构建文件统一放在 build 文件夹中)
mkdir -p build
cd build

# 运行 CMake 配置
cmake .. \
-DANDROID_NDK=${NDK_PATH} \ # NDK 安装目录
  -DCMAKE_TOOLCHAIN_FILE=${NDK_PATH}/build/cmake/android.toolchain.cmake \ # NDK 交叉编译工具链
  -DANDROID_ABI=arm64-v8a \  # 指定生成库支持的架构
  -DANDROID_NATIVE_API_LEVEL=26 # 指定支持的最低版本

# 运行 make
make

以往我们的编译,我们的编译文件都是和 CMakeLists 文件放在同一级文件夹的,会有些混乱,所以这里我们将构建文件都放在了 build 文件中,然后执行 ./build.sh 就可以编译出 Android 平台上能够使用的 so 库了

Android 中 导入第三方 so 库

这样我们就编译好了得到了 so 库,然后试试在 Android studio 上看看能不能正常使用,创建一个全新的 C++ Android 项目,导入 so 库,它的文件路径是这样的

大致做的步骤是引入头文件,导入 so 库,然后在 C 代码中,调用 so 库的 add 方法,运行后结果如下

可以发现,sum 计算正确,也正常输出了,可以证明我们的 so 库已经成功的在 Android 平台上运行了