App瘦身最佳實踐

天之界線2010發表於2019-03-02

本文會不定期更新,推薦watch下專案。
如果喜歡請star,如果覺得有紕漏請提交issue,如果你有更好的點子可以提交pull request。
本文的示例程式碼主要是基於作者的經驗來編寫的,若你有其他的技巧和方法可以參與進來一起完善這篇文章。

業務方和開發都希望app儘量的小,本文會給出多個實用性的技巧來幫助開發者進行app的瘦身工作。瘦身和減負雖好,但需要注意瘦身對於專案可維護性的影響,建議根據自身的專案進行技巧的選取。

本文固定連線:github.com/tianzhijiex…


一、背景

目前app的大小越來越大,使用者對於過大的app接受度不高,所以除了外掛化和RN的方案外,我們只能老老實實的進行app的瘦身工作。

二、需求

  1. 我要利用混淆來讓我的程式碼儘可能少
  2. 最好能用最少的切圖完成功能
  3. layout檔案不要太多,太多了亂
  4. 能動態下載的就做動態
  5. 我希望能用大小最小的圖片
  6. 如果能用svg,我就用svg
  7. 對於無用的資源,我要as能自動刪除掉
  8. 中國文字博大精深,而我只要我需要的字的字型
  9. 最好能根據下載使用者手機的cpu和解析度來引入不同的資源

三、實現

分析app組成結構

做瘦身之前一定要了解自己app的組成結構,要有針對性的進行優化,並且要逐步記錄比對,這樣才能更好的完成此項工作。關於apk的大小,我推薦google的這個視訊。目前as的2.2預覽版中已經有了apk分析器,功能相當強大,此外你還可以利用nimbledroid來分析apk。

App瘦身最佳實踐
nimbledroid是一個強大的工具,推薦一試

我們都知道apk是由:

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

這幾個部分構成的。

下面我會利用as的分析工具,以微信、微博、淘寶為例進行講述。

App瘦身最佳實踐
概覽

分析完成後你還可以看到具體類目佔的百分比,清晰明瞭。旁邊的“對比”按鈕提供了diff的功能,讓你可以方便的進行apk優化前後的對比,簡直利器。

App瘦身最佳實踐

App瘦身最佳實踐
diff

assets

assets目錄可以存放一些配置檔案或資原始檔,比如webview的本地html,react native的jsbundle等,微信的整個assets佔用了13.4M。如果你的應用對本地資源要求很少的話,這個檔案應該不會太大。

App瘦身最佳實踐

lib

lib目錄下會有各種so檔案,分析器會檢查出專案自己的so和各種庫的so。微博和微信一樣只支援了arm一個平臺,淘寶支援了arm和x86兩個平臺。

App瘦身最佳實踐

resources.arsc

這個檔案是編譯後的二進位制資原始檔,裡面是id-name-value的一個map。因為微信做了資源的混淆,所以這裡可以看到資源名稱都是不可讀的。

App瘦身最佳實踐

索性放個微博的圖,易於大家理解:

App瘦身最佳實踐

META-INF

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

App瘦身最佳實踐

res

res目錄存放的是資原始檔,包括圖片、字串。raw資料夾下面是音訊檔案,各種xml檔案等等。因為微信做了資源混淆,圖片名字都不可讀了。

App瘦身最佳實踐
res

微博就沒有做資源混淆,所以可讀性較好:

App瘦身最佳實踐

dex

dex檔案是java程式碼打包後的位元組碼,一個dex檔案最多隻支援65535個方法,這也是為什麼微信有了三個dex檔案的原因。

App瘦身最佳實踐

因為dex分包是不均勻的,你可以理解為裝箱,一個箱子的大小是固定的,但你程式碼的量是不確定的,微信把前兩個箱子裝滿了,最後還剩了2m多的程式碼,這些程式碼也佔用了一個箱子,最終產生了上圖不均勻的情況。

現在,我們已經知道了apk中各個檔案的大小和它們佔的比例,下面就可以開始針對性的進行優化了。

優化assets

assets中會存放資原始檔,這個目錄中不同廠的app存放的內容各有不同,所以優化也比較難。自從引入RN以來,這個目錄下還會有jsbundle的資訊(可參考全民k歌)。如果你有地址選擇的功能,這裡還會存放地址的對映檔案。
對於這塊的資源,as是不會進行主動的刪減的,所以一切都是需要靠開發者進行手動管理的。

App瘦身最佳實踐
全民k歌中的bundle檔案

刪除無用字型

中文字型是相當大的,我一直不建議將字型檔案隨意丟棄到assets中。有時候一個小功能急著上,開發者為了追求速度,可以先放在這裡圖省事。但一定要知道這個隱患,並且一定要多和產品核對功能的必要性。對於有些只會用在logo中的字型,我推薦將字型檔案進行刪減處理。

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

經過測試,已經把專案5MB的藝術字型,按需求提取後,佔用只有20KB,並且可正常使用。

App瘦身最佳實踐
gif2 (1).gif-204.5kB

減少icon-font的使用

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

App瘦身最佳實踐

App瘦身最佳實踐

  • svg:549位元組
  • png:375位元組(單一解析度的一張圖)
  • ion-font:1.1kb

動態下載資源

字型、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檔案:

App瘦身最佳實踐

android上也有一個7z庫幫助我們方便的使用7z,這個庫我目前沒用到,有需求的同學可以嘗試一下。

優化lib

配置abiFilters

一個硬體裝置對應一個架構(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
    }
  }
}複製程式碼
  • enable: 啟用ABI拆分機制
  • exclude: 預設情況下所有ABI都包括在內,允許移除一些ABI
  • include:指明要包含哪些ABI
  • reset():重置ABI列表為只包含一個空字串(這也是允許的,在與include一起使用來可以表示要使用哪一個ABI)
  • universalApk:指示是否打包一個通用版本(包含所有的ABI)。預設值為 false。

根據手機的cpu來引入so

我們在捨棄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;
    }
}複製程式碼

注意:

  1. 如果你和我一樣用到了renderscript,那麼你必須包含v7,否則會出現模糊異常的問題。
  2. 如果你用了RN,那麼對於x86需要謹慎的保留,否則可能會出現使用者找不到so而崩潰的情況。畢竟rn是一個全域性的東西,稍有不慎就可能會出現開機崩的情況。
  3. so這個東西還是比較危險的,我們雖然可以通過統計cpu型號來降低風險,但我還是推薦釋出app前走一遍大量機型的雲測,通過雲測平臺把風險進一步降低。
  4. 小廠的專案可能會捨棄一些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”
   ...
>複製程式碼

如果想了解更多資訊或者想知道這種配置的限制,可以瀏覽下SmallerAPK(8)

優化resources.arsc

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

id name default v11
0x7f090002 PopupAnimation @ref/0x7f040042, @ref/0x7f040041

我們在程式執行的時候肯定要經常用到id,因此它在安裝之後仍需要被頻繁的讀取。如果將這個檔案進行了壓縮,在每次讀取前系統都必須進行解壓的工作。這就會有一些效能和記憶體的開銷,綜合考慮下來,壓縮這個檔案是得不償失的。

刪除無用的資源對映

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

ArscBlamer

App瘦身最佳實踐

進行資源名稱混淆

微信團隊開源了一個資源混淆工具,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

META-INF資料夾中有三個檔案,分別是MANIFEST.MF、CERT.SF、CERT.RSA。下面我將會列出簡要的分析,如果你希望更詳盡的瞭解原理,可以檢視《Android APK 簽名檔案MANIFEST.MF、CERT.SF、CERT.RSA分析》

MANIFEST.MF

App瘦身最佳實踐

每一個資原始檔(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

App瘦身最佳實踐

這裡有一項SHA1-Digest-Manifest的值,這個值就是MANIFEST.MF檔案的SHA-1並base64編碼後的值。後面幾項的值是對MANIFEST.MF檔案中的每項再次SHA1並base64編碼後的值。所以你會看到在manifest.mf中的資源名稱在這裡也出現了,比如abc_btn_check_material這個系統資原始檔就出現了兩次。

MANIFEST.MF:

App瘦身最佳實踐

CERT.SF

App瘦身最佳實踐

  • 前者: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瘦身就完全無關了,這塊我平時也沒有仔細研究過,就不誤人子弟了。具體的簽名過程可以參考:blog.csdn.net/asmcvc/arti…

優化建議

通過分析得出,除了CERT.RSA沒有壓縮機會外,其餘的兩個檔案都可以通過混淆資源名稱的方式進行壓縮。

App瘦身最佳實踐

優化res

資原始檔的優化一直是我們的重頭戲。如果要和它進行對比,上文的META-INF檔案的優化簡直可以忽略不計。res的優化分為兩塊:一個是文字資源(shape、layout等)優化和圖片資源優化。本節僅探討除圖片資源優化外的內容,關於圖片的內容下面會另起一節。

App瘦身最佳實踐

說明:
上圖中有-v4,-v21這樣的檔案有些是app開發者自己寫的,但大多都是系統在打包的時候自動生成的,所以你只需要考慮自己專案中的drawable-mdpi、drawable-hdpi、drawable-xhdpi、drawable-xxhdpi即可。

通過as刪除無用資源

在as的任何檔案中右擊,選擇清除無用資源即可刪除沒有用到的資原始檔。

App瘦身最佳實踐

不要勾選清除id!如果清除了id,會影響databinding的使用(id絕對佔不了多少空間)

App瘦身最佳實踐

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是一種較適合做音效的音訊格式。當年我初中做遊戲的時候,我全都是用的mp3和png,最終遊戲達到了2G。在換為ogg和jpg後,遊戲縮小到了1G以內(因為遊戲中音訊和大圖較多,所以效果比較誇張)。

移動端的音訊主要是音效和短小的音訊,所以淘寶大量選擇了ogg格式,微博的選擇格式比較多,有wav、mp3、ogg,我更加推薦淘寶的做法。當然,你仍舊不要忘記opus格式,opus也是一種有失真壓縮格式,如果感興趣的話也可以嘗試一下。

App瘦身最佳實踐

統一應用風格,減少shape檔案

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

App瘦身最佳實踐

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

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

元素 屬性01 屬性02 屬性03 屬性04
形狀 正方形 三角形 圓角矩形 圓形
顏色
有無陰影
陰影大小 3dp 5dp
陰影角度 90° 120° 180°
水波紋效果

各個元素組合後會產生大量的樣式,shape和layer-list當然可以實現各種組合,但這樣的話光按鈕的背景檔案就有n個,很不好維護。

一般為了開發方便,都會把需要用到的各種selector圖片事先定義好,做業務的時候只需要去呼叫就行。但這大量的selector檔案對於業務開發者來說也是有記憶難度的,所以我推薦使用SelectorInjection這個庫,它可以將上面的每個元素進行各種組合,用最少的資原始檔來實現大量的按壓效果。

用庫雖然好,但庫也會帶來學習成本,所以引入者可以將上述的組合定義為按鈕的一個個的style。因為style本身是支援繼承的,對於這樣的組合形態來說,繼承真是是一大利器。當你的style有良好的命名後,呼叫者只需要知道引入什麼style就行,至於你用了什麼屬性別人才不希望管呢。

如果業務開發中有一些特別特殊的按壓狀態,沒有任何複用的價值,那你就可以利用庫提供的豐富屬性在layout檔案中進行實現,再也不用手忙腳亂的到處定義selector檔案了。

App瘦身最佳實踐

我將不能繼承和不靈活的shape變成了一個個單一的屬性,通過庫將多個屬性進行組合,接著利用支援繼承的style來將多個屬性固定成一個配置檔案,最後對外形成強制的規範性約束,至此便完成了減少selector檔案的工作。

使用toolbar,減少menu檔案

menu檔案是actionBar時代的產物,as雖然對於menu的支援做的還不錯,但我也很難愛上它。

menu的設計初衷是解耦和抽象,但因為過度的解耦和定製,讓開發變得很不方便,很多專案已經不再使用menu.xml作為actionbar的選單了。

就目前的形勢來看,toolbar是android未來的方向。我雖然作為一個對actionbar和actionbar的相容處理相當瞭解的人,但我還是不得不承認actionbar的時代過去了。如果你不信,我可以告訴你淘寶的menu檔案就3個,微博的menu檔案就9個,如果你還是苦苦依戀著actionbar的配置模式,我推薦一個庫AppBar,它可以讓你在用靈活的toolbar的同時也享受到配置menu的便利性。

App瘦身最佳實踐

限制靈活性,減少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的重構功能自動完成,絕不拖泥帶水。

App瘦身最佳實踐

<!-- 使用include -->
<include
    android:id="@+id/bottom_ub"
    layout="@layout/demo_uiblock"
    android:layout_width="match_parent"
    android:layout_height="100dp"
    />複製程式碼

2. ListHeader

App瘦身最佳實踐

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大小的優化,但是如果你放錯了圖片,對於app啟動時的記憶體大小會有一定的影響

思考一下,如果把一個本來應該放在drawable-xxhdpi裡面的圖片放在了drawable資料夾中會出現什麼問題呢?
在xxhdpi裝置上,圖片會被放大3倍,圖片記憶體佔用就會變為原來的9倍!

國內也有很多人說可以用一套圖片來做,不用出多套圖,藉此來達到app瘦身和給設計減負的目的。谷歌官方是建議為不同解析度出不同的圖片,為此國內也有不少文章討論過這件事情,這篇總結的不錯推薦一讀。

每次說到這個話題的時候總有很多人有不同的看法,況且很多人還不知道.9圖也是需要切多份的,所以這裡我還是先分析一下大廠的放圖策略,最後我們們再討論下較優的方案。

分析過程見:《淘寶、微博、微信的 Android 圖片放置策略》

廠商 mdpi hdpi xhdpi xxhdpi
淘寶 小icon 表情 國家icon 棄用
微博 小icon 背景圖&表情 背景圖 背景圖
微信 棄用 表情 大圖 棄用

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

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

成年人不看對錯,只看利弊,所以還請大家權衡一二。

丟棄特定資源

如果我們希望保留或丟棄特定的資源,需要在專案中建立一個res/raw/keep.xml檔案,這裡可以使用tools:keeptools: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》裡面的相關內容。

App瘦身最佳實踐

這是谷歌給出的建議是:VD->WebP->Png->JPG

  1. 如果是純色的icon,那麼用svg
  2. 如果是兩種以上顏色的icon,用webp
  3. 如果webp無法達到效果,選擇png
  4. 如果圖片沒有alpha通道,可以考慮jpg

使用VectorDrawable

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

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格式。

App瘦身最佳實踐

關於webP的優缺點和實踐方案,建議移步到《WebP的問題和解決方案》繼續閱讀。

複用圖片

利用現有的圖片進行復用是一個相當有用的方案,關於複用的原則建議和設計進行討論,當設計師認為二者均為同一圖形的時候才可被認為可複用。

複用相同的icon

我們通過svg可以讓一張圖片適用於不同大小的容器中,以達到複用的目的。最常見的例子就是“叉”,除非你的x是有多種顏色的,那麼這種表示關閉的icon可以複用到很多地方。

App瘦身最佳實踐
可以複用的x

App瘦身最佳實踐
利用svg和tint

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

使用Tint

著色器(tint)是一個強大的工具,我將其和shape、svg等結合後產生了化學反應。

TintMode共有6種,分別是:add,multiply,screen,srcatop,srcin(預設),src_over。

App瘦身最佳實踐

一般用預設的模式就可以搞定大多數需求了,使用到的控制元件主要是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,一定要通過雲測等手段做下相容性測試,下圖是我對於上述屬性的測試結果:

App瘦身最佳實踐
完美相容

複用按壓效果

一個應用中的list頁面都應該做一定程度的統一,對於有限長度的list,我們可能偏向於用ScrollView做,對於無限長的list用RecyclerView做,但對於它們的按壓效果我強烈建議採用同一個樣式。

App瘦身最佳實踐

以微信為例,它的所有列表都是白色的item,我的優化思路如下:

  1. 列表由LinearLayout、RecyclerView組成
  2. 分割線用統一的shape進行繪製,不用切圖
  3. 整個列表背景設定為白色
  4. 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" />複製程式碼

App瘦身最佳實踐
旋轉圖片

這種方法雖好,但是不要濫用。需要讀程式碼的人具備這種知識,否則會出現不好維護的情況。而且在設計師真的是認為兩個圖有如此的關係的時候才可這樣實現,萬不可耍小聰明。

壓縮圖片

圖片的壓縮策略是:

  1. 優先壓大圖,後小圖
  2. 不壓.9圖(svg在俠義上不算圖)
  3. 對於開屏大圖片的壓縮需注意力度,要和設計確認後再做
  4. 對於體積特別大(超過50k)的圖片資源可以考慮有失真壓縮

關於如何量化兩張圖片在視覺上的差別,Google 提供了一個叫butteraugli的工具,有興趣的同學可以嘗試一下。

App瘦身最佳實踐

關於更加詳細的內容可以參考:《smallerapk-part-6-image-optimization》《QQ音樂團隊的PNG圖片壓縮對比分析》

ImageOptim

mac上超好用的圖片壓縮工具是ImageOptim,它整合了很多好用圖片壓縮庫,很多blog中的圖片也是用它來壓縮的。

App瘦身最佳實踐

值得一提的是,藉助Zopfli,它可以在不改變png影象質量的情況下使圖片大小明顯變小。

pngquant

pngquant也是一款著名的壓縮工具,對於png的療效還不錯。它不一定適合app中那種背景透明的小icon,所以對比起tinypng來說,優勢不明顯。

App瘦身最佳實踐

資料來自:www.jianshu.com/p/a721fbaa6…

tinypng

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有以下限制:

  • aapt工具不會壓縮資源/資料夾中包含的png檔案
  • 影象檔案需要使用256個或更少的顏色的aapt工具來優化它們
  • aapt工具可能會使已壓縮的png檔案膨脹。原因請看:Smaller PNGs, and Android’s AAPT tool

App瘦身最佳實踐

如果你自己做了圖片壓縮,那麼請使用cruncherEnabled來禁用aapt的壓縮功能:

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
}複製程式碼

