写点什么

Andorid&Kotlin 编译速度原理剖析(上),lambda 表达式的作用与好处

发布于: 2021 年 11 月 08 日

@get:Optional


abstract val versionName: Property<String?>


//版本号


@get:Input


@get:Optionalabstract


abstract val versionCode: Property<Int?>


//父类 NonIncrementalTask 的唯一抽象方法,也就是 BuildConfig 的主要逻辑处理方法


override fun doTaskAction() {


//获取类里面的属性包括一些自定义的属性


val buildConfigData = BuildConfigData.Builder()


.setBuildConfigPackageName(buildConfigPackageName.get())


.apply {


//此处省略了 BUILD_TYPE、FLAVOR、DEBUG 等属性的获取,思路是一样的


if (hasVersionInfo.get()) {


versionCode.orNull?.let {


addIntField("VERSION_CODE", it)


addStringField("VERSION_NAME", "${versionName.getOrElse("")}")


}


}


//


val generator: GeneratedCodeFileCreator =


if (bytecodeOutputFile.isPresent) {


//创建一个 JVM 字节码 BuildConfig,Kotlin 版本进行了改造


BuildConfigByteCodeGenerator(byteCodeBuildConfigData)


} else {


//创建一个 java 文件的 BuildConfig,java 版本的 GenerateBuildConfig 一直是这种方案


BuildConfigGenerator(sourceCodeBuildConfigData)


}


}


//调用内部实现类,用 JavaWriter 创建 generator.generate()


}


}


复制代码


可以看到 GenerateBuildConfig 已经改成了 Kotlin,同时其他的系统 Task 也都变成了 Kotlin 版本。看来谷歌也是下了血本了。Kotlin 的相关知识比如协程、suspend、非阻塞式挂起函数、扩展函数、泛型也会写一些文章欢迎点赞关注,给作者一些动力。言归正常可以看到 GenerateBuildConfig 继承了 NonIncrementalTask,这个父类也是 Kotlin 版本改造后才有的基本上其他的系统 Task 也都继承于这个类。主要作用是一个增量编译处理类。内部有一个抽象方法 doTaskAction,也就是 GenerateBuildConfig 里面的主要逻辑实现方法。同时还有个 cleanUpTaskOutputs 方法在 doTaskAction 之前调用,主要作用于确保在任务运行之前删除任务输出。


生成 Java 类的主要逻辑流程:


doTaskAction-->buildConfigData -->BuildConfigGenerator-->JavaWriter


复制代码


生成字节码类的主要逻辑流程:


doTaskAction-->buildConfigData -->BuildConfigByteCodeGenerator-->ClassWriter


复制代码


主要流程拆分


  1. 生成 buildConfigData 类,这是一个 Builder 的设计模式

  2. 添加一些默认的属性比如:BUILD_TYPE、FLAVOR、DEBUG 等

  3. isPresent 则生成 BuildConfigByteCodeGenerator 否则生成 BuildConfigGenerator

  4. 如果是 BuildConfigGenerator 则通过 items.get()添加自定义的属性

  5. 调用 generate 生成具体实现类内部用 JavaWriter or ClassWriter 实现


系统其他 Task、对应实现类和作用



三、编译耗时检测



1、gradlew 命令

对于较大的项目或者实现大量自定义 Transfrom-API 项目,可能需要深入了解构建流程才能找到瓶颈。为此,可以剖析 Gradle 执行构建生命周期的每个阶段和每个构建任务所需的时间。


如需生成和查看构建性能剖析报告,请按以下步骤操作:


1、打开项目根目录下的命令行终端。


2、输入以下命令,可以先执行 Claen。因为如果某个任务的输入内容(例如源代码)未发生更改,Gradle 就会跳过它。因此输入内容未发生更改的第二个 build 始终会以更快的速度运行,因为任务不会重复运行。在 build 之前运行 clean 任务可以确保您能够剖析完整的构建流程。


//mac


gradlew clean


//window


gradle clean


复制代码


3、执行完 Clean 后可以根据需要分析的构建环境执行以下命令


//mac


gradlew assembleDebug --profile


//window


gradle assembleDebug --profile


复制代码


4、构建完成后,可以在项目的根目录下的/build/reports/profile/ 目录找到对应的 html 报告


5、可以查看报告中的每个标签页以了解您的构建,例如,Task Execution 标签页显示了 Gradle 执行各个构建任务所花费的时间。这里需要注意的地方是,Summary 的 task Execution 是每个模块累计相加,实际上多个模块都是并行的。


