Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android Proguard工具使用和配置詳解

Android Proguard工具使用和配置詳解

編輯:關於Android編程

Android開發中的Proguard

Proguard是Android開發時經常會用到的一個工具,在Android SDK中已經集成了一個免費的Proguard版本,位於/tools/proguard目錄中。

在Android Studio開發的Android項目中,通過修改module下面的build.gradle文件來開啟使用Proguard選項,當開啟了此選項後,Android Studio在編譯該module時就會使用指定的配置來對編譯之後的Java字節碼進行處理,得到一個優化後的jar包。

在最新的Android Studio 2.1.2版本創建的Android工程中,module中的build.gradle有如下一段配置。這裡的minifyEnabled即用來控制在編譯時是否需要啟用Proguard,將minifyEnabled修改為true,即表示啟用Proguard。proguardFiles配置的是Proguard對此module進行處理時使用的配置文件,’proguard-android.txt’是Android SDK中自帶的一個基本Progurad配置文件,它同樣位於/tools/proguard目錄中,’proguard-rules.pro’則是當前module所在目錄中的一個配置文件,默認是空白的,需要由開發者自行實現,當啟用了Proguard之後需要編輯這個文件,向其中添加適合當前項目的Proguard配置。

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

除了’proguard-android.txt’,Android SDK中還自帶了另外一個Pruguard配置文件’proguard-android-optimize.txt’,它同樣位於/tools/proguard目錄中,和’proguard-android.txt’ 的區別在於,’proguard-android-optimize.txt’中開啟了Proguard optimize的選項(optimize是Proguard的一項功能),而’proguard-android.txt’ 中沒有開啟optimize選項。如果需要開啟optimize,可以將這裡的’proguard-android.txt’修改為’proguard-android-optimize.txt’。當然,如果熟悉Proguard的配置,也可以直接編輯module中的’proguard-rules.pro’,向其中添加optimize的配置。

這裡有幾點需要注意的地方:

在較早的Android Studio 版本中,這裡的一些選項名字可能會有所不同,可能會看到runProguard這樣的配置。這是因為Android Plugin for Gradle 在0.14.0版本中修改了DSL中的幾個名字。雖然在實際項目中不會再使用這麼老的版本了,但是網絡上很多文章仍然是在老版本基礎上寫的。

Proguard雖然被集成到了Android SDK中,但是Proguard並非是Google開發的,他最早是個人開發者Eric Lafortune在業余時間做的一個開源項目,後來Eric Lafortune加入了GuardSquare(一家總部在比利時的公司)擔任CTO,Proguard也被GuardSquare當做公司的產品來宣傳,不過仍然是開源且免費的。 GuardSquare還有另外一款基於Proguard的產品Dexguard,這款產品是收費的,當然功能上也比Proguard要強大。從名字上就可以看出這款產品是專門針對Android APK優化的。

在Android Plugin for Gradle 2.0版本中集成了一個實驗性質的工具,可以用來對代碼進行shrinker,也就是可以去掉代碼中沒有用到的那些變量,方法和類。目前這個工具仍然是experimental,由於沒有混淆和優化的功能,和Proguard相比沒有任何優勢。也許Google會繼續開發,未來可能會像Android Studio取代Eclipse + ADT 一樣,取代掉Proguard,當然也可能會廢棄掉。

Proguard功能介紹

Proguard經常被看做Android平台的代碼混淆工具,這種看法是比較片面的。Proguard項目誕生於2002年,而Android 1.0是2008年才發布的,也就是說早在Android發布之前Proguard就已經存在了。Proguard不僅適用於Android項目,也適合對其他使用Java開發項目的優化。此外,Proguard也不僅僅是一個代碼混淆工具,代碼混淆只是Proguard四項功能中的其中一項。它之所以被認為是Android平台的代碼混淆工具,是因為Google將其集成到了Android SDK和Android項目的編譯過程中,成為Android開發默認的代碼優化工具,從而被廣大的Android開發者所熟悉。此外,開發者對代碼混淆功能的需求比其他功能要迫切的多,Proguard代碼混淆功能成為開發者必選的一項功能。

Proguard幫助文檔中是這樣描述的

ProGuard is a Java class file shrinker, optimizer, obfuscator, and preverifier.

從這段話中可以看出,Proguard可以對Java class文件執行shrink,optimize,obfuscate和preverify四項優化。這四項優化也代表了Proguard的四項功能。

shrink

shrink功能的作用是移除代碼中沒有使用到的類,方法和成員變量,從而減少文件大小。

這些沒有使用到的類,方法和成員變量的產生主要有兩種情況,一種是自身代碼中由於功能,需求的變更,或者代碼的重構,導致原先的一些代碼不再被使用,這些代碼有時會忘記刪除,有些則是故意保留下來,以備以後使用,這些被廢棄或故意存留的代碼對程序的運行沒有任何用處。另一種是項目中經常會包含一些開源代碼或第三方SDK,而項目通常只會使用到開源代碼或第三方SDK中的部分功能,那些沒有用到的功能對應的代碼同樣是可以去掉的。這些沒有用處的代碼,如果靠開發者手動刪除是很費事費力的,尤其是第三方的代碼,而且手動從項目中刪除是有副作用的,如果後面又需要用到第三方SDK中的某項功能,又得費力加回來。

