背景
当前项目内用了腾讯的AndResGuard对资源文件的大小进行了一次深度优化。AndResGuard负责将文件名,arsc文件和R文件也进行了一次混淆,能把整体的资源文件大小压缩。
但是奈何也不是一个尽善尽美的方案,所以我们打算在其基础上进行一次二次开发。
AndResGuard原理
我先简单的介绍下AndResGuard(后面简称ARG)是原理。
首先我们需要先编译我们的app项目,等到所有编译流程走完之后生成apk文件,然后ARG会去将apk文件解压并拷贝一份副本,之后从副本中把arsc以及其他的资源文件进行混淆重命名文件等操作,最后再把这个副本重新打包成apk,然后对apk进行重签名等操作。
只有了解了完整的ARG的流程之后,我们才可以对其进行二次开发和二次优化。首先当然先是设立目标了,我们要做什么,然后可以怎么做?
TODO
我们打算做些什么?
-
是不是能将混淆的流程放到apk编译流程中,充分的利用编译时多线程的能力呢?
-
是不是可以对混淆的规则进行二次调整,从而达到压缩比例的提升。
-
有没有办法节省一下编译速度的问题,提升插件的效率。
ACTION
在开发之前,肯定是要先进行方案梳理还有竞品分析的,先找找有没有什么竞品可以帮助我们。
我们在调研的过程中,美团,腾讯,头条都有对应的资源文件的混淆方案。其中腾讯的就是ARG,而ARG也是使用最多的。而美团貌似也没有找到开源项目所以没有后续的跟进。而头条的AabResGuard主要是肩负了头条的App Bundle的压缩,同时也做了普通的资源混淆。朋友说出海项目app bundle的压缩主要是靠这个。
我们参考了
如何更改编译任务的执行顺序
在对Aab的代码分析过程中,我们其实发现了一些很神奇很微妙的点,对于我们后续的优化产生了重大的启发。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | 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在构造一个混淆任务的时候篡改的任务执行的依赖顺序。
一个普通的安卓app Bundle 执行的顺序是
而aab的plugin则是在其中过程中插入了一个自定义的混淆task,也就是上述代码中的
这里科普个小姿势,gradle task的任务顺序是通过有向无环图(DAG)的数据结构进行排序的,所以当任务之间有依赖关系的情况下,gradle会根据DAG的排序顺序执行。基本上如果有任意出现dependsOn的你都可以简单的把他们理解为DAG。
观察一个项目编译的流程
有时候会有同学说,面试的时候问什么编译流程吗,真实开发中完全不会用到呀。但是有时候多个技能也没啥不好的呀。
还是用了之前打印Task耗时的一段代码逻辑,将一个Apk编译的task进行了打印。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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变种,但是并不影响任务。其中混进的
为什么要选择这个节点?
当我们编译一个apk的时候,会在
我们可以先看下aapt编译的大概的一个过程,最后我发现了一个有意思的目录
同时我又做了个大胆的实验,如果我把混淆的ap_放在这里,然后覆盖同名文件。那么会不会在后续编译出来的apk就是一个混淆过的apk呢?
而实验结果也正如我所推测的是一样的,最后编译出来的apk就是一个混淆过的apk。
这里要留一些小遗憾了,我本来想把整个编译流程的Task源代码摸一摸的,但是尝试性的看了下这部分源代码,但是奈何太难了而且debug成本太高了,所以我也没有仔细看懂。
第一个任务完成
从上述流程走通之后,我们只要把ARG的代码进行二次开发,根据对应task任务进行优化,这样我们的第一个任务也就完成了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | 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