91欧美超碰AV自拍|国产成年人性爱视频免费看|亚洲 日韩 欧美一厂二区入|人人看人人爽人人操aV|丝袜美腿视频一区二区在线看|人人操人人爽人人爱|婷婷五月天超碰|97色色欧美亚州A√|另类A√无码精品一级av|欧美特级日韩特级

0
  • 聊天消息
  • 系統消息
  • 評論與回復
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會員中心
創(chuàng)作中心

完善資料讓更多小伙伴認識你,還能領取20積分哦,立即完善>

3天內不再提示

介紹得物App在資源優(yōu)化上做的一些實踐

OSC開源社區(qū) ? 來源:得物技術 ? 2023-07-24 09:00 ? 次閱讀
加入交流群
微信小助手二維碼

掃碼添加小助手

加入工程師交流群

包體積優(yōu)化中,資源優(yōu)化一般都是首要且容易有成效的優(yōu)化方向。資源優(yōu)化是通過優(yōu)化APK中的資源項來優(yōu)化包體積,本文我們會介紹得物App在資源優(yōu)化上做的一些實踐。

1

插件優(yōu)化

插件優(yōu)化資源在得物App最新版本上收益12MB。插件優(yōu)化的日志在包體積平臺有具體的展示,也是為了提供一個資源問題追溯的能力。

270254d0-27b3-11ee-962d-dac502259ad0.png

1.1 插件環(huán)境配置

插件首先會初始化環(huán)境配置,如果機器上未安裝運行環(huán)境則會去oss下載對應的可執(zhí)行文件。

273cbcce-27b3-11ee-962d-dac502259ad0.png

1.2 圖片壓縮

在開發(fā)階段,開發(fā)同學首先會通過TinyPNG等工具主動對圖片進行壓縮,而對于三方庫和一些業(yè)務遺漏處理的圖片則會在打包的時候通過gradle插件進行壓縮。

圖片壓縮插件使用 cwebp 對圖片進行webp轉換,使用 guetzli 對JPEG進行壓縮,使用pngquant對PNG 進行壓縮,使用 gifsicle 對gif進行壓縮。在實施對過程中,對于 res 目錄下的文件優(yōu)先使用 webp 處理,對assets 目錄下的文件則進行同格式壓縮。下面先介紹下資源壓縮插件的工作模式和原理。

1.2.1 Res圖片壓縮

第一步,找到并遍歷 ap_ 文件

278e517e-27b3-11ee-962d-dac502259ad0.png

這里對 ap_ 文件進行一下簡單介紹,ap_ 文件是由 AAPT2 生成的,AAPT2(Android 資源打包工具)是一種構建工具,Android Studio 和 Android Gradle 插件使用它來編譯和打包應用的資源。AAPT2 會解析資源、為資源編制索引,并將資源編譯為針對 Android 平臺進行過優(yōu)化的二進制格式。

AAPT2這個工具在打包過程中主要做了下列工作: 把"assets"和"res/raw"目錄下的所有資源進行打包(會根據不同的文件后綴選擇壓縮或不壓縮),而"res/"目錄下的其他資源進行編譯或者其他處理(具體處理方式視文件后綴不同而不同,例如:".xml"會編譯成二進制文件,".png"文件會進行優(yōu)化等等)后才進行打包; 會對除了assets資源之外所有的資源賦予一個資源ID常量,并且會生成一個資源索引表resources.arsc; 編譯AndroidManifest.xml成二進制的XML文件; 把上面3個步驟中生成結果保存在一個*.ap_文件,并把各個資源ID常量定義在一個 R.java R.txt中;

第二步,解壓 ap_ 文件,找到 res/drawable 、res/mipmap 、res/raw 目錄下的圖片進行壓縮

fun compressImg(imgFile: File): Long {
    if (ImageUtil.isJPG(imgFile) || ImageUtil.isGIF(imgFile) || ImageUtil.isPNG(imgFile)) {
        val lastIndexOf = imgFile.path.lastIndexOf(".")
        if (lastIndexOf < 0) {
            println("compressImg ignore ${imgFile.path}")
            return 0
        }
        val tempFilePath =
                "${imgFile.path.substring(0, lastIndexOf)}_temp${imgFile.path.substring(lastIndexOf)}"


        if (ImageUtil.isJPG(imgFile)) {
            Tools.cmd("guetzli", "--quality 85 ${imgFile.path} $tempFilePath")
        } else if (ImageUtil.isGIF(imgFile)) {
            Tools.cmd("gifsicle", "-O3 --lossy=25 ${imgFile.path} -o $tempFilePath")
        } else if (ImageUtil.isPNG(imgFile)) {
            Tools.cmd(
                    "pngquant",
                    "--skip-if-larger --speed 1 --nofs --strip --force  --quality=75  ${imgFile.path} --output $tempFilePath"
            )
        }
        val oldSize = imgFile.length()
        val tempFile = File(tempFilePath)
        val newSize = tempFile.length()
        return if (newSize in 1 until oldSize) {
            val imgFileName: String = imgFile.path
            if (imgFile.exists()) {
                imgFile.delete()
            }
            tempFile.renameTo(File(imgFileName))
            oldSize - newSize
        } else {
            if (tempFile.exists()) {
                tempFile.delete()
            }
            0L
        }
    }
    return 0
}
圖片的壓縮收益最大,且實施簡單,風險最低,是資源優(yōu)化的首選。

1.2.2Assets圖片壓縮

Assets 圖片壓縮的處理方式與 res 下差不多,區(qū)別僅僅在于掛載的 task 與 壓縮模式不同,Assets 下單資源由于是通過 AssetsManager 按照名稱獲取的,且使用場景不可控,無法明確感知業(yè)務使用對格式是否有要求的前提下,同格式壓縮是相對穩(wěn)妥的方案。

val mergeAssets = project.tasks.getByName("merge${variantName}Assets")
mergeAssets.doLast { task ->
    (task as MergeSourceSetFolders).outputDir.asFileTree.files.filter {
        val originalPath = it.absolutePath.replace(task.outputDir.get().toString() + "/", "")
        val filter = context.compressAssetsExtension.whiteList.contains(originalPath)
        if (filter) {
            println("Assets compress ignore:$originalPath")
        }
        !filter
    }.forEach { file ->
        val originalPath = file.absolutePath.replace(task.outputDir.get().toString() + "/", "")
        val reduceSize = CompressUtil.compressImg(file)
        if (reduceSize > 0) {
            assetsShrinkLength += reduceSize
            assetsList.add("$originalPath => reduce[${byteToSize(reduceSize)}]")
        }
    }
    println("assets optimized:${byteToSize(assetsShrinkLength)}")
}

1.3 資源去重

相較于壓縮,資源的去重需要對arsc文件格式有一點了解。為了便于理解,這里先對arsc二進制文件進行一點簡單的介紹。 resource.arsc文件是Apk打包過程中的產生的一個資源索引文件,它是一個二進制文件,源碼ResourceTypes.h 定義了其數據結構。通過學習resource.arsc文件結構,可以幫助我們深入了解apk包體積優(yōu)化中使用到的 重復資源刪除、資源文件名混淆 技術。

27d385aa-27b3-11ee-962d-dac502259ad0.png

將apk使用AS 打開也能看到resource.arsc中存儲的信息

2800053a-27b3-11ee-962d-dac502259ad0.png

說回到資源去重,去重打原理很簡單,找到資源文件目錄下相同的文件,然后刪除掉重復的文件,最后到 arsc 中修改記錄,將刪除的文件索引名稱進行替換。

由于刪除重復資源在 arsc 中只是對常量池中路徑替換,并沒有刪除 arsc 中的記錄,也沒有修改PackageChunk 中的常量池內容,也就是對應上圖中的 Name 字段,故而重復資源的刪除安全性比較高。

下面介紹下具體實施方案:

第一步遍歷ap文件,通過 crc32 算法找出相同文件。之所以選擇 crc32 是因為 gralde 的 entry file 自帶 crc32 值,不需要進行額外計算,但是 crc32 是有沖突風險的,故而又對 crc32 的重復結果進行 md5 二次校驗。

第二步則是對原始重復文件的刪除

第三步修改 ResourceTableChunk 常量池內容,進行資源重定向

