C/C++语言基础 –> JNI编程 –> NDK编程 –> 音视频处理 –> 图像处理等等 ,从零到一 , 零基础学习NDK开发 。学习本专题 , 你需要具备一定的Android开发知识 C/C++基础, 知道如何创建一个Android工程 , 有一定的开发经验更佳 。

前言

为什么要了解C/C++编译器?

它能够在后续移植第三框架(编译Android可用的库),编译这些库时,我们需要清楚的了解需要传递什么参数以及命令。

如何通过NDK gcc 编译出可以在手机上运行的可执行文件?

如果通过NDK gcc 编译出动态库.so 文件

环境

本文使用的环境:

我是在Mac上写的。

Linux(可以安装虚拟机或者买一个服务器)\Mac(最好又一个Mac)

VMware Ubuntu安装详细过程

(不推荐使用Window,如果你非要使用)
windows命令行 (推荐在Linux玩就行)

Windows C/C++编译器: https://sourceforge.net/projects/mingw/files/

配置环境变量 PATH: ${MinGW安装目录}/MinGW/bin

NDK 版本为ndk-r16b/ndk-17(注意不要使用ndk18把gcc 还有gnustl移除了 后面带着看了更新日志的,不能使用gcc的命令了,使用可执行文件的应用程序不再需要同时提供PIE和非PIE可执行文件。ndk18存在许多问题,不知道为何Google要这样做)

image.png

C/C++ 编译器介绍

clang

clang是一个轻量级的编辑器支持C、C++、ObjectC.基于LLVM(LLVM是以C++编写而成的构架编译器的框架系统,也可以说是一个用于开发编译器相关的库)

gcc(英语:GNU Compiler Collection,缩写为GCC)

GNU C 编译器,原本只能处理C语言,GCC在发布后很快地得到扩展,变得可处理C++。之后也变得可处理Fortran、Pascal、Objective-C、Java、Ada,Go与其他语言。(GNU 计划又称革奴计划 目标是创建一套完全自由的操作系统)许多操作系统,包括许多类Unix系统,如Linux及BSD家族都采用GCC作为标准编译器。苹果计算机预装的Mac OS X操作系统也采用这个编译器。摘自-维基百科

g++

GNU C++ 编译器,gcc和g++都能够编译c/c++,但编译但时候行为不同。

clang 也是一个编译器,对比gcc它具有编译速度更快、编译产出更小的优点但是某些软件在使用clang编译时候因为源码中内容的问题会出现错误。
clang++与clang就相当于gcc与g++。

对比gcc与g++

  • 后缀为.c的源文件,gcc把它当作是C程序,而g++当作c++程序。然而后缀为.cpp的源文件,都被当成c++程序。
  • g++ 会自动链接c++标准库stl,gcc不会。
  • gcc不会定义_cplusplus宏,而g++会。

linux 安装

apt install build-essential #安装gcc、g++与make

mac 直接使用gcc命令

如何使用GCC编译c源文件

现在打开Linux/Mac 命令行

输入:gcc g++ 查看是否安装成功,出现如下图则说明gcc g++ 安装成功了。
image.png

编写.c源文件
输入:vim main.c
按:I 键,然后输入一下内容:
image.png
输入完毕后,按“esc”键。

然后按“:” 再按“wq” 回车键保存退出

vim 按键图,不熟悉的可以学习一下
image.png

以上完毕后,会再当前的目录创建一个”main.c”文件,ls 命令查看。

继续输入:gcc main.c
如果出现以下错误,说明没有导入头件<stdio.h> vim main.c 命令重新编辑。
image.png

编译成功会生成两个文件
image.png

然后继续输入:gcc -o main main.c 会生成 main 可执行文件
image.png

输入:./main 执行,这样 c 文件就被编译完毕了
image.png

编译过程

一个c/c++ 文件是如何变成可执行文件呢?

一个c/c++ 文件要经过预处理(preprocessing)、编译(compilation)、汇编阶段、链接(linking)才能变成可执行文件

  • 预处理阶段

输入:gcc -E main.c -o main.i 会生成main.i 如下文件:

image.png

-E的作用是让gcc在预处理结束后停止编译。

预处理阶段主要处理include和define等。它把#include包含进来的.h 文件插入到#include所在的位置,把源程序中使用到的用#define定义的宏用实际的字符串代替。

可以打开main.i 文件查看。

  • 编译阶段

输入:gcc -S main.i -o main.s,生成main.s 文件

image.png

-S 的作用是编译后结束,编译生成了汇编文件。

这个阶段中,gcc 首先要检查代码的规范性、是否有语法错误等,以确定代码的实际要做的工作,在检查无误后,gcc 把代码翻译成汇编语言。

main.s.png

  • 汇编阶段

输入:gcc -c main.s -o main.o

生成 main.o 文件
汇编阶段把 .s文件翻译成二进制机器指令文件.o,这个阶段接收.c, .i, .s的文件都没有问题。

  • 链接阶段

输入:gcc -o main main.s

生成 main 可执行文件

链接阶段,链接的是函数库。在main.c中并没有定义”printf”函数实现,且在预编译中包含进的”stdio.h”中也只有该函数的声明。系统把这些函数实现都被做到名为libc.so的动态库。

/etc/ld.so.conf:
在没有特别指定时,gcc会到系统编译器只会使用/lib和/usr/lib这两个目录下的库文件。如果存在一个so不在这两个目录,在编译时候就会出现找不到的情况。
/etc/ld.so.conf文件中可以指定而外的编译链接库路径。
输入:cat /etc/ld.so.conf:

include /etc/ld.so.conf.d/*.conf #引入其他的conf文件
/usr/local/lib #增加库搜索目录

#编辑完成后 使用 ldconfig 更新

输入:./main 便可以执行main可执行文件。

以上便是main.c 的整个编译过程。

课外知识:如何生成静态库和动态库

函数库一般分为静态库和动态库两种
静态库是指编译链接时,把库文件的代码全部加入到可执行文件中,因此生成的文件比较大,但在运行时也就不再需要库文件了。Linux中后缀名为”.a”。

动态库与之相反,在编译链接时并没有把库文件的代码加入到可执行文件中,而是在程序执行时由运行时链接文件加载库。Linux中后缀名为”.so”,如前面所述的libc.so就是动态库。gcc在编译时默认使用动态库。

静态库节省时间:不需要再进行动态链接,需要调用的代码直接就在代码内部

动态库节省空间:如果一个动态库被两个程序调用,那么这个动态库只需要在内存中

Java中在不经过封装的情况下只能直接使用动态库。

#生成静态库
# -fPIC 产生与位置无关代码 
#可能会被不同的进程加载到不同的位置上,如果共享对象中的指令使用了绝对地址。
#那么在共享对象被加载时就必须根据相关模块的加载位置对这个地址做调整,也就是修改这些地址,
#让它在对应进程中能正确访问,那么就不能实现多进程共享一份物理内存(无法动态共享)
gcc -fPIC -c  Test.c -o Test.o
ar r libTest.a Test.o 

#生成动态库
gcc -fPIC -shared Test.c -o libTest.so
#或者
gcc -fPIC -c Test.c  #生成.o
gcc -shared Test.o -o libTest.so

#使用库
#默认优先使用动态库
gcc main.c -L. -lTest -o main
#强制使用静态库
#-Wl 表示传递给 ld 链接器的参数
#最后的 -Bdynamic 表示 默认仍然使用动态库 
gcc main.c -L. -Wl,-Bstatic  -lTest -Wl,-Bdynamic -o main
#使用动态库链接的程序,linux运行需要将动态库路径加入/etc/ld.so.conf
#mac(dylib)和windows(dll)可以将动态库和程序(main)放在一起
#mac 中gcc会被默认链接到xcode的llvm,不支持上面的指定链接动态库

#查看可执行文件符号
nm main

#打包 .a 到so
#--whole-archive: 将未使用的静态库符号(函数实现)也链接进动态库 
#--no-whole-archive : 默认,未使用不链接进入动态库
gcc -shared -o libMain.so -Wl,--whole-archive libMain.a -Wl,--no-whole-archive

作者:动脑学院安卓学院
链接:https://www.jianshu.com/p/9ce282dc3966
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

如何使用NDK中的gcc

思考一个问题,如果用上面编译出来的main可执行文件放到手机中,能执行吗?

Android虽然也是基于Linux操作系统类似ARM的嵌入式系统,但是它和Pc端的CPU的指令集显然是不一样的。

image.png (手机需要root)

那么如何在交叉编译出手机上可以执行的文件呢?

首先,我们先要了解几个命令:

头文件与库文件指定

--sysroot=XX
    使用xx作为这一次编译的头文件与库文件的查找目录,查找下面的 usr/include usr/lib目录
-isysroot XX
    头文件查找目录,覆盖--sysroot ,查找 XX/usr/include
-isystem XX
    指定头文件查找路径(直接查找根目录)
-IXX
    头文件查找目录
优先级:
    -I -> -isystem -> sysroot

-L:XX
    指定库文件查找目录
-lxx.so
    指定需要链接的库名

需要指定 NDK的gcc 进行编译

/xxx/ndk-bundle/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64/bin/arm-linux-androideabi-gcc

同时还要指定头文件和库文件,因为运行.c的源文件,内部引用了头文件和库文件,需要指定ndk中的头文件和库文件。

指定库文件

--sysroot=/xxx/ndk-bundle/platforms/android-21/arch-arm 

指定多个头文件

-isystem /xxx/ndk-bundle/sysroot/usr/include
-isystem /xxx/ndk-bundle/sysroot/usr/include/arm-linux-androideabi

为了方便调用,我们将上面的一大段写成一个变量来使用

在命令行输入:

$ export CC="/xxx/ndk-bundle/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64/bin/arm-linux-androideabi-gcc --sysroot=/xxx/ndk-bundle/platforms/android-21/arch-arm -isystem /xxx/ndk-bundle/sysroot/usr/include -isystem /xxx/ndk-bundle/sysroot/usr/include/arm-linux-androideabi"

然后我们就可以编译 main.c 文件了

输入: $CC -pie main.c -o main

image.png

很明显我们将main 编译成了可以在ARM中执行的文件。

然后将main文件拷贝到手机中执行,手机需要root权限

image.png

如何将.c源文件编译成.so动态库

将main.c 编译成动态库.so文件

输入:
$ mv main.c test.c

$CC -fPIC -shared test.c -o libTest.so

image.png

将libTest.so 拷贝到AS 到jniLibs中去。

image.png

CMakeLists.txt 中添加如下

image.png

image.png

这样便可以调用libTest.so