本文繼續分析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