包瘦身

一腿堤、瘦身優(yōu)化及 Apk 分析方案介紹

1.1 瘦身優(yōu)勢

我們首先來介紹下狼电,為什么我們需要做 APK 的瘦身優(yōu)化?

1.1.1下載轉(zhuǎn)化率

APK 瘦身優(yōu)化在實際的項目中優(yōu)先級是比較低的,因為做了之后它的好處不是那么明顯,尤其是那些還沒有到 穩(wěn)定期 的項目

App 的發(fā)展歷程是從 項目初期 => 成長期 => 穩(wěn)定期霉晕,對于處于 發(fā)展初期與成長期 的項目而言,可能會做 啟動優(yōu)化捞奕、卡頓優(yōu)化牺堰,但是一般不會做 瘦身優(yōu)化,瘦身優(yōu)化 最主要的好處是對應(yīng)用 下載轉(zhuǎn)化率 的影響颅围,它是 App 業(yè)務(wù)運營的重要指標(biāo)之一伟葫,在項目精細化運營的階段是非常重要的。

因為如果你的 App 與其它同類型的 App 相比 Apk 體積要更小的話院促,那么你的 App 下載率就可能要高一些筏养。

包體積越小斧抱,用戶下載等待的時間也會越短,所以下載轉(zhuǎn)換成功率也就越高撼玄。

所以夺姑,安裝包大小與下載轉(zhuǎn)化率的關(guān)系 大致是成反比 的墩邀,即安裝包越大掌猛,下載轉(zhuǎn)換率就越小。一個 80MB 的應(yīng)用眉睹,用戶即使點了下載荔茬,也可能因為網(wǎng)絡(luò)速度慢、突然反悔導(dǎo)致下載失敗竹海。而對于一個 20MB 的應(yīng)用慕蔚,用戶點了下載之后,在猶豫要不要下的時候可能就已經(jīng)下載完了斋配。

而且孔飒,現(xiàn)在很多大型的 App 一般都會有一個 Lite 版本的 App,這個也是出于下載轉(zhuǎn)化率方面的考慮艰争。

1.1.2坏瞄、應(yīng)用市場

Google Play 應(yīng)用市場強制要求超過 100MB 的應(yīng)用只能使用 APK 擴展文件方式 上傳。

1.1.3渠道合作商的要求

當(dāng)我們的 App 做大之后甩卓,可能需要跟各個手機廠商合作預(yù)裝鸠匀,這些 渠道合作商會對你的 App 做詳細的要求,只有達到相應(yīng)的要求后才允許你的App 預(yù)裝到手機上逾柿。

而且缀棍,越大的 App 其單價成本也會越高。所以机错,瘦身也是我們項目做大之后一定會遇到的一個問題爬范。

1.2 APK 組成

Android 項目最終會編譯成一個 .apk 后綴的文件,實際上它就是一個 壓縮包弱匪。

它內(nèi)部還有很多不同類型的文件坦敌,這些文件,按照大小痢法,共分為如下四類:

  • 1)狱窘、代碼相關(guān):classes.dex,我們在項目中所編寫的java 文件财搁,經(jīng)過編譯之后會生成一個 .class 文件蘸炸,而這些所有的 .class 文件呢,它最終會經(jīng)過 dx 工具編譯生成一個 classes.dex尖奔。
  • 2)搭儒、資源相關(guān):res穷当、assets、編譯后的二進制資源文件 resources.arsc 和 清單文件 等等淹禾。

res 和 assets 的不同在于 res 目錄下的文件會在 .R 文件中生成對應(yīng)的資源 ID馁菜,而 assets 不會自動生成對應(yīng)的 ID,而是通過 AssetManager 類的接口來獲取铃岔。

此外汪疮,每當(dāng)在 res 文件夾下放一個文件時,aapt 就會自動生成對應(yīng)的 id 并保存在 .R 文件中毁习,但 .R 文件僅僅只是保證編譯程序不會報錯智嚷,實際上在應(yīng)用運行時,系統(tǒng)會根據(jù) ID 尋找對應(yīng)的資源路徑纺且,而 resources.arsc 文件就是用來記錄這些 ID 和 資源文件位置對應(yīng)關(guān)系 的文件盏道。

  • 3)、So 相關(guān):lib目錄下的文件载碌,這塊文件的優(yōu)化空間其實非常大猜嘱。

此外,還有 META-INF嫁艇,它存放了應(yīng)用的 簽名信息朗伶,其中主要有 3個文件,如下所示:

  • 1)裳仆、MANIFEST.MF:其中每一個資源文件都有一個對應(yīng)的 SHA-256-Digest(SHA1) 簽名腕让,MANIFEST.MF 文件的 SHA256(SHA1) 經(jīng)過 base64 編碼的結(jié)果即為 CERT.SF 中的 SHA256(SHA1)-Digest-Manifest 值
  • 2)、CERT.SF:除了開頭處定義的 SHA256(SHA1)-Digest-Manifest 值歧斟,后面幾項的值是對 MANIFEST.MF 文件中的每項再次 SHA256(SHA1) 經(jīng)過 base64 編碼后的值纯丸。
  • 3)、CERT.RSA:其中包含了公鑰静袖、加密算法等信息觉鼻。首先,對前一步生成的 CERT.SF 使用了 SHA256(SHA1)生成了數(shù)字摘要并使用了 RSA 加密队橙,接著坠陈,利用了開發(fā)者私鑰進行簽名。然后捐康,在安裝時使用公鑰解密仇矾。最后,將其與未加密的摘要信息(MANIFEST.MF文件)進行對比解总,如果相符贮匕,則表明內(nèi)容沒有被修改。

1.3 APK分析

1.3.1 使用 ApkTool 反編譯工具分析 APK

第一種方式花枫,就是使用 ApkTool 這個反編譯工具刻盐,它的官網(wǎng)地址如下

ApkTool 官方網(wǎng)站

其具體的 反編譯命令 如下所示:

apktool d xxx.apk

ApkTool反編譯實戰(zhàn)
1掏膏、下載并配置apktool
apktool 下載配置官方文檔

java -jar apktool_2.3.4.jar apktool d app-release.apk

反編譯完成之后,它就會在當(dāng)前的文件夾下面生成 app-release 的目錄敦锌,目錄結(jié)構(gòu)如下所示:

1.3.2 使用AS 2.2之后提供的Analyze APK

Analyze APK 具有如下功能:

  • 1)馒疹、可以直觀地查看到 APK 的組成,比如大小乙墙、占比等等颖变。
  • 2)、查看 dex 文件的組成伶丐。
  • 3)悼做、對不同的 APK 進行對比分析疯特。

我們可以 直接將電腦上的 apk 拖進 AS 中就可以自動使用 Analyze APK 打開 apk哗魂。

然后,我們就可以看到 APK 文件的絕對大小以及各組成文件的百分占比


2345截圖20200614155727.png

可以看到漓雅,Awesome-WanAndroid 應(yīng)用的 classes.dex 的大小為 3.3MB录别,總占比為 42.2%。并且邻吞,libres 目錄也有 1.9MB组题,總占比大概為 25%,因此抱冷,對于 Awesome-WanAndroid App的優(yōu)化方向就應(yīng)該是 dex 為主崔列、so 和 res 為輔 了。

此外旺遮,我們還可以查看 classes.dex 中還包含有哪些類赵讯,如下圖所示:

2345截圖20200614155941.png

我們平時在做 競品分析 的時候,就能夠很方便地來 看一下我們 App 的競品用到了哪些第三方 SDK耿眉。同時边翼,我們也可以從清單文件中很方便地查看 APK 文件的最終版本,因為 Analyze APK 能夠直接對清單文件進行解析鸣剪。

此外组底,在應(yīng)用右上角還有一個 Compare with previos APK 的按鈕,我們點擊它之后筐骇,就可以 將當(dāng)前的 APK 與別的版本的 APK 進行對比债鸡,這樣就可以對新舊兩個版本的 APK 文件大小進行對比。

1.3.3 使用 nimbledroid 進行 APK 性能分析

nimbledroid官網(wǎng)

nibledroid 是美國哥倫比亞大學(xué)的博士創(chuàng)業(yè)團隊研發(fā)出來的分析 Android App 性能指標(biāo)的系統(tǒng)铛纬,分析的方式有靜態(tài)和動態(tài)兩種方式厌均,如下所示:

  • 1)、靜態(tài)分析:可以分析出APK安裝包中大文件排行榜饺鹃,Dex 方法數(shù)和知名第三方 SDK 的方法數(shù)及占代碼整體的比例莫秆。
  • 2)间雀、動態(tài)分析:可以給出 冷啟動時間, 列出 Block UI 的具體方法, 內(nèi)存占用, 以及 Hot Methods, 從這些分析報告中, 可以 定位出具體的優(yōu)化點财松。

它的使用方式其實非常簡單辕羽,只需要直接上傳APK 即可。然后气筋,nimbledroid 網(wǎng)站的后臺就會自動對 APK 進行分析缝驳,并最終給出一份 全面的 APK 分析報告连锯。

1.3.4 使用 android-classshark 進行 APK 分析

android-classshark項目地址

android-classshark 是一個 面向 Android 開發(fā)人員的獨立二進制檢查工具,它可以 瀏覽任何的 Android 可執(zhí)行文件用狱,并且檢查出信息运怖,比如類的接口、成員變量等等夏伊,此外摇展,它還可以支持多種格式,比如說 APK溺忧、Jar咏连、Class、So 以及所有的 Android 二進制文件如清單文件等等鲁森。

android-classshark 實戰(zhàn)
首先祟滴,我們從它的 Github 地址上下載對應(yīng)的 ClassyShark.jar,地址如下所示:
ClassyShark.jar-下載地址
然后歌溉,我們雙擊打開 ClassShark.jar垄懂,拖動我們的 APK 到它的工作空間即可

接下來痛垛,我們就可以看到 Apk 的分析界面了草慧,這里我們點擊 classes 下的 classes.dex,在分析界面 左邊 可以看到該 dex 的方法數(shù)和文件大小榜晦,并且冠蒋,最下面還顯示出了該 dex 中包含有 Native Call 的類。

2345截圖20200614160847.png

我們點擊左上角的 Methods count 還可以切換到 方法數(shù)環(huán)形圖標(biāo)統(tǒng)計界面乾胶,我們不僅可以 直觀地看到各個包下的方法數(shù)和相對大小抖剿,還可以看到各個子包下的方法數(shù)和相對大小。


2345截圖20200614161038.png

二识窿、代碼瘦身方案探索

在講解如何對 Dex 進行優(yōu)化之前斩郎,可能有很多同學(xué)對 Dex 還沒有足夠的了解,這里我們就先詳細地了解下 Dex喻频。

2.1 Dex 探秘

Dex 是 Android 系統(tǒng)的可執(zhí)行文件缩宜,包含 應(yīng)用程序的全部操作指令以及運行時數(shù)據(jù)。
因為 Dalvik 是一種針對嵌入式設(shè)備而特殊設(shè)計的 Java 虛擬機,所以 Dex 文件與標(biāo)準(zhǔn)的 Class 文件在結(jié)構(gòu)設(shè)計上有著本質(zhì)的區(qū)別锻煌。

當(dāng) Java 程序被編譯成 class 文件之后妓布,還需要使用 dx 工具將所有的 class 文件整合到一個 dex 文件中.
這樣 dex 文件就將原來每個 class 文件中都有的共有信息合成了一體,這樣做的目的是 保證其中的每個類都能夠共享數(shù)據(jù)宋梧,這在一定程度上 降低了信息冗余匣沼,同時也使得 文件結(jié)構(gòu)更加緊湊

與傳統(tǒng) jar 文件相比捂龄,Dex 文件的大小能夠縮減 50% 左右释涛。


1710fc925db3af53.png

