专栏首页码上积木从构建工具看 Android APK 编译打包流程

从构建工具看 Android APK 编译打包流程

前言

在Android Studio中,我们几乎每天都在用run,generate APK等功能。

那你有没有想过这其中的原理呢?比如编译打包的流程?AAPT是什么?这其中有哪些task会执行?

今天就和大家一起去探索下Android中编译打包的那些事儿。

粗谈构建流程

对于编译打包过程,Android官网上有一张图做了简单介绍:

图画的比较简单,标出了大体流程,可以发现整个构建流程大概分为两部分:编译(Compile),打包(Package)

  • 编译

编译过程就是将 java文件 编译成 class文件,最后优化成 dex文件

  • 打包

打包流程就是将DEX文件和编译后的资源组合成单个APK,并且签名,生成最终的APK。

在这些工作中会有很多工具来辅助完成,比如AAPT,aidl,javac,apk builder 等等。

那在Android Studio中,又是由谁来调度这些工具的呢?Gradle构建工具。

也就是说,在我们点击 generate APK 之后,Gradle就会执行一系列的约定好的 task,每个task有自己的构建工作,按照编译打包的顺序,分别调用具体的工具,最终组织起了整个构建流程。

Gradle Task

在Android Studio中,我们运行一个debug包,Build控制台上就可以看到所有的构建相关task:

Starting Gradle Daemon...
Gradle Daemon started in 902 ms
> Task :app:preBuild UP-TO-DATE
> Task :app:preDebugBuild UP-TO-DATE
> Task :app:compileDebugAidl NO-SOURCE
> Task :app:compileDebugRenderscript NO-SOURCE
> Task :app:generateDebugBuildConfig UP-TO-DATE
> Task :app:checkDebugAarMetadata UP-TO-DATE
> Task :app:generateDebugResValues UP-TO-DATE
> Task :app:generateDebugResources UP-TO-DATE
> Task :app:mergeDebugResources UP-TO-DATE
> Task :app:createDebugCompatibleScreenManifests UP-TO-DATE
> Task :app:extractDeepLinksDebug UP-TO-DATE
> Task :app:processDebugMainManifest
> Task :app:processDebugManifest
> Task :app:javaPreCompileDebug UP-TO-DATE
> Task :app:mergeDebugNativeDebugMetadata NO-SOURCE
> Task :app:mergeDebugShaders UP-TO-DATE
> Task :app:compileDebugShaders NO-SOURCE
> Task :app:generateDebugAssets UP-TO-DATE
> Task :app:mergeDebugAssets UP-TO-DATE
> Task :app:compressDebugAssets UP-TO-DATE
> Task :app:processDebugJavaRes NO-SOURCE
> Task :app:mergeDebugJniLibFolders UP-TO-DATE
> Task :app:mergeDebugNativeLibs UP-TO-DATE
> Task :app:stripDebugDebugSymbols NO-SOURCE
> Task :app:validateSigningDebug UP-TO-DATE
> Task :app:mergeLibDexDebug
> Task :app:processDebugManifestForPackage
> Task :app:processDebugResources
> Task :app:compileDebugKotlin UP-TO-DATE
> Task :app:compileDebugJavaWithJavac UP-TO-DATE
> Task :app:compileDebugSources UP-TO-DATE
> Task :app:mergeDebugJavaResource UP-TO-DATE
> Task :app:dexBuilderDebug
> Task :app:mergeExtDexDebug
> Task :app:mergeProjectDexDebug
> Task :app:packageDebug
> Task :app:assembleDebug

BUILD SUCCESSFUL in 22s
27 actionable tasks: 10 executed, 17 up-to-date

我精简了下,留下了比较重要的task:

//aidl 转换aidl文件为java文件
> Task :app:compileDebugAidl

//生成BuildConfig文件
> Task :app:generateDebugBuildConfig

//获取gradle中配置的资源文件
> Task :app:generateDebugResValues

// merge资源文件
> Task :app:mergeDebugResources

