1、冗餘資源優化
1、優化 shrinkResources 流程真正去除無用資源
resources.arsc 中可能會存在很多 無用的資源對映,我們可以使用 android-arscblamer,它是一個命令列工具,能夠 解析 resources.arsc 檔案並檢查出可以優化的部分,比如一些空的引用。此外,當我們通過 shrinkResources true 來 開啟資源壓縮,資源壓縮工具只會把無用的資源替換成預定義的版本而不是移除。那麼,如何高效地對無用資源自動進行去除呢?我們可以 在 Android 構建工具執行 package${flavorName}Task 之前通過修改 Compiled Resources 來實現自動去除無用資源,具體的實現原理如下:
首先,收集 ompiled Resources 中被替換的預定義版本的資源名稱
通過檢視 Zip 格式資源包中每個 ZipEntry 的 CRC-32 checksum 來尋找被替換的預定義資源,預定義資源的 CRC-32 定義在 ResourceUsageAnalyze 中,如下所示:
// A 1x1 pixel PNG of type BufferedImage.TYPE_BYTE_GRAYpublic static final long TINY_PNG_CRC = 0x88b2a3b0L;// A 3x3 pixel PNG of type BufferedImage.TYPE_INT_ARGB with 9-patch markerspublic static final long TINY_9PNG_CRC = 0x1148f987L;// The XML document <x/> as binary-packed with AAPTpublic static final long TINY_XML_CRC = 0xd7e65643L;
2)然後,使用 android-chunk-utils 把 resources.arsc 中對應的定義移除。
https://github.com/madisp/android-chunk-utils
2、重複資源優化在大型 App 專案的開發中,一個 App 一般會有多個業務團隊進行開發,其中每個業務團隊在資源提交時的資源名稱可能會有重複的,這將會 引發資源覆蓋的問題,因此,每個業務團隊都會為自己的 資原始檔名新增字首。
這樣就導致了這些資原始檔雖然 內容相同,但因為 名稱的不同而不能被覆蓋,最終都會被整合到 APK 包中。這裡,我們還是可以 在 Android 構建工具執行 package${flavorName}Task 之前通過修改 Compiled Resources 來實現重複資源的去除,具體放入實現原理可細分為如下三個步驟:
1)首先,通過資源包中的每個ZipEntry的CRC-32 checksum來篩選出重複的資源。2)然後,通過android-chunk-utils修改resources.arsc,把這些重複的資源都重定向到同一個檔案上。3)最後,把其它重複的資原始檔從資源包中刪除,僅保留第一份資源。具體的實現程式碼如下所示:
variantData.outputs.each { def apFile = it.packageAndroidArtifactTask.getResourceFile(); it.packageAndroidArtifactTask.doFirst { def arscFile = new File(apFile.parentFile, "resources.arsc"); JarUtil.extractZipEntry(apFile, "resources.arsc", arscFile); def HashMap<String, ArrayList<DuplicatedEntry>> duplicatedResources = findDuplicatedResources(apFile); removeZipEntry(apFile, "resources.arsc"); if (arscFile.exists()) { FileInputStream arscStream = null; ResourceFile resourceFile = null; try { arscStream = new FileInputStream(arscFile); resourceFile = ResourceFile.fromInputStream(arscStream); List<Chunk> chunks = resourceFile.getChunks(); HashMap<String, String> toBeReplacedResourceMap = new HashMap<String, String>(1024); // 處理arsc並刪除重複資源 Iterator<Map.Entry<String, ArrayList<DuplicatedEntry>>> iterator = duplicatedResources.entrySet().iterator(); while (iterator.hasNext()) { Map.Entry<String, ArrayList<DuplicatedEntry>> duplicatedEntry = iterator.next(); // 保留第一個資源,其他資源刪除掉 for (def index = 1; index < duplicatedEntry.value.size(); ++index) { removeZipEntry(apFile, duplicatedEntry.value.get(index).name); toBeReplacedResourceMap.put(duplicatedEntry.value.get(index).name, duplicatedEntry.value.get(0).name); } } for (def index = 0; index < chunks.size(); ++index) { Chunk chunk = chunks.get(index); if (chunk instanceof ResourceTableChunk) { ResourceTableChunk resourceTableChunk = (ResourceTableChunk) chunk; StringPoolChunk stringPoolChunk = resourceTableChunk.getStringPool(); for (def i = 0; i < stringPoolChunk.stringCount; ++i) { def key = stringPoolChunk.getString(i); if (toBeReplacedResourceMap.containsKey(key)) { stringPoolChunk.setString(i, toBeReplacedResourceMap.get(key)); } } } } } catch (IOException ignore) { } catch (FileNotFoundException ignore) { } finally { if (arscStream != null) { IOUtils.closeQuietly(arscStream); } arscFile.delete(); arscFile << resourceFile.toByteArray(); addZipEntry(apFile, arscFile); } } }}
然後,我們再看看圖片壓縮這一項。
3、圖片壓縮一般來說,1000行程式碼在APK中才會佔用 5kb 的空間,而圖片呢,一般都有 100kb 左右,所以說,對圖片做壓縮,它的收益明顯是更大的,而往往處於快速開發的 App 沒有相關的開發規範,UI 設計師或開發同學如果忘記了新增圖片時進行壓縮,新增的就是原圖,那麼包體積肯定會增大很多。
對於圖片壓縮,我們可以在 tinypng 這個網站進行圖片壓縮,但是如果 App 的圖片過多,一個個壓縮也是很麻煩的。
因此,我們可以 使用 TinyPngPlugin 或 TinyPIC_Gradle_Plugin 來對圖片進行自動化批量壓縮。
https://github.com/Deemonser/TinyPngPlugin
https://github.com/meili/TinyPIC_Gradle_Plugin/blob/master/README.zh-cn.md
但是,需要注意的是,在 Android 的構建流程中,AAPT 會使用內建的壓縮演算法來優化 res/drawable/ 目錄下的 PNG 圖片,但這可能會導致本來已經優化過的圖片體積變大,因此,可以通過在 build.gradle 中 設定 cruncherEnabled 來禁止 AAPT 來優化 PNG 圖片,程式碼如下所示:
aaptOptions { cruncherEnabled = false}
此外,我們還要注意對圖片格式的選擇,對於我們普遍使用更多的 png 或者是 jpg 格式來說,相同的圖片轉換為 webp 格式之後會有大幅度的壓縮。
對於 png 來說,它是一個無損格式,而 jpg 是有損格式。jpg 在處理顏色圖片很多時候根據壓縮率的不同,它有時候會去掉我們肉眼識別差距比較小的顏色,但是 png 會嚴格地保留所有的色彩。所以說,在圖片尺寸大,或者是色彩鮮豔的時候,png 的體積會明顯地大於 jpg。
下面,我們就著重講解下如何針對性地選擇圖片格式。
4、使用針對性的圖片格式在 Google I/O 2016 中,講到了如何選擇相應的圖片格式。首先,如果能用 VectorDrawable 來表示的話,則優先使用 VectorDrawable;否則,看是否支援 WebP,支援則優先用 WebP;如果也不能使用 WebP,則優先使用 PNG,而 PNG 主要用在展示透明或者簡單的圖片,對於其它場景可以使用 JPG 格式。簡單來說可以歸結為如下套路:
VD(純色icon)->WebP(非純色icon)->Png(更好效果) ->jpg(若無alpha通道)
用 圖形化 的形式如下所示:
使用向量圖片之後,它能夠有效的減少應用中圖片所佔用的大小,向量圖形在 Android 中表示為 VectorDrawable 物件。
它 僅僅需100位元組的檔案即可以生成螢幕大小的清晰影象,但是,Android 系統渲染每個 VectorDrawable 物件需要大量的時間,而較大的影象需要更長的時間。因此,建議 只有在顯示純色小 icon 時才考慮使用向量圖形。(我們可以利用這個 線上工具 將向量圖轉換成 VectorDrawable)。
http://inloop.github.io/svg2android/
最後,如果要在專案中使用 VD,則以下幾點需要著重注意:
1)、必須通過 app:arcCompat 屬性來使用 svg,如果通過 src,則在低版本手機上會出現不相容的問題。
2)、可能會不相容selector,在 Activity 中手動相容即可,相容程式碼如下所示:
static { AppCompatDelegate.setCompatVectorFromResourcesEnabled(true) }3)、不相容第三方庫。
4)、效能問題:當Vector比較簡單時,效率肯定比Bitmap高,複雜則效率會不如Bitmap。
5)、不便於管理:建議原則為同目錄多型別檔案,以字首區別,不同目錄相同型別檔案,以意義區分。
與 VD 類似,還有一種向量圖示 iconFont,即 字型圖示,圖示就在字型檔案裡面,它看著是個圖示,其實卻是個文字。
它的 優勢 有如下三個方面:
1)、同 VD 一樣,由於 IconFont 是向量圖示,所以可以輕鬆解決圖示適配問題。
2)、圖示以 .ttf 字型檔案的形式存在專案中,而 .ttf 檔案一般放在 assets 資料夾下,它的體積很小,可以減小 APK 的體積。
3)、一套圖示資源可以在不同平臺使用且資源維護方便。
它的 缺點 也很明顯,大致有如下三個方面:
1)、需要自定義 svg 圖片,並將其轉換為 ttf 檔案,圖示製作成本比較高。
2)、新增圖示時需要重新制作 ttf 檔案。
3)、只能支援單色,不支援漸變色圖示。
如果你想要使用 iconfont,可以在阿里的 iconfont 上尋找資源。此外,使用 Android-Iconics 可以在你的應用中便於使用任何的 iconfont 或 .svg 圖片作為 drawable。
https://github.com/mikepenz/Android-Iconics
https://github.com/forJrking/FontZip
最後,如果我們 僅僅想提取僅需要的美化文字,以壓縮 assets 下的字型檔案大小,可以使用 FontZip 字型提取工具。
如果不是純色小 icon 型別的圖片,則建議使用 WebP。只要你的 App 的 minSdkVersion 高於 14(Android 4.0+) 即可。WebP 不僅支援透明度,而且壓縮率比 JPEG 更高,在相同畫質下體積更小。但是,只有 Android 4.2.1+ 才支援顯示含透明度的 WebP,此外,它的 相容性不好,並且不便於預覽,需使用瀏覽器開啟。
對於應用之前就存在的圖片,我們可以使用 PNG轉換WebP 的轉換工具來進行轉換。但是,一個一個轉換開發效率太低,因此我們可以 使用WebpConvert_Gradle_Plugin 這個 gradle 外掛去批量進行轉換,它的實現原理是 在 mergeXXXResource Task 和 processXXXResource Task 之間插入了一個 WebpConvertPlugin task 去將 png、jpg 圖片批量替換成了 webp 圖片。
https://github.com/meili/WebpConvert_Gradle_Plugin/blob/master/README.zh-cn.md
此外,在 Gradle 構建 APK 的過程中,我們可以判斷當前 App 的 minSdkVersion 以及圖片檔案的型別來選用是否能使用 WebP,程式碼如下所示:
boolean isPNGWebpConvertSupported() { if (!isWebpConvertEnable()) { return false } // Android 4.0+ return GradleUtils.getAndroidExtension(project).defaultConfig.minSdkVersion.apiLevel >= 14 // 4.0}boolean isTransparencyPNGWebpConvertSupported() { if (!isWebpConvertEnable()) { return false } // Lossless, Transparency, Android 4.2.1+ return GradleUtils.getAndroidExtension(project).defaultConfig.minSdkVersion.apiLevel >= 18 // 4.3}def convert() { String resPath = "${project.buildDir}/${AndroidProject.FD_INTERMEDIATES}/res/merged/${variant.dirName}" def resDir = new File("${resPath}") resDir.eachDirMatch(~/drawable[a-z0-9-]*/) { dir -> FileTree tree = project.fileTree(dir: dir) tree.filter { File file -> return (isJPGWebpConvertSupported() && (file.name.endsWith(SdkConstants.DOT_JPG) || file.name.endsWith(SdkConstants.DOT_JPEG))) || (isPNGWebpConvertSupported() && file.name.endsWith(SdkConstants.DOT_PNG) && !file.name.endsWith(SdkConstants.DOT_9PNG)) }.each { File file -> def shouldConvert = true if (file.name.endsWith(SdkConstants.DOT_PNG)) { if (!isTransparencyPNGWebpConvertSupported()) { shouldConvert = !Imaging.getImageInfo(file).isTransparent() } } if (shouldConvert) { WebpUtils.encode(project, webpFactorQuality, file.absolutePath, webp) } } }} 最後,這裡再補充下在平時專案開發中對 圖片放置優化的大概思路,如下所示:
1)聊天表情出一套圖 => hdpi。2)純色小 icon 使用 VD => raw。3)背景大圖出一套 => xhdpi。4)logo 等權重比較大的圖片出兩套 => hdpi,xhdpi。5)若某些圖在真機中有異常,則用多套圖。6)若遇到奇葩機型,則針對性補圖。然後,我們來講解下資源如何進行混淆。
5、資源混淆同程式碼混淆類似,資源混淆將 資源路徑混淆成單個資源的路徑,這裡我們可以使用 AndroidResGuard,它可以使冗餘的資源路徑變短,例如將 res/drawable/wechat 變為 r/d/a。
AndroidResGuard 專案地址
https://github.com/shwenzhang/AndResGuard/blob/master/README.zh-cn.md
6、R Field 的內聯優化我們可以通過內聯 R Field 來進一步對程式碼進行瘦身,此外,它也解決了 R Field 過多導致 MultiDex 65536 的問題。
要想實現內聯 R Field,我們需要 通過 Javassist 或者 ASM 位元組碼工具在構建流程中內聯 R Field,其程式碼如下所示:
ctBehaviors.each { CtBehavior ctBehavior -> if (!ctBehavior.isEmpty()) { try { ctBehavior.instrument(new ExprEditor() { @Override public void edit(FieldAccess f) { try { def fieldClassName = JavassistUtils.getClassNameFromCtClass(f.getCtClass()) if (shouldInlineRField(className, fieldClassName) && f.isReader()) { def temp = fieldClassName.substring(fieldClassName.indexOf(ANDROID_RESOURCE_R_FLAG) + ANDROID_RESOURCE_R_FLAG.length()) def fieldName = f.fieldName def key = "${temp}.${fieldName}" if (resourceSymbols.containsKey(key)) { Object obj = resourceSymbols.get(key) try { if (obj instanceof Integer) { int value = ((Integer) obj).intValue() f.replace("\\$_=${value};") } else if (obj instanceof Integer[]) { def obj2 = ((Integer[]) obj) StringBuilder stringBuilder = new StringBuilder() for (int index = 0; index < obj2.length; ++index) { stringBuilder.append(obj2[index].intValue()) if (index != obj2.length - 1) { stringBuilder.append(",") } } f.replace("\\$_ = new int[]{${stringBuilder.toString()}};") } else { throw new GradleException("Unknown ResourceSymbols Type!") } } catch (NotFoundException e) { throw new GradleException(e.message) } catch (CannotCompileException e) { throw new GradleException(e.message) } } else { throw new GradleException("******** InlineRFieldTask unprocessed ${className}, ${fieldClassName}, ${f.fieldName}, ${key}") } } } catch (NotFoundException e) { } } }) } catch (CannotCompileException e) { } }}這裡,我們可以 直接使用蘑菇街的 ThinRPlugin。
https://github.com/meili/ThinRPlugin/blob/master/README.zh-cn.md
它的實現原理為:android 中的 R 檔案,除了 styleable 型別外,所有欄位都是 int 型變數/常量,且在執行期間都不會改變。所以可以在編譯時,記錄 R 中所有欄位名稱及對應值,然後利用 ASM 工具遍歷所有 Class,將除 R$styleable.class 以外的所有 R.class 刪除掉,並且在引用的地方替換成對應的常量,從而達到縮減包大小和減少 Dex 個數的效果。
此外,最近 ByteX 也增加了 shrink_r_class 的 gradle 外掛,它不僅可以在編譯階段對 R 檔案常量進行內聯,而且還可以 針對 App 中無用 Resource 和無用 assets 的資源進行檢查。
https://github.com/bytedance/ByteX/blob/master/shrink-r-plugin/README-zh.md
7、資原始檔最少化配置我們需要 根據 App 目前所支援的語言版本去選用合適的語言資源,例如使用了 AppCompat,如果不做任何配置的話,最終 APK 包中會包含 AppCompat 中所有已翻譯語言字串,無論應用的其餘部分是否翻譯為同一語言。
對此,我們可以 通過 resConfig 來配置使用哪些語言,從而讓構建工具移除指定語言之外的所有資源。同理,也可以使用 resConfigs 去配置你應用需要的圖片資原始檔類,如 "xhdpi"、"xxhdpi" 等等,程式碼如下所示:
android { ... defaultConfig { ... resConfigs "zh", "zh-rCN" resConfigs "nodpi", "hdpi", "xhdpi", "xxhdpi", "xxxhdpi" } ...} 此外,我們還以 利用 Density Splits 來選擇應用應相容的螢幕尺寸大小,程式碼如下所示:
android { ... splits { density { enable true exclude "ldpi", "tvdpi", "xxxhdpi" compatibleScreens 'small', 'normal', 'large', 'xlarge' } } ...}8、儘量每張圖片只保留一份
比如說,我們統一隻把圖片放到 xxhdpi 這個目錄下,那麼 在不同的解析度下它會做自動的適配,即 等比例地拉伸或者是縮小。
9、資源線上化
我們可以 將一些圖片資源放在伺服器,然後 結合圖片預載入 的技術手段,這些 既可以滿足產品的需要,同時可以減小包大小。
10、統一應用風格
對於主要由 C/C++ 實現的 Native Library 而言,常規的優化方式就是 去除 Debug 資訊,使用 C++_shared 等等。下面,對於 So 瘦身,我們看看還有哪些方案。
1、So 移除方案So 是 Android 上的動態連結庫,在我們 Android 應用開發過程中,有時候 Java 程式碼不能滿足需求,比如一些 加解密演算法或者音視訊編解碼功能,這個時候就必須要通過 C 或者是 C++ 來實現,之後生成 So 檔案提供給 Java 層來呼叫,在生成 So 檔案的時候就需要考慮生成市面上不同手機 CPU 架構的檔案。目前,Android 一共 支援7種不同型別的 CPU 架構,比如常見的 armeabi、armeabi-v7a、X86 等等。理論上來說,對應架構的 CPU 它的執行效率是最高的,但是這樣會導致 在 lib 目錄下會多存放了各個平臺架構的 So 檔案,所以 App 的體積自然也就更大了。因此,我們就需要對 lib 目錄進行縮減,我們 在 build.gradle 中配置這個 abiFiliters 去設定 App 支援的 So 架構,其配置程式碼如下所示:
defaultConfig { ndk { abiFilters "armeabi" }}一般情況下,應用都不需要用到 neon 指令集,我們只需留下 armeabi 目錄就可以了。
因為 armeabi 目錄下的 So 可以相容別的平臺上的 So,相當於是一個萬金油,都可以使用。但是,這樣 別的平臺使用時效能上就會有所損耗,失去了對特定平臺的優化。
2、So 移除方案優化版上面我們說到了想要完美支援所有型別的裝置代價太大,那麼,我們能不能採取一個 折中的方案,就是 對於效能敏感的模組,它使用到的 So,我們都放在 armeabi 目錄當中隨著 Apk 發出去,然後我們在程式碼中來判斷一下當前裝置所屬的 CPU 型別,根據不同裝置 CPU 型別來載入對應架構的 So 檔案。
這裡我們舉一個小栗子,比如說我們 armeabi 目錄下也加上了 armeabi-v7 對應的 So,然後我們就可以在程式碼當中做判斷,如果你是 armeabi-v7 架構的手機,那我們就直接載入這個 So,以此達到最佳的效能,這樣包體積其實也沒有增加多少,同時也實現了高效能的目的,比如 微信和騰訊視訊 App 裡面就使用了這種方式,如下圖所示:
看到上圖中的 libimagepipeline_x86.so,下面我們就以這個 so 為例來寫寫載入它的虛擬碼,如下所示:
String abi = "";// 獲取當前手機的CPU架構型別if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { abi = Buildl.CPU_ABI;} else { abi = Build.SUPPORTED_ABIS[0];}if (TextUtils.equals(abi, "x86")) { // 載入特定平臺的So} else { // 正常載入}
接下來,我們再了解下 So 優化當中別的優化方式。
3、使用 XZ Utils 對 Native Library 進行壓縮Native Library 同 Dex 一樣,也可以使用 XZ Utils 進行壓縮,對於 Native Library 的壓縮,我們 只需要去載入啟動過程相關的 Library,而其它的都可以在應用首次啟動時進行解壓,並且,壓縮效果與 Dex 壓縮的效果是相似的。
此外,關於 Nativie Library 壓縮之後的解壓,我們也可以使用 Facebook 的 so 載入庫 SoLoader,它 能夠解壓應用的 Native Library 並能遞迴地載入在 Android 平臺上不支援的依賴項。由於這套方案對啟動時間的影響比較大,所以先把它壓箱底下吧。
https://github.com/facebook/SoLoader
4、刪除 Native Library 中無用的匯出 symbol我們可以去 分析程式碼中的 JNI 方法以及不同 Library 庫的方法呼叫,然後找出無用的 symbol 並刪除,這樣 Linker 在編譯的時候也會把 symbol 對應的無用程式碼給刪除。
在 Buck 有 NativeRelinker 這個類,它就實現了這個功能,其 類似於 Native Library 的 ProGuard Shrinking 功能。
https://github.com/facebook/buck/blob/master/src/com/facebook/buck/android/relinker/NativeRelinker.java
至此,可以看到,FaceBook 出品的 Buck 同 ReDex 一樣,裡面的功能都十分強大,Buck 除了實現 Library Merge 和 Relinker 功能之外,還實現了三大功能,如下所示:
多語言拆分分包支援ReDex 支援如果有相應需求或對 Buck 感興趣的同學可以去看看它們的實現原始碼。
5、So 動態下載我們可以 將部分 So 檔案使用動態下發的形式進行載入。也就是在業務程式碼操作之前,我們可以先從伺服器下載下來 So,接下來再使用,這樣包體積肯定會減少不小。
但是,如果要把這項技術 穩定落地到實際生產專案中需要解決一些問題,具體的 so 動態化關鍵技術點和需要避免的坑可以參見 動態下發 so 庫在 Android APK 安裝包瘦身方面的應用,這裡就不多贅述了。
其它優化方案1、外掛化我們可以使用外掛化的手段 對程式碼結構進行調整,如果我們 App 當中的每一個功能都是一個外掛,並且都是可以從伺服器下發下來的,那 App 的包體積肯定會小很多。
外掛化相關的知識非常多而且不屬於我們的重點,並且,外掛化嚴格來說屬於 基礎架構研發 這塊的知識,掌握它是成為 Android 架構師的必經之路,關於 Android 架構師的學習路線 可以參考 Awesome-Android-Architecture,預計今年會完成部分學習內容,敬請期待。
https://github.com/JsonChao/Awesome-Android-Architecture
2、業務梳理我們需要 回顧過去的業務,合理地去 評估並刪除無用或者低價值的業務。
3、轉變開發模式如果所有的功能都不能移除,那就可能需要去轉變開發模式,比如可以更多地 採用 H5、小程式 這樣開發模式。
包體積監控對於應用包體積的監控,也應該和記憶體監控一樣,去作為正式版本的釋出流程中的一環,並且應該 儘量地去實現自動化與平臺化。(這裡建議 任何大於 100kb 的功能都需要審批,特別是需要引入第三方庫時,更應該慎重)
1、包體積監控的緯度
包積的監控,主要可以從如下 三個緯度 來進行:
大小監控:通常是記錄當前版本與上一個或幾個版本直接的變化情況,如果當前版本體積增長較大,則需要分析具體原因,看是否有優化空間。依賴監控:包括J ar、aar 依賴。規則監控:我們可以把包體積的監控抽象為無用資源、大檔案、重複檔案、R 檔案等這些規則包體積的 大小監控 和 依賴監控 都很容易實現,而要實現 規則監控 卻得花不少功夫,幸運的是 Matrix 中的 ApkChecker 就實現了包體積的規則監控,其 使用文件與實現原理 微信團隊已經寫得很清楚了,這裡就不再一一贅述,有興趣的同學可以去研究下。
https://github.com/Tencent/matrix/wiki/Matrix-Android-ApkChecker
1、怎麼降低 Apk 包大小?我們在回答的時候要注意一些 可操作的乾貨,同時注意結合你的 專案週期。主要可以從以下 三點 來回答:
1)程式碼:Proguard、統一三方庫、無用程式碼刪除。2)資源:無用資源刪除、資源混淆。3)So:只保留 Armeabi、更優方案。在專案初期,我們一直在不斷地加功能,加入了很多的程式碼、資源,同時呢,也沒有相應的規範,所以說,UI 同學給我們很多 UI 圖的時候,都是沒有經過壓縮的圖片,長期累積就會導致我們的包體積越來越大。
到了專案穩定期的時候,我們對各種運營資料進行考核,發現 APK 的包大小影響了使用者下載的意願,於是我們就著手做包體積的優化,我們採用的是 Android Studio 自帶的 Analyze APK 來做的包體積分析,主要就是做了程式碼、資源、So 等三個方面的重點優化。
首先,針對於程式碼瘦身.
第一點,我們首先 使用 Proguard 工具進行了混淆,它將程式程式碼轉換為功能相同,但是不容易理解的形式。
比如說將一個很長的類轉換為字母 a,同時,這樣做還有一個好處,就是讓程式碼更加安全了。第二點呢,我們將專案中使用到的一些 第三方庫進行了統一,比如說圖片庫、網路庫、資料庫等,不允許專案中出現功能相同,但是卻實現不一樣的庫。
同時也做了 規範,之後引入的三方庫,需要去考量它的大小、方法數等,而且呢,如果只是需要一個很大庫的一個小功能,那我們就修改原始碼,只引入部分程式碼即可。第三點,我們將專案中的 無用程式碼進行了刪減,我們使用了 AOP 的方式統計到了哪些 Activity 以及 fragment 在真實的場景下沒有使用者使用,這樣你就可以刪除掉了。
對於那些不是 Activity 或者是 Fragment 的類,我們切了很多類的建構函式,這樣你就可以統計出來這些類在線上有沒有真正被呼叫到。但是,對於程式碼的瘦身效果,實際上不是很明顯。
接下來,我們做了資源的瘦身。
首先,我們 移除了專案當中冗餘的資原始檔,這一點在專案當中一定會遇到。然後,我們做了 資源圖片的壓縮,UI 同學給我們資源圖片的時候,需要確認已經是壓縮過的圖片,同時,我們還會做一個 兜底策略,在打包的時候,如果圖片沒有被壓縮過,那我們就會再來壓縮一遍,這個效果就非常的明顯。
對於資源,我們還做了 資源的混淆,也就是將冗餘的資源名稱換成簡短的名字,資源壓縮的效果要比程式碼瘦身的效果要好的多。
最後,我們做了 So 的瘦身。
首先,我們只保留了 armeabi 這個目錄,它可以 相容別的 CPU 架構,這點的優化效果非常的明顯。
移除了對別的架構適配 So 之後,我們還做了另外一個處理,對於專案當中使用到的視訊模組的 So,它對效能要求非常高,所以我們採用了另外一種方式,我們將所有這個模組下的 So 都放到了 armeabi 這個目錄下,然後在程式碼中做判斷,如果是別的 CPU 架構,那我們就載入對應 CPU 架構的 So 檔案即可。
這樣即減少了包體積,同時又達到了效能最佳。最後,通過實踐可以看出 So瘦身的效果一般是最好的。
2、Apk 瘦身如何實現長效治理?主要可以從以下 兩個方面 來進行回答:
發版之前與上個版本包體積對比,超過閾值則必須優化。推進外掛化架構改進。在大型專案中,最好的方式就是 結合 CI,每個開發同學 在往主幹合入程式碼的時候需要經過一次預編譯,這個預編譯出來的包對比主幹打出來的包大小,如果超過閾值則不允許合入,需要提交程式碼的同學自己去優化去提交的程式碼。
此外,針對專案的 架構,我們可以做 外掛化的改造,將每一個功能模組都改造成外掛,以外掛的形式來支援動態下發,這樣應用的包體積就可以從根本上變小了。
總結在本篇文章中,我們主要從以下 七個方面 講解了 Android 包體積優化相關的知識:
1)、瘦身優化及 Apk 分析方案:瘦身優勢、APK 組成、APK 分析。
2)、程式碼瘦身方案探索:Dex 探祕、ProGuard、D8 與 R8 優化、去除 debug 資訊與行號資訊、Dex 分包優化、使用 XZ Utils 進行 Dex 壓縮、三方庫處理、移除無用程式碼、避免產生 Java access 方法、利用 ByteX Gradle 外掛平臺中的程式碼優化外掛。
3)、資源瘦身方案探索:冗餘資源優化、重複資源優化、圖片壓縮、使用針對性的圖片格式、資源混淆、R Field 的內聯優化、資源合併方案、資原始檔最少化配置、儘量每張圖片只保留一份、資源線上化、統一應用風格。
4)、So 瘦身方案探索:So 移除方案、So 移除方案優化版、使用 XZ Utils 對 Native Library 進行壓縮、對 Native Library 進行合併、刪除 Native Library 中無用的匯出 symbol、So 動態下載。
5)、其它優化方案:外掛化、業務梳理、轉變開發模式。
6)、包體積監控。
7)、瘦身優化常見問題。
如果要想對包體積做更深入的優化,我們就必須對 APK 組成,Dex、So 動態庫以及 Resource 檔案格式,還有 APK 的編譯流程 有深入地了解,這樣我們才能有 足夠的內功素養 去實現包體積的深度優化。
此外,在做效能優化過程中,為了提升研發效率,降低研發成本,我漸漸發現 AOP 編譯插樁、Gradle 自動化構建 的知識越來越重要;並且,一旦涉及 Native 層甚至 Android 核心層的深度優化時,就越發感覺到功力不足。