Android 编译优化

软件研发中,耗费最多的并不是编写代码,而是代码编译和代码不断调试的过程。对于我们Android来说,随着项目的不断迭代,以及业务模块的不断增加,项目技术栈的增加,项目编译会越来越慢。随着业务的扩展,相信很多的公司都已经做了模块化/组件化。
背景 创建一个 Project 后可以创建多个 Module,这个 Module 就是所谓的模块。一个简单的例子,可能在写代码的时候我们会把首页、消息、我的模块拆开,每个 tab 所包含的内容就是一个模块,这样可以减少 module 的代码量,但是每个模块之间的肯定是有页面的跳转,数据传递等,比如 A 模块需要 B 模块的数据,于是我们会在 A 模块的 gradle 文件内通过 implementation project(':B')依赖 B 模块,但是 B 模块又需要跳转到 A 模块的某个页面,于是 B 模块又依赖了 A 模块。这样的开发模式依然没有解耦,改一个bug依然会改动很多模块,并不能解决大型项目的问题。于是就有了组件的概念,我们日常业务需求开发的组件叫做业务组件,如果这个业务需求是可以被普遍复用的,那么叫做业务基础组件,譬如图片加载、网络请求等框架组件我们称为基础组件。于是一个典型的组件化架构通常如下图所示。
Android 编译优化
文章图片

实线表示直接依赖关系,虚线表示间接依赖。比如壳工程肯定是要依赖业务基础组件、业务组件、module_common公共库的。业务组件依赖业务基础组件,但并不是直接依赖,而是通过”下沉接口“来实现间接调用。业务组件之间的依赖也是间接依赖。最后common组件依赖所有需要的基础组件,common也属于基础组件,它只是统一了基础组件的版本,同时也提供了给应用提供一些抽象基类,比如BaseActivity、BaseFragment,基础组件初始化等。
编译优化 Android编译流程 Android apk的编译构建分为四个步骤:

  1. 代码编译:将源代码,R文件,AIDL生成的文件等 编译成.class文件;
  2. 代码合成:通过dex工具将.class文件和工程依赖的第三方库文件生成虚拟机可执行的.dex文件,如果使用了MultiDex会产生多个dex文件;
  3. 资源打包:apkbuilder工具将.dex文件,apt编译后的资源文件,三方库中的资源文件打包生成签名对齐的apk文件;
  4. 签名和对齐:使用Jarsigner和Zipalign对文件进行签名和对齐,生成最终的apk文件。
以下是gradle编译一个app module 的task链:
gradle clean assembleDebug -x lint check –stacktrace:app:clean//清理上次编译的遗留,删除module下的build文件夹 :app:preDebugBuild//debug版本预编译 :app:checkDebugManifest //AndroidManifest检查 :app:prepareDebugDependencies//检查debug版本的依赖 :app:compileDebugAidl// 编译debug版本的aidl文件 :app:compileDebugRenderscript//编译Renderscript文件 :app:generateDebugBuildConfig//generated/source文件夹下,生成buildConfig文件夹 :app:generateDebugAssets//生成Assets文件到generated下的asset文件夹 :app:mergeDebugAssets//在intermediates下生成assets文件夹,将其他module/aar中的assets文件拷贝过来 :app:generateDebugResValues //生成res value文件 :app:generateDebugResources //生成Resources文件 :app:mergeDebugResources//merge(合并)资源文件 :app:processDebugManifest//将merge后的Manifest文件放在intermediates/manifests文件夹下 :app:processDebugResources//处理资源文件,生成R.txt文件,同时也生成对应的multidex文件夹 :app:generateDebugSources//合成资源文件在generated文件夹下生成对应的R.java文件 :app:compileDebugJavaWithJavac//使用javac生成java文件 :app:compileDebugNdk//ndk编译 :app:compileDebugSources//编译资源文件 :app:transformClassesWithDexForDebug//将.class文件转换成.dex文件 :app:mergeDebugJniLibFolders//合并jni(.so)文件 :app:transformNative_libsWithMergeJniLibsForDebug//转换jni文件 :app:processDebugJavaRes//处理java资源 :app:transformResourcesWithMergeJavaResForDebug //转换java资源文件 :app:validateSigningDebug//验证签名 :app:packageDebug//打包 :app:assembleDebug//apk编译完成