Proguard的shrink功能會自動分析jar包中各個類,方法之類的調用和依賴關系,對那些沒有用到的類,方法和成員變量進行剔除。

optimize

optimize過程會在Java字節碼層面上進行優化,剔除方法中一些冗余的調用。幫助文檔中列出了一些當前Proguard支持的優化措施。

對常量表達式進行計算
刪除不必要的字段訪問和方法調用
刪除不必要的代碼分支
刪除不必要的比較和instanceof測試
刪除未使用的代碼塊
合並相同的代碼塊
減少變量的分配
刪除只賦值但沒有使用的成員變量,以及未使用的方法參數
內聯常量字段,方法參數和返回值
內聯很短的方法和只被調用一次的方法
簡化尾遞歸調用
合並類和接口
盡可能的將方法修飾為private,static和final
盡可能的將類修飾為static和final
移除只被一個類實現的接口
其他200多項細節優化,比如用 <<1 代替 *2 運算
刪除Log的相關代碼(可選)

obfuscator

obfuscator也就是使用Proguard最常被提到的混淆功能。由於Java代碼編譯之後的jar文件中仍然包含了調試信息,源文件名,行號,類名,方法名,成員變量名,參數名,局部變量名等信息,通過反編譯可以很容易的將這些信息還原出來。通過obfuscator,可以將jar包中不需要對外暴露的類名、方法名和變量名替換成一些簡短的,對人來說沒有意義的名字。

以下是一段代碼混淆前和混淆後的對比。可以看到類名,成員變量名,方法名,參數名和局部變量名都被替換成了一些簡單無意義的名字,從混淆後的代碼中就很難理解原先代碼的邏輯。然而這兩段代碼對計算機來說是完全等價的。

此外,由於混淆後jar包中原先很長的名字被替換成了簡短的名字,這使得jar包的體積更小了。這也是混淆帶來的另一個附加的好處。

public enum Edge {
    private float mCoordinate;

    public static float getWidth() {
        return Edge.RIGHT.getCoordinate() - Edge.LEFT.getCoordinate();
    }

    public static float getHeight() {
        return Edge.BOTTOM.getCoordinate() - Edge.TOP.getCoordinate();
    }

    private static float adjustLeft(float x, Rect imageRect, float imageSnapRadius, float aspectRatio) {

        float resultX = x;

        if (x - imageRect.left < imageSnapRadius)
            resultX = imageRect.left;

        else {
            float resultXHoriz = Float.POSITIVE_INFINITY;
            float resultXVert = Float.POSITIVE_INFINITY;

            if (x >= Edge.RIGHT.getCoordinate() - MIN_CROP_LENGTH_PX)
                resultXHoriz = Edge.RIGHT.getCoordinate() - MIN_CROP_LENGTH_PX;

            if (((Edge.RIGHT.getCoordinate() - x) / aspectRatio) <= MIN_CROP_LENGTH_PX)
                resultXVert = Edge.RIGHT.getCoordinate() - (MIN_CROP_LENGTH_PX * aspectRatio);

            resultX = Math.min(resultX, Math.min(resultXHoriz, resultXVert));
        }
        return resultX;
    }
}
public enum a
{
  private float e;

  public static float a()
  {
    return c.c() - a.c();
  }

  public static float b()
  {
    return d.c() - b.c();
  }

