Android NDK——必知必会之使用gcc/g++在Linux下完成交叉编译(八)

版权声明:本文为CrazyMo_原创,转载请在显著位置注明原文链接 https://blog.csdn.net/CrazyMo_/article/details/83379994

引言

前一篇文章Android NDK——必知必会之Makefile和CMake基本使用语法概述(七)简单介绍了MakeFile和CMake的相关知识,这篇就从小结下在Linux下交叉编译的基本流程。

一、在Linux下通过gcc/g++编译运行C/C++

1、编译的基本流程

一个C/C++源文件通常需要经过预处理(preprocessing)编译(compilation)、**汇编(assembly)**和 链接(linking) 四个流程才会变成可以在系统平台上的可执行文件,但是并不意味着必须要执行四个过程所有对应的命令,以编译main.c为例:
这里写图片描述

  • 预处理阶段主要处理include和define等,它把#include包含进来的.h 文件插入到#include所在的位置,把源程序中使用到的用#define定义的宏用实际的字符串代替。
//其中 -E的作用是让gcc在预处理结束后停止编译。
root@Mo-vm:/usr/local/src/share# gcc -E main.c -o mainclient.i
root@Mo-vm:/usr/local/src/share# ls
main.c  mainclient.i

  • 编译阶段时,gcc首先要检查代码的规范性、是否有语法错误等,以确定代码的实际要做的工作,在检查无误后,gcc把代码翻译成汇编语言。
// -S的作用是编译后结束,编译生成了汇编文件。
root@Mo-vm:/usr/local/src/share# gcc -S mainclient.i -o mainclient.s
root@Mo-vm:/usr/local/src/share# ls
main.c  mainclient.i  mainclient.s

  • ​ 汇编阶段把 .s文件翻译成二进制机器指令文件.o,这个阶段接收.c, .i, .s的文件都没有问题。
root@Mo-vm:/usr/local/src/share# gcc -c mainclient.s -o mainclient.o
root@Mo-vm:/usr/local/src/share# ls
main.c  mainclient.i  mainclient.o  mainclient.s

  • 链接阶段是链接的是函数库(包含静态库和动态库),在main.c中并没有定义”printf”的函数实现,且在预编译中包含进的”stdio.h”中也只有该函数的声明,系统把这些函数实现都被做到名为libc.so的动态库。
root@Mo-vm:/usr/local/src/share# gcc -o mainclient mainclient.s
root@Mo-vm:/usr/local/src/share# ls
main.c  mainclient  mainclient.i  mainclient.o  mainclient.s
root@Mo-vm:/usr/local/src/share# ./mainclient 
excute c in linux 
root@Mo-vm:/usr/local/src/share# 

默认情况下,gcc编译器只会使用/lib和/usr/lib这两个目录下的库文件,如果存在一个so不在这两个目录,在编译时候就会出现找不到的情况,幸好还可以在**/etc/ld.so.conf文件中可以指定而外的编译链接库路径**,需要引入第三方的库时也可以在**/etc/ld.so.conf**文件里更新

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

#编辑完成后 使用 ldconfig 更新

2、在Linux环境下通过gcc/g++把C/C++源文件,编译成可执行的文件(CPU指令集)。

这里写图片描述
如上图所示,生成了两个可执行的文件(仅隶属于与当前编译环境所支持的CPU指令集),但是这两个都无法在Android平台上运行(要验证也很简单找一台root了的手机然后把生成的可执行文件push到手机里,接着就像在Linux执行一样直接用命令行运行即可),虽然Android系统也是基于Linux系统的,但是在Linux上编译出来的可执行文件仅支持当前的CPU,很明显不同平台的CPU所支持的指令集是不一样的(即C/C++不跨平台)。这也是前面我们介绍NDK目录下的子目录platforms下每个系统下都有不同架构的子目录,通过NDK提供的这些工具我们就可以编译对应架构的函数库。

#生成静态库
# -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 : 默认,未使用不链接进入动态库

二、通过NDK进行交叉编译

