深入分析Android构建过程

资源合并

如果项目引入了android support包,又或许依赖于其它第三方aar库,那构建前会将aar解压并与本地资源合并,这里的资源主要包括assets目录,res目录及Androidmanifest.xml。

当第三方依赖中的assets或res文件与本地文件有冲突时,会优先选用本地文件。但res/values略有不同,此目录下的strings.xml、color.xml、styles.xml等文件会被整合到一个叫values.xml的文件中去,后与各第三方依赖中的values.xml进行内容上的合并,不会像res其它子目录文件一样直接舍弃第三方冲突文件。

Androidmanifest.xml的合并相比来说则要复杂一些,除了第三方依赖中的manifest,项目还可以在不同目录下分别拥有manifest文件。构建过程中,会根据manifest中元素、属性及赋值来生成一个manifest文件,并应用于后续的打包过程。gradle为不同的manifest赋予了不同的优先级,其顺序如下:

buildType 设置 > productFlavor 设置 > src/main > dependency&library
XML元素及属性的冲突会根据以下规则进行解决:

当然也会有一些例外的:

uses-feature android:required与uses-library android:required默认为true,根据or规则合并;

如未指定uses-sdk,minSdkVersion跟targetSdkVersion将被设置为1。而冲突时会使用高优化级的设置;

若library的minSdkVersion高于src/main的设置,则会引发error,但可通过overrideLibrary解决。若未指定targetSdkVersion,则其值与minSdkVersion一致;

若library的targetSdkVersion低于src/main的设置,需要添加一些额外的权限保证library能正常运行;

manifest元素只与子manifest元素合并;

intent-filter元素在合并中不会被改变,只会被添加到其父节点中去;

冲突发生时,可通过合并冲突标记进行解决,需要引入android tools命名空间,详情请参阅官方文档。另外,manifest在对文件进行合并后,还会根据build.gradle的设置覆盖相关属性。

AAPT打包

资源合并后,即进入到编译阶段,先会把项目资源中的xml编译成二进制并生成R.java及资源索引表resources.arsc,其流程如下:

由图可见,assets是不需要做任何处理的,res/raw只需分配id后与assets一起直接打包到应用程序中;基于下述原因,其它xml文件则会被编译成二进制。

编译过程中,会把xml中的字符串进行收集去重,形成字符串资源池,元素中用到字符串的地方将被替换成相应的索引。另外,标签属性/値都会转换为资源id,进一步减少文件大小;

二进制格式的xml把标签属性/値转换为资源id后,避免了字符串解析,从而提高了解析速度;

经过AAPT(Android Asset Packaging Tool)处理后,会输出2个文件:一个R.java,为项目各资源分配了不同的id,将和java源码一起参与到后续的编译过程,id为4字节无符号整数,最高字节表示package id,次高字节表示type id,后2字节表示资源在当前类型中出现的序号,如R.string.appname=0x7f07006b中的0x7f代表当前正在编译的资源包,0x07代表string类型,0x006b代表app_name在string类型中出现的序号;另一个为app.ap,实际上为一个压缩包,包含了assets、res、Androidmanifest.xml与resources.arsc

资源索引表resources.arsc记录了从资源id到文件路径的转换关系,当应用通过Resources类获取res文件资源时,会先从resources.arsc中拿到文件路径,然后通过AssetManager进行访问。

Application Component -> resources.arsc -> AssetManager -> apk
从上述流程中可以看到,若要进行资源的混淆,可在分析resources.arsc格式后,修改内容中文件路径的指向并对资源文件进行相应的重命名即可。

另外,AAPT还可对png图进行优化、指定文件以stored还是deflated模式添加到压缩包中等操作。

源码编译

当项目中包含aidl时,会先调用aidl工具生成java代码;renderscript亦然,需要先调用llvm-rs-cc,只是它不仅会自动生成java文件,还会产生相应的.bc文件,.bc文件将打包到apk中

至此,java代码都已准备完毕。下一步要进行的是通过javac命令将java源码编译成.class字节码,用以编译的classpath包含以下内容:

android.jar,具体版本由targetSdkVersion指定;

build.gradle中添加的第三方依赖;

编译后可对代码进行混淆处理,主要包括删除无用类、字节码优化、重命名等操作,只需在build.gradle中配置混淆规则即可

buildTypes {
  release {
    minifyEnabled true
    proguardFiles getDefaultProguardFile('proguard-android.txt')
    proguardFile 'proguard/proguard-rules.pro'
  }
}

生成dex

如果项目涉及分dex,那在调用dx命令前,需要做一些准备的工作,把编译后的class文件打包成jar包allclasses.jar,然后生成主dex中必须包含的文件列表。主要包括collect、shrink及create 3个步骤。