  private static float a(float paramFloat1, Rect paramRect, float paramFloat2, float paramFloat3)
  {
    float f1 = paramFloat1;
    if (paramFloat1 - paramRect.left < paramFloat2)
    {
      f1 = paramRect.left;
    }
    else
    {
      float f2 = Float.POSITIVE_INFINITY;
      float f3 = Float.POSITIVE_INFINITY;
      if (paramFloat1 >= c.c() - 40.0F) {
        f2 = c.c() - 40.0F;
      }
      if ((c.c() - paramFloat1) / paramFloat3 <= 40.0F) {
        f3 = c.c() - 40.0F * paramFloat3;
      }
      f1 = Math.min(f1, Math.min(f2, f3));
    }
    return f1;
  }

preverifier

preverifier用來對Java class進行預驗證。預驗證主要是針對JME開發來說的,Android中沒有預驗證過程,所以不需要這項優化。

Proguard工作過程

在Proguard幫助文檔中給出了一個Proguard工作流程圖

這裡寫圖片描述

可以看到,Proguard會對輸入的jar文件按照shrink - optimize - obfuscate - perverify的順序依次進行處理,最後得到輸出jar文件。Proguard使用library jars來輔助對input jars類之間的依賴關系進行解析, library jars自身不會被處理,也不會被包含到output jars中。

Entry points(進入點)

設想我們手動對一個jar文件進行shrink處理,為了決定這個jar文件中哪些類和方法應該被保留,我們需要分析jar文件中方法之間的調用過程,得到一張方法之間的依賴關系圖。然而這時仍然不知道哪些類和方法需要保留,不能因為一個類的某個方法被另外一個類的某個方法調用了,就認為這兩個類和這兩個方法就應該被保留,它們可能都沒有被其他代碼所調用。仔細分析這個過程,由於jar文件是一個孤立的個體,無論其內部有怎樣復雜的調用和依賴關系,如果我們不指定一個搜索的起點的話,那麼整個jar包中所有的類和方法都是可以被shrink的。這個搜索的起點就是jar文件對外提供的一個入口。這個入口可能是Java中的main方法,可能是Android中的四大組件,可能是一個SDK對外提供的APIs…總之,要執行shrink之前必須先指定一個或若干個入口。

在Proguard中,將jar文件的入口稱為Entry points。在Proguard的四項功能中,只有preverifier不需要用到Entry points,其他三項功能都必須在配置中指定Entry points後才可以執行。

Proguard在執行shrink時會將Entry points作為起點,遞歸的搜索整個jar文件中類和方法之間的調用關系圖,只有通過Entry points直接或間接調用的那些方法和類才會被保留,其他的類和方法都將被刪除。

在optimize過程中,沒有被指定為Entry points的類和方法可能會被修飾為private, static以及final,方法中沒有用到的參數可能會被移除,有些方法還可能會被內聯(整個方法被刪除,方法的代碼直接拷貝到調用處,替代原先的方法調用)。這些優化的目的是為了提高執行的效率,並附帶的減少一些包體大小。但如果jar包一個類和方法需要被外部使用,則顯然不能執行這類優化,否則外部將不能通過原先約定的方式來使用這些類和方法。所以,Proguard對指定Entry points的類和方法不會執行這些優化。

在obfuscator過程中,如果一個類和方法沒有被指定為Entry points,則這個類和方法的名字將會被重命名為無意義的名字。同樣,如果jar包一個類和方法需要被外部使用,則顯然不能執行這類修改,否則外部將不能通過原先約定的名字來使用這些類和方法。所以,Proguard對指定Entry points的類和方法不會執行混淆操作。

Entry points和反射

在jar文件內部,可能會有一部分類和方法是通過Java反射方式來調用的,Proguard在分析jar包中類和方法之間的調用關系時,會考慮到反射方式的調用。如下反射方式調用的類和方法能夠被Proguard正確的分析,其中”SomeClass”,”someField”,”someMethod”指的是某個編譯時的字符串常量,SomeClass是某個明確的類型。

Class.forName("SomeClass")
SomeClass.class
SomeClass.class.getField("someField")
SomeClass.class.getDeclaredField("someField")
SomeClass.class.getMethod("someMethod", new Class[] {})
SomeClass.class.getMethod("someMethod", new Class[] { A.class })
SomeClass.class.getMethod("someMethod", new Class[] { A.class, B.class })
SomeClass.class.getDeclaredMethod("someMethod", new Class[] {})
SomeClass.class.getDeclaredMethod("someMethod", new Class[] { A.class })
SomeClass.class.getDeclaredMethod("someMethod", new Class[] { A.class, B.class })
AtomicIntegerFieldUpdater.newUpdater(SomeClass.class, "someField")
AtomicLongFieldUpdater.newUpdater(SomeClass.class, "someField")
AtomicReferenceFieldUpdater.newUpdater(SomeClass.class, SomeType.class, "someField")

例如,在某段代碼中執行了Class.forName(“MyClass”),說明這裡需要引用MyClass類,因此MyClass類不能被shrink(假設這段代碼所在的方法本身不會被shrink),同時在執行obfuscate時,如果MyClass的類名可以被混淆(沒有指定為Entry points),則在重命名MyClass類名的同時會修改Class.forName()參數中的字符串。

然而不幸的是,很多時候Proguard無法准確的判斷出所有通過反射方式來調用的類和方法,這主要是那些在運行時才能確定的名字的反射調用。例如,在某段代碼中執行了Class.forName(getPackageName()+ “.R”),Proguard在分析該段代碼時,並不知道這裡的getPackageName()+ “.R”指的是哪個類,所以也就無從知道是否要保持該類不被shrink和obfuscate。在這種情況下,雖然這個類並不會被jar包外部所使用,也需要將該類指定為Entry points。不然的話,如果這個類沒有被代碼中其他地方使用,那麼它會在shrink時被刪除,即使沒有被刪除,它也會在obfuscate時被重命名為其他的名字,這樣當代碼執行到Class.forName(getPackageName()+ “.R”)時還是無法找到這個類。

所以,通常需要將代碼中那些通過反射方式調用,又不能自動推斷出調用關系的類和方法手動添加到Entry points的配置中。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved