本文继续分析booster的实现原理。更多相关文章见booster-分析java
booster-task-compression
这个组件主要作了3件事:android
resourceXX.ap_
文件中的资源在分析它们的实现以前,咱们先来了解一下Android的资源编译过程:git
对于资源编译有哪些步骤我并无找到比较详细官方文档,不过咱们能够经过查看com.android.tools.build:gradle
的源码来了解这个过程。构建一个app
包所涉及的到GradleTask(好比assembleRelease)
的源码大概位于ApplicationTaskMamager.java
文件中:github
ApplicationTaskManager.javaweb
@Override
public void createTasksForVariantScope(final TaskFactory tasks, final VariantScope variantScope) {
BaseVariantData variantData = variantScope.getVariantData();
...
// Create all current streams (dependencies mostly at this point)
createDependencyStreams(tasks, variantScope);
...
// Add a task to process the manifest(s)
recorder.record(
ExecutionType.APP_TASK_MANAGER_CREATE_MERGE_MANIFEST_TASK,
project.getPath(),
variantScope.getFullVariantName(),
() -> createMergeApkManifestsTask(tasks, variantScope));
...
// Add a task to merge the resource folders
recorder.record(
ExecutionType.APP_TASK_MANAGER_CREATE_MERGE_RESOURCES_TASK,
project.getPath(),
variantScope.getFullVariantName(),
(Recorder.VoidBlock) () -> createMergeResourcesTask(tasks, variantScope, true));
// Add a task to merge the asset folders
recorder.record(
ExecutionType.APP_TASK_MANAGER_CREATE_MERGE_ASSETS_TASK,
project.getPath(),
variantScope.getFullVariantName(),
() -> createMergeAssetsTask(tasks, variantScope, null));
recorder.record(
ExecutionType.APP_TASK_MANAGER_CREATE_PROCESS_RES_TASK,
project.getPath(),
variantScope.getFullVariantName(),
() -> {
// Add a task to process the Android Resources and generate source files
createApkProcessResTask(tasks, variantScope);
// Add a task to process the java resources
createProcessJavaResTask(tasks, variantScope);
});
...
}
复制代码
上面我只截取了ApplicationTaskMamager.createTasksForVariantScope()
部分代码,createTasksForVariantScope()
就是用来建立不少Task
来构建一个可运行的App
的。经过这个方法咱们能够看到构建一个App
包含下列步骤:api
Manifest
文件(MergeApkManifestsTask
)res
资源(MergeResourcesTask
)assets
资源(MergeAssetsTask
)_.ap
文件(ApkProcessTesTask
)上面我省略了不少步骤没有列出来。bash
booster资源压缩的实现原理就是建立了一些Task
插入在上面的步骤之间来完成自定义的操做app
这个操做会在app构建
完成MergeResourcesTask
以后进行:ide
//移除冗余资源的 task, 执行位于资源合并以后
val klassRemoveRedundantFlatImages = if (aapt2) RemoveRedundantFlatImages::class else RemoveRedundantImages::class
val reduceRedundancy = variant.project.tasks.create("remove${variant.name.capitalize()}RedundantResources", klassRemoveRedundantFlatImages.java) {
it.outputs.upToDateWhen { false }
it.variant = variant
it.results = results
it.sources = { variant.scope.mergedRes.search(pngFilter) }
}.dependsOn(variant.mergeResourcesTask)
复制代码
即会根据当前不一样的AAPT
版本建立不一样的冗余图片移除任务(操做的图片格式为png
, 但不包括.9.png
)。工具
若是对资源编译采用的是AAPT
,则执行的任务为RemoveRedundantImages
:
open class RemoveRedundantImages: DefaultTask() {
lateinit var variant: BaseVariant
lateinit var results: CompressionResults
lateinit var sources: () -> Collection<File>
@TaskAction
open fun run() {
TODO("Reducing redundant resources without aapt2 enabled has not supported yet")
}
}
复制代码
能够看到RemoveRedundantImages
并无作什么具体的操做。实际上gradle
会在AAPT
资源合并操做以前移除冗余的资源,具体规则是:
默认状况下,
Gradle
会合并同名的资源,如可能位于不一样资源文件夹中的同名可绘制对象。这一行为不受shrinkResources
属性控制,也没法停用,由于当多个资源与代码查询的名称匹配时,有必要利用这一行为来避免错误。只有在两个或更多个文件具备彻底相同的资源名称、类型和限定符时,才会进行资源合并。Gradle
会在重复项中选择它认为最合适的文件(根据下述优先顺序),而且只将这一个资源传递给AAPT
,以便在APK文件中分发。
Gradle
会在如下位置查找重复资源:
- 与主源集关联的主资源,一般位于 src/main/res/。
- 变体叠加,来自编译类型和编译特性。
- 库项目依赖项。
Gradle
会按如下级联优先顺序合并重复资源 : 依赖项 → 主资源 → 编译特性 → 编译类型
更具体的合并规则可查看: 合并重复资源
固然gradle
的资源合并操做是必须的
Android Gradle Plugin 3.0.0
及更高版本默认会启用AAPT2
。相较于AAPT
,AAPT2
会利用增量编译加快app打包过程当中资源的编译。对于AAPT2
更加详细的介绍能够参考 : developer.android.com/studio/comm…
当app
编译使用的是AAPT2
时,booster RemoveRedundantFlatImages
的处理:
internal open class RemoveRedundantFlatImages : RemoveRedundantImages() {
@TaskAction
override fun run() {
val resources = sources().parallelStream().map {
it to it.metadata
}.collect(Collectors.toSet())
resources.groupBy({
it.second.resourceName.substringBeforeLast('/') // 同文件夹下的文件
}, {
it.first to it.second
}).forEach { entry ->
entry.value.groupBy({
it.second.resourceName.substringAfterLast('/')
}, {
it.first to it.second
}).map { group ->
group.value.sortedByDescending {
it.second.config.screenType.density // 按密度降序排序
}.takeLast(group.value.size - 1) //同名文件,取密度最大的
}.flatten().parallelStream().forEach {
try {
if (it.first.delete()) { // 删除冗余的文件
val original = File(it.second.sourcePath)
results.add(CompressionResult(it.first, original.length(), 0, original))
} else {
logger.error("Cannot delete file `${it.first}`")
}
} catch (e: IOException) {
logger.error("Cannot delete file `${it.first}`", e)
}
}
}
}
}
复制代码
RemoveRedundantFlatImages
所作的操做是: 在资源合并后,对于同名的png图片,它会取density
最高的图片,而后把其余的图片删除
好比你有下面3张启动图:
经booster
处理后就会剩下mipmap-xxxhdpi -> ic_launcher.png
这一张图片打包到apk中。
booster
图片压缩的大体实现是:
minSdkVersion > 17
的应用,在资源编译过程当中使用cwebp
命令将图片转为webp
格式。minSdkVersion < 17
的应用,在资源编译过程当中使用pngquant
命令对图片进行压缩。对于这两个工具的详细了资料能够参考下面文章:
webp使用指南 : developers.google.com/speed/webp/…
pngquant使用实践 : juejin.im/entry/587f1…
图片资源的压缩分为两步:
assets
下的图片资源压缩res
下的图片资源压缩这里直接压缩assets下图片资源是存在一些问题的:若是工程中引入了
flutter
,flutter
中对图片资源是明文引用的,booster
将图片转为webp
格式的话会形成flutter
中图片失效。所以这点要注意。
这里就不去跟源码的详细步骤了,由于涉及的点不少。其实主要实现就是建立一个Task, 将图片文件转为webp
以res
的资源压缩为例, 会执行到下面的代码:
nternal open class CwebpCompressImages : CompressImages() {
open fun compress(filter: (File) -> Boolean) {
sources().parallelStream().filter(filter).map { input ->
val output = File(input.absolutePath.substringBeforeLast('.') + ".webp")
ActionData(input, output, listOf(cmdline.executable!!.absolutePath, "-mt", "-quiet", "-q", "80", "-o", output.absolutePath, input.absolutePath))
}.forEach {
val s0 = it.input.length()
val rc = project.exec { spec ->
spec.isIgnoreExitValue = true
spec.commandLine = it.cmdline
}
when (rc.exitValue) {
}
}
}
}
复制代码
cmdline.executable!!.absolutePath
就是表明cwbp
命令的位置。
resourceXX.ap_
文件中的资源这个操做的入口代码是:
class CompressionVariantProcessor : VariantProcessor {
override fun process(variant: BaseVariant) {
variant.processResTask.doLast {
variant.compressProcessedRes(results) //从新压缩.ap_文件
variant.generateReport(results) //生成报告文件
}
...
}
}
复制代码
compressProcessedRes()
的具体实现是:
private fun BaseVariant.compressProcessedRes(results: CompressionResults) {
val files = scope.processedRes.search {
it.name.startsWith("resources") && it.extension == "ap_"
}
files.parallelStream().forEach { ap_ ->
val s0 = ap_.length()
ap_.repack {
!NO_COMPRESS.contains(it.name.substringAfterLast('.'))
}
val s1 = ap_.length()
results.add(CompressionResult(ap_, s0, s1, ap_))
}
}
复制代码
即找到全部的resourcesXX.ap_
文件,而后对他们进行从新压缩打包。ap_.repack
方法实际上是把里面的每一个文件都从新压了一遍(已经压过的就再也不压了):
private fun File.repack(shouldCompress: (ZipEntry) -> Boolean) {
//建立一个新的 .ap_ 文件
val dest = File.createTempFile(SdkConstants.FN_RES_BASE + SdkConstants.RES_QUALIFIER_SEP, SdkConstants.DOT_RES)
ZipOutputStream(dest.outputStream()).use { output ->
ZipFile(this).use { zip ->
zip.entries().asSequence().forEach { origin ->
// .ap_ 中的文件再压缩一遍
val target = ZipEntry(origin.name).apply {
size = origin.size
crc = origin.crc
comment = origin.comment
extra = origin.extra
//若是已经压缩过就再也不压缩了
method = if (shouldCompress(origin)) ZipEntry.DEFLATED else origin.method
}
output.putNextEntry(target)
zip.getInputStream(origin).use {
it.copyTo(output)
}
..
}
}
}
//覆盖掉老的.ap_文件
if (this.delete()) {
if (!dest.renameTo(this)) {
dest.copyTo(this, true)
}
}
}
复制代码
对resourcesXX.ap_
文件的压缩报告以下:
46.49% xxx/processDebugResources/out/resources-debug.ap_ 153,769 330,766 xxx/out/resources-debug.ap_
复制代码
压缩前:391KB , 压缩后:177KB; 即压缩了46.49%
我新建了一个Android
工程,在使用booster
压缩前打出的apk大小为2.8MB
, 压缩后打出的apk大小为2.6MB
。
实际上booster-task-compression
这个组件对于减少apk
的大小仍是有很显著的效果的。不过是不是适用于项目则须要根据项目具体状况来考虑。
更多文章见 : AdvancedAdnroid