+9

Giảm tối đa kích thước của một ứng dụng Android khi phát hành với công cụ R8

Tối ưu hóa app size với R8

  • Lời mở đầu: Xin chào các bạn, hôm nay mình sẽ trình bày cách để tối ưu hóa size của một ứng dụng và một số rule keep của proguard file. Trong khi các bạn đã trải qua một quá trình dài phát triển và fixbug sml thì sản phẩm cũng đến giai đoạn phát hành lên CH Play phải không nào. Tuy vậy, rất có thể app của bạn dù trên bản debug chạy rất là nuột nhưng trên bản release thì nó lại không work ??? mặc dù đã test rất kỹ rồi. Trông gai còn phía trước :v.
  • Chắc hẳn trên tư cách là người dùng khi bạn tải một ứng dụng cùng tính năng tương tự thì một ứng dụng có size thấp mình nghĩ sẽ được ngta ưu tiên hơn. Và rồi R8 được ra đời để cải tiến vấn đề trên.

1. Giới thiệu

1.1 R8 là một công cụ chuyển đổi mã byte java của chúng ta thành mã dex được tối ưu hóa. Nó tối ưu hóa như loại bỏ các lớp, phương thức không sử dụng, v.v. Nó chạy trong thời gian compile. Nó giúp chúng ta giảm kích thước của bản build và làm cho ứng dụng của chúng ta an toàn hơn.

1.2 Khi build app với Android Gradle plugin 3.4.0 trở lên, R8 sẽ được mặc định bật, thay thế cho proguard trước đây.

1.3 Khi enable shrinking, những công việc mà R8 tool sẽ work như sau:

  • Code shrinking (tree-shaking): detect và remove unreachable code những class, field, method và attribute không được sử dụng, trong app và library mà app sử dụng. Vd: bạn chỉ sử dụng vài API của một thư viện to đùng, những phần còn lại sẽ được remove.

  • Resource shrinking: detect và remove unused resource trong app và library mà app sử dụng bao gồm cả những resource được tham chiếu đến bởi những đoạn code không được sử dụng (kết hợp với Code shrinking và bắt buộc phải enable Code shrinking để sử dụng được).

  • Obfuscation: Rút ngắn tên của các class và các thành phần của class, từ đó giảm kích thước của file DEX.

  • Optimization: Quét và viết lại những đoạn code chưa tối ưu để tiếp tục giảm kích thước của file DEX. VD: một đoạn câu lệnh if/else có phần else không bao giờ được chạy, tool sẽ loại bỏ đoạn code trong phần else.

    // bật tối ưu hóa bổ sung default: false
    android.enableR8.fullMode=true
    

2. Bật tính năng rút gọn, làm rối mã nguồn và tối ưu hoá

Khi sử dụng Android Studio 3.4 hoặc trình bổ trợ Android cho Gradle 3.4.0 trở lên, R8 là trình biên dịch mặc định, dùng để chuyển đổi mã byte Java của dự án thành định dạng DEX chạy trên nền tảng Android. Tuy nhiên, khi bạn tạo một dự án mới bằng Android Studio, các tính năng rút gọn, làm rối và tối ưu hoá mã sẽ không được bật theo mặc định. Đó là do những tính năng tối ưu hoá tại thời điểm biên dịch này sẽ làm tăng thời gian xây dựng (build time) dự án và có thể gây ra lỗi nếu bạn không tuỳ chỉnh đầy đủ mã cần giữ lại. Vì vậy nên enable nó với bản release, chính vì vậy mới xuất hiện lỗi nó không hđ trên bản release như mình nói ở trên =)).

