App 的编译和打包流程

1、APK 的组成

我们都知道,APK 其实是一个 zip 类型的压缩包,而一个典型的 APK 通常都会包含了以下七部分的内容:
我们都知道,APK 其实是一个 zip 类型的压缩包,而一个典型的 APK 通常都会包含了以下七部分的内容:

  • 1、AndroidManifest.xml:如果 App 是一本书,那么这个文件就是它的 “封面” 和 “目录” 。它记载了 App 的名称、权限声明、所包含的组件等一系列信息。

  • 2、classes.dex:它是由项目源码生成的 .class 文件经过进一步地转换而生成的 Android 系统可识别的 Dalvik Byte Code。并且,由于 Android 系统中的字节码和标准 JVM 中的字节码是有区别的,所以如果 App 中引用了第三方 jar 包的话,那么通常情况下它也会被包含在 classes.dex 中。

  • 3、resources.arsc:资源索引表,包含编译后的二进制资源文件。每当在 res 文件夹下放一个文件时,aapt 就会自动生成对应的 id 并保存在 .R 文件中,但 .R 文件仅仅只是保证编译程序不会报错,实际上在应用运行时,系统会根据 ID 寻找对应的资源路径,而 resources.arsc 文件就是用来记录这些 ID 和 资源文件位置对应关系 的文件。

  • 4、res 目录:未编译的资源文件。

  • 5、asserts:额外建立的资源文件夹。res 和 assets 的不同在于 res 目录下的文件会在 .R 文件中生成对应的资源 ID,而 assets 不会自动生成对应的 ID,而是通过 AssetManager 类的接口来获取。

  • 6、libs 目录:如果存在的话,存放的是 ndk 编出来的 so 库 。

  • 7、META-INF 目录:用于保存 App 的签名和校验信息,以保证程序的完整性。当生成 APK 包时,系统会对包中的所有内容做一次校验,然后将结果保存在这里。而手机在安装这一 App 时还会对内容再做一次校验,并和 META-INF 中的值进行比较,以避免 APK 被恶意篡改。其中包含如下 三个文件,如下所示:

    • 1)、MANIFEST.MF:其中每一个资源文件都有一个对应的 SHA-256-Digest(SHA1) 签名,MANIFEST.MF 文件的 SHA256(SHA1) 经过 base64 编码的结果即为 CERT.SF 中的 SHA256(SHA1)-Digest-Manifest 值。
    • 2)、CERT.SF:除了开头处定义的 SHA256(SHA1)-Digest-Manifest 值,后面几项的值是对 MANIFEST.MF 文件中的每项再次 SHA256(SHA1) 经过 base64 编码后的值。
    • 3)、CERT.RSA:其中包含了公钥、加密算法等信息。首先,对前一步生成的 CERT.SF 使用了 SHA256(SHA1)生成了数字摘要并使用了 RSA 加密,接着,利用了开发者私钥进行签名。然后,在安装时使用公钥解密。最后,将其与未加密的摘要信息(MANIFEST.MF文件)进行对比,如果相符,则表明内容没有被修改。

Gradle Task

> 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:javaPreCompileDebug 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 UP-TO-DATE
> Task :app:processDebugManifest UP-TO-DATE
> Task :app:processDebugManifestForPackage UP-TO-DATE
> Task :app:processDebugResources UP-TO-DATE
> Task :app:compileDebugJavaWithJavac UP-TO-DATE
> 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:mergeDebugJavaResource UP-TO-DATE
> Task :app:checkDebugDuplicateClasses UP-TO-DATE
> Task :app:desugarDebugFileDependencies UP-TO-DATE
> Task :app:mergeExtDexDebug UP-TO-DATE
> Task :app:mergeLibDexDebug UP-TO-DATE
> Task :app:dexBuilderDebug UP-TO-DATE
> Task :app:mergeProjectDexDebug UP-TO-DATE
> 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:packageDebug UP-TO-DATE

> Task :app:installDebug
//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

生成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(打包资源文件)

2、APK 的编译打包流程

APK 打包流程图

打包流程可简述为如下 八个步骤:

处理.aidl file

1、首先,.aidl(Android Interface Description Language)文件需要通过 aidl 工具转换成编译器能够处理的 Java 接口文件。

打包资源文件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文件、没有签名)。

2、同时,资源文件(包括 AndroidManifest.xml、布局文件、各种 xml 资源等等)将被 AAPT(Asset Packaging Tool)(Android Gradle Plugin 3.0.0 及之后使用 AAPT2 替代了 AAPT)处理为最终的 resources.arsc,并生成 R.java 文件以保证源码编写时可以方便地访问到这些资源。

Android Gradle插件 3.0.0 及更高版本默认情况下会启用 AAPT2,而老版本的AAPT已经被弃用,那么AAPT2到底优化改进了什么呢?

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

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

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

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


     


AndroidManifest.xml:15: error: unknown element  found.

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

    @color/pink
    // 修改为
    @color/pink

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

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

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

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

5)、库配置不正确

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

编译Compiler javac(编译java文件)

3、然后,通过 Java Compiler 编译 R.java、Java 接口文件、Java 源文件,最终它们会统一被编译成 .class 文件。

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

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

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

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

生成dex文件 dx/r8/d8 (编译class文件)

4、因为 .class 并不是 Android 系统所能识别的格式,所以还需要通过 dex 工具将它们转化为相应的 Dalvik 字节码(包含压缩常量池以及清除冗余信息等工作)。这个过程中还会加入应用所依赖的所有 “第三方库”

再谈谈这三个工具(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包)

5、这一步就是生成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

jarsigner/apksigner(签名)

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

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

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

7、然后,通过签名工具 Jarsigner 或者其它签名工具对 APK 进行签名得到签名后的 APK。如果是在 Debug 模式下,签名所用的 keystore 是系统自带的默认值,否则我们需要提供自己的私钥以完成签名过程。

  • 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 文件中。

ZipAlign对齐

zipalign 是一种归档对齐工具,可对 Android 应用 (APK) 文件提供重要的优化
它会使 APK 中的所有未压缩数据(例如图片或原始文件)在 4 字节边界上对齐。
这里涉及到一个Data structurealignment(数据对齐)的知识点,其大概意思就是如果数据是自然对齐的,CPU读写就会更高效。

签名工具的不同带来的对齐处理的顺序不同

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

8、最后,如果是正式版的 APK,还会利用 ZipAlign 工具进行对齐处理,以提高程序的加载和运行速度。而对齐的过程就是将 APK 文件中所有的资源文件距离文件的起始位置都偏移4字节的整数倍,这样通过 mmap 访问 APK 文件的速度会更快,并且会减少其在设备上运行时的内存占用。

为什么 XML 资源文件要从文本格式编译成二进制格式?

主要基于以下 两点原因:

  • 1、空间占用更小:因为所有 XML 元素的标签、属性名称、属性值和内容所涉及到的字符串都会被统一收集到一个字符串资源池中,并且会去重。有了这个字符串资源池,原来使用字符串的地方就会被替换成一个索引到字符串资源池的整数值,从而可以减少文件的大小。
  • 2、解析效率更高:二进制格式的 XML 文件解析速度更快。这是由于二进制格式的 XML 元素里面不再包含有字符串值,因此就避免了进行字符串解析,从而提高了解析效率。

Android 资源管理框架又是如何快速定位到最匹配资源的?

主要基于两个文件,如下所示:

  • 1、资源 ID 文件 R.java:赋予每一个非 assets 资源一个 ID 值,这些 ID 值以常量的形式定义在 R.java 文件中。
  • 2、资源索引表 resources.arsc:用来描述那些具有 ID 值的资源的配置信息。

3、签名算法的原理

什么是签名?

在 Apk 中写入一个 “指纹”。指纹写入以后,Apk 中有任何修改,都会导致这个指纹无效,Android 系统在安装 Apk 进行签名校验时就会不通过,从而保证了安全性。

那么,为什么要签名?
主要有 两点原因,如下所示:

  • 1、确保 Apk 来源的真实性。
  • 2、确保 Apk 没有被第三方篡改。

数字摘要

对一个任意长度的数据,通过一个 Hash 算法计算后,都可以得到一个固定长度的二进制数据,这个数据就称为 “摘要”。

在签名和校验的流程之中,应用了许多密码学的知识,这里我们需要先大致了解一下。

Hash(散列算法)的基础原理

Hash 算法就是 将数据(如一段文字)运算变为另一固定长度值。它的特点主要有如下 三点:

  • 1、唯一性。
  • 2、固定长度:比较常用的 Hash 算法有 MD5 和 SHA1,MD5 的长度是128位,SHA1 的长度是160位。
  • 3、不可逆性。
    而常用的 Hash 算法有如下 三种:
  • 1、SHA-1:在密码学中,SHA-1(安全散列算法1)是一种加密散列函数,它接受输入并产生一个160 位(20 字节)散列值,称为消息摘要。
  • 2、MD5:MD5 消息摘要算法(英语:MD5 Message-Digest Algorithm),一种被广泛使用的密码散列函数,可以产生出一个128位(16字节)的散列值(hash value),用于确保信息传输完整一致。
  • 3、SHA-2:名称来自于安全散列算法2(Secure Hash Algorithm 2)的缩写,一种密码散列函数算法标准,其下又可再分为六个不同的算法标准,包括了:SHA-224、SHA-256、SHA-384、SHA-512、SHA-512/224、SHA-512/256。