Summary:构建时间概要


Configuration:配置时间


DependencyResolution:依赖解析花费的时间


TaskExecution:每个任务执行的时间


2、自定义 Gradle 生命周期实现方法

可以看到在每次的运行构建编译后会对每个 gradleTask 进行耗时的打印,因此可以针对耗时任务严重的 Task 做针对性的优化处理还可以针对耗时超过一定时间的任务做监控,如果触发了临界值就会做报警处理这样就保证了以后的 Task 一直处于较低的耗时,因为内容比较多这个监控方案第二章的时候会详细讲解。


其他生命周期的方法以省略,具体代码如下:


import java.util.concurrent.TimeUnitclass


TimingsListener implements TaskExecutionListener, BuildListener {


private long startTime


private timings = []


@Override


void beforeExecute(Task task) {


startTime = System.nanoTime()


}


@Override


void afterExecute(Task task, TaskState taskState) {


def ms = TimeUnit.MILLISECONDS.convert(


System.nanoTime() - startTime, TimeUnit.NANOSECONDS);


timings.add([ms, task.path])


task.project.logger.warn "{ms}ms"


}


@Override


void buildFinished(BuildResult result) {


println "Task timings:"


for (timing in timings) {


if (timing[0] >= 50) {


printf "%7sms %s\n", timing


}


}


}


}


gradle.addListener new TimingsListener()


复制代码


四、编译优化常规方案




俗话说的好“预先善其事,必先利其器”、“磨刀不误砍柴工” 、“先谋而后动”等。大致意思那就是先把需要用到的工具进阶升级下才能打怪更加的无伤或者在打怪前先计划好何时动手,何时使用必杀技等。根据以上结论就有了以下几种编译速度的优化方案:

1、使用最新版本工具

谷歌也一直很值开发中的痛楚,同时自己也改造了系统的 Gradle Task 和出了一些针对构建速度的 Studio 工具比如:Instant Run、Apply Changes。Instant Run 这个技术是基于 Transfrom-API 技术,Transfrom-API 业界好多的热修复框架也是基于这个思想来实现的但是由于诟病太多在 Android Studio 3.5 Instant Run 就被废弃了。后来又出了 Apply Changes 它依赖的是 Android 8.0 开始虚拟机支持的特殊指令 (JVMTI) 来进行类的替换。这两个工具后面的深度编译速度优化章节会详细的介绍就不再这里陈诉了,回归正题。


几乎每次更新时,Android 工具都会有一定构建方面的优化所以说我们可以把以下工具升级到最新的版本:


2、Debug 环境只编译需要的资源

避免编译不必要的资源


避免编译和打包不测试的资源(例如,其他语言本地化和屏幕密度资源)。为此,您可以仅为“dev”或者“debug”的版本指定一个语言资源和屏幕密度,如下面的示例中所示:


android {


...


productFlavors {


debug {


...


//在 debug 环境编译时只会处理中文的语言和 xxhdpi 的资源图片


//这样就减少了打包的第一步 AAPT 的资源合并的流程,


resConfigs "zh", "xxhdpi"


}


...


}


}


复制代码


对调试 build 停用 Crashlytics


如果您不需要运行 Crashlytics 报告,请按如下方法停用该插件,以提高调试 build 的构建速度:


