一篇文章了解Android编译构建 编译优化

前言

作为一名Android开发,我们总会面对下面这个按钮,对于一些大型项目,或者对于在编译期间做了很多task的工程都会花去相当多的时间。就我而言,经常跑一个debug的包需要3分钟左右,当跑release的包需要10分钟左右,如果是一些性能差的电脑,这个时间会消耗的更多。今天文章的主题有两个部分,一个是Android的编译都做了哪些事,还有一个是如何提高我们的编译是速度以及相关原理。
今天的主题将围绕下面这个按钮开始讲述。
在这里插入图片描述

什么是编译

大家在刚学计算机基础这门课程的时候就知道,“计算机只认识0和1,我们写的编译程序经编译器翻译成由0和1构成的二进制格式才能由计算机执行”。所以,我们写的java语言或者kotlin语言最终都会转化成0,1的二进制数据。在这里,这些二进制数据有一个名字—字节码(各种不同平台的虚拟机与所有平台都统一使用的程序存储格式)。在java虚拟机中,使用java编译器可以把java代码编译为存储字节码的Class文件。Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑的排列在Class文件中。对于具体的词法分析、语法分析以及底层编译原理就不在这里细讲了。

具体看下下面的图。

在这里插入图片描述

你可以把编译简单理解为,将高级语言转化为机器或者虚拟机所能识别的低级语言的过程。对于 Android 来说,这个过程就是把 Java 或者 Kotlin 转变为 Android 虚拟机运行的Dalvik 字节码(DEX文件)的过程。

java字节码和 Dalvik字节码


javac Sample.java   // 生成Sample.class,也就是Java字节码
//  javac -source 1.8 -target 1.8 Sample.java 也可以通过指定版本与dex对应上
javap -v Sample     // 查看Sample类的Java字节码

//通过Java字节码,生成Dalvik字节码
dx --dex --output=Sample.dex Sample.class   

dexdump -d Sample.dex   // 查看Sample.dex的Dalvik的字节码

//javac .class
  public void test();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #13                 // String 哈哈哈哈
         5: invokevirtual #15                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 8: 0
        line 9: 8

Dalvik字节码

JVM、DVM(dalvik)和ART

Dalvik与JVM的区别
  1. Java 虚拟机是基于栈实现,而 Android 虚拟机是基于寄存器实现。基于寄存器的虚拟机对于编译后变大的程序来说,在它们执行的时候,花费的时间更短。
  2. Java虚拟机运行java字节码,Dalvik虚拟机运行的是其专有的文件格式Dex
Dalvik与ART的区别
  1. 平台支持区别
     Dalvik:Android 4.4及其以下系统使用。
     ART:Android 4.4以上系统使用。

  2. 工作原理区别

  • Dalvki采用JIT(Just In Time)技术。
    JIT为“即时编译技术”,当App运行时,每当遇到一个新类,JIT编译器就会对这个类进行编译,经过编译后的代码,会被优化成相当精简的原生型指令码(即native code),这样在下次执行到相同逻辑的时候,速度就会更快。
  • ART采用AOT技术。
    预先 (AOT) 编译。在ART 环境中,应用在第一次安装的时候,字节码就会预先编译成机器码,使其成为真正的本地应用。之后打开App的时候,不需要额外的翻译工作,直接使用本地机器码运行,因此运行速度提高。ART需要应用程序在安装时,就把程序代码转换成机器语言,所以这会消耗掉更多的存储空间,但消耗掉空间的增幅通常不会超过应用代码包大小的20%
    由于有了一个转码的过程,所以应用安装时间难免会延长

构建流程

我们经常点击构建按钮,就会看到Android studio下方开始不停的执行task,最终会生成一个APK并打开我们设置的launch Activity。以官方的话来说,
Android 编译系统会编译应用资源和源代码,然后将它们打包成可供您测试、部署、签署和分发的 APK。我们具体可以看下整个构建流程。

图片名称

