专栏首页逮虾户AndResGuard编译速度优化

AndResGuard编译速度优化

背景

各位大佬好久不见了,最近忙于搞这个黑科技的开发工作,没有时间写博客,见谅见谅。

当前项目内用了腾讯的AndResGuard对资源文件的大小进行了一次深度优化。AndResGuard负责将文件名,arsc文件和R文件也进行了一次混淆,能把整体的资源文件大小压缩。

但是奈何也不是一个尽善尽美的方案,所以我们打算在其基础上进行一次二次开发。

AndResGuard原理

我先简单的介绍下AndResGuard(后面简称ARG)是原理。

首先我们需要先编译我们的app项目,等到所有编译流程走完之后生成apk文件,然后ARG会去将apk文件解压并拷贝一份副本,之后从副本中把arsc以及其他的资源文件进行混淆重命名文件等操作,最后再把这个副本重新打包成apk,然后对apk进行重签名等操作。

只有了解了完整的ARG的流程之后,我们才可以对其进行二次开发和二次优化。首先当然先是设立目标了,我们要做什么,然后可以怎么做?

TODO

我们打算做些什么?

  1. 是不是能将混淆的流程放到apk编译流程中,充分的利用编译时多线程的能力呢?
  2. 是不是可以对混淆的规则进行二次调整,从而达到压缩比例的提升。
  3. 有没有办法节省一下编译速度的问题,提升插件的效率。

ACTION

在开发之前,肯定是要先进行方案梳理还有竞品分析的,先找找有没有什么竞品可以帮助我们。

我们在调研的过程中,美团,腾讯,头条都有对应的资源文件的混淆方案。其中腾讯的就是ARG,而ARG也是使用最多的。而美团貌似也没有找到开源项目所以没有后续的跟进。而头条的AabResGuard主要是肩负了头条的App Bundle的压缩,同时也做了普通的资源混淆。朋友说出海项目app bundle的压缩主要是靠这个。

我们参考了AabResGuard的修改任务执行顺序的方式,把ARG的执行顺序进行了一次合理的变更。

如何更改编译任务的执行顺序

在对Aab的代码分析过程中,我们其实发现了一些很神奇很微妙的点,对于我们后续的优化产生了重大的启发。

private fun createAabResGuardTask(project: Project, scope: VariantScope) {
        val variantName = scope.variantData.name.capitalize()
        val bundleTaskName = "bundle$variantName"
        if (project.tasks.findByName(bundleTaskName) == null) {
            return
        }
        val aabResGuardTaskName = "aabresguard$variantName"
        val aabResGuardTask: AabResGuardTask
        aabResGuardTask = if (project.tasks.findByName(aabResGuardTaskName) == null) {
            project.tasks.create(aabResGuardTaskName, AabResGuardTask::class.java)
        } else {
            project.tasks.getByName(aabResGuardTaskName) as AabResGuardTask
        }
        aabResGuardTask.setVariantScope(scope)

        val bundleTask: Task = project.tasks.getByName(bundleTaskName)
        val bundlePackageTask: Task = project.tasks.getByName("package${variantName}Bundle")
        bundleTask.dependsOn(aabResGuardTask)
        aabResGuardTask.dependsOn(bundlePackageTask)
        // AGP-4.0.0-alpha07: use FinalizeBundleTask to sign bundle file
        // FinalizeBundleTask is executed after PackageBundleTask
        val finalizeBundleTaskName = "sign${variantName}Bundle"
        if (project.tasks.findByName(finalizeBundleTaskName) != null) {
            aabResGuardTask.dependsOn(project.tasks.getByName(finalizeBundleTaskName))
        }
    }
复制代码

这一部分代码是Aab的plugin在构造一个混淆任务的时候篡改的任务执行的依赖顺序。

variantName代表构建的一个变种,可以是多渠道构建也可以是debug release的变种。

一个普通的安卓app Bundle 执行的顺序是bundlevariantName之后马上执行一个package{variantName}Bundle。

而aab的plugin则是在其中过程中插入了一个自定义的混淆task,也就是上述代码中的aabResGuardTaskName,这样当一个package{variantName}Bundle被执行的时候,则是会把顺序变更为bundlevariantName-aabResGuardTaskName-package

这里科普个小姿势,gradle task的任务顺序是通过有向无环图(DAG)的数据结构进行排序的,所以当任务之间有依赖关系的情况下,gradle会根据DAG的排序顺序执行。基本上如果有任意出现dependsOn的你都可以简单的把他们理解为DAG。

