【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)

这个例子中,目的是利用 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)

那么,如何编写 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)

如果 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)

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"], 
}

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

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

  • testkey:普通 apk ,则默认使用该签名
  • platform:如果 apk 需要对系统中存在的文件夹进行访问等,或者需要完成一些系统的核心功能,则使用改签名,这种方式编译出来的 apk 所在进程的 uid 为 system
  • shared:如果 apk 需要和 home/contacts 进程共享数据,则使用该签名
  • media:如果 apk 是 media/download 系统中的一环,则使用该签名
  • 5
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一场雪ycx

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值