NDK 编译系列文章共三篇,目录如下:
NDK 编译(一)—— Linux 知识汇总
NDK 编译(二)—— NDK 编译与集成 FFmpeg
NDK 编译(三)—— CMake 原生构建工具
由于很多时候我们需要在 Linux 环境下进行编译,所以有必要熟悉 Linux 的常用操作以及 Shell 脚本的相关知识。
1、Linux 常用操作
1.1 常用快捷键
命令 | 作用 |
---|---|
Ctrl + A | 光标移至最前面 |
Ctrl + E | 光标移至最后面 |
Ctrl + U | 清除所有内容 |
Tab | 自动补齐输入 |
Up、Down | 切换之前、之后输入的命令 |
1.2 文件操作
常用命令
cd 目录切换指令:
命令 | 作用 |
---|---|
cd … | 切换至上一级目录 |
cd dirname | 切换到 dirname 路径下 |
cd 或 cd ~ | 切换到用户的根目录 root 下 |
cd / | 切换到系统根目录 / 下,该目录包含 root |
ls 查看目录下文件:
命令 | 作用 |
---|---|
ls | 仅列出当前目录下的文件名称 |
ls -a | 显示包含隐藏文件(即 . 开头的文件)在内的文件名 |
ls -l | 显示文件详情(创建者,创建时间,权限等) |
ls -all 或 -la | 即 -a 和 -l 的叠加,显示包含隐藏文件在内的详情(创建者,创建时间,权限) |
ls -lh | 显示文件大小在内的详情 |
ls -R | 递归查看目录内文件结构 |
其他文件相关命令:
命令 | 作用 |
---|---|
pwd | 查看当前路径 |
mkdir dirname | 在当前目录下创建名为 dirname 的目录 |
touch xxx.xx | 创建名为 xxx.xx 的文件 |
rm dirname/ | 非递归删除目录 |
rm -rf dirname/ | 递归遍历删除目录,-r 递归,-f 强制 |
cp f.txt New/ | 将 f.txt 文件拷贝到 New 目录下 |
mv f.txt fff.txt | 将 f.txt 文件重命名为 fff.txt 文件 |
文件解读
在系统根目录下输入 ls -lah 命令,结果如下:
[root@iZbp18dho1ypob4dgeh3fwZ /]# ls -lah
total 64K
dr-xr-xr-x. 18 root root 4.0K Nov 14 17:27 .
dr-xr-xr-x. 18 root root 4.0K Nov 14 17:27 ..
-rw-r--r-- 1 root root 0 Jul 28 08:19 .autorelabel
lrwxrwxrwx. 1 root root 7 Feb 9 2022 bin -> usr/bin
dr-xr-xr-x. 5 root root 4.0K Jul 28 16:28 boot
drwxr-xr-x 17 root root 3.0K Nov 14 17:28 dev
drwxr-xr-x. 92 root root 4.0K Dec 19 11:45 etc
drwxr-xr-x. 2 root root 4.0K Feb 9 2022 home
lrwxrwxrwx. 1 root root 7 Feb 9 2022 lib -> usr/lib
lrwxrwxrwx. 1 root root 9 Feb 9 2022 lib64 -> usr/lib64
drwx------. 2 root root 16K Jul 28 16:13 lost+found
drwxr-xr-x. 2 root root 4.0K Feb 9 2022 media
drwxr-xr-x. 2 root root 4.0K Feb 9 2022 mnt
drwxr-xr-x. 2 root root 4.0K Feb 9 2022 opt
dr-xr-xr-x 209 root root 0 Nov 14 17:27 proc
dr-xr-x---. 6 root root 4.0K Dec 19 10:58 root
drwxr-xr-x 28 root root 840 Dec 19 11:45 run
lrwxrwxrwx. 1 root root 8 Feb 9 2022 sbin -> usr/sbin
drwxr-xr-x. 2 root root 4.0K Feb 9 2022 srv
dr-xr-xr-x 13 root root 0 Nov 14 17:27 sys
drwxrwxrwt 4 root root 100 Dec 19 11:17 tmp
drwxr-xr-x. 12 root root 4.0K Jul 28 16:13 usr
drwxr-xr-x. 21 root root 4.0K Jul 28 16:23 var
每一行从左至右的含义,以 lrwxrwxrwx. 1 root root 7 Feb 9 2022 bin -> usr/bin
为例:
- 文件权限:分为三个部分:
- 第一个字符表示文件类型。l 表示符号连接(软连接),d 表示文件夹,- 表示普通文件,c 表示字符设备文件,b表示块设备文件,p 表示管道,s 表示套接字
- 接下来三组字符是权限组,每组三个字符。三个组别依次是文件所有者、所属组和其他用户对该文件的权限,而 rwx 分别表示具备读权限、写权限和执行权限,如不具备某项权限该位置用 - 代替
- 对 SELinux 的特殊标记,即
lrwxrwxrwx.
最后的 . 表示文件或目录具有扩展的安全上下文,意味着 SELinux 系统可能在该符号链接上应用了特定的安全策略和访问控制规则
- 文件相关连接数:例子中是 1,表示在该目录下,包括自身在内有 1 个链接指向它(类似于 Windows 的快捷方式)
- 文件所有者与文件所属组:连续两个 root 表示该文件属于 root 用户和 root 组
- 符号链接目标字符数:符号链接目标 user/bin 有 7 个字符。如果是一个普通文件夹,这个位置就是显示该文件夹的大小
- 文件最后的修改时间:
Feb 9 2022
表示文件最后修改时间为 2022 年 2 月 9 日 - 符号链接的名称和目标:在例子中符号链接名为 “bin”,它指向目标 “usr/bin”。这表示通过访问 “bin”,实际上是访问 “usr/bin” 目录。普通的文件夹或文件的话这里显示的就是文件夹或文件名
1.3 命令执行的原理
在 Linux 终端,如 XShell 或 Mac 终端中输入了命令后,终端内部的命令解析器会去系统根目录的 /bin 目录下查找该命令(注意它并不是一个文件,而是一个系统命令的可执行程序)。比如终端输入 date 会执行 ./date 命令,解析器会去 /bin 下寻找 date 程序,由于 /bin 是连接到 /usr/bin 的,所以实际上是去找 /usr/bin/date 程序并执行,最后将结果返回给终端。
1.4 环境变量操作
使用 export 可以定义系统的临时环境变量,关闭窗口重新打开后,该临时环境变量就不存在了:
[root@ ~]# export AAAA=12345
[root@ ~]# echo $AAAA
12345
全局环境变量需要在 /etc/profile 文件中添加:
[root@frank ~]# vim /etc/profile
[root@frank ~]# source /etc/profile
[root@frank ~]# echo $BBB
Test
在编辑 profile 文件时加入:
export BBB="Test"
编辑完成后重启终端或者通过 source /etc/profile
命令执行脚本文件(注意不是普通的执行文件)并将脚本中的命令应用到当前 Shell 会话中,然后才能输出 BBB 的内容。
1.5 用户与用户组
命令 | 作用 |
---|---|
sudo su root | 从普通用户切换到 root 用户 普通用户的命令提示符是 $,root 用户的是 # |
exit | 退出 root 用户权限 |
chmod 777 FileName | 将 FileName 文件的权限修改为 777,对应 rwxrwxrwx 4 可读的,对应 r,2 可写,对应 w,1 可执行,对应 x |
chmod a+x FileName | 修改 FileName 文件的权限,给所有组加上可执行权限 x + 前面可以是 a(所有组)、u(文件拥有者)、g(同组)、o(其他组) + 后面可以是 r(读)、w(写)、x(执行) |
adduser UserName | 添加一个用户(组) |
chown UserName FileName | 修改文件(夹)的所有者 |
chgrp UserGroupName FileName | 修改文件(夹)的所属组 |
举个例子:
[root@frank NDK24]# touch test.txt
[root@frank NDK24]# ls -l
total 0
-rw-r--r-- 1 root root 0 Dec 19 16:33 test.txt
root 用户先一个新文件 test.txt,可以看到默认权限是 -rw-r–r–。然后创建一个新用户,将 test.txt 的所有者和所属组都修改成新用户的:
[root@frank NDK24]# adduser NewUser
[root@frank NDK24]# chown NewUser test.txt
[root@frank NDK24]# ls -l
total 0
-rw-r--r-- 1 NewUser root 0 Dec 19 16:33 test.txt
[root@frank NDK24]# chgrp NewUser test.txt
[root@frank NDK24]# ls -l
total 0
-rw-r--r-- 1 NewUser NewUser 0 Dec 19 16:33 test.txt
最后修改用户权限,可以用数字也可以用字母限定法:
[root@frank NDK24]# chmod +x test.txt
[root@frank NDK24]# ls -l
total 0
-rwxr-xr-x 1 NewUser NewUser 0 Dec 19 16:33 test.txt
[root@frank NDK24]# chmod o+w test.txt
[root@frank NDK24]# ls -l
total 0
-rwxr-xrwx 1 NewUser NewUser 0 Dec 19 16:33 test.txt
[root@frank NDK24]# chmod 777 test.txt
[root@frank NDK24]# ls -l
total 0
-rwxrwxrwx 1 NewUser NewUser 0 Dec 19 16:33 test.txt
首先用 +x 为所有组都添加可执行权限,再通过 o+w 给其他组加上写权限,最后用数字 777 给所有组附上所有权限。
需要注意的是,+x 和 +r 默认都是对三组生效,只有 +w 是默认对第一组,即当前用户添加写权限。
此外,删除权限,如果使用字符限定法可以用 - 号,数字限定直接计算删除后权限的值即可,比如:
[root@frank NDK24]# chmod o-rwx test.txt
[root@frank NDK24]# ls -l
total 0
-rwxrwx--- 1 NewUser NewUser 0 Dec 19 16:33 test.txt
[root@frank NDK24]# chmod 140 test.txt
[root@frank NDK24]# ls -l
total 0
---xr----- 1 NewUser NewUser 0 Dec 19 16:33 test.txt
先用 o-rwx 删除其他组的所有权限,然后再用 140 保留所有者的执行和同组的读权限,删除其他权限。
1.6 其他命令
命令 | 作用 |
---|---|
clear | 清除屏幕 |
date | 获取当前服务器时间 |
lsb_release -a | 查看当前的系统版本 |
cat FileName | 快速查看文件内容 |
tac FileName | 倒序查看文件内容 |
vi FileName | 用 vim 编辑文件,C/C++ 不高亮 |
vim FileName | 用 vim 编辑文件,C/C++ 高亮 |
more FileName | 分页查看文件内容 |
head -2 FileName | 只查看文件的前两行 |
tail -3 FileName | 只查看文件的最后三行 |
df -h | 查看磁盘情况 |
2、Vim 的使用
Vim 的设计理念:不让手指离开字母区就能完成所有编辑工作,抛弃鼠标和上下左右等区域。
2.1 Vim 的工作模式
Vim 有四种工作模式:
- 命令模式:Vim 的默认模式。在命令模式下,你可以输入各种命令来执行编辑、移动光标、搜索等操作。进入命令模式的方式是按下
Esc
键。 - 插入模式:在插入模式下,你可以直接输入文本内容。在命令模式下按下
i
键即可进入插入模式。你也可以按下a
键进入插入模式并将光标定位在当前字符的后面。 - 可视模式:可视模式用于选择文本块以进行复制、剪切和粘贴等操作。在命令模式下按下
v
键即可进入可视模式。你可以使用光标键来选择文本块。 - 命令行模式:在命令行模式下,你可以输入各种指令来保存文件、退出 Vim 等。进入命令行模式的方式是在命令模式下按下
:
(冒号)键。
2.2 Vim 的常用命令
可能会有不同模式下相同按键具有不同功能的情况。
命令模式
输入以下字符会进入插入模式,不同字符的表现略有不同:
- i:在当前位置进入插入模式
- I:在当前行首位进入插入模式
- a:光标向后移一位进入插入模式
- A:光标移至当前行末尾进入插入模式
- s:删除光标所在字符后进入插入模式
- S:删除光标所在行后进入插入模式
光标操作相关:
- u:撤销之前的操作
- h:光标向左移动
- j:光标向下移动
- k:光标向上移动
- l:光标向右移动
- 5G:跳转多行
- gg:跳到第一行
- G:跳到最后一行
- $:跳到行尾
- 0:跳到行首
删除、复制、粘贴:
- dw:删除单词
- dd:删除一行
- 3dd:删除当前行在内的后续三行
- yy:复制一行
- dd:剪切一行
- P:将剪切复制的内容粘贴到当前行
- p:将剪切复制的内容粘贴到下一行
查找、替换:
- /:/ 后面紧接着要查找的内容,比如 /cat 就是查找文件内的与 cat 匹配(大小写敏感)的内容,该内容会被高亮
- r + 一个字符:r 后紧跟一个字符,会把光标所在的字符替换成 r 后输入的字符
命令行模式
命令行模式在使用中其实就是在冒号后面输入要执行的指令,与命令模式的区分并没有很明显。
常用的命令行:
:q!
:退出不保存之前编辑过的内容:set number
:显示行号:s/include/xxx/g
::s/
是一个替换指令的前缀,include 指代要被替换的内容,xxx 是替换后的内容。默认情况下,只会替换当前行第一个 include,如果加上末尾的 /g,就会替换当前行所有的 include:1,6s/include/xxx/g
:表示对第 1 行到第 6 行的代码执行上一条的操作:%s/include/xxx/g
:表示对整个文件内都执行替换
注意末尾的 /g 就是表示对每一行所有匹配的部分都进行替换,而不只是第一个。
此外还有分屏相关的命令:
- :vsp:分成左右两个屏幕
- :sp:分成上下两个屏幕
- :q:退出当前屏幕
- :wqall:退出全部屏幕
- Ctrl + ww:切换屏幕
3、Shell 脚本编程入门
主要介绍一些基本的 Shell 语法。
3.1 第一个脚本
新建一个脚本文件 test1.sh 并输入以下内容:
#!/bin/bash
echo "Hello world!"
保存以后尝试运行这个脚本:
[root@frank NDK24]# ./test1.sh
-bash: ./test1.sh: Permission denied
[root@frank NDK24]# chmod u+x test1.sh
[root@frank NDK24]# ./test1.sh
Hello world!
[root@frank NDK24]# /bin/bash test1.sh
Hello world!
[root@frank NDK24]# sh test1.sh
Hello world!
过程如下:
- 第一次用
./test1.sh
运行脚本,提示权限拒绝,因此我们给该文件加上当前用户可执行权限,然后再次以相同命令运行,输出了 Hello world! - 第二次用 bash 解释器运行脚本,也成功得到了输出。从命令中能看出 bash 解释器的路径为 /bin/bash
- 第三次用 sh 解释器运行脚本也能成功输出
这里有一个细节要注意,就是脚本内容的第一行 #!/bin/bash
一定不要忘记 bin 前面的斜杠,否则运行时会报错:
-bash: ./test1.sh: bin/bash: bad interpreter: No such file or directory
因为这句话是指定解释器路径的,而上面提到了,bash 解释器的路径为 /bin/bash,缺少了 bin 前的斜杠导致找不到解释器运行时就会报错了。如果你不确定 bash 解释器的位置,可以通过 which 命令获取:
[root@frank ~]# which bash
/usr/bin/bash
此外我们还需了解 sh 与 bash 这两个解释器的区别:
sh
和bash
是两种不同的 Shell 程序,它们在使用和功能上有一些区别。
sh(Bourne Shell):
sh
是最早的 Unix Shell,也是许多 Unix 系统默认的命令解释器。它遵循 POSIX 标准,并提供了基本的 Shell 功能,如变量、条件语句和循环等。sh
是一种较为简单和轻量级的 Shell,不具备一些现代 Shell 的高级功能。bash(Bourne Again Shell):
bash
是sh
的增强版,也是许多 Linux 系统默认的 Shell。bash
在兼容sh
的基础上,提供了更多的功能和特性,包括命令补全、命令历史、作业控制等。它是一个功能更强大、更灵活的 Shell,对脚本编写和交互式操作都提供了更多便利。主要区别如下:
bash
比sh
提供更多的特性和功能。bash
兼容sh
,因此sh
脚本可以在bash
中运行,但bash
脚本不一定可以在sh
中运行。bash
支持交互式操作,包括命令历史、命令补全等,而sh
的功能较为简化。bash
的语法更加灵活,提供了更多的控制结构、扩展功能和内置命令。在大多数情况下,如果没有特殊需求,使用
bash
作为默认的 Shell 是一个更好的选择,因为它提供了更多的功能和便利。然而,为了保持最大的兼容性,一些脚本仍然使用sh
作为脚本的解释器。
3.2 变量的定义与使用
先来看一个简单例子:
#!/bin/bash
#无类型,无须声明变量类型,但是要注意等号两侧不能有空格
A=10
#使用变量的值,前面要加$
echo A=$A
声明一个变量 A 并打印了它的值,运行该脚本:
[root@frank NDK24]# ./test2.sh
A=10
[root@frank NDK24]# echo $?
0
成功输出 A=10,然后通过输出 $?
,可以显示上一条命令的退出状态码,以验证命令是否执行成功,0 表示成功。
再来看一个稍复杂点的例子:
#!/bin/bash
# 1.输出指令执行结果,指令要大写
echo PWD:$PWD
# 2.获取指令参数
echo 当前Shell脚本的名字:$0
echo 参数一:$1
echo 参数二:$2
# 3.通过 ? 获取执行状态
if(($?));then
echo 执行失败
else
echo 执行成功
fi
# 4.通过符号获取参数内容与数量
echo 外界传递参数内容是:$*
echo 外界传递参数的数量:$#
通过 sh test3.sh
执行脚本时,在后面加上两个参数:
[root@frank NDK24]# sh test3.sh param1 param2
PWD:/root/NDKLearning
当前Shell脚本的名字:test3.sh
参数一:param1
参数二:param2
执行成功
外界传递参数内容是:param1 param2
外界传递参数的数量:2
对结果的解释如下:
-
在脚本文件里面获取指令时一定要大写,就像例子中的 PWD,而在 Shell 命令行内就是小写 pwd
-
执行脚本时后续跟着的参数在脚本内通过 $1、$2 等获取,而 $0 则表示脚本文件名称,通过不同的指令运行脚本得到的结果也不同:
- 例子中通过 sh 命令运行,得到的就是文件名本身 test3.sh
- 如果通过 ./test3.sh 运行,那么得到的结果就是 ./test3.sh,是脚本文件的相对路径
如果想要在任何情况下都只获取文件名不包含路径,可以使用 basename 命令提取文件名:
echo "当前Shell脚本的名字:$(basename "$0")"
-
if 后的双括号 (()) 是必须的,用于对括号内的表达式作为数学运算或逻辑比较进行求值,不能用于字符串比较(字符串比较用 [] 或 [[]]):
#!/bin/bash var1="abcde" var2="zzzzz" # 判断是否相等 if [ $var1 = $var2 ] then echo "等于" else echo "不等于" fi
3.3 循环
for 循环
Shell 内置了 seq 指令来生成数列:
[root@frank ~]# seq 1 5
1
2
3
4
5
[root@frank ~]# seq -f "file%03g" 10 -2 0
file010
file008
file006
file004
file002
file000
默认步长为 1,可以通过 -f 指定输出的字符串格式。通过 seq 实现循环操作:
#!/bin/bash
for i in `seq 1 5`
do
echo 遍历的数字是:$i
done
借助循环,可以完成一个累加功能:
#!/bin/bash
result=0
for((i=0;i<=100;i++))
do
result=`expr $i + $result`
done
echo 1~100累加结果是:$result
注意等号两侧不能有空格,但是加号两侧必须有空格。此外,给 result 赋值还可以有简化写法:
result=$((i+result))
((result+=i))
此外还需注意上述代码中出现的反引号 ``
表示将命令的输出结果嵌入到另一个命令或表达式中。
while 循环
#!/bin/bash
while((i<10))
do
((i++))
echo 遍历的值:$i
done
3.4 压缩打包
将当前目录下所有的 .sh 文件打包进 all.tgz 压缩包中:
#!/bin/bash
tar -czf all.tgz $(find . -name "*.sh")
视频中讲解的方法有问题:
#!/bin/bash
for i in `find . -name "*.sh"`
do
echo $i
tar -czf all.tgz $i
done
按照这种循环,将一个文件存入 all.tgz 后,然后再将下一个文件打包进 all.tgz,后面的会覆盖前面的,导致最后结果是只有一个 all.tgz 内保存着一个 sh 文件。
3.5 读取文件内容
#!/bin/bash
while read lineVar
do
echo $lineVar
done<`pwd`/test1.sh
将当前目录下的 test1.sh 文件每读出一行内容就赋值给 lineVar 变量,并且输出这个 lineVar 变量。
3.6 算数运算符
基本方式,注意数字的前后要有空格:
[root@frank ~]# result=$(( 100 + 100 ))
[root@frank ~]# echo $result
200
这种方式比较麻烦,有一种稍微简单些的方式:
[root@frank ~]# result=`expr 100 + 100`
[root@frank ~]# echo $result
200
也是加号前后的空格不能省略。
3.7 重定向
将文件内容重定向(读取)到 Shell 命令行:
[root@frank NDK24]# cat 0<test1.sh
0 表示命令行,< 表示重定向到命令行。
将命令行的内容重定向(输出)到文件中:
[root@frank NDK24]# echo 666 > test7.sh
test7.sh 文件内容为 666。
3.8 函数
Shell 函数内可以声明变量,这个变量没有像 Java 似的堆或栈的概念:
#!/bin/bash
function test01() {
var1="Terry"
echo $var1
}
# 调用函数
test01
Shell 函数的参数不是定义在函数声明的括号内,而是在函数内用 $1、$2 等符号标记,然后在调用函数或运行脚本时传入:
-
调用函数时传入参数:
#!/bin/bash function test01() { var1=$1 echo $var1 } # 传入参数 9999 test01 9999
-
运行脚本时传入参数:
#!/bin/bash function test01() { var1=$1 echo $var1 } # 等待运行脚本时在命令行传入参数 test01 $1
执行脚本时在后面传入参数:
[root@frank NDK24]# ./test7.sh 666 666
4、交叉编译
4.1 概述
什么是交叉编译:
交叉编译是指在一台主机上进行编译,生成能够在另一种不同体系结构或操作系统上运行的可执行程序或库文件。
通常情况下,编译器在编译过程中会针对当前主机的体系结构和操作系统进行优化,并生成与主机平台兼容的可执行文件。但是,当我们需要在不同的体系结构或操作系统上运行程序时,就需要进行交叉编译。
例如,假设你正在使用一台 x86 架构的主机开发软件,并希望在 ARM 架构的嵌入式设备上运行该软件。由于这两种架构具有不同的指令集和系统调用,你需要进行交叉编译,以便在 x86 主机上生成适用于 ARM 架构的可执行文件。
交叉编译的过程涉及到使用特定的交叉编译工具链,包括交叉编译器、交叉链接器和相关的库文件。这些工具链能够将源代码编译成目标体系结构或操作系统所需的可执行文件或库。
交叉编译可以扩展软件的移植性和跨平台能力,使开发人员能够在一台主机上开发和测试针对不同体系结构或操作系统的程序,从而提高开发效率和灵活性。
为什么需要交叉编译:
举个例子,用 Linux 服务器编译出来的可执行文件,在 Linux 服务器内可以直接执行。但是将该文件拷贝到 Android 设备的 /data/local/tmp 目录下(拷贝到这个目录下可以执行,其他目录文件没有执行权限也不能修改权限),并执行该文件,会出现类似错误:
/system/bin/sh: ./mainEXE: not executable: 64-bit ELF file
或这样的错误:
ELF: inaccessible or not found
说明非交叉编译的执行文件,不能在安卓文件系统中运行,因此才需要通过 NDK 交叉编译,这样的产物才能在 Android 环境中运行。
4.2 安装 Linux 工具 lrzsz
工具 sz
和 rz
通常用于通过终端进行文件传输,特别是在使用 ssh
连接到远程 Linux 主机时。使用 rz
命令从本地计算机向远程主机上传文件,使用 sz
命令从远程主机下载文件。这些工具通常是 lrzsz
软件包的一部分。
命令安装
要安装 lrzsz
软件包,可以使用以下命令,具体取决于你的 Linux 发行版和包管理器:
对于基于 Debian/Ubuntu 的系统(使用 apt):
sudo apt install lrzsz
对于基于 CentOS/RHEL 的系统(使用 yum):
sudo yum install lrzsz
对于基于 Fedora 的系统(使用 dnf):
sudo dnf install lrzsz
对于其他 Linux 发行版,请根据其所使用的包管理器进行适当的安装命令。
源码安装
root 账号登陆后,依次执行以下命令:
wget http://www.ohse.de/uwe/releases/lrzsz-0.12.20.tar.gz
tar zxvf lrzsz-0.12.20.tar.gz
cd lrzsz-0.12.20
./configure && make && make install
上面安装过程默认把 lsz 和 lrz 安装到了 /usr/local/bin/ 目录下,现在我们并不能直接使用。下面创建软链接,并命名为 rz/sz:
# 安装到这里来
cd /usr/local/bin
# 设置快捷方式
ln -s /usr/local/bin/lrz rz
# 设置快捷方式
ln -s /usr/local/bin/lsz sz
4.3 NDK 下载
在 Linux 服务器中下载 Linux 版本的 NDK:
[root@frank AndroidNDK]# wget https://googledownloads.cn/android/repository/android-ndk-r26b-linux.zip
使用 unzip 解压下载到的 android-ndk-r26b-linux.zip:
[root@frank AndroidNDK]# unzip android-ndk-r26b-linux.zip
解压会得到 android-ndk-r26b-linux 文件夹,去 android-ndk-r26b/toolchains/llvm/prebuilt/linux-x86_64/bin/
目录下根据运行设备的 CPU 架构以及 Android 系统的版本号找到你需要的编译器。比如我在 API33 的模拟器上运行,就可以选择 x86_64-linux-android33-clang
,使用该命令编译:
[root@frank AndroidNDK]# android-ndk-r26b/toolchains/llvm/prebuilt/linux-x86_64/bin/x86_64-linux-android33-clang test.c -o test
可以得到编译产物 test。
将 x86_64-linux-android33-clang 命令所在的文件夹添加到环境变量中,这样就不用输入全路径,并且可以通过 Tab 补全指令,使用更加方便。在 /etc/profile 中添加:
export PATH="/root/AndroidNDK/android-ndk-r26b/toolchains/llvm/prebuilt/linux-x86_64/bin:$PATH"
或者也可以把这条指令定义成全局变量:
export NDK_GCC_x86_64=/root/AndroidNDK/android-ndk-r26b/toolchains/llvm/prebuilt/linux-x86_64/bin/x86_64-linux-android33-clang
4.4 动态库编译与配置
接下来在 Linux 下交叉编译动态库并配置到 Android Studio 中使用。首先,我们在 Linux 下写一个 C 的源文件 test.c:
#include<stdio.h>
int get() {
return 777;
}
然后使用 NDK 的 clang 编译该源文件成一个动态库文件:
[root@frank AndroidNDK]# x86_64-linux-android33-clang -fPIC -shared test.c -o libtest.so
在命令执行的目录下会生成 libtest.so 文件,将这个文件拷贝到 AS 项目的 /app/main/jniLibs/x86_64/ 目录下,然后配置 CMakeLists:
# 将 libtest.so 所在的 /jniLibs/x86_64/ 目录添加到 CMAKE_CXX_FLAGS 中
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -L${CMAKE_SOURCE_DIR}/../jniLibs/${CMAKE_ANDROID_ARCH_ABI}")
# 将 test 库链接到目标库 compile 中
target_link_libraries(
compile
test)
然后在 native-lib.cpp 文件中声明库中的函数:
// 因为库是用 C 写的,因此这里要声明用 C 的方式编译
extern "C" {
extern int get();
}
extern "C"
JNIEXPORT jint JNICALL
Java_com_linux_compile_MainActivity_getFromJNI(JNIEnv *env, jobject thiz) {
return get();
}
在 UI 上将 Native 方法 getFromJNI() 的结果显示出来:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.sampleText.text = getFromJNI().toString()
}
external fun getFromJNI(): Int
屏幕上就会显示 get() 返回的 777。
4.5 静态库编译与配置
编译静态库分两步:
-
将源文件编译成对应的 .o 文件:
[root@frank AndroidNDK]# aarch64-linux-android33-clang -fPIC -c test.c -o test.o
-
将 .o 文件打包成 .a 文件:
[root@frank AndroidNDK]# llvm-ar rcs libtest.a test.o
llvm-ar 与 aarch64-linux-android33-clang 在同一个目录下,由于之前已经添加到环境变量中,所以可以直接用。
将 libtest.a 拷贝到 AS 项目的 /app/main/cpp/ 目录下,然后配置 CMakeLists:
# 添加 test 库
add_library(test STATIC IMPORTED)
# 将静态库文件设置给 test 变量
set_target_properties(test PROPERTIES IMPORTED_LOCATION
${CMAKE_SOURCE_DIR}/libtest.a)
# 链接 test 库到主库 compile
target_link_libraries(
compile
test)
运行过程跟动态库一样,就不再赘述。需要注意的是,为了保证程序正常运行,还需要在 build.gradle 配置关于 CPU 架构的内容:
android {
defaultConfig {
externalNativeBuild {
cmake {
abiFilters "x86_64"
}
}
ndk {
abiFilters "x86_64"
}
}
}