booster分析-App資源壓縮

本文繼續分析booster的實現原理。更多相關文章見booster-分析java

booster-task-compression這個組件主要作了3件事:android

  1. 刪除冗餘的圖片資源
  2. 壓縮圖片資源
  3. 從新壓縮resourceXX.ap_文件中的資源

在分析它們的實現以前,咱們先來了解一下Android的資源編譯過程:git

回顧 App 資源編譯步驟

對於資源編譯有哪些步驟我並無找到比較詳細官方文檔,不過咱們能夠經過查看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

  1. 下載依賴
  2. 合併Manifest文件(MergeApkManifestsTask)
  3. 合併res資源(MergeResourcesTask)
  4. 合併assets資源(MergeAssetsTask)
  5. 處理資源,生成_.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 的冗餘資源的移除

若是對資源編譯採用的是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的資源合併操做是必須的

AAPT2 的冗餘資源的移除

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張啓動圖:

  • mipmap-hdpi -> ic_launcher.png
  • mipmap-xhdpi -> ic_launcher.png
  • mipmap-xxxhdpi -> ic_launcher.png

booster處理後就會剩下mipmap-xxxhdpi -> ic_launcher.png這一張圖片打包到apk中。

圖片資源的壓縮

booster圖片壓縮的大體實現是:

  1. 對於minSdkVersion > 17的應用,在資源編譯過程當中使用cwebp命令將圖片轉爲webp格式。
  2. 對於minSdkVersion < 17的應用,在資源編譯過程當中使用pngquant命令對圖片進行壓縮。

對於這兩個工具的詳細了資料能夠參考下面文章:

webp使用指南 : developers.google.com/speed/webp/…

pngquant使用實踐 : juejin.im/entry/587f1…

具體實現

圖片資源的壓縮分爲兩步:

  1. assets下的圖片資源壓縮
  2. 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

相關文章
相關標籤/搜索