首先会通过Androidmanifest.xml过滤出项目中使用到的四大组件(Activity、Service、receiver、provider)、Application及Instrumentation,并写入manifest_keep.txt文件,这些都是会默认添加到主dex的,无须手动设置。除此之外,默认添加的还有继承于 BackupAgent 及 Annotation 的类。若有额外的类需要被加入到主dex中,可以新建一个文件并以proguard的语法指定,然后在build.gradle中把此文件配置到multiDexKeepProguard中去。此过程关键代码如下:

void generateKeepListFromManifest() {
  SAXParser parser = SAXParserFactory.newInstance().newSAXParser()
  Writer out = new BufferedWriter(new FileWriter(getOutputFile()))  try {
    parser.parse(getManifest(), new ManifestHandler(out))    // add a couple of rules that cannot be easily parsed from the manifest.
    out.write("""-keep public class * extends android.app.backup.BackupAgent {<init>();}
-keep public class * extends java.lang.annotation.Annotation {*;}
""")    if (proguardFile != null) {
      out.write(Files.toString(proguardFile, Charsets.UTF_8))
    }
  } finally {
    out.close()
  }
}

这个时候,会执行一个叫shrinkXxxMultiDexComponents(Xxx为build types名称)的任务。实际上是调用了proguard,只是要比常规的proguard简单一些,不执行混淆、优化跟预检几个步骤,只需要shrink即可,以allclasses.jar为输入、manifest_keep.txt为混淆配置文件,把指定内容及其引用标记起来,然后添加到componentClasses.jar中去。

