App瘦身

app組成結構

咱們都知道apk是由:html

  • asserts
  • lib
  • res
  • dex
  • META-INF
  • androidManifest

AS 提供了 Analyze APK 功能。java

旁邊的「對比」按鈕提供了diff的功能,讓你能夠方便的進行apk優化先後的對比。react

assets

assets目錄能夠存放一些配置文件或資源文件,好比webview的本地html,react native的jsbundle等,微信的整個assets佔用了13.4M。若是你的應用對本地資源要求不多的話,這個文件應該不會太大。android

lib

lib目錄下會有各類so文件,分析器會檢查出項目本身的so和各類庫的so。微博和微信同樣只支持了arm一個平臺,淘寶支持了arm和x86兩個平臺。git

淘寶:github

resources.arsc

這個文件是編譯後的二進制資源文件,裏面是id-name-value的一個map。由於微信作了資源的混淆,因此這裏能夠看到資源名稱都是不可讀的。web

META-INF

META-INF目錄下存放的是簽名信息,用來保證apk包的完整性和系統的安全性,幫助用戶避免安裝來歷不明的盜版apk。算法

res

res目錄存放的是資源文件。包括圖片、字符串。raw文件夾下面是音頻文件,各類xml文件等等。由於微信作了資源混淆,圖片名字都不可讀了。chrome

dex

dex文件是java代碼打包後的字節碼,一個dex文件最多隻支持65536個方法,這也是爲何微信有了三個dex文件的緣由。api

由於dex分包是不均勻的,你能夠理解爲裝箱,一個箱子的大小是固定的,但你代碼的量是不肯定的,微信把前兩個箱子裝滿了,最後還剩了2m多的代碼,這些代碼也佔用了一個箱子,最終產生了上圖不均勻的結果。

優化assets

assets中會存放資源文件,這個目錄中各個app存放的內容都有所不一樣,因此優化也比較難。自從引入RN以來,這個目錄下還會有jsbundle的信息。若是你有地址選擇的功能,這裏還會存放地址的映射文件(可參考全名k歌)。對於這塊的資源,as是不會進行主動的刪減的,因此一切都是須要靠開發者進行手動管理的。

全名k歌中的bundle文件:

刪除無用字體

中文字體是至關大的,我一直不建議將字體文件隨意丟棄到assets中。有時候一個小功能急着上,開發者爲了追求速度,能夠先放在這裏圖省事。但必定要知道這個隱患,而且必定要多和產品覈對功能的必要性。此外,對於有些只會用在logo中的字體,我推薦將字體文件進行刪減處理。

FontZip是一個字體提取工具,readme中寫到:

通過測試,已經把項目5MB的藝術字體,按需求提取後,佔用只有20KB,而且可正常使用。

減小icon-font的使用

icon-font和svg都能完成一些icon的展現,但由於icon-font在assets中難以管理,而且功能和svg有所重疊,因此我建議減小icon-font的使用,利用svg進行代替,畢竟一個很小的icon-font也比svg大呢。我給出一個提供各類格式icon的網站,方便你們進行測試:icomoon.io/app/

  • svg:549字節
  • png:375字節(單一分辨率)
  • ion-font:1.1kb

動態下載資源

字體、js代碼這樣的資源能動態下載的就作動態下載,雖然這樣會有出錯的可能性,複雜度也會提高,但這個對於app的瘦身和用戶來講是有長遠的好處的。若是你用了RN,你就能夠在app運行時動態去拉取最新的代碼,將圖片和js代碼一併下載後解壓使用。

壓縮資源文件

有些資源文件是必需要隨着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 {  
        try {  
            if (zipStream != null) {  
                zipStream.close();  
            }  
            if (bufferedStream != null) {  
                bufferedStream.close();  
            }  
            if (fileStream != null) {  
                fileStream.close();  
            }  
        } catch (IOException e) {  
            e.printStackTrace();  
        }  
    }  
}  
複製代碼

全名k歌中的assets目錄下我就發現了大量的zip文件:

android上也有一個7z庫幫助咱們方便的使用7z。

優化lib

配置abiFilters

一個硬件設備對應一個架構(mips、arm或者x86),只保留與設備架構相關的庫文件夾(主流的架構都是arm的,mips屬於小衆,默認也是支持arm的so的,但x86的不支持)能夠大大下降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"  
    }  
}  
複製代碼

以後生成的apk中就會排出多餘的平臺文件了。armeabi就不用說了,這個是必須包含的,v7是一個圖形增強版本,x86是英特爾平臺的支持庫。

Build multiple APKs

官方例子

分析用戶手機的cpu