// merge assets文件
> Task :app:mergeDebugAssets
> Task :app:compressDebugAssets

// merge所有的manifest文件
> Task :app:processDebugManifest

//AAPT 生成R文件
> Task :app:processDebugResources

//编译kotlin文件
> Task :app:compileDebugKotlin

//javac 编译java文件
> Task :app:compileDebugJavaWithJavac

//转换class文件为dex文件
> Task :app:dexBuilderDebug

//打包成apk并签名
> Task :app:packageDebug

这里涉及到的代码很多,今天就不详细说了,task主要涉及到的工具和功能我已经标注上了,下面就具体说说编译打包流程

(感兴趣的朋友可以自己看看源码,查看gradle源码的方法我也放到文末的附目录了)

aidl(编译aidl文件)

对于AIDL,大家应该都很熟悉,它是一种用于进程间通信的接口文件。

其实它是Google为了帮助我们进行进程间通信的简便写法,最后还是需要被解析编译为java文件,而做这个工作的就是aidl工具,存在于sdk/build-tools目录。

主要的工作就是将项目中的aidl文件编译为java文件

生成BuildConfig文件,资源文件

在引入Gradle编译工具之后,Apk的打包流程就多了这么一步,生成BuildConfig文件和资源文件

也就是会根据build.gradle里面配置的内容生成相应的java代码或者res代码。简单举个例子:

 //build.gradle
    buildTypes {
        debug{
            buildConfigField("boolean", "ISDEBUG", "true")
            resValue "string", "TestName", "love1"
        }
        release {
            buildConfigField("boolean", "ISDEBUG", "false")
            resValue "string", "TestName", "love2"
        }
    }

    //BuildConfig.java
    public final class BuildConfig {
    // Field from build type: debug
    public static final boolean ISDEBUG = true;
 }

 R.string.TestName

merge 资源文件

这一步就是合并res资源文件,assets文件,manifest文件。

因为在项目中会依赖不同的库、组件,也会有多渠道的需求,所以merge这一步操作就是将不同地方的资源文件进行整合。

多个manifest文件需要整理成一个完整的文件,所以如果有属性冲突这一步就会报错。资源文件也会整理分类到不同的分辨率目录中。

AAPT/AAPT2(打包资源文件)

然后就是打包资源文件,涉及到的工具是AAPT。

AAPT,全称Android Asset Packaging Tool,所以这个构建工具就是用来打包资源文件的。

资源文件包括:图片,res目录下的xml文件,AndroidManifest.xml文件;

处理资源文件主要包括两步:

  • 1、编译:将资源文件编译为二进制格式。

把所有的Android资源文件进行解析,生成扩展名为.flat的二进制文件。比如是png图片,那么就会被压缩处理,采用.png.flat的扩展名。

  • 2、链接:合并所有已编译的文件并打包到一个软件包中。

首先,这一步会生成辅助文件,比如R.java(R文件),R文件大家应该都比较熟悉,就是一个资源索引文件,我们平时引用也都是通过R.的方式引用资源id。

最后,会将R文件和之前的二进制文件进行打包,打包到一个APK压缩包(没有dex文件、没有签名)。

再扩展一个问题,关于AAPT2。(之前有朋友面试遇到问这个的,真是问的比较细啊?,所以这里就提一嘴) Android Gradle插件 3.0.0 及更高版本默认情况下会启用 AAPT2,而老版本的AAPT已经被弃用,那么AAPT2到底优化改进了什么呢?

  • 1、链接过程优化

在AAPT中是没有链接功能的,会将所有的资源进行编译生成压缩包。这样处理方式有个缺点就是每次编译都要全量编译。

所以在AAPT2中用到链接的功能,当修改了某个资源文件之后,只需要重新编译这个改变的文件,然后与其他资源进行链接即可,支持了增量更新,大大提升了效率。

  • 2、行为变化

对一些行为进行了优化,一些错误的元素以前不会报错,只会警告或者忽略,现在会直接报错,保证程序正确运行。比如

1)、在以前的AAPT版本,Android 清单文件中出现错误的节点元素只会被忽略或警告,而AAPT2开始会对这些节点进行报错,比如:

<activity android:name=".MainActivity">
     <action android:name="android.intent.action.TEST" />
</activity>

AndroidManifest.xml:15: error: unknown element <action> found.

2)、在AAPT2中,无法通过name属性指明资源类型了,需要单独使用type属性:

    <item name="attr/my_attr">@color/pink</item>
    // 修改为
    <item type="attr" name="my_attr">@color/pink</item>

3)、ForegroundLinearLayout(前景色相关)属性限制严格

foregroundInsidePadding属性,不属于android命名空间,所以AAPT2的改进就是对于这个属性使用更加严格了,原来使用android:foregroundInsidePadding的时候会被忽略,现在会报错,需要改为foregroundInsidePadding。

4)、@ 资源引用符号使用严格

对于遗漏或者错误引用@(资源引用符号)时候,AAPT2会报错。

5)、库配置不正确

当某些库创建过程中R文件字段声明为final会导致报错,AAPT2就会对这种情况进行优化。

javac(编译java文件)

接下来就是编译java文件了,用到的工具就是大家熟知的javac,通过它将java文件编译成.class文件。

注解代码也是在这个阶段生成的。当注解的生命周期被设置为CLASS的时候,就代表该注解会在编译class文件的时候生效,并且存在与java源文件和Class字节码文件。

javac的基础命令还是可以了解下:

javac -d destdir(class文件存放目录) srcFile(java文件)

dx/r8/d8 (编译class文件)

这一步就是转换.class文件为dex文件。

有人就奇怪了,.class文件不就是JVM可以识别的二进制文件吗,为什么还要进行一次转化呢?

这就涉及到另一个问题:JVM 和 Dalvik(ART)的区别。

其中一个重要的区别就是Dalvik(ART)有自己的二进制文件,也就是.dex文件,所以需要将class文件进行再一次转换。

你可以把dex文件理解为一个class文件包,里面装着很多的class文件,让这些类能够共享数据,类似这种关系:

再谈谈这三个工具(dx/r8/d8)的区别:

  • dx是最早的转换工具,用于转换class文件为dex文件。
  • Android Studio 3.1之后,引入了D8编译器和 R8 工具。

注意这里的措辞:D8 编译器和 R8 工具。

所以D8就是用来代替dx用来进行转换class文件的,它的优势在于:编译更快、更小的dex文件、更好的性能。

R8工具是用来替代ProGuard的,用于代码的压缩和混淆。

编译class文件过程也常用于编译插桩,比如ASM,通过直接操作字节码文件完成代码修改或生成。

apkbuilder/zipflinger(生成APK包)

这一步就是生成APK文件,将manifest文件、resources文件、dex文件、assets文件等等打包成一个压缩包,也就是apk文件。

在老版本使用的工具是apkbuilder,但是在最新的版本我发现没有这个工具了,sdk目录下也找不到了。

所以我想到从打包的task——packageDebug中找找答案,果然,让我找到了新的打包工具——zipflinger

//PackageAndroidArtifact.java (packageDebug相关代码)
for (File arch : archives) {
    mApkCreator.writeZip(arch, pathNameMap::get, name -> !names.contains(name));
}

mApkCreator =new ApkFlinger(mCreationData, compressionLevel, !mIsDebuggableBuild);

/** An implementation of [ApkCreator] using the zipflinger library */
class ApkFlinger

同时在Android Studio的更新日志中也找到了对应的说明:

Android 构建团队不断进行更改以提高生成性能,在此版本中(Android Studio 3.6),我们将默认打包工具更改为 zipflinger 以进行调试生成。

zipalign(对齐处理)

zipalign 是一种归档对齐工具,可对 Android 应用 (APK) 文件提供重要的优化

具体来说,它会使 APK 中的所有未压缩数据(例如图片或原始文件)在 4 字节边界上对齐。

这里涉及到一个Data structurealignment(数据对齐)的知识点,其大概意思就是如果数据是自然对齐的,CPU读写就会更高效。

有的朋友可能会疑惑,这个对齐处理不是应该放在签名之后吗?其实这里就涉及到了签名工具的不同带来的对齐处理的顺序不同:

  • 如果使用的是 apksigner,只能在为 APK 文件签名之前执行 zipalign。
  • 如果使用的是 jarsigner,只能在为 APK 文件签名之后执行 zipalign。

下面具体聊聊两种签名工具。

jarsigner/apksigner(签名)

在生成APK文件之后,必须对该apk文件进行签名,否则无法被安装。

之前大家比较熟知的签名工具是JDK提供的jarsigner,而apksigner是Google专门为Android提供的签名和签证工具。

其区别就在于jarsigner只能进行v1签名,而apksigner可以进行v2、v3、v4签名。

什么?还有v4?我开始看到的时候也是大吃一惊,没想到都有v4签名了,那就顺带介绍下这几个签名机制吧:

  • v1签名

v1签名方式主要是利用META-INFO文件夹中的三个文件。

首先,将apk中除了META-INFO文件夹中的所有文件进行进行摘要写到 META-INFO/MANIFEST.MF;然后计算MANIFEST.MF文件的摘要写到CERT.SF;最后计算CERT.SF的摘要,使用私钥计算签名,将签名和开发者证书写到CERT.RSA。

所以META-INFO文件夹中这三个文件就能保证apk不会被修改。

但是缺点也很明显,META-INFO文件夹不会被签名,所以美团针对这种签名方式设计了一种多渠道打包方案:

利用pythone在META-INFO文件夹中创建一个文件,其名称就是渠道名,然后用java去读取文件名获取渠道。

  • v2签名

Android7.0之后,推出了v2签名,为了解决v1签名速度慢以及签名不完整的问题。

apk本质上是一个压缩包,而压缩包文件格式一般分为三块:

文件数据区,中央目录结果,中央目录结束节。

而v2要做的就是,在文件中插入一个APK签名分块,位于中央目录部分之前,如下图:

这样处理之后,文件就完成无法修改了。

  • v3签名

Android 9 推出了v3签名方案,和v2签名方式基本相同,不同的是在v3签名分块中添加了有关受支持的sdk版本和新旧签名信息,可以用作签名替换升级。

  • v4签名

Android 11 推出了v4签名方案。

v4 签名基于根据 APK 的所有字节计算得出的 Merkle 哈希树。它完全遵循 fs-verity 哈希树的结构,将签名存储在单独的.apk.idsig 文件中。

小结图

附1、查看 Gradle 源码

这里提供一种Gradle源码的查看方式,就是导入Gradle库,然后在External Libraries中查看:

implementation 'com.android.tools.build:gradle:4.1.1'

先以依赖的方式导入gradle库,然后编译,就能在左侧External Libraries栏中看到源码了:

参考

https://developer.android.google.cn/studio/build/index.html https://cloud.tencent.com/developer/article/1392511 https://www.jianshu.com/p/e73510605c56 https://blog.csdn.net/qq_43278826/article/details/86543932

感谢大家的阅读,有一起学习的小伙伴可以关注下公众号—码上积木❤️ 每日一个知识点,建立完整体系架构。

本文分享自微信公众号 - 码上积木(Lzjimu),作者:积木zz

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2021-03-31

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Apk 反编译前期了解

    LZ-Says:学习之路,似乎枯燥乏味,唯有耐着性子,独自前行,当光明笼罩的那一刻,一切,也仿佛明亮了许多。

    HLQ_Struggle
  • Android 项目构建流程

    当我们打开一个项目,我们可以看到的是我们写的Java Code文件or Other JVM Code,资源文件,Build配置文件,但是通过run the pr...

    李林LiLin
  • Android 项目编译过程

    Android 工程构建的持续集成,需要搭建一套编译和打包自动化流程,比如建立每日构建系统、自动生成发布文件等等。这些都需要我们对Android工程的编译和打包...

    阳光岛主
  • Android&Kotlin编译速度原理剖析

    由于内容比较多,所以拆分了两部分来讲解。欢迎点赞和关注给作者一些动力感谢感谢。如果有任何的想法和创意都可以直接和我联系讨论。整体内容主要分为六部分来介绍:

    Rouse
  • Android逆向 | 基础知识篇 - 03 - 关于 Android 体系结构的学习

    简单来说,apk就是一个带有签名的zip格式的压缩包,签名为了保护开发者的权益和标识apk。做为android逆向学习的第一步,了解apk的文件结构和生成过程是...

    咸鱼学Python
  • Android自动打包工具aapt详解

    概念 在Android.mk中有LOCAL_AAPT_FLAGS配置项,在gradle中也有aaptOptions,那么aapt到底是干什么的呢? aap...

    xiangzhihong
  • Gradle for Android简要指南

    前言 Android官网构建系统介绍https://developer.android.com/studio/build/index.html

    Anymarvel
  • Unity编译Android的原理解析和apk打包分析

    最近由于想在Scene的脚本组件中,调用Android的Activity的相关接口,就需要弄明白Scene和Activity的实际对应关系,并对Unity调用A...

    张坤
  • Android插件化基础3----Android的编译打包流程详解

    .apk文件其实就是一个压缩包,把文件的后缀改成.zip,用压缩软件解压搜就可的下图(我是mac)

    隔壁老李头
  • android gradle如何修改生成的apk名字

    说实话,我在大法工作的时候,就见过Gradle。但是当时我一直不知道这是什么东西。而且大法工具组的工程师还将其和Android Studio大法版一起推送,偶一...

    砸漏
  • 写给小白的android基础面试笔试题(一)

    序言:由于公司的某些方面原因,LZ最近都处于找工作的状态,年关了,最关键的LZ还只是个没毕业的孩子,所以工作越来越不好找了,到哪里投简历都是动不动就好几年经验,...

    用户2802329
  • 1.安卓逆向学习入门记录

    Android 安全与逆向分析, 自从2017年网络安全法的实施,网络安全受到越来越多的企业和行业的重视,Android 安全的重要性已无需多言,只要有智能机的...

    WeiyiGeek
  • 「万物生长」一个APK从诞生到活跃在Android手机上

    上述之前在其他文章里面也常见的图,而这张图讲述一个APK的诞生流程,可以分为以下的几个流程

    ClericYi
  • Android Studio中的Gradle依赖深入讲解

    Android studio依赖项目是使用gradle管理的,依赖一个项目、一个jar包、一个工程,都可以在这里进行配置,本文将给大家详细介绍关于Android...

    砸漏
  • Android 逆向--BUFF | Smali 介绍与学习

    静态分析反编译代码往往是逆向分析的第一步,在对代码逻辑大致了解后,我们就可以使用动态分析的方法验证猜想、加速分析和实现破解。

    Python编程与实战
  • Gradle从入门到了解 - 简书

    Gradle(英[g'reɪdl])是一个任务驱动型的构建工具,是一个依赖管理工具,更是一个编程框架。 它抛弃了基于XML的各种繁琐配置,取而代...

    用户2802329
  • 有关Android插件化的一些总结思考

    最近几年移动开发业界兴起了「 插件化技术 」的旋风,各个大厂都推出了自己的插件化框架,各种开源框架都评价自身功能优越性,令人目不暇接。随着公司业务快速发展,项目...

    Android技术干货分享
  • 如何读取APK的Activity(Python实现)

    adb是我们日常工作每天都会接触的,也是最简单有效。通过adb命令我们可以查看当前手机内应用的包名和Activity信息。常用的命令例如:

    用户5521279
  • 详解Android v1、v2、v3签名(小结)

    了解 HTTPS 通信的同学都知道,在消息通信时,必须至少解决两个问题:一是确保消息来源的真实性,二是确保消息不会被第三方篡改。

    砸漏

扫码关注云+社区

领取腾讯云代金券