使用 R8 壓縮您的應用

做者 / Google 軟件工程師 SørenGjesse 和 Christoffer Adamsenjava

人們更傾向於安裝並保留較小和安裝佔用空間更小的應用,在新興市場中尤其明顯。有了 R8 編譯器,您能夠經過壓縮、混淆和優化,更全面的縮小應用體積。android

本文咱們將對 R8 的特性進行一個簡要的介紹,並介紹可預期的代碼縮減程度以及如何在 R8 中啓用這些功能。ios

R8 的壓縮特性

R8 經過下面 4 項特性來減小 Android 應用大小:git

  • 搖樹優化 (Tree shaking): 使用靜態代碼分析來查找和刪除沒法訪問的代碼和未實例化的類型;
  • 優化 : 經過刪除無效代碼,選擇性內聯,移除未使用的參數和類合併來優化代碼大小;
  • 重命名標識,即混淆處理 : 使用短名稱以及縮短包命名空間;
  • 減小調試信息 : 規範化調試信息並壓縮行號信息。

爲何須要 R8 壓縮

開發應用時,全部代碼都應有目的並在應用中實現相應功能。不過,大多數應用都會使用 JetpackOkHttpGuavaGsonGoogle Play 服務 等第三方庫,而且用 Kotlin 編寫的應用始終包含 Kotlin 標準庫。當您使用這其中的某個第三方庫時,您的應用中一般只使用其中很小一部分。若不壓縮,全部庫代碼都會保留在您的應用中。github

您的代碼大小也可能比實際須要的大,由於冗長的代碼有時能夠提升可讀性和可維護性: 例如,您可能會盡可能使用有意義的變量名和建造者模式 (builder pattern) 來幫助其餘人更容易檢查和理解您的代碼。可是這些模式會加大代碼量。一般,您本身編寫的代碼有很大的壓縮空間。算法

啓用 R8 來壓縮您的應用

要在 release build 上啓用 R8 壓縮,須要在應用的主 build.gradle 文件中將 minifyEnable 屬性設置爲 true,以下所示:編程

android {
  buildTypes {
    release {
      minifyEnabled true
    }
  }
}

別被 minifyEnable 這個名字所迷惑,它會啓用 R8 的代碼縮減功能。api

R8 能縮減多少應用大小?

R8 能夠大大減少應用的大小。例如,去年的 Google I/O 應用大小爲 18.55 MB,壓縮前包含 150,220 個方法和 3 個 DEX 文件。壓縮後,應用大小縮小到 6.45 MB,包含 45,831 個方法和 1 個 DEX 文件。R8 縮減了 65% 的 DEX 文件大小 (測量數據來自 Android Studio 3.5.1 和 IOSched 示例應用)。app

基本壓縮算法

爲簡單起見,咱們寫了一個基於 Java 編程語言的程序做爲參考:jvm

class com.example.JavaHelloWorld {
  private void unused() {
    System.out.println("Unused");
  }

  private static void greeting() {
    System.out.println("Hello, world!");
  }

  public static void main(String[] args) {
    greeting();
  }
}

程序的入口是 static void main 方法,咱們使用如下 keep 規則 指定該方法:

-keep class com.example.JavaHelloWorld {
      public static void main(java.lang.String[]);
}

R8 縮減算法的運做方式以下:

  • 首先,它從程序常見的入口點跟蹤全部可訪問的代碼。這些入口點由 R8 keep 規則定義。例如,在此 Java 代碼示例中,R8 會在 main 方法處開始運行。
  • 在該示例中,R8 從 main 方法跟蹤到 greeting 方法。greeting 方法是在運行時被調用的,所以跟蹤在此處中止。
  • 跟蹤完成後,R8 使用搖樹優化來刪除未使用的代碼。在此示例中,搖樹刪除了未使用的方法,由於 R8 的跟蹤過程檢測到從任何已知的入口都沒法到達該方法。
  • 接下來,R8 將標識重命名爲較短的名稱,這些名稱在 DEX 文件中佔用較少的空間。在示例中,R8 可能會將 greeting 方法重命名爲短名稱 a:
class com.example.JavaHelloWorld {

  private static void a() {
    System.out.println("Hello, world!");
  }

  public static void main(String[] args) {
    a();
  }
}
  • 最後,應用代碼優化。縮減代碼大小的內聯是其一。在此示例中,將方法 a 的主體直接遷移到 main 中,代碼會顯得更簡潔:
class com.example.JavaHelloWorld {

  public static void main(String[] args) {
    System.out.println("Hello, world!");
  }
}

如您所見,處理後的代碼比原始代碼短得多。

使用 R8 壓縮應用前的準備工做

正如獨立的 Java 程序同樣,Android 應用有許多常見的入口點: Activity (活動)Service (服務)Content Provider (內容提供者)Broadcast Receiver (廣播接收者)aapt2 工具經過基於 Android Manifest 文件生成 keep 規則來爲您處理這些入口點。

除了這些熟知的入口點,Android 應用還須要其餘標準的 keep 規則。這些規則由 Android Gradle 插件提供,您能夠在配置構建時指定該默認配置文件:

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

應用代碼中的反射

反射 (Reflection) 會致使 R8 在跟蹤代碼時沒法識別到代碼的入口點。第三方庫也可能用到反射,而且因爲第三方庫其實是您的應用的一部分,您 (做爲應用開發者) 將負責這些庫以及您本身的代碼中使用的反射。第三方庫可能附帶了它們本身的規則,可是切記,有些庫不必定是爲 Android 編寫的,抑或是未考慮縮減問題,所以它們可能須要其餘配置。

以一個 Kotlin 類爲例,該類具備一個名爲 name 的字段和一個 main 方法,該方法建立一個實例並將該實例序列化爲 JSON:

class Person(val name: String)

fun printJson() {
   val gson = Gson()
   val person = Person("Søren Gjesse")
   println(gson.toJson(person))
}

縮減代碼後,運行程序將輸出一個空的 JSON 對象 {}。這是由於 R8 僅將字段名視爲寫入 (在 Person 構造函數中),但從未讀取,所以 R8 會將其移除。最後 Person 丟失了字段值,形成空的 JSON 對象。可是,該字段由 Gson 序列化讀取,而 Gson 使用反射的方式來執行此操做,所以 R8 沒法看到此字段已被讀取。

要保留名稱字段,請在您的 proguard-rules.pro 文件中添加一個保留規則 -keep:

-keep class com.example.myapplication.Person {
    public java.lang.String name;
}

此規則告訴 R8 不要處理 Person 類中的 name 的字段。將其放置在適當位置後,運行代碼便可獲得預期的 JSON 對象 {"name": "SørenGjesse"}

最後,在配置項目時,請確保將 proguard-rules.pro 文件添加到 build.gradle 配置中:

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

瞭解更多

有興趣更深刻了解 R8 壓縮器如何運做嗎?請參考 R8 開發者文檔 瞭解更多!

相關文章
相關標籤/搜索