有道詞典Android客户端包體積優化之路

語言: CN / TW / HK

摘要

有道詞典已經發展成一個綜合性的學習平台,小巧快速的初心仍然指引着我們不斷進行啟動速度及包體積優化。本文主要介紹了包體積包含的內容以及優化方法,分析了技術實現的具體細節,在接下來的工作中,我們會對啟動速度、安裝包體積以及內存佔用等多方面進行持續優化,歡迎大家關注!

youdao

一、背景

ydtech

有道詞典從移動互聯網之初就憑藉小巧快速、功能強大的印象讓用户愛上翻譯查詞,愛上學習。隨着業務不斷地迭代以及功能不斷完善,有道詞典不再是單純的查詞軟件,而是變成了用户的綜合學習平台。我們探索過社區、問答、直播、信息流等業務,目前也承載着音頻、視頻、課程、背單詞、寫作批改等等的功能。詞典已經發展成為一個綜合性的學習平台,小巧快速的初心仍然指引着我們不斷進行啟動速度以及包體積優化。

經過了不斷的性能優化,目前我們的冷啟動時間已經能維持在業界標準水平3s以內。我們近一個季度主要的性能優化工作集中在安裝包體積優化上面。經過一系列的努力,我們包體積減少了23.7%,安裝包體積從177MB減少到135MB,整體少了42MB。

以下詳細介紹我們的分析以及實現細節。

youdao

二、分析

ydtech

介紹下包體積包含的內容以及優化方法概述。

一般的APK安裝包包含了以下一些目錄和資源:(排序)

META-INF/ 簽名文件

assets/ 程序使用的輔助資源文件

res/ 沒有編譯進入resources.arsc 資源文件,一般是圖片

lib/ 依賴的不同native平台的庫文件

resource.arsc 編譯之後的文案、色值、大小、主題等資源索引

classes.dex 編譯後的代碼

AndroidMenifest.xml 應用的名稱、版本、訪問權限和引用的庫文件信息

可以看出佔比較大的部分主要是分別是assets/、lib/、res/、classes.dex以及resources.arsc,大概對應的就是資源、庫文件、代碼以及資源索引。我們主要的優化思路如下(其中藍色框部分為目前已經處理部分):

youdao

三、技術實現細節

ydtech

3.1

圖片壓縮

在APK打包的過程中,aapt 工具會默認對圖片進行無損壓縮,不過默認的壓縮並不能達到一個很好的壓縮效果,經過了對比webp以及tinypng的壓縮效果,我們最終選擇了使用tinypng對圖片進行壓縮。並且我們編寫了編譯工具,對圖片進行自動化壓縮。

有損webp > tinypng > 無損webp

比如這張啟動圖,原大小724KB,壓到75%左右的質量只有23.7KB。效果上有一點點差異,但可以接受。那麼我們是否可以把全部png圖壓成有損webp呢?答案是否定的,可以看看下面的例子:

壓縮前:

壓縮後:


可以看到,相同的壓縮質量下(75%),這個圖就變得十分模糊,哪怕選擇到了99%的壓縮質量,漸變區域依然會出現一些沒有自然過渡的條紋。

對於上述的情況,用tinypng方案更好

原圖:643KB,

tinypng: 152KB,

webp:339KB

綜上,對於有損webp,無法找到一個固定的壓縮質量來適配所有場景。有損webp有些時候甚至比tinypng還大,但顯示質量更差。

我們最初使用的抖音的McImage插件對圖片進行處理,不過這個方案存在一些明顯的問題:

1.方案採用有損webp,有損webp無法定一個通用的壓縮質量適應所有場景。

2.每次打包都要對所有圖進行壓縮,嚴重影響迭代效率。打包機要40分鐘,且經常OOM。

3.沒有對assets目錄的圖片進行處理。

針對以上問題,我們自己開發了一套使用tinypng的自動化圖片壓縮工具,做出以下調整:

1.對於大圖png,用手工壓成有損webp。收益大,且風險可控。

2.對於非大圖,開發了一個image-optimization插件進行壓縮。該插件方案為:

· png轉tinypng。雖然是有損的,但從抽樣來看,肉眼完全看不到明顯變化。

· 對assets進行處理。assets內有前端png圖,轉tinypng不轉webp的好處是不需要單獨改html、js等文件,且對低版本系統兼容性更友好。flutter相關項目的flutter_assets圖片比較大且沒注意壓縮。插件統一處理可以不需要打開flutter工程單獨優化、重新打包。

· 對於已壓縮的圖片,做緩存處理,不需要重新壓縮,打包的時候動態替換。壓縮緩存跟隨詞典工程提交到gitlab統一管理。

以下是我們圖片自動化壓縮插件處理的流程圖:

這裏壓縮圖是否可用判斷,主要是大小判斷,如果壓出來比原圖大,那麼將捨棄。比如crunchPng壓縮就存在這種情況。

附加1:

因為已經用了tinypng統一壓縮,那麼google官方自帶的crunchPng建議關閉,否則打包速度變慢,而且優化好的圖片也可能又變大,加入這行即可:

buildTypes.all { 
  isCrunchPngs = false
}

附加2:

無損webp和tinypng對比

如圖所示,全量換tinypng比全量換webp(包含assets)少7.7MB。如果考慮到assets內的14.7MB其實是不能簡單換webp的,差距會更大。

附加3:

tinypng已經是最好的方案嗎?

參考另一個ImageOptim工具,它結合OptiPNG, PNGCrush, AdvanceComp, PNGOUT, Jpegoptim + Jpegtran, 和 Gifsicle 等幾個工具提供最好的優化效果,而且是幾乎無損的。對於小部分圖片ImageOptim壓出來小,看起來沒有差別。不過壓縮速度非常慢。

所以,如果做到極致的話,可以進行多種壓縮方案,選最佳的圖作為替換。且我們的image-optimization插件從一開始設計的時候就預留了這種可擴展性。

附加4:

AndResGuard優化對比

試了一下效果不明顯,且出現部分資源丟失而崩潰的情況。效果不明顯的原因,猜測是目前R8對資源名也有混淆壓縮(以前proguard沒有),所以AndResGuard現在的作用比較微弱。至於7zip的壓縮沒有開,理論上會導致啟動速度變慢,覺得得不償失(另外會導致Google Pay的Patch優化算法失效)。

3.2

resources.arsc優化

· 語言包優化

打開resources.arsc的string,我們可以看到如下表格,會發現大量空的地方(如上圖)。這些空白的地方,其實是用FF FF FF…字符進行佔位的,佔用了很多空間(如下圖)。由於有道詞典沒有進行國際化翻譯(有一個國際化版本叫U-Dictionary,歡迎支持),因此刪掉不必要的語言版本有助於減少體積。

android {
    defaultConfig {
        resConfigs "zh"
    }
}


· 如上所示,增加一行,保留中文即可。收穫比想象中大,直接減少了3MB。

· dimens優化查看了最近幾個版本的arsc體積,發現有一個版本增加了5MB。

· 在這個版本我們做了平板適配功能,由於我們採用的是SmallestWith限定符適配方案(可以先了解下這個屏幕適配方案),因此產生大量的尺寸資源。


一共是有3000多個資源,每一個資源有“values-sw300dp”到"values-sw1200dp"共90個版本,這塊存在較大的優化空間。

sqb_px_xx”這一項是用於字體適配的,但詞典用到最大的字體是“sqb_px_144”,所以優化了生成規則,減少了這一類資源。

優化後,資源數量由3012變成1662,減少了近一半。直接減少了2.5MB。

3.3

業務代碼的消除

由於Proguard以及lint等工具是從代碼引用的角度進行分析和代碼裁剪,如果一些廢棄的代碼不先進行刪除會影響後續工作的效果。對於一些已經廢棄沒有入口的業務,不進行處理的話那麼代碼、資源會只增不減。業務刪減應該是所有包體積流程的第一步,否則後面的去掉無用資源、圖片壓縮、混淆等等效果都要打一個折扣。如果時間有限的話,那麼刪最近的需求會比刪遠古時代的需求收益會大點,原因是越靠近現在的項目,圖片資源、字體資源,以及用到so庫都會比較大(尤其是音視頻)。

這部分工作主要是對業務功能的整理以及溝通部分陳舊業務是否可以進行刪除,除此之外就是需要細緻的引用分析將廢棄業務相關代碼剝離出來進行刪除。

一個良好的項目架構對於日後業務代碼的剝離有很大好處。目前新開發的功能我們採用的是分層分模塊的組織架構,功能模塊之間不存在相互依賴,因此以後對於業務的抽離或者刪除會更加方便。


3.4

無用資源刪除

對於無用資源刪除我們主要使用了兩個方法,一個是通過 lint 工具找到應用中可能沒有使用的資源並逐一進行判斷確認沒有使用後進行刪除,第二個是在build.gradle文件中加入shrinkResources在編譯階段使用R8工具進行刪除。

buildTypes {
        release {
            // Zipalign優化
            zipAlignEnabled true
            // 移除無用的resource文件
            shrinkResources true
            // 移除沒用的代碼
            minifyEnabled true
        }
}


使用 lint 工具需要注意對以下一些場景進行再次判斷確認

1. 對於反射性引用資源,可能會被識別成無用資源,比如push用到的通知欄icon

2. DataBinding用到的layout資源會被識別成無用資源

3.4

壓縮混淆

使用R8工具在編譯階段對代碼進行壓縮混淆,從而達到壓縮安裝包體積的效果。主要分為以下4個步驟:

1. 壓縮(shrink) 移除未使用的類、方法、字段等;

2. 優化(optimize) 優化字節碼、簡化代碼等操作;

3. 混淆(obfuscate) 使用簡短的、無意義的名稱重命名類名、方法名、字段等;

4. 預校驗(preverify) 為class添加預校驗信息。

我們在兩年前就引入了Proguard,不過考慮到混淆帶來的問題使用了-dontobfuscate配置取消混淆。我們發現之前的規則中從依賴庫中繼承了 -dontoptimize 的配置導致優化也沒有生效。這次優化中,我們全面解決了混淆帶來的眾多問題,全面開啟了優化以及混淆。

由於我們之前已經開啟過了壓縮,因此需要使用到的類已經在proguard中進行了保留。開啟混淆後還需要處理以下一些問題:

· getIdentifier 通過名稱獲取資源問題。如果是普通模式,則會自動不去掉相關資源:

· 檢查Resources.getValue 相關邏輯

· 檢查AssetManager.open相關邏輯

· 反射,全局搜一下反射包,修改相關位置 java.lang.reflect