// 查詢重復資源
val groupResources = ZipFile(apFile).groupsResources()
// 獲取
val resourcesFile = File(unZipDir, "resources.arsc")
val md5Map = HashMap>()
val newResouce = FileInputStream(resourcesFile).use { stream ->
    val resouce = ResourceFile.fromInputStream(stream)
    groupResources.asSequence()
        .filter { it.value.size > 1 }
        .map { entry ->
            entry.value.forEach { zipEntry ->
                if (whiteList.isEmpty() || !whiteList.contains(zipEntry.name)) {
                    val file = File(unZipDir, zipEntry.name)
                    MD5Util.computeMD5(file).takeIf { it.isNotEmpty() }?.let {
                        val set = md5Map.getOrDefault(it, HashSet())
                        set.add(zipEntry)
                        md5Map[it] = set
                    }
                }
            }
            md5Map.values
        }
        .filter { it.size > 1 }
        .forEach { collection ->
            // 刪除多余資源
            collection.forEach { it ->
                val zips = it.toTypedArray()
                // 所有的重復資源都指定到這個第一個文件上
                val coreResources = zips[0]
                for (index in 1 until zips.size) {
                    // 重復的資源
                    val repeatZipFile = zips[index]
                    result?.add("${repeatZipFile.name} => ${coreResources.name}    reduce[${byteToSize(repeatZipFile.size)}]")
                    // 刪除解壓的路徑的重復文件
                    File(unZipDir, repeatZipFile.name).delete()
                    // 將這些重復的資源都重定向到同一個文件上
                    resouce
                        .chunks
                        .filterIsInstance()
                        .forEach { chunk ->
                            val stringPoolChunk = chunk.stringPool
                            val index = stringPoolChunk.indexOf(repeatZipFile.name)
                            if (index != -1) {
                                // 進行剔除重復資源
                                stringPoolChunk.setString(index, coreResources.name)
                            }
                        }
                }
            }
        }


    resouce
}

1.4資源混淆

資源混淆則是在資源去重打基礎上更進一步,與代碼混淆的思路一致,用長路徑替換短路徑,一來減小文件名大小,二來降低arsc中常量池中二進制文件大小。

長路徑替換短路徑修改 ResourceTableChunk 即可,與重復資源處理如出一轍。

同時我們發(fā)現 PackageChunk 中常量池中字段還是原來的內容,但是并不影響apk的運行。因為通過getDrawable(R.drawable.xxx)方式加載的資源在編譯后對應的是getDrawable(0x7f08xxxx)這種16進制的內容,其實就是與 arsc 中的 ID 對應,用不上 Name 字段。而通過getResources().getIdentifier()方式調用的我們通過白名單keep住了,Name 字段在這里也是可以移除的。

        val resourcesFile = File(unZipDir, "resources.arsc")
        val newResouce = FileInputStream(resourcesFile).use { inputStream ->
            val resouce = ResourceFile.fromInputStream(inputStream)
            resouce
                .chunks
                .filterIsInstance()
                .forEach { chunk ->
                    val stringPoolChunk = chunk.stringPool
                    // 獲取所有的路徑
                    val strings = stringPoolChunk.getStrings() ?: return@forEach


                    for (index in 0 until stringPoolChunk.stringCount) {
                        val v = strings[index]


                        if (v.startsWith("res")) {
                            if (ignore(v, context.proguardResourcesExtension.whiteList)) {
                                println("resProguard  ignore  $v ")
                                // 把文件移到新的目錄
                                val newPath = v.replaceFirst("res", whiteTempRes)
                                val parent = File("$unZipDir${File.separator}$newPath").parentFile
                                if (!parent.exists()) {
                                    parent.mkdirs()
                                }
                                keeps.add(newPath)
                                // 移動文件
                                File("$unZipDir${File.separator}$v").renameTo(File("$unZipDir${File.separator}$newPath"))
                                continue
                            }
                            // 判斷是否有相同的
                            val newPath = if (mappings[v] == null) {
                                val newPath = createProcessPath(v, builder)
                                // 創(chuàng)建路徑
                                val parent = File("$unZipDir${File.separator}$newPath").parentFile
                                if (!parent.exists()) {
                                    parent.mkdirs()
                                }
                                // 移動文件
                                val isOk =
                                    File("$unZipDir${File.separator}$v").renameTo(File("$unZipDir${File.separator}$newPath"))
                                if (isOk) {
                                    mappings[v] = newPath
                                    newPath
                                } else {
                                    mappings[v] = v
                                    v
                                }
                            } else {
                                mappings[v]
                            }
                            strings[index] = newPath!!
                        }
                    }


                    val str2 = mappings.map {
                        val startIndex = it.key.lastIndexOf("/") + 1
                        var endIndex = it.key.lastIndexOf(".")


                        if (endIndex < 0) {
                            endIndex = it.key.length
                        }
                        if (endIndex < startIndex) {
                            it.key to it.value
                        } else {
//                            val vStartIndex = it.value.lastIndexOf("/") + 1
//                            var vEndIndex = it.value.lastIndexOf(".")
//                            if (vEndIndex < 0) {
//                                vEndIndex = it.value.length
//                            }
//                            val result = it.value.substring(vStartIndex, vEndIndex)
                            // 使用相同的字符串,以減小體積
                            it.key.substring(startIndex, endIndex) to "du"
                        }
                    }.toMap()


                    // 修改 arsc PackageChunk 字段
                    chunk.chunks.values.filterIsInstance()
                        .flatMap { it.chunks.values }
                        .filterIsInstance()
                        .forEach {
                            for (index in 0 until it.stringCount) {
                                it.getStrings()?.forEachIndexed { index, s ->
                                    str2[s]?.let { result ->
                                        it.setString(index, result)
                                    }
                                }
                            }
                        }


                    // 將 mapping 映射成 指定格式文件,供給反混淆服務使用
                    val mMappingWriter: Writer = BufferedWriter(FileWriter(file, false))
                    val packageName = context.proguardResourcesExtension.packageName
                    val pathMappings = mutableMapOf()
                    val idMappings = mutableMapOf()
                    mappings.filter { (t, u) -> t != u }.forEach { (t, u) ->
                        result?.add(" $t => $u")
                        compress[t]?.let {
                            compress[u] = it
                            compress.remove(t)
                        }
                        val pathKey = t.substring(0, t.lastIndexOf("/"))
                        pathMappings[pathKey] = u.substring(0, u.lastIndexOf("/"))
                        val typename = t.split("/")[1].split("-")[0]
                        val path1 = t.substring(t.lastIndexOf("/") + 1, t.indexOf("."))
                        val path2 = u.substring(u.lastIndexOf("/") + 1, u.indexOf("."))
                        val path = "$packageName.R.$typename.$path1"
                        val pathV = "$packageName.R.$typename.$path2"
                        if (idMappings[path].isNullOrEmpty()) {
                            idMappings[path] = pathV
                        }
                    }
                    generalFileResMapping(mMappingWriter, pathMappings)
                    generalResIDMapping(mMappingWriter, idMappings)
                }


            // 刪除res下的文件
            FileOperation.deleteDir(File("$unZipDir${File.separator}res"))
            // 將白名單的文件移回res
            keeps.forEach {
                val newPath = it.replaceFirst(whiteTempRes, "res")
                val parent = File("$unZipDir${File.separator}$newPath").parentFile
                if (!parent.exists()) {
                    parent.mkdirs()
                }
                File("$unZipDir${File.separator}$it").renameTo(File("$unZipDir${File.separator}$newPath"))
            }
            // 收尾刪除 res2
            FileOperation.deleteDir(File("$unZipDir${File.separator}$whiteTempRes"))
            resouce
        }

白名單配置必不可少,保證反射調用資源不參與混淆

createProcessPath 用于將長路徑修改為短路徑

修改 PackageChunk 中的常量池,用于極致的包體裁剪,未壓縮前減小包體300kb,arsc壓縮后降低包體70kb

282ae886-27b3-11ee-962d-dac502259ad0.png

生成資源混淆mapping文件,提供給包體積服務進行資源名稱還原使用

資源混淆的落地過程必須要謹慎,對存量代碼,在得物app中我們先通過字節(jié)碼掃描找出所有反射調用資源的地方,配置keep文件。對于后續(xù)業(yè)務開發(fā)中新增的反射調用則通過測試流程及早發(fā)現問題。

1.5 ARSC壓縮

Arsc 壓縮降低的體積非??捎^,壓縮后的arsc 700kb,未壓縮的約 7MB。實施起來通過 7zip對 arsc文件壓縮即可。