android {
buildTypes {
    release {
        // enable r8
        minifyEnabled true

        // using proguard rule: default true
        useProguard true

        // 
        shrinkResources true

        // R8 configuration files.
        proguardFiles getDefaultProguardFile(
                'proguard-android-optimize.txt'),
                'proguard-rules.pro'
    }
}

3. Tệp cấu hình R8

R8 cũng sử dụng các file chứa rules của ProGuard làm căn cứ để thực hiện các chứng năng shrinking, obfuscation hay optimization. Bởi vậy, khi migrate từ ProGuard lên R8, chúng ta gần như không cần phải sửa đổi gì nhiều để app chạy nuột như bình thường. Dưới đây là những file rules của ProGuard mà R8 sẽ sử dụng:

Source Position Description
Android Studio <module-dir>/proguard-rules.pro Mặc định được generate khi tạo project
Android Gradle plugin Compile time Trình bổ trợ Android cho Gradle sẽ tạo ra proguard-android-optimize.txt, trong đó chứa các quy tắc hữu ích cho hầu hết các dự án Android và cho phép tạo các chú thích @Keep*.
Library dependencies // to do // todo
Android Asset Package Tool 2 (AAPT2) // to do // todo
Custom configuration files // to do // todo

Để xuất ra báo cáo đầy đủ về tất cả quy tắc mà R8 đang áp dụng khi xây dựng dự án, hãy đưa nội dung sau vào tệp proguard-rules.pro trong mô-đun của bạn:

// You can specify any path and filename.
-printconfiguration ~/tmp/full-r8-config.txt

4. Tối ưu hóa code

Code shrinking là quá trình phân tích và loại bỏ những đoạn code sẽ không được chạy lúc run-time. Từ đó, nó sẽ làm giảm đáng kể size của file apk. Image1

Từ hình ảnh trên có thể phân tích quá trình shrinking như sau: R8 xác định tất cả các entry point của app + các file config ở trên. Những entry point này bao gồm tất cả các đoạn code mà Android chạy để mở một Activity hoặc Service của app. Từ mỗi entry point, R8 sẽ cho ra một graph về tất cả các method, member variable,... Tất cả các class mà app sẽ access đến lúc run-time. Những thành phần không được đưa vào graph sẽ là unreachable code và sẽ được loại bỏ. VD trên entry point là MainActivity.class, các func foo(), faz(), AwesomeApi.class, bar() là được truy cập tại thời ddieeerm runtime còn baz() là unreachable code và sẽ được loại bỏ sau quá trình Code shrinking.

5. Lưu ý quan trọng để app có thể work ngon lành với rule Keep được quy định.

Rule chia thành 2 thành phần:

  • [x] Phần đầu xác định "cách keep".

  • [x] Phần sau là định danh của class hoặc member class cần được keep.

  • Mục đích của R8 là giảm size của file APK đến mức thấp nhất có thể. Bởi vậy, chúng ta cần chọn chính xác phiên bản -keep để không làm giảm hiệu quả của R8. Những gạch đầu dòng dưới đây sẽ thể hiện quá trình nào sẽ được chạy (✔ là có và ✘ là không)

5.1)

No rules: R8 sẽ thực hiện cả 2 quá trình shrink và obfuscate với cả class hoặc member class.

5.2)

keep: R8 sẽ không thực hiện cả 2 quá trình shrink và obfuscate với class hoặc member class.

5.3)

keepclassmembers: R8 sẽ chỉ thực hiện 2 quá trình shrink và obfuscate với class. Tức là nếu class không được sử dụng, class sẽ bị remove. Nếu class được sử dụng, class sẽ được giữ lại và bị đổi tên. Tuy nhiên, các thành phần bên trong sẽ được giữ nguyên (không remove hay đổi tên gì cả)

5.4)

keepnames: R8 sẽ chỉ thực hiện quá trình shrink với class hoặc member class. Nếu class hoặc member class được sử dụng, tên của chúng sẽ không bị thay đổi.

5.5)

keepclassmembernames: R8 thực hiện quá trình shrink với cả class và member nhưng chỉ obfuscate với class. Tức là nếu class hoặc member class không được sử dụng chúng sẽ bị loại bỏ, nếu được sử dụng, chỉ class bị đổi tên, member class sẽ được giữ nguyên tên.

keepclasseswithmembers: Cái này không có bảng so sánh bởi vì nó giống như -keep. Sự khác biệt là nó chỉ áp dụng cho các class có tất cả các thành viên trong đặc tả class. VD: nó sẽ giúp các hàm tạo có tham số truyền vào như ở dưới.

-keepclasseswithmembers class * { 
public <init>(android.content.Context, android.util.AttributeSet); 
} 