public void execute(ProGuardTask proguardComponentsTask) {
  proguardComponentsTask.dontobfuscate();
  proguardComponentsTask.dontoptimize();
  proguardComponentsTask.dontpreverify();  // 方法未完,略过...}

到了CreateMainDexList,会调用dx命令,传入allclasses.jar、componentClasses.jar,分析后者依赖,把它直接引用的类也添加到主dex中,并生成新的multidex配置文件maindexlist.txt,至此,准备工作完成。

经过上一阶段编译的处理,已经生成了标准的java字节码,可在标准的java虚拟机上运行。但android使用了它特有的dalvik虚拟机,这就需要我们为它提供另一不同的格式。dx工具为此而出现,可将.classes文件转换添加到dalvik可执行文件.dex中去。当项目发展到一定规模,需要进行分dex处理时,可通过上述步骤生成的maindexlist.txt指定dex该如何拆分。

遗憾的是,以上关于分dex的内容都是理想的情况,现实却很残酷。如果项目中开启了proguard,那它会在分dex的shrink处理前完成,导致allclasses.jar是混淆处理后的代码,而manifest_keep.txt却未曾混淆,后续生成componentClasses.jar 及 maindexlist.txt 的过程也就都不再可靠了。要解决这个问题,在shrink前通过混淆输出的符号表mapping.txt对manifest_keep.txt进行修正是个不错的选择。

打包签名

此时万事俱备,只要把资源包app.ap_、可执行文件classes.dex及项目(包含第三方依赖)中的非源码文件一起添加到压缩包中去,我们的安装包(.apk文件)也就生成了。

另外,apk需要经过签名才可以发布。可通过jarsigner工具完成。

zipalign

文件对齐并非android构建的必要步骤,但对齐处理后可提高系统访问安装包资源的效率。即使执行了zipalign,也只有以stored模式添加到apk中的文件是需要对齐的。如若对图片等资源进行了极限压缩或在aapt打包时选择了deflated,那可对齐的文件也就没多少了

通过build tools中的zipalign工具以下命令可对压缩包进行对齐

zipalign -f -v 4 app.apk toapp.apk
以下命令则起到了检验压缩包有没有对齐的作用:

zipalign -c -v 4 app.apk
总结

本文主要介绍了android构建的各个主要步骤,并重点讲述了资源合并打包与dex生成的过程。最后,用一张图概括下构建的总体流程:

(0)

相关推荐

  • 深入分析Android构建过程

    资源合并 如果项目引入了android support包,又或许依赖于其它第三方aar库,那构建前会将aar解压并与本地资源合并,这里的资源主要包括assets目录,res目录及Androidmanifest.xml. 当第三方依赖中的assets或res文件与本地文件有冲突时,会优先选用本地文件.但res/values略有不同,此目录下的strings.xml.color.xml.styles.xml等文件会被整合到一个叫values.xml的文件中去,后与各第三方依赖中的values.xml

  • Android App开发中Gradle构建过程的配置方法

    在build文件中使用了Android或者Java插件之后就会自动创建一系列可以运行的任务. Gradle中有如下一下默认约定的任务: 1. assemble 该任务包含了项目中的所有打包相关的任务,比如java项目中打的jar包,Android项目中打的apk 2. check 该任务包含了项目中所有验证相关的任务,比如运行测试的任务 3. build 该任务包含了assemble和check 4. clean 该任务会清空项目的所有的输出,删除所有在assemble任务中打的包 assemb

  • jenkins 远程构建Android的过程详解

    由于企业的需求,需要做一个网站开分享每个版本的Android的app,所以需要使用的工具如下: Jenkins平台,远程编译环境服务器一台,web服务器一台,根据自己的选择,可以搭配自己的资源,废话少说,直奔主题 1. Jenkins的操作 在Jenkins中添加一个节点,设置好远程的工作目录,创建好服务器的标签,然后创建好相关的环境键值对,比如Android_home,Java_home,Gradle_home等,这些都是比较平常的操作,这里就不罗嗦了,值得注意的有两点:第一,java的路径问

  • 详解Gradle构建过程

    Gradle构建过程 根据在上图中所示,Gradle 的构建过程主要分为三个阶段: 初始化阶段 配置阶段 执行阶段 监听Gradle初始化时机 在这个初始化阶段中主要有两个时机需要关注: setting.gradle 执行结束的监听 //1.setting.gradle 执行结束的监听 gradle.settingsEvaluated { println "settings.gradle 初始化执行结束" } 参与构建的Project对象创建完毕的监听 //2.参与构建的Project

  • Android 定时任务过程详解

    在Android开发中,通过以下三种方法定时执行任务: 一.采用Handler与线程的sleep(long)方法(不建议使用,java的实现方式) 二.采用Handler的postDelayed(Runnable, long)方法(最简单的android实现) 三.采用Handler与timer及TimerTask结合的方法(比较多的任务时建议使用) android里有时需要定时循环执行某段代码,或者需要在某个时间点执行某段代码,这个需求大家第一时间会想到Timer对象,没错,不过我们还有更好的

  • vue和webpack项目构建过程常用的npm命令详解

    vue //最新稳定版 cnpm install vue //全局安装 vue-cli cnpm install --global vue-cli //创建一个基于 webpack 模板的新项目 vue init webpack my-project //进入项目目录,运行 cd my-project cnpm install cnpm run dev  webpack //全局安装webpack cnpm install -g webpack //安装到你的项目目录 cnpm install

  • 如何理解Java中基类子对象的构建过程从"基类向外"进行扩散的?

    <Java编程思想>复用类一章,提出基类的子对象的构建过程是从基类"向外"进行扩散的. 下面通过实例进行讲解,首先看下面的代码: import static net.mindview.util.Print.*; //<java编程思想>提供的类库 /** * @author Administrator * */ public class Cat extends Animal { public Cat() { // TODO Auto-generated cons

  • Angular 多模块项目构建过程

    引言 两个月前,已存在录题系统,需要构建出题系统,且两套系统存在公用的实体.组件以及服务,如何在构建新系统的同时复用原系统的代码成为了项目难点. 当时的解决方案是将两个系统放在一个应用里,并为该应用配置两套构建方案,当进行 ng serve 或 ng build 时,加载相应配置,动态构建出两套系统,从而解决了共享代码的问题. 现在再去看 Angular ,理解又不同了. 新的思想与理解都源于后端的思考,在构建后端项目时,为了实现代码复用,会构建多模块. 就像下图所示一样,通用的代码放在 Cor

  • 详解Android系统启动过程

    计算机是如何启动的 计算机的硬件包括:CPU,内存,硬盘,显卡,显示器,键盘鼠标等输入输出设备.所有的软件都是存放在硬盘中,程序执行时,需要将程序从硬盘上读取到内存中,然后加载到CPU中来运行.当按下开机键时,内存中什么都没有,因此需要借助某种方式,将操作系统加载到内存中,而完成这项任务的就是BIOS. 引导阶段 BIOS:BIOS是主板芯片上的一个程序,计算机通电后,第一件事情就是读取BIOS. BIOS首先进行硬件检测,检查计算机硬件能否满足运行的基本条件.如果硬件出现问题,主板发出不同的蜂

  • Taro打包Android apk过程详解

    首先,我们使用使用命令创建模板项目,创建的命令如下. taro init myApp 然后,使用 yarn 或者 npm install安装依赖包,并使用下面的命令编译Taro项目. yarn dev:rn 启动后会开启一个监听的进程. 不过,细心的你可能会发现,使用taro init命令初始化的项目是没有原生模块支持的,原来Taro使用了一个壳子工程,首先使用下面的命令下载壳子工程taro-native-shell,如下所示. git clone git@github.com:NervJS/t

随机推荐