Android的apk檔案越來越大了這已經是一個不爭的事實。在Android 還是最初版本的時候,一個app的apk檔案大小也還只有2 MB左右,到了現在,一個app的apk檔案大小已經升級到10MB到20MB這個範圍了。apk檔案大小的爆炸式增長主要是因為使用者對app質量的期待越來越高以及開發者的開發經驗增長,具體體現在以下幾個方面:
- Android裝置 dpi 的多樣化 ([l|m|tv|h|x|xx|xxx]dpi)
- Android平臺的進化,開發工具的改進以及開源類庫生態系統的豐富
- 使用者對高質量UI的期待
- 其他原因
Android開發者在設計一個app的時候應該將最終釋出一個輕量級app作為一個最佳實踐來考慮。為什麼?首先這就意味著你擁有了一個簡潔,易維護程式碼基礎。其次,官方應用商店對超過50MB的apk設定了擴充包檔案下載選項,apk檔案在50MB以下更容易讓使用者下載。最後,我們的應用程式環境是一個頻寬有限,儲存空間有限的環境,apk安裝檔案越小,下載就會越快,安裝也會更快,良性迴圈,最後說不定使用者因為這個給好評。
在大部分情況下,apk大小的增長是為了滿足消費者的需要和期待。然而,我認為apk大小的增速已經超過了使用者對app期待的增速。所以,很大程度上,官方應用商店裡面的那些程式可以瘦身至它們現在大小的一半甚至更多。在這篇文章裡面,我將寫下一些關於如何給apk檔案瘦身的招式,希望你們能夠喜歡。
APK 檔案格式
在說如何給apk瘦身之前,讓我們先來看看apk檔案內部的結構到底是怎麼一回事。說簡單點,一個apk檔案就是包含一些檔案的壓縮包。作為開發者,我們通過使用 unzip
命令解壓這個apk檔案一探apk的內部結構。下面的檔案結構就是我們使用 unzip <your_apk_name>.apk
1這個命令所獲得的:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
/lib /armeabi /armeabi-v7a /x86 /mips /META-INF MANIFEST.MF CERT.RSA CERT.SF /res AndroidManifest.xml classes.dex resources.arsc |
我們可能對上面大部分的檔案和目錄都很熟悉。它們和我們在實際開發app的時候所看到得專案結構一樣,包含了: /assets
, /lib
, /res
,AndroidManifest.xml
. 還有一些檔案可能是我們第一次看到。一般說來,classes.dex
, 它包含了我們所寫的Java程式碼經過編譯後class檔案;resources.arsc
包含了預編譯之後的資原始檔(比如values檔案,XML drawables 檔案等。)。
由於apk檔案只是一個簡單地壓縮檔案,這就意味著它有兩種大小:即壓縮前的大小和壓縮後的大小。這篇文章我將主要關注壓縮後的大小。
如何減少apk檔案大小
減少apk檔案大小可以從幾個方面入手。由於每個app都是不同的,所以沒有什麼絕對規則來給apk檔案瘦身。作為apk檔案的三個重要組成部分,我們可以考慮從它們開始入手:
- Java 原始碼
- 資原始檔(resources/assets)
- native code
所以接下來的招式都是由減少這些元件的大小出發,進而減少整個app的大小。
掌握良好的編碼習慣
這是減少apk檔案至關重要的第一步。你要對自己的程式碼瞭如子掌。你要移除掉所有無用處的dependency libraries,讓你的程式碼一天比一天優秀,持續地優化你的程式碼。總而言之,保持一個簡潔,最新的程式碼基礎是減少apk檔案至關重要的一環。
當然,從零開始一個專案併為這個專案保持一份簡潔的程式碼基礎很容易。專案越老,這個工作就越困難。事實上,擁有一大段歷史背景的專案必須要去處理各種死程式碼和無用程式碼。還好有許多的開發工具可以幫我們來做這些事情……
使用 Proguard
Proguard 是一個很強悍的工具,它可以幫你在程式碼編譯時對程式碼進行混淆,優化和壓縮。它有一個專門用來減少apk檔案大小的功能叫做 tree-shaking。Proguard 會遍歷你的所有程式碼然後找出無用處的程式碼。所有這些不可達(或者不需要)的程式碼都會在生成最終的apk檔案之前被清除掉。Proguard 也會重新命名你的類屬性,類和介面,然整個程式碼儘可能地保持輕量級水平。
也許現在你會認為 Proguard 是一個相當有效地工具。但是能力越大,責任也就越大。現在許多開發這認為Proguard有點讓人不省心,因為它會重度依賴反射。哪些類或者屬性需要被處理或者不能處理都要開發者對 Proguard 進行配置。
廣泛使用 Lint
Proguard 只會對 Java 程式碼起作用,那麼對哪些資原始檔呢?比如一張圖片 my_image
在 res/drawable
資料夾中,沒有被使用,Proguard 只會移除掉 R
類中的引用,但是圖片依然還在資料夾中。
Lint 一個靜態的程式碼分析器,你只需通過呼叫 ./gradlew lint
這個簡單地命令它就能幫你檢查所有無用的資原始檔。它在檢測完之後會提供一份詳細的資原始檔清單,並將無用的資源列在“UnusedResources: Unused resources” 區域之下。只要你不通過反射來反問這些無用資源,你就可以放心地移除這些檔案了。
Lint 會分析資原始檔(比如 /res
資料夾下面的檔案) ,但是會跳過 assets 檔案 ( /assets
資料夾下面的檔案)。事實上assets 檔案是可以通過它們的檔名直接訪問的,而不需要通過Java引用或者XML引用。因此,Lint 也不能判定某個 asset 檔案在專案中是否有用。這全取決於開發者對這個資料夾的維護了。如果你沒有使用某個asset 檔案,那麼你就可以直接清除這個檔案。
對資原始檔進行取捨
Android 支援多種裝置。Android的系統設計讓它可以支援裝置的多樣性:螢幕密度,螢幕形狀,螢幕大小等等。到了Android 4.4,它支援的螢幕密度包括: ldpi, mdpi, tvdpi, hdpi, xhdpi, xxhdpi and xxxhdpi。但是你要知道的一點是,Android 支援這麼多的螢幕密度並不意味著你需要為每一個螢幕密度提供相應的資原始檔。
如果你知道某些螢幕密度的裝置只有很少部分使用者在使用,那麼你就可以直接不需要使用相應螢幕密度的資原始檔。就我個人而言,我只會為我的應用提供 hdpi, xhdpi and xxhdpi2 這幾個螢幕密度的支援。如果某些裝置不是這幾個螢幕密度的,不用擔心,Android 系統會自動使用存在的資源為裝置計算然後提供資原始檔。
我這麼做得原因很簡單。首先,這些裝置螢幕密度就能覆蓋我 80% 的使用者。其次,xxxhdpi 這個螢幕密度只是在為未來的裝置做準備,但是未來還未到來。最後,我真的不怎麼關心低螢幕密度(比如mdpi 和 ldpi),無論我為這幾個螢幕密度努力,結果都是令人傷心地,還不如直接讓Android系統對 hdpi 資原始檔進行適當地縮放來匹配相應地低端機型。
同樣地,在 drawable-nodpi
資料夾裡面維持一個檔案也能節省空間。當然前提是你覺得對這個檔案進行相應地縮放之後呈現的效果你能接受或者這個檔案出現的機率很少。
資原始檔最少化配置
Android 開發經常會依賴各種外部開原始碼庫,比如Android Support Library, Google Play Services, Facebook SDK 等等。但是這些庫裡面並不是所有的資原始檔你都會用到。比如, Google Play Services 裡面會有一些為其他語種提供翻譯,而你的app又不需要這個語種的翻譯,而且這個庫裡面還包含了我的app中不支援的 mdpi 資原始檔
還好從Android Gradle Plugin 0.7 開始,你可以配置你的app的build系統。這主要是通過配置 resConfig
和 resConfigs
以及預設的配置選項。下面的 DSL (Domain Specific Language)就會阻止 aapt(Android Asset Packaging Tool)打包app中不需要的資原始檔。
1 2 3 4 5 6 |
defaultConfig { // ... resConfigs "en", "de", "fr", "it" resConfigs "nodpi", "hdpi", "xhdpi", "xxhdpi", "xxxhdpi" } |
壓縮圖片
Aapt(Android Asset Packaging Tool)就內建了 保真影像壓縮演算法。例如,一個只需 256 色的真彩PNG圖片會被aapt 通過一個顏色調色盤轉化成一個 8-bit PNG 檔案。這可以幫助你減少圖片檔案的大小。當然你還可以通過Google查詢相應的優化工具,比如 pngquant, ImageAlpha 和 ImageOptim 等。你可以從中選擇一個適合你的工具。
還有一種只在Android平臺上存在的圖片檔案也可以優化,它就是 9-patches。就我目前所知道,我還沒發現有這個檔案的優化工具。然而你只需要求你的設計師將它的可擴充套件區域和內容區域儘可能地減少即可。這不但可以減少資原始檔的大小,還能使得以後資原始檔的維護變得更加簡單。
限制app支援的cpu 架構的數目
一般說來Android 使用Java 程式碼即可以滿足大部分需求,不過還是有一小部分案例需要使用一些 native code。就像之前對資原始檔那樣opinionated,你可以這些 native code opinionated。 在當前的Android 生態系統中,讓你的app支援 armabi 和 x86 架構就夠了。這裡有一篇相當不錯的關於如何瘦身native 程式碼庫的文章,你可以參考參考。
儘可能地重用
重用資源可能是你在進行移動開發時需要了解的最重要的優化技巧之一。比如在一個 ListView
或者 RecyclerView
,重用可以幫助你在列表滾動時保持介面流暢。重用還可以幫你減少apk檔案的大小。例如,Android 提供了幾個工具為一個asset檔案重新著色,在Android L中你可以使用 android:tint
和android:tintMode
來達到效果,在老版本中則可以使用 ColorFilter
。
如果系統中有兩種圖片,一種圖片是另一種圖片翻轉180°得到的,那麼你就可以移除一種圖片,通過程式碼實現。比如你現在有兩種圖片分別命名為 ic_arrow_expand
和 ic_arrow_collapse
:
你可以直接移除掉 ic_arrow_collapse
檔案,然後在ic_arrow_expand
的基礎上建立一個 RotateDrawable
。這個方法也可以讓你減少設計人員的工作:
1 2 3 4 5 6 7 |
<?xml version="1.0" encoding="utf-8"?> <rotate xmlns:android="http://schemas.android.com/apk/res/android" android:drawable="@drawable/ic_arrow_expand" android:fromDegrees="180" android:pivotX="50%" android:pivotY="50%" android:toDegrees="180" /> |
在合適的時候使用程式碼渲染影像
在某些情況下,直接使用Java 程式碼渲染影像也能獲得不錯的效果。比如逐幀動畫就是一個很好的例子。最近我都在嘗試一些Android Wear 的開發,瞭解了一下Android wearable support library。就像其他的Android support library 一樣,這個庫裡面也有一些工具來處理穿戴裝置的。
不過讓我吃驚的是,當我簡單地構建了一個 “Hello World”示例,最後得到的apk檔案竟然有1.5MB。於是我快速地研究了一下 wearable-support.aar
檔案,發現這個庫有兩個逐幀動畫,並分別支援了3種不同的螢幕密度:一個 “success” 動畫 (31 frames) 和一個 “open on phone” 動畫 (54 frames)。
這個逐幀success動畫是被一個叫做 AnimationDrawable
所定義的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
<?xml version="1.0" encoding="utf-8"?> <animation-list xmlns:android="http://schemas.android.com/apk/res/android" android:oneshot="true"> <item android:drawable="@drawable/generic_confirmation_00163" android:duration="33"/> <item android:drawable="@drawable/generic_confirmation_00164" android:duration="33"/> <item android:drawable="@drawable/generic_confirmation_00165" android:duration="33"/> <item android:drawable="@drawable/generic_confirmation_00166" android:duration="33"/> <item android:drawable="@drawable/generic_confirmation_00167" android:duration="33"/> <item android:drawable="@drawable/generic_confirmation_00168" android:duration="33"/> <item android:drawable="@drawable/generic_confirmation_00169" android:duration="33"/> <item android:drawable="@drawable/generic_confirmation_00170" android:duration="33"/> <item android:drawable="@drawable/generic_confirmation_00171" android:duration="33"/> <item android:drawable="@drawable/generic_confirmation_00172" android:duration="33"/> <item android:drawable="@drawable/generic_confirmation_00173" android:duration="33"/> <item android:drawable="@drawable/generic_confirmation_00174" android:duration="33"/> <item android:drawable="@drawable/generic_confirmation_00175" android:duration="333"/> <item android:drawable="@drawable/generic_confirmation_00185" android:duration="33"/> <item android:drawable="@drawable/generic_confirmation_00186" android:duration="33"/> <item android:drawable="@drawable/generic_confirmation_00187" android:duration="33"/> <item android:drawable="@drawable/generic_confirmation_00188" android:duration="33"/> <item android:drawable="@drawable/generic_confirmation_00189" android:duration="33"/> <item android:drawable="@drawable/generic_confirmation_00190" android:duration="33"/> <item android:drawable="@drawable/generic_confirmation_00191" android:duration="33"/> <item android:drawable="@drawable/generic_confirmation_00192" android:duration="33"/> <item android:drawable="@drawable/generic_confirmation_00193" android:duration="33"/> </animation-list> |
這樣做得好處就是 (我當然在諷刺) 每幀顯示33ms,這使得整個動畫保持在30fps的頻率。如果每幀16ms這將會導致整個庫是之前的兩倍大。如果你去看原始碼你會發現很有趣。在 generic_confirmation_00175
這一幀 (15 行) 將持續顯示 333ms。 generic_confirmation_00185
緊跟著它。這個優化節省了9個類似的幀 (包含了從176 幀到 184 幀) 。不過最後神奇的是 wearable-support.aar
竟然神奇的包含了這個9個完全無用的幀,而且還以3中螢幕密度展示。3
在程式碼中來渲染這樣的動畫明顯會很花時間。然而當你維持動畫執行在60fps這樣的頻率可以大幅度的減少apk的大小。在寫這篇博文的時候,Android還沒提供工具來渲染這樣的動畫。但是我希望Google正在開發新一代的輕量級實時渲染系統來保證material design的細節呈現。當然“Adobe After Effect to VectorDrawable” 之類的設計工具也能提供很多方便。
如何更進一步?
上面所有的招式都集中在app或者library 開發者。也許我們還可以在app分發渠道方面為apk大小做出一些改變?我想可以在app 分發伺服器端做一些改進,或者在官方應用商店。例如,我們可以期待官方應用商店在使用者安裝app的時候為裝置繫結相應的native 庫而摒棄那些無用的。
同樣地,我們還可以想象只根據目標裝置的配置來打包應用。不幸的是,這可能破壞Android 生態一個重要的功能特性:配置熱置換。事實上,Android一開始就是位處理各種實時的配置更改(語言,螢幕轉向)而設計的。如果我們移除掉與目標螢幕不相容的資原始檔,這可以極大的減少檔案大小。不過Android需要處理實時的螢幕密度更改。即便我們假設廢除這種功能,我們仍然需要處理為不同的螢幕密度設計的圖片以及其他配置(比如螢幕朝向,最小寬度等)。
伺服器端的apk打包看起來很強大。但這樣會冒很大得風險,因為最終傳送給使用者的apk會於開發者發給的伺服器的完全不同。分發一些缺失資原始檔的apk可能會導致app崩潰。
總結
設計就是在一個約束集裡面找出最好的方案。顯然apk檔案的大小就是一個約束。不要害怕為了讓多個方面變得更好而放鬆一個方面的約束。例如,當你要降低UI的渲染效果時,不要猶豫,因為這可以讓apk的大小減小,同時使得app的執行也更加流暢。你99%的使用者是感受不到UI質量變低的,但是他們會注意到apk檔案變小了,執行也更加流暢了。總之,你需要將app各方面進行整體考慮,而不是僅僅幾個方面的斟酌。