@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,不然會出現模糊異常的問題。
  • 若是你用了RN,那麼對於x86須要謹慎的保留,不然可能會出現用戶找不到so而崩潰的狀況。畢竟rn是一個全局的東西,稍有不慎就可能會出現開機崩的狀況。
  • so這個東西仍是比較危險的,咱們雖然能夠經過統計cpu型號來下降風險,但我仍是推薦發佈app前走一遍大量機型的雲測,經過雲測平臺把風險進一步下降。
  • 小廠的項目可能會捨棄一些so,但隨着公司規模的增大,你將來仍舊要重複考慮這個問題。因此我推薦在崩潰系統中上傳用戶cpu型號的信息,這樣咱們就能夠在第一時間知道因找不到so引發的崩潰量,至因而否須要增長so就看問題的嚴重程度了。

避免複製so

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」  
   ...  
>  
複製代碼

優化resources.arsc

resources.arsc中存放了一個對應關係:

咱們在程序運行的時候確定要常常用到id,所以它在安裝以後仍須要被頻繁的讀取。若是將這個文件進行壓縮,在每次讀取前系統都必須進行解壓的操做,這就會有一些性能和內存的開銷,綜合考慮下這是得不償失的。

刪除無用的資源映射

resources.arsc的正確瘦身方式是刪除沒必要要的string entry,你能夠藉助 android-arscblamer 來檢查出能夠優化的部分,好比一些空的引用。

進行資源名稱混淆

微信團隊開源了一個資源混淆工具,AndResGuard。它將資源的名稱進行了混淆,因此能夠用它對resources.arsc進行優化,只是具體優化效果與編碼方式、id數量、平均減小命名長度有關。

咱們一眼就能夠知道表2確定比表1存儲的字符要小,因此整個文件的大小確定也要小一些。

關於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

META-INF文件夾中有三個文件,分別是MANIFEST.MF、CERT.SF、CERT.RSA。下面我將會列出簡要的分析,若是你但願更詳盡的瞭解原理,能夠查看《Android APK 簽名文件MANIFEST.MF、CERT.SF、CERT.RSA分析》

MANIFEST.MF

