android開發的奧秘(深入探索Android包瘦身)
2023-04-18 04:40:39
碼個蛋(codeegg) 第 945 次推文
作者:jsonchao連結:https://juejin.im/post/5e7ad1c0e51d450edc0cf053
複習上篇:《深入探索 Android 包瘦身(上)》
資源瘦身方案探索
眾所周知,Android構建工具鏈中使用了AAPT/AAPT2工具來對資源進行處理,Manifest、Resources、Assets 的資源經過相應的 ManifesMerger、ResourcesMerger、AssetsMerger 資源合併器將多個不同 moudule 的資源合併為了 MergedManifest、MergedResources、MergedAssets。然後,它們被 AAPT 處理後生成了 R.java、Proguard Configuration、Compiled Resources。如下圖左上方所示:
其中 Proguard Configuration、Compiled Resources的作用如下所示:
Proguard Configuration:這是AAPT工具為Manifest中聲明的四大組件與布局文件中使用的各種Views所生成的混淆配置,該文件通常存放在${project.buildDir}/${AndroidProject.FD_INTERMEDIATES}/proguard-rules/${flavorName}/${buildType}/aapt_rules.txt。
Compiled Resources:它是一個Zip格式的文件,這個文件的路徑通常為${project.buildDir}/${AndroidProject.FD_INTERMEDIATES}/res/resources-${flavorName}-${buildType}-stripped.ap_。在經過zip解壓之後,可以發現它包含了res、AndroidManifest.xml和resources.arsc 這三部分。並且,從上面的APK構建流程中可以得知,Compiled Resources會被apkbuilder打包到APK包中,它其實就是APK的資源包。因此,我們可以通過 Compiled Resources 文件來修改不同後綴文件資源的壓縮方式來達到瘦身效果的。但是需要注意的是,resources.arsc 文件最好不要壓縮存儲,如果壓縮會影響一定的性能,尤其是在冷啟動時間方面造成的影響。並且,如果在 Android 6.0 上開啟了 android:extractNativeLibs=」false」 的話,So 文件也不能被壓縮。
1、冗餘資源優化
1、使用 Lint 的 Remove Unused ResourceAPK的資源主要包括圖片、XML,與冗餘代碼一樣,它也可能遺留了很多舊版本當中使用而新版本中不使用的資源,這點在快速開發的App中更可能出現。我們可以通過點擊右鍵,選中Refactor,然後點擊Remove Unused Resource => preview可以預覽找到的無用資源,點擊Do Refactor可以去除冗餘資源。如下圖所示:
需要注意的,Android Lint 不會分析 assets 文件夾下的資源,因為 assets 文件可以通過文件名直接訪問,不需要通過具體的引用,Lint 無法判斷資源是否被用到。
2、優化 shrinkResources 流程真正去除無用資源resources.arsc中可能會存在很多無用的資源映射,我們可以使用 android-arscblamer,它是一個命令行工具,能夠解析 resources.arsc 文件並檢查出可以優化的部分,比如一些空的引用。
此外,當我們通過 shrinkResources true來開啟資源壓縮,資源壓縮工具只會把無用的資源替換成預定義的版本而不是移除。那麼,如何高效地對無用資源自動進行去除呢?
我們可以 在 Android 構建工具執行 package${flavorName}Task 之前通過修改 Compiled Resources 來實現自動去除無用資源,具體的實現原理如下:
1)、首先,收集 Compiled 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 as binary-packed with AAPTpublic static final long TINY_XML_CRC = 0xd7e65643L;
2)、然後,使用 android-chunk-utils 把 resources.arsc 中對應的定義移除。
3)、最後,刪除資源包中對應的資源文件即可。
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> duplicatedResources = findDuplicatedResources(apFile); removeZipEntry(apFile, "resources.arsc"); if (arscFile.exists) { FileInputStream arscStream = ; ResourceFile resourceFile = ; try { arscStream = new FileInputStream(arscFile); resourceFile = ResourceFile.fromInputStream(arscStream); List chunks = resourceFile.getChunks; HashMap toBeReplacedResourceMap = new HashMap(1024); // 處理arsc並刪除重複資源 iterator<Map.Entry<String, ArrayList>> iterator = duplicatedResources.entrySet.iterator; while (iterator.hasNext) { Map.Entry<String, ArrayList> 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 != ) { IOUtils.closeQuietly(arscStream); } arscFile.delete; arscFile <WebP(非純色icon)->Png(更好效果) ->jpg(若無alpha通道)
用 圖形化的形式如下所示:
使用矢量圖片之後,它能夠有效的減少應用中圖片所佔用的大小,矢量圖形在 Android 中表示為 VectorDrawable 對象。它僅僅需100位元組的文件即可以生成屏幕大小的清晰圖像,但是,Android 系統渲染每個 VectorDrawable 對象需要大量的時間,而較大的圖像需要更長的時間。因此,建議只有在顯示純色小 icon 時才考慮使用矢量圖形。(我們可以利用這個 在線工具 將矢量圖轉換成 VectorDrawable)。
最後,如果要在項目中使用 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。最後,如果我們僅僅想提取僅需要的美化文字,以壓縮 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 圖片。
此外,在 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 項目地址
下面,我們就使用 AndroidResGuard來對資源進行混淆。
1、AndroidResGuard 實戰1、首先,我們在項目的根 build.gradle 文件下加入下面的插件依賴:
classpath 'com.tencent.mm:AndResGuard-gradle-plugin:1.2.17'
2、然後,在項目 module 下的 build.gradle 文件下引入其插件:
apply plugin: 'AndResGuard'
3、接著,加入 AndroidResGuard 的配置項,如下是默認設置好的配置:
andResGuard { // mappingFile = file("./resource_mapping.txt") mappingFile = use7zip = true useSign = true // 打開這個開關,會keep住所有資源的原始路徑,只混淆資源的名字 keepRoot = false // 設置這個值,會把arsc name列混淆成相同的名字,減少string常量池的大小 fixedResName = "arg" // 打開這個開關會合併所有哈希值相同的資源,但請不要過度依賴這個功能去除去冗餘資源 mergeDuplicatedRes = true whiteList = [ // for your icon "R.drawable.icon", // for fabric "R.string.com.crashlytics.*", // for google-services "R.string.google_app_id", "R.string.gcm_defaultSenderId", "R.string.default_web_client_id", "R.string.ga_trackingId", "R.string.firebase_database_url", "R.string.google_api_key", "R.string.google_crash_reporting_api_key" ] compressFilePattern = [ "*.png", "*.jpg", "*.jpeg", "*.gif", ] sevenzip { artifact = 'com.tencent.mm:SevenZip:1.2.17' //path = "/usr/local/bin/7za" } /** * 可選:如果不設置則會默認覆蓋assemble輸出的apk **/ // finalApkBackupPath = "${project.rootDir}/final.apk" /** * 可選: 指定v1籤名時生成jar文件的摘要算法 * 默認值為「SHA-1」 **/ // digestalg = "SHA-256"}
4、最後,我們點擊右邊的項目 module/Tasks/andresguard/resguardRelease 即可生成資源混淆過的 APK。如下圖所示:
APK生成目錄如下:
對於 AndResGuard 工具,主要有 兩個功能,一個是資源混淆,一個是資源的極限壓縮。下面,我們就來分別了解下它們的實現原理。
2、AndResGuard 的資源混淆原理資源混淆工具主要是通過 短路徑的優化,以達到減少 resources.arsc、metadata 籤名文件以及 ZIP 文件大小的效果,其效果分別如下所示:
1)、resources.arsc:它記錄了資源文件的名稱與路徑,使用混淆後的短路徑 res/s/a,可以減少文件的大小。
2)、metadata 籤名文件:籤名文件 MANIFEST.MF 與 CERT.SF 需要記錄所有文件的路徑以及它們的哈希值,使用短路徑可以減少這兩個文件的大小。
3)、ZIP 文件:ZIP 文件格式裡面通過其索引記錄了每個文件 Entry 的路徑、壓縮算法、CRC、文件大小等等信息。短路徑的優化減少了記錄文件路徑的字符串大小。
3、AndResGuard 的極限壓縮原理AndResGuard使用了7-Zip 的大字典優化,APK的整體壓縮率可以提升 3% 左右,並且,它還支持針對 resources.arsc、PNG、JPG 以及 GIF 等文件進行強制壓縮(在編譯過程中,這些文件默認不會被壓縮)。那麼,為什麼 Android 系統不會去壓縮這些文件呢?主要基於以下兩點原因:
1)、壓縮效果不明顯:上述格式的文件大部分已經被壓縮過,因此,重新做Zip壓縮效果並不明顯。比如 重新壓縮PNG和JPG格式只能減少3%~5%的大小。
2)、基於讀取時間和內存的考慮:針對於沒有進行壓縮的文件,系統可以使用 mmap 的方式直接讀取,而不需要一次性解壓並放在內存中。
此外,抖音 Android 團隊還開源了針對於海外市場 App Bundle APK 的 AabResGuard 資源混淆工具,對它的實現原理有興趣的同學可以去了解下。然後,我們再看看資源瘦身的其它方案。
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。它的實現原理為:android 中的 R 文件,除了 styleable 類型外,所有欄位都是 int 型變量/常量,且在運行期間都不會改變。所以可以在編譯時,記錄 R 中所有欄位名稱及對應值,然後利用 ASM 工具遍歷所有 Class,將除 R$styleable.class 以外的所有 R.class 刪除掉,並且在引用的地方替換成對應的常量,從而達到縮減包大小和減少Dex個數的效果。此外,最近ByteX也增加了 shrink_r_class 的gradle插件,它不僅可以在編譯階段對R文件常量進行內聯,而且還可以針對 App 中無用 Resource 和無用 assets 的資源進行檢查。
7、資源合併方案
我們可以把所有的資源文件合併成一個大文件,而 一個大資源文件就相當於換膚方案中的一套皮膚。它的效果比資源混淆的效果會更好,但是,在此之前,必須要解決解析資源與管理資源的問題。其相應的解決方案如下所示:
模擬系統實現資源文件的解析:我們需要使用自定義的方式把 PNG、JPG 以及 XML 文件轉換為 Bitmap 或者 Drawable。
使用 mmap 加載大資源與資源緩存池管理資源:使用 mmap 加載大資源的方式可以充分減少啟動時間與系統內存的佔用。而且,需要使用 Glide 等圖片框架的資源緩存池 ResourceCache 去釋放不再使用的資源文件。
8、資源文件最少化配置
我們需要 根據 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' } } ...}
9、儘量每張圖片只保留一份
比如說,我們統一隻把圖片放到 xhdpi這個目錄下,那麼在不同的解析度下它會做自動的適配,即等比例地拉伸或者是縮小。
10、資源在線化
我們可以 將一些圖片資源放在伺服器,然後結合圖片預加載的技術手段,這些既可以滿足產品的需要,同時可以減小包大小。
11、統一應用風格
如設定統一的 字體、尺寸、顏色和按鈕按壓效果、分割線 shape、selector 背景等等。
阿里大佬十年面試了 2000 人,總結這7 條金科玉律
深入探索 Android 包瘦身(上)
Android 多線程技術哪家強?
資源瘦身的這些方法用過哪些?
,