c/c++编译器配置(交叉编译重要参数)、交叉编译动态库与as配置、mk初步

gcc/g++/clang,至关于javac:java

了解c/c++编译器的基本使用,可以在后续移植第三方框架进行交叉编译时,清楚的了解应该传递什么参数。android

clang:c++

clang 是一个C、C++、Object-C的轻量级编译器。基于LLVM (LLVM是以C++编写而成的构架编译器的框架系统,能够说是一个用于开发编译器相关的库)shell

gcc:windows

GNU C编译器。本来只能处理C语言,很快扩展,变得可处理C++。(GNU计划,又称革奴计划。目标是建立一套彻底自由的操做系统)架构

g++:框架

GNU c++编译器函数

gcc、g++、clang都是编译器。工具

  • gcc和g++都可以编译c/c++,可是编译时候行为不一样。
    这块须要特别的注意,并不是gcc是为c而生,而g++是为c++而生的。学习

  • clang也是一个编译器,对比gcc,它具备编译速度更快、编译产出更小等优势,可是某些软件在使用clang编译时候由于源码中内容的问题会出现错误。

  • clang++与clang就至关于gcc与g++。

对于gcc与g++:

  1. 后缀为.c的源文件,gcc把它看成是C程序,而g++看成是C++程序;后缀为.cpp的,二者都会认为是c++程序

  2. g++会自动连接c++标准库stl,gcc不会

  3. gcc不会定义__cplusplus宏,而g++会

编译器过程:

一个C/C++文件要通过预处理(preprocessing)、编译(compilation)、汇编(assembly)、和链接(linking)才能变成可执行文件。

  • 预处理
    gcc -E main.c  -o main.i 

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

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

  • 编译阶段

    gcc -S main.i -o main.s

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

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

  • 汇编阶段

    gcc -c main.s -o main.o

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

  • 链接阶段

    gcc -o main main.s

    ​连接阶段,连接的是函数库。在main.c中并无定义”printf”的函数实现,且在预编译中包含进的”stdio.h”中也只有该函数的声明。系统把这些函数实现都被作到名为libc.so的动态库。
    函数库通常分为静态库和动态库两种:

    • 静态库是指编译连接时,把库文件的代码所有加入到可执行文件中,所以生成的文件比较大,但在运行时也就再也不须要库文件了。Linux中后缀名为”.a”。
    • 动态库与之相反,在编译连接时并无把库文件的代码加入到可执行文件中,而是在程序执行时由运行时连接文件加载库。Linux中后缀名为”.so”,如前面所述的libc.so就是动态库。gcc在编译时默认使用动态库。

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

    动态库节省空间:若是一个动态库被两个程序调用,那么这个动态库只须要在内存中。
    关于这二者的区别我们用一个具体的例子来形象的说明一下:
    一、假若有一个静态库a.a,它里面包含一个test函数,而后有个源文件source.c,它里面有个test1函数,而这个源文件须要连接a.a这个静态库,当编译完成以后source.a里面就拥有了test+test1两个函数了,也就是编译期就将全部的符号加入到.so库中。
    二、假若有一个动态库a.so,它里面包含一个test函数,而后有个源文件source.c,它里面有个test1函数,而这个源文件须要连接a.o这个动态库,当编译完成以后source.a里面就只有一个test函数,在运行时会动态的加载a.so。

    注:Java中在不通过封装的状况下只能直接使用动态库,也就是说:

了解了上面的一大堆理论以后,下面来动手作实验,先新建一个main.c文件:

而后用gcc命令来将其编译运行一下:

那若是将main这个可执行文件放到Android手机上,可否也能正常执行输出呢?我们来将它放到手机的sdcard上【注意:该手机须要的文件的执行权限才行,不然会报无权限,反正我用的带root的模拟器执行都不行,最后找了部完全root的华为机子来作的实验】,以下:

这是为啥呢?实际上是Android手机的CPU不一样,因此CPU的指令集也不一样,要在mac上编译出来的可执行文件能在Android上也能运行这里就须要涉及到交叉编译相关的东东了,其实在NDK中提供有交叉编译工具,先进我们的SDK来瞅一下:

