• 【Android】编译系统之 make 和 Soong


    make 简介

    make 是一个自动化构建工具,make 通过读取 Makefile 的文件,将源代码自动构建成可执行程序和库文件。

    而 Makefile 文件中定义了目标程序的依赖关系和生成目标程序的相关规则。

    在早期, make 被包含在 Unix 系统中,随着 GNU/LinuxUnix 衍生出来并发扬光大,GNU/Linux 保留并扩展了原始的 make ,加入了许多内置函数和自动变量等等,形成了GNU make

    make 早期主要用于构建C语言开发的项目,后来逐渐发展,广泛用于构建 C、C++、java 等各种语言开发的项目。

    Android 6.0 及以下版本的系统源码就是使用 Makefile(Android.mk) 来构建的,后来在 Android 8.0 及之后,谷歌极力推广 Android.bp,但实际上各个原厂的 Android9.0Android10.0 系统源码代码中仍有不少的 Android.mk 文件。

    Java 项目中常用的构建工具有 antmavengradle,它们都有自己的命令工具、构建规则、配置文件,例如,

    • ant 的命令工具为 ant,配置文件为 xml 文件
    • gradle 的命令工具为 gradlew,配置文件为 build.gradle

    同样的,make 作为自动化构建的祖师爷,也有着自己的命令工具、构建规则、配置文件。

    • make 的命令工具是 make,配置文件为 Makefile

    在 Makefile 文件中,描述了工程的构建规则,由命令工具 make 来解释其中的规则。make 在执行时,需要读取至少一个 Makefile 文件。

    Makefile 的构成

    一个完整的 Makefile 一般包含 4 个元素:

    • 指示符:指示符包括一系列的关键字、内置函数、自动化变量、环境变量,指示符指明在 make 程序读取 makefile 文件过程中所要执行的一个动作
    • 规则:它描述了在何种情况下如何更新一个或者多个 Makefile 文件
    • 变量:使用一个变量名代表一个变量值,当定义了一个变量以后,Makefile 后续在需要使用这个变量值内容的地方,可以通过引用这个变量名来实现
    • 注释:Makefile 中 “#” 字符后的内容被作为是注释内容

    例如,Makefile 中的关键字有如下几种:

    • define:用于定义变量
    • endef:定义变量的结束符,一般来说,define 和 endef 成对使用
    • ifdef:判断变量是否已定义
    • ifndef:判断变量是否未定义
    • ifeq:判断两个变量是否相等
    • ifneq:判断两个变量是否不相等
    • else:条件语句的分支处理
    • endif:条件语句的结束符
    • include:用于包含其他 Makefile 文件
    • sinclude:等价于 include(用于兼容非 GNU make)
    • override:用于重载变量
    • export:将一个变量和它的值加入到当前工作的环境变量中
    • unexport:与 export 作用相反

    Makefile 中的内置函数、环境变量、自动化变量也很多,篇幅有限,这里就不一一举例了。

    我们重点看下 Makefile 的在 Android 中的应用,在 Android 中的 Android.mk 就是一个 Makefile 文件。

    Android.mk 的作用

    Android.mk 是 Android 提供的一种 Makefile 文件,属于 GUN makefile 的一部分,会被编译系统解析一次或多次,因此我们应尽量少的在 Android.mk 中声明变量,也不要假定任何东西不会在解析过程中定义。

    Android.mk 文件主要是告诉编译系统,以什么样的规则编译我们的源代码,并生成对应的目标文件,目标文件可以分为以下几种:

    • apk 文件,一般的 Android 应用程序
    • jar 文件,java 类库
    • c\c++ 应用程序,可执行的 c\c++ 应用程序
    • c\c++ 静态库,打包成 .a 文件
    • c\c++ 动态库, 打包成 .so 文件

    注意:只有动态库可以被 install 或者 copy 到 apk,静态库则可以被链接入动态库。

    它是用来指定诸如编译生成 so 库名、引用的头文件目录、需要编译的 .c.cpp 文件和 .a 静态库文件等。

    Android.mk 文件语法允许我们将 source 打包成一个 modules,而这个 modules 可以是静态库或者动态库。我们可以在一个 Android.mk 中定义一个或多个 modules, 也可以将同一份 source 加进多个 modules

    Build System 帮我们处理了很多细节而不需要我们再关心,例如:我们不需要在 Android.mk 中列出头文件和外部依赖文件。

    Android.mk 案例分析

    首先看一个最简单的 Android.mk 的例子:

    # 每个 Android.mk 文件必须以定义 LOCAL_PATH 为开始,它用于在开发 tree 中查找源文件
    # 宏 my-dir 则由 Build System 提供,返回包含 Android.mk 的目录路径
    LOCAL_PATH := $(call my-dir)
    
    # CLEAR_VARS 变量由 Build System 提供,并指向一个指定的 GNU Makefile,
    # 它负责清理很多 LOCAL_xxx,例如:LOCAL_MODULE, LOCAL_SRC_FILES, LOCAL_STATIC_LIBRARIES 等等。但不清理LOCAL_PATH,
    # 这个清理动作是必须的,因为所有的编译控制文件由同一个 GNU Makefile 解析和执行,其变量是全局的,所以清理后才能避免相互影响
    include $(CLEAR_VARS)
    
    # LOCAL_MODULE 模块必须定义,以表示 Android.mk 中的每个模块,名字必须唯一且不包含空格
    # Build System 会自动添加适当的前缀和后缀
    # 例如,foo,要产生动态库,则生成 libfoo.so,但请注意:如果模块名被定为:libfoo,则生成 libfoo.so. 不再加前缀
    LOCAL_MODULE := hello
    
    # LOCAL_SRC_FILES 变量必须包含将要打包如模块的 C/C++ 源码,我们不必列出头文件,build System 会自动帮我们找出依赖文件
    # 缺省的 C++ 源码的扩展名为 .cpp,也可以通过 LOCAL_CPP_EXTENSION 修改。
    LOCAL_SRC_FILES := hello.c
    
    # BUILD_SHARED_LIBRARY 是 Build System 提供的一个变量,指向一个 GNU Makefile Script
    # 它负责收集自从上次调用 include $(CLEAR_VARS) 后的所有 LOCAL_XXX 信息,并决定编译为什么
    # BUILD_STATIC_LIBRARY:编译为静态库。
    # BUILD_SHARED_LIBRARY:编译为动态库
    # BUILD_EXECUTABLE:编译为 Native C 可执行程序
    # BUILD_PREBUILT:Android 8.1 以及之后的版本使用
    include $(BUILD_SHARED_LIBRARY)
    
    • 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

    这个例子中,目的是利用 Android.mk 生成 so 文件,每行代码的含义注释写得很清楚了,

    另外要注意的是:在 Android 8.1 中,已经将 PREBUILT_STATIC_LIBRARYPREBUILT_SHARED_LIBRARY 两个宏废弃,统一使用 BUILD_PREBUILT 预编译,并通过 LOCAL_MODULE_CLASS 来指定编译文件类型。如:

    LOCAL_MODULE_CLASS := STATIC_LIBRARIES
    # LOCAL_MODULE_CLASS := SHARED_LIBRARIES
    # LOCAL_MODULE_CLASS := APPS
    include $(BUILD_PREBUILT)
    
    • 1
    • 2
    • 3
    • 4

    那么,如何编写 Android.mk 编译一个 apk 呢?

    可以参照下面这个示例:

    LOCAL_PATH := $(call my-dir)
    include $(CLEAR_VARS)
    LOCAL_MODULE := Test
    LOCAL_MODULE_TAGS := optional
    LOCAL_SRC_FILES := $(LOCAL_MODULE).apk
    LOCAL_MODULE_CLASS := APPS
    LOCAL_MODULE_SUFFIX := $(COMMON_ANDROID_PACKAGE_SUFFIX)
    LOCAL_CERTIFICATE := PRESIGNED # LOCAL_CERTIFICATE := platform (使用平台签名)
    # 可选项,如果不添加此变量,则预装到 system/app 下,此 apk 将不能被卸载,
    # 添加后,被安装到 data/app 目录下,可卸载
    LOCAL_MODULE_PATH := $(TARGET_OUT_DATA_APPS)
    include $(BUILD_PREBUILT)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    如果 apk 还包含本地的 so 库,则应该这么写:

    LOCAL_PATH := $(call my-dir)
    include $(CLEAR_VARS)
    LOCAL_MODULE := Test
    LOCAL_MODULE_TAGS := optional
    LOCAL_SRC_FILES := $(LOCAL_MODULE).apk
    LOCAL_MODULE_CLASS := APPS
    LOCAL_MODULE_SUFFIX := $(COMMON_ANDROID_PACKAGE_SUFFIX)
    LOCAL_CERTIFICATE := PRESIGNED # LOCAL_CERTIFICATE := platform (使用平台签名)
    
    # 引入本地 so 库
    LOCAL_PREBUILT_JNI_LIBS := \
    	/lib/so1.so \
    	/lib/so2.so \
    	/lib/so3.so
    LOCAL_DEX_PREOPT := true
    
    # 可选项,如果不添加此变量,则预装到 system/app 下,此 apk 将不能被卸载,
    # 添加后,被安装到 data/app 目录下,可卸载
    LOCAL_MODULE_PATH := $(TARGET_OUT_DATA_APPS)
    include $(BUILD_PREBUILT)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    Soong 编译系统

    我们先看看 Android 系统编译构建的发展演变过程:

    • Android 7.0 以前使用 GNU MakeAndroid.mk
    • Android 7.0 引入 ninjakati
    • Android 8.0 使用 Android.bp 来替换 Android.mk,引入 Soong 编译系统
    • Android 9.0 强制使用 Android.bp

    以上涉及到的名词是什么意思呢?下面来简单介绍一下:

    • ninja:是一个编译框架,会根据相应的 ninja 格式的配置文件进行编译,ninja 文件一般不会手动修改,而是通过将 Android.bp 文件转换成 ninja 格文件来编译

    • Android.bp:它的出现是为了替换掉 Android.mk 文件,但它不支持条件语句,所以在实际项目中,如果构建的脚本必须包含条件语句,建议使用 Android.mk 或使用 Go 语言

    • Soong:它是为了 Android 系统编译而设计出来的工具,与 make 编译系统类似,可以认为它对标的是 make 编译系统,Soong 主要负责对 Android.bp 进行语义解析,并将其转换成 ninja 文件,Soong 还会编译生成一个 androidmk 命令,用于将 Android.mk 文件转换为 Android.bp 文件

    • Blueprint:它是生成、解析 Android.bp 的工具,是 Soong 的一部分,Blueprint 只是负责解析文件格式

    • Kati:专为 Android 开发的一个基于 GolangC++ 的工具,主要功能是把 Android.mk 文件转换成 ninja 文件

    Android.bp、Android.mk、ninja 之间转换关系图如下:

    在这里插入图片描述

    那么为什么要谷歌要逐渐遗弃 GNU Make 而使用 Soong 呢?

    原因是使用 GNU Make 编译,在 Android 层面慢慢变得缓慢、容易出错、无法扩展且难以测试,而 Soong 构建系统正好提供了 Android 系统构建所需的灵活性。

    Android.bp 简介

    Android.bp 的语法在设计上要比 Android.mk 简单,但是它不支持条件语句,所以在实际项目中,如果构建的脚本必须包含条件语句,建议使用 Android.mk 或使用 Go 语言。Android.bp 文件中的模块以 模块类型 开头,然后是一组键值对属性:name: value

    Android.bp 常见模块类型

    在 Android.bp 中我们会基于模块类型来构建我们所需要的东西,常用的有以下几种类型:

    android_app

    用于构建 apk,与 Android.mk 中的BUILD_PACKAGE作用相同。

    java_library

    用于将源码构建并链接到设备的 .jar 文件中。

    默认情况下,java_library 只有一个变量,它生成一个包含根据设备引导类路径编译的 .class 文件的 .jar 包。生成的 jar 不适合直接安装在设备上,通过会用作另一个模块的 static_libs 依赖项。

    如果指定 “installable:true” 将生成一个包含 classes.dex 文件的 .jar 文件,适合在设备上安装。指定 'host_supported:true' 将产生两个变量,一个根据 device 的 bootclasspath 编译,另一个根据 host 的 bootclasspath 编译。

    java_library_static

    作用等同于 java_library,但是 java_library_static 已过时,不推荐使用

    android_library

    将源码与 Android 资源文件一起构建并链接到设备的 .jar 文件中。

    android_library 有一个单独的变体,它生成一个包含根据 device 的 bootclasspath 编译的 .class 文件的 .jar 文件,以及一个包含使用 aapt2 编译的android资源的 .package-res.apk 文件。生成的 apk 文件不能直接安装在设备上,但可以用作 android_app 模块的 static_libs 依赖项。

    cc_library

    为 device 或 host 创建静态库或共享库。

    默认情况下,cc_library 具有针对设备的单一变体。指定 host_supported:true 还会创建一个以主机为目标的库。

    cc_library 相关的模块类型还有 cc_library_sharedcc_library_headerscc_library_static等。

    下面是一个简单示例:

    // 表示该模块用于构建一个 apk
    android_app {
    	// 模块都必须具有 name 属性,且值是唯一的
        name: "Test", 
        
        // 以字符串列表的形式指定用于构建模块的源文件
        srcs: [
            "src/**/*.java",
            "com/example/xxx/*.aidl",
        ],
        
        // 引入静态依赖库
        static_libs: [
            "androidx.cardview_cardview",
            "androidx.recyclerview_recyclerview",
            "TestLib",
        ],
        
        // 引入java库
        libs: ["android.car"],
        
        // 指定资源文件的位置
        resource_dirs: ["res"],
        
        // 设定 apk 安装路径为 priv-app
        privileged: true,
        
        // 是否启用代码优化,android_app 中默认为 true,java_library 中默认为 false
        optimize: {
            enabled: false,
        },
        
        // 是否预先生成 dex 文件,默认为 true。
        // 该属性会影响应用的首次启动速度以及 Android 系统的启动速度
        dex_preopt: {
            enabled: false,
        },
        
        // 设置该标记后会使用 sdk 的 hide 的 api 來编译,
        // 如果编译的 APK 中需要使用系统级 API,必须设定该值,
        // 和 Android.mk 中的 LOCAL_PRIVATE_PLATFORM_APIS 的作用相同
        platform_apis: true,
        
        // 表示生成的 apk 会被安装在系统的 product 分区,
        // 和 Android.mk 中 LOCAL_PRODUCT_MODULE 作用相同
        product_specific: true,
        
        // 用于指定 APK 的签名方式
        certificate: "platform",
    }
    
    // 表示该模块用于构建一个 Lib
    android_library {
        name: "TestLib",
        
        srcs: [
        	"xxx/**/*.java", 
        	"xxx/**/*.kt",
        ],
        
        resource_dirs: ["res"],
        
        // 用于指定 Manifest 文件
        manifest: "AndroidManifest-withoutActivity.xml",
        
        platform_apis: true,
        
        optimize: {
            enabled: false,
        },
        
        dex_preopt: {
            enabled: false,
        },
        
        static_libs: [
            "androidx.cardview_cardview",
            "androidx.recyclerview_recyclerview",
            "androidx.palette_palette",
            "car-assist-client-lib",
            "android.car.userlib",
            "androidx-constraintlayout_constraintlayout"
        ],
        
        libs: ["android.car"], 
    }
    
    • 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
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86

    在该例子中,在 Test 模块中,将 TestLib 模块作为静态依赖库引入了,如果执行 make Test 命令,将会生成 Test.apk 文件,

    需要注意的是,certificate 用于指定APK的签名方式,而Android 中共有四中签名方式:

    • testkey:普通 apk ,则默认使用该签名
    • platform:如果 apk 需要对系统中存在的文件夹进行访问等,或者需要完成一些系统的核心功能,则使用改签名,这种方式编译出来的 apk 所在进程的 uid 为 system
    • shared:如果 apk 需要和 home/contacts 进程共享数据,则使用该签名
    • media:如果 apk 是 media/download 系统中的一环,则使用该签名
  • 相关阅读:
    趣学python编程 (五、常用IDE环境推荐)
    关于Flask高级_上下文的介绍和基本操作
    【Python】-- 115道语法练习题
    百度地图开发入门(6):3D建筑
    SpringBoot项目在使用Maven打包war中遇到的问题
    面试官随便问几个问题就知道你究竟做没做过微信支付宝支付
    C# +SQL 存储过程 实现系统数据权限审查AOP效果
    你不知道的javascript【阅读】
    智能abc是什么输入法:win10可用的智能abc输入法免费下载
    [含lw+源码等]计算机毕业论文Java项目源码下载微信小程序记事本+后台管理系统[包运行成功]
  • 原文地址:https://blog.csdn.net/yang553566463/article/details/126603493