Shrink, obfuscate, and optimize your app - Part 1

Shrink, obfuscate, và optimize trong Android - Part 1

Để tạo ra một app với yêu cầu lưu trữ nhỏ nhất có thể, được bảo vệ bằng cách làm tối nghĩa các fuction hoặc property để tránh việc dịch ngược bằng các tool decompile, loại bỏ các thành phần unused trong code và resources, chúng ta cần enable shrinking . Khi shrinking đã được enable nó đồng thời kích hoạt obfuscation, obfuscation sẽ giúp rút gọn tên của các classes, menbers và thực hiện optimization, với optimization nó sẽ thực hiện nhiều chiến lược khác nhau để giảm tối đa kích cở ứng dụng của chúng ta. Trong bài post này, mình sẽ mô tả làm thế nào mà R8 thực hiện những task compile-time cho project cũng như làm thế nào chúng ta có thể customize chúng. Bài viết sẽ gồm 2 phần:

Khi chúng ta build project sử dụng Android Gradle plugin 3.4.0 hoặc cao hơn, nó không còn dùng ProGuard để thực hiện compile-time code optimization. Thay vào đó là sử dụng R8 compiler để xử lý các compile-time tasks:

  • Code shrinking (or tree-shaking): Detect và remove một cách an toàn các class, field, method, và attributes không sử dụng trong app cũng như các library dependency (đây là một tool hữu ích để chúng ta chỉ làm việc trong 64k references limit). Ví dụ, nếu chúng ta chỉ sử dụng 1 vài API của library dependency, shrinking có thể nhận biết được những code trong lib không sử dụng và remove.
  • Resource shrinking: Remove những resouces không sử dụng trong package app, kể cả unused resource trong lib dependency. Nó sẽ kết hợp với code shrinking để xem xét những resouces không được reference đến sao đó remove nó một cách an toàn.
  • Obfuscation: Rút gọn tên các class và member nhầm giảm kích thước file DEX cũng như gây khó khăn cho việc xem lại source code bị dịch ngược.
  • Optimization: Kiểm tra và viết lại code của chúng ta để tiếp tục giảm kích thước của file DEX. Ví dụ, nếu R8 phát hiện một nhánh else {} không bao giờ được sử dụng, nó sẽ remove nhánh else {} này.

Khi chúng ta thực hiện release một version cho ứng dụng, mặc định thì R8 sẽ tự động thực hiện các task compile-time được mô tả như trên cho chúng ta. Tuy nhiên chúng ta hoàn toàn có thể disable một số task nhất định hoặc customize hành vi của R8 thông qua ProGuard rules files. Thực tế, R8 làm việc với tất cả những file ProGuard hiện tại của bạn, vì vậy việc update plugin Android Gradle để sử dụng R8 không cần phải thay đổi các rules hiện tại.

Enable shrinking, obfuscation, and optimization

Khi chúng ta sử dụng Android Studio 3.4 hoặc Android Gradle plugin 3.4.0 và cao hơn, R8 là compiler mặc định để convert project sang Java bytecode đưa vào trong định dạng file DEX chạy trên Android platform. Tuy nhiên khi chúng ta tạo một new project với Android Studio, shrinking, obfuscation và code optimization mặc định bị disable. Đó là vì có những compile-time optimizations làm tăng thời gian build project và có thể gây ra một số bugs nếu chúng ta không có custommize để keep code.

Vậy nên, cách tốt nhất là chúng ta nên enable các task compile-time khi build final version. Để enable shrinking, obfuscation, và optimization, chúng ta thực hiện thêm các code sau vào trong file build.gradle của project-level.

android {
    buildTypes {
        release {
            // Enables code shrinking, obfuscation, and optimization for only
            // your project's release build type.
            minifyEnabled true

            // Enables resource shrinking, which is performed by the
            // Android Gradle plugin.
            shrinkResources true

            // Includes the default ProGuard rules files that are packaged with
            // the Android Gradle plugin. To learn more, go to the section about
            // R8 configuration files.
            proguardFiles getDefaultProguardFile(
                    'proguard-android-optimize.txt'),
                    'proguard-rules.pro'
        }
    }
    ...
}

