Android ProGuard:代碼混淆壓縮

寫這篇文章的目的

一直以來,在項目中須要進行代碼混淆時每次都要去翻文檔,很麻煩。也沒有像寫代碼那樣記得那麼多。既然要查來查去,就不如本身捋一捋這個知識點了,被人寫的終究仍是別人的。因此本身去翻看了不少文章和官方文檔,總結下就把這篇文章寫下來了。之後方便查找和修改,也加深這個知識的理解。javascript

前言

Android 開發中,打包避免不了各類優化,開啓混淆能夠很好就是其中一種優化方式。爲了使你打包的 apk 儘量小,應該在打包 apk 的時候開啓代碼壓縮功能移除沒有被使用的代碼和資源。可是這和混淆有什麼關係?php

代碼壓縮混淆:ProGuard 能夠從你打包的應用程序檢測和刪除未使用的類、字段、方法和屬性,固然也會包括libraries裏的,這樣對64K引用限制頗有幫助的。ProGuard 也會優化字節碼,把類、字段、屬性和方法用短的名稱表示-混淆(官方文檔上也說了還有刪除未使用的代碼指令,這個有時間再研究下了)。
資源壓縮:(Resource shrinking) 配合 Gradle 使用,能夠安全移除沒有使用的打包時應用程序沒有使用到的資源,包括代碼庫中的資源。css

1、代碼混淆壓縮配置

那麼混淆 (ProGuard) 改怎麼用?html

android {
  buildTypes {
  release {
            minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } } 

上面這段代碼很眼熟啊,就是項目 build.gradle 文件裏的一段代碼,不過項目建立時默認 minifyEnabled 的值是 false,須要開發者手動改成 true,或者點擊這樣:java

選中項目例如app-按F4-進入 Project Structure -右邊選擇 Build Type -選擇 release -把Minify Enable 設置爲truelinux

看下兩個分別打包出來的效果:本身感覺下,能夠看到先後apk大小和代碼的變化android


空項目混淆先後不比較(左爲混淆)

若是留意 gradle 控制檯的換,會有:proguard:xxxx的信息。代碼就這麼被混淆和壓縮了,原本不少有意義的單詞都被一些字母代替了。數組

每次build完以後,ProGuard 都會輸出一些文件在項目模塊 /build/outputs/mapping/release/下:安全

  • dump.txt :描述 APK 中全部類文件的內部結構
  • mapping.txt :提供原始和混淆後類、方法、字段名的映射對照
  • seeds.txt :列出未混淆的類和成員
  • usage.txt :列出從 APK 中移除的代碼

關於mapping.txt使用的場景也是比較多的,好比使用bugly等平臺可能須要上傳mapping或者用Android Studio查看 apk 的 classes.dex 時,有須要的話可使用Load ProGuard mappings...加載mapping.txt,這樣能夠看到混淆前的代碼樣子bash

注意:若是在debug模式下設置minifyEnabled true,並且須要使用Instant Run增量構建的時候,ProGuard只會刪除不使用的代碼,不會混淆代碼。不過這個狀況也是在debug下使用的吧,通常在debug模式下不開啓混淆的,由於項目build的時間就很長了,再開個混淆那就更長了。若是你真的要這麼debug的話:用useProguard false 便可。這樣代碼也會被混淆。
這個需求我暫時沒有遇到過,只是在debug下有開啓過混淆,可是沒有用Instant Run
代碼以下:

android {
    buildTypes {
        debug {
            minifyEnabled true useProguard false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } ... } 

2、移除沒有被使用的資源

android {
    ...
    buildTypes {
        release {
            shrinkResources true minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } } 

加了一句shrinkResources true。但是在用 Android studio 打開 apk 的時候你仍是發現有那個文件的存在,不過是以前的那個文件被替換成了 1x1(67B) 大小的圖片了。

3、自定義代碼保留規則

並非全部代碼都須要被混淆的,通常默認的 ProGuardFile <sdk>\tools\proguard\proguard-android.txt下已經默認作好了一些設置,哪些些代碼不能被混淆的,可是在實際開發中可能還不夠,還須要開發者自定義一些混淆規則。在這裏我有個小建議:若是須要添加第三方庫到你的 app,那麼同時順便也找下這個庫的混淆規則,並把它加入到你的項目裏。混淆規則通常遵循如下幾點:

  • AndroidManifest.xml 中使用到的類,好比四大組件
  • JNI 調用的方法
  • 運行時操做的代碼,好比反射
  • 枚舉....
自定義混淆的方法:

1. 使用註解 @Keep
如下代碼的效果是類和成員都會保留。只想保留成員能夠在成員上使用@keep

@Keep public class User { private String userName; public String getUserName() { return userName; } } 

哪些類、方法、屬性、字段須要保留的,不被混淆的,用@Keep能夠了,就是這麼簡單,這個幾乎沒怎麼用。

2. 在 proguard-rules.pro 中定義
這個文件在項目的根目錄下。
這個是用得比 @Keep 用都多,好比添加第三庫的時候,通常會提供相應的混淆規則,這些規則須要加入到混淆文件中。

4、經常使用命令說明

  1. -keep : 保留指定的類和成員(字段、方法)。
  2. -keepclassmembers:保留指定的類成員,若是成員所在的類也保留下來的話。也就是說類沒有保留,那麼成員也不存在了或者說這個命令不能影響類。
  3. -keepclasseswithmembers:保留指定的類和成員,前提條件是所指定類中要有的指定的類成員。好比: User 類中沒有 name,可是你指定保留 name,那麼這個命令是不生效的。
  4. -keepnames-keep,allowshrinking 的簡寫,指定保留指定類和成員,若是代碼壓縮階段類或成員沒有被刪除。
  5. -keepclassmembernames-keepclassmembers,allowshrinking 的簡寫,保留指定的成員,若是代碼壓縮階段成員沒有被刪除。
  6. -keepclasseswithmembernames-keepclasseswithmembers,allowshrinking 的簡寫,保留指定類和成員,若是代碼壓縮後壓縮沒有被刪除。好比:User 類中沒有 name,可是你指定保留 name,那麼這個命令是不生效的(User 該混淆混淆)。
  7. -dontwarn:指定不警告未解決的引用和其餘重要問題。
  8. -keepattributes-keepattributes SourceFile,LineNumberTable-renamesourcefileattribute SourceFile 配合使用。分別是 保存用於調試堆棧跟蹤的行號信息 和 隱藏原始的源文件名。在項目的 ProGuard 文件取消註釋就能夠用了。-keepattributes其餘屬性

說明:allowshrinking 表示容許被刪除。相似的修飾符還有 其餘的

保留項 不會被移除(壓縮)、混淆(重命名) 會被移除(壓縮)、混淆(重命名)
類、成員 -keep -keepnames
成員 -keepclassmembers -keepclassmembernames
類、成員(若是有指定成員) -keepclasseswithmembers -keepclasseswithmembernames

使用規則

[命令] [類規範]{
      [成員];
}

注意:須要不須要保留成員,能夠不用{[成員];}

  1. 【類規範】:限定條件,經過限定條件定位到符合條件的類。寫起了和java語法差很少,不過能夠帶有通配符
[註解] [修飾符] 類定義符 類名 [extends | implements 類名] 

中括號裏的是可選項,分割線|提供選擇,具體以下:

  • 註解:@xxx
  • 修飾符:public | private | final | abstract | ... ,前加 ! 表示非xxx
  • 類定義符:interface | class | enuminterface | enum 前加 ! 表示非interface | enum ,註解用class表示
  • 類名:全類名
  • extends | implements 類名:限定前面的類繼承或實現某個類
  • $:內部類
  • 通配符:? - 任何單個字符;* - 不包括包分隔符的其餘任何部分;** - 任何部分
  1. 【成員】:類成員的限定條件,經過限定條件匹配到符合條件的成員。寫起了也是和java差很少,能夠用通配符,每句結束有;
{
    [@annotationtype] [[!]public|private|protected|static|volatile|transient ...] <fields> | (fieldtype fieldname); [@annotationtype] [[!]public|private|protected|static|synchronized|native|abstract|strictfp ...] <methods> | <init>(argumenttype,...) | classname(argumenttype,...) | (returntype methodname(argumenttype,...)); [@annotationtype] [[!]public|private|protected|static ... ] *; ... } 簡單的就是: { [註解] [修飾符] <fields> | 類型 字段名; [註解] [修飾符] <methods> | 方法 | <init>(參數類型,...) | classname(參數類型,...) | 返回值類型 方法名(參數,...); [註解] [修飾符] *; ... } 
  • 註解:@xxx
  • 修飾符:成員修飾符,前面加!表示非xxx
  • *:任何方法或字段
  • <init>:任何構造器,構造器也能夠用類名或者全類名來指定
  • <fields>:任何字段
  • <methods>:任何方法
  • 字段名:字段名,能夠和通配符使用
  • 方法名:方法名,能夠和通配符使用
  • 類型:全類名,基本數據類型除外
  • 參數類型:全類名,基本數據類型除外
  • 返回值類型:全類名,基本數據類型除外
  • 通配符:%-任何基本數據類型;?-任何單個字符;*-不包括包分隔符的其餘任何部分;**-任何部分;***-任何類型(數組等);...-任意長度任意類型參數

5、使用舉例

  • 忽略某個內容的警告信息
-dontwarn javax.annotation.** 
  • 不混淆某個類及成員
-keep public class com.xin.proguard.nonuse.Keep{ *; } 
  • 不混淆某個包下的全部類及成員
-keep public class com.xin.proguard.nonuse.**{ *; } 
  • 不混淆有類名有Bean的類及其成員
-keep class **Bean**{ *; } 
  • 不混淆某個接口的實現
-keep class * implements com.bumptech.glide.module.GlideModule 
  • 不混淆某個類的子類
-keep class * extends com.bumptech.glide.module.AppGlideModule 
  • 不混淆某個成員
-keepclassmembers class android.support.design.internal.BottomNavigationMenuView{ private boolean mShiftingMode; } 
  • 不混淆某個類的指定方法
-keepclassmembers class com.xin.proguard.nonuse.User{ public void setUserName(java.lang.String); } 
  • 移除Log打印
    先確認 build.gradle 配置以下,緣由是配置 -dontoptimize 狀況下沒法移除log打印。
buildTypes {
        release {
            shrinkResources true minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } 
-assumenosideeffects class android.util.Log{ public static *** v(...); public static *** i(...); public static *** d(...); public static *** w(...); public static *** e(...); } 

大膽的建議:若是很明確某個包或者某個類改怎麼處理的話,用-keepnames-keep好。

6、自定義資源保留規則

使用前請肯定在項目 build.gradle 中配置了 shrinkResources true。自定義資源保留規則須要在項目的 xml 文件中定義:根節點 <resources>,須要保留資源用 tools:keep,須要移除資源用 tools:discard;兩個資源之間能夠用 , 分隔、* 能夠用做通配符。

可能有人會問:若是我把同一個資源同時做爲 tools:keeptools:discard 的值,這咋整?告訴你,這個資源會被移除。固然,誰會這麼傻逼作這樣的事呢,哈哈哈。不要問我是怎麼知道的。說多都是淚,我就是那個傻逼

嚴格模式tools:shrinkMode="strict"tools:shrinkMode 默認值是safe,默認狀況若是在代碼中使用Resources.getIdentifier(),混淆器會把全部匹配的資源都標記爲已使用的,且不可刪除。

好比:下面的代碼會致使全部以img_爲前綴的資源都會被標記爲「已使用」

String name = String.format("img_%1d", angle + 1); res = getResources().getIdentifier(name, "drawable", getPackageName()); 

例如:在res/raw/下建立keep.xmlkeep.xml內容以下
在res/其它文件夾下建立其餘名稱的xml文件也行

<?xml version="1.0" encoding="utf-8"?> <resources xmlns:tools="http://schemas.android.com/tools" tools:keep="@layout/l_used*_c,@layout/l_used_a,@layout/l_used_b*" tools:discard="@layout/unused2,@layout/unused3" /> 

若是開啓了嚴格模式,並且也在代碼中動態獲取資源,那麼就要使用tools:keep屬性保留指定的資源。不然使用代碼獲取的資源不能正常使用。

項目構建的時候不會把 keep.xml 打包到 apk 裏。不是 apk 裏沒有 keep.xml 文件,而是
keep.xml 裏只剩個空殼。

刪除可替代資源
資源壓縮能夠刪除未使用的代碼和資源,一樣也能夠刪除一些可替代的資源。好比國際化中一些語言資源string.xmlvalues-xx,佈局資源layout-xx等。當你不須要所有語言時,根據實際須要把指定的語言打包到apk裏,那麼資源壓縮器就能夠幫到你了。你能夠在項目的gradle文件配置resConfig(resConfigs)屬性,只保留顯示聲明的,未聲明的將被刪除。

例如如下代碼展現只是用語言資源爲中文:

android {
    defaultConfig {
        ...
        resConfig "zh" } } 

若是是多個資源那就把 resConfig 改成 resConfigs 多了個 s 便可,固然,只聲明一個資源時也能夠用 resConfigs 。例如,只保留英語、法語、豎屏佈局資源:

android {
    defaultConfig {
        ...
        resConfigs "en", "fr", "port" } } 

屏幕密度資源配置請參考這裏configure-split

關於 Android 混淆和資源壓縮就到此結束了,好久沒寫東西了,寫得不要請見諒,有不妥之處歡迎指出。感謝閱讀。

(End)

參考連接

相關文章
相關標籤/搜索