开启InstantRun Android Studio 2.0 推出了InstantRun,意为瞬间编译,在编译开发时减少应用的部署及构建时间。如果需要开启InstantRun,需要Gradle2.0和minSdkVersion15以上版本。
构建流程:代码变更-->编译-->应用构建-->应用部署-->app重启-->activity重启-->完成修改变更
实现即时运行的机制:修改代码后,增量构建(产生增量dex),然后通过判断更新资源的复杂度去选择执行热更新,温更新或者冷更新;
  • 热部署:生效时不需要重启app,也不需要重启activity
  • 温部署:重启activity后才能看到更新
  • 冷部署:app需要重启,但不是重新安装
InstantRun主要干了两件事:
  1. 使用manifest-merger整合项目的manifest,通过aapt工具将合成的AndroidManifest.xml文件与res资源编译到增量apk中;
  2. 代码修改后,通过javac将java文件编译成class文件,然后打包成dex文件,同样放置在增量apk中;
gradle编译优化 我们知道,Android工程是使用gradle进行构建的,所以,优化Android的编译时间,在gradle方面有很多的措施。
properties配置优化
#开启并行编译,仅仅适用于模块化项目(存在多个 Library 库工程依赖主工程) org.gradle.parallel=true # 使用编译缓存 android.emableBuildCache=true # 开启构建缓存,Gradle 3.5新的缓存机制,可以缓存所有任务的输出, #不同于buildCache仅仅缓存dex的外部libs,它可以复用任何时候的构建缓存,设置包括其它分支的构建缓存 org.gradle.caching=true # 构建初始化需要执行许多任务,例如java虚拟机的启动,加载虚拟机环境,加载class文件等等, # 配置此项可以开启线程守护,并且仅仅第一次编译时会开启线程(Gradle 3.0版本以后默认支持) # 保证jvm编译命令在守护进程中编译apk,daemon可以大大减少加载jvm和classes的时间 org.gradle.daemon=true # 最大的优势在于帮助多 Moudle 的工程提速,在编译多个 Module 相互依赖的项目时, # Gradle 会按需选择进行编译,即仅仅编译相关的 Module org.gradle.configureondemand=true # 配置编译时的虚拟机大小,加大编译时AndroidStudio使用的内存空间 # -Xmx2048m:指定 JVM 最大允许分配的堆内存为 2048MB,它会采用按需分配的方式。 #-XX:MaxPermSize=512m:指定 JVM 最大允许分配的非堆内存为 512MB,同上堆内存一样也是按需分配的。 org.gradle.jvmargs=-Xmx3072m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8

过滤gradle task 在执行构建任务时,选择性的去除并不需要运行的gradle task任务。
tasks.whenTaskAdded(new Action() { @Override void execute(Task task) { if (task.name.contains("lint")//不扫描潜在bug可以使用该项 || task.name == "clean" || task.name.contains("Aidl")//项目中用到Aidl则不可以舍弃这个任务 || task.name.contains("mockableAndroidJar")//用不到测试时可以先关闭 || task.name.contains("UnitTest")//用不到测试时可以先关闭 || task.name.contains("AndroidTest")//用不到测试时可以先关闭 || task.name.contains("Ndk") || task.name.contains("Jni")//用不到NDK和jni时关闭 ) { task.enabled = false } } })

使用本地gradle 使用本地的gradle文件,避免从网络拉取的情况。
Android 编译优化
文章图片

其他 【Android 编译优化】将不需要频繁改动的module从setting.gradle中去掉,直接引用module对应的aar文件。工程中有多个module时,会先编译每一个module之后再编译主工程,尽量少的module依赖肯定会加快编译速度。另外,如果你使用的是Kotlin+JetPack方式来构建的Android项目,那么可以尝试使用KSP:告别KAPT,使用KSP为Android编译提速

    推荐阅读