如果想深入地了解 Dex 文件格式,可以參見Google 官方教程 - Dex格式倦沧。
Dex 一般在應(yīng)用包體積中占據(jù)了不少比重唇撬,并且,Dex 數(shù)量越多展融,App 的安裝時間也會越長窖认。
有哪些方式可以優(yōu)化 Dex 這部分的體積。

2.2 ProGuard

Java 是一種跨平臺的愈污、解釋型語言耀态,而 Java 源代碼被編譯成 中間 ”字節(jié)碼” 存儲于 Class 文件之中轮傍。

那么暂雹,我們?yōu)槭裁匆褂么a混淆呢?
由于跨平臺的需要创夜,Java 字節(jié)碼 中包括了很多源代碼信息杭跪,如變量名、方法名驰吓,并且通過這些名稱來訪問變量和方法涧尿,這些 符號帶有許多語義信息,很 容易被反編譯成 Java 源代碼檬贰。

為了防止這種現(xiàn)象姑廉,我們可以使用 Java 混淆器對 Java 字節(jié)碼進行混淆。

代碼混淆也被稱為 花指令翁涤,它 將計算機程序的代碼轉(zhuǎn)換成一種功能上等價桥言,但是難以閱讀和直接理解的形式。

混淆就是對發(fā)布出去的程序進行重新組織和處理葵礼,使得處理后的代碼與處理前代碼完成相同的功能号阿,而混淆后的代碼很難被反編譯,即使反編譯成功也很難得出程序的真正語義鸳粉。

混淆器的 作用 不僅僅是 保護代碼扔涧,它也有 精簡編譯后程序大小 的作用,其 通過縮短變量和函數(shù)名以及丟失部分無用信息等方式,能使得應(yīng)用包體積減小枯夜。

2.2.1混淆的形式

目前弯汰,代碼混淆的形式主要有 三種,如下所示:

  • 1)湖雹、將代碼中的各個元素蝙泼,比如類、函數(shù)劝枣、變量的名字改變成無意義的名字汤踏。例如將 hasValue 轉(zhuǎn)換成單個的字母 a。這樣舔腾,反編譯閱讀的人就無法通過名字來猜測用途溪胶。
  • 2)、重寫 代碼中的 部分邏輯稳诚,將它變成 功能上等價哗脖,但是又 難以理解 的形式。比如它會 改變循環(huán)的指令扳还、結(jié)構(gòu)體才避。
  • 3)、打亂代碼的格式*氨距,比如多加一些空格或刪除空格桑逝,或者將一行代碼寫成多行,將多行代碼改成一行俏让。

2.2.2 Proguard 的作用

在 Android SDK 里面集成了一個工具 — Proguard楞遏,它是一個免費的 Java 類文件 壓縮、優(yōu)化首昔、混淆寡喝、預(yù)先校驗 的工具。

它的 主要作用 大概可以概括為 兩點勒奇,如下所示:

  • 1)预鬓、瘦身:它可以檢測并移除未使用到的類、方法赊颠、字段以及指令格二、冗余代碼,并能夠?qū)ψ止?jié)碼進行深度優(yōu)化巨税。最后蟋定,它還會將類中的字段、方法草添、類的名稱改成簡短無意義的名字驶兜。
  • 2)、安全:增加代碼被反編譯的難度,一定程度上保證代碼的安全抄淑。

所以說屠凶,混淆不僅是保障 Android 程序源碼安全 的 第一道門檻,而且在一定程度上肆资,使用它能夠減小 優(yōu)化字節(jié)碼 的大小矗愧。優(yōu)化字節(jié)碼 的處理流程如下圖所示

1710fc47b7819ff1.png

而它的作用具體可以細分三點,如下所示:
1郑原、壓縮(Shrinking)
默認(rèn)開啟唉韭,以減小應(yīng)用體積,移除未被使用的類和成員犯犁,并且 會在優(yōu)化動作執(zhí)行之后再次執(zhí)行属愤,因為優(yōu)化后可能會再次暴露一些未被使用的類和成員。我們可以使用如下規(guī)則來關(guān)閉壓縮:

-dontshrink 關(guān)閉壓縮

2酸役、優(yōu)化(Optimization)
默認(rèn)開啟住诸,在 字節(jié)碼級別執(zhí)行優(yōu)化,讓應(yīng)用 運行的更快涣澡。使用如下規(guī)則可進行優(yōu)化相關(guān)操作:

-dontoptimize 關(guān)閉優(yōu)化
-optimizationpasses n 表示proguard對代碼進行迭代優(yōu)化的次數(shù)贱呐,Android一般為5

3、混淆(Obfuscation)
默認(rèn)開啟入桂,增大反編譯難度奄薇,類和類成員會被隨機命名,除非用 優(yōu)化字節(jié)碼 等規(guī)則進行保護事格。使用如下規(guī)則可以關(guān)閉混淆:

-dontobfuscate 關(guān)閉混淆

Proguard 的優(yōu)化細節(jié)
Proguard 中所做的優(yōu)化包括 內(nèi)聯(lián)惕艳、修飾符、合并類和方法等 30 多種優(yōu)化項驹愚,在特定的情況下,它盡可能地做了相應(yīng)的優(yōu)化
下面列出了部分的 優(yōu)化細節(jié):

  • 1)劣纲、優(yōu)化了 Gson 庫的使用逢捺。
  • 2)、把類都標(biāo)記為 final癞季。
  • 3)劫瞳、把枚舉類型簡化為常量。
  • 4)绷柒、把一些類都垂直合并進當(dāng)前類的結(jié)構(gòu)中志于。
  • 5)、把一些類都水平合并進當(dāng)前類的結(jié)構(gòu)中废睦。
  • 6)伺绽、移除 write-only 字段。
  • 7)、把類標(biāo)記為私有的奈应。
  • 8)澜掩、把字段的值跨方法地進行傳遞。
  • 9)杖挣、把一些方法標(biāo)記為私有肩榕、靜態(tài)或 final。
  • 10)惩妇、解除方法的 synchronized 標(biāo)記株汉。
  • 11)、移除沒有使用的方法參數(shù)歌殃。

Proguard 的配置
混淆之后郎逃,默認(rèn)會在工程目錄 app/build/outputs/mapping/release 下生成一個 mapping.txt 文件,這就是 混淆規(guī)則

所以我們可以根據(jù)這個文件把混淆后的代碼反推回原本的代碼挺份。要使用混淆褒翰,我們只需配置如下代碼即可:

buildTypes {
    release {
        // 1、是否進行混淆
        minifyEnabled true
        // 2匀泊、開啟zipAlign可以讓安裝包中的資源按4字節(jié)對齊优训,這樣可以減少應(yīng)用在運行時的內(nèi)存消耗
        zipAlignEnabled true
        // 3、移除無用的resource文件:當(dāng)ProGuard 把部分無用代碼移除的時候各聘,
        // 這些代碼所引用的資源也會被標(biāo)記為無用資源揣非,然后
        // 系統(tǒng)通過資源壓縮功能將它們移除。
        // 需要注意的是目前資源壓縮器目前不會移除values/文件夾中
        // 定義的資源(例如字符串躲因、尺寸早敬、樣式和顏色)
        // 開啟后,Android構(gòu)建工具會通過ResourceUsageAnalyzer來檢查
        // 哪些資源是無用的大脉,當(dāng)檢查到無用的資源時會把該資源替換
        // 成預(yù)定義的版本搞监。主要是針對.png、.9.png镰矿、.xml提供了
        // TINY_PNG琐驴、TINY_9PNG、TINY_XML這3個byte數(shù)組的預(yù)定義版本秤标。
        // 資源壓縮工具默認(rèn)是采用安全壓縮模式來運行绝淡,可以通過開啟嚴(yán)格壓縮模式來達到更好的瘦身效果。
        shrinkResources true
        // 4苍姜、混淆文件的位置牢酵,其中 proguard-android.txt 為sdk默認(rèn)的混淆配置,
        // 它的位置位于android-sdk/tools/proguard/proguard-android.txt衙猪,
        // 此外馍乙,proguard-android-optimize.txt 也為sdk默認(rèn)的混淆配置布近,
        // 但是它默認(rèn)打開了優(yōu)化開關(guān)。并且潘拨,我們可在配置混淆文件將android.util.Log置為無效代碼吊输,
        // 以去除apk中打印日志的代碼。而 proguard-rules.pro 是該模塊下的混淆配置铁追。
        proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        signingConfig signingConfigs.release
    }
}

首先季蚂,在注釋1處,我們可以通過配置 minifyEnabled 來決定是否進行混淆琅束。

然后扭屁,在注釋2處,通過 配置 zipAlignEnabled 為 true 可以讓安裝包中的資源按 4 字節(jié)對齊涩禀,這樣可以減少應(yīng)用在運行時的內(nèi)存消耗料滥。

接著,在注釋3處艾船,配置 shrinkResources 為 true 可以移除無用的 resource 文件:當(dāng) ProGuard 把部分無用代碼移除的時候葵腹,這些代碼所引用的資源也會被標(biāo)記為無用資源,然后屿岂,系統(tǒng)會通過資源壓縮功能將它們移除践宴。需要注意的是 目前資源壓縮器目前不會移除 values / 文件夾中定義的資源(例如字符串、尺寸爷怀、樣式和顏色)阻肩。開啟后,Android 構(gòu)建工具會通過 ResourceUsageAnalyzer 來檢查哪些資源是無用的运授,當(dāng)檢查到無用的資源時會把該資源替換成預(yù)定義的版本烤惊。主要是針對 .png、.9.png吁朦、.xml 提供了 TINY_PNG柒室、TINY_9PNG、TINY_XML 這 3 個 byte 數(shù)組的預(yù)定義版本喇完。資源壓縮工具默認(rèn)是采用 安全壓縮模式 來運行伦泥,可以通過開啟 嚴(yán)格壓縮模式 來達到 更好的瘦身效果。

最后锦溪,在注釋
4處,我們可以配置混淆文件的位置府怯,其中 proguard-android.txt 為 sdk 默認(rèn)的混淆配置刻诊,它的位置位于 android-sdk/tools/proguard/proguard-android.txt,此外牺丙,proguard-android-optimize.txt 也是 sdk 默認(rèn)的混淆配置则涯,但是它 默認(rèn)打開了優(yōu)化開關(guān)复局。此外,我們也可以在配置混淆文件將 android.util.Log 置為無效代碼粟判,以去除 apk 中打印日志的代碼亿昏。而 proguard-rules.pro 是該模塊下的混淆配置。

在執(zhí)行完 ProGuard 之后档礁,ProGuard 都會在
${project.buildDir}/outputs/mapping/${flavorDir}/ 生成以下文件:

文件名 描述
dump.txt APK中所有類文件的內(nèi)部結(jié)構(gòu)
mapping.txt 提供原始與混淆過的類角钩、方法和字段名稱之間的轉(zhuǎn)換,可以通過proguard.obfuscate.MappingReader來解析
seeds.txt 列出未進行混淆的類和成員
usage.txt 列出從APK移除的代碼

下面呻澜,我們再回顧下混淆的基本規(guī)則递礼。
混淆的基本規(guī)則

# * 表示僅保持該包下的類名,而子包下的類名還是會被混淆
-keep class com.json.chao.wanandroid.*
# ** 表示把本包和所含子包下的類名都保持
-keep class com.json.chao.wanandroid.**

# 既保持類名羹幸,又保持里面的內(nèi)容不被混淆
-keep class com.json.chao.wanandroid.* {*;}

# 也可以使用Java的基本規(guī)則來保護特定類不被混淆脊髓,比如extend,implement等這些Java規(guī)則
-keep public class * extends android.app.Activity

# 保留MainPagerFragment內(nèi)部類JavaScriptInterface中的所有public內(nèi)容不被混淆
-keepclassmembers class com.json.chao.wanandroid.ui.fragment.MainPagerFragment$JavaScriptInterface {
    public *;
}