App瘦身最佳實踐

通過分析後你可以得出很多有用的結論,比如某個第三方庫是否已經不用了、自己專案的哪個包的方法數最多、目前程式碼情況是否合理等等。

statistic

我是通過Statistic這個as外掛來評估專案中開發人員寫的程式碼量的,它生成的報表也不錯:

App瘦身最佳實踐
預覽

App瘦身最佳實踐
java程式碼

現在我可以知道:

  • 哪些類空行數太多,是不是沒有按照程式碼規範來
  • 哪些類的程式碼量很少,是否有存在的必要
  • 哪些類行數過多,是否沒有遵守單一職責原則,是否可以進行進一步的拆分

apk method

你還可以用apk-method-count這個工具來檢視專案中各個包中的方法數,它會生成樹形結構的文件,十分直觀。

App瘦身最佳實踐

利用Lint分析無用程式碼

如果你想刪掉沒有用到的程式碼,可以藉助as中的Inspect Code對工程做靜態程式碼檢查。

App瘦身最佳實踐
search action

App瘦身最佳實踐

Lint是一個相當強大的工具,它能做的事情自然不限於檢查無用資源和程式碼,它還能檢測丟失的屬性、寫錯的單位(dp/sp)、放錯目錄的圖片、會引起記憶體溢位的程式碼等等。從eclipse時代發展到現在,lint真的是越來越方便了!

Lint的強大也會帶來相應的缺點,缺點就是生成的資訊量過多,不適合快速定位無用的程式碼。

我推薦的流程是到下圖中的類目中直接看無用的程式碼和方法。

App瘦身最佳實踐

注意:
這種刪除無用程式碼的工作需要反覆多次的進行(比如一月一次)。當你刪除了無用程式碼後,這些程式碼中用到的資源也會被標記為無用,這時就可以通過上文提到的Remove Unused Resources來刪除了。

通過proguard來刪除無用程式碼

手動刪除無用程式碼的流程太繁瑣了,如果是一兩次倒還會帶來刪除程式碼的爽快感,但如果是專人機械性的持續工作,那個人肯定是要瘋的。為了保證每次打包後的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/rtm/release模式

debug模式是開發者的除錯模式,這個模式下log全開,並且會有一些幫助除錯的工具(比如:leakcanarystetho),我們可以通過debugCompilereleaseCompile來做不同的依賴,有時候也會需要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檔案:

App瘦身最佳實踐
image_1arpi1tfq1dd5pga35j10ntuhjjc.png-12.3kB

復刻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庫方面的做法:

  1. C++執行時庫統一使用stlport_shared
    之前微信中的C++執行庫大多使用靜態編譯方式,使用stlport_shared方式可減小APK包大小,相當於把大家公有的程式碼提取出來放一份,減少冗餘。同時也會節省一點記憶體,載入so的時候動態庫只會載入一次,靜態庫則隨著so的載入被載入多份記憶體映像。
  2. 把公用的C++模組抽成功能庫
    其實與上面的思路是一致的,主要為了減少冗餘模組。大家都用到的一些基礎功能,應該抽成基礎模組。

四、總結

app的瘦身是一個長期並且艱鉅的工作,如果是小公司建議一兩個月做一次。大公司的話一般都會對app的大小進行持續的統計和追蹤,瘦身工作會有專人負責。總之,希望大家在閱讀完本文後可以著手對專案進行優化工作,帶來真正的收益。

App瘦身最佳實踐
developer-kale@foxmail.com

App瘦身最佳實踐
weibo:@天之界線2010

參考資料:
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