总共可以分为以下4部分

  • 1.编译器将您的源代码转换成 DEX 文件(Dalvik 可执行文件,其中包括在 Android 设备上运行的字节码),并将其他所有内容转换成编译后的资源。
  • 2.APK 打包器将 DEX 文件和编译后的资源组合成单个 APK。
  • 3.APK 打包器使用调试或发布密钥库为 APK 签名
  • 4.在生成最终 APK 之前,打包器会使用 zipalign 工具对应用进行优化,以减少其在设备上运行时所占用的内存。

在这里插入图片描述

在上文我们了解到我们的java和kotlin代码会被Android虚拟机转化成dex文件,那我们的资源文件以及xml又是如何打包的呢。我们来了解下 AAPT2

AAPT2

AAPT2(Android 资源打包工具)是一种构建工具,Android Studio 和 Android Gradle 插件使用它来编译和打包应用的资源。AAPT2 会解析资源、为资源编制索引,并将资源编译为针对 Android 平台进行过优化的二进制格式。

AAPT2 支持通过启用增量编译实现更快的资源编译。这是通过将资源处理拆分为两个步骤来实现的:

  • 编译:将资源文件编译为二进制格式。
  • 链接:合并所有已编译的文件并将它们打包到一个软件包中。

如何优化编译速度

这里我们来讲讲增量编译相关优化方案

增量编译–对用户源程序局部修改后进行的重新编译的工作只限于修改的部分及与之相关部分的内容。相关部分的确定由编译系统确定,对用户是透明的。增量编译对软件开发,尤其是在调试期,可以大大缩短编译时间, 提高编译效率,这也是增量编译的优势所在。

关于增量编译方案

我们在上文了解了整个apk的构建流程,每次正常打apk都需要完整的链路走过一次,假设我们只改动了部分代码,能不能只编译我们改动的部分,其余的部分利用缓存机制,来帮助我们快速构建apk呢。
我们来看下目前市面上已有的编译方案。

instant-run

Instant Run的作用是使得开发过程中的改动可以不用完整编译并重新安装app就能应用,也就是更快看到改动的实际效果,节省时间。实现的原理是通过修改原先的构建过程在初始编译中实现插桩,在后面的改动中只编译改动的部分,并把产物推送到设备上,并通过植入app中的runtime加载新的变动。

  • 因其代码增量是通过运行期hack method实现,所以进行了instant-run后,实际App没有重新走原有该走的生命周期,导致要看到类似onCreate,onResume等生命周期方法修改后的效果,必须手动重启一次进程,另外因为不同手机指令集合的不同,instant-run还会有一定挂掉的机会。
  • 在 Gradle 4.6 之前,如果项目中运用了 Annotation Processor。为了保证准确性,本次修改以及它依赖的模块都需要全量 javac,而这个过程是非常慢的。

因为 instant-run存在一些弊端,在 Android Studio 3.5 及更高版本中,Apply Changes 可让您将代码和资源更改推送给正在运行的应用,而无需重启应用(在某些情况下,甚至无需重启当前的 Activity)。但是Apply Changes 使用搭载 Android 8.0(API 级别 26)或更高版本的设备上支持。

Apply Changes

在 Android Studio 3.5 及更高版本中,Apply Changes 可让您将代码和资源更改推送给正在运行的应用,而无需重启应用(在某些情况下,甚至无需重启当前的 Activity)。

在这里插入图片描述
Apply Changes 使用搭载 Android 8.0(API 级别 26)或更高版本的设备上支持的 Android JVMTI 实现中的功能。对于UI的改动,我们能通过apply changes快速在手机中查看我们修改的地方。
同时,也存在一定限制:
某些代码和资源更改必须在重启应用之后才能应用,其中包括以下更改:

  • 添加或删除方法或字段
  • 更改方法签名
  • 更改方法或类的修饰符
  • 更改类继承行为
  • 更改枚举中的值
  • 添加或移除资源
  • 更改应用清单
  • 更改原生库(SO 文件)

整体设计需要考虑两个点

  • 1.找出差异是什么
  • 2.如何将修改的代码发送到设备并应用。

我们看下Apply Changes整体框架图

图片名称

比较dex文件