观察一个项目编译的流程

有时候会有同学说,面试的时候问什么编译流程吗,真实开发中完全不会用到呀。但是有时候多个技能也没啥不好的呀。

还是用了之前打印Task耗时的一段代码逻辑,将一个Apk编译的task进行了打印。

    159ms  :libres:generateDebugRFile
    186ms  :libres:compileDebugJavaWithJavac
    181ms  :app:processFlavor2Flavor1DebugManifest
    121ms  :app:mergeFlavor2Flavor1DebugResources
    999ms  :app:processFlavor2Flavor1DebugResources
   1025ms  :app:compileFlavor2Flavor1DebugKotlin
   1163ms  :app:resguardFlavor2Flavor1Debug
   1183ms  :app:mergeFlavor2Flavor1DebugNativeLibs
    296ms  :app:compileFlavor2Flavor1DebugJavaWithJavac
    451ms  :app:transformClassesWithDexBuilderForFlavor2Flavor1Debug
     99ms  :app:mergeProjectDexFlavor2Flavor1Debug
    124ms  :app:mergeFlavor2Flavor1DebugJavaResource
    295ms  :app:packageFlavor2Flavor1Debug

当我们开始编译一个Apk的时候,从上到下的任务栈大概就是和上面的类似了,我demo中增加了plavor变种,但是并不影响任务。其中混进的resguardFlavor2Flavor1Debug这个任务就是我们的资源混淆的任务,实现规则基本就和字节的aab的方案类似。然后我们插入的节点选择是在processFlavor2Flavor1DebugResources之后,同时在mergeFlavor2Flavor1DebugJavaResource之前去执行我们的混淆任务。

为什么要选择这个节点?

当我们编译一个apk的时候,会在build/intermediates文件夹下生成很多输入输出的文件,这个是我之前在开发transform的时候找到的小技巧。然后我就在这个文件夹下搜索,并观察哪个是我们资源文件编译完成的任务节点呢?

我们可以先看下aapt编译的大概的一个过程,最后我发现了一个有意思的目录processed_res,也就是上面说的processFlavor2Flavor1DebugResources这个任务了。这个文件夹下面会有个out文件目录,其中会包含一个.ap_的文件,基于一个开发的敏锐的嗅觉,我发现真相只有一个(shi n ji tsu wa i tsu mo hi to tsu),我用jadx去反编译了下这个文件,发现里面存放的就是所有的资源文件,arsc文件。

同时我又做了个大胆的实验,如果我把混淆的ap_放在这里,然后覆盖同名文件。那么会不会在后续编译出来的apk就是一个混淆过的apk呢?

而实验结果也正如我所推测的是一样的,最后编译出来的apk就是一个混淆过的apk。

这里要留一些小遗憾了,我本来想把整个编译流程的Task源代码摸一摸的,但是尝试性的看了下这部分源代码,但是奈何太难了而且debug成本太高了,所以我也没有仔细看懂。

第一个任务完成

从上述流程走通之后,我们只要把ARG的代码进行二次开发,根据对应task任务进行优化,这样我们的第一个任务也就完成了。

private fun runGradleTask(absPath: String, outputFile: File, minSDKVersion: Int): File? {
        val packageName = applicationId
        val whiteListFullName = ArrayList()
        configuration?.let {
            val sevenzip = project.extensions.findByName("sevenzip") as ExecutorExtension
            configuration.whiteList.forEach { res ->
                if (res.startsWith("R")) {
                    whiteListFullName.add("$packageName.$res")
                } else {
                    whiteListFullName.add(res)
                }
            }
            val builder = InputParam.Builder()
                .setMappingFile(configuration.mappingFile)
                .setWhiteList(whiteListFullName)
                .setUse7zip(configuration.use7zip)
                .setMetaName(configuration.metaName)
                .setFixedResName(configuration.fixedResName)
                .setKeepRoot(configuration.keepRoot)
                .setMergeDuplicatedRes(configuration.mergeDuplicatedRes)
                .setCompressFilePattern(configuration.compressFilePattern)
                .setZipAlign(getZipAlignPath())
                .setSevenZipPath(sevenzip.path)
                .setOutBuilder(useFolder(outputFile))
                .setApkPath(absPath)
                .setUseSign(configuration.useSign)
                .setDigestAlg(configuration.digestalg)
                .setMinSDKVersion(minSDKVersion)

            if (configuration.finalApkBackupPath != null && configuration.finalApkBackupPath.isNotEmpty()) {
                builder.setFinalApkBackupPath(configuration.finalApkBackupPath)
            } else {
                builder.setFinalApkBackupPath(absPath)
            }
            builder.setSignatureType(InputParam.SignatureType.SchemaV1)
            val inputParam = builder.create()
            return Main.gradleRun(inputParam)
        }
        return null
    }
复制代码

这个就是ARG调用资源文件混淆的代码了,我们基本不需要对其进行大改造就能把这个编译的优化完成了,而且可以充分的利用gradle的多线程,因为processRes的task和transform是并行的。

数据对比

图1 是我们更改之后的解压速度以及执行顺序,图2则是使用原生的ARG的速度,可以发现我们虽然只是变更了下任务的执行,但是从速度上也得到了很大的优化。其中一部分原因是因为ARG解压重新打包的是整个apk项目,而我们则只是操作了资源文件生成的假的apk项目而已。而且由于是并发任务,所以其实速度会更快一点。

通过多线程完成并行

就这?有没有办法将这个编译速度更提升一步呢?

我们是不是可以考虑直接把任务执行在线程内,这样下一个task就可以继续执行了,只要在编译完成之前把任务执行好是不是就可以把这部分资源混淆的时间也给优化掉呢,说干就干,直接上代码。

open class ResProguardTask : DefaultTask() {
    private var executor: ExecutorService = Executors.newSingleThreadExecutor()
    private var future: Future<out Any>? = null
    
    @TaskAction
    fun execute() {
        future = executor.submit {
          // 资源文件混淆了
        }
    }

    fun await(){
        future?.get()
    }
}
复制代码

假定上面就是我们定义的资源文件的Task,我们把其中资源文件混淆的操作全部放在了ExecutorService内,当TaskAction被执行之后,由于后续执行在线程内,所以马上就会执行下一个Task,那么这种写法是不是就完全Ok了呢。

秉承着程序猿的严谨性,其实如果假定我们这个future比较耗时1分半,然后编译的总时长是1分钟,那么当我们在合并打包的时候就会出现问题,就会导致这次资源混淆失败。有没有办法在最后Task执行之前等待我们的Future完全执行完呢?大家有没有注意到我下面写的await操作,由于Future的特性,只有当所有方法被执行完之后get才会有值,否则这里就是个while(true)的循环。那么我们如何在最后合包的Task之前做等待呢?

        val bundlePackageTask: Task = project.tasks.getByName("package${variantName}")
        bundlePackageTask.doFirst {
                val resProguardTask = project.tasks.getByName(resGuardTaskName)
                        as ResProguardTask
                resProguardTask.await()
        }
复制代码

这个地方,我们只要充分的利用Task提供的doFirst和doLast方法,就能在任务的前后进行任意的操作,这里我们做了一次等待,等待所有我们资源文件混淆的future执行完成之后才允许packageTask执行。

有人在代码里投毒

在插件实际上线的阶段,我们碰到了一个非常奇怪的问题,资源文件混淆失败了。最后实际调试中发现了由于项目开启了shrink,所以在r8阶段项目重新生成了一个ap_文件,而这个文件才是最后apk合成包所用的。

val manager = scope.transformManager
        val field = manager.javaClass.getDeclaredField("transforms").apply {
            isAccessible = true
        }
        val list = field.get(manager) as List
        list.forEach {
            if (it is ShrinkResourcesTransform) {
            }
        }
}

我最后反射了scope内持有的transforms的列表,然后把ShrinkResourcesTransform这个transform拉取了出来,最后获取了这个Transform转化的Task,之后在这个任务之前做了上面的await操作,这样就能保证在ShrinkResourcesTransform执行之前,就会完成资源文件混淆的操作。

吐槽

讲道理groovy真的烂,由于没有编译时的告警,所以你也不知道你的代码写的到底是正确还是错误的。而且以前编写一个gradle插件,以前都采用的是上传本地aar的方式,所以吧尤其的恶心,导致我最近写新的插件的时候用的都是kotlin,真香。

最后

(゜-゜)つロ 干杯~-bilibili。

