隨著業(yè)務(wù)的快速迭代,抖音 Android 端包大小爆炸性增長(zhǎng)。包大小直接影響下載轉(zhuǎn)化率、推廣成本、運(yùn)行內(nèi)存、安裝時(shí)間等因素,因此對(duì) apk 進(jìn)行瘦身是一件很有必要且收益很大的事情。apk 主要由 dex、resource、asserts、native libraries 和 meta-data 組成,對(duì)于每個(gè)部分,可以專(zhuān)門(mén)做包大小優(yōu)化。抖音 Android 經(jīng)過(guò)一段時(shí)間的努力,包大小優(yōu)化取得了階段性成果。目前仍在不斷優(yōu)化。
-優(yōu)化前優(yōu)化后的百分比73MB61.5MB15.7%抖音 lite10MB4.9MB51%
資源在 apk 包體積占很大比例,優(yōu)化資源是包體積優(yōu)化的重要組成部分。本文將本著追求極致的原則,詳細(xì)闡述抖音 Android 端對(duì)資源部分的優(yōu)化措施。
2.圖片壓縮2.1 圖片壓縮原理圖片大小計(jì)算公式不壓縮:圖片大小=長(zhǎng) x 寬 x 圖片位深。原始圖像(1920x1080),若每個(gè)像素 32bit 表示(RGBA),那么圖像所需的存儲(chǔ)尺寸 1920x1080x4 = 8294400Byte,大約 8M,很難接受這么大的圖片。因此,我們使用的圖片被壓縮。圖片壓縮采用空間冗余和視覺(jué)冗余原理:
空間冗余利用圖像上各采樣點(diǎn)顏色之間的空間連貫性,將應(yīng)一個(gè)像素存儲(chǔ)的數(shù)據(jù)合并壓縮存儲(chǔ),并在加載壓還原。通常,無(wú)損壓縮使用空間冗余原理。視覺(jué)冗余是指由于生理特性的限制,人類(lèi)視覺(jué)系統(tǒng)對(duì)圖像場(chǎng)的關(guān)注不均勻,人們對(duì)細(xì)微的顏色差異感覺(jué)不明顯。例如,人類(lèi)視覺(jué)的一般分辨率是 26 灰度等級(jí),而一般圖像的量化是 28 灰度等級(jí),即視覺(jué)冗余。通常,有損壓縮利用人類(lèi)視覺(jué)冗余原理,消除人類(lèi)眼睛冗余信息。2.2 優(yōu)勢(shì)抖音 Android 研發(fā)團(tuán)隊(duì)開(kāi)發(fā)了 Gradle 插件 McI ** ge, 在編譯期間hook 資源,開(kāi)源算法 pngquant/guetzli 壓縮,支持 webp和 tinypng 與一些已知方案相比,具有以下優(yōu)點(diǎn):
McI ** ge 現(xiàn)支持 webp 壓縮比高于 tinypng,不過(guò) Android 上 webp 需要兼容,下面會(huì)詳細(xì)介紹;tinypng 不開(kāi)源,每個(gè)賬戶(hù)每月只能免費(fèi)壓縮 500 張;McI ** ge 基于開(kāi)源算法使用的壓縮算法;McI ** ge 不僅能壓縮 module 中的圖片也可以壓縮 jar 和 aar 中圖;McI ** ge 支持壓縮算法的擴(kuò)展,選擇壓縮算法時(shí)更方便擴(kuò)展;與行業(yè)內(nèi)其他方案相比,McI ** ge 還支持包含透明度的壓縮 webp 圖片兼容 aapt2 資源 hook。2.3 收益McI ** ge 支持兩種優(yōu)化方法,不能同時(shí)使用:
Compress,pngquant 壓縮 png 圖片,guetzli 壓縮 jpg 圖片;ConvertWebp,webp 壓縮 pngpng 圖片。webp 壓縮比高于 pngquant、guetzli,所以現(xiàn)在更推薦使用 ConvertWebp 這種壓縮方法。McI ** ge 也用于字節(jié)跳動(dòng)旗下多種產(chǎn)品的圖片壓縮優(yōu)化,收入如下:描述收入抖音-Compress9.5MB抖音-ConvertWebp11.6MB火山-ConvertWebp3.6MBVigo-ConvertWebp4MBVigo aab-Compress1.2MBvigo aab-ConvertWebp3.2MB多閃-ConvertWebp3.5MB
2.4 其他除了壓縮和優(yōu)化圖片,McI ** ge 還提供以下功能:
大圖檢測(cè)app/build/mci ** ge_result 將在目錄下生成 mci ** ge_log.txt 日志文件,除了輸出轉(zhuǎn)換結(jié)果的日志外,還輸出了大像素圖片和大體積圖片,閾值可以是 McI ** geConfig 設(shè)置在內(nèi),方便大圖復(fù)盤(pán)優(yōu)化包大??;還支持編譯階段檢測(cè),直接檢測(cè)到大圖 block 編譯,可及時(shí)提交大圖;壓縮算法易于擴(kuò)展。如果您想訪問(wèn)其他壓縮算法,只需繼承 AbstractTask,實(shí)現(xiàn) ITask 界面中的 work 方法可以;支持多線程壓縮。把所有 task 的執(zhí)行放入線程池中,大大縮短了 mci ** ge 執(zhí)行時(shí)間;增加圖片緩存 cache,進(jìn)一步縮短包裝時(shí)間。mci ** ge 的過(guò)程不到 10s;緩存路徑可配置;可配置壓縮質(zhì)量,以滿(mǎn)足不同壓縮質(zhì)量的需要。緩存文件也將根據(jù)不同的壓縮質(zhì)量進(jìn)行保存和命中;掃描不包含透明通道的圖片到 app/build/mci ** ge_result 目錄下。3.webp 無(wú)侵入性兼容性3.1 tinypng 和 webp 的選擇tinypng 與 webp 哪個(gè)壓縮比更高?網(wǎng)上找不到兩種壓縮算法壓縮比的直接比較,需要更直觀的比較,所以做了以下實(shí)驗(yàn):
通過(guò)不同的算法壓縮對(duì)比掃描項(xiàng)目中 1960 的圖片:描述大小原圖13463.07KBwebp 壓縮4177.18KBtinypng 壓縮6732.18KB
從項(xiàng)目中找到 490 圖片,新建 demo,壓縮圖片后,不同算法更包裝 apk 的大?。?p>描述大小原圖 APK9617.53KBwebp 壓縮 APK3924.06KBtinypng 壓縮 APK5386.80KB通過(guò)這兩組實(shí)驗(yàn)對(duì)比,可以看出 webp 壓縮比優(yōu)于 tinypng 的。以前手動(dòng)使用 。webp 工具壓縮了抖音工程中的所有圖片,包的大小減少了 1.6MB 左右。因此選擇了 Webp 壓縮算法。
3.2 方案選型webp 壓縮算法,相較于 pngquant、guetzli、tinypng,webp 壓縮比較高,所以 webbp 壓縮圖片應(yīng)該是更好的選擇。Android 設(shè)備對(duì) webp 支持存在兼容性問(wèn)題, 4.3 以上完全支持。通過(guò)官方網(wǎng)站,我們知道我們想直接使用透明 webp,minSDK 至少需要 18。
頭條,包括抖音和今日頭條, Android 應(yīng)用,大部分 minSDK 是 16,不能直接使用 webp 圖片,需要做低版本容性。通過(guò)大量的研究,我們找到了三種兼容性的方法:
-具體 提供優(yōu)缺點(diǎn)api 兼容性太簡(jiǎn)單,侵入性太強(qiáng),必須使用特定接口或特定 View 進(jìn)行加載LayoutInflater setFactory 兼容性很容易實(shí)現(xiàn)。它需要針對(duì)所有 I ** geView 及子 View 處理,必須有統(tǒng)一的 Activity、Fregment 的基類(lèi)處理運(yùn)行時(shí) hook 替換系統(tǒng)的關(guān)鍵方法和方法可以實(shí)現(xiàn)無(wú)侵入性的復(fù)雜性
3.3 方案實(shí)現(xiàn)要實(shí)現(xiàn)無(wú)侵入性兼容性,運(yùn)行時(shí) hook 是最好的選擇。但運(yùn)行時(shí) hook 解決以下問(wèn)題:
選擇的 hook 方案要穩(wěn)定可靠;hook 點(diǎn)應(yīng)足夠收斂,以確保所有分析圖片的操作都能滿(mǎn)足預(yù)期。3.3.1 Hook 方案要穩(wěn)定可靠通過(guò)對(duì) Xposed、AndFix、Cydia Substrate、dexposed 等常見(jiàn)的 Android Java hook調(diào)查對(duì)比 方案,dexposed 不需要 root、又能 hook 系統(tǒng)方法的特點(diǎn),最終選擇 dexposed:
dexposed 在 Dalvik 上部相對(duì)穩(wěn)定,只需針對(duì) 4.3 以下手機(jī)版做 hook,沒(méi)有必要考慮版本兼容性和系統(tǒng)升級(jí);通過(guò)內(nèi)部數(shù)據(jù),抖音 4.3 用戶(hù)不多,占用戶(hù)總數(shù)的萬(wàn)分之幾,風(fēng)險(xiǎn)較低。3.3.2 Hook 點(diǎn)要足夠收斂通過(guò)閱讀源代碼,發(fā)現(xiàn)所有圖片都被加載并分析成 Bit ** p 的過(guò)程最終調(diào)用到 Bit ** pFactory 中的方法。比如 I ** geView 的 setI ** geResource() 調(diào)用路徑如下:
I ** geView 的 setI ** geResource 過(guò)程,Bit ** p 是通過(guò) 創(chuàng)建的Bit ** pFactory如 View 的 setBackgroundResource(int resid)源代碼如下:
查閱所有加載圖片的 api,都會(huì)經(jīng)歷 Resources 調(diào)用 getDrawable 的過(guò)程。將調(diào)用到 Drawable 相關(guān)方法,然后通過(guò) Bit ** pFactory 分析不同類(lèi)型的資源(FileByteArrayStreamFileDescriptory)為 Bit ** p??梢酝茢?,Bit ** pFactory 是 Android 系統(tǒng)通過(guò)不同的資源類(lèi)型加載成 Bit ** p 的統(tǒng)一接口,從 Bit ** pFactory 注釋也可以看到:
由于系統(tǒng)加載分析 Bit ** p 的過(guò)程已經(jīng)足夠收斂了,都是通過(guò) Bit ** pFactory因此 Bit ** pFactory 是個(gè)很好的 hook 點(diǎn)。
有穩(wěn)定的 Hook 方案和足夠收斂的 Hook 點(diǎn),方案實(shí)現(xiàn)方便,使用 dexposed 對(duì) Bit ** pFactory 可以替換關(guān)鍵方法。
4.多 DPI 優(yōu)化Android 為了適配各種不同分辨率或者模式的設(shè)備,為開(kāi)發(fā)者設(shè)計(jì)了同一資源多個(gè)配置的資源路徑,app 通過(guò) resource 獲取圖片資源時(shí),根據(jù)設(shè)備配置自動(dòng)加載適當(dāng)?shù)馁Y源,但這些配置的問(wèn)題是高分辨率設(shè)備包含低分辨率無(wú)用圖片或低分辨率設(shè)備包含高分辨率無(wú)用圖片。
一般情況下,針對(duì)國(guó)內(nèi)應(yīng)用市場(chǎng),App 為了減少包的大小,會(huì)選擇市場(chǎng)份額最高的 dpi(google 推薦 xxhdpi)與所有設(shè)備兼容。海外應(yīng)用市場(chǎng) APP,大部分都會(huì)通過(guò) AppBundle 打包上傳至 Google Play,能享受動(dòng)態(tài)分發(fā) dpi 不同分辨率的手機(jī)可以下載不同的功能 dpi 圖片資源,所以我們需要提供多套 dpi 滿(mǎn)足所有設(shè)備。在項(xiàng)目中,只有一套 dpi,有的有很多套 dpi,針對(duì)上述兩種場(chǎng)景,我們?cè)诎b時(shí)合并資源, ** 資源,減少了包大小。
4.1 DPI ** (bundle 打包)在國(guó)內(nèi)項(xiàng)目中,為了減少圖片的占用,市場(chǎng)占用率一般較高的 dpi 適配,如只保留 xxhdpi 分辨率圖片。這導(dǎo)致了市場(chǎng)上 的兩個(gè)問(wèn)題2k如果未來(lái)手機(jī)的主流分辨率是 xxxhdpi,然后修改項(xiàng)目中數(shù)千張圖片的成本會(huì)很高。另一個(gè)問(wèn)題是,公司的許多海外產(chǎn)品都是通過(guò) 制造的AppBundle 打包上傳到 Google Play 的,能夠給不同設(shè)備用戶(hù)下發(fā)不同 dpi 資源。但項(xiàng)目中只有 xxhdpi,仍然下發(fā) xxhdpi 的圖片不能通過(guò)降低 dpi 減小包的大小。在巴西,80%的用戶(hù)使用 xhdpi 和 hdpi 手機(jī),xxhdpi 圖片比 hdpi 占用多了一倍,這部分收入相當(dāng)高。
因此,我們通過(guò)壓縮分辨率將高分辨率圖片降低到低分辨率,項(xiàng)目業(yè)務(wù)只存儲(chǔ)最高 dpi 圖片,打包時(shí)需要 ** 篩選。我們?cè)?hook 圖片壓縮 task,在圖片壓縮之前,獲得所有 ,包括依賴(lài)庫(kù)PNG 圖片,使用 Graphics2D 降低圖的分辨率文件夾中降低圖像分辨率。然后執(zhí)行圖像壓縮 task,防止重新采樣后圖片大小增加。
我們只縮放圖片的分辨率,不降低圖片的采樣率,所以顯示效果沒(méi)有區(qū)別。不同的 dpi 應(yīng)調(diào)整具體分辨率,我們根據(jù) Google 的定義制作了一個(gè)表格:
我們 ** 一張 xxhdpi 的默認(rèn) logo 到所有 dpi,流程如下圖所示,xhdpi 和 mdpi 文件夾下沒(méi)有相應(yīng)的圖片,** ;在 hdpi 有相應(yīng)的圖片,跳過(guò);xxxhdpi 沒(méi)有相應(yīng)的圖片,但為了避免降低圖片精度,不能夾 到更高的分辨率文件** ,跳過(guò)。
最終收益如圖,公司內(nèi)海外產(chǎn)品 TikTok 研發(fā)團(tuán)隊(duì)在使用該方案優(yōu)化時(shí),ldpi 相比 xxhdpi 減少了 2.5M 包大小。同時(shí),低分辨率手機(jī)加載圖片時(shí)直接加載對(duì)應(yīng) dpi 圖片資源,不再需要對(duì)高分辨率圖片進(jìn)行縮放處理,提高了性能。
在 ** 時(shí)需要注意這些問(wèn)題:為了處理包括依賴(lài)庫(kù)中的所有圖片,在資源合并階段進(jìn)行了 ** ,這樣會(huì)導(dǎo)致.cache 目錄的很多路徑下會(huì)多出大量圖片資源,因此這個(gè)插件我們?cè)?CI 上開(kāi)啟,避免本地打包新增大量圖片,提交到代碼倉(cāng)庫(kù)。同時(shí),由于.cache 中被 ** 了多份圖片,需要在 assemble 打包流程中進(jìn)行多 dpi 去重。在 CI 上會(huì)有并發(fā)場(chǎng)景,同時(shí) ** 和壓縮會(huì)導(dǎo)致.cache 目錄下同時(shí)存在 a.png 和 a.webp,出現(xiàn) Duplicated 錯(cuò)誤,因此最后需要掃描刪除同名的.png 文件。
4.2 多 DPI 去重(assemble 打包)針對(duì)普通打包模式(直接產(chǎn)出 apk,比如抖音包),我們可以選擇只保留一份分辨率偏高的的圖片,這樣高分辨率設(shè)備可以拿到合適的圖片、低分辨率設(shè)備通過(guò) Resource 獲取時(shí)會(huì)自動(dòng)進(jìn)行縮放,依然可以保證合理的運(yùn)行內(nèi)存。
多 dpi 圖片可以通過(guò) Android 自帶的 resConfig 去重,但這個(gè)配置只對(duì)資源的 qualifier 去重,比如對(duì)像素密度和屏幕尺寸不會(huì)同時(shí)做去重,抖音使用基于 AndResguard 修改的方式對(duì) drawable 去重,可以定義不同配置的優(yōu)先級(jí)和作用范圍。根據(jù)優(yōu)化配置確保留一份資源,優(yōu)化方式如下圖(灰色數(shù)據(jù)表示會(huì)被刪除):
5.重復(fù)資源合并隨著項(xiàng)目的迭代,項(xiàng)目中難免會(huì)出現(xiàn)相同的資源被重復(fù)添加到資源路徑中,對(duì)于這類(lèi)文件,人工處理肯定是不可行的,可以在打包階段自動(dòng)去重。
抖音選擇在 AndResguard 階段對(duì)所有的資源進(jìn)行分析,對(duì) md5 相同的資源文件保留一份,刪除其余的重復(fù)的文件,然后在 AndResguard 寫(xiě)入 arsc 文件時(shí)進(jìn)行將刪除的資源文件對(duì)應(yīng)的資源路徑指向唯一保留的一份資源文件。優(yōu)化方式如下圖:
下圖是抖音 511 版本接入多 dpi 去重與重復(fù)資源合并功能的優(yōu)化結(jié)果:
6.shrinkResource 嚴(yán)格模式6.1 背景隨著項(xiàng)目的開(kāi)發(fā)迭代,我們會(huì)有許多資源已經(jīng)不再使用了,但仍然存在于項(xiàng)目中。雖然開(kāi)源的字節(jié)碼插件開(kāi)發(fā)平臺(tái) ByteX 開(kāi)發(fā)插件在 ProGuard 之前掃描出一些無(wú)用資源,但因?yàn)檫@一步?jīng)]有經(jīng)過(guò)無(wú)用代碼刪除,因此掃描出的結(jié)果并不全。而 shrinkResources 是 google 官方提供的優(yōu)化此類(lèi)無(wú)用資源的方法,它運(yùn)行在 Proguard 之后,能標(biāo)記所有無(wú)用資源并將其優(yōu)化。
6.2 收益抖音 Android 在開(kāi)啟 shrinkResources 嚴(yán)格模式后,shrink 資源數(shù) 600+,收益大小 0.57MB。
6.3 接入方法shrinkResources 是由 Google 官方提供的工具,因此詳細(xì)的接入方式參考 Google Developer 上的文檔即可。
6.4shrinkResources 原理默認(rèn)情況下,Resource shrink 是 safe 模式的,即其會(huì)幫助我們識(shí)別類(lèi)似val name = String.for ** t("img_%1d", angle + 1)val res = resources.getIdentifier(name, "drawable", packageName)這樣模式的代碼,從而保證我們?cè)诜瓷湔{(diào)用資源文件的時(shí)候,也是能夠安全返回資源的。從源碼來(lái)看,Resource shrink 時(shí)會(huì)幫助我們識(shí)別以下五種情況:
而 Resource shrink 使用了一種最笨但卻最安全的方法去獲取匹配的前綴/后綴字符串,那就是將應(yīng)用中所有的字符串都認(rèn)為是可能的前綴/后綴匹配字符串。
所以這就造成了在安全模式下,不小心被某個(gè)字符串所匹配到的資源,即使沒(méi)有被使用也會(huì)被保留下來(lái)。以我們的項(xiàng)目為例,在 com.ss.android.ugc.aweme.utils.PatternUtils 中,我們有以下代碼:
在安全模式下,這就造成了所有以 tt 開(kāi)頭的無(wú)用資源都不會(huì)被 shrink 掉(這也就是為什么嚴(yán)格模式一開(kāi),ttlive_ 開(kāi)頭的無(wú)用資源那么多的原因)。
而嚴(yán)格模式打開(kāi)后,其作用便是強(qiáng)行關(guān)閉這一段的字符匹配的過(guò)程:
當(dāng)然這也就造成了我們?cè)谑褂?getIdentifier() 的時(shí)候是不安全的,因?yàn)閲?yán)格模式下是不會(huì)匹配任何字符串的,所以在開(kāi)啟嚴(yán)格模式之后,一定要嚴(yán)格檢查所有被 shrink 的資源,是否有自己需要反射的資源!
6.5 shrinkResources 兼容 Dynamic FeatureAppBundle 是 Google 近年來(lái)力推的一個(gè)功能,它能夠讓我們的 apk 按照不同的維度生成下發(fā),也提供了一個(gè)動(dòng)態(tài)下發(fā)功能的方式,Dynamic Feature。但是如果我們?cè)陂_(kāi)啟 Dynamic Feature 之后使用 shrinkResources,則提示以下錯(cuò)誤:
由此看來(lái) Google 官方并不支持 App Bundle 使用 Dynamic Feature 時(shí)使用 shrink resource。在 Google Issue Tracker 上發(fā)現(xiàn)已經(jīng)有人對(duì)此提交過(guò) Issue 了,相關(guān) Issue。而 Google 的回復(fù)也是簡(jiǎn)單粗暴----計(jì)劃中,但是沒(méi)有時(shí)間:
但是正常來(lái)說(shuō),如果做的好的話,我們的 App Bundle 的 Dynamic Feature 模塊是很少會(huì)引用 Master 的資源的,即使有,使用 keep.xml 的方式也能將這種資源給保留下來(lái)。因此,理論上來(lái)說(shuō),單獨(dú)對(duì) Master 模塊進(jìn)行 shrinkResource 并注意反射調(diào)用的話,是沒(méi)多大問(wèn)題的。Dynamic Feature 下檢查 shrinkResources 配置是在 Configuring 階段
因此我們的想法便是在配置階段不開(kāi)啟 shrinkResources 開(kāi)關(guān),而在后面執(zhí)行資源處理任務(wù)的時(shí)候自行插入 shrinkResources 的 Task:
這樣就能在 Dynamic Feature 下開(kāi)啟 shrinkResources 的 Task 了,整個(gè)代碼編寫(xiě)十分簡(jiǎn)單,不到 50 行就能完成:
7.資源混淆(兼容 aab 模式)資源 id 與資源全路徑的映射關(guān)系記錄在 arsc 文件中,app 通過(guò)資源 id 再通過(guò) Resource 獲取對(duì)應(yīng)的資源,所以對(duì)于映射關(guān)系中的資源路徑做名字混淆可以達(dá)到減少包體積的效果。
抖音啟用了微信開(kāi)源的 AndResguard 進(jìn)行資源混淆,在開(kāi)源的基礎(chǔ)上進(jìn)行了增加了 MD5 去、多 DPI 只保留一份資源等優(yōu)化。內(nèi)部有很多海外產(chǎn)品,在上架 Google Play 時(shí)需要走 aab,因此團(tuán)隊(duì)做了資源混淆的 aab 兼容-- aabResguard(開(kāi)源 | AabResGuard: AAB 資源混淆工具),已開(kāi)源。
8.ARSC 瘦身8.1 背景resources.arsc 這個(gè)文件在很多項(xiàng)目中都占用了相當(dāng)多的空間。常見(jiàn)的優(yōu)化方法是使用 AndResGuard 混淆減少文件名及目錄長(zhǎng)度,7z 壓縮,如果有海外產(chǎn)品的話可以動(dòng)態(tài)下發(fā)語(yǔ)言。我們?cè)谧鐾赀@些優(yōu)化后,內(nèi)部有很多海外產(chǎn)品,涉及到多語(yǔ)言的關(guān)系,ARSC 依然很大,我們決定嘗試進(jìn)一步優(yōu)化。經(jīng)過(guò)調(diào)研,最終我們對(duì) 3 個(gè)方面做了優(yōu)化,分別是刪除無(wú)用 Name、合并字符串池中重復(fù)字符串、刪除無(wú)用文案,最終帶來(lái)的收益是 1.6MB。在此之前,我們還在 AndResGuard 的基礎(chǔ)上完成了重復(fù) MD5 文件圖片合并,原理是一樣的。
8.2 原理先貼一張 arsc 結(jié)構(gòu)的圖,這個(gè)二進(jìn)制文件的數(shù)據(jù)結(jié)構(gòu)相當(dāng)復(fù)雜,AndResGuard 其實(shí)只修改了這個(gè)文件的一小部分,至于更多的修改就無(wú)能為力了,于是我們自己解析了這個(gè)文件進(jìn)行分析。網(wǎng)上也有不少關(guān)于這個(gè)文件格式的說(shuō)明,這里就不贅述了。推薦老羅和尼古拉斯的博客以及 aapt2 源碼。google 提供的 android-arscblamer 和 apktool 的代碼也值得一看。
下面用一張圖簡(jiǎn)單描述一下修改過(guò)程:
如圖,字符串其實(shí)是通過(guò)索引的方式來(lái)獲取的,所有字符串都保存在兩個(gè)字符串池中(單個(gè) package),一個(gè)是全局字符串池,一個(gè)是 package 下的字符串池,我們只需要修改指向全局字符串的偏移值就行了。name 和 value 所在二進(jìn)制位置如下圖。
8.3 方案8.3.1 刪除無(wú)用 NameAndResGuard 在今年的 7 月也增加了這個(gè)功能,我們來(lái)看一下實(shí)現(xiàn)原理。Name 對(duì)應(yīng)的字符串池是 package 字符串池,由于這個(gè)字符串池中只包含所有 Name,我們操作可以稍微暴力一點(diǎn),先做一份備份,然后清空字符串池,添加一個(gè)用于替換的字符串,賦值為 [name_removed]。
首先要確定哪些 name 是通過(guò) getIdentifier 調(diào)用,配置成白名單。遍歷 name 項(xiàng),如果不在白名單,那么把這一個(gè) name 的偏移替換成 0,使其指向[name_removed]。如果 name 在白名單,那么不應(yīng)該刪除,我們通過(guò)備份的字符串池找到這個(gè) name 對(duì)應(yīng)的字符串,添加到字符串池中,把偏移指向?qū)?yīng)下標(biāo)即可。
抖音通過(guò)這個(gè)優(yōu)化減少了包大小 70k。
8.3.2 合并重復(fù)字符串value 所對(duì)應(yīng)的是全局字符串池,雖然名字聽(tīng)起來(lái)不會(huì)有重復(fù)值,但在我們掃描排序后發(fā)現(xiàn)其實(shí)有很多重復(fù)字符串(用 AppBundle 打包就不會(huì)存在這個(gè)問(wèn)題) 在抖音項(xiàng)目中,這個(gè)字符串池里有 1k+個(gè)重復(fù)字符串,合并這些字符串是非常必要的。
我們先遍歷所有數(shù)據(jù),然后把字符串池的重復(fù)字符串合并,記錄偏移的修改,最后把需要修改的 value 的引用指向新的偏移。這個(gè)過(guò)程需要操作 arsc 數(shù)據(jù)結(jié)構(gòu)的 ResValuel 和 ResTableMap,以保證所有 string 類(lèi)型的值都能得到替換。
抖音通過(guò)這個(gè)優(yōu)化減少了包大小 30k。
8.3.3 刪除無(wú)用文案在打包過(guò)程中,其實(shí)所有 strings.xml 中保存的字符串都是不會(huì)被優(yōu)化的,隨著項(xiàng)目逐漸變大,一些廢棄文案或者下個(gè)版本才有用的文案被引入了 apk 中,我們?cè)?Proguard 后再次掃描,發(fā)現(xiàn)了 3000+個(gè)無(wú)用字符串。內(nèi)部的一些海外項(xiàng)目中,有的文案被翻譯成 100 多個(gè)國(guó)家的語(yǔ)言,占用了極大的空間。
刪除的方法和上面類(lèi)似,都是指向替換的字符串所在偏移。如圖可能會(huì)存在兩個(gè)不同 name 指向同一個(gè)字符串,需要判斷待刪除的字符串是否還有其他引用。
不同項(xiàng)目收益可能不太一樣,公司內(nèi)部海外項(xiàng)目對(duì)這些無(wú)用文案進(jìn)行了替換,減少了 1.5M 包大小左右。
8.4 實(shí)現(xiàn)如果是普通的 assemble 打包,直接在 ProcessResources 過(guò)程中獲取 ap_文件中的 arsc 文件,利用我們的工具修改即可。
如果是 AppBundle 方式打包,修改 ap_是沒(méi)有用的,因?yàn)樽詈螽a(chǎn)物是用 aapt 以 proto 格式生成的 resources.pb 文件,要修改只能 hook aapt 過(guò)程。這個(gè)文件和 arsc 文件結(jié)構(gòu)不太一樣,好在我們可以使用官方提供的 Resources 類(lèi)解析、生成 pb 文件,使用相似的方法修改即可。
修改效果如圖:
8.5 進(jìn)一步優(yōu)化arsc 中的偏移數(shù)組是有優(yōu)化空間的,我們會(huì)在未來(lái)嘗試進(jìn)行優(yōu)化。用二進(jìn)制編輯器打開(kāi) arsc 文件可以發(fā)現(xiàn),這樣的 FF 值在文件中大量存在。
是什么導(dǎo)致了這樣的空間浪費(fèi)?我們可以看到下圖中框選的空白,每一個(gè)都代表了其字符串所在的偏移值,這里并沒(méi)有值,賦值 FF FF FF FF 作為默認(rèn)偏移值,浪費(fèi)了 4 字節(jié)空間。某些列(configuration)可能就只有幾個(gè)格子有值,如圖抖音中 drawable 有 4k+張圖片,有 24 列,大多數(shù) configuration 只有幾張圖片,因此浪費(fèi)了 4k*23*4≈380k。大致估算,抖音可以減少 1M 體積。(壓縮前)
如下圖 facebook 針對(duì) arsc 文件的處理,我們可以把一行只有一個(gè)值的 id 抽出來(lái),單獨(dú)放到一個(gè) Resource Type 中,每一個(gè) id 只有一個(gè)值,避免了上述空間浪費(fèi)情況。但這樣做修改了 ID,因此對(duì)應(yīng)的代碼中的 ID 也要修改,涉及了逆向 xml 以及 dex,提高了修改成本。還有一種思路是修改 aapt 源碼,沒(méi)有直接改 arsc 靈活。
原創(chuàng):抖音Android團(tuán)隊(duì),分享鏈接
抖音包大小優(yōu)化-資源優(yōu)化9.總結(jié)上述就是我們抖音 Android 端在包大小優(yōu)化方面針對(duì)資源做的一些嘗試和積累,力求追求極致。
我們針對(duì)包大小優(yōu)化,在其他方面還做了很多優(yōu)化措施:針對(duì) so 優(yōu)化,做了 so 合并、stl 版本統(tǒng)一、精簡(jiǎn)導(dǎo)出符號(hào)表和 so 壓縮等措施;針對(duì)代碼優(yōu)化,細(xì)化混淆規(guī)則,開(kāi)發(fā) bytex 插件進(jìn)行無(wú)用代碼掃描、acess 方法內(nèi)聯(lián)、getter/setter 方法內(nèi)聯(lián)、刪除行號(hào)等優(yōu)化措施。
除了優(yōu)化措施,良好的包大小監(jiān)控系統(tǒng)是防止包大小劣化最重要的工具,否則包大小優(yōu)化措施取得的收益抵不過(guò)業(yè)務(wù)快速迭代帶來(lái)的包大小增長(zhǎng)。抖音 Android 端結(jié)合 CI、Cony 平臺(tái),開(kāi)發(fā)出了一套代碼合入前置檢查系統(tǒng),每個(gè)分支增量超過(guò)閾值不準(zhǔn)合入;還開(kāi)發(fā)了分業(yè)務(wù)線監(jiān)控包大小的工具,便于監(jiān)控每個(gè)業(yè)務(wù)線包大小增長(zhǎng)和給各個(gè)業(yè)務(wù)線定包大小指標(biāo)。