下面我们手动尝试经过NDK的交互编译工具来尝试一下,前提固然得下载Android NDK才行,具体直接上官网下就成了,个人电脑已经下载好了,因此下面关心的就是如何来进行交叉编译啦,首先固然得用上图中的NDK提供的gcc啦,为了方便使用我们先将这个gcc的文件路径配置成临时的环境变量,免得每次编译时须要写一大堆的路径,以下:

好,我们来用它来对main.c进行编译一下:

缘由是因为此时须要用NDK提供的头文件来进行连接编译了, 那如何指定头文件的查找路径为NDK提供的头文件呢,有以下方式能够指定,下面先来了解一下【很是重要,是编译三方库很是重要的知识点】

  • --sysroot=XX
    使用XX做为这一次编译的头文件与库文件的查找目录,查找XX下面的 usr/include、usr/lib目录。
  • -isysroot XX
    头文件查找目录,覆盖--sysroot ,查找 XX/usr/include。什么意思,好比说"gcc --sysroot=目录1 main.c",若是main.c中依赖于头文件和库文件,则会到目录1中的user/include和user/lib目录去查找,而若是"gcc --sysroot=目录1 -isysroot 目录2 main.c"意味着会查找头文件会到目录2中查找而非--sysroot所指定的目录1下的/usr/include了,固然查找库文件仍是在目录1下的user/lib目录去查找。
  • -isystem XX
    指定头文件查找路径(直接查找根目录)。好比"gcc --sysroot=目录1 -isysroot 目录2 -isystem 目录3  -isystem 目录4 main.c"意味着头文件查找除了会到目录2下的/usr/include,还会到isystem指定的目录3和目录4下进行查找,注意:这个isystem指定的目录就是头文件查找的全路径,而非像isysroot所指定的目录还须要定位到/usr/include目录。
  • -IXX
    头文件查找目录。

其查找头文件的优先级为:
-I -> -isystem -> sysroot

好比说:“gcc --sysroot=目录1 -isysroot 目录2 -isystem 目录3  -isystem 目录4 -I目录5 main.c”,其头文件首先会去目录5找,若是没找到则会到目录3和4找,若是还没找到则会到目录2找。

  • -LXX
    指定库文件查找目录。
  • -lxx.so
    指定须要连接的库名。

咱们以前在写JNI程序时用到了Android的日志,以下:

其这个头文件中的具体实现库其实就是在NDK中的这个目录里面,以下:

其实在Android Sutdio建立支付C++工程时其实默认就将这个头文件库的查找在CMakeLists.txt已经进行声明了,以下:

若是用参数的形式来指定库查找目录其实就能够这样写:“gcc -L/Users/xiongwei/android-sdks/Android/sdk/ndk-bundle/platforms/android-21/arch-arm/usr/lib -llog”, 固然还能够用--sysroot来指令库文件的查找路径,只是路径指定须要在/usr/lib以前就成,如“gcc --sysroot/Users/xiongwei/android-sdks/Android/sdk/ndk-bundle/platforms/android-21/arch-arm/ -llog”。

明白了上面的参数以后,下面我们采用交叉编译的方式来对咱们的main.c进行从新编译,因为NDK的路径比较深,因此仍是采用临时环境变量的方式来弄,如今就是要指定头文件的查找目录,因此头文件的位置涉及到两处:

因此先把这个路径定义上:

基本上有这个路径就能够了,可是在NDK16或NDK17还有以下头文件查找路径:

因此能够用-isystem参数来指定,以下:

另外还有一个子目录须要指定:

固然仍是能够用-isystem参数来指定,以下:

那我们再来加上这个CFLAGS参数编译一下:

呃,貌似配错了,具体是由于写得有问题,以下:

因此修改一下:

此时就正常编译了,我们此时用file命令来查看一下该生成的可执行程序的文件信息,能够有个新发现:

正好就是Android能运行的指令集,因此下面我们再将这个main导到手机上,而后此次来运行看可否见证奇迹:

而后进入到adb shell中进行执行,以下:

pie 位置无关的可执行程序:

可是有可能在windows中既始用交叉编译在手机上执行时也会报错,例如:

此就就须要加一个-pie参数了,以下:

因此手动交叉编译成Android手机能正常执行的统一加"-pie"参数就成了。

费了这么多功夫就为了能编一个能在Android手机上执行的可执行文件有啥做用么?做用实际上是很是之大的哈,在以后的学习中会有体现滴,明白了这个手动编译的原理,对于未来任务三方库要编译成能在Android运行库,明白了以上内容就能让本身变得驾轻就熟,练内功滴!!

交叉编译动态库与as配置:

在上面我们编译出来的并不是是一个动态库而只是一个可执行文件,要想要在Android工程中来使用JNI就必须将期编译成.so的动态库,由于:

因此我们接下来利用交叉编译工具来编成动态库,而后在Android Studio中进行使用,下面先来编写一个新的.c源文件:

此时须要将它编译成动态库,就须要加一个“-fPIC -shared”参数,具体用法以下:

因此我们来使用一下,注意仍是使用NDK提供的交叉编译的GCC命令哈:

注意一下so的名称是以libXXX.so为规则,由于咱们要在Android工程中来使用它,因此将它拷到我们的Android工具当中,以下:

而后还须要建一个cpu架构相关的文件夹,相似于咱们在日常引入三方.so时同样,好比:

因此校仿:

那接下来我们就是要在程序中来调用这个动态库中的test()方法,因为没有提供这个动态库相关的头文件,因此可使用以下关键字:

代码已经写好了,不过要正式运行以前还得进行CMakeLists.txt一系列的配置才行,下面一步步来进行配置,首先咱们的so的目录在编译时是须要依赖于它可是目前尚未指令编译时查找库的路径,根据以前介绍的交叉编译的参数可使用以下:

那如何在CMakeLists.txt中来设置呢,涉及到一些规则,记住就成了,以下:

其中还有一个小技巧,就是对于CPU架构文件夹的指定可使用动态的方式而不用手动写死,不是不一样的CPU架构的.so都是不同的嘛:

涉及到须要修改的地方在它:

能够改用以下这种动态的方式:

这样当咱们要编译其它的CPU架构时就能够动态的替换,这里先还原写死的方式,待后面须要的时候再用这个动态的方式,接着来则须要指定要连接的.so库,配置以下:

其中也就相似于写了以下参数:

好了,接着还须要去build.gradle中进行NDK的配置,首先指定咱们要编译的CPU架构,目前只支持"armeabi-v7a",因此配置以下:

而后编译运行一下:

这是为啥?其实有一个很是小的细节没有注意形成:

再次编译:

因此更改一下:

再次编译,发现编译终于木有问题了,接下来我们在MainActivity中来调用一下jni:

而后再次编译运行:

正常输出啦,可是有可能在其它手机上会输出以下异常:

此时就须要在调用以前先将我们的libTest.so动态库给加载进来,以下:

至于为啥要再加load一次咱们的生成的Test动态库,其实仍是跟动静态库有关,若是是引用的静态库的话就不会有这个问题,这个在以后再作实验来讲明这个问题。下面仍是回到gradle对ndk配置相关的东东,在上面作实现的工程是由于建项目时就已经勾选了支付NDK的环境,以下:

那若是对于一个没有勾选这个支持的Android工程咱们怎么来加入对NDK的支持呢?下面建一个全新的不支持NDK的Android工程:

而后接下来就是来在build.gradle中来进行配置将该工程变为支持NDK的,以下:

若是不确认写得对不对,能够点击看一下可否连接到源码,若是能连接到源码那写得确定是对滴,以下:

 而后继续:

这种语法其实看着不是很符合java的语法,其实也能够用另一种面向对象的方式来配置,具体以下:

可是貌似在个人Android Studio中这种语法不支持,因此我们仍是以上面标红的方式来配置,基本默认新建带NDK的工程就是使用的这种方式,接下来来配置要编译的CPU架构:

而后在外层还有一个externalNativeBuild配置,此次咱们能够改用面向对象的方式,以下:

那这两个NDK相关的配置有啥区别呢?实际上是有区别的:

因为我们想经过mk的方式来进行编译,因此能够在外层这样写:

 

因此咱们能够在src下新建一个Android.mk文件,以下:

Android.mk

微小 GNU makefile 片断。

将源文件分组为模块。 模块是静态库、共享库或独立可执行文件。 可在每一个 Android.mk 文件中定义一个或多个模块,也可在多个模块中使用同一个源文件。 

 

关于Android.mk的编译脚本的编写先日后放,这里关于ndk配置还差一个东东,以下:

实际上是这样的:

好,as中关于ndk的配置相关的基本已经配好了,接下来就是新建一个c/c++的源文件我们来尝试着编译一下:

那这个源代码该要如何进行编译呢?这里就须要用到了咱们建的Android.mk这个编译脚本文件啦,首先来熟悉一下大概的语法:

变量和宏

定义本身的任意变量。在定义变量时请注意,NDK 构建系统会预留如下变量名称:

  •  LOCAL_ 开头的名称,例如 LOCAL_MODULE
  •  PRIVATE_NDK_ 或 APP 开头的名称。构建系统在内部使用这些变量。
  • 小写名称,例如 my-dir。构建系统也是在内部使用这些变量。

若是为了方便而须要在 Android.mk 文件中定义本身的变量,建议在名称前附加 MY_

经常使用内置变量

变量名 含义 示例
BUILD_STATIC_LIBRARY 构建静态库的Makefile脚本 include $(BUILD_STATIC_LIBRARY)
PREBUILT_SHARED_LIBRARY 预编译共享库的Makeifle脚本 include $(PREBUILT_SHARED_LIBRARY)
PREBUILT_STATIC_LIBRARY 预编译静态库的Makeifle脚本 include $(PREBUILT_STATIC_LIBRARY)
TARGET_PLATFORM Android API 级别号 TARGET_PLATFORM := android-22
TARGET_ARCH CUP架构 arm arm64 x86 x86_64
TARGET_ARCH_ABI CPU架构 armeabi armeabi-v7a arm64-v8a

模块描述变量

变量名 描述
LOCAL_MODULE_FILENAME 覆盖构建系统默认用于其生成的文件的名称 LOCAL_MODULE := foo LOCAL_MODULE_FILENAME := libnewfoo
LOCAL_CPP_FEATURES 特定 C++ 功能 支持异常:LOCAL_CPP_FEATURES := exceptions
LOCAL_C_INCLUDES 头文件目录查找路径 LOCAL_C_INCLUDES := $(LOCAL_PATH)/include
LOCAL_CFLAGS 构建 C  C++ 的编译参数  
LOCAL_CPPFLAGS c++  
LOCAL_STATIC_LIBRARIES 当前模块依赖的静态库模块列表  
LOCAL_SHARED_LIBRARIES    
LOCAL_WHOLE_STATIC_LIBRARIES --whole-archive 将未使用的函数符号也加入编译进入这个模块
LOCAL_LDLIBS 依赖 系统库 LOCAL_LDLIBS := -lz

导出给引入模块的模块使用:

LOCAL_EXPORT_CFLAGS

LOCAL_EXPORT_CPPFLAGS

LOCAL_EXPORT_C_INCLUDES

LOCAL_EXPORT_LDLIBS

上面大体了解以后接下来须要在Android.mk中加入编译规则,具体以下:

因此依照上面的规则将其编写到Android.mk中:

我们能够将其路径打印看一下:

不过在编译前还得先把mk整个配置给填充完,因此:

而后我们来编译一下:

而后咱们看一下编译出来的动态库:

这是由于咱们在NDK这块只配了它:

那若是咱们增长一个"x86"呢?

而后我们再看一下编出来的APK中包含的动态库的类型:

以上就是经过手动的方式来给我们的一个普通Android工程增长Ndk的支持,下面来继续解读一下我们在.mk中编写的脚本的含义:

假如要有多源文件则以空格分开,若是想换行的话能够以“\”,好比:

好了,此次学习的东东说实话有些杂,可是这些知识点是很是很是之重要的基础,只有把基础打牢了才能在将来的NDK学习之路走得更加的远,坚持!!!

相关文章
相关标签/搜索