# 僅希望保護類下的特定內(nèi)容時需使用匹配符
<init>;     //匹配所有構(gòu)造器
<fields>;   //匹配所有字段
<methods>;  //匹配所有方法
# 還可以在上述匹配符前面加上private 栅受、public将硝、native等來進一步指定不被混淆的內(nèi) 容
-keep class com.json.chao.wanandroid.app.WanAndroidApp {
    public <fields>;
}
# 也可以加入?yún)?shù),以下表示用java.lang.String作為入?yún)⒌臉?gòu)造函數(shù)不會被混淆
-keep class com.json.chao.wanandroid.app.WanAndroidApp {
    public <init>(java.lang.String);
}

# 不需要保持類名屏镊,僅需要把該類下的特定成員保持不被混淆時使用keepclassmembers
# 如果擁有某成員依疼,要保留類和類成員使用-keepclasseswithmembers

需要注意的是,在 AndroidMainfest 中的類默認(rèn)不會被混淆闸衫,所以四大組件和 Application 的子類和 Framework 層下所有的類默認(rèn)不會進行混淆涛贯,并且自定義的 View 默認(rèn)也不會被混淆。
因此蔚出,我們不需要手動在 proguard-rules.pro 中去添加如下代碼:

-keep public class * extends android.app.Activity
-keep public class * extends android.app.Appliction
-keep public class * extends android.app.Service
-keep public class * extends android.content.BroadcastReceiver
-keep public class * extends android.content.ContentProvider
-keep public class * extends android.app.backup.BackupAgentHelper
-keep public class * extends android.preference.Preference
-keep public class * extends android.view.View
-keep public class com.android.vending.licensing.ILicensingService

而 Application 和四大組件是必須在 AndroidMainfest 中進行注冊的弟翘,所以如果想要通過 混淆四大組件和 Application、自定義 View 的方式去減小APK的體積是行不通的骄酗,因為沒有規(guī)則去配置如何混淆四大組件和 Application稀余。

因此,對于混淆的優(yōu)化趋翻,我們能做的只能是 盡量保證 keep 范圍的最小化睛琳,以此實現(xiàn)應(yīng)用混淆程度的最大化。

# 輸出 ProGuard 的最終配置
-printconfiguration configuration.txt

混淆實戰(zhàn)
這里踏烙,我們就對 Awesome-WanAndroid 應(yīng)用進行混淆师骗,看看該應(yīng)用混淆前后 APK的體積變化。

下面這張圖是 Awesome-WanAndroid 混淆前的 APK 組成結(jié)構(gòu)圖讨惩,可以看到占用了大概 8.3MB 的體積辟癌,其中 dex 部分占用了 3.6MB

1710fc4acc90ba93.png

混淆之后荐捻,APK 的體積會如何變化呢黍少?我們看看 混淆后的 APK 組成結(jié)構(gòu)圖寡夹,如下所示:

1710fcb76ea48d22.png

可以看到,原先兩個 dex 文件變?yōu)榱艘粋€厂置,而且 dex 的大小也縮減到了 2.2MB菩掏,大小整整縮減了 1.4MB,dex 部分的壓縮效果將近 40%。
APK 整體的壓縮效果也有 17%。

所以英上,混淆的確是 APK 瘦身的首選手段施戴。此外,在 Android Studio 3.1 或之后的版本都會默認(rèn)采用 D8 作為 Dex 的編譯器,并且,在2019年10月,被認(rèn)作為混淆的替代品的 R8 就已經(jīng)默認(rèn)集成進 Android Gradle plugin 中了沼溜。

D8 與 R8 優(yōu)化

D8 優(yōu)化優(yōu)化效果
D8 的 優(yōu)化效果 總的來說可以歸結(jié)為如下 四點:

  • 1)、Dex的編譯時間更短
  • 2)游添、.dex文件更小系草。
  • 3)、D8 編譯的 .dex 文件擁有更好的運行時性能唆涝。
  • 4)找都、包含 Java 8 語言支持的處理。
    開啟 D8
    在 Android Studio 3.0 需要主動在 gradle.properties 文件中新增:
android.enableD8 = true

Android Studio 3.1 或之后的版本 D8 將會被作為默認(rèn)的 Dex 編譯器廊酣。
R8 優(yōu)化
R8 官方文檔(目前已經(jīng)開源)
R8 是 Proguard 壓縮與優(yōu)化部分的替代品能耻,并且它仍然使用與 Proguard 一樣的 keep 規(guī)則。

如果我們僅僅想在 Android Studio 中使用 R8亡驰,當(dāng)我們在 build.gradle 中打開混淆的時候晓猛,R8 就已經(jīng)默認(rèn)集成進 Android Gradle plugin 中了。

如果我們當(dāng)前使用的是 Android Studio 3.4 或 Android Gradle 插件 3.4.0 及其更高版本凡辱,R8 會作為默認(rèn)編譯器戒职。

否則,我們 必須要在 gradle.properties 中配置如下代碼讓 App 的混淆去支持 R8透乾,如下所示:

android.enableR8=true
android.enableR8.libraries=true

那么洪燥,R8 與混淆相比優(yōu)勢在哪里呢?

ProGuard 和 R8 都應(yīng)用了基本名稱混淆:它們 都使用簡短乳乌,無意義的名稱重命名類捧韵,字段和方法。

他們還可以 刪除調(diào)試屬性汉操。

但是纫版,R8 在 inline 內(nèi)聯(lián)容器類中更有效,并且在刪除未使用的類客情,字段和方法上則更具侵略性其弊。例如,R8 本身集成在 ProGuard V6.1.1 版本中膀斋,在壓縮 apk 的大小方面梭伐,與 ProGuard 的 8.5% 相比,使用 R8 apk 尺寸減小了約 10%仰担。并且糊识,隨著 Kotlin 現(xiàn)在成為 Android 的第一語言,R8 進行了 ProGuard 尚未提供的一些 Kotlin 的特定的優(yōu)化摔蓝。

從表面上看赂苗,ProGuard 和 R8 非常相似。它們都使用相同的配置贮尉,因此在它們之間進行切換很容易拌滋。放大來看的話,它們之間也存在一些差異猜谚。R8 能更好地內(nèi)聯(lián)容器類败砂,從而避免了對象分配。但是 ProGuard 也有其自身的優(yōu)勢魏铅,具體有如下幾點:

  • 1)昌犹、ProGuard 在將枚舉類型簡化為原始整數(shù)方面會更加強大。
    它還傳遞常量方法參數(shù)览芳,這通常對于使用應(yīng)用程序的特定設(shè)置調(diào)用的通用庫很有用斜姥。ProGuard 的多次優(yōu)化遍歷通常可以產(chǎn)生一系列優(yōu)化沧竟。例如铸敏,第一遍可以傳遞一個常量方法參數(shù),以便下一遍可以刪除該參數(shù)并進一步傳遞該值屯仗。刪除日志代碼時搞坝,多次傳遞的效果尤其明顯。ProGuard 在刪除所有跟蹤(包括組成日志消息的字符串操作)方面更有效魁袜。

  • 2)桩撮、ProGuard 中應(yīng)用的模式匹配算法可以識別和替換短指令序列,從而提高代碼效率并為更多優(yōu)化打開了機會峰弹。在優(yōu)化遍歷的順序中店量,尤其是數(shù)學(xué)運算和字符串運算可從中受益。

  • 3鞠呈、最后融师,ProGuard 具有獨特的能力來優(yōu)化使用 GSON 庫將對象序列化或反序列化為 JSON 的代碼。該庫嚴(yán)重依賴反射蚁吝,這很方便旱爆,但效率低下舀射。而 ProGuard 的優(yōu)化功能可以 通過更高效,直接的訪問方式 來代替它怀伦。

如果想單獨對 Dex 或 jar 包 使用 R8脆烟,可以根據(jù)最上面的官方文檔可以很快的在 python 環(huán)境下運行起來

2.4 去除 debug 信息與行號信息

在講解什么是 deubg 信息與行號信息之前,我們需要先了解 Dex 的一些知識房待。

我們都知道邢羔,JVM 運行時加載的是 .class 文件,而 Android 為了使包大小更加緊湊桑孩、運行時更加高效就發(fā)明了 Dalvik 和 ART 虛擬機拜鹤,兩種虛擬機運行的都是 .dex 文件,當(dāng)然 ART 虛擬機還可以同時運行 oat 文件流椒。

所以 Dex 文件里的信息內(nèi)容和 Class 文件包含的信息是一樣的敏簿,不同的是 Dex 文件對 Class 中的信息做了去重,一個 Dex 包含了很多的 Class 文件镣隶,并且在結(jié)構(gòu)上有比較大的差異极谊,Class 是流式的結(jié)構(gòu),Dex 是分區(qū)結(jié)構(gòu)安岂,Dex 內(nèi)部的各個區(qū)塊間通過 offset 來進行索引轻猖。

為了在應(yīng)用出現(xiàn)問題時,我們能在調(diào)試的時候去顯示相應(yīng)的調(diào)試信息或者上報 crash 或者主動獲取調(diào)用堆棧的時候能通過 debugItem 來獲取對應(yīng)的行號域那,我們都會在混淆配置中加上下面的規(guī)則:

-keepattributes SourceFile, LineNumberTable

這樣就會保留 Dex 中的 debug 與行號信息咙边,此時的 Dex 結(jié)構(gòu)圖 如下所示:


1710fc925e9014d7.png

從圖中可以看到,Dex 文件的結(jié)構(gòu)主要分為 四大塊:header 區(qū)次员,索引區(qū)败许,data 區(qū),map 區(qū)淑蔚。

而我們的 debug 與行號信息就保存在 data 區(qū)中的 debugItems 區(qū)域市殷。

而 debug_items 里面主要包含了 兩種信息,如下所示:

  • 1)刹衫、調(diào)試的信息:包含函數(shù)的參數(shù)和所有的局部變量醋寝。
  • 2)、排查問題的信息:包含所有的指令集行號與源文件行號的對應(yīng)關(guān)系带迟。

根據(jù) Google 官方的數(shù)據(jù)音羞,debugItem 一般占 Dex 的比例有 5% 左右,如果我們能去除 debug 與行號信息仓犬,就能更進一步對 Dex 進行瘦身嗅绰,但是會失去調(diào)試信息的功能,那么,有什么方式可以去掉 debugItem窘面,同時又能讓 crash 上報的時候能拿到正確的行號呢翠语?

可以嘗試直接修改 Dex 文件,保留一小塊 debugItem民镜,讓系統(tǒng)查找行號的時候指令集行號和源文件行號保持一致啡专,這樣任何監(jiān)控上報的行號都直接變成了指令集行號。

每一個方法都會有一個 debugInfoItem制圈,每一個 debuginfoItem 里面都有一個指令集行號和源文件行號的映射關(guān)系,這了我們直接把多余的 debugInfoItem 全部刪掉畔况,只保留了一個 debugInfoItem鲸鹦,這樣所有的方法都會指向同一個 debugInfoItem,并且這個 debugInfoItem 中的指令集行號和源文件行號保持一致跷跪,這樣不管用什么方式來查找行號馋嗜,拿到的都是指令集行號。需要注意的是吵瞻,采用這種方案 需要兼容所有虛擬機的查找方式葛菇,因此 僅僅保留一個 debugInfoItem 是不夠的,需要對 debugInfoItem 進行分區(qū)橡羞,并且 debugInfoItem 表不能太大眯停。

關(guān)于如何去除 Dex 中的 Debug 信息是通過 ReDex 的 StripDebugInfoPass 來完成的,其配置如下所示:

{
    "redex" : {
        "passes" : [
            "StripDebugInfoPass",
            "RegAllocPass"
        ]
    },
    "StripDebugInfoPass" : {
        "drop_all_dbg_info" : false,
        "drop_local_variables" : true,
        "drop_line_numbers" : false,
        "drop_src_files" : false,
        "use_whitelist" : false,
        "cls_whitelist" : [],
        "method_whitelist" : [],
        "drop_prologue_end" : true,
        "drop_epilogue_begin" : true,
        "drop_all_dbg_info_if_empty" : true
    },
    "RegAllocPass" : {
        "live_range_splitting": false
    }
}