R8 configuration files

R8 dùng ProGuard rules file để sửa đổi hành vi mặc định của chính nó. Những file này có file có thể sửa được, nhưng có những file được generate tự động bởi compile-time tool như AAPT2 hoặc kết thừa từ các lib dependency. Bảng dưới đây mô tả nguồn của các file ProGuard rules mà R8 dùng.

Source Location Description
Android Studio <module-dir>/proguard-rules.pro Khi chúng ta tạo một module mới với Android Studio, IDE sẽ tạo một file proguard-rules.pro trong thư mục root của module đó.

Mặc định file này không áp dụng bất kì rules nào. Vì vậy chúng ta có thể thêm các ProGuard rule của chúng ta ở đây.
Android Gradle plugin Được tạo bởi Android Gradle plugin lúc compile time. Android Gradle plugin tạo ra proguard-android-optimize.txt với các rules cho hầu hết các Android project và enable @Keep* annotations.

Mặc định khi chúng ta tạo module mới thì file này sẽ được include vào module-level build.gradle trong release build.
Library dependencies AAR libraries: <library-dir>/proguard.txt

JAR libraries: <library-dir>/META-INF/proguard/
Nếu một AAR lib được publish với ProGuard rules file của chính nó và chúng ta include ARR như một compile-time denpendency, R8 sẽ tự động áp dụng nó vào rules khi compile project của chúng ta.
Sử dụng các file rule được đóng gói bởi thư viện AAR là rất hữu ích vì điều đó giúp cho thư viện hoạt động một cách chính xác nhất, ở đây người phát triển thư viện đã giúp chúng ta giải quyết các vấn đề.

Tuy nhiên, chúng ta cũng cần lưu ý rằng các rules trong AAR không thể remove được và nó có thể gây ảnh hưởng đến các thành phần khác của app. Ví dụ, nếu một thư viện include một rule disable optimization thì rule này sẽ disable optimization của toàn bộ project chúng ta.
Android Asset Package Tool 2 (AAPT2) Sau khi build project với minifyEnabled true: module-dir>/build/intermediates/proguard-rules/debug/aapt_rules.txt AAPT2 tạo ra các keep rules dựa trên các references đến các class trong manifest, layouts, và các resources khác của app. Ví dụ, AAPT2 sẽ include một keep rule cho mỗi Activity rằng chúng ta sẽ đăng kí chúng trong manifest của app.
Custom configuration files <module-dir>/proguard-rules.pro Để thêm các rules của chúng ta mong muốn và R8 sẽ áp dụng chúng khi compile-time.

Khi chúng ta set thuộc tính minifyEnabledtrue, R8 sẽ kết hợp các rules từ tất cả các nguồn được liệt kê trong danh sách trên. Điều này rất quan trọng khi chúng ta gặp vấn đề với R8, vì complile-time của các dependency khác như library dependency có thể thay đổi hành vi của R8 mà chúng ta không hề biết đến.

Để xuất toàn bộ report về tất cả các rules R8 áp dụng khi build project, chúng ta cần include code sau vào file proguard-rules.pro của module.

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

Include additional configurations

Khi chúng ta tạo một project hoặc module mới sử dụng Android Studio, IDE sẽ tạo một file <module-dir>/proguard-rules.pro để include các rules của chúng ta. Chúng ta cũng có thể add thêm các rules từ các file khác bằng các add chúng và thuộc tính proguardFiles trong file build.gradle của module.

Ví dụ, chúng ta có thể add các rule được chỉ rõ với mỗi build varitant bằng việc thêm các thuộc tính proguardFiles khác tương ứng với block productFlavor. Như file gradle bên dưới chúng ta thêm flavor2-rules.pro vào flavor2 product flavor. Bây giờ, flavor2 dùng tất cả 3 ProGuard rules vì các rule từ release block cũng được áp dụng.

