vivo 遊戲中心包體積最佳化方案與實踐

vivo互联网技术發表於2024-11-18

作者:來自 vivo 網際網路大前端團隊- Ke Jie

介紹 App 包體積最佳化的必要性,遊戲中心 App 在實際最佳化過程中的有效措施,包括一些最佳化建議以及最佳化思路。

一、包體積最佳化的必要性

安裝包大小與下載轉化率的關係大致是成反比的,即安裝包越大,下載轉換率就越差。Google 曾在 2019 的谷歌大會上給出過一個統計結論,包體積體大小每上升 6MB,應用下載轉化率就會下降 1%,在不同地區的表現可能會有所差異。

vivo 遊戲中心包體積最佳化方案與實踐

APK 減少 10MB,在不同國家轉化率增長

(注:資料來自於 googleplaydev:Shrinking APKs, growing installs

二、遊戲中心 APK 組成

vivo 遊戲中心包體積最佳化方案與實踐

APK 包含以下目錄:

  • META-INF/:包含 CERT.SF 、CERT.RSA 簽名檔案、MANIFEST.MF 清單檔案。

  • assets/:包含應用的資源。

  • res/:包含未編譯到 resources.arsc 中的資源。

  • lib/:支援對應 CPU 架構的 so 檔案。

  • resources.arsc:資源索引檔案。

  • classes.dex:可以理解的 dex 檔案就是專案程式碼編譯為 class 檔案後的集合。

  • AndroidManifest.xml:包含核心 Android 清單檔案。此檔案列出了應用的名稱、版本、訪問許可權和引用的庫檔案。

發現佔包體積比較大的主要是 lib、res、assets、resources 這幾個部分,最佳化主要也從這幾個方面入手。

三、包體積檢測工具

Matrix-ApkChecker 作為 Matrix 系統的一部分,是針對 Android 安裝包的分析檢測工具,根據一系列設定好的規則檢測 APK 是否存在特定的問題,並輸出較為詳細的檢測結果報告,用於分析排查問題以及版本追蹤。

配置遊戲中心的 Json,主要檢測 APK 是否經過了資源混淆、不含 Alpha 通道的 PNG 檔案、未經壓縮的檔案型別、冗餘的檔案、無用資源等資訊。

對於生成的檢測檔案進行分析,可以最佳化不少體積。

工具 Matrix Apkcheck 介紹:https://github.com/Tencent/matrix/wiki/Matrix-Android-ApkChecker

四、包體積最佳化措施

4.1 不含 Alpha 通道的 PNG 大圖

vivo 遊戲中心包體積最佳化方案與實踐

專案中存在較多這種型別的圖,可以替換為 JPG 或者 WebP 圖,能減少不少體積。

4.2 程式碼做減法

隨著業務的迭代,很多業務場景是不會再使用了,涉及到相關的資源和類檔案都可以刪除掉,相應的 APK 中 res 和 dex 都會相應減少。遊戲中心這次去掉了些經過迭代後沒有使用的業務場景和資源。

4.3 資原始檔最少化配置

針對內銷的專案,本地的 string.xml 或者 SDK 中的 string.xml 檔案中的多語言,是根本用不到的。這部分資源可以最佳化掉,能減少不少體積。

在 APP 的 build.gradle 中下新增 resConfigs "zh-rCN", "zh-rTW", "zh-rHK"。這樣配置不影響英文、中文、中國臺灣繁體、中國香港繁體語言的展示。

vivo 遊戲中心包體積最佳化方案與實踐

資原始檔最少化配置前

vivo 遊戲中心包體積最佳化方案與實踐

資原始檔最少化配置後

4.4 配置資源最佳化

很多專案為了適配各種尺寸的解析度,同一份資源可能在不同的解析度的目錄下放置了各種檔案,然後現在主流的機型都是 xxh 解析度,遊戲遊戲中心針對了內建的 APK,配置了優先使用"xxhdpi", "night-xxhdpi"。

這麼配置如果 xxhdpi、night-xxhdpi 存在資原始檔,就會優先使用該解析度目錄下檔案,如果不存在則會取原來解析度目錄下子資源,能避免出現資源找不到的情形。

defaultConfig {
        resConfigs isNotBaselineApk ? "" : ["xxhdpi", "night-xxhdpi"]
}

vivo 遊戲中心包體積最佳化方案與實踐

vivo 遊戲中心包體積最佳化方案與實踐

4.5 內建包去除 v1 簽名

同樣對於內建包來說,肯定都是 Android 7 及以上的機型了,可以考慮去掉 v1 簽名。

signingConfigs {
    gameConfig {
        if (isNotBaselineApk) {
            print("v1SigningEnabled true")
            v1SigningEnabled true
        } else {
            print("v1SigningEnabled false")
            v1SigningEnabled false
        }
        v2SigningEnabled true
    }
}

vivo 遊戲中心包體積最佳化方案與實踐

去掉 v1 簽名後,上圖的三個檔案在 APK 中會消失,也能較少 600k 左右的體積。

4.6 動效資原始檔最佳化

發現專案中用了不少的 GIF、Lottie 檔案、SVG 檔案,佔用了很大一部分體積。考慮將這部分替換成更小的動畫檔案,目前遊戲中心接入了 PAG 方案。替換了部分 GIF 圖和 Lottie 檔案。

PAG 檔案採用可擴充套件的二進位制檔案格式,可單檔案整合圖片音訊等資源,匯出相同的 AE 動效內容,在檔案解碼速度和壓縮率上均大幅領先於同型別方案,大約為 Lottie 的 0.5 倍,SVG 的 0.2 倍。

實際上可能由於設計匯出的 Lottie 或者 GIF 不規範,在匯出 PAG 檔案時會提醒最佳化點,實際部分資源的壓縮比率達到了 80~90%,部分動效資源從幾百 K 降到了幾十 K。

具體可以參考 PAG 官網:https://github.com/Tencent/libpag/blob/main/README.zh_CN.md

遊戲中心這邊將比較大的 GIF 圖,較多的 Lottie 圖做過 PAG 替換。

舉例

(1)遊戲中心的榜單排行頁上的頭圖,UI 那邊匯出的符合效果的 GIF 大小為 701K,替換為 PAG 格式後同樣效果的圖大小為 67K,只有原來的 1/10 不到。

vivo 遊戲中心包體積最佳化方案與實踐

(2)遊戲中心的入口空間 Lottie 動效最佳化。

vivo 遊戲中心包體積最佳化方案與實踐

一份 Lottie 動效大概是這樣的,一堆資源問題加上 Json 檔案。像上述動效的整體資源為 112K,同樣的動效格式轉換為 PAG 格式後,資源大小變成 6K,只有原大小的 5%左右。之後新的動效會優先考慮使用 PAG。

4.7 編譯期間最佳化圖片

以遊戲中心 App 為例,圖片資源約佔用了 25%的包體積,因此對圖片壓縮是能立杆見效的方式。

WebP 格式相比傳統的 PNG 、JPG 等圖片壓縮率更高,並且同時支援有損、無損、和透明度。

思路就是在是在 mergeRes 和 processRes 任務之間插入 WebP 壓縮任務,利用 Cwebp 對圖片在編譯期間壓縮。

vivo 遊戲中心包體積最佳化方案與實踐

(注:圖片來源於https://booster.johnsonlee.io/zh/guide/shrinking/png-compression.html#pngquant-provider

已有的解決方法

(1)可以採用滴滴的方案 booster,booster-task-compression-cwebp 。

參考連結:https://github.com/didi/booster

(2)公司內部官網模組也有類似基於 booster 的外掛,基於 booster 提供的 API 實現的圖片壓縮外掛。壓縮過後需要對所有頁面進行一次點檢,防止圖片失真,針對失真的圖片,可以採用白名單的機制。

4.8 動態化載入 so

同樣以遊戲中心為例,so 的佔比達到了 45.1%,可以對使用場景較少和較大的 so 進行動態化載入的策略,在需要使用的場景下載到本地,動態去載入。

使用的場景去服務端下載到本地載入的流程可以由以下流程圖表示。

vivo 遊戲中心包體積最佳化方案與實踐

流程可以歸納為下載、解壓、載入,主要問題就是解決 so 載入問題。

載入 so 庫的傳統做法是使用:

System.loadLibrary(library);

經常會出現 UnsatisfiedLinkError,Relinker 庫能大幅減小報錯的機率:

ReLinker.loadLibrary(context, "mylibrary")

具體可以參考:https://github.com/KeepSafe/ReLinker

按需載入的情形,風險與收益是並存的,有很多情況需要考慮到,比如下載觸發場景、網路環境、載入失敗是否有降級策略等等,也需要做好給使用者的提示互動。

4.9 內建包只放 64 位 so

目前新上市的手機 CPU 架構都是 arm64-v8a, 對應著 ARMV8 架構,所以在打包的時候針對內建專案,只打包 64 位 so 進去。

ndk {
            if ("64" == localMultilib)
                abiFilters "arm64-v8a"
            else if ("32" == localMultilib)
                abiFilters "armeabi"
            else
                abiFilters "armeabi", "arm64-v8a"
        }
//其中localMultilib為配置項變數
 
String localMultilib = getLocalMultilib()
String getLocalMultilib() {
    def propertyKey = "LOCAL_MULTILIB"
    def propertyValue = rootProject.hasProperty(propertyKey) ? rootProject.getProperty(propertyKey) : "both"
    println " --> ${project.name}: $propertyKey[$propertyValue], $propertyKey[${propertyValue.class}]"
    return propertyValue
}

4.10 開啟程式碼混淆、移除無用資源、ProGuard 混淆程式碼

android {
    buildTypes {
        release {
            minifyEnabled true
            shrinkResources true
        }
    }
}

shrinkResources 和 minifyEnabled 必須同時開啟才有效。

特別注意:這裡需要強調一點的是開啟之後無用的資源或者圖片並沒有真正的移除掉,而是用了一個同名的佔位符號。

可以透過 ProGuard 來實現的,ProGuard 會檢測和移除程式碼中未使用的類、欄位、方法和屬性,除此外還可以最佳化位元組碼,移除未使用的程式碼指令,以及用短名稱混淆類、欄位和方法。

proguard-android.txt 是 Android 提供的預設混淆配置檔案,在配置的 Android sdk /tools/proguard 目錄下,proguard-rules.pro 是我們自定義的混淆配置檔案,我們可以將我們自定義的混淆規則放在裡面。

android {
    buildTypes {
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'),'proguard-rules.pro'
        }
    }
}

4.11 R 檔案內聯最佳化

如果我們的 App 架構如下:

vivo 遊戲中心包體積最佳化方案與實踐

編譯打包時每個模組生成的 R 檔案如下:

R_lib1 = R_lib1;
R_lib2 = R_lib2;
R_lib3 = R_lib3;
R_biz1 = R_lib1 + R_lib2 + R_lib3 + R_biz1(biz1本身的R)
R_biz2 = R_lib2 + R_lib3 + R_biz2(biz2本身的R)
R_app = R_lib1 + R_lib2 + R_lib3 + R_biz1 + R_biz2 + R_app(app本身R)

可以看出各個模組的 R 檔案都會包含下層元件的 R 檔案內容,下層的模組生成的 id 除了自己會生成一個 R 檔案外,同時也會在全域性的 R 檔案生成一個,R 檔案的數量同樣會膨脹上升。多模組情況下,會導致 APK 中的 R 檔案將急劇的膨脹,對包體積的影響很大。

由於 App 模組目前的 R 檔案中的資源 ID 全部是 final 的, Java 編譯器在編譯時會將 final 常量進行 inline 內聯操作,將變數替換為常量值,這樣專案中就不存在對於 App 模組 R 檔案的引用了,這樣在程式碼縮減階段,App 模組 R 檔案就會被移除,從而達到包體積最佳化的目的。

基於以上原理,如果我們將 library 模組中的資源 ID 也轉化為常量的話,那麼 library 模組的 R 檔案也可以移除了,這樣就可以有效地減少我們的包體積。

現在有不少開源的 R 檔案內聯方法,比如滴滴開源的 booster 與位元組開源的 bytex 都包含了 R 檔案內聯的外掛。

booster 參考:

https://booster.johnsonlee.io/zh/guide/shrinking/res-index-inlining.html#%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8

bytex 參考:

https://github.com/bytedance/ByteX/blob/master/access-inline-plugin/README-zh.md

五、最佳化效果

5.1 最佳化效果

上述最佳化措施均在遊戲中心實際中採用,以遊戲中心某個相同的版本為例子,前後體積對比如下圖所示:

vivo 遊戲中心包體積最佳化方案與實踐

(1)包體積最佳化的比例達到了 31%,包體積下降了 20M 左右,從長久來說對應用的轉換率可以提升 3%的點左右。

(2)啟動速度相對於未最佳化版本提升 2.2%個點。

5.2 總結

(1)讀者想進行體積最佳化之前,需先分析下 APK 的各個模組佔比,主要針對佔比高的部分進行最佳化,比如:遊戲中心中 lib、res、assets、resources 佔比較高,就針對性的進行了最佳化;

(2)動效方案的切換、so 動態載入、編譯期間圖片最佳化等措施是長久的,相比於未進行最佳化,時間越長可能減少的體積越明顯;

(3)資原始檔最小化配置、配置資源最佳化,簡單且效果顯著;

(4)後續會對 dex 進行進一步探索,目前專案中程式碼基本上都在做加法,越來越複雜,很少有做減法,導致 dex 逐漸增大,目前還在探索怎麼進一步縮小 dex 體積。

相關文章