-keepclasseswithmembers class * { 
public <init>(android.content.Context, android.util.AttributeSet, int); 
} 

keepclasseswithmembernames: tương tự như thằng trên nhưng nó giống với -keepname. VD:

-keepclasseswithmembernames class * {
   public <init>(android.content.Context, android.util.AttributeSet);
}

-keepclasseswithmembernames class * {
   public <init>(android.content.Context, android.util.AttributeSet, int);
}

6. Resource Shrinking:

Để có thể enabled Resource shrinking, ta bắt buộc phải enable minifyEnabled bởi chỉ khi remove được unreachable code, những unused resource tương ứng mới xuất hiện hoàn toàn. Để enable Resource shrinking, ta thêm đoạn sau vào file build.gradle của project:

  • Xác định những resource cần được keep:

Nếu muốn giữ lại hoặc loại bỏ tài nguyên nào đó, bạn hãy tạo tệp XML trong dự án bằng một thẻ <resources>, sau đó chỉ định tài nguyên nào cần giữ lại trong thuộc tính tools:keep(giữ lại) và tài nguyên cần loại bỏ trong thuộc tính tools:discard(loại bỏ). Cả hai thuộc tính này đều chấp nhận danh sách tên tài nguyên được phân tách nhau bằng dấu phẩy. Bạn có thể sử dụng ký tự dấu hoa thị làm ký tự đại diện.

Ví dụ:

<?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" />

Sở dĩ ta cần xác định những file cần keep có thể là bởi app chỉ tham chiếu động đến resource đó:

val idString = "icon_" + (LOADING_START_FRAME + i)
val id = resources.getIdentifier(idString, "drawable", getContext().getPackageName())
val drawable = resources.getDrawable(id, null)

Với tools:discard, tại sao lại không chủ động xóa file resource đó đi mà lại phải phụ thuộc vào R8? Thực chất, tools:discard hiệu quả khi app sử dụng build variant, tùy vào flavor mà resource nào sẽ được xóa đi để giảm kích thước của file apk đến mức tối đa.

7. Remove unused alternative resource

Quá trình Resource shrinking chỉ loại bỏ những file resource mà code không reference đến. Điều đó có nghĩa là những file resource của các config khác nhau sẽ không bị remove. Để xác định file resource của config nào cần được remove. Ta làm như sau:

android {
    defaultConfig {
        ...
        resConfig "en", "fr"
    }
}

Với đoạn code ở trên, quá trình Resource shrinking sẽ loại bỏ những file resource không phải ngôn ngữ Anh hoặc Pháp. Việc này thích hợp khi bạn build nhiều file APK tương ứng với nhiều config device khác nhau.

8. Obfuscate code

Mục đích của quá trình obfuscation là làm giảm size app bằng việc cắt ngắn tên các class, method và field của app. Vì thay đổi tên của các thành phần, developer có thể gặp một số trở ngại sau khi obfuscate code như khó khăn khi trace code, xác định crash... Ngoài ra, với những class được access thông qua reflection, bạn nên keep lại những file đó để app không crash vì không tìm thấy các thành phần tương ứng.

9. Decode obfuscated stack trace

Sau khi code được obfuscate, việc đọc code cũng như trace code là cực kỳ khó khăn. Bởi vậy, R8 sẽ tạo một file mapping.txt với mỗi lần build. File này chứa thông tin của các file được obfuscate như tên class, method, field hay số thứ tự dòng mà R8 đã thay đổi. Từ file này, ta có thể map từ file obfuscated thành file gốc để dễ hiểu, dễ đọc hơn. File mapping.txt này được lưu ở đường dẫn <module-name>/build/outputs/<build-type>/.

Lưu ý: Vì file mapping.txt sẽ được ghi đè mỗi lần build project, bạn nên lưu file này mỗi lần release để có thể trace lại code dù ở bất kỳ version nào của app.

10. Tài liệu tham khảo

11. Tổng kết

Trên đây là bài trình bày về R8 để tối ưu hóa size cho app và một số note keep rule quan trọng config file proguard để release thành công ứng dụng. Cảm ơn các bạn đã đọc bài viết. Xin chào và hẹn gặp lại.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí