本文會不按期更新,推薦watch下項目。
若是喜歡請star,若是以爲有紕漏請提交issue,若是你有更好的點子能夠提交pull request。
本文的示例代碼主要是基於做者的經驗來編寫的,若你有其餘的技巧和方法能夠參與進來一塊兒完善這篇文章。javascript
業務方和開發都但願app儘可能的小,本文會給出多個實用性的技巧來幫助開發者進行app的瘦身工做。瘦身和減負雖好,但須要注意瘦身對於項目可維護性的影響,建議根據自身的項目進行技巧的選取。html
本文固定鏈接:github.com/tianzhijiex…java
目前app的大小愈來愈大,用戶對於過大的app接受度不高,因此除了插件化和RN的方案外,咱們只能老老實實的進行app的瘦身工做。react
作瘦身以前必定要了解本身app的組成結構,要有針對性的進行優化,而且要逐步記錄比對,這樣才能更好的完成此項工做。關於apk的大小,我推薦google的這個視頻。目前as的2.2預覽版中已經有了apk分析器,功能至關強大,此外你還能夠利用nimbledroid來分析apk。android
咱們都知道apk是由: ios
這幾個部分構成的。git
下面我會利用as的分析工具,以微信、微博、淘寶爲例進行講述。github
分析完成後你還能夠看到具體類目佔的百分比,清晰明瞭。旁邊的「對比」按鈕提供了diff的功能,讓你能夠方便的進行apk優化先後的對比,簡直利器。web
assets目錄能夠存放一些配置文件或資源文件,好比webview的本地html,react native的jsbundle等,微信的整個assets佔用了13.4M。若是你的應用對本地資源要求不多的話,這個文件應該不會太大。算法
lib目錄下會有各類so文件,分析器會檢查出項目本身的so和各類庫的so。微博和微信同樣只支持了arm一個平臺,淘寶支持了arm和x86兩個平臺。
這個文件是編譯後的二進制資源文件,裏面是id-name-value的一個map。由於微信作了資源的混淆,因此這裏能夠看到資源名稱都是不可讀的。
索性放個微博的圖,易於你們理解:
META-INF目錄下存放的是簽名信息,用來保證apk包的完整性和系統的安全性,幫助用戶避免安裝來歷不明的盜版apk。
res目錄存放的是資源文件,包括圖片、字符串。raw文件夾下面是音頻文件,各類xml文件等等。由於微信作了資源混淆,圖片名字都不可讀了。
微博就沒有作資源混淆,因此可讀性較好:
dex文件是java代碼打包後的字節碼,一個dex文件最多隻支持65535個方法,這也是爲何微信有了三個dex文件的緣由。
由於dex分包是不均勻的,你能夠理解爲裝箱,一個箱子的大小是固定的,但你代碼的量是不肯定的,微信把前兩個箱子裝滿了,最後還剩了2m多的代碼,這些代碼也佔用了一個箱子,最終產生了上圖不均勻的狀況。
如今,咱們已經知道了apk中各個文件的大小和它們佔的比例,下面就能夠開始針對性的進行優化了。
assets中會存放資源文件,這個目錄中不一樣廠的app存放的內容各有不一樣,因此優化也比較難。自從引入RN以來,這個目錄下還會有jsbundle的信息(可參考全民k歌)。若是你有地址選擇的功能,這裏還會存放地址的映射文件。
對於這塊的資源,as是不會進行主動的刪減的,因此一切都是須要靠開發者進行手動管理的。
中文字體是至關大的,我一直不建議將字體文件隨意丟棄到assets中。有時候一個小功能急着上,開發者爲了追求速度,能夠先放在這裏圖省事。但必定要知道這個隱患,而且必定要多和產品覈對功能的必要性。對於有些只會用在logo中的字體,我推薦將字體文件進行刪減處理。
FontZip是一個字體提取工具,readme中寫到:
通過測試,已經把項目5MB的藝術字體,按需求提取後,佔用只有20KB,而且可正常使用。
icon-font和svg都能完成一些icon的展現,但由於icon-font在assets中難以管理,而且功能和svg有所重疊,因此我建議減小icon-font的使用,利用svg進行代替,畢竟一個很小的icon-font也比svg大。這裏給出一個提供各類格式icon的網站,方便你們進行測試:icomoon.io/app/
字體、js代碼這樣的資源能動態下載的就作動態下載,雖然這樣會增長出錯的可能性,複雜度也會提高,但對於app的瘦身和用戶來講是有長遠的好處的。
若是你用了rn,你能夠在app運行時動態去拉取最新的代碼,將圖片和js代碼一併下載後解壓使用。也能夠把rn模塊化,主線的rn代碼隨着app發佈,入口較深的次要界面能夠在app啓動後經過斷點下載。
有些資源文件是必需要隨着app一併發佈的。對於這樣的文件,能夠採用壓縮存儲的方式,在須要資源的時候將其解壓使用,下面就是解壓zip文件的代碼示例:
public static void unzipFile(File zipFile, String destination) throws IOException {
FileInputStream fileStream = null;
BufferedInputStream bufferedStream = null;
ZipInputStream zipStream = null;
try {
fileStream = new FileInputStream(zipFile);
bufferedStream = new BufferedInputStream(fileStream);
zipStream = new ZipInputStream(bufferedStream);
ZipEntry entry;
File destinationFolder = new File(destination);
if (destinationFolder.exists()) {
deleteDirectory(destinationFolder);
}
destinationFolder.mkdirs();
byte[] buffer = new byte[WRITE_BUFFER_SIZE];
while ((entry = zipStream.getNextEntry()) != null) {
String fileName = entry.getName();
File file = new File(destinationFolder, fileName);
if (entry.isDirectory()) {
file.mkdirs();
} else {
File parent = file.getParentFile();
if (!parent.exists()) {
parent.mkdirs();
}
FileOutputStream fout = new FileOutputStream(file);
try {
int numBytesRead;
while ((numBytesRead = zipStream.read(buffer)) != -1) {
fout.write(buffer, 0, numBytesRead);
}
} finally {
fout.close();
}
}
long time = entry.getTime();
if (time > 0) {
file.setLastModified(time);
}
}
} finally {
// ...
}
}複製代碼
全民k歌中的assets目錄下我就發現了大量的zip文件:
android上也有一個7z庫幫助咱們方便的使用7z,這個庫我目前沒用到,有需求的同窗能夠嘗試一下。
一個硬件設備對應一個架構(mips、arm或者x86),只保留與設備架構相關的庫文件夾(主流的架構都是arm的,mips屬於小衆)能夠大大下降lib文件夾的大小。配置方式也十分簡單,直接配置abiFilters便可:
defaultConfig {
versionCode 1
versionName '1.0.0'
renderscriptTargetApi 23
renderscriptSupportModeEnabled true
// http://stackoverflow.com/questions/30794584/exclude-jnilibs-folder-from-production-apk
ndk {
abiFilters "armeabi", "armeabi-v7a" ,"x86"
}
}複製代碼
armeabi就不用說了,這個是必須包含的,v7是一個圖形增強版本(若是用到模糊算法,則不要刪除),x86是英特爾平臺的支持庫。
官方例子:
按 ABI 拆分
android {
...
splits {
abi {
enable true
reset()
include 'x86', 'armeabi-v7a', 'mips'
universalApk true
}
}
}複製代碼
include
一塊兒使用來能夠表示要使用哪個ABI)咱們在捨棄so以前必定要進行用戶cpu型號的統計,這樣你才能放心大膽地進行操做。
我先是花了幾個版本的時間統計了用戶的cpu型號,而後排除了沒有或少許用戶纔會用到的so,以達到瘦身的目的。
@NonNull
public static String getCpuName() {
String name = getCpuName1();
if (TextUtils.isEmpty(name)) {
name = getCpuName2();
if (TextUtils.isEmpty(name)) {
name = "unknown";
}
}
return name;
}
private static String getCpuName1() {
String[] abiArr;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
abiArr = Build.SUPPORTED_ABIS;
} else {
abiArr = new String[]{Build.CPU_ABI, Build.CPU_ABI2};
}
StringBuilder abiStr = new StringBuilder();
for (String abi : abiArr) {
abiStr.append(abi);
abiStr.append(',');
}
return abiStr.toString();
}
private static String getCpuName2() {
try {
FileReader e = new FileReader("/proc/cpuinfo");
BufferedReader br = new BufferedReader(e);
String text = br.readLine();
String[] array = text.split(":\\s+", 2);
e.close();
br.close();
return array[1];
} catch (IOException var4) {
var4.printStackTrace();
return null;
}
}複製代碼
注意:
renderscript
,那麼你必須包含v7,不然會出現模糊異常的問題。so有個常年大坑:在Android 6.0以前,so文件會壓縮到apk中,系統在安裝應用的時候,會把so文件解壓到data分區。這樣同一個so文件會有兩份存在,一個在apk裏,一個在data中。這也致使多佔用了一倍的空間,並且會出現各類詭異的錯誤。這個策略雖然和apk的瘦身無關,但它和app安裝在用戶手機中的大小有關,所以咱們也是須要多多留意的。
Starting from Android Studio 2.2 Preview 2 and newest build tools, the build process will automatically store native libraries uncompressed and page aligned in the APK
在6.0+中,能夠經過以下的方式進行申明:
<application
android:extractNativeLibs=」false」
...
>複製代碼
若是想了解更多信息或者想知道這種配置的限制,能夠瀏覽下SmallerAPK(8)。
resources.arsc中存放了一個對應關係:
id | name | default | v11 |
---|---|---|---|
0x7f090002 | PopupAnimation | @ref/0x7f040042, @ref/0x7f040041 | … |
咱們在程序運行的時候確定要常常用到id,所以它在安裝以後仍須要被頻繁的讀取。若是將這個文件進行了壓縮,在每次讀取前系統都必須進行解壓的工做。這就會有一些性能和內存的開銷,綜合考慮下來,壓縮這個文件是得不償失的。
resources.arsc的正確瘦身方式是刪除沒必要要的string entry
,你能夠藉助 android-arscblamer 來檢查出能夠優化的部分,好比一些空的引用。
微信團隊開源了一個資源混淆工具,AndResGuard。它將資源的名稱進行了混淆,因此能夠用它對resources.arsc
進行優化,只是具體優化效果與編碼方式、id數量、平均減小命名長度有關。
表1:
id | name | default | v11 |
---|---|---|---|
0x7f090001 | Android | @ref/0x7f040042, @ref/0x7f040041 | … |
0x7f090002 | ios | @ref/0x7f040042, @ref/0x7f040041 | … |
0x7f090003 | Windows Phone | @ref/0x7f040042, @ref/0x7f040041 | … |
表2:
id | name | default | v11 |
---|---|---|---|
0x7f090001 | a | @ref/0x7f040042, @ref/0x7f040041 | … |
0x7f090002 | b | @ref/0x7f040042, @ref/0x7f040041 | … |
0x7f090003 | c | @ref/0x7f040042, @ref/0x7f040041 | … |
咱們一眼就能夠知道表2確定比表1存儲的字符要小,因此整個文件的大小確定也要小一些。
詳細信息請參考:smallerapk-part-3-removing-unused-resources
關於AndResGuard
這個壓縮工具其實就是一個task,使用也十分簡單,具體的用法請參考中文文檔。
原理介紹:安裝包立減1M--微信Android資源混淆打包工具
andResGuard {
mappingFile = null
use7zip = true
useSign = true
keepRoot = false
whiteList = [
//for your icon
"R.drawable.icon",
//for fabric
"R.string.com.crashlytics.*",
//for umeng update
"R.string.umeng*",
"R.string.UM*",
"R.layout.umeng*",
"R.drawable.umeng*",
//umeng share for sina
"R.drawable.sina*"
]
compressFilePattern = [
"*.png",
"*.jpg",
"*.jpeg",
"*.gif",
"resources.arsc"
]
sevenzip {
artifact = 'com.tencent.mm:SevenZip:1.1.9'
//path = "/usr/local/bin/7za"
}
}複製代碼
使用這個工具的時候須要注意一些東西:像友盟這種喜歡用反射獲取資源的SDK就是一個坑(友盟的SDK就是坑王!)對於app啓動圖標這樣的icon能夠不作混淆,推薦將其放入白名單中。
META-INF文件夾中有三個文件,分別是MANIFEST.MF、CERT.SF、CERT.RSA。下面我將會列出簡要的分析,若是你但願更詳盡的瞭解原理,能夠查看《Android APK 簽名文件MANIFEST.MF、CERT.SF、CERT.RSA分析》。
每個資源文件(res開頭)下面都有一個SHA1-Digest的值。這個值爲該文件SHA-1值進行base64編碼後的結果。
若是要探究原理,能夠看下SignApk.java。這個類中的main方法:
public static void main(String[] args) {
//...
// MANIFEST.MF
Manifest manifest = addDigestsToManifest(inputJar);
je = new JarEntry(JarFile.MANIFEST_NAME);
je.setTime(timestamp);
outputJar.putNextEntry(je);
manifest.write(outputJar);
//...
}複製代碼
private static void writeSignatureFile(Manifest manifest, OutputStream out)
throws IOException, GeneralSecurityException {
Manifest sf = new Manifest();
Attributes main = sf.getMainAttributes();
main.putValue("Signature-Version", "1.0");
main.putValue("Created-By", "1.0 (Android SignApk)");
BASE64Encoder base64 = new BASE64Encoder();
MessageDigest md = MessageDigest.getInstance("SHA1");
PrintStream print = new PrintStream(
new DigestOutputStream(new ByteArrayOutputStream(), md),
true, "UTF-8");
// Digest of the entire manifest
manifest.write(print);
print.flush();
main.putValue("SHA1-Digest-Manifest", base64.encode(md.digest()));
Map<String, Attributes> entries = manifest.getEntries();
for (Map.Entry<String, Attributes> entry : entries.entrySet()) {
// Digest of the manifest stanza for this entry.
print.print("Name: " + entry.getKey() + "\r\n");
for (Map.Entry<Object, Object> att : entry.getValue().entrySet()) {
print.print(att.getKey() + ": " + att.getValue() + "\r\n");
}
print.print("\r\n");
print.flush();
Attributes sfAttr = new Attributes();
sfAttr.putValue("SHA1-Digest", base64.encode(md.digest()));
sf.getEntries().put(entry.getKey(), sfAttr);
}
sf.write(out);
}複製代碼
上述代碼說明了SHA1-Digest-Manifest是MANIFEST.MF文件的SHA1並base64編碼的結果。
這裏有一項SHA1-Digest-Manifest的值,這個值就是MANIFEST.MF文件的SHA-1並base64編碼後的值。後面幾項的值是對MANIFEST.MF文件中的每項再次SHA1並base64編碼後的值。因此你會看到在manifest.mf中的資源名稱在這裏也出現了,好比abc_btn_check_material
這個系統資源文件就出現了兩次。
MANIFEST.MF:
CERT.SF
若是你把前一個文件打開在後面加上\n\r,而後進行編碼,你就會獲得CERT.SF中的值。
Map<String, Attributes> entries = manifest.getEntries();
for (Map.Entry<String, Attributes> entry : entries.entrySet()) {
// Digest of the manifest stanza for this entry.
print.print("Name: " + entry.getKey() + "\r\n");
for (Map.Entry<Object, Object> att : entry.getValue().entrySet()) {
print.print(att.getKey() + ": " + att.getValue() + "\r\n");
}
print.print("\r\n");
print.flush();
Attributes sfAttr = new Attributes();
sfAttr.putValue("SHA1-Digest", base64.encode(md.digest()));
sf.getEntries().put(entry.getKey(), sfAttr);
}
sf.write(out);複製代碼
CERT.RSA包含了公鑰、所採用的加密算法等信息。它對前一步生成的MANIFEST.MF使用了SHA1-RSA算法,用開發者的私鑰進行簽名,在安裝時使用公鑰解密它。解密以後,將它與未加密的摘要信息(即,MANIFEST.MF文件)進行對比,若是相符,則代表內容沒有被修改。
這點和app瘦身就徹底無關了,這塊我平時也沒有仔細研究過,就不誤人子弟了。具體的簽名過程能夠參考:blog.csdn.net/asmcvc/arti…
經過分析得出,除了CERT.RSA沒有壓縮機會外,其他的兩個文件均可以經過混淆資源名稱的方式進行壓縮。
資源文件的優化一直是咱們的重頭戲。若是要和它進行對比,上文的META-INF文件的優化簡直能夠忽略不計。res的優化分爲兩塊:一個是文本資源(shape、layout等)優化和圖片資源優化。本節僅探討除圖片資源優化外的內容,關於圖片的內容下面會另起一節。
說明:
上圖中有-v4,-v21這樣的文件有些是app開發者本身寫的,但大多都是系統在打包的時候自動生成的,因此你只須要考慮本身項目中的drawable-mdpi、drawable-hdpi、drawable-xhdpi、drawable-xxhdpi便可。
在as的任何文件中右擊,選擇清除無用資源
便可刪除沒有用到的資源文件。
不要勾選清除id!若是清除了id,會影響databinding的使用(id絕對佔不了多少空間)
Tips:
作此操做以前,請務必產生一次commit,操做完成後必定要經過git看下diff。這樣既方便查看被刪除的文件,又能夠利用git進行誤刪恢復。
shrinkResources顧名思義————收縮資源。將它設置爲true後,每次打包的時候就會自動排除無用的資源(不只僅是圖片)。有了它的幫忙,即便你忘記手動刪除無用的資源文件也沒事。
buildTypes {
release {
zipAlignEnabled true
minifyEnabled true
shrinkResources true // 是否去除無效的資源文件
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
signingConfig signingConfigs.release
}
rtm.initWith(buildTypes.release)
rtm {}
debug {
multiDexEnabled true
}
}複製代碼
大部分應用其實並不須要支持幾十種語言的,微信也作了根據地區選擇性下載語言包的功能。做爲國內應用,咱們能夠只支持中文。推薦在項目的build.gradle
中進行以下配置:
android {
//...
defaultConfig {
resConfigs "zh"
}
}複製代碼
這樣在打包的時候就會排除私有項目、android系統庫和第三方庫中非中文的資源文件了,效果仍是比較顯著的。
通常raw文件下會放音頻文件。若是raw文件夾下有音頻文件,儘可能不要使用無損(如:wav)的音頻格式,能夠考慮同等質量但文件更小的音頻格式。
ogg是一種較適合作音效的音頻格式。當年我初中作遊戲的時候,我全都是用的mp3和png,最終遊戲達到了2G。在換爲ogg和jpg後,遊戲縮小到了1G之內(由於遊戲中音頻和大圖較多,因此效果比較誇張)。
移動端的音頻主要是音效和短小的音頻,因此淘寶大量選擇了ogg格式,微博的選擇格式比較多,有wav、mp三、ogg,我更加推薦淘寶的作法。固然,你仍舊不要忘記opus格式,opus也是一種有損壓縮格式,若是感興趣的話也能夠嘗試一下。
一個應用的界面風格是必需要統一的,這個越早作越好,最基本的就是統一顏色和按鈕的按壓效果。無UI設計和扁平化風格流行後,卻是給應用瘦身帶來了極大的的福利。界面變得越樸實,咱們能夠用shape畫的東西就越多。
當你的app統一過每種顏色對應的按下顏色後,接下來就須要統一按鈕的形狀、按鈕的圓角角度、有無陰影的樣子、陰影投射角度,陰影範圍等等,最後還要考慮是否支持水波紋效果。
我簡單將按鈕分爲下列元素:
元素 | 屬性01 | 屬性02 | 屬性03 | 屬性04 |
---|---|---|---|---|
形狀 | 正方形 | 三角形 | 圓角矩形 | 圓形 |
顏色 | 紅 | 黃 | 藍 | 綠 |
有無陰影 | 有 | 無 | ||
陰影大小 | 3dp | 5dp | ||
陰影角度 | 90° | 120° | 180° | |
水波紋效果 | 有 | 無 |
各個元素組合後會產生大量的樣式,shape和layer-list固然能夠實現各類組合,但這樣的話光按鈕的背景文件就有n個,很很差維護。
通常爲了開發方便,都會把須要用到的各類selector圖片事先定義好,作業務的時候只須要去調用就行。但這大量的selector文件對於業務開發者來講也是有記憶難度的,因此我推薦使用SelectorInjection這個庫,它能夠將上面的每一個元素進行各類組合,用最少的資源文件來實現大量的按壓效果。
用庫雖然好,但庫也會帶來學習成本,因此引入者能夠將上述的組合定義爲按鈕的一個個的style。由於style自己是支持繼承的,對於這樣的組合形態來講,繼承真是是一大利器。當你的style有良好的命名後,調用者只須要知道引入什麼style就行,至於你用了什麼屬性別人才不但願管呢。
若是業務開發中有一些特別特殊的按壓狀態,沒有任何複用的價值,那你就能夠利用庫提供的豐富屬性在layout文件中進行實現,不再用手忙腳亂的處處定義selector文件了。
我將不能繼承和不靈活的shape變成了一個個單一的屬性,經過庫將多個屬性進行組合,接着利用支持繼承的style來將多個屬性固定成一個配置文件,最後對外造成強制的規範性約束,至此便完成了減小selector文件的工做。
menu文件是actionBar時代的產物,as雖然對於menu的支持作的還不錯,但我也很難愛上它。
menu的設計初衷是解耦和抽象,但由於過分的解耦和定製,讓開發變得很不方便,不少項目已經再也不使用menu.xml做爲actionbar的菜單了。
就目前的形勢來看,toolbar是android將來的方向。我雖然做爲一個對actionbar和actionbar的兼容處理至關了解的人,但我仍是不得不認可actionbar的時代過去了。若是你不信,我能夠告訴你淘寶的menu文件就3個,微博的menu文件就9個,若是你仍是苦苦依戀着actionbar的配置模式,我推薦一個庫AppBar,它可讓你在用靈活的toolbar的同時也享受到配置menu的便利性。
減小layout文件有兩個方法:複用和融合(include)。
把一些頁面共用的佈局抽出來,這不管是對layout文件的管理仍是瘦身都是極爲有用的。
就好比說任何一個app的list頁面是至關多的,從佈局層面來講就是一個ListView或者RecyclerView,其背後還可能會有loading的view,空狀態的view等等,因此個人建議是創建一個list_layout.xml,其他的list頁面能夠複用或者include它,這樣會從很大程度上減小layout文件的數目。
對於能夠被複用的layout咱們能夠作統一管理,可是對於不會被複用的layout怎麼辦呢?
假設一個頁面是由兩個區域組合而成的,fragment的作法是一個頁面中放兩個container,而後再寫兩個layout,但實際上這兩個layout常常是沒有任何複用價值的。我但願找到一種方式,在view區塊尚未複用需求的時候用一個layout搞定,須要被複用的時候也能夠快速、無痛的拆分出來。
1. UiBlock
UiBlock是一個相似於fragment的解耦庫,它能夠爲同一個layout中不一樣區域的view進行邏輯解耦(由於layout可預覽的特性,ui定位方面不是難題),它能幫咱們儘量少地創建layout文件。
若是將來需求發生了變更,layout文件中的一塊view須要抽出成獨立的layout文件的時候,UiBlock的邏輯代碼幾乎不用改動,你只須要把抽出的layout文件include進來,而後在include
標籤上定義一個id便可。而這個工做能夠經過as的重構功能自動完成,毫不拖泥帶水。
<!-- 使用include -->
<include android:id="@+id/bottom_ub" layout="@layout/demo_uiblock" android:layout_width="match_parent" android:layout_height="100dp" />複製代碼
2. ListHeader
public void addHeaderToListView(ListView listView, View header) {
if (header == null) {
throw new IllegalArgumentException("Can't add a null header view to ListView");
}
ViewGroup viewParent = (ViewGroup) header.getParent();
viewParent.removeView(header);
AbsListView.LayoutParams params = new AbsListView.LayoutParams(
header.getLayoutParams().width,
header.getLayoutParams().height);
header.setLayoutParams(params);
listView.addHeaderView(header); // add
}複製代碼
我將listView和它的沒有複用價值的header放到了同一個layout中,而後在activity中利用上述代碼進行了操做,最終完成了用一個layout文件給listView加頭的工做。這段代碼我好久沒動過了,有利有弊,放在這裏我也僅僅是舉個例子,但願能夠幫助你們擴展下思路。
作過濾鏡和貼紙的同窗應該會注意到貼紙、表情這類的東西是至關大的,對於這類的圖片資源我強烈建議經過在線商店進行獲取。這樣既可讓你踏踏實實的賣貼紙,又能夠減少應用的大小。這麼作雖然有必定的複雜度和出錯機率,但投入產出比仍是很不錯的。
這個雖然不算是app大小的優化,可是若是你放錯了圖片,對於app啓動時的內存大小會有必定的影響:
思考一下,若是把一個原本應該放在drawable-xxhdpi裏面的圖片放在了drawable文件夾中會出現什麼問題呢?
在xxhdpi設備上,圖片會被放大3倍,圖片內存佔用就會變爲原來的9倍!
國內也有不少人說能夠用一套圖片來作,不用出多套圖,藉此來達到app瘦身和給設計減負的目的。谷歌官方是建議爲不一樣分辨率出不一樣的圖片,爲此國內也有很多文章討論過這件事情,這篇總結的不錯推薦一讀。
每次說到這個話題的時候總有不少人有不一樣的見解,何況不少人還不知道.9圖也是須要切多份的,因此這裏我仍是先分析一下大廠的放圖策略,最後我們再討論下較優的方案。
分析過程見:《淘寶、微博、微信的 Android 圖片放置策略》
廠商 | mdpi | hdpi | xhdpi | xxhdpi |
---|---|---|---|---|
淘寶 | 小icon | 表情 | 國家icon | 棄用 |
微博 | 小icon | 背景圖&表情 | 背景圖 | 背景圖 |
微信 | 棄用 | 表情 | 大圖 | 棄用 |
經過分析得出,傳統的出多個分辨率圖片的作法在大廠中已經發生了改變,阿里系、騰訊系的產品都採用了一套圖走天下的路子。這樣的作法仍是有利有弊的,權衡之下我給出以下建議:
成年人不看對錯,只看利弊,因此還請你們權衡一二。
若是咱們但願保留或丟棄特定的資源,須要在項目中建立一個res/raw/keep.xml
文件,這裏可使用tools:keep
和 tools:discard
兩個屬性來保留或丟棄資源。
兩個屬性均可以使用逗號,
分隔符聲明資源名稱列表。也可使用*
做爲匹配符。
android {
...
buildTypes {
release {
shrinkResources true // 開啓
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'),
'proguard-rules.pro'
}
}
}複製代碼
keep:
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:discard="@layout/unused2" />複製代碼
discard:
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:shrinkMode="safe" tools:discard="@layout/unused2" />複製代碼
開啓嚴格模式後,可能對於編譯會產生一些問題,警告也會增多,因此需謹慎開啓此功能。
res/raw/keep.xml
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:shrinkMode="strict" />複製代碼
shrinkMode
默認的值爲safe
,你將它指定爲strict
便開啓了嚴格模式。嚴格模式下,apk會保留肯定引用到的資源。
Gradle Console中的日誌中也會有移除APK資源的信息::
:android:shrinkDebugResources
Removed unused resources: Binary resource data reduced from 2570KB to 1711KB: Removed 33%
:android:validateDebugSigning複製代碼
apk構建完成後會Gradle會在/build/outputs/mapping/release/
下生成resource.txt
,這個文件包括詳細信息,如資源參考其餘資源和使用或刪除資源的詳細信息等。
例如:找出爲何@drawable/ic_plus_anim_016
,仍然包含在你的APK中,在resource.txt
搜索該文件名,你可能會發現它是被另外一個資源引用,以下:
16:25:48.005 [QUIET] [system.out] @drawable/add_schedule_fab_icon_anim : reachable=true
16:25:48.009 [QUIET] [system.out] @drawable/ic_plus_anim_016複製代碼
要 爲何add_schedule_fab_icon_anim
仍然在使用,搜索咱們能夠知道應該有代碼引用着add_schedule_fab_icon_anim
。
此部分的內容大量參考自《Shrink Your Code and Resources》一文,請移步官網去詳細瞭解。
有時候引用的三方庫會帶有一些配置文件xxxx.properties,或者license信息,打包的時候想去掉這些信息,就能夠這樣作
android {
packagingOptions {
exclude 'proguard-project.txt'
exclude 'project.properties'
exclude 'META-INF/LICENSE.txt'
exclude 'META-INF/LICENSE'
exclude 'META-INF/NOTICE.txt'
exclude 'META-INF/NOTICE'
exclude 'META-INF/DEPENDENCIES.txt'
exclude 'META-INF/DEPENDENCIES'
}
}複製代碼
對於圖片的優化應該是放在優化res一節中進行講解的,可是由於圖片這塊比重太大了,因此我讓其獨立成爲一節。
想要作好圖片的優化工做首先要知道應該選擇什麼樣的圖片格式,對於這點我推薦一個視頻,方便你們進行深刻的瞭解。也推薦參考下《移動端圖片格式調研 | Garan no dou》裏面的相關內容。
這是谷歌給出的建議是:VD->WebP->Png->JPG
VD即VectorDrawable,是android上的svg實現類。在經歷了長達半年的緩慢兼容之路後,如今終於被support庫兼容了,官方文檔中給出了這樣一個例子:
// Gradle Plugin 2.0+
android {
defaultConfig {
vectorDrawables.useSupportLibrary = true
}
}複製代碼
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="@drawable/ic_add"
/>複製代碼
配置好後,咱們就能夠利用強大的svg來替換純色icon了。
由於svg矢量圖的優點,終於能夠經過一套圖適配多個機型了。svg的好處有不少,缺點也很多,關於svg的優缺點和實踐方案,建議移步:http://todo(未寫完)
webp是一種新的圖片格式。從Android4.0+開始原生支持,可是不支持包含透明度,直到4.2.1+才支持顯示含透明度的webp,使用的時候要特別注意。
Lossy WebP support (suitable for replacing most JPEGs and some PNGs) is guaranteed on Android 4.0+ devices. Newer WebP features (transparency, lossless, suitable for PNGs) are supported since Android 4.2.1+
咱們能夠經過智圖或者isparta將其它格式的圖片轉換成webP格式。
關於webP的優缺點和實踐方案,建議移步到《WebP的問題和解決方案》繼續閱讀。
利用現有的圖片進行復用是一個至關有用的方案,關於複用的原則建議和設計進行討論,當設計師認爲兩者均爲同一圖形的時候纔可被認爲可複用。
咱們經過svg可讓一張圖片適用於不一樣大小的容器中,以達到複用的目的。最多見的例子就是「叉」,除非你的x是有多種顏色的,那麼這種表示關閉的icon能夠複用到不少地方。
上圖中我經過組合的方式將長得同樣的icon(facebook、renren等)複用到了不一樣的界面中,不只實現了效果,可維護性也不錯。
着色器(tint)是一個強大的工具,我將其和shape、svg等結合後產生了化學反應。
TintMode共有6種,分別是:add,multiply,screen,srcatop,srcin(默認),src_over。
通常用默認的模式就能夠搞定大多數需求了,使用到的控件主要是TextView和ImageButton。ImageButton官方已經給出了支持方案,TextView由於有四個Drawable,官方的tint屬性在低版本又不可用,因此我讓SelectorTextView支持了一下。若是你想要了解具體的兼容方法,能夠參考代碼或《Drawable 着色的後向兼容方案》。
ImageButton
android:tint="@color/blue"複製代碼
SelectorTextView
app:drawableLeftTint="@color/orange"
app:drawableRightTint="@color/green"
app:drawableTopTint="@color/green"
app:drawableBottomTint="@color/green"複製代碼
由於我用了SelectorTextView和SelectorImageButton,因此我對於背景的tint沒有什麼需求,也就沒作兼容性測試,有興趣的同窗能夠嘗試一下。
若是你決定要採用tint,必定要經過雲測等手段作下兼容性測試,下圖是我對於上述屬性的測試結果:
一個應用中的list頁面都應該作必定程度的統一,對於有限長度的list,咱們可能偏向於用ScrollView作,對於無限長的list用RecyclerView作,但對於它們的按壓效果我強烈建議採用同一個樣式。
以微信爲例,它的全部列表都是白色的item,個人優化思路以下:
若是一個icon能夠經過另外一個icon的旋轉變換來獲得,那麼咱們就能夠經過以下方法來實現:
<?xml version="1.0" encoding="utf-8"?>
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/blue_btn_icon" // 原始icon
android:fromDegrees="180" // 旋轉角度
android:pivotX="50%"
android:pivotY="50%"
android:toDegrees="180" />複製代碼
這種方法雖好,可是不要濫用。須要讀代碼的人具有這種知識,不然會出現很差維護的狀況。並且在設計師真的是認爲兩個圖有如此的關係的時候纔可這樣實現,萬不可耍小聰明。
圖片的壓縮策略是:
關於如何量化兩張圖片在視覺上的差異,Google 提供了一個叫butteraugli的工具,有興趣的同窗能夠嘗試一下。
關於更加詳細的內容能夠參考:《smallerapk-part-6-image-optimization》和《QQ音樂團隊的PNG圖片壓縮對比分析》
mac上超好用的圖片壓縮工具是ImageOptim,它集成了不少好用圖片壓縮庫,不少blog中的圖片也是用它來壓縮的。
值得一提的是,藉助Zopfli,它能夠在不改變png圖像質量的狀況下使圖片大小明顯變小。
pngquant也是一款著名的壓縮工具,對於png的療效還不錯。它不必定適合app中那種背景透明的小icon,因此對比起tinypng來講,優點不明顯。
數據來自:www.jianshu.com/p/a721fbaa6…
tinypng是一款至關著名的商用壓縮工具,tinypng提供了開放接口供開發者開發屬於本身的壓縮工具(付費服務)。tinypng對於免費用戶也算友好,每個月能夠免費壓縮幾百張圖片。
我經過TinyPic來使用tinypng,更加簡單方便。我通常是發版本前才作一次圖片壓縮,每次debug的時候是直接跳過這個task的,徹底不影響平常的debug。
tinyinfo {
apiKey = 'xxxxxxxxx'
//編譯時是否跳過此task
skip = true
//是否打印日誌
isShowLog = true
}複製代碼
有人說tinypng的缺點是在壓縮某些帶有過渡效果(帶alpha值)的圖片時,圖片可能會失真,這時你能夠將png圖片轉換爲webP格式的圖片來解決此問題。
aapt能夠在構建過程期間優化放置在res/drawable/
中的圖像資源,以及無損壓縮。 aapt可將不須要多於256種顏色的真彩色png轉換爲帶有調色板的8位png,藉此來獲得質量相同但佔用內存較小的圖像。
請記住,aapt有如下限制:
若是你本身作了圖片壓縮,那麼請使用cruncherEnabled
來禁用aapt的壓縮功能:
android {
defaultConfig {
//...
}
aaptOptions {
cruncherEnabled = false
}
}複製代碼
dex自己的體積仍是很可觀的,雖然說代碼這東西不佔用多少存儲空間,可是微信這樣的大廠的dex已經達到了20多M。我大概估計了一下,若是你沒有達到方法數上限,那麼你的dex的大小大約是10M,可沒有用multiDex的又有幾家呢?
要優化這個部分,你首先要對公司的、android庫的、第三方庫的代碼進行深刻的瞭解。我用了dexcount來記錄項目的方法數:
dexcount {
format = "list"
includeClasses = false
includeFieldCount = true
includeTotalMethodCount = false
orderByMethodCount = false
verbose = false
maxTreeDepth = Integer.MAX_VALUE
teamCityIntegration = false
}複製代碼
經過分析後你能夠得出不少有用的結論,好比某個第三方庫是否已經不用了、本身項目的哪一個包的方法數最多、目前代碼狀況是否合理等等。
我是經過Statistic
這個as插件來評估項目中開發人員寫的代碼量的,它生成的報表也不錯:
如今我能夠知道:
你還能夠用apk-method-count這個工具來查看項目中各個包中的方法數,它會生成樹形結構的文檔,十分直觀。
若是你想刪掉沒有用到的代碼,能夠藉助as中的Inspect Code
對工程作靜態代碼檢查。
Lint是一個至關強大的工具,它能作的事情天然不限於檢查無用資源和代碼,它還能檢測丟失的屬性、寫錯的單位(dp/sp)、放錯目錄的圖片、會引發內存溢出的代碼等等。從eclipse時代發展到如今,lint真的是愈來愈方便了!
Lint的強大也會帶來相應的缺點,缺點就是生成的信息量過多,不適合快速定位無用的代碼。
我推薦的流程是到下圖中的類目中直接看無用的代碼和方法。
注意:
這種刪除無用代碼的工做須要反覆屢次的進行(好比一月一次)。當你刪除了無用代碼後,這些代碼中用到的資源也會被標記爲無用,這時就能夠經過上文提到的Remove Unused Resources
來刪除了。
手動刪除無用代碼的流程太繁瑣了,若是是一兩次倒還會帶來刪除代碼的爽快感,但若是是專人機械性的持續工做,那我的確定是要瘋的。爲了保證每次打包後的apk都包含儘量少的無用代碼,咱們利用一下proguard這個強大的工具。
android {
buildTypes {
release {
minifyEnabled true // 是否混淆
}
}
}複製代碼
雖然這種方式成果顯著,但也須要配合正確的proguard配置才能起做用,推薦看下《讀懂Android中的代碼混淆》一文。
這種利用混淆來刪除代碼的方式是一種保險措施,真正治本的方法仍是在開發過程當中隨手刪除無用的代碼,畢竟開發者纔是最清楚一段代碼該不應被刪的。我以前就是隨手清理了下沒用的代碼,而後就莫名其妙的不用使用mulitdex了。
咱們在測試的時候可能會隨便寫點測試方法,好比main方法之類的,而且還會引入一些測試庫。對於測試環境的代碼gradle提供了很方便的androidTest和test目錄來隔離生產環境。
對於測試時用到的大量庫,能夠進行test依賴,這樣就能夠保證測試代碼不會污染線上代碼,也能夠防止把測試工具、代碼等發佈到線上的錯誤(微博就出過這樣的錯誤)。
// Dependencies for local unit tests
testCompile 'junit:junit:4.12'
testCompile 'org.hamcrest:hamcrest-junit:2.0.0.0'
// Android Testing Support Library's runner and rules
androidTestCompile 'com.android.support:support-annotations:24.1.1'
androidTestCompile 'com.android.support.test:runner:0.5'
androidTestCompile 'com.android.support.test:rules:0.5'
// Espresso UI Testing
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})複製代碼
PS:在layout中利用tools
也是爲了達到上述目的。
debug模式是開發者的調試模式,這個模式下log全開,而且會有一些幫助調試的工具(好比:leakcanary,stetho),咱們能夠經過debugCompile
和releaseCompile
來作不一樣的依賴,有時候也會須要no-op(關於no-op的內容能夠參考下開發第三方庫最佳實踐)。
dependencies {
debugCompile 'com.squareup.leakcanary:leakcanary-android:1.4-beta2'
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta2'
testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta2'
}複製代碼
debug和release是android自己自帶的兩種生產環境,在實際中咱們可能須要有多個環境,好比提測環境、預發環境等,我以rtm(Release to Manufacturing 或者 Release to Marketing的簡稱)環境作例子。
首先在目錄下建立rtm文件:
復刻release的配置:
buildTypes {
release {
zipAlignEnabled true
minifyEnabled true
shrinkResources true // 是否去除無效的資源文件
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
signingConfig signingConfigs.release
}
rtm.initWith(buildTypes.release)
rtm {}
debug {
multiDexEnabled true
}
}複製代碼
配置rtm依賴:
ext {
leakcanaryVersion = '1.3.1'
}
dependencies {
debugCompile "com.squareup.leakcanary:leakcanary-android:$leakcanaryVersion"
rtmCompile "com.squareup.leakcanary:leakcanary-android-no-op:$leakcanaryVersion"
releaseCompile "com.squareup.leakcanary:leakcanary-android-no-op:$leakcanaryVersion"
}複製代碼
rtm環境天然也有動態替換application文件的能力,我爲了方便非開發者區分app類別,我作了啓動icon的替換。
<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.kale.example" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" > <application android:name=".RtmApplication" android:allowBackup="true" android:icon="@drawable/rtm_icon" tools:replace="android:name,android:icon" /> </manifest>複製代碼
如今我能夠將環境真正須要的代碼打包,不須要的代碼所有剔除,以達到瘦身的目的。
谷歌最近有意將support-v4庫進行拆分,可無奈v4被引用的地方太多了,但這不失爲一個好的開始。目前來看使用拆分後的support庫是沒有什麼優勢的,因此我也不建議如今就開始動手,當谷歌和第三方庫做者都開始真的往這方面想的時候,你再開始吧。
mulitdex會進行分包,分包的結果天然比原始的包要大一些些,能不用mulitdex則不用。但若是方法數超了,除了插件化和RN動態發包等奇淫巧技外我也沒什麼好辦法了。
同一功能就用一個庫,禁止一個app中有多個網絡庫、多個圖片庫的狀況出現。若是一個庫很大,而且申請了各類權限,那麼就去考慮換掉他。
話人人都會說,但若是一個項目是由多個項目成員合做完成的,是很難避免重複引用庫的問題的。同一個功能用不一樣的庫,或者一個庫用不一樣版本的現象比比皆是,這也是很難去解決的。個人解決方案是部門之間多溝通,儘可能作base層,base層由少數人進行維護,正如微信在so庫方面的作法:
- C++運行時庫統一使用stlport_shared
以前微信中的C++運行庫大多使用靜態編譯方式,使用stlport_shared方式可減少APK包大小,至關於把你們公有的代碼提取出來放一份,減小冗餘。同時也會節省一點內存,加載so的時候動態庫只會加載一次,靜態庫則隨着so的加載被加載多分內存映像。- 把公用的C++模塊抽成功能庫
其實與上面的思路是一致的,主要爲了減小冗餘模塊。你們都用到的一些基礎功能,應該抽成基礎模塊。
app的瘦身是一個長期而且艱鉅的工做,若是是小公司建議一兩個月作一次。大公司的話通常都會對app的大小進行持續的統計和追蹤,瘦身工做會有專人負責。總之,但願你們在閱讀完本文後能夠着手對項目進行優化工做,帶來真正的收益。
參考資料:
smaller apk系列文章
減小APK的大小,Android官方這樣說
那些你不知道的APK 瘦身,讓你的APK更小
Android技術專題]APK瘦身看這一篇文章就夠了
如何將apk大小減小6M的
Android Vector曲折的兼容之路
淘寶、微博、微信的 Android 圖片放置策略
Putting Your APKs on Diet
Shrink Your Code and Resources
Reduce APK Size