2.5 Dex 分包優(yōu)化

Dex 分包優(yōu)化原理
當(dāng)我們的 APK 過大時卿泽,Dex 的方法數(shù)就會超過65536個莺债,因此,必須采用 mutildex 進行分包签夭,但是此時每一個 Dex 可能會調(diào)用到其它 Dex 中的方法齐邦,這種 跨 Dex 調(diào)用的方式會造成許多冗余信息,具體有如下兩點:

  • 1)第租、多余的 method id:跨 Dex 調(diào)用會導(dǎo)致當(dāng)前dex保留被調(diào)用dex中的方法id措拇,這種冗余會導(dǎo)致每一個dex中可以存放的class變少,最終又會導(dǎo)致編譯出來的dex數(shù)量增多慎宾,而dex數(shù)據(jù)的增加又會進一步加重這個問題丐吓。
  • 2)、其它跨dex調(diào)用造成的信息冗余:除了需要多記錄被調(diào)用的method id之外璧诵,還需多記錄其所屬類和當(dāng)前方法的定義信息汰蜘,這會造成 string_ids、type_ids之宿、proto_ids 這幾部分信息的冗余族操。

為了減少跨 Dex 調(diào)用的情況,我們必須 盡量將有調(diào)用關(guān)系的類和方法分配到同一個 Dex 中。但是各個類相互之間的調(diào)用關(guān)系是非常復(fù)雜的色难,所以很難做到最優(yōu)的情況泼舱。

所幸的是,ReDexCrossDexDefMinimizer 類分析了類之間的調(diào)用關(guān)系枷莉,并 使用了貪心算法去計算局部的最優(yōu)解(編譯效果和dex優(yōu)化效果之間的某一個平衡點)娇昙。
使用 "InterDexPass" 配置項可以把互相引用的類盡量放在同個 Dex,增加類的 pre-verify笤妙,以此提升應(yīng)用的冷啟動速度冒掌。

在 ReDex 中使用 Dex 分包優(yōu)化跨 dex 調(diào)用造成的信息冗余的配置代碼如下所示:

{
    "redex" : {
        "passes" : [
            "InterDexPass",
            "RegAllocPass"
        ]
    },
    "InterDexPass" : {
        "minimize_cross_dex_refs": true,
        "minimize_cross_dex_refs_method_ref_weight": 100,
        "minimize_cross_dex_refs_field_ref_weight": 90,
        "minimize_cross_dex_refs_type_ref_weight": 100,
        "minimize_cross_dex_refs_string_ref_weight": 90
    },
    "RegAllocPass" : {
        "live_range_splitting": false
    },
    "string_sort_mode" : "class_order",
    "bytecode_sort_mode" : "class_order"
}

為了衡量優(yōu)化效果,我們可以使用 Dex 信息有效率 這個指標(biāo)蹲盘,公式如下所示:

Dex 信息有效率 = define methods數(shù)量 / reference methods 數(shù)量

如果 Dex 有效率在 80% 以上股毫,就說明基本合格了。
使用 ReDex 進行分包優(yōu)化召衔、去除 debug 信息及行號信息

下面铃诬,我們就使用 Redex 來對上一步生成的 app-release-proguardwithr8.apk 進行進一步的優(yōu)化。(macOS 環(huán)境下

1苍凛、首先趣席,我們需要輸入一下命令去去安裝 Xcode 命令行工具

xcode-select --install

2、然后醇蝴,使用 homebrew 安裝 redex 項目使用到的依賴庫

brew install autoconf automake libtool python3
brew install boost jsoncpp

需要注意的是宣肚,2020年2月10號版本源碼的 redex 需要的 boost 版本為 V1.71 及以上,當(dāng)你使用 brew install boost 安裝 boost 時可能獲取到的 boost 版本會低于 V1.71哑蔫,此時可能是 brew 版本需要更新钉寝,使用 brew upgrade 去更新 brew 倉庫的版本 或者可以直接從 boost 官網(wǎng)下載最新的 boost 源碼/usr/local/Cellar/ 目錄下,我當(dāng)前使用的是 boost V1.7.2源碼下載地址 中的 boost_1_72_0.zip闸迷。

3嵌纲、接著,從 Github 上獲取 ReDex 的源碼并切換到 redex 目錄下

git clone https://github.com/facebook/redex.git
cd redex

4腥沽、下一步逮走,使用 autoconf 和 make 去構(gòu)建 ReDex

# 如果你使用的是 gcc, 請使用 gcc-5
autoreconf -ivf && ./configure && make -j4
sudo make install

5、然后今阳,配置 Redex 的 config 代碼

在 Redex 在運行的時候师溅,它是根據(jù) redex/config/default.config 這個配置文件中的通道 passes 中添加不同的優(yōu)化項來對 APK 的 Dex 進行處理的,我們可以參考 redex/config/default.config 這個默認(rèn)的配置盾舌,里面的 passes 中不同的配置項都有特定的優(yōu)化墓臭。

為了優(yōu)化 App 的包體積,我們再加上 interdex_stripdebuginfo.config 中的配置項去刪除 debugInfo 和減少跨 Dex 調(diào)用的情況妖谴,最終的 interdex_stripdebuginfo.config 配置代碼 如下所示:

{
    "redex" : {
        "passes" : [
            "StripDebugInfoPass",
            "InterDexPass",
            "RegAllocPass"
        ]
    },
    "StripDebugInfoPass" : {
        "drop_all_dbg_info" : false,
        "drop_local_variables" : true,
        "drop_line_numbers" : false,
        "drop_src_files" : false,
        "use_whitelist" : false,
        "cls_whitelist" : [],
        "method_whitelist" : [],
        "drop_prologue_end" : true,
        "drop_epilogue_begin" : true,
        "drop_all_dbg_info_if_empty" : true
    },
    "InterDexPass" : {
        "minimize_cross_dex_refs": true,
        "minimize_cross_dex_refs_method_ref_weight": 100,
        "minimize_cross_dex_refs_field_ref_weight": 90,
        "minimize_cross_dex_refs_type_ref_weight": 100,
        "minimize_cross_dex_refs_string_ref_weight": 90
    },
    "RegAllocPass" : {
        "live_range_splitting": false
    },
    "string_sort_mode" : "class_order",
    "bytecode_sort_mode" : "class_order"
}

6窿锉、最后酌摇,執(zhí)行相應(yīng)的 redex 優(yōu)化命令

這里我們使用 Redex 命令對上一 Dex 優(yōu)化中得到的 app_release-proguardwithr8.apk 進行 Dex 分包優(yōu)化和去除 debugInfo,它使用了貪心這種局部最優(yōu)解的方式去減少跨 Dex 調(diào)用造成的信息冗余嗡载,命令如下所示(注意窑多,在 redex 的前面可能需要加上 Android sdk 的路徑,因為 redex 中使用到了sdk下的zipalign工具):

ANDROID_SDK=/Users/quchao/Library/Android/sdk redex --sign -s wan-android-key.jks -a wanandroid -p wanandroid -c ~/Desktop/interdex_stripdebuginfo.config -P app/proguard-rules.pro -o ~/Desktop/app-release-proguardwithr8-stripdebuginfo-interdex.apk ~/Desktop/app-release-proguardwithr8.apk

上述 redex 命令的 關(guān)鍵參數(shù)含義 如下所示:

  • --sign:對生成的apk進行簽名洼滚。
  • -s:配置應(yīng)用的簽名文件埂息。
  • -a: 配置應(yīng)用簽名的 key_alias。
  • -p:配置應(yīng)用簽名的 key_password遥巴。
  • -c:指定 redex 進行 Dex 處理時需要依據(jù)的 CONFIG 配置文件千康。
  • -o:指定生成 APK 的全路徑。

使用上面的 redex 命令我們就可以對優(yōu)化后的 APK 進行 再簽名和混淆挪哄,等待一會后(如果你的 APK 的 Dex 數(shù)量和體積很大吧秕,可能會比較久),就會生成 優(yōu)化后的 APK

2.6 使用 XZ Utils 進行 Dex 壓縮

XZ Utils 官文文檔

XZ Utils 是具有高壓縮率的免費通用數(shù)據(jù)壓縮軟件迹炼,它同 7-Zip 一樣,都是 LZMA Utils 的后繼產(chǎn)品颠毙,內(nèi)部使用了 LZMA/LZMA2 算法斯入。

LZMA 提供了高壓縮比和快速解壓縮,因此非常適合嵌入式應(yīng)用蛀蜜。LZMA 的 主要功能 如下:

  • 1)刻两、壓縮速度:在3 GHz雙核CPU上為3 MB / s。
  • 2)滴某、減壓速度:在現(xiàn)代3 GHz CPU(Intel磅摹,AMD,ARM)上為20-50 MB / s霎奢。在簡單的1 GHz RISC - CPU(ARM户誓,MIPS,PowerPC)上為5-15 MB / s幕侠。
  • 3)帝美、解壓縮的較小內(nèi)存要求:8-32 KB + DictionarySize。
  • 4)晤硕、用于解壓縮的代碼大械刻丁:2-8 KB(取決于速度優(yōu)化)。

相對于典型的壓縮文件而言舞箍,XZ Utils 的輸出比 gzip 小 30%舰褪,比 bzip2 小 15%。在 FaceBook 的 App 中就使用了 Dex 壓縮 的方式疏橄,而且它 將 Dex 壓縮后的文件都放在了 assets 目錄中占拍,如下圖所示:


1710fc4bcbc338cc.png
1710fc84a4248c10.png

我們先看到上圖中的 classes.dex,其中僅包含了啟動時要用到的類,這樣可以為 Dex 壓縮文件 secondary.dex.jar.xzs 的解壓爭取時間刷喜。

此外残制,在 secondary.dex.jar.xzs 文件的下面,我們注意到掖疮,有一系列的 secondary-x.dex.jar.xzs.tmp~.meta 文件初茶,它保存了壓縮前每一個 Dex 文件的映射元數(shù)據(jù)信息,在應(yīng)用首次啟動解壓的時候我們還需要用到它浊闪。

盡管 classes.dex 為首次啟動解壓 Dex 壓縮文件爭取了時間恼布,但是由于文件太大,在低端機上的解壓時間可能會有 3~5s搁宾。

而且折汞,當(dāng) Dex 非常多的時候會增加應(yīng)用的安裝時間,如果還使用了壓縮 Dex 的方式盖腿,那么首次生成 ODEX 的時間可能就會超過1分鐘爽待。

為了解決這個問題,F(xiàn)acebook 使用了 oatmeal 這套工具去 根據(jù) ODEX 文件的格式翩腐,自己生成了一個 ODEX 文件鸟款。而在 正常的流程 下,系統(tǒng)會使用 fork 子進程的方式去處理 dex2oat 的過程茂卦。

但是何什,oatmeal 采用了 代理 dex2oat 省去 fork 進程所帶來耗時 的這種方式,如果在1個 10MB 的 Dex等龙,可以將 dex2oat 的耗時降至 100ms处渣,而在 Android 5.0 上生成一個 ODEX 的耗時大約在 10 秒以上,在 Android 8.0 使用 speed 模式也需要 1 秒左右的時間蛛砰。但是由于 每個 Android 系統(tǒng)版本的 ODEX 格式都有一些差異罐栈,oatmeal 需要分版本適配,因此 Dex 壓縮的方案我們可以先壓壓箱底暴备。

2.7 三方庫處理

實際的開發(fā)過程中悠瞬,我們會用到各種各樣的三方庫。尤其當(dāng)項目變大之后涯捻,開發(fā)人員眾多浅妆,因此引入的三方庫也會非常多,比如說障癌,有人引入了一個 Fresco 圖片庫凌外,然后這個庫你可能不熟悉,你會引入一個 Glide涛浙,并且另一個人它可能又會引入他熟悉的圖片庫 Picasso康辑,所以項目中可能會存在多個相同功能的三方 SDK摄欲,這一點,在大型項目當(dāng)中一定會存在疮薇。

因此胸墙,我們在做代碼瘦身的時候,需要將三方庫進行統(tǒng)一按咒,比如說 將圖片加載庫迟隅、網(wǎng)絡(luò)庫、數(shù)據(jù)庫以及其他基礎(chǔ)庫進行統(tǒng)一励七,去掉冗余的庫智袭。

同時,在選擇第三方 SDK 的時候掠抬,我們可以將包大小作為選擇的指標(biāo)之一吼野,我們應(yīng)該 盡可能地選擇那些比較小的庫來實現(xiàn)相同的功能。

例如两波,對于圖片加載功能來說瞳步,Picasso、Glide腰奋、Fresco 它們都可以實現(xiàn)谚攒,但是你引入 Fresco 之后會導(dǎo)致包大小增加很多,而 Picasso 卻只增加了不到 100kb氛堕,所以引入不同的三方 SDK 對包大小的影響是不一樣的。

這里野蝇,我們可以使用 AS 插件 Android Methods Count讼稚,安裝之后,它會自動在 build.gradle 文件中顯示你引入的三方庫的方法數(shù)绕沈。

最后锐想,如果我們引入三方庫的時候,可以 只引入部分需要的代碼乍狐,而不是將整個包的代碼都引入進來赠摇。

很多庫的代碼結(jié)構(gòu)都設(shè)計的比較好,比如 Fresco浅蚪,它將圖片加載的各個功能藕帜,如 webp、gif 功能進行了剝離惜傲,它們都處于單個的庫當(dāng)中洽故。如果我們只需要 Fresco 的 webp 功能,那我們可以將除 webp 之外的別的庫都給刪掉盗誊,這樣你引入的三方庫就很小了,包大小就降下來了寥袭。

如下所圖所示拱镐,我們可以僅僅保留 Fresco 的 webp 功能,其它依賴都可以去掉梨熙。


1710fc2e8b76d5f4.png

如果你引入的三方庫 沒有進行過結(jié)構(gòu)剝離,就需要 修改源碼刀诬,只提取出來你需要的功能即可咽扇。

2.8 移除無用代碼

移除無用代碼時我們經(jīng)常會碰到下面兩個問題:

  • 1)、業(yè)務(wù)代碼只增不減舅列。
  • 2)肌割、代碼太多不敢刪除。

這里帐要,有一個很好的方法可以 準(zhǔn)確地判斷哪些類在線上環(huán)境下用戶肯定不會用到了把敞。

我們可以通過 AOP 的方式來做,對于 Activity 來說榨惠,其實非常簡單奋早,我們只需要 在每個 Activity 的 onCreate 當(dāng)中加上統(tǒng)計 即可,然后到了線上之后赠橙,如果這個 Activity 被統(tǒng)計了耽装,就說明它還在被使用。

而對于那些 不是 Activity 的類期揪,我們可以 利用 AOP 來切它們的構(gòu)造函數(shù)掉奄,一個類如果它被使用,那它的構(gòu)造函數(shù)肯定會被調(diào)用到凤薛。例如姓建,下面就是 使用 AspectJ 對某個包下的類進行構(gòu)造函數(shù)切面 的代碼:

@After("execution(org.jay.launchstarter.Task.new(..)")
public void newObject(JoinPoint point) {
    LogHelper.i(" new " + point.getTarget().getClass().getSimpleName());
}

其中,new 表示是 切的構(gòu)造函數(shù)缤苫,括號中的 .. 表示的是 匹配所有構(gòu)造參數(shù)速兔。

此外,我們也可以直接使用 coverage 插件 來做 線上無用代碼分析活玲,需要注意的是涣狗,在注冊上報數(shù)據(jù)的時候記得把服務(wù)器名改為自己的

最后舒憾,我們也可以在線下使用 Simian工具 或者 Lint 來 掃描出重復(fù)的代碼镀钓。

使用 Lint 檢測無效代碼

步驟:點擊菜單欄 Analyze -> Run Inspection by Name -> unused declaration -> Moudule ‘a(chǎn)pp’ -> OK

2.9 避免產(chǎn)生 Java access 方法

access 方法是什么?
為了能提供內(nèi)部類和其外部類直接訪問對方的私有成員的能力珍剑,又不違反封裝性要求掸宛,Java 編譯器在編譯過程中自動生成 package 可見性的靜態(tài) access$xxx 方法,并且在需要訪問對方私有成員的地方改為調(diào)用對應(yīng)的 access 方法招拙。

避免產(chǎn)生 access 方法的方式

  • 1)唧瘾、在開發(fā)過程中需要注意在可能產(chǎn)生 access 方法的情況下適當(dāng)調(diào)整措译,比如去掉 private,改為 package 可見性饰序。
  • 2)领虹、使用 ASM 在編譯時刪除生成的 access 方法。

因為優(yōu)化效果不是很明顯求豫,這里就不多介紹了
西瓜視頻apk瘦身之 Java access 方法刪除

此外塌衰,在 ReDex 中也提供了 access-marking 這個功能去除代碼中的 Access 方法,并且蝠嘉,在 ReDex 還有 type-erasure 的功能最疆,它 與 access-marking 的優(yōu)化效果一樣,不僅能減少包大小蚤告,也能提升 App 的啟動速度努酸。

2.10 利用 ByteX Gradle 插件平臺中的代碼優(yōu)化插件

如果你想在項目的編譯階段去除 access 方法,這里我更加建議直接使用 ByteXaccess_inline 插件杜恰。
除了 access_inlie 之外获诈,在 ByteX 中還有 四個 很實用的代碼優(yōu)化 Gradle 插件可以幫助我們有效減小 Dex 文件的大小,如下所示:

2.11 小結(jié)

回顧下我們上述使用的各種 Dex 優(yōu)化方式,其中镣屹,不少優(yōu)化項都使用到了 ReDex。對于 ReDex 來說价涝,目前它提供的比較強大的功能有 五種女蜈,分別如下所示:

  • 1)、Interdex:類重排和文件重排色瘩、Dex 分包優(yōu)化伪窖。其中對于類重排和文件重排,Google 在 Android 8.0 的時候引入了 Dexlayout居兆,它是一個用于分析 dex 文件覆山,并根據(jù)配置文件對其進行重新排序的庫。
    與 ReDex 類似泥栖,Dexlayout 通過將經(jīng)常一起訪問的部分 dex 文件集中在一起簇宽,程序可以因改進文件位置從而擁有更好的內(nèi)存訪問模式勋篓,以節(jié)省 RAM 并縮短啟動時間。
    不同于ReDex的是它使用了運行時配置信息對 Dex 文件的各個部分進行重新排序魏割。
    因此譬嚣,只有在應(yīng)用運行之后,并在系統(tǒng)空閑維護的時候才會將 dexlayout 集成到 dex2oat 的設(shè)備進行編譯钞它。

  • 2)拜银、Oatmeal:直接生成 Odex 文件。

  • 3)遭垛、StripDebugInfo:去除 Dex 中的 Debug 信息尼桶。

  • 4)、源碼中 access-marking 模塊:刪除 Java access 方法

  • 5)锯仪、源碼中 type-erasure 模塊:類型擦除泵督。
    可以看到,ReDex 的功能非常強大卵酪,如果能夠深入了解 ReDex 源碼中的各個功能模塊的實現(xiàn)幌蚊,你將具有非常強硬的技術(shù)資本。

最近溃卡,抖音 Android 團隊 已經(jīng)將上述部分模塊的實現(xiàn)以 Gradle Transform + ASM 的形式集成進了 ByteX

最后溢豆,還有一些 代碼編寫方面的優(yōu)化,如可以在開發(fā)過程 盡量減少 enum 的使用瘸羡,每減少一個 enum 可以減少大約 1.0 到 1.4 KB 的大小漩仙。

3 資源瘦身方案探索

眾所周知,Android 構(gòu)建工具鏈中使用了 AAPT/AAPT2 工具來對資源進行處理犹赖,Manifest队他、Resources、Assets 的資源經(jīng)過相應(yīng)的 ManifesMerger峻村、ResourcesMerger麸折、AssetsMerger 資源合并器將多個不同 moudule 的資源合并為了 MergedManifest、MergedResources粘昨、MergedAssets垢啼。然后,它們被 AAPT 處理后生成了 R.java张肾、Proguard Configuration芭析、Compiled Resources。如下圖左上方所示

1710fc302da5fd87.png

其中 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_
    在經(jīng)過 zip 解壓之后,可以發(fā)現(xiàn)它 包含了res芍秆、AndroidManifest.xml和resources.arsc 這三部分惯疙。
    且翠勉,從上面的 APK 構(gòu)建流程中可以得知,Compiled Resources 會被 apkbuilder 打包到 APK 包中螟碎,它其實就是 APK 的資源包眉菱。
    因此,我們可以 通過 Compiled Resources 文件來修改不同后綴文件資源的壓縮方式來達到瘦身效果的掉分。
    但是需要注意的是俭缓,resources.arsc 文件最好不要壓縮存儲,如果壓縮會影響一定的性能酥郭,尤其是在冷啟動時間方面造成的影響华坦。并且,如果在 Android 6.0 上開啟了 android:extractNativeLibs=”false” 的話不从,So 文件也不能被壓縮惜姐。

3.1 冗余資源優(yōu)化

3.1.1 使用 Lint 的 Remove Unused Resource**

APK 的資源主要包括圖片、XML椿息,與冗余代碼一樣歹袁,它也可能遺留了很多舊版本當(dāng)中使用而新版本中不使用的資源,這點在快速開發(fā)的 App 中更可能出現(xiàn)寝优。我們可以通過點擊右鍵条舔,選中 Refactor,然后點擊 Remove Unused Resource => preview 可以預(yù)覽找到的無用資源乏矾,點擊 Do Refactor 可以去除冗余資源孟抗。


1710fc4f66620859.png

需要注意的,Android Lint 不會分析 assets 文件夾下的資源钻心,因為 assets 文件可以通過文件名直接訪問凄硼,不需要通過具體的引用,Lint 無法判斷資源是否被用到捷沸。

3.1.2 優(yōu)化 shrinkResources 流程真正去除無用資源

resources.arsc 中可能會存在很多 無用的資源映射摊沉,我們可以使用 android-arscblamer,它是一個命令行工具痒给,能夠 解析 resources.arsc 文件并檢查出可以優(yōu)化的部分坯钦,比如一些空的引用。
此外侈玄,當(dāng)我們通過 shrinkResources true 來 開啟資源壓縮,資源壓縮工具只會把無用的資源替換成預(yù)定義的版本而不是移除吟温。那么序仙,如何高效地對無用資源自動進行去除呢?
我們可以 在 Android 構(gòu)建工具執(zhí)行 package${flavorName}Task 之前通過修改 Compiled Resources 來實現(xiàn)自動去除無用資源鲁豪,具體的實現(xiàn)原理如下:
1)潘悼、首先律秃,收集 Compiled Resources 中被替換的預(yù)定義版本的資源名稱
通過查看 Zip 格式資源包中每個 ZipEntry 的 CRC-32 checksum 來尋找被替換的預(yù)定義資源,預(yù)定義資源的 CRC-32 定義在 ResourceUsageAnalyze 中治唤,如下所示:

// A 1x1 pixel PNG of type BufferedImage.TYPE_BYTE_GRAY
public static final long TINY_PNG_CRC = 0x88b2a3b0L;

// A 3x3 pixel PNG of type BufferedImage.TYPE_INT_ARGB with 9-patch markers
public static final long TINY_9PNG_CRC = 0x1148f987L;

// The XML document <x/> as binary-packed with AAPT
public static final long TINY_XML_CRC = 0xd7e65643L;

2)棒动、然后,使用 android-chunk-utils 把 resources.arsc 中對應(yīng)的定義移除宾添。
3)船惨、最后,刪除資源包中對應(yīng)的資源文件即可缕陕。

3.2 重復(fù)資源優(yōu)化.

在大型 App 項目的開發(fā)中粱锐,一個 App 一般會有多個業(yè)務(wù)團隊進行開發(fā),其中每個業(yè)務(wù)團隊在資源提交時的資源名稱可能會有重復(fù)的扛邑,這將會 引發(fā)資源覆蓋的問題怜浅,因此,每個業(yè)務(wù)團隊都會為自己的 資源文件名添加前綴蔬崩。

這樣就導(dǎo)致了這些資源文件雖然 內(nèi)容相同恶座,但因為 名稱的不同而不能被覆蓋,最終都會被集成到 APK 包中沥阳。

這里跨琳,我們還是可以 在 Android 構(gòu)建工具執(zhí)行 package${flavorName}Task 之前通過修改 Compiled Resources 來實現(xiàn)重復(fù)資源的去除,具體放入實現(xiàn)原理可細分為如下三個步驟:

  • 1)沪袭、首先湾宙,通過資源包中的每個ZipEntry的CRC-32 checksum來篩選出重復(fù)的資源。
  • 2)冈绊、然后侠鳄,通過android-chunk-utils修改resources.arsc,把這些重復(fù)的資源都重定向到同一個文件上死宣。
  • 3)伟恶、最后,把其它重復(fù)的資源文件從資源包中刪除毅该,僅保留第一份資源博秫。

具體的實現(xiàn)代碼如下所示:

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并刪除重復(fù)資源
                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.3 圖片壓縮

一般來說眶掌,1000行代碼在APK中才會占用 5kb 的空間挡育,而圖片呢,一般都有 100kb 左右朴爬,所以說即寒,對圖片做壓縮,它的收益明顯是更大的,而往往處于快速開發(fā)的 App 沒有相關(guān)的開發(fā)規(guī)范母赵,UI 設(shè)計師或開發(fā)同學(xué)如果忘記了添加圖片時進行壓縮逸爵,添加的就是原圖,那么包體積肯定會增大很多凹嘲。

對于圖片壓縮师倔,我們可以在 tinypng 這個網(wǎng)站進行圖片壓縮,但是如果 App 的圖片過多周蹭,一個個壓縮也是很麻煩的趋艘。

因此,我們可以 使用 McImage谷醉、TinyPngPluginTinyPIC_Gradle_Plugin 來對圖片進行自動化批量壓縮致稀。

在 Android 的構(gòu)建流程中,AAPT 會使用內(nèi)置的壓縮算法來優(yōu)化 res/drawable/ 目錄下的 PNG 圖片俱尼,但這可能會導(dǎo)致本來已經(jīng)優(yōu)化過的圖片體積變大抖单,因此,可以通過在 build.gradle 中 設(shè)置 cruncherEnabled 來禁止 AAPT 來優(yōu)化 PNG 圖片遇八,代碼如下所示:

aaptOptions {
    cruncherEnabled = false
}

此外矛绘,我們還要注意對圖片格式的選擇,對于我們普遍使用更多的 png 或者是 jpg 格式來說刃永,相同的圖片轉(zhuǎn)換為 webp 格式之后會有大幅度的壓縮货矮。

對于 png 來說,它是一個無損格式斯够,而 jpg 是有損格式囚玫。jpg 在處理顏色圖片很多時候根據(jù)壓縮率的不同,它有時候會去掉我們?nèi)庋圩R別差距比較小的顏色读规,但是 png 會嚴(yán)格地保留所有的色彩抓督。

所以說,在圖片尺寸大束亏,或者是色彩鮮艷的時候铃在,png 的體積會明顯地大于 jpg。

3.4使用針對性的圖片格式

在 Google I/O 2016 中碍遍,講到了如何選擇相應(yīng)的圖片格式.

首先定铜,如果能用 VectorDrawable 來表示的話,則優(yōu)先使用 VectorDrawable怕敬;否則揣炕,看是否支持 WebP,支持則優(yōu)先用 WebP东跪;如果也不能使用 WebP畸陡,則優(yōu)先使用 PNG矮烹,而 PNG 主要用在展示透明或者簡單的圖片,對于其它場景可以使用 JPG 格式罩锐。

VD(純色icon)->WebP(非純色icon)->Png(更好效果) ->jpg(若無alpha通道)

使用矢量圖片之后,它能夠有效的減少應(yīng)用中圖片所占用的大小卤唉,矢量圖形在 Android 中表示為 VectorDrawable 對象涩惑。

它 僅僅需100字節(jié)的文件即可以生成屏幕大小的清晰圖像,但是桑驱,Android 系統(tǒng)渲染每個 VectorDrawable 對象需要大量的時間竭恬,而較大的圖像需要更長的時間。

它 僅僅需100字節(jié)的文件即可以生成屏幕大小的清晰圖像熬的,但是痊硕,Android 系統(tǒng)渲染每個 VectorDrawable 對象需要大量的時間,而較大的圖像需要更長的時間押框。

最后岔绸,如果要在項目中使用 VD,則以下幾點需要著重注意:

  • 1)橡伞、必須通過 app:arcCompat 屬性來使用 svg盒揉,如果通過 src,則在低版本手機上會出現(xiàn)不兼容的問題兑徘。
  • 2)刚盈、可能會不兼容selector,在 Activity 中手動兼容即可挂脑,兼容代碼如下所示:
    static { AppCompatDelegate.setCompatVectorFromResourcesEnabled(true) }
  • 3)藕漱、不兼容第三方庫。
  • 4)崭闲、性能問題:當(dāng)Vector比較簡單時肋联,效率肯定比Bitmap高,復(fù)雜則效率會不如Bitmap镀脂。
  • 5)牺蹄、不便于管理:建議原則為同目錄多類型文件,以前綴區(qū)別薄翅,不同目錄相同類型文件沙兰,以意義區(qū)分。

與 VD 類似翘魄,還有一種矢量圖標(biāo) iconFont鼎天,即 字體圖標(biāo),圖標(biāo)就在字體文件里面暑竟,它看著是個圖標(biāo)斋射,其實卻是個文字育勺。它的 優(yōu)勢 有如下三個方面:

  • 1)、同 VD 一樣罗岖,由于 IconFont 是矢量圖標(biāo),所以可以輕松解決圖標(biāo)適配問題涧至。
  • 2)、圖標(biāo)以 .ttf 字體文件的形式存在項目中桑包,而 .ttf 文件一般放在 assets 文件夾下,它的體積很小南蓬,可以減小 APK 的體積。
  • 3)哑了、一套圖標(biāo)資源可以在不同平臺使用且資源維護方便赘方。

它的 缺點 也很明顯,大致有如下三個方面:

  • 1)弱左、需要自定義 svg 圖片窄陡,并將其轉(zhuǎn)換為 ttf 文件,圖標(biāo)制作成本比較高拆火。
  • 2)跳夭、添加圖標(biāo)時需要重新制作 ttf 文件。
  • 3)榜掌、只能支持單色优妙,不支持漸變色圖標(biāo)。

如果你想要使用 iconfont憎账,可以在阿里的 iconfont 上尋找資源套硼。此外,使用 Android-Iconics 可以在你的應(yīng)用中便于使用任何的 iconfont 或 .svg 圖片作為 drawable胞皱。最后邪意,如果我們 僅僅想提取僅需要的美化文字,以壓縮 assets 下的字體文件大小反砌,可以使用 FontZip 字體提取工具雾鬼。

如果不是純色小 icon 類型的圖片,則建議使用 WebP宴树。只要你的 App 的 minSdkVersion 高于 14(Android 4.0+) 即可策菜。WebP 不僅支持透明度,而且壓縮率比 JPEG 更高酒贬,在相同畫質(zhì)下體積更小又憨。

但是,只有 Android 4.2.1+ 才支持顯示含透明度的 WebP锭吨,此外蠢莺,它的 兼容性不好,并且不便于預(yù)覽零如,需使用瀏覽器打開躏将。

對于應(yīng)用之前就存在的圖片锄弱,我們可以使用 PNG轉(zhuǎn)換WebP 的轉(zhuǎn)換工具來進行轉(zhuǎn)換。但是祸憋,一個一個轉(zhuǎn)換開發(fā)效率太低会宪,因此我們可以 使用WebpConvert_Gradle_Plugin 這個 gradle 插件去批量進行轉(zhuǎn)換,它的實現(xiàn)原理是 在 mergeXXXResource Task 和 processXXXResource Task 之間插入了一個 WebpConvertPlugin task 去將 png蚯窥、jpg 圖片批量替換成了 webp 圖片狈谊。

此外,在 Gradle 構(gòu)建 APK 的過程中沟沙,我們可以判斷當(dāng)前 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)
            }
        }
    }
}   

最后壁榕,這里再補充下在平時項目開發(fā)中對 圖片放置優(yōu)化的大概思路矛紫,如下所示:

  • 1)、聊天表情出一套圖 => hdpi牌里。
  • 2)颊咬、純色小 icon 使用 VD => raw。
  • 3)牡辽、背景大圖出一套 => xhdpi喳篇。
  • 4)、logo 等權(quán)重比較大的圖片出兩套 => hdpi态辛,xhdpi麸澜。
  • 5)檩禾、若某些圖在真機中有異常欢峰,則用多套圖。
  • 6)狼渊、若遇到奇葩機型熟史,則針對性補圖馁害。

3.5 資源混淆

同代碼混淆類似,資源混淆將 資源路徑混淆成單個資源的路徑蹂匹,這里我們可以使用 AndroidResGuard碘菜,它可以使冗余的資源路徑變短,例如將 res/drawable/wechat 變?yōu)?r/d/a限寞。

AndroidResGuard 項目地址

下面忍啸,我們就使用 AndroidResGuard 來對資源進行混淆。

3.5.1昆烁、AndroidResGuard 實戰(zhàn)

1吊骤、首先,我們在項目的根 build.gradle 文件下加入下面的插件依賴:

classpath 'com.tencent.mm:AndResGuard-gradle-plugin:1.2.17'

2静尼、然后白粉,在項目 module 下的 build.gradle 文件下引入其插件:

apply plugin: 'AndResGuard'

3传泊、接著,加入 AndroidResGuard 的配置項鸭巴,如下是默認(rèn)設(shè)置好的配置:

andResGuard {
    // mappingFile = file("./resource_mapping.txt")
    mappingFile = null
    use7zip = true
    useSign = true
    // 打開這個開關(guān)眷细,會keep住所有資源的原始路徑,只混淆資源的名字
    keepRoot = false
    // 設(shè)置這個值鹃祖,會把arsc name列混淆成相同的名字溪椎,減少string常量池的大小
    fixedResName = "arg"
    // 打開這個開關(guān)會合并所有哈希值相同的資源,但請不要過度依賴這個功能去除去冗余資源
    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"
    }

    /**
    * 可選: 如果不設(shè)置則會默認(rèn)覆蓋assemble輸出的apk
    **/
    // finalApkBackupPath = "${project.rootDir}/final.apk"

    /**
    * 可選: 指定v1簽名時生成jar文件的摘要算法
    * 默認(rèn)值為“SHA-1”
    **/
    // digestalg = "SHA-256"
}

3.5.2 AndResGuard 的資源混淆原理

資源混淆工具主要是通過 短路徑的優(yōu)化恬口,以達到 減少 resources.arsc校读、metadata 簽名文件以及 ZIP 文件大小 的效果,其效果分別如下所示:

  • 1)祖能、resources.arsc:它記錄了資源文件的名稱與路徑歉秫,使用混淆后的短路徑 res/s/a,可以減少文件的大小
  • 2)养铸、metadata 簽名文件:簽名文件 MANIFEST.MF 與 CERT.SF 需要記錄所有文件的路徑以及它們的哈希值雁芙,使用短路徑可以減少這兩個文件的大小。
  • 3)钞螟、ZIP 文件:ZIP 文件格式里面通過其索引記錄了每個文件 Entry 的路徑兔甘、壓縮算法、CRC鳞滨、文件大小等等信息洞焙。短路徑的優(yōu)化減少了記錄文件路徑的字符串大小。

3.5.3 AndResGuard 的極限壓縮原理

AndResGuard 使用了 7-Zip 的大字典優(yōu)化拯啦,APK 的 整體壓縮率可以提升 3% 左右闽晦。

并且,它還支持針對 resources.arsc提岔、PNG仙蛉、JPG 以及 GIF 等文件進行強制壓縮(在編譯過程中,這些文件默認(rèn)不會被壓縮)碱蒙。

那么荠瘪,為什么 Android 系統(tǒng)不會去壓縮這些文件呢?主要基于以下 兩點原因:

  • 1)赛惩、壓縮效果不明顯:上述格式的文件大部分已經(jīng)被壓縮過哀墓,因此,重新做 Zip 壓縮效果并不明顯喷兼。比如 重新壓縮 PNG 和 JPG 格式只能減少 3%~5% 的大小篮绰。
  • 2)、基于讀取時間和內(nèi)存的考慮:針對于 沒有進行壓縮的文件季惯,系統(tǒng)可以使用 mmap 的方式直接讀取吠各,而不需要一次性解壓并放在內(nèi)存中臀突。
    此外,抖音 Android 團隊還開源了針對于海外市場 App Bundle APK 的 AabResGuard 資源混淆工具

3.6 R Field 的內(nèi)聯(lián)優(yōu)化

我們可以通過內(nèi)聯(lián) R Field 來進一步對代碼進行瘦身贾漏,此外候学,它也解決了 R Field 過多導(dǎo)致 MultiDex 65536 的問題。

要想實現(xiàn)內(nèi)聯(lián) R Field纵散,我們需要 通過 Javassist 或者 ASM 字節(jié)碼工具在構(gòu)建流程中內(nèi)聯(lián) 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伍掀。

它的實現(xiàn)原理為:android 中的 R 文件掰茶,除了 styleable 類型外,所有字段都是 int 型變量/常量蜜笤,且在運行期間都不會改變符匾。所以可以在編譯時,記錄 R 中所有字段名稱及對應(yīng)值瘩例,然后利用 ASM 工具遍歷所有 Class,將除 R$styleable.class 以外的所有 R.class 刪除掉甸各,并且在引用的地方替換成對應(yīng)的常量垛贤,從而達到縮減包大小和減少 Dex 個數(shù)的效果。

此外趣倾,最近 ByteX 也增加了 shrink_r_classgradle 插件聘惦,它不僅可以在編譯階段對 R 文件常量進行內(nèi)聯(lián),而且還可以 針對 App 中無用 Resource 和無用 assets 的資源進行檢查儒恋。

3.7 資源合并方案

我們可以把所有的資源文件合并成一個大文件善绎,而 一個大資源文件就相當(dāng)于換膚方案中的一套皮膚。

它的效果 比資源混淆的效果會更好诫尽,但是禀酱,在此之前,必須要解決 解析資源 與 管理資源 的問題牧嫉。其相應(yīng)的解決方案如下所示:

  • 模擬系統(tǒng)實現(xiàn)資源文件的解析:我們需要使用自定義的方式把 PNG剂跟、JPG 以及 XML 文件轉(zhuǎn)換為 Bitmap 或者 Drawable。
  • 使用 mmap 加載大資源與資源緩存池管理資源:使用 mmap 加載大資源的方式可以充分減少啟動時間與系統(tǒng)內(nèi)存的占用酣藻。而且曹洽,需要使用 Glide 等圖片框架的資源緩存池 ResourceCache 去釋放不再使用的資源文件。

3.8 資源文件最少化配置

我們需要 根據(jù) App 目前所支持的語言版本去選用合適的語言資源辽剧,例如使用了 AppCompat送淆,如果不做任何配置的話,最終 APK 包中會包含 AppCompat 中所有已翻譯語言字符串怕轿,無論應(yīng)用的其余部分是否翻譯為同一語言偷崩。

對此辟拷,我們可以 通過 resConfig 來配置使用哪些語言,從而讓構(gòu)建工具移除指定語言之外的所有資源环凿。同理梧兼,也可以使用 resConfigs 去配置你應(yīng)用需要的圖片資源文件類,如 "xhdpi"智听、"xxhdpi" 等等羽杰,代碼如下所示:

android {
    ...
    defaultConfig {
        ...
        resConfigs "zh", "zh-rCN"
        resConfigs "nodpi", "hdpi", "xhdpi", "xxhdpi", "xxxhdpi"
    }
    ...
}    

此外,我們還以 利用 Density Splits 來選擇應(yīng)用應(yīng)兼容的屏幕尺寸大小到推,代碼如下所示:

android {
    ...
    splits {
        density {
            enable true
            exclude "ldpi", "tvdpi", "xxxhdpi"
            compatibleScreens 'small', 'normal', 'large', 'xlarge'
        }
    }
    ...
}

3.9 盡量每張圖片只保留一份

比如說考赛,我們統(tǒng)一只把圖片放到 xhdpi 這個目錄下,那么 在不同的分辨率下它會做自動的適配莉测,即 等比例地拉伸或者是縮小颜骤。

3.10 資源在線化

我們可以 將一些圖片資源放在服務(wù)器,然后 結(jié)合圖片預(yù)加載 的技術(shù)手段捣卤,這些 既可以滿足產(chǎn)品的需要忍抽,同時可以減小包大小。

3.11 統(tǒng)一應(yīng)用風(fēng)格

如設(shè)定統(tǒng)一的 字體董朝、尺寸鸠项、顏色和按鈕按壓效果、分割線 shape子姜、selector 背景 等等祟绊。

4、 So 瘦身方案探索

對于主要由 C/C++ 實現(xiàn)的 Native Library 而言哥捕,常規(guī)的優(yōu)化方式就是 去除 Debug 信息牧抽,使用 C++_shared 等等。下面遥赚,對于 So 瘦身扬舒,我們看看還有哪些方案。

4.1 So 移除方案

So 是 Android 上的動態(tài)鏈接庫凫佛,在我們 Android 應(yīng)用開發(fā)過程中呼巴,有時候 Java 代碼不能滿足需求,比如一些 加解密算法或者音視頻編解碼功能御蒲,這個時候就必須要通過 C 或者是 C++ 來實現(xiàn)衣赶,之后生成 So 文件提供給 Java 層來調(diào)用,在生成 So 文件的時候就需要考慮生成市面上不同手機 CPU 架構(gòu)的文件厚满。

目前府瞄,Android 一共 支持7種不同類型的 CPU 架構(gòu),比如常見的 armeabi、armeabi-v7a遵馆、X86 等等鲸郊。

理論上來說,對應(yīng)架構(gòu)的 CPU 它的執(zhí)行效率是最高的货邓,但是這樣會導(dǎo)致 在 lib 目錄下會多存放了各個平臺架構(gòu)的 So 文件秆撮,所以 App 的體積自然也就更大了。

因此换况,我們就需要對 lib 目錄進行縮減职辨,我們 在 build.gradle 中配置這個 abiFiliters 去設(shè)置 App 支持的 So 架構(gòu),其配置代碼如下所示:

defaultConfig {
    ndk {
        abiFilters "armeabi"
    }
}

一般情況下戈二,應(yīng)用都不需要用到 neon 指令集舒裤,我們只需留下 armeabi 目錄就可以了。因為 armeabi 目錄下的 So 可以兼容別的平臺上的 So觉吭,相當(dāng)于是一個萬金油腾供,都可以使用。但是鲜滩,這樣 別的平臺使用時性能上就會有所損耗伴鳖,失去了對特定平臺的優(yōu)化。

4.2 So 移除方案優(yōu)化版

上面我們說到了想要完美支持所有類型的設(shè)備代價太大徙硅,那么榜聂,我們能不能采取一個 折中的方案,就是 對于性能敏感的模塊闷游,它使用到的 So,我們都放在 armeabi 目錄當(dāng)中隨著 Apk 發(fā)出去贴汪,然后我們在代碼中來判斷一下當(dāng)前設(shè)備所屬的 CPU 類型脐往,根據(jù)不同設(shè)備 CPU 類型來加載對應(yīng)架構(gòu)的 So 文件。

這里我們舉一個小栗子扳埂,比如說我們 armeabi 目錄下也加上了 armeabi-v7 對應(yīng)的 So业簿,然后我們就可以在代碼當(dāng)中做判斷,如果你是 armeabi-v7 架構(gòu)的手機阳懂,那我們就直接加載這個 So梅尤,以此達到最佳的性能,這樣包體積其實也沒有增加多少岩调,同時也實現(xiàn)了高性能的目的

String abi = "";
// 獲取當(dāng)前手機的CPU架構(gòu)類型
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 {
    // 正常加載
    
}

4.3 使用 XZ Utils 對 Native Library 進行壓縮

Native Library 同 Dex 一樣,也可以使用 XZ Utils 進行壓縮巷燥,對于 Native Library 的壓縮,我們 只需要去加載啟動過程相關(guān)的 Library号枕,而其它的都可以在應(yīng)用首次啟動時進行解壓缰揪,并且,壓縮效果與 Dex 壓縮的效果是相似的葱淳。

此外钝腺,關(guān)于 Nativie Library 壓縮之后的解壓抛姑,我們也可以使用 Facebook 的 so 加載庫 SoLoader,它 能夠解壓應(yīng)用的 Native Library 并能遞歸地加載在 Android 平臺上不支持的依賴項艳狐。由于這套方案對啟動時間的影響比較大定硝,所以先把它壓箱底下吧。

4.4 對 Native Library 進行合并

在 Android 4.3(API 17) 之前毫目,單個進程加載的 SO 數(shù)量是有限制的蔬啡,在 Google 的 linker.cpp 源碼中有很明顯的定義

為了解決這個問題,FaceBook 寫了一個 合并Native Library的demo蒜茴,我們可以 按照自身 App 的 so 情況來配置需要合并哪些對象星爪。由于合并共享對象(即 .so 文件)在原先的構(gòu)建流程中是無法實現(xiàn)的,因此 FaceBook 更改了鏈接庫的方式粉私,并把它集成到了構(gòu)建系統(tǒng) Buck 中顽腾。

該功能允許每個應(yīng)用程序指定應(yīng)合并的 .so 庫,從而避免意外引入不必要的依賴關(guān)系诺核。然后抄肖,Buck 負責(zé)為每個合并的 .so 庫收集所有對象(文件),并將它們與適當(dāng)?shù)囊蕾図楁溄釉谝黄稹?/p>

4.5 刪除 Native Library 中無用的導(dǎo)出 symbol

我們可以去 分析代碼中的 JNI 方法以及不同 Library 庫的方法調(diào)用窖杀,然后找出無用的 symbol 并刪除漓摩,這樣 Linker 在編譯的時候也會把 symbol 對應(yīng)的無用代碼給刪除。

Buck 有 NativeRelinker 這個類入客,它就實現(xiàn)了這個功能管毙,其 類似于 Native Library 的 ProGuard Shrinking 功能

至此桌硫,可以看到夭咬,F(xiàn)aceBook 出品的 Buck 同 ReDex 一樣,里面的功能都十分強大铆隘,Buck 除了實現(xiàn) Library Merge 和 Relinker 功能之外卓舵,還實現(xiàn)了三大功能,如下所示:

  • 1)膀钠、多語言拆分
  • 2)掏湾、分包支持
  • 3)、ReDex 支持

4.6 So 動態(tài)下載

我們可以 將部分 So 文件使用動態(tài)下發(fā)的形式進行加載肿嘲。也就是在業(yè)務(wù)代碼操作之前融击,我們可以先從服務(wù)器下載下來 So,接下來再使用雳窟,這樣包體積肯定會減少不小砚嘴。

