熱文導讀 | 點擊標題閱讀
2017 Android秋招面試總結 && 面試資源推薦
Android高級進階架構系列教程視頻分享
吊炸天!74款APP完整源碼
摘要: 如何瘦身是 APK 的重要優化技術。APK 在安裝和更新時都需要經過網絡下載到設備,APK 越小,用戶體驗越好。本文作者通過對 APK 內在機制的詳細解析,給出了對 APK 各組成成分的優化方法及技術,並實現了一個基本 APK 的最小化過程。
正文:
高爾夫運動中,分數最小者勝出。
讓我們將這一原則應用到 Android App 開發中。我們將玩轉一個稱為「ApkGolf」的 APK,目的是創建一個儘可能具有最少字節數的 App,並可安裝在運行 Oreo 的設備上。
一開始,我們用 Android Studio 生成一個預設的 App,創建密鑰庫(Keystore)
(https://developer.android.com/studio/publish/app-signing.html#generate-key)
並對 App 籤名,然後使用命令stat -f%z $filename測定生成 APK 文件的字節數大小。
進一步,為確保該 APK 工作正常,我們將在一臺運行 Oreo 的 Nexus 5x 手機上安裝它。
看上去挺漂亮。但是現在我們的 APK 大小近乎 1.5Mb。
考慮到我們 App 的功能非常簡單,1.5Mb 的規模看上去過於臃腫了。因此,我們要深入了解一下該項目,看看是否有一些能立竿見影地削減文件大小的地方。Android Studio 生成了:
擴展AppCompatActivity而得到的MainActivity;
使用根視圖ConstraintLayout的布局文件;
Value 文件,其中包含三種顏色、一個字符串資源(Resource)和一個主題(Theme);
AppCompat和ConstraintLayout的支持庫;
一個AndroidManifest.xml文件;
PNG 格式的啟動圖標,分別是正方形、圓形和前臺的。
看上去首當其衝的目標是啟動圖標文件,因為 APK 中共包含了 15 個圖像文件,並且在mipmap-anydpi-v26下還有兩個 XML 文件。下面,讓我們使用 Android Studio 的 APK Analyser
(https://developer.android.com/studio/build/apk-analyzer.html)
對該 APK 文件做一個定量分析。
給出的結果與我們的最初假設大相逕庭,其中顯示 Dex 文件是大頭,而上述資源僅佔 APK 大小的 20%。
文件大小佔比classes.dex74%res20%resources.arsc4%META-INF2%AndroidManifest.xml<1%下面讓我們逐個分析每個文件的行為。
看上去罪魁禍首是classes.dex文件,它佔據了 73% 的空間,因而它成為我們的首要削減目標。該文件為 Dex 格式
(https://source.android.com/devices/tech/dalvik/dex-format) ,
其中包含了我們的全部編譯後代碼,以及對 Android 框架和支持庫中外部方法的引用。
然而android.support軟體包中引用了超過 13000 種的方法,對於一個簡單的「Hello World」App 而言,完全沒有必要。
目錄「res」中包含了大量的布局(Layout)文件、Drawable 和動畫,它們並非在 Android Studio UI 中立刻可見。同樣,它們也是由支持庫推入其中的,約佔 APK 規模的 20%。
在resources.arsc文件中,還包含了對每個資源的引用。
目錄「META-INF」中包含有CERT.SF、MANIFEST.MF和CERT.RSA文件,這些文件都需要 v1 APK 籤名
(https://source.android.com/security/apksigning/v2#v1-verification) 。
如果有攻擊者修改了我們 APK 中的代碼,籤名就會不匹配。這一機制保障了用戶能避免執行第三方惡意軟體的風險。
在MANIFEST.MF文件中列出了 APK 中的所有文件。其中,CERT.SF文件中包含了文件清單的摘要,以及每個文件的獨立摘要。CERT.RSA文件中包含了一個公鑰,用於驗證CERT.SF文件的完整性。
在籤名文件中,沒有目標明顯可優化。
看上去AndroidManifest文件非常類似於我們的原始輸入文件。唯一差別在於,文件中的字符串和 Drawable 等資源被整數資源 ID 所替代,這些 ID 以0x7F開頭。
我們尚未在 App 的build.gradle文件中設置允許最小化(Minification)和資源收縮(Resource Shrinking)。我們現在做此設置:
android { buildTypes { release { minifyEnabled true shrinkResources true proguardFiles getDefaultProguardFile( 'proguard-android.txt'), 'proguard-rules.pro' } }}
-keep class com.fractalwrench.** { *; }
將minifyEnabled屬性設置為「true」值,這將啟用 Proguard
(https://www.guardsquare.com/en/proguard) ,
該功能將從 App 中剝離出那些未使用的代碼,並對符號的名稱做模糊化處理,使得 App 難以被反向工程。
設置shrinkResources屬性,將會在 APK 中移除任何並非直接引用的資源。這時如果我們使用反射機制間接地訪問資源,就會導致問題,但是本文給出的 App 並不存在這樣的問題。
我們已經實現了 APK 規模減半,並未對我們的 APP 有任何可見的影響。
對於那些尚未在 App 中啟用AndroidManifest.xml和shrinkResources的開發人員,這是本文給出的最需要重視的並應學會的技巧。他們僅花費數小時做配置和測試,就能輕鬆地削減數兆的規模。
現在classes.dex文件已削減到佔用 APK 的 57%。在我們的 Dex 文件中,大多數方法引用屬於android.support軟體包,因此我們將要去除該支持庫。具體做法為:
public class MainActivity extends Activity
<?xml version="1.0" encoding="utf-8"?><TextView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:text="Hello World!" />
天哪,我們剛剛實現了近十倍的削減,即從 786Kb 削減到 108Kb。唯一可見的更改是工具條(Toolbar)的顏色,現在它使用了預設的 OS 主題。
目錄「res」現在佔用 APK 規模約 95%,原因是所有的加載圖標。如果這些 PNG 圖片是由我們自己的設計師所給出的,那麼我們可以嘗試 將它們轉換為 WebP 格式,該格式更加高效,並被 API 15 及以上所支持。
幸運的是,Google 已經優化了我們的 Drawable。即便沒有這種優化,ImageOptim 也可優化 PNG 並從中剝離不必要的元數據。
讓我們當一次壞人,將我們所有的加載圖標替換為單一的單像素黑點,並置於未驗證的res/drawable目錄中。圖片大小約 67 個字節。
我們已經移除了幾乎全部的資源,因此毫不奇怪 APK 規模已經削減了約 95%。但是resources.arsc依然引用了如下項:
讓我們從第一項著手。
Android 框架會膨脹我們的 XML 文件
(https://developer.android.com/reference/android/view/LayoutInflater.html) ,
並自動創建一個TextView對象,用於Activity對象的contentView。
我們可以嘗試一些跳過中間的過程,具體做法是移除 XML 文件,並使用程序設置contentView。這樣會降低資源的規模,因為我們減少了一個 XML 文件。但是 Dex 文件將會增大,因為我們引用了額外的TextView方法。
TextView textView = new TextView(this);textView.setText("Hello World!");setContentView(textView);
讓我們查看一下這一權衡做法的工作情況,它削減了 5710 個字節。
App 名稱(優化為 6034 字節,削減 4%)下面我們將刪除strings.xml文件,並將AndroidManifest中的android:label屬性值更改為「A」。這看上去是一個小更改,但是它從resources.arsc中刪除了一項,削減了 Manifest 文件中的字符數,並從「res」目錄中移除了一個文件。略有裨益,我們削減了 228 個字節。
Android Platform 代碼庫中的resources.arsc的文檔
(https://android.googlesource.com/platform/frameworks/native/+/jb-dev/libs/utils/README)
告訴我們,APK 中的每個資源通過resources.arsc中的一個整數 ID 引用。這些 ID 具有兩個命名空間(Namespace):
0x01: 系統資源(預裝在 framework-res.apk 中);0x7f: 應用資源(捆綁在應用的.apk 文件中)。
那麼如果在0x01命名空間中引用了一個資源,我們的 APK 發生了什麼?我們應該可以在削減文件規模的同時,得到一個更漂亮的圖標。
android:icon="@android:drawable/btn_star"
雖然文檔是這樣說的,但是在一個生產 App 中,我們應該保持「永遠不要信任系統資源」這一原則。該步驟會導致 Google Play 驗證失敗,而且考慮到我們知道某些製造商已經重定義了白色
(https://www.reddit.com/r/androiddev/comments/71fpru/android_color_resources_not_safe/),
因此在具體操作時需要慎重。
Manifest 文件(優化為 5252 字節,削減 1%)目前為止,我們尚未對 Manifest 文件下手。
android:allowBackup="true"android:supportsRtl="true"
移除這些屬性將會削減 48 個字節。
看上去 Dex 文件中依然包括BuildConfig和R。
-keep class com.fractalwrench.MainActivity { *; }
如果我們精煉 Proguard 規則,就會清除掉這些類。
現在對我們的Activity賦予一個混淆後的名字。對於正常類,Proguard 可自動實現混淆功能,但是考慮到Activity類名會通過Intents喚醒,因此預設情況下不要混淆Activity的名字。
MainActivity -> c.javacom.fractalwrench.apkgolf -> c.c
META-INF(優化為 3307 字節,削減 33%)當前在 App 籤名中,我們使用了 v1 和 v2 籤名。看上去這完全是浪費,尤其是 v2 會對整個 APK 做哈希,提供了更高級的保護能力和性能
(https://source.android.com/security/apksigning/#apk-signing-schemes)。
在 APK Analyser 中,v2 籤名並不可見,因為它在 APK 文件本身中以二進位塊的形式存在。v1 籤名是可見的,它是以CERT.RSA 和 CERT.SF文件的形式給出。
Android Studio UI 中提供了 v1 籤名的複選框,我們需要去除該選擇,並生成一個籤名的 APK。我們也需要做相反的過程。
看上去從此以後我們使用的是 v2。
現在我們要手工編輯我們的 APK 了。我們將使用如下命令:
# 1. 創建一個未籤名的 APK。./gradlew assembleRelease# 2. 解壓縮歸檔文件。unzip app-release-unsigned.apk -d app# 對文件進行編輯。# 3. 壓縮歸檔文件zip -r app app.zip# 4. 運行 zipalign。zipalign -v -p 4 app-release-unsigned.apk app-release-aligned.apk# 5. 使用 v2 籤名運行 apksigner。apksigner sign --v1-signing-enabled false --ks $HOME/fake.jks --out signed-release.apk app-release-unsigned.apk# 6. 驗證籤名。apksigner verify signed-release.apk
此連結
adb shell am start -a android.intent.action.MAIN -n c.c/.c
下面給出新的 Manifest 文件:
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="c.c"> <application> <activity android:name="c" android:exported="true" /> </application></manifest>
我們還移除了加載圖標。
削減方法引用(優化為 2179 字節,削減 12%)
package c.c;import android.app.Application;public class c extends Application {}
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="c.c"> <application android:name=".c" /></manifest>
我們可以使用 adb 驗證該 APK 是可以成功安裝的,也可以通過 Setting App 做驗證。
Dex 優化(優化為 1961 字節,削減 10%)在此次優化中,我花費了多個小時研究 Dex 文件格式
理解 Manifest 文件(優化為 1961 字節,削減 0%)非籤名 APK 中的 Manifest 文件是二進位的 XML 格式,該格式看上去並沒有官方的文檔。我們可以使用 HexFiend編譯器去修改文件內容
(https://github.com/ridiculousfish/HexFiend) 。
無需理解 Manifest 文件(優化為 1777 字節,削減 9%)
下圖給出了一些 Manifest 文件中的重要成分。如果沒有這些成分,APK 將會安裝失敗。
一些事情即刻是很明顯的,例如 Manifest 文件和軟體包標記。在字符串池中還可以找到軟體包名稱和 versionCode。
讓我們查看一下最終的 APK。
終歸,我們使用 v2 籤名在 APK 中留名。讓我們創建一個利用壓縮破解的新密鑰庫。
這可削減 20 個字節。
查看英文原文:https://fractalwrench.co.uk/posts/playing-apk-golf-how-low-can-an-android-app-go/
如你有好的文章想和大家分享歡迎投稿,直接向我投遞文章連結即可
Java和Android架構
微信掃描或者點擊下方二維碼領取Android高級進階資源
關注後回復「百度」、「阿里」、「騰訊」、「資源」有驚喜
公眾號:JANiubility
更多學習資料點擊下面的「閱讀原文」獲取