每個資源文件(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編碼的結果。

CERT.SF

這裏有一項SHA1-Digest-Manifest的值,這個值就是MANIFEST.MF文件的SHA-1並base64編碼後的值。後面幾項的值是對MANIFEST.MF文件中的每項再次SHA1並base64編碼後的值。因此你會看到在manifest.mf中的資源名稱在這裏也出現了,好比abc_btn_check_material這個系統資源文件就出現了兩次。

MANIFEST.MF:

CERT.SF

前者是:4XHnecusACTIgtImUjC7bQ9HNM8=,後者是YFDDnTUd6St4932sE/Xk6H0HMoc=。若是你把前一個文件打開在後面加上\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

CERT.RSA包含了公鑰、所採用的加密算法等信息。它對前一步生成的MANIFEST.MF使用了SHA1-RSA算法,用開發者的私鑰進行簽名,在安裝時使用公鑰解密它。解密以後,將它與未加密的摘要信息(即,MANIFEST.MF文件)進行對比,若是相符,則代表內容沒有被修改。這點和app瘦身就徹底無關了,就是android的apk簽名機制。

具體的簽名過程能夠參考:blog.csdn.net/asmcvc/arti…

優化建議

經過分析得出,除了CERT.RSA沒有壓縮機會外,其他的兩個文件均可以經過混淆資源名稱的方式進行壓縮。

優化res

這裏的優化會分爲兩塊,一個是文本資源(shape、layout等)優化,還有一個就是圖片資源優化。

說明:

上圖中有-v4,-v21這樣的文件有些是app開發者本身寫的,但大多都是系統在打包的時候自動生成的,因此你只須要考慮本身項目中的drawable-mdpi、drawable-hdpi、drawable-xhdpi、drawable-xxhdpi便可。

經過as刪除無用資源

在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中資源的大小

  • assets目錄容許下面有多級子目錄,而raw下不容許存在目錄結構
  • assets中的文件不會產生R文件映射,但raw會
  • 若是你app最低支持的版本不是2.3的話,assets和raw應該都不會對資源文件的大小進行限制
  • raw文件會生成R文件映射,能夠被as的lint分析,而assets則不能
  • raw缺乏子目錄的缺點讓其沒法成爲存放大量文件的目錄

通常raw文件下會放音頻文件。若是raw文件夾下有音頻文件,儘可能不要使用無損(如:wav)的音頻格式,能夠考慮同等質量但文件更小的音頻格式。

ogg是一種較適合作音效的音頻格式。移動端的音頻主要是音效和短小的音頻,因此淘寶大量選擇了ogg格式,微博的選擇格式比較多,有wav、mp三、ogg,我更加推薦淘寶的作法。固然,你仍舊不要忘記opus格式,opus也是一種有損壓縮格式,若是感興趣的話也能夠嘗試一下。

統一應用風格,減小shape文件

一個應用的界面風格是必需要統一的,這個越早作越好,最基本的就是統一顏色和按鈕的按壓效果。無UI設計和扁平化風格流行後,卻是給應用瘦身帶來了極大的的福利。界面變得越樸實,咱們能夠用shape畫的東西就越多。

當你的app統一過每種顏色對應的按下顏色後,接下來就須要統一按鈕的形狀、按鈕的圓角角度、有無陰影的樣子、陰影投射角度,陰影範圍等等,最後還要考慮是否支持水波紋效果。

我簡單將按鈕分爲下列元素:

上面的各個元素會產生大量的組合,shape和layer-list固然能夠實現各類組合,但這樣的話光按鈕的背景文件就有多個,很很差維護。 通常爲了開發方便,都會把須要用到的各類selector圖片事先定義好,作業務的時候只須要去調用就行。但這大量的selector文件對於業務開發者來講也是有記憶難度的,因此我推薦使用SelectorInjection這個庫,它能夠將上面的每一個元素進行各類組合,用最少的資源文件來實現大量的按壓效果。

用庫雖然好,但庫也會帶來學習成本,因此引入者能夠將上述的組合定義爲按鈕的一個個的style。由於style自己是支持繼承的,對於這樣的組合形態來講,繼承簡直是一大利器。當你的style有良好的命名後,調用者只須要知道引入什麼style就行,至於你用了什麼屬性別人才不但願管呢。若是業務開發中有一些特別特殊的按壓狀態,沒有任何複用的價值,那你就能夠利用庫提供的豐富屬性在layout文件中進行實現,不再用手忙腳亂的處處定義selector文件了。

我將不能繼承和不靈活的shape變成了一個個單一的屬性,經過庫將多個屬性進行組合,接着利用支持繼承的style來將多個屬性固定成一個配置文件,最後對外造成強制的規範性約束,至此便完成了減小selector文件的工做。

使用toolbar,減小menu文件

若是你仍是苦苦依戀着actionbar的配置模式,我推薦一個庫AppBar,它可讓你在用靈活的toolbar的同時也享受到配置menu的便利性。

限制靈活性,減小layout文件

減小layout文件有兩個方法:複用和融合(include)。

複用layout文件

把一些頁面共用的佈局抽出來,這不管是對layout文件的管理仍是瘦身都是極爲有用的。就好比說任何一個app的list頁面是至關多的,從佈局層面來講就是一個ListView或者RecyclerView,其背後還可能會有loading的view,空狀態的view等等,因此個人建議是創建一個list_layout.xml,其他的list頁面能夠複用或者include它,這樣會從很大程度上減小layout文件的數目。

融合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圖也是須要切多份的,因此這裏我仍是先分析一下大廠的放圖策略,最後我們再討論下較優的方案。

  1. 淘寶 mdpi:

mdpi中存留了一些android原始的icon,這個從命名和前綴就能看出來。經過圖片大小分析,這個目錄下面都是一些很小的icon,還有一些沒有用到的icon(這個launcher圖片也很好的說明了淘寶的歷史)。

hdpi:

hdpi中分爲兩部分:表情和其餘圖片。f+數字的圖片都是表情圖片,淘寶僅僅有一套表情圖片,而且都放在這個目錄下。除了少許的圖片和mdip的圖片一致(好比用戶頭像的place_holder)外,其他的圖片和mdpi的圖片徹底不一樣。順便說一下,此目錄下除了表情以外,其他的都是一些小icon,絕對屬於尺寸很小的那類。

xhdpi:

xhdpi又和hdpi不一樣了,它裏面有大量的國家icon。除此以外就是一些對清晰度要求較高的icon。

xxhdpi:

xxhdpi就沒什麼東西了,幾張圖而已。

其餘:

有後綴的文件夾中除了5張左右的淘寶本身的icon外,其他都是系統的圖片,均以abc開頭。我不清楚淘寶到底有沒有使用到這些圖片,但我能夠確定地說其中有着冗餘圖片,或許有着進一步優化的方案。

總結: 淘寶的放置圖片策略是大量的圖片在hdpi,xhdpi中,好比表情圖在hdpi中,國家圖在xhdpi中,大多數圖片都僅有一套,少數全局的icon是會有多張的狀況(極少,估計只有十幾張)。xxhdpi和mhdpi僅僅做爲補充,沒有太大的做用。

淘寶最使人好奇的點在於它的資源文件很小,可是so文件至關大:

  1. 微博

微博是一個典型的android風格的app,它的drawable全都是有後綴的,徹底符合安卓標準的默認打包策略,它還有根據像素密度的圖片,甚至有ldpi的目錄。

mdpi-v4

mdpi中有大量的小icon,裏面有個叫作share_wx的,從名字一會兒就知道是微信分享的icon,但實際是微博的logo,比較有趣。其他的都是一些邊邊角角的圖標,量不大,因此主力圖確定不在這裏。

hdpi-v4

這是微博圖片存放的主要目錄,有不少大背景和表情,微博的表情圖片和淘寶同樣都是在hdpi中的,它以lxh,emoji等前綴開頭,用來區分不一樣風格的表情。

xhdpi-v4和xxhdpi-v4*

這裏放了一些背景大圖,我也發現了大量和hdpi中同樣的圖片,因此能夠大膽的假設微博是作了不一樣像素的圖片的。這也證明了個人想法——微博是很標準的android應用。

ldpi-v4

這個目錄的確沒什麼用,微博自身也不會維護這個目錄,這全都是第三方庫和應用商店給的圖片,微博開發者只須要放進來就好。

sw400dp和sw32dp-400dp

這些目錄放了一些爲不一樣分辨率準備的長得相同的icon,固然還有微博本身的logo。

  1. 微信

經過上面的分析,咱們是否是能夠得出一些經驗了呢?

  • 大量的圖片都在hdpi和xdpi中
  • 表情圖片在hdpi中
  • anim目錄中都是xml文件
  • drawable目錄中有大量的xml和少許的png和.9圖
  • layout文件中是所有的xml
  • raw中放置音頻
  • svg圖片在raw、drawable或assets中
  • 最大的文件夾是圖片文件
  • layout文件較小
  • 相同的圖片,高分屏的確定比低分屏的大

ok,如今我們就能夠來看微信的資源了,混淆怕啥,友盟混淆的abcd代碼都能看懂,微信的adcd資源也應該不難。

raw(a9)

這個目錄中放置了大量的svg圖片和mp3文件,從專業的角度來想,drawable目錄下確定不會放mp3,因此這個確定是raw文件了。這個目錄下有大量的svg,因此能夠看出微信已經實現了全svg化,而且已經在線上穩定運行了。這點在微信早期的公衆號上也能夠獲得佐證,詳細請看Android微信上的SVG

文中提到了:

第一步,拿到.svg後綴的資源文件(UI很容導出這種圖片),放在raw目錄下而不是drawable目錄。 第二步,把 R.drawable.xxx 換成 R.raw.xxx;把 @drawable/xxx 換成 @raw/xxx。

layout(f)

如今有了兩個線索,那麼初步估計上面的三個目錄確定是咱們常見的目錄,不然不會那麼大。

打開f後發現這個就是layout文件,因此其他的確定就是圖片文件了。

hdpi(y)

y是2.9m,裏面有大量的表情,因此我判斷它是hdpi,爲了更加證明這個猜想,我找到了兩張相同的圖片。

a2中:

y中:

y中圖片是11kb,a2中圖片是76kb,這明顯說明y是hdpi,a2是xhdpi。

xhdpi(a2):

xhdpi中的圖片和hdpi中的圖片相同的很少,微信在這裏放的是一些大圖。

drawable(k)

drawable自己沒啥能夠說的,可是微信的drawable中.9圖份額不多,因此我在想是否svg能夠在必定的程度上完成一些.9的工做呢?

總結:

  • 微信的表情都在hdpi中,僅有一套圖片,這點幾乎已經成爲了標準
  • 微信已經實現了svg化,svg圖片在raw中
  • 微信傾向於把較大的圖片放在xhdpi中,僅出一套圖
  • 微信和淘寶同樣都是儘可能選擇一套圖來完成需求

優化思路

經過分析得出,傳統的出多個分辨率圖片的作法在大廠中已經發生了改變,阿里系、騰訊系的產品都採用了一套圖走天下的路子。這樣的作法仍是有利有弊的,權衡之下我給出以下建議:

  • 聊天表情就出一套圖,放在hdpi中
  • 純色小icon用svg作
  • 背景等大圖,出一套放在xhdpi中 l- ogo等權重較大的圖片可針對hdpi,xhdpi作兩套圖
  • 若是某些圖在真機中確實展現異常,那就用多套圖
  • 若是遇到奇葩機型,可針對性的補圖

優化圖片

使用VectorDrawable

想要作好圖片的優化工做最重要的一點是知道應該選擇什麼樣的圖片格式,對於這點我推薦一個視頻,方便你們進行深刻的瞭解。

這是谷歌給出的建議,簡單來講就是:VD->WebP->Png->JPG

  • 若是是純色的icon,那麼用svg
  • 若是是兩種以上顏色的icon,用webp
  • 若是webp沒法達到效果,選擇png
  • 若是圖片沒有alpha通道,能夠考慮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轉VectorDrawable

先去這裏下載svg圖片:icomoon.io/app/#/selec…

而後利用這個在線工具轉換成VectorDrawable。

svg的兼容性

support庫的代碼質量仍是不錯的,可是svg畢竟是一個圖片格式,因此使用svg前仍是須要格外慎重的。我寫了一個demo,把用到的全部屬性都作了示例,而後利用雲測服務進行兼容性測試。

測試svg也挺簡單的,首先看會不會崩潰,而後看各個分辨率、各個api下是否會有顯示不正常的狀況,若是都ok,那麼就能夠準備引入到項目裏面了。 具體的測試代碼在SelectorInjection,我測試下來100%經過。

svg的使用技巧

設置恰當的寬高

svg圖片是有默認寬高的,設計也會給出一個默認寬高,設置一個合適的默認寬高對之後的圖片複用會有很大幫助。

TextView中drawableLeft等屬性是不能設置圖片的寬高的,但ImageView能夠。若是你的圖片會被複用,建議將圖片的寬高設置爲TextView中的drawable寬高。

利用padding和scaleType屬性

ImageView中的svg默認狀況下是會隨着控件的大小而改變的,它不會像png那樣保持本身的原始大小。咱們能夠利用這一特性,再配合padding和scaleType屬性來完成各類效果。

  • 圖1:60x60,svg自動鋪滿控件
  • 圖2:30x30,svg被壓縮到原始大小如下
  • 圖3:60x60,使用scaleType,讓svg保持原始寬度
  • 圖4:60x60,使用padding,對svg進行任意比例的壓縮
<ImageView  
    android:layout_width="60dp"  
    android:layout_height="60dp"  
  
    android:tint="@color/blue"  
    app:srcCompat="@drawable/facebook"  
    />  
  
<ImageView  
    android:layout_width="30dp"  
    android:layout_height="30dp"  
  
    android:tint="@color/orange"  
    app:srcCompat="@drawable/facebook"  
    />  
  
<ImageView  
    android:layout_width="60dp"  
    android:layout_height="60dp"  
  
    android:scaleType="centerInside"  
    android:tint="@color/red"  
    app:srcCompat="@drawable/facebook"  
    />  
  
<ImageView  
    android:layout_width="60dp"  
    android:layout_height="60dp"  
  
    android:padding="10dp"  
    android:tint="@color/green"  
    app:srcCompat="@drawable/facebook"  
    />  
複製代碼

svg的問題和解決方案

容易寫錯屬性

svg的支持是要經過app:srcCompat這個屬性來作的,若是稍微一不注意寫成了src,那麼就會出現低版本手機上不兼容的問題。你能夠嘗試經過配置Lint規則或是利用腳本進行文件的遍歷等方式來防止出現因開發寫錯屬性而崩潰的問題。

不兼容selector

將svg放入selector中的時候可能會出現一些問題,stackoverflow上也給出瞭解決方案,就是下面這段代碼放在Activity中。

static {  
    AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);  
}  
複製代碼