· 處理Retrofit報錯問題(https://github.com/square/retrofit/issues/3588),目前使用升級Gradle插件版本進行解決

Caused by: java.lang.IllegalArgumentException: Method return type must not include a type variable or wildcard: ho8<su3<?>>
    
for method CheckInApi.popupConfig
    at retrofit2.Utils.methodError(SourceFile:5)
    at retrofit2.Utils.methodError(SourceFile:1)
    at retrofit2.ServiceMethod.parseAnnotations(SourceFile:7)
    at retrofit2.Retrofit.loadServiceMethod(SourceFile:4)
    at retrofit2.Retrofit$1.invoke(SourceFile:6)
    at java.lang.reflect.Proxy.invoke(Proxy.java:1006)
    at $Proxy23.popupConfig(Unknown Source)
    at com.youdao.dict.checkin.CheckInPopupManager.requestPopupConfig(SourceFile:3)
    at java.lang.reflect.Method.invoke(Native Method)


Proguard的規則會很大程度上影響R8對代碼壓縮和混淆帶來的效果,因此對壓縮規則的回顧以及整理可以幫助進一步的體積壓縮。

3.6

字體優化

字體優化這部分是在之前的版本已經實現過的,取得的效果也挺明顯,這裏補充説明一下。

· 字體裁剪

一般的字體庫大小會有十幾二十兆。但實際上用到的字符只有很少一部分,因此針對實際的使用場景對字體庫進行適當的裁剪,收益非常大。

常用字列表:https://github.com/DavidSheh/CommonChineseCharacter

字體壓縮工具:https://github.com/forJrking/FontZip

· 字體合併

一般來説,我們開發都會模塊化,不同的團隊採用在開發不同功能的時候,有可能用到相同的字體。如果稍不注意就會複製成兩份、三份,文件大大增加。詞典這邊的方案是把共有的字體下沉到底層core基礎庫,供各個模塊引用。

youdao

四、展望

ydtech

經過了上述的工作,目前詞典的安裝包體積優化了23.7%,整體減少了42MB。在接下來的Q2,我們將準備做兩方面的事情。

4.1

包體積監控

在包體積優化的過程中,我們在含辛茹苦地砍掉一點體積之後,轉過頭來發現別的同學又隨隨便便扔進去幾MB的大圖。因此,如何堅守勝利的果實,讓包體積保持最佳狀態成了重中之重。

打包任務增加了是否檢查包大小限制(默認都要檢查) 的選項;merge request之後,詞典的打包任務會觸發自動構建;

打包任務完成之後,如果需要檢查包大小,那就開始觸發apkcheck步驟;具體如下:

1. 打包任務完成之後增加腳本操作,把本次構建的數據(如apk文件地址,mapping文件地址,R文本地址等)寫入臨時文件;

2. 打包任務構建後操作增加 Trigger parameterized build on other projects,觸發apk 大小檢查任務;

3. 開始檢查流程,檢查流程根據參數對apk進行檢查任務,並且把任務結果生成html;

4.2

動態分發

· 整體業務分發

可以使用插件化以及動態加載等技術,不過這些可能不是最難的,最難的是如何把一些祖傳的、低頻的、而又相互依賴的代碼抽離出來,形成獨立模塊去做分發、動態加載。

· 業務子功能分發(預計可優化39.6MB)

1. 數據庫(單詞鎖屏8MB)

單詞鎖屏可以保留幾百kb數據在本地讓用户備用,同時再下載完整的詞庫。

2. OCR引擎數據(22.5MB)

用户應該可以按需下載訓練模型,而不是直接內置;當沒有訓練模型的時候,可以直接網絡請求。

3. 字體(9.1MB)

除了查詞等高頻業務,低頻業務的字體可以動態分發,有則顯示,無則使用系統的即可。但emoji的兼容庫比較特殊,主要用在首頁信息流的帖子、UGC發帖等。如果沒有兼容庫,用户在遇到特別的emoji中可能會顯示“豆腐塊”,這個時候如果emoji字體庫還沒下載完成,需要進行替換兜底處理。另外哪怕用户系統已經有這個emoji內置字體,也有可能顯示效果各個手機不太一樣,需要跟UI確認一下是要替換掉,還是暫時這樣顯示。

保持住詞典小巧快速、功能強大的初心是我們不停進行性能優化的動力,在接下來的工作中,我們會對啟動速度、安裝包體積以及內存佔用等多方面進行持續優化和改進,歡迎大家繼續關注和支持!

youdao

五、參考

ydtech

1. Reduce your app size

2. Shrink, obfuscate, and optimize your app

3. 抖音圖片壓縮插件McImage

4. 騰訊包體積監控ApkChecker


往期推薦

本文分享自微信公眾號 - 有道技術團隊(youdaotech)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閲讀的你也加入,一起分享。