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 执行的顺序是bundle$variantName之后马上执行一个package${variantName}Bundle

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

这里科普个小姿势,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,R文件。

同时我又做了个大胆的实验,如果我把混淆的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<String>()
        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项目而已。而且由于是并发任务,所以其实速度会更快一点。

作者:究极逮虾户
链接:https://juejin.im/post/6866966991338242061

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容