28686652-27b3-11ee-962d-dac502259ad0.png

但是 Target Sdk 在30以上 arsc 壓縮被禁了。壓縮 resources.arsc 雖然能帶來包體上的收益,但也有弊端,它將帶來內存和運行速度上的劣勢。不壓縮的resources.arsc系統可以使用mmap來節(jié)約內存的使用(一個app的資源至少被3個進程所持有:自己, launcher, system),而壓縮的resources.arsc會存在于每個進程中。

2

資源下發(fā)

Apk 中的存量大資源在打包后包體積平臺檢測出來,針對問題資源排期處理。動態(tài)下發(fā)和無用刪除則是處理存量資源的常用手段,同時通過 CI 前置管控新增資源過大的情況。

資源下發(fā)的主體主要是 so 文件和圖片,對下發(fā)的資源的管控則需可以通過平臺化管理。堵不如疏,能下發(fā)的資源就下發(fā)是包體優(yōu)化的一大利器。

288df3fe-27b3-11ee-962d-dac502259ad0.png

下發(fā)的資源通過動態(tài)資源管理平臺進行處理

28d0366a-27b3-11ee-962d-dac502259ad0.png

3

無用資源刪除

無用資源的檢測結合bytex的 resCheck 編譯期 與 matrix-apk-canary smail 掃描的結果,將業(yè)務可以處理的部分在平臺上展示,版本迭代過程中邊迭代邊治理,能夠有效防止無用資源的持續(xù)惡化。

28ff97de-27b3-11ee-962d-dac502259ad0.png

4

總結

本文主要介紹了得物APP資源優(yōu)化做了的一些動作,其中對資源優(yōu)化插件的工作模式進行了重點介紹。當然,對于資源依舊有不少手段可以完善,比如提供高效簡單的 9 圖下發(fā)方案,包體積平臺增加圖片相似度檢測能力、把一些次級的資源通過插件包下發(fā)都是之后可以嘗試的地方。





審核編輯:劉清

聲明:本文內容及配圖由入駐作者撰寫或者入駐合作網站授權轉載。文章觀點僅代表作者本人,不代表電子發(fā)燒友網立場。文章及其配圖僅供工程師學習之用,如有內容侵權或者其他違規(guī)問題,請聯系本站處理。 舉報投訴
  • 處理器
    +關注

    關注

    68

    文章

    20256

    瀏覽量

    252425
  • 二進制
    +關注

    關注

    2

    文章

    809

    瀏覽量

    43043
  • RAW
    RAW
    +關注

    關注

    0

    文章

    21

    瀏覽量

    4208
  • png
    png
    +關注

    關注

    0

    文章

    14

    瀏覽量

    4703

原文標題:得物Android包體積資源優(yōu)化實踐

文章出處:【微信號:OSC開源社區(qū),微信公眾號:OSC開源社區(qū)】歡迎添加關注!文章轉載請注明出處。

收藏 人收藏
加入交流群
微信小助手二維碼

掃碼添加小助手