但是,如果要把這項技術(shù) 穩(wěn)定落地到實際生產(chǎn)項目中需要解決一些問題,具體的 so 動態(tài)化關(guān)鍵技術(shù)點和需要避免的坑可以參見 動態(tài)下發(fā) so 庫在 Android APK 安裝包瘦身方面的應(yīng)用际长,這里就不多贅述了耸采。

5 其它優(yōu)化方案

5.1 插件化

我們可以使用插件化的手段 對代碼結(jié)構(gòu)進行調(diào)整,如果我們 App 當(dāng)中的每一個功能都是一個插件工育,并且都是可以從服務(wù)器下發(fā)下來的虾宇,那 App 的包體積肯定會小很多。

5.2 業(yè)務(wù)梳理

回顧過去的業(yè)務(wù)如绸,合理地去 評估并刪除無用或者低價值的業(yè)務(wù)嘱朽。

5.3 轉(zhuǎn)變開發(fā)模式

如果所有的功能都不能移除,那就可能需要去轉(zhuǎn)變開發(fā)模式怔接,比如可以更多地 采用 H5搪泳、小程序 這樣開發(fā)模式。

6 包體積監(jiān)控

對于應(yīng)用包體積的監(jiān)控扼脐,也應(yīng)該和內(nèi)存監(jiān)控一樣岸军,去作為正式版本的發(fā)布流程中的一環(huán),并且應(yīng)該 盡量地去實現(xiàn)自動化與平臺化瓦侮。

(這里建議 任何大于 100kb 的功能都需要審批艰赞,特別是需要引入第三方庫時,更應(yīng)該慎重)

6.1 包體積監(jiān)控的緯度

包體積的監(jiān)控肚吏,主要可以從如下 三個緯度 來進行:

  • 1)方妖、大小監(jiān)控:通常是記錄當(dāng)前版本與上一個或幾個版本直接的變化情況,如果當(dāng)前版本體積增長較大罚攀,則需要分析具體原因党觅,看是否有優(yōu)化空間。
  • 2)斋泄、依賴監(jiān)控:包括J ar杯瞻、aar 依賴。
  • 3)是己、規(guī)則監(jiān)控:我們可以把包體積的監(jiān)控抽象為無用資源又兵、大文件任柜、重復(fù)文件卒废、R 文件等這些規(guī)則。

Matrix 中的 ApkChecker 就實現(xiàn)了包體積的規(guī)則監(jiān)控

7 瘦身優(yōu)化常見問題

瘦身優(yōu)化是性能優(yōu)化當(dāng)中不那么重要的一個分支宙地,不過對于處于穩(wěn)定運營期的產(chǎn)品會比較有幫助摔认。下面我們就來看看對于瘦身優(yōu)化有哪些常見問題。

7.1 怎么降低 Apk 包大姓唷参袱?

我們在回答的時候要注意一些 可操作的干貨,同時注意結(jié)合你的 項目周期。主要可以從以下 三點 來回答:

  • 1)抹蚀、代碼:Proguard剿牺、統(tǒng)一三方庫、無用代碼刪除环壤。
  • 2)晒来、資源:無用資源刪除、資源混淆郑现。
  • 3)湃崩、So:只保留 Armeabi、更優(yōu)方案接箫。

在項目初期攒读,我們一直在不斷地加功能,加入了很多的代碼辛友、資源薄扁,同時呢,也沒有相應(yīng)的規(guī)范瞎领,

UI 同學(xué)給我們很多 UI 圖的時候泌辫,都是沒有經(jīng)過壓縮的圖片,長期累積就會導(dǎo)致我們的包體積越來越大九默。

到了項目穩(wěn)定期的時候震放,我們對各種運營數(shù)據(jù)進行考核,發(fā)現(xiàn) APK 的包大小影響了用戶下載的意愿驼修,于是我們就著手做包體積的優(yōu)化殿遂,我們采用的是 Android Studio 自帶的 Analyze APK 來做的包體積分析,主要就是做了代碼乙各、資源墨礁、So 等三個方面的重點優(yōu)化。

首先耳峦,針對于代碼瘦身
第一點恩静,我們首先 使用 Proguard 工具進行了混淆,它將程序代碼轉(zhuǎn)換為功能相同蹲坷,但是不容易理解的形式驶乾。比如說將一個很長的類轉(zhuǎn)換為字母 a,同時循签,這樣做還有一個好處级乐,就是讓代碼更加安全了。

第二點呢县匠,我們將項目中使用到的一些 第三方庫進行了統(tǒng)一风科,比如說圖片庫撒轮、網(wǎng)絡(luò)庫、數(shù)據(jù)庫等贼穆,不允許項目中出現(xiàn)功能相同题山,但是卻實現(xiàn)不一樣的庫。同時也做了 規(guī)范故痊,之后引入的三方庫臀蛛,需要去考量它的大小、方法數(shù)等崖蜜,而且呢浊仆,如果只是需要一個很大庫的一個小功能,那我們就修改源碼豫领,只引入部分代碼即可抡柿。

第三點,我們將項目中的 無用代碼進行了刪減等恐,我們使用了 AOP 的方式統(tǒng)計到了哪些 Activity 以及 fragment 在真實的場景下沒有用戶使用洲劣,這樣你就可以刪除掉了。

對于那些不是 Activity 或者是 Fragment 的類课蔬,我們切了很多類的構(gòu)造函數(shù)囱稽,這樣你就可以統(tǒng)計出來這些類在線上有沒有真正被調(diào)用到。但是二跋,對于代碼的瘦身效果战惊,實際上不是很明顯。
資源的瘦身
移除了項目當(dāng)中冗余的資源文件扎即,這一點在項目當(dāng)中一定會遇到吞获。

然后,我們做了 資源圖片的壓縮谚鄙,UI 同學(xué)給我們資源圖片的時候各拷,需要確認(rèn)已經(jīng)是壓縮過的圖片,同時闷营,我們還會做一個 兜底策略烤黍,在打包的時候,如果圖片沒有被壓縮過傻盟,那我們就會再來壓縮一遍速蕊,這個效果就非常的明顯。

對于資源莫杈,我們還做了 資源的混淆互例,也就是將冗余的資源名稱換成簡短的名字奢入,資源壓縮的效果要比代碼瘦身的效果要好的多筝闹。
** So 的瘦身**
我們只保留了 armeabi 這個目錄媳叨,它可以 兼容別的 CPU 架構(gòu),這點的優(yōu)化效果非常的明顯关顷。

對于項目當(dāng)中使用到的視頻模塊的 So糊秆,它對性能要求非常高,所以我們采用了另外一種方式议双,我們將所有這個模塊下的 So 都放到了 armeabi 這個目錄下痘番,然后在代碼中做判斷,如果是別的 CPU 架構(gòu)平痰,那我們就加載對應(yīng) CPU 架構(gòu)的 So 文件即可汞舱。

7.2 Apk 瘦身如何實現(xiàn)長效治理?

主要可以從以下 兩個方面 來進行回答:

  • 1)宗雇、發(fā)版之前與上個版本包體積對比昂芜,超過閾值則必須優(yōu)化。
  • 2)赔蒲、推進插件化架構(gòu)改進泌神。

在大型項目中,最好的方式就是 結(jié)合 CI舞虱,每個開發(fā)同學(xué) 在往主干合入代碼的時候需要經(jīng)過一次預(yù)編譯欢际,這個預(yù)編譯出來的包對比主干打出來的包大小,如果超過閾值則不允許合入矾兜,需要提交代碼的同學(xué)自己去優(yōu)化去提交的代碼损趋。

8 總結(jié)

  • 1)、瘦身優(yōu)化及 Apk 分析方案:瘦身優(yōu)勢椅寺、APK 組成舶沿、APK 分析。
  • 2)配并、代碼瘦身方案探索:Dex 探秘括荡、ProGuard、D8 與 R8 優(yōu)化溉旋、去除 debug 信息與行號信息畸冲、Dex 分包優(yōu)化、使用 XZ Utils 進行 Dex 壓縮观腊、三方庫處理邑闲、移除無用代碼、避免產(chǎn)生 Java access 方法梧油、利用 ByteX Gradle 插件平臺中的代碼優(yōu)化插件苫耸。
  • 3)、資源瘦身方案探索:冗余資源優(yōu)化儡陨、重復(fù)資源優(yōu)化褪子、圖片壓縮量淌、使用針對性的圖片格式、資源混淆嫌褪、R Field 的內(nèi)聯(lián)優(yōu)化呀枢、資源合并方案、資源文件最少化配置笼痛、盡量每張圖片只保留一份裙秋、資源在線化、統(tǒng)一應(yīng)用風(fēng)格缨伊。
  • 4)耸成、So 瘦身方案探索:So 移除方案曹质、So 移除方案優(yōu)化版、使用 XZ Utils 對 Native Library 進行壓縮、對 Native Library 進行合并湃番、刪除 Native Library 中無用的導(dǎo)出 symbol街州、So 動態(tài)下載一疯。
  • 5)灰粮、其它優(yōu)化方案:插件化、業(yè)務(wù)梳理漏益、轉(zhuǎn)變開發(fā)模式蛹锰。
  • 6)、包體積監(jiān)控绰疤。
  • 7)铜犬、瘦身優(yōu)化常見問題。

如果要想對包體積做更深入的優(yōu)化轻庆,我們就必須對 APK 組成癣猾,Dex、So 動態(tài)庫以及 Resource 文件格式余爆,還有 APK 的編譯流程 有深入地了解纷宇,這樣我們才能有 足夠的內(nèi)功素養(yǎng) 去實現(xiàn)包體積的深度優(yōu)化。

** AOP 編譯插樁蛾方、Gradle 自動化構(gòu)建**

參考
深入探索 Android 包體積優(yōu)化(匠心制作)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末像捶,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子桩砰,更是在濱河造成了極大的恐慌拓春,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件亚隅,死亡現(xiàn)場離奇詭異硼莽,居然都是意外死亡,警方通過查閱死者的電腦和手機煮纵,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進店門懂鸵,熙熙樓的掌柜王于貴愁眉苦臉地迎上來偏螺,“玉大人,你說我怎么就攤上這事矾瑰。” “怎么了隘擎?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵殴穴,是天一觀的道長。 經(jīng)常有香客問我货葬,道長采幌,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任震桶,我火速辦了婚禮休傍,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘蹲姐。我一直安慰自己磨取,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布柴墩。 她就那樣靜靜地躺著忙厌,像睡著了一般。 火紅的嫁衣襯著肌膚如雪江咳。 梳的紋絲不亂的頭發(fā)上逢净,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天,我揣著相機與錄音歼指,去河邊找鬼爹土。 笑死,一個胖子當(dāng)著我的面吹牛踩身,可吹牛的內(nèi)容都是我干的胀茵。 我是一名探鬼主播,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼挟阻,長吁一口氣:“原來是場噩夢啊……” “哼宰掉!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起赁濒,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤轨奄,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后拒炎,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體挪拟,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年击你,在試婚紗的時候發(fā)現(xiàn)自己被綠了玉组。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片谎柄。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖惯雳,靈堂內(nèi)的尸體忽然破棺而出朝巫,到底是詐尸還是另有隱情,我是刑警寧澤石景,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布劈猿,位于F島的核電站,受9級特大地震影響潮孽,放射性物質(zhì)發(fā)生泄漏揪荣。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一往史、第九天 我趴在偏房一處隱蔽的房頂上張望仗颈。 院中可真熱鬧,春花似錦椎例、人聲如沸挨决。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽凰棉。三九已至,卻和暖如春陌粹,著一層夾襖步出監(jiān)牢的瞬間撒犀,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工掏秩, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留或舞,地道東北人。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓蒙幻,卻偏偏與公主長得像映凳,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子邮破,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,916評論 2 344

推薦閱讀更多精彩內(nèi)容