開啓這個flag後,你就能夠正常的使用Selector這樣的DrawableContainers了。

不支持自定義控件

AppCompatActivity會自動將xml文件中的ImageView替換爲AppCompatImageView,可是若是用了你的自定義控件,那麼這種機制就無效了,因此自定義控件中儘可能使用AppCompatImageView來代替ImageView,使用 setImageResource()來設置資源。若是你的自定義控件中須要得到drawable或者是有自定義需求,那麼能夠參考AppCompatImageView中的svg的helper類來編寫。

public class AppCompatImageHelper {  
  
    private final ImageView mView;  
  
    public AppCompatImageHelper(ImageView view) {  
        mView = view;  
    }  
  
    public void loadFromAttributes(AttributeSet attrs, int defStyleAttr) {  
        TintTypedArray a = null;  
        try {  
            Drawable drawable = mView.getDrawable();  
  
            if (drawable == null) {  
                a = TintTypedArray.obtainStyledAttributes(mView.getContext(), attrs,  
                        R.styleable.AppCompatImageView, defStyleAttr, 0);  
  
                // If the view doesn't already have a drawable (from android:src), try loading // it from srcCompat final int id = a.getResourceId(R.styleable.AppCompatImageView_srcCompat, -1); if (id != -1) { drawable = AppCompatResources.getDrawable(mView.getContext(), id); if (drawable != null) { mView.setImageDrawable(drawable); } } } if (drawable != null) { DrawableUtils.fixDrawable(drawable); } } finally { if (a != null) { a.recycle(); } } } public void setImageResource(int resId) { if (resId != 0) { final Drawable d = AppCompatResources.getDrawable(mView.getContext(), resId); if (d != null) { DrawableUtils.fixDrawable(d); } mView.setImageDrawable(d); } else { mView.setImageDrawable(null); } } boolean hasOverlappingRendering() { final Drawable background = mView.getBackground(); if (Build.VERSION.SDK_INT >= 21 && background instanceof android.graphics.drawable.RippleDrawable) { // RippleDrawable has an issue on L+ when used with an alpha animation. // This workaround should be disabled when the platform bug is fixed. See b/27715789 return false; } return true; } } 複製代碼

不兼容第三方庫

市面上有不少優秀的圖片加載庫,它們通常都會支持多種圖片路徑的加載,好比磁盤圖片,網絡圖片,res圖片等等,對於svg這樣的圖片格式,它們是否支持就要你們結合本身的圖片框架進行調研了。

性能問題

關於動畫和性能方面的問題,《Android Vector曲折的兼容之路》中給出了具體的示例和建議:

Bitmap的繪製效率並不必定會比Vector高,它們有必定的平衡點,當Vector比較簡單時,其效率是必定比Bitmap高的,因此爲了保證Vector的高效率,Vector須要更加簡單,PathData更加標準、精簡,當Vector圖像變得很是複雜時,就須要使用Bitmap來代替了。 Vector適用於icon、Button、ImageView的圖標等小的icon,或者是須要的動畫效果,因爲Bitmap在GPU中有緩存功能,而Vector並無,因此Vector圖像不能作頻繁的重繪。 Vector圖像過於複雜時,不只僅要注意繪製效率,初始化效率也是須要考慮的重要因素。這點能夠參考微信中的svg。 SVG加載速度會快於PNG,但渲染速度會慢於PNG,畢竟PNG有硬件加速,但平均下來,加載速度的提高彌補了繪製的速度缺陷。

不便於管理

svg是沒辦法在文件目錄下進行預覽的,其放置的目錄也和其餘圖片不一樣,若是沒有作好管理工做,將來的drawable目錄就會變得愈加的混亂。其實,對於目錄或者包內文件的管理有個很簡單的原則:同目錄多類型文件,之前綴區分;不一樣目錄,同類型文件,以意義區分。

drawable目錄下有多種類型的文件,咱們利用英文排序的原則將這些文件簡單分爲svg、.9圖、shape、layer-list這幾類。

經過規範特定的前綴,就能夠造成一個便於查找和理解的目錄樹,以達到分類的目的。

在特定前綴的規範下,次級分類的命名就能夠按照功能或用途來作區分,好比button或share的icon就能夠用不一樣的前綴來標識。強烈建議開發和設計定一個命名標準,這樣開發就不用對設計出的圖片進行重命名了,並且還能夠保證兩個部門有一致的認知。

這個僅僅是一個簡單的分類方法,在實際中須要靈活使用。若是一些文件都是用於某種特定類型的,那麼能夠自定義前綴。好比我對於按鈕使用的形狀就用了btn做爲前綴,而忽略了它們自己的文件類型。

不便於預覽

svg是一個特殊格式的文件,可預覽性大大低於png等經常使用的圖片格式,但幸虧win下能夠直接在文件目錄下預覽svg圖像,效果十分不錯。

使用WebP

webp做爲一種新的圖片格式,從Android4.0+開始原生支持,可是不支持包含透明度,直到4.2.1+才支持顯示含透明度的webp,使用的時候要特別注意。 webp相比於png最明顯的問題是加載稍慢,不過如今的智能設備硬件配置愈來愈高,這點差別愈來愈小。騰訊以前有一篇對於webp的分析文十分不錯,若是你準備要用webp了,那麼它絕對值得一看。

**注意:**若是你的項目最低支持到4.2.1,那麼你能夠繼續閱讀了,若是項目還須要支持到4.0版本,我建議暫時不要上webp,成本過高。

png轉webp

咱們能夠經過智圖或者isparta將其它格式的圖片轉換成webP格式。

webp的問題

兼容性很差

官方文檔中說只有在4.2.1+以上的機型,才能解析無損或者有透明度調整的webp圖片,4.0+纔開始支持無透明度的webp圖片。我經過雲測發現,在4.0~4.2.1的系統中,帶有透明度的webp圖片雖然不會崩潰,可是徹底沒法顯示。

《APK瘦身記,如何實現高達53%的壓縮效果》一文中也提到有alpha值的jpg圖片,通過webp轉換後,沒法在4.0,4.1的Android系統上運行的問題,具體緣由見官方文檔:

除了兼容性問題外,webp在某些機型和rom上可能會出現一些「神奇」的問題。在三星的部分機型上,部分有alpha通道的圖中會有一條很明顯的黑線(三星的rom對於shape的alpha的支持也有問題,是紅線)。在小米2刷成4.xx的手機上,系統未能正確識別xml文件中描述的webp圖片,也會致使加載webp失敗。

不便於預覽

由於webp的圖片格式是很難預覽的,as也沒有辦法直接預覽webp格式,我通常是經過chrome瀏覽器打開webp,十分不方便。

咱們知道gradle在build時,有一個mergeXXXResource Task,它將項目的各個aar中全部的res資源統一整合到/build/intermediates/res/flavorName/{buildType}目錄下。

webpConvertPlugin這個gradle插件能夠在mergeXXXResource Task和processXXXResource Task之間插入一個task,這個task會將上述目錄下的drawable進行統一處理,將項目目錄裏的png、jpg圖片(不包含.9圖片,webp轉換後顯示效果不佳)批量處理成webp圖片,這樣可讓咱們在平常開發時用png、jpg,正式發包時用webp。

複用圖片

複用相同的icon

咱們經過svg可讓一張圖片適用於不一樣大小的容器中,以達到複用的目的。最多見的例子就是「叉」,除非你的x是有多種顏色的,那麼這種表示關閉的icon能夠複用到不少地方。

上圖中我經過組合的方式將長得同樣的icon(facebook、renren等)複用到了不一樣的界面中,不只實現了效果,可維護性也不錯。

使用Tint

着色器(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,個人優化思路以下:

  • 列表由LinearLayout、RecyclerView組成
  • 分割線用統一的shape進行繪製,不用切圖
  • 整個列表背景設置爲白色
  • item的背景是一個selector文件,正常時顏色是透明,按下後出現灰色

經過旋轉來複用

若是一個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" />  
複製代碼

壓縮圖片

圖片的壓縮策略是:

優先壓大圖,後小圖 不壓.9圖(svg在俠義上不算圖) 對於開屏大圖片的壓縮需注意力度,要和設計確認後再作 對於體積特別大(超過50k)的圖片資源能夠考慮有損壓縮 關於如何量化兩張圖片在視覺上的差異,Google 提供了一個叫butteraugli的工具,有興趣的同窗能夠嘗試一下。

ImageOptim

mac上超好用的圖片壓縮工具是ImageOptim,它集成了不少好用圖片壓縮庫,不少blog中的圖片也是用它來壓縮的。

值得一提的是,藉助Zopfli,它能夠在不改變png圖像質量的狀況下使圖片大小明顯變小。

pngquant

pngquant也是一款著名的壓縮工具,對於png的療效還不錯,但它不必定就適合app中那種背景透明的小icon,因此對比起tinypng來講,優點不明顯。

tinypng

tinypng是一款至關著名的商用壓縮工具,tinypng提供了開放接口供開發者開發屬於本身的壓縮工具(付費服務)。tinypng對於免費用戶也算友好,每個月能夠免費壓縮幾百張圖片。

我用gradle插件來使用tinypng,更加簡單方便。我通常的作法是發版本前才作一次圖片壓縮,每次debug的時候是直接跳過這個task的,徹底不影響平常的debug。

tinyinfo {  
    apiKey = 'xxxxxxxxx'  
    //編譯時是否跳過此task  
    skip = true  
    //是否打印日誌  
    isShowLog = true  
} 
複製代碼

有人說tinypng的缺點是在壓縮某些帶有過渡效果(帶alpha值)的圖片時,圖片可能會失真,對於這種圖片你能夠將png圖片轉換爲webP格式。

注意事項

aapt默認會在打包時進行圖片的壓縮工做(不管你知不知道,它一直在默默的工做),若是你已經作了圖片壓縮了,那麼建議手動禁止這個功能,不然「可能會」出現圖片二次壓縮後反而變大的狀況,緣由請看:Smaller PNGs, and Android’s AAPT tool。

android {    
    defaultConfig {    
        //...  
    }    
  
    aaptOptions {    
        cruncherEnabled = false   
    }    
}   
複製代碼

優化dex

dex自己的體積仍是很可觀的,雖然說代碼這東西不佔用多少存儲空間,可是微信這樣的大廠的dex已經達到了20多M。我大概估計了一下,若是你沒有達到方法數上限,那麼你的dex的大小大約是10M。縱觀應用市場,沒有用multiDex的又有幾家呢?

記錄方法數和代碼行數

dexcout

要優化這部分,首先須要對公司的、android庫的、第三方庫的代碼進行深刻的瞭解,我用了dexcount來記錄項目的方法數:

dexcount {  
    format = "list"  
    includeClasses = false  
    includeFieldCount = true  
    includeTotalMethodCount = false  
    orderByMethodCount = false  
    verbose = false  
    maxTreeDepth = Integer.MAX_VALUE  
    teamCityIntegration = false  
}  
複製代碼

經過分析你能夠知道代碼的具體狀況了,好比某個第三方庫是否已經不用了、本身項目的哪一個包的方法數最多、目前代碼狀況是否合理等等。

statistic

我是經過Statistic這個as插件來評估項目中開發人員寫的代碼量的,它生成的報表也不錯:

如今我能夠知道: 哪些類空行數太多,是否是沒有按照代碼規範來; 哪些類的代碼量不多,是否有存在的必要; 哪些類行數過多,是否沒有遵照單一職責原則,是否能夠進行進一步的拆分

apk method

你還能夠用apk-method-count這個工具來查看項目中各個包中的方法數,它會生成樹形結構的文檔,十分直觀。

利用Lint分析無用代碼

若是你想刪掉沒有用到的代碼,能夠藉助as中的Inspect Code對工程作靜態代碼檢查。

Lint是一個至關強大的工具,它能作的事情天然不限於檢查無用資源和代碼,它還能檢測丟失的屬性、寫錯的單位(dp/sp)、放錯像素目錄的圖片、會引發內存溢出的代碼等等。從eclipse時代發展到如今,lint真的是愈來愈方便了,咱們如今只須要點一點就行。

注意:

這種刪除無用代碼的工做須要反覆屢次進行(好比一月一次)。當你刪除了無用代碼後,這些代碼中用到的資源也會被標記爲無用,這時就能夠經過上文提到的Remove Unused Resources來刪除。

經過proguard來刪除無用代碼

手動刪除無用代碼的流程太繁瑣了,若是是一兩次倒還會帶來刪除代碼的爽快感,但若是是專人機械性持續工做,那我的確定要瘋的。爲了保證每次打包後的apk都包含儘量少的無用代碼,咱們能夠在build.gradle中進行以下配置:

android {  
    buildTypes {  
        release {  
            minifyEnabled true // 是否混淆  
        }  
    }  
}  
複製代碼

雖然這種方式成果顯著,但也須要配合正確的proguard配置才能起做用,推薦看下讀懂 Android 中的代碼混淆一文。

這種利用混淆來刪除代碼的方式是一種保險措施,真正治本的方法仍是在開發過程當中隨手刪除無用的代碼,畢竟開發者纔是最清楚一段代碼該不應被刪的。

剔除測試代碼

咱們在測試的時候可能會隨便寫點測試方法,好比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/rtm/release模式

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庫

谷歌最近有意將support-v4庫進行拆分,可是無奈v4被引用的地方太多了,但這不失爲一個好的開始。目前來看使用拆分後的support庫是沒有什麼優勢的,因此我也不建議如今就開始動手,當谷歌和第三方庫做者都開始真的往這方面想的時候,你再開始吧。

減小方法數,不用mulitdex

mulitdex會進行分包,分包的結果天然比原始的包要大一些些,能不用mulitdex則不用。但若是方法數超了,除了插件化和RN動態發包等奇淫巧技外我也沒什麼好辦法了。

使用更小庫或合併現有庫

同一功能就用一個庫,禁止一個app中有多個網絡庫、多個圖片庫的狀況出現。若是一個庫很大,而且申請了各類權限,那麼就去考慮換掉他。

話人人都會說,但若是一個項目是由多個項目成員合做完成的,很難避免重複引用庫的問題,同一個功能用不一樣的庫,或者一個庫用不一樣版本的現象比比皆是,這也是很難去解決的。個人解決方案是部門之間多溝通,儘可能作base層,base層由少數人進行維護,正如微信在so庫層面的作法:

C++運行時庫統一使用stlport_shared 以前微信中的C++運行庫大多使用靜態編譯方式,使用stlport_shared方式可減少APK包大小,至關於把你們公有的代碼提取出來放一份,減小冗餘。同時也會節省一點內存,加載so的時候動態庫只會加載一次,靜態庫則隨着so的加載被加載多分內存映像。 把公用的C++模塊抽成功能庫 其實與上面的思路是一致的,主要爲了減小冗餘模塊。你們都用到的一些基礎功能,應該抽成基礎模塊。

相關文章
相關標籤/搜索