所谓交叉编译可以简单理解为在一个平台环境下编译出另一个平台可运行的CPU 指令集的过程(这个概念仅仅是我个人的理解)而NDK 中的toolchains\xxx-linux-androideabi-4.9\prebuilt\windows-x86_64\bin目录则提供了一些列不同CPU 架构的交叉编译工具,我们只要合理配置使用即可,如下图所示:
这里写图片描述
以上图形只做了两步工作,配置局部环境变量(非必须步骤),使用NDK中提供的工具进行编译,原本我也以为就是需要使用对应的工作进行编译就可以了,而却提示找不到标准库中的头文件,这是因为在C/C++源码中默认链接到的标准库的(相当于是javac 中会有个classpath的环境变量),而当我们使用NDK中的工具进行编译的时候,需要使用NDK提供的对应的头文件,只要在执行编译命令的时候添加上对应的参数即可。所以总结起来使用NDK进行交叉编译主要步骤有:

1、指定要链接的头文件和库文件

/**
*使用xx作为这一次编译的头文件与库文件的查找目录,配置了之后就会在编译的时候自动去查找XX目录下的usr/include的头文件和usr/lib目录下的库文件,如gcc --sysroot=目录1 main.c 编译时就会去 目录1/usr/include查找头文件和目录1/usr/lib找库文件(一般是.a和.so)
*/
--sysroot=XX   

/**
*指定头文件查找目录,配置之后会自动覆盖--sysroot中的头文件查找目录,查找XX/usr/include的头文件,如gcc --sysroot=目录1 -isysroot 目录2 main.c 编译时就不会去 目录1/usr/include下找头文件而是去 目录2/user/include下查找头文件和到目录1/usr/lib下查找库文件
*/
-isysroot XX   

/**
*指定头文件查找目录,配置之后会直接查找查找XX目录下的头文件,如gcc --sysroot=目录1 -isysroot 目录2  -isystem 目录3 main.c 编译时就不会去 目录1/usr/include下找头文件而是到 目录2/user/include和目录3下查找头文件和到目录1/usr/lib下查找库文件
*/
-isystem XX   

/**
*指定头文件查找目录,配置之后会直接查找查找XX目录下的头文件,如gcc --sysroot=目录1 -isysroot 目录2  -isystem 目录3 -I 目录4 main.c 编译时就不会去 目录1/usr/include下找头文件而是到 目录2/user/include和目录3及目录4下查找头文件和到目录1/usr/lib下查找库文件
*/
-I XX         
/*****其中优先级: -I > -isystem > -isysroot所谓优先级指的是同时配置了这些指令的话编译时会先到优先级大的目录下查找,找到了就不会去查找其他目录的头文件了,所以以上的注释都是在优先级高的找不到的时候再继续往低一级的查找,而且不会去递归去查找指定目录下的子目录********/

/**
*链接指定XX目录下的xx.so,gcc -L目录1 -l库名,比如连接到NDK的日志库和libEGL库可以使用gcc -LC:\AndroidIDE\SDK\ndk-bundle\platforms\android-26\arch-arm\usr\lib -llog.so -lEGL.so(当然也可以使用--sysroot来指定)
*/
-L:XX  -lxx.so  

只要在编译时配置上对应的头文件和库文件目录,即可编译出能成功在Android上可执行的文件,不过这里需要注意的是不同版本的NDK 编译的指令有所不同,我的是在NDK_r16b下编译的,一开始使用 -isystem /usr/local/ndk/android-ndk-r16b/sysroot/usr/include/arm-linux-androideabi/asm 配置时编译报asm/types.h: No such file or directory,原因就是因为 -isystem /usr/local/ndk/android-ndk-r16b/sysroot/usr/include/arm-linux-androideabi/asm这样配置的时候,就回去xxx/asm下去找,
而代码中是include asm/types.h 这样引入的就会去 xxx/asm/asm/types.h查找所以自然出错。简单来说:指定为xxxx, include asm/types.h 就是去xxxx/asm/types.h

这里写图片描述

2、打包成配置成静态库和动态库

函数库一般分为静态库(Linux中后缀名为”.a”)和动态库(Linux中后缀名为”.so”)两种。

  • 静态库是指编译链接时,把库文件的代码全部加入到可执行文件中,因此生成的文件比较大(打包成APK也较大),但在运行时也就不再需要库文件了,所以静态库节省时间,不需要再进行动态链接,需要调用的代码直接就在代码内部

  • 动态库则与之相反,在编译链接时并没有把库文件的代码加入到可执行文件中,而是在程序执行时由运行时链接文件加载库,gcc在编译时默认使用动态库,动态库能节省空间,如果一个动态库被两个程序调用,那么这个动态库只需要在内存中即可,而且Java在不经过封装的情况下只能直接使用动态库。

5.0及以下与6.0及以上的注意事项:若存在两个动态库libhello-jni.solibTest.solibhello-jni.so依赖于libTest.so (使用NDK下的ndk-depends可查看依赖关系),则:

扫描二维码关注公众号,回复: 5790399 查看本文章
//<=5.0:
	System.loadLibrary("Test");
	System.loadLibrary("hello-jni");
//>=6.0:
	System.loadLibrary("hello-jni");

比如在已经编译好的静态库clib.a(或者动态库clib.so)中有一个test方法,而现在需要在另一个源文件crazy.c中使用clib.a中的test2方法中使用clib.a中的test方法,最后再把crazy.c打包成一个静态库或者动态库crazy.a或者crazy.so,如果clib是被以静态库引用的话则最终的crazy.a或者crazy.so库都会存在clib.a的所有方法实现(因为编译时就将所有符号加载到输出库),反之则只存在crazy.c的方法实现而不存在clib中的test方法(因为使用动态库是通过在运行时动态加载动态库的方法)

三、CPU架构类型APP_ABI

不同架构的CPU的指令集有所不同,不同 Android 设备可能会使用不同的 CPU,因此支持不同的指令集而需要生成的不同的CPU架构(ndk r17 只支持:armeabi-v7a, arm64-v8a, x86, x86_64)

指令集
基于 ARMv7 的设备上的硬件 FPU 指令 APP_ABI := armeabi-v7a
ARMv8 AArch64 APP_ABI := arm64-v8a
IA-32 APP_ABI := x86
Intel64 APP_ABI := x86_64
MIPS32 APP_ABI := mips
MIPS64 (r6) APP_ABI := mips64
所有支持的指令集 APP_ABI := all

查看Android设备 CPU架构的命令
adb shell cat /proc/cpuinfo
adb shell getprop ro.product.cpu.abi

apk在安装的时候,如果手机是armeabi-v7a的,则会首先查看apk中是否存在armeabi-v7a目录,如果没有就会查找armeabi。所以必须保证cpu目录下so数量一致,如果目标是armeabi-v7a,但是拥有一个armeabi的,也可以把它放到armeabi-v7a目录下,但是反过来不行。

ABI(横 so)/CPU(竖 手机) armeabi armeabi-v7a arm64-v8a x86 x86_64
ARMV5 支持
ARMV7 支持 支持
ARMV8 支持 支持 支持
X86 支持
X86_64 支持 支持

四、配置Android Studio NDK 项目

如果一开始新建Module时没有配置支持NDK,也可以在中途自己配置支持NDK,因为Gradle脚本中各种节点名称是BaseExtension.class中对应的方法名称,通过Groovy语法编写。
在这里插入图片描述
另外如果不知道各节点可以配置哪些属性,可以通过Java的形式来确定,其实就是Java对象的成员变量:

        externalNativeBuild.ndkBuild(new Action<CoreExternalNativeCmakeOptions>() {
            @Override
            void execute(CoreExternalNativeCmakeOptions ndkBuildOptions) {
                ndkBuildOptions.abiFilters=...
                ndkBuildOptions.targets=...
            }
        })

需要注意的是同一节点所在的层次不一样效果也会有所不同,比如在defaultConfig节点中的
externalNativeBuild的作用是指导编译源文件,而与defaultConfig同级的externalNativeBuild的作用是配置Native 构建监本的路径。

1、通过ndkBuild使用Android.mk方式进行配置

import com.android.build.gradle.internal.dsl.CoreExternalNativeCmakeOptions
import com.android.build.gradle.internal.dsl.NdkBuildOptions

apply plugin: 'com.android.application'

android {
    compileSdkVersion 28
    defaultConfig {
        ...
        // 指导我们的 源文件 编译
        externalNativeBuild{
            ndkBuild{
                // armeabi-v7a
                abiFilters "armeabi-v7a"
            }
        }
// (注释部分的Java形式为说明原理Gradle里貌似不直接支持)       
// externalNativeBuild.ndkBuild(new Action<CoreExternalNativeCmakeOptions>() {
//            @Override
//            void execute(CoreExternalNativeCmakeOptions ndkBuildOptions) {
//                // 你希望编译你的 c/c++ 源文件 编译几种cpu(arm、x86 )
//                ndkBuildOptions.abiFilters.add("armeabi-v7a")
//                ndkBuildOptions.abiFilters.add("x86")
//            }
//        })
        //externalNativeBuild.cmake

        // 应该打包几种cpu,比如: 集成了第三方库 ,第三方库中提供了 arm的 提供了 x86的可以在此处 指导 只打包 arm,生成出来的apk 就只会包含 arm的
        ndk{
            abiFilters "armeabi-v7a"
        }
    }
    // externalNativeBuild.cmake
    //配置 native 的编译脚本路径
//    externalNativeBuild.ndkBuild(new Action<NdkBuildOptions>() {
//        @Override
//        void execute(NdkBuildOptions ndkBuildOptions) {
//            ndkBuildOptions.path = "src/main/cpp/Android.mk"
//    }
//    })

    externalNativeBuild{
        ndkBuild{
            path  "Android.mk"
        }
    }
    
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:28.0.0-rc01'
}

1.1、在Module目录下的Gradle脚本android里的defaultConfig节点下添加externalNativeBuild配置cpu的eabi架构

android {
    compileSdkVersion 26
    ...
	    defaultConfig {
	    	...
			externalNativeBuild{
			            ndkBuild{
			            //设置源文件只编译armeabi-v7a 架构的
			                abiFilters 'armeabi-v7a'
			            }
			        }
	        }
	        
			 ndkBuild{
            //设置只打包出包含armeabi-v7a 架构的apk
                abiFilters 'armeabi-v7a'
            }
		}
}

1.2、编写自己的Android.mk文件

Android.mk

#源文件在的位置。宏函数 my-dir 返回当前目录(包含 Android.mk 文件本身的目录)的路径。
LOCAL_PATH := $(call my-dir)

$(info "LOCAL_PATH:======== ${LOCAL_PATH}")

#引入其他makefile文件。CLEAR_VARS 变量指向特殊 GNU Makefile,可为您清除许多 LOCAL_XXX 变量
#不会清理 LOCAL_PATH 变量
include $(CLEAR_VARS)
#存储您要构建的模块的名称 每个模块名称必须唯一,且不含任何空格
#如果模块名称的开头已是 lib,则构建系统不会附加额外的前缀 lib;而是按原样采用模块名称,并添加 .so 扩展名。
LOCAL_MODULE := abc
#包含要构建到模块中的 C 和/或 C++ 源文件列表 以空格分开
LOCAL_SRC_FILES := native-lib.c \
c.c
#构建动态库
include $(BUILD_SHARED_LIBRARY)

1.3、在Module目录下的Gradle脚本android里的与defaultConfig节点同级的节点添加

android {
    compileSdkVersion 26
    ...
	    defaultConfig {
	    	...
			externalNativeBuild{
			            ndkBuild{
			                abiFilters 'armeabi-v7a'
			            }
			        }
	        }
		}
		...
		externalNativeBuild{
			            ndkBuild{
			           		//配置指向Android.mk文件的路径,如:path "Android.mk"代表与module下的build.gradle脚本同目录
			                path "xxxx/xxx/Android.mk"
			            }
			        }
	        }
}

2、通过CMakeList方式进行配置

1.1、在Module目录下的Gradle脚本android里的defaultConfig节点下添加externalNativeBuild配置cpu的eabi架构

android {
    compileSdkVersion 26
    ...
	    defaultConfig {
	    	...
			externalNativeBuild{
			            ndkBuild{
			                abiFilters 'armeabi-v7a'
			            }
			        }
	        }
		}
}

1.2、编写自己的CMakeList.txt文件


cmake_minimum_required(VERSION 3.4.1)

# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.

file(GLOB LIBGIF_SOURCE src/main/cpp/*.c src/main/cpp/*.cpp)
set(LIBNAME crazygif)

add_library( # Sets the name of the library.
             ${LIBNAME}

             # Sets the library as a shared library.
             SHARED

             # Provides a relative path to your source file(s).
             ${LIBGIF_SOURCE})

include_directories(src/main/cpp)

target_link_libraries( # Specifies the target library.
                       ${LIBNAME}
                       # Links the target library to the log library included in the NDK.

                       jnigraphics
                       log )

1.3、在Module目录下的Gradle脚本android里的与defaultConfig节点同级的节点添加externalNativeBuild 节点

    externalNativeBuild {
        cmake {
            path "CMakeLists.txt"
        }
    }

猜你喜欢

转载自blog.csdn.net/CrazyMo_/article/details/83379994