签名和校验的主要过程

签名就是 在摘要的基础上再进行一次加密,对摘要加密后的数据就可以当作数字签名。
签名过程:
签名过程可以细分为 三步,如下所示:

  • 1、计算摘要:通过 Hash 算法提取出原始数据的摘要。
  • 2、计算签名:再通过基于密钥(私钥)的非对称加密算法对提取出的摘要进行加密,加密后的数据就是签名信息。
  • 3、写入签名:将签名信息写入原始数据的签名区块内。
    校验过程:
    校验过程同样也可以分为 三步,如下:
  • 1、提取摘要:首先用同样的 Hash 算法从接收到的数据中提取出摘要。
  • 2、解密签名:使用发送方的公钥对数字签名进行解密,解密出原始摘要。
  • 3、比较摘要:如果解密后的数据和提取的摘要一致,则校验通过;如果数据被第三方篡改过,解密后的数据和摘要将会不一致,则校验不通过。
    那么,我们该如何保证公钥的可靠性呢?答案是 数字证书。

数字证书

数字证书是 身份认证机构(Certificate Authority)颁发的,主要包含了以下 六类信息:

  • 1、证书颁发机构
  • 2、证书颁发机构签名
  • 3、证书绑定的服务器域名
  • 4、证书版本、有效期
  • 5、签名使用的加密算法(非对称算法,如 RSA)
  • 6、公钥等

接收方收到消息后,需要先向 CA 验证证书的合法性,再进行签名校验。

需要注意的是,Apk 的证书通常是自签名的,也就是由开发者自己制作,没有向 CA 机构申请。Android 在安装 Apk 时并没有校验证书本身的合法性,只是从证书中提取公钥和加密算法,这也正是对第三方 Apk 重新签名后,还能够继续在没有安装这个 Apk 的系统中继续安装的原因

keystore 和证书格式

keystore 文件中包含了 私钥、公钥和数字证书。根据编码不同,keystore 文件分为很多种,Android 使用的是 Java 标准 keystore 格式 JKS(Java Key Storage),所以通过 Android Studio 导出的 keystore 文件是以 .jks 结尾的。

keystore 使用的 证书标准是 X.509,X.509 标准也有多种 编码格式,常用的有两种:pem(Privacy Enhanced Mail)和 der(Distinguished Encoding Rules)。jks 使用的是 der 格式,但是,Android 也支持直接使用 pem 格式的证书进行签名。

下面,我们了解下两种证书编码格式的区别,如下所示:

  • DER(Distinguished Encoding Rules):二进制格式,所有类型的证书和私钥都可以存储为 der 格式。
  • PEM(Privacy Enhanced Mail):base64 编码,内容以-----BEGIN xxx----- 开头,以-----END xxx----- 结尾。

jarsigner 和 apksigner 的区别

Android 提供了 两种对 Apk 的签名方式,一种是基于 JAR 的签名方式,另一种是基于 Apk 的签名方式,它们的 主要区别在于使用的签名文件不一样:jarsigner 使用 keystore 文件进行签名;而 apksigner 除了支持使用 keystore 文件进行签名外,还支持直接指定 pem 证书文件和私钥进行签名。

在我们签名时,除了要指定 keystore 文件和密码外,也要指定 alias 和 key 的密码,这是为什么呢?

keystore 是一个密钥库,也就是说它可以存储多对密钥和证书,keystore 的密码是用于保护 keystore 本身的,每一对密钥和证书是通过 alias 来区分的。所以 jarsigner 是支持使用多个证书对 Apk 进行签名的,apksigner 也同样支持。

Android Apk V1 验证签名的原理

Android Apk V1 验证签名的过程主要可以分为如下 四步:

  • 1、解析出 CERT.RSA 文件中的证书、公钥,解密 CERT.RSA 中的加密数据。
  • 2、解密结果和 CERT.SF 的指纹进行对比,保证 CERT.SF 没有被篡改。
  • 3、接着,将 CERT.SF 中的内容再和 MANIFEST.MF 中的指纹对比,保证 MANIFEST.MF 文件没有被篡改。
  • 4、MANIFEST.MF 中的内容和 APK 所有文件指纹逐一对比,保证 APK 没有被篡改。

附1、查看 Gradle 源码

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

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

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

参考

深入探索编译插桩技术(一、编译基础)
从构建工具看 Android APK 编译打包流程

你可能感兴趣的:(App 的编译和打包流程)