我们组最近开源的路由项目 BRouter,欢迎各位大佬点赞啊。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 优化Gradle提升Gradle编译速度

    一. 我们先说一下提升Gralde编译速度. 针对这个问题, 先讲一下配置相关设置提升编译速度.

    砸漏
  • Java动态编译优化——提升编译速度(N倍)

    最近一直在研究Java8 的动态编译, 并且也被ZipFileIndex$Entry 内存泄漏所困扰,在无意中,看到一个第三方插件的动态编译。并且编译速度是原来...

    执笔记忆的空白
  • Android性能优化之APK 极限压缩(资源越多,效果越显著)

    随着项目的不断迭代,代码量跟资源文件不断增多。那么就会出现打包后的 APK 文件越来越大,如果突然有一天你们老板或领导叫你优化 APK 大小,你还不知道怎么优化...

    Android技术干货分享
  • iOS 微信编译速度优化分享

    前言 岁月真是个养猪场,这几年,人胖了,微信代码也翻了。记得 14 年转岗来微信时,用自己笔记本编译微信工程才十来分钟。如今用公司配的 17 年款 27-in...

    微信终端开发团队
  • Android | 资源冲突覆盖的一些思考

    啥是资源冲突覆盖,就是两个不同的文件,有着相同的文件名,在打包apk后引起的系列问题。本文将从情景、解决思路、延伸,三个方面展开。

    Holiday
  • Android | 资源冲突覆盖的一些思考

    啥是资源冲突覆盖,就是两个不同的文件,有着相同的文件名,在打包apk后引起的系列问题。本文将从情景、解决思路、延伸,三个方面展开。

    Holiday
  • 【webpack 性能优化】编译速度从 50S 到 7S

    随着项目不断发展壮大,组件数量开始变得越来越多,项目也开始变得庞大,webpack 编译的时间也会越来越久,我们现在的项目编译一次在 40s ——70s 之间,...

    ConardLi
  • VS小技巧 | Visual Studio 使用插件迅速找出编译速度慢的瓶颈,优化编译速度

    嫌项目编译太慢?不一定是 Visual Studio 的问题,有可能是你项目的引用关系决定这个编译时间真的省不下来。

    Enjoy233
  • 如何优化 Android Studio 启动、编译和运行速度?

    ?作为一名 Android 程序员,选择一个好的 IDE 工具可以使开发变得非常高效,很多程序员喜欢使用 Google 的 Android Studio来进行...

    非著名程序员
  • 编译优化

    风骨散人Chiam
  • Android APP 终极瘦身指南

    APK瘦身即是对APK大小进行压缩策略,减小APK安装包大小,更小的安装包更有助于吸引用户安装;虽然说APK瘦身对于Android对应用可分配内存的限制影响不大...

    Android技术干货分享
  • 深入探索 Android 包瘦身(中)

    作者:jsonchao 链接:https://juejin.im/post/5e7ad1c0e51d450edc0cf053

    陈宇明
  • APK瘦身-是时候给App进行减负了!

    APK瘦身即是对APK大小进行压缩策略,减小APK安装包大小,更小的安装包更有助于吸引用户安装。前一段时间我司某一App进行APK的瘦身,最终也达到了减小10M...

    Android技术干货分享
  • 详解Android Gradle插件3.0挖坑日记

    为了提升编译速度,这几天用上了 AS 3.0 和 Gradle 3.0 插件,不得不说不论是 AS 3.0,还是 Gradle 3.0 都变化非常大,具体的更新...

    砸漏
  • UE4提升编译速度

    UE是一个巨大的工程,在没有increbuild类似工具的情况下,编译会很耗时,以下设置会提升本地的编译速度。

    小伏羲
  • www6663388com请拨18687679362环球国际iOS 微信编译速度优化分享

    岁月真是个养猪场,这几年,人胖了,微信代码也翻了。记得 14 年转岗来微信时,用自己笔记本编译微信工程才十来分钟。如今用公司配的 17 年款 27-inch i...

    用户7106032
  • Android 黑科技 |Gradle Plugin使用场景

    一直想写一些关于安卓plugin的应用场景。只要想法够胆子大,这个能做很多你意想不到的优化点。

    逮虾户
  • 早期(编译期)优化

      相当多新生的java语法特性,都是靠编译器的“语法糖”来实现,而不是依赖虚拟机的底层改进来支持,java中即时编译器地运行期的优化过程对于程序运行来说更重要...

    YGingko
  • GCC编译优化选项

    查看GCC各选项打开的优化项:gcc -Q --help=optimizers。分为如下:

    后台搬砖鹅

扫码关注云+社区

领取腾讯云代金券