android {


...


buildTypes {


debug {


ext.enableCrashlytics = false


}


}


复制代码


禁止自动生成 build ID


如果想要将 Crashlytics 用于调试 build,可以通过阻止 Crashlytics 在每次构建过程中使用唯一 build ID 更新应用资源,提高增量构建的速度。由于此 build ID 存储在清单引用的资源文件中,因此禁止自动生成 build ID 还可以将 Apply Changes 和 Crashlytics 一起用于调试 build。如果需要阻止 Crashlytics 自动更新其 build ID 可以配置如下:


android {


...


buildTypes {


debug {


ext.alwaysUpdateBuildId = false


}


}


复制代码

3、版本将图片转换为 WebP

WebP 是一种既可以提供有损压缩(像 JPEG 一样)也可以提供透明度(像 PNG 一样)的图片文件格式,不过与 JPEG 或 PNG 相比,这种格式可以提供更好的压缩。减小图片文件大小可以加快构建速度(无需在构建时进行压缩),尤其是当应用使用大量图片资源时。不过,在解压缩 WebP 图片时,能会注意到设备的 CPU 使用率有小幅上升。通过使用 Android Studio,您可以轻松地将图片转换为 WebP 格式。步骤如下:


  1. 右键点击某个图片文件或包含一些图片文件的文件夹,然后点击 Convert to WebP

  2. Converting Images to WebP 对话框随即打开。默认设置取决于当前模块的 minSdkVersion 设置。

  3. 点击 OK 以开始转换。如果要转换多张图片,只需一步即可完成转换操作,并且可以撤消转换操作以便一次性还原已转换的所有图片。

  4. 如果在上面选择了无损转换,系统会立即进行转换。图片会在原始位置进行转换。如果选择了有损转换,请继续执行下一步。

  5. 如果您选择了有损转换,并且选择在保存之前查看每张转换后图片的预览效果,那么 Android Studio 会在转换过程中显示每张图片,以便检查转换结果。

  6. 点击 Finish。图片会在原始位置进行转换。


左侧是原始 JPG 图片,右侧是有损编码 WebP 图片。对话框中显示了原始图片和转换后图片的文件大小。您可以向左或向右拖动滑块以更改质量设置,并能够立即看到编码图片的效果和文件大小。


4、格式停用 PNG

如果无法(或者不想)将 PNG 图像转换为 WebP 格式,仍可以在每次构建应用时停用自动图片压缩,从而提高构建速度。如果使用的是 Android 插件 3.0.0 或更高版本,默认情况下仅针对“调试”构建类型停用 PNG 处理。如需针对其他构建类型停用此优化,请将以下代码添加到 build.gradle 文件中:


android {


...


buildTypes {


debug{


//禁用 PNG 压缩。


crunchPngs false


}


}


复制代码

5、开启 gradle 缓存

构建缓存可以存储构建项目时 Android Plugin for Gradle 生成的特定输出(例如,未打包的 AAR 和经过 dex 预处理的远程依赖项)。使用缓存时,干净构建的速度会显著加快,因为构建系统在进行后续构建时可以直接重用这些缓存的文件,而无需重新创建。


#开启 gradle 缓存


org.gradle.caching=true


android.enableBuildCache=true


复制代码

6、开启 kotlin 的增量和并行编译

#开启 kotlin 的增量和并行编译


kotlin.incremental=true


kotlin.incremental.java=true


kotlin.incremental.js=true


kotlin.caching.enabled=true


kotlin.parallel.tasks.in.project=true


复制代码

7、使用静态依赖项版本

build.gradle 文件中声明依赖项时,您应当避免在结尾处使用带加号的版本号,例如 'com.android.tools.build:gradle:2.+'。使用动态版本号可能会导致意外的版本更新和难以解析版本差异,并会因 Gradle 检查有无更新而减慢构建速度。应该使用静态/硬编码版本号。

8、合理调整堆大小

#设置 jvmargs 大小 org.gradle.jvmargs=-Xmx4000M


复制代码

9、kapt 优化

APT:Java 提供了一个编译时期插件, 在代码编译期对源代码进行扫描,找出代码中的注解, 根据开发者定义的解析规则生成新的 Java 文件, 并且执行生成的代码将会与你手动编写的代码一起被 javac 编译。


KAPT:官方提供三种解决方案已经迭代到 kapt3 选用的也是第三种方案:


  1. 重新设计,但违背与 java 共存原则。

  2. 生成"存根类"这个类里面所有方法的方法体为空,也就是只保留类的结构,然后把这些"存根类"加入 javac classpath 中编译。方法返回类型是需要对表达式进行分析,这样会大大降低编译速度

  3. Kotlin 代码编译成 Java 编译器可识别的二进制文件


#优化 kapt kapt.use.worker.api=true //并行运行 kapt.incremental.apt=true //增量编译 kapt.include.compile.classpath=false //开启缓存 kapt { useBuildCache = true }

10、使用增量注解处理器

Android Gradle 插件 3.3.0 及更高版本改进了对增量注解处理的支持。因此,如需提高增量构建速度,可以更新 Android Gradle 插件并尽可能仅使用增量注解处理器。


此外,如果在应用中使用 Kotlin,就需要使用 kapt 1.3.30 及更高版本才能在 Kotlin 代码中支持增量注解处理器。如果必须使用一个或多个不支持增量构建的注释处理器,注释处理将不会是增量的。但是,如果项目使用的是 kapt,Java 编译仍然是增量的。


第三方增量注释处理器支持 :

评论

发布
暂无评论
Andorid&Kotlin编译速度原理剖析(上),lambda表达式的作用与好处