加入工程師交流群

    評論

    相關推薦
    熱點推薦

    【「Altium Designer 25 電路設計精進實踐」閱讀體驗】+本書概覽與內容特點介紹

    ,比較有參考意義。 本書特點:全彩印刷理論結合實踐,前面介紹理論,后面介紹實踐,尤其是后面的SAM V71可以參考自己做個類似的開發(fā)板。
    發(fā)表于 02-14 15:56

    【「龍芯之光 自主可控處理器設計解析」閱讀體驗】+可測試性設計章節(jié)閱讀與自己的一些感想

    ,也分享下 自己的一些感想。 先介紹了兩個術語DFT可測試性 設計,ATE自動測試設備,DFT目的是測試出制造問題而不是邏輯 bug,因為DFT也是固定設計的測試邏輯。 然后介紹了可控性,客觀性的概念
    發(fā)表于 01-15 23:30

    并聯使用MOS存在一些問題,要怎樣才能避免這些問題?

    并聯使用MOS存在一些問題,那我們要怎樣才能避免這些問題? 首先,器件的致性定要好。
    發(fā)表于 12-10 08:19

    一些神經網絡加速器的設計優(yōu)化方案

    問題介紹 1.利用本地存儲 參考 CPU 的多級存儲,片內增加多級存儲,類似于 Cache ,利用片 Memory 存儲部分數據,做到數據復用,減少訪問 DRAM,越是靠近 ALU 計算
    發(fā)表于 10-31 07:14

    蜂鳥E203的浮點指令集F的一些實現細節(jié)

    周期。 總結 本文介紹的內容是為了完成基礎功能:對蜂鳥E203 RISC-V內核的微架構實現進行優(yōu)化,添加F拓展的過程中的一些記錄。
    發(fā)表于 10-24 08:57

    Vivado浮點數IP核的一些設置注意點

    : 總結 本文介紹的內容是為了完成基礎功能:對蜂鳥E203 RISC-V內核的微架構實現進行優(yōu)化添加F拓展的過程中的一些記錄。
    發(fā)表于 10-24 06:25

    工業(yè)聯網如何促進生產資源優(yōu)化配置

    工業(yè)聯網通過實時數據采集與監(jiān)控、預測性維護、生產流程優(yōu)化、供應鏈協同管理、數據分析與決策支持等核心手段,實現了生產資源的動態(tài)調配與高效利用,顯著提升了制造業(yè)的生產效率、降低了成本,并推動了柔性生產
    的頭像 發(fā)表于 09-30 16:54 ?1364次閱讀

    數字IC設計:方法、技巧與實踐

    了如何在RTL設計中考慮綜合和后端設計的問題;然后,給出了一些最常見的設計實例和代碼;最后,介紹了仿真的相關知識。第5章為邏輯綜合和相關技術。主要介紹了綜合工具的功能和基本使用方法,包括基本的綜合和
    發(fā)表于 05-28 16:06

    HarmonyOS優(yōu)化應用內存占用問題性能優(yōu)化

    、使用生命周期管理優(yōu)化ArkTS內存 組件的生命周期,指的是組件自身的一些可自執(zhí)行的方法,這些方法會在特殊的時間點或遇到一些特殊頁面行為時被自動觸發(fā)而執(zhí)行。 (
    發(fā)表于 05-23 15:35

    HarmonyOS優(yōu)化應用包體積大小問題性能優(yōu)化

    、概述 減小應用包大小是提升應用下載和安裝體驗的重要方式。通過壓縮、精簡或者復用應用中的代碼或資源,可以有效降低應用包體積大小,減少空間占用,從而達到提升應用下載和安裝速度的目的。了解如何
    發(fā)表于 05-20 14:50

    Debian和Ubuntu哪個好一些?

    兼容性對比Debian和Ubuntu哪個好一些,并為您揭示如何通過RAKsmart服務器釋放Linux系統的最大潛能。
    的頭像 發(fā)表于 05-07 10:58 ?1157次閱讀

    FX2LP USB配置GPIF中斷時遇到一些問題,求解決

    你好,我 FX2LP USB 配置 GPIF 中斷時遇到一些問題。 我啟用了 INT4 中斷并從 GPIF 中選擇了源 INT4,然后啟用了 GPIF 完成中斷,但我看不到中斷 4 工作。 我該如何
    發(fā)表于 05-06 08:00

    明遠智睿SSD2351核心板聯網領域的應用實踐

    環(huán)境條件。 明遠智睿SSD2351核心板憑借其豐富接口和強大運算能力,聯網的各個細分領域都有著出色的應用實踐。它為智能家居、工業(yè)聯網、農業(yè)
    發(fā)表于 04-11 11:50

    樹莓派自動化控制項目中的一些潛在應用

    自動化控制項目中的一些潛在應用。之前,我們已經為Arduino平臺探討了相同的話題。我們確定Arduino是個出色的教育工具,但由于一些限制,它無法工業(yè)環(huán)境中完全
    的頭像 發(fā)表于 03-25 09:45 ?625次閱讀
    樹莓派<b class='flag-5'>在</b>自動化控制項目中的<b class='flag-5'>一些</b>潛在應用

    幾種485自動收發(fā)通信電路介紹

    、成本較低等優(yōu)點,工業(yè)農業(yè)民用控制領域使用廣泛。不過類似于max485這種芯片,般需要處理器去分時控制信號的接收和發(fā)送,操作還是麻煩了點點,也會多消耗
    發(fā)表于 03-11 09:20