android {
    ...
    buildTypes {
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile(
              'proguard-android-optimize.txt'),
              // List additional ProGuard rules for the given build type here. By default,
              // Android Studio creates and includes an empty rules file for you (located
              // at the root directory of each module).
              'proguard-rules.pro'
        }
    }
    flavorDimensions "version"
    productFlavors {
        flavor1 {
          ...
        }
        flavor2 {
            proguardFile 'flavor2-rules.pro'
        }
    }
}

Shrink your code

Code shrinking với R8 mặc định được enable khi chúng ta set thuộc tính minifyEnabledtrue.

Code shrinking là quá trình của việc loại bỏ code mà R8 xác định nó không được yêu cầu lúc runtime. Quá trình này có thể giúp giảm thiểu một cách đáng kể kích thước của app, ví dụ như chúng ta include nhiều library dependency nhưng chỉ tận dụng các thành phần nhỏ chức năng của thư viện.

Để shrink code của app, R8 đầu tiên xác định tất cả entry points trong app dựa trên việc kết hợp các file config đã đề cập phía trên. Những entry point bao gồm tất cả các class của Android có thể dùng để open các Activity hoặc service của ứng dụng. Bắt đầu từ entry point, R8 kiểm tra các code trong ứng dụng để build một graph cho tất cả các methods, member, variable và các class khác app có thể truy cập lúc runtime. Các code không được connect vào graph được xem là unreachable và có thể remove khỏi app.

Như hình bên dưới chúng ta có một app với runtime library dependency. Trong khi kiểm tra code của app, R8 xác định rằng các method foo(), faz(), và bar() có thể truy cập từ entry point MainActivity.class. Tuy nhiên, class OkayApi.class hoặc phương thức baz()của nó không bao giờ được sử dụng bởi app tại runtime, và R8 sẽ remove code này khi shrinking app.

R8 xác định các entry point thông qua các -keep rules trong file Config R8 trong project. Nó chỉ ra các class mà R8 không nên discard khi thực hiện shrinking và R8 sẽ xem những class đó có thể là entry point vào app. Android Gradle plugin và AAPT2 tự động tạo các keep rule được yêu cầu bởi hầu hết các project cho chúng ta, như là các activity, view, và service. Tuy nhiên nếu cần customize lại những keep rules này, thì bạn có thể xem nội dung tiếp sau đây customize which code to keep.

Customize which code to keep

Trong hầu hết các tình huống, các file ProGuard rule mặc định (proguard-android-optimize.txt) là đủ để R8 chỉ remove những unused code. Tuy nhiên, với một vài tình huống khó khăn hơn để cho R8 có thể phân tích một cách chính xác nhất và có thể remove code ứng dụng thật sự cần. Một vài ví dụ cho việc remove không đúng code:

  • Khi app gọi method từ Java Native Interface (JNI)
  • Khi app looks up code tại runtime (như với reflection )

Việc testing app có thể phát hiện được những lỗi do việc remove code không phù hợp, tuy nhiên chúng ta cũng có thể kiểm tra code thông qua generating a report of removed code, mình sẽ đề cập đến nội dung này sau.

Để fix lỗi và buộc R8 giữ lại code chúng ta cần add -keep line vào trong file ProGuard rules. Ví dụ:

-keep public class MyClass

Hoặc chúng ta có thể add @Keep annotation vào code chúng ta muốn keep. Việc sử dụng @Keep sẽ giữ nguyên toàn bộ class. Nếu chúng ta chỉ cần giữ lại các method/field cụ thể thì ta có thể sử dụng @Keep cho chúng. Lưu ý, annotation này chỉ có trong AndroidX lib và khi chúng ta include file ProGuard rules của Android Gradle plugin, như đã đề cập ở Enable shrinking.

Kết luận

Trong phần này mình đã trình bày về các khái niệm cơ bản của shrinking, obfuscation, và optimization, cũng như các file config rule trong Android và cách để shrinking code trong app. Trong phần tiếp theo mình sẽ giới thiệu về cách làm thế nào để Shrink resource, Obfuscate code, Optimization code và giải quyết các vấn đề xung quanh việc sử dụng R8.

Tham khảo

  1. https://developer.android.com/studio/build/shrink-code