在后台进程中利用D8的DEX文件分析功能来检查Android Studio部署到设备上的每个.dex文件的内容。在.dex文件中的各个类上计算基于校验和的指纹,并将结果临时存储在主机工作站上的缓存数据库中。通过将新编译的指纹与以前的编译的指纹进行比较,“应用更改”能够在短时间内有效地提取更改的类。

增量推送

“应用更改”需要计算安装的APK与最近构建的APK之间不同的文件,而不必从设备中提取所有内容。这次,它仅获取压缩文件的中央目录,并保守估计相应APK之间的差异。通过仅传输已更改的部分,Android Studio传输的数据比完整的APK上传要少得多。在大多数使用情况下,总有效负载减少到几个KiB,而不是几个MiB。

BUCK

BUCK建立了一套完善的依赖规则以及细化的缓存系统来缩减编译时间,其增量构建的原理,实际是以工程目录为单位进行增量构建,发生变更时候,变更的工程,以及该工程作为父节点或祖先节点的工程,均需要重新构建。无论是 Buck 的exopackage,还是代码的增量编译,Buck 都更加高效。Buck还有分包支持,ReDex 支持等功能。

但是BUCK的整体接入成本还是比较高的。要去掉原有的gradle方案

Freeline

多任务并发,多级缓存,增量范围最小化,懒加载,基于长链接无安装式运行期动态替换,基线对齐触发机制,可调试。详细细节可以参考 Freeline的官方文档。

增量编译具体举措

  • 更换编译机器。装备升级,CPU升级效果杠杠的。

  • 升级 Gradle 和 SDK Build Tools。在新版本中,Android致力于优化Gradle的构建。

  • 采用上述增量编译方案的某一种。

代码优化工具

对于debug测试包,我们关心apk构建的速度以便我们能快速开发。而对于需要发布的release包,我们更关心包的大小以及性能的稳定。

ProGuard

接入proGuard后,我们的app构建流程将变化如下。

在这里插入图片描述

d8

d8 是一种命令行工具,Android Studio 和 Android Gradle 插件使用该工具来将项目的 Java 字节码编译为在 Android 设备上运行的 DEX 字节码,该工具支持您在应用的代码中使用 Java 8 语言功能。

在这里插入图片描述

R8

当您使用 Android Gradle 插件 3.4.0 或更高版本构建项目时,该插件不再使用 ProGuard 执行编译时代码优化,而是与 R8 编译器协同工作。
在编译时做了以下事情

  1. 代码缩减 :从应用及其库依赖项中检测并安全地移除不使用的类、字段、方法和属性
  2. 资源缩减:从封装应用中移除不使用的资源,包括应用库依赖项中不使用的资源。
  3. 混淆处理:缩短类和成员的名称,从而减小 DEX 文件的大小。
  4. 优化:检查并重写代码,以进一步减小应用的 DEX 文件的大小。例如,如果 R8 检测到从未采用过给定 if/else 语句的 else {} 分支,则会移除 else {} 分支的代码。(当然有规范的话,我们在日常业务中应该用Lint检测来优化我们的代码,也能方便我们阅读代码)

在这里插入图片描述

R8 的最终目的 加快编译速度,更强大的代码优化。

ReDex

ReDex是Facebook开源的Android字节码(dex)优化器,它直接输入的对象是 Dex,而不是“.class”文件。ReDex拥有目前更强大的编译速度以及代码优化。具体配置可以参考 Redex官方。能有效的降低包提及,优化了android原有的打包策略。

总结

对于如何提高编译速度, google在每个版本升级中都进行了优化,同时,行业内也开源了自身的好的编译方案,对于我们日常的业务开发中,可以根据自身的业务需求来选择方案。
对于我们日常的debug开发,为了能快速看到我们相关功能的具体实现。增量编译的速度成为了关键,在编译过程中,我们看到了一些代码插桩的技术。将在之后给各位详细介绍Android中如何进行代码插桩的方案和一些用例。

参考资料

https://time.geekbang.org/column/article/82468

https://source.android.com/devices/tech/dalvik/dalvik-bytecode

https://developer.android.com/studio/build?hl=zh-cn

https://developer.android.com/studio/command-line/aapt2?hl=zh-cn

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值