Rút gọn, làm rối mã nguồn và tối ưu hoá ứng dụng

Để giảm tối đa kích thước ứng dụng, bạn nên bật tính năng rút gọn (shrinking) trong bản phát hành để xoá mã và tài nguyên không sử dụng. Khi bật tuỳ chọn rút gọn, tuỳ chọn này sẽ kích hoạt đồng thời các tính năng làm rối (giúp rút ngắn tên lớp và các thành phần của lớp trong ứng dụng) và tính năng tối ưu hoá (áp dụng các chiến lược linh hoạt hơn để giảm tối đa kích thước của ứng dụng). Trang này sẽ mô tả cách R8 thực hiện các tác vụ này tại thời điểm biên dịch cũng như hướng dẫn bạn cách tuỳ chỉnh các tác vụ đó.

Khi bạn xây dựng dự án bằng trình bổ trợ Android cho Gradle 3.4.0 trở lên, trình bổ trợ này sẽ không sử dụng ProGuard để tối ưu hoá mã trong thời gian biên dịch nữa. Thay vào đó, trình bổ trợ này sẽ kết hợp với trình biên dịch R8 để xử lý các tác vụ sau đây tại thời điểm biên dịch:

  • Rút gọn mã (hoặc loại bỏ mã chết (tree-shaking)): nhận diện và loại bỏ một cách an toàn các lớp, trường, phương thức và thuộc tính không sử dụng khỏi ứng dụng cũng như các phần phụ thuộc thư viện của ứng dụng. Đây là một công cụ hữu ích giúp tạm thời khắc phục giới hạn tham chiếu 64k. Ví dụ: nếu bạn chỉ sử dụng một số API của phần phụ thuộc thư viện, tính năng rút gọn này có thể nhận diện mã thư viện mà ứng dụng không sử dụng và xoá chính mã đó khỏi ứng dụng. Để tìm hiểu thêm, hãy tham khảo phần nội dung về cách rút gọn mã.
  • Rút gọn tài nguyên: xoá tài nguyên không sử dụng khỏi ứng dụng đóng gói, bao gồm cả tài nguyên không sử dụng đến trong phần phụ thuộc thư viện của ứng dụng. Tính năng này được sử dụng kết hợp với tính năng rút gọn mã sao cho khi xoá mã không sử dụng, những tài nguyên không được tham chiếu đến nữa cũng được xoá một cách an toàn. Để tìm hiểu thêm, hãy tham khảo phần nội dung về cách rút gọn tài nguyên.
  • Làm rối: rút ngắn tên lớp và các thành phần của lớp, nhờ đó làm giảm kích thước tệp DEX. Để tìm hiểu thêm, hãy chuyển đến phần nội dung về cách làm rối mã.
  • Tối ưu hoá: kiểm tra và viết lại mã để giảm tối đa kích thước tệp DEX của ứng dụng. Ví dụ: nếu R8 phát hiện lệnh rẻ nhánh else {} của một câu lệnh if/else nào đó không bao giờ được sử dụng thì R8 sẽ xoá mã của lệnh rẻ nhánh else {} đó. Để tìm hiểu thêm, hãy tham khảo phần nội dung tối ưu hoá mã.

Theo mặc định, khi tạo phiên bản phát hành của ứng dụng, R8 sẽ tự động thực hiện các thao tác biên dịch nêu trên. Tuy nhiên, bạn có thể vô hiệu hoá một số thao tác hoặc tuỳ chỉnh hành vi của R8 thông qua các tệp quy tắc ProGuard. Trên thực tế, R8 kết hợp hiệu quả với tất cả tệp quy tắc ProGuard hiện có, vì vậy, khi cập nhật trình bổ trợ Android cho Gradle để sử dụng R8, bạn không bắt buộc phải thay đổi các quy tắc hiện tại của mình.

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, tốt nhất bạn nên bật các tác vụ này tại thời điểm biên dịch khi tạo phiên bản kiểm thử ứng dụng cuối cùng trước khi phát hành. Để bật tính năng rút gọn, làm rối và tối ưu hoá, hãy kèm theo các thành phần sau trong tệp build.gradle ở cấp dự án.

Groovy

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'
        }
    }
    ...
}

Kotlin

android {
    buildTypes {
        getByName("release") {
            // Enables code shrinking, obfuscation, and optimization for only
            // your project's release build type.
            isMinifyEnabled = true

            // Enables resource shrinking, which is performed by the
            // Android Gradle plugin.
            isShrinkResources = 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"
            )
        }
    }
    ...
}

Tệp cấu hình R8

R8 sử dụng tệp quy tắc ProGuard để chỉnh sửa trạng thái mặc định của ứng dụng và giúp bạn hiểu rõ hơn về cấu trúc của ứng dụng, chẳng hạn như lớp đóng vai trò là điểm truy cập (entry point) mã ứng dụng. Bạn có thể chỉnh sửa một số tệp quy tắc này. Tuy nhiên, một số quy tắc có thể được tạo tự động bằng các công cụ tại thời điểm biên dịch, chẳng hạn như AAPT2, hoặc kế thừa từ các phần phụ thuộc thư viện của ứng dụng. Bảng bên dưới mô tả nguồn gốc của các tệp quy tắc ProGuard được R8 sử dụng.

Nguồn Vị trí Mô tả
Android Studio <module-dir>/proguard-rules.pro Khi tạo một mô-đun mới bằng Android Studio, IDE này sẽ tạo một tệp proguard-rules.pro trong thư mục gốc của mô-đun đó.

Theo mặc định, tệp này không áp dụng bất kỳ quy tắc nào. Do đó, hãy thêm các quy tắc ProGuard riêng của bạn vào đây, chẳng hạn như các quy tắc lưu giữ (keep rule) tuỳ chỉnh.

Trình bổ trợ Android cho Gradle Được trình bổ trợ Android cho Gradle tạo ra tại thời điểm biên dịch. 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*.

Theo mặc định, khi tạo một mô-đun mới bằng Android Studio, tệp build.gradle cấp mô-đun sẽ bao gồm tệp quy tắc này trong bản phát hành.

Lưu ý: Trình bổ trợ Android cho Gradle sẽ bao gồm các tệp quy tắc ProGuard bổ sung được xác định trước, nhưng bạn nên sử dụng proguard-android-optimize.txt.

Phần phụ thuộc thư viện Thư viện AAR (đề xuất được tự động áp dụng): <library-dir>/proguard.txt

Thư viện JAR: <library-dir>/META-INF/proguard/

Nếu một thư viện AAR được xuất bản có chứa tệp quy tắc ProGuard riêng và bạn đính kèm thư viện AAR này như một phần phụ thuộc tại thời điểm biên dịch, thì R8 sẽ tự động áp dụng các quy tắc đó khi biên dịch dự án.

Việc đóng gói tệp quy tắc cùng với thư viện AAR sẽ rất hữu ích nếu thư viện này cần các quy tắc lưu giữ nhất định để có thể hoạt động một cách chính xác. Điều này có nghĩa là nhà phát triển thư viện đã giúp bạn khắc phục sự cố nếu có.

Tuy nhiên, nên lưu ý rằng, các quy tắc ProGuard chỉ là phần bổ sung nên bạn không thể xoá một số quy tắc nhất định trong phần phụ thuộc của thư viện AAR vì điều này có thể ảnh hưởng đến việc tổng hợp các phần khác trong ứng dụng. Ví dụ: nếu một thư viện chứa một quy tắc dùng để tắt tính năng tối ưu hoá mã, thì quy tắc đó sẽ vô hiệu hoá tính năng tối ưu hoá cho toàn bộ dự án.

Công cụ đóng gói tài nguyên Android 2 (AAPT2) Sau khi xây dựng dự án của bạn bằng minifyEnabled true: <module-dir>/build/intermediates/proguard-rules/debug/aapt_rules.txt AAPT2 sẽ tạo ra các quy tắc lưu giữ dựa trên việc tham chiếu đến các lớp trong tệp kê khai, bố cục và tài nguyên khác của ứng dụng. Ví dụ: AAPT2 bao gồm quy tắc lưu giữ cho mỗi Hoạt động (Activity) mà bạn đăng ký trong tệp kê khai của ứng dụng dưới dạng một điểm truy cập.
Tệp cấu hình tuỳ chỉnh Theo mặc định, khi tạo một mô-đun mới bằng Android Studio, IDE này sẽ tạo <module-dir>/proguard-rules.pro để bạn có thể thêm vào các quy tắc riêng của mình. Bạn có thể thêm các cấu hình bổ sung, sau đó R8 sẽ áp dụng các cấu hình này tại thời điểm biên dịch.

Khi bạn thiết lập thuộc tính minifyEnabled thành true, R8 sẽ kết hợp các quy tắc từ tất cả nguồn có sẵn nêu trên. Điều này rất quan trọng và bạn cần ghi nhớ khi khắc phục sự cố với R8, vì các phần phụ thuộc tại thời điểm biên dịch khác, chẳng hạn như các phần phụ thuộc thư viện, có thể thay đổi hành vi của R8 mà bạn không biết.

Để 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

Thêm các cấu hình bổ sung

Khi tạo một dự án hoặc mô-đun mới bằng Android Studio, IDE này sẽ tạo tệp <module-dir>/proguard-rules.pro để bạn đưa vào các quy tắc riêng của mình. Bạn cũng có thể thêm các quy tắc bổ sung trong các tệp khác bằng cách thêm các quy tắc đó vào thuộc tính proguardFiles trong tệp build.gradle trong mô-đun của bạn.

Ví dụ: bạn có thể thêm các quy tắc dành riêng cho từng biến thể bản dựng (build variant) bằng cách thêm một thuộc tính proguardFiles khác trong khối lệnh productFlavor tương ứng. Tệp Gradle dưới đây sẽ thêm flavor2-rules.pro vào phiên bản sản phẩm (product flavor) flavor2. Hiện tại, flavor2 sử dụng cả ba quy tắc ProGuard vì các quy tắc đó cũng được áp dụng trong khối release.

Groovy

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'
        }
    }
}

Kotlin


android {
    ...
    buildTypes {
        getByName("release") {
            isMinifyEnabled = 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.add("version")
    productFlavors {
        create("flavor1") {
            ...
        }
        create("flavor2") {
            proguardFile("flavor2-rules.pro")
        }
    }
}

Rút gọn mã

Theo mặc định, tính năng rút gọn mã trong R8 sẽ được bật khi bạn đặt thuộc tính minifyEnabled thành true.

Rút gọn mã (còn được gọi là rung cây (tree shaking)) là quá trình xoá mã mà R8 xác định là không cần thiết trong thời gian chạy (runtime). Quá trình này có thể giảm đáng kể kích thước của ứng dụng trong trường hợp ứng dụng chứa nhiều phần phụ thuộc thư viện nhưng chỉ sử dụng một phần nhỏ chức năng trong đó chẳng hạn.

Để rút gọn mã của ứng dụng, trước tiên, R8 sẽ xác định tất cả điểm truy cập mã ứng dụng dựa trên nhóm tệp cấu hình kết hợp. Các điểm truy cập này gồm tất cả lớp mà nền tảng Android có thể sử dụng để mở Hoạt động hoặc dịch vụ của ứng dụng. Xuất phát từ mỗi điểm truy cập, R8 sẽ kiểm tra mã ứng dụng để xây dựng sơ đồ tất cả phương thức, biến thành viên và các lớp khác mà ứng dụng có thể truy cập trong thời gian chạy. Mã nào không kết nối với sơ đồ này sẽ được xem là không thể tiếp cận và có thể bị xoá khỏi ứng dụng.

Hình 1 cho thấy một ứng dụng có phần phụ thuộc thư viện trong thời gian chạy. Khi kiểm tra mã ứng dụng, R8 xác định rằng các phương thức foo(), faz()bar() có thể tiếp cận được từ điểm truy cập MainActivity.class. Tuy nhiên, ứng dụng không bao giờ sử dụng lớp OkayApi.class hoặc phương thức baz() của lớp này trong thời gian chạy. Do đó R8 sẽ xoá mã đó trong quá trình rút gọn ứng dụng.

Hình 1. Tại thời điểm biên dịch, R8 tạo một sơ đồ dựa trên các quy tắc lưu giữ kết hợp của dự án để xác định những mã không thể truy cập được.

R8 xác định điểm truy cập thông qua các quy tắc -keep trong tệp cấu hình R8 của dự án. Điều này có nghĩa là các quy tắc lưu giữ xác định những lớp nào R8 không được loại bỏ trong quá trình rút gọn ứng dụng. Đồng thời, R8 coi những lớp đó là các điểm có thể truy cập vào ứng dụng. Trình bổ trợ Android cho Gradle và AAPT2 tự động tạo các quy tắc lưu giữ (cần thiết cho hầu hết các dự án ứng dụng) cho bạn, chẳng hạn như các hoạt động, khung hiển thị và dịch vụ của ứng dụng. Tuy nhiên, nếu bạn cần tuỳ chỉnh hành vi mặc định này thông qua các quy tắc lưu giữ bổ sung, hãy đọc phần nội dung về cách tuỳ chỉnh mã được giữ lại.

Ngược lại, nếu bạn chỉ muốn giảm kích thước tài nguyên của ứng dụng, hãy chuyển sang phần hướng dẫn cách rút gọn tài nguyên.

Tuỳ chỉnh mã cần lưu giữ

Trong hầu hết các trường hợp, tệp quy tắc ProGuard mặc định (proguard-android- optimize.txt ) là đủ để R8 xoá mã không sử dụng. Tuy nhiên, trong một số trường hợp R8 không thể phân tích chính xác và có thể xoá nhầm mã mà ứng dụng thực sự cần. Sau đây là một số ví dụ về trường hợp xoá mã không chính xác:

  • Khi ứng dụng gọi một phương thức từ Giao diện gốc Java (JNI)
  • Khi ứng dụng tra cứu mã trong thời gian chạy (chẳng hạn như tính năng phản chiếu (reflection))

Việc kiểm thử ứng dụng sẽ giúp phát hiện mọi lỗi phát sinh do xoá mã không phù hợp. Tuy nhiên, bạn cũng có thể kiểm tra xem mã nào đã xoá bằng cách tạo một báo cáo về các mã đã xoá.

Để khắc phục lỗi và buộc R8 phải giữ lại một số mã nhất định nào đó, hãy thêm một dòng -keep trong tệp quy tắc ProGuard. Ví dụ:

-keep public class MyClass

Ngoài ra, bạn có thể thêm chú thích @Keep vào mã bạn muốn giữ lại. Việc thêm @Keep vào một lớp sẽ giúp giữ nguyên toàn bộ lớp này. Nếu thêm chú thích này vào phương thức hoặc trường thì phương thức/trường (bao gồm tên của phương thức/trường này) cũng như tên lớp sẽ được giữ nguyên. Lưu ý rằng chú thích này chỉ có sẵn khi sử dụng Thư viện chú thích AndroidX và khi bạn đóng gói tệp quy tắc ProGuard này với trình bổ trợ Android cho Gradle, như được mô tả trong phần nội dung về cách bật tính năng rút gọn ứng dụng.

Bạn nên cân nhắc một số thứ khi sử dụng tuỳ chọn -keep; để biết thêm thông tin về cách tuỳ chỉnh tệp quy tắc, hãy tham khảo Hướng dẫn sử dụng ProGuard. Phần Khắc phục sự cố sẽ trình bày các vấn đề phổ biến khác có thể gặp trong quá trình xoá mã.

Xoá thư viện gốc

Theo mặc định, các thư viện mã gốc sẽ bị loại ra khỏi các bản phát hành của ứng dụng. Thao tác xoá này bao gồm việc xoá bảng biểu tượng và thông tin gỡ lỗi trong mọi thư viện gốc mà ứng dụng sử dụng. Việc loại bỏ các thư viện mã gốc giúp giảm đáng kể kích thước ứng dụng; tuy nhiên, bạn không thể chẩn đoán các sự cố có thể xảy ra trên Google Play Console do thiếu thông tin (chẳng hạn như tên lớp và tên hàm).

Hỗ trợ xử lý trục trặc với mã gốc

Google Play Console báo cáo các trục trặc với mã gốc trong Android vitals. Qua vài bước, bạn có thể tạo và tải một tệp biểu tượng gỡ lỗi gốc lên cho ứng dụng. Tệp này sẽ kích hoạt dấu vết ngăn xếp sự cố gốc tượng trưng (bao gồm tên lớp và tên hàm) trong Android vitals, cho phép bạn gỡ lỗi ứng dụng trong bản phát hành chính thức. Các bước này sẽ khác nhau tuỳ thuộc vào phiên bản của trình bổ trợ Android cho Gradle được sử dụng trong dự án và kết quả bản dựng của dự án.

Trình bổ trợ Android cho Gradle phiên bản 4.1 trở lên

Nếu dự án xây dựng một tệp định dạng Android App Bundle thì bạn có thể tự động đưa tệp biểu tượng gỡ lỗi gốc vào tệp định dạng này. Để đưa tệp này vào các bản phát hành, hãy thêm nội dung sau vào tệp build.gradle của ứng dụng:

android.buildTypes.release.ndk.debugSymbolLevel = { SYMBOL_TABLE | FULL }

Chọn cấp biểu tượng gỡ lỗi từ tuỳ chọn sau:

  • Sử dụng SYMBOL_TABLE để lấy tên hàm trong dấu vết ngăn xếp tượng trưng của Play Console. Cấp độ này sẽ hỗ trợ cấu trúc tombstones.
  • Sử dụng FULL để lấy tên hàm, tệp và số dòng trong dấu vết ngăn xếp tượng trưng của Play Console.

Nếu dự án xây dựng một APK, hãy sử dụng chế độ cài đặt bản dựng build.gradle được hiển thị trước đó để tạo tệp biểu tượng gỡ lỗi gốc riêng biệt. Tải tệp biểu tượng gỡ lỗi gốc lên Google Play Console theo cách thủ công. Là một phần trong quy trình xây dựng, trình bổ trợ Android cho Gradle sẽ xuất ra tệp này tại vị trí dưới đây trong dự án:

app/build/outputs/native-debug-symbols/variant-name/native-debug-symbols.zip

Trình bổ trợ Android cho Gradle phiên bản 4.0 trở về trước (và các hệ thống xây dựng khác)

Là một phần trong quy trình xây dựng, trình bổ trợ Android cho Gradle sẽ lưu giữ một bản sao của các thư viện chứa biểu tượng gỡ lỗi trong thư mục của dự án. Cấu trúc thư mục này tương tự như sau:

app/build/intermediates/cmake/universal/release/obj/
├── armeabi-v7a/
│   ├── libgameengine.so
│   ├── libothercode.so
│   └── libvideocodec.so
├── arm64-v8a/
│   ├── libgameengine.so
│   ├── libothercode.so
│   └── libvideocodec.so
├── x86/
│   ├── libgameengine.so
│   ├── libothercode.so
│   └── libvideocodec.so
└── x86_64/
    ├── libgameengine.so
    ├── libothercode.so
    └── libvideocodec.so
  1. Nén nội dung của thư mục này:

    cd app/build/intermediates/cmake/universal/release/obj
    zip -r symbols.zip .
    
  2. Tải tệp symbols.zip lên Google Play Console theo cách thủ công.

Rút gọn tài nguyên

Tính năng rút gọn tài nguyên chỉ hiệu quả khi kết hợp với tính năng rút gọn mã. Sau khi trình rút gọn mã xoá tất cả mã không sử dụng, trình rút gọn tài nguyên có thể nhận dạng những tài nguyên nào mà ứng dụng vẫn sử dụng. Điều này đặc biệt chính xác khi bạn thêm các thư viện mã có chứa tài nguyên—bạn phải xoá mã thư viện không còn sử dụng để các tài nguyên thư viện trở thành tài nguyên không được tham chiếu và do đó sẽ bị trình rút gọn tài nguyên xoá bỏ.

Để bật tính năng rút gọn tài nguyên, hãy thiết lập thuộc tính shrinkResources thành true trong tệp build.gradle (cùng vớiminifyEnabled để bật tính năng rút gọn mã). Ví dụ:

Groovy

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

Kotlin


android {
    ...
    buildTypes {
        getByName("release") {
            isShrinkResources = true
            isMinifyEnabled = true
            proguardFiles(
                getDefaultProguardFile("proguard-android.txt"),
                "proguard-rules.pro"
            )
        }
    }
}

Nếu bạn chưa từng tạo ứng dụng bằng cách dùng minifyEnabled để rút gọn mã, hãy thử làm điều này trước khi bật shrinkResources. Lý do là có thể bạn cần chỉnh sửa tệp proguard-rules.pro để lưu giữ các lớp hoặc phương thức được tạo hoặc gọi động trước khi bắt đầu xoá tài nguyên.

Tuỳ chỉnh tài nguyên cần giữ lại

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 và tài nguyên cần loại bỏ trong thuộc tính tools:discard. 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" />

Lưu tệp này trong phần tài nguyên của dự án, ví dụ như trong thư mục res/raw/keep.xml. Bản dựng sẽ không đóng gói tệp này vào ứng dụng.

Bạn có thể cho rằng thật ngớ ngẩn khi chỉ định tài nguyên nào sẽ được huỷ trong khi bạn có thể xoá những tài nguyên này. Tuy nhiên, việc này có thể hữu ích khi sử dụng các biến thể bản dựng. Ví dụ: bạn có thể đặt tất cả tài nguyên của mình vào thư mục chung của dự án, sau đó tạo tệp keep.xml khác nhau cho mỗi biến thể bản dựng khi bạn biết một tài nguyên cụ thể nào đó có vẻ như được dùng trong mã (và do đó không bị trình rút gọn xoá đi) nhưng thực sự không được dùng cho biến thể bản dựng đã cho. Cũng có thể các công cụ tạo bản dựng đã xác định không chính xác tài nguyên cần thiết. Vấn đề này có thể xảy ra vì trình biên dịch thêm mã tài nguyên nội tuyến (inline) và sau đó, trình phân tích tài nguyên không nhận thấy sự khác biệt giữa tài nguyên được tham chiếu thực sự và giá trị số nguyên trong mã chứa cùng giá trị.

Bật tính năng kiểm tra tham chiếu nghiêm ngặt

Thông thường, trình rút gọn tài nguyên có thể xác định chính xác một tài nguyên nào đó có được sử dụng hay không. Tuy nhiên, nếu mã chứa lời gọi hàm Resources.getIdentifier() (hoặc nếu thư viện của bạn thực hiện điều này – thư viện AppCompat), điều đó có nghĩa rằng mã của bạn đang tra cứu tên tài nguyên dựa trên các chuỗi được tạo động. Khi thực hiện điều này, trình rút gọn tài nguyên sẽ hoạt động theo cơ chế bảo vệ mặc định và đánh dấu tất cả tài nguyên có định dạng tên phù hợp dưới dạng tài nguyên đã được sử dụng và không được xoá.

Ví dụ: mã sau đây sẽ đánh dấu tất cả tài nguyên có tiền tố img_ là đã được sử dụng.

Kotlin

val name = String.format("img_%1d", angle + 1)
val res = resources.getIdentifier(name, "drawable", packageName)

Java

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

Trình rút gọn tài nguyên cũng xem xét tất cả hằng số kiểu chuỗi trong mã cũng như các tài nguyên res/raw/ khác nhau để tìm các URL tài nguyên có định dạng tương tự như file:///android_res/drawable//ic_plus_anim_016.png. Nếu tìm thấy các chuỗi này hoặc các chuỗi khác nhưng có thể dùng để tạo các URL như vậy, thì trình rút gọn sẽ không xoá các chuỗi đó.

Đây là ví dụ về chế độ rút gọn an toàn được bật theo mặc định. Tuy nhiên, bạn có thể tắt tính năng xử lý theo hướng "cẩn tắc vô ưu" này và chỉ định trình rút gọn tài nguyên chỉ giữ lại những tài nguyên nào chắc chắn được sử dụng. Để thực hiện việc này, hãy đặt shrinkMode thành strict trong tệp keep.xml như sau:

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
    tools:shrinkMode="strict" />

Nếu bật chế độ rút gọn nghiêm ngặt và mã của bạn cũng tham chiếu đến các tài nguyên chứa các chuỗi được tạo động như hiển thị ở trên, thì bạn phải lưu giữ các tài nguyên đó theo cách thủ công thông qua thuộc tính tools:keep.

Xoá tài nguyên thay thế không sử dụng

Trình rút gọn tài nguyên Gradle chỉ xoá các tài nguyên không được mã ứng dụng tham chiếu đến, nghĩa là trình rút gọn này sẽ không xoá các tài nguyên thay thế cho các cấu hình thiết bị khác nhau. Nếu cần, bạn có thể dùng thuộc tính resConfigs của trình bổ trợ Android cho Gradle để xoá các tệp tài nguyên thay thế mà ứng dụng không cần nữa.

Ví dụ: nếu bạn đang sử dụng một thư viện có chứa tài nguyên ngôn ngữ (chẳng hạn như AppCompat hoặc Dịch vụ Google Play), thì ứng dụng sẽ bao gồm tất cả chuỗi ngôn ngữ đã dịch cho các thông báo trong những thư viện đó bất kể phần còn lại của ứng dụng có được dịch sang cùng một ngôn ngữ hay không. Nếu chỉ muốn chỉ giữ lại những ngôn ngữ mà ứng dụng hỗ trợ chính thức, bạn có thể chỉ định những ngôn ngữ đó trong thuộc tính resConfig. Mọi tài nguyên cho các ngôn ngữ chưa được chỉ định sẽ bị xoá.

Đoạn mã sau đây cho phép giới hạn tài nguyên ngôn ngữ chỉ còn tiếng Anh và tiếng Pháp:

Groovy

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

Kotlin

android {
    defaultConfig {
        ...
        resourceConfigurations.addAll(listOf("en", "fr"))
    }
}

Khi phát hành một ứng dụng theo định dạng Android App Bundle, theo mặc định, chỉ những ngôn ngữ được định cấu hình trên thiết bị của người dùng mới được tải xuống khi cài đặt ứng dụng. Tương tự, chỉ các tài nguyên phù hợp với mật độ màn hình của thiết bị và các thư viện gốc phù hợp với ABI (giao diện nhị phân ứng dụng) của thiết bị mới được bao gồm trong tệp tải xuống. Để biết thêm thông tin, hãy tham khảo cấu hình Android App Bundle.

Với các ứng dụng cũ được phát hành bằng APK (được tạo trước tháng 8 năm 2021), bạn có thể tuỳ chỉnh mật độ màn hình hoặc tài nguyên ABI để đưa vào APK bằng cách tạo nhiều APK, trong đó mỗi mục tiêu có một cấu hình thiết bị khác nhau.

Hợp nhất các tài nguyên trùng lặp

Theo mặc định, Gradle cũng hợp nhất các tài nguyên có tên giống hệt nhau, chẳng hạn như các đối tượng có thể vẽ có cùng tên có thể nằm trong các thư mục tài nguyên khác nhau. Thuộc tính shrinkResources không kiểm soát cũng như không thể kích hoạt hành vi này nhằm tránh lỗi xảy ra khi nhiều tài nguyên có tên khớp với tên đang được tìm kiếm trong mã của bạn.

Việc hợp nhất tài nguyên chỉ xảy ra khi hai hoặc nhiều tệp có cùng tên, loại cũng như bộ hạn định (qualifier). Gradle sẽ chọn tệp tốt nhất trong số các tệp trùng lặp (dựa trên thứ tự ưu tiên được mô tả bên dưới) và chỉ truyền duy nhất tài nguyên đó cho AAPT để phân phối trong cấu phần mềm cuối cùng.

Gradle tìm các tài nguyên trùng lặp trong các vị trí sau:

  • Các tài nguyên chính, liên kết với nhóm tài nguyên chính, thường nằm trong src/main/res/.
  • Các lớp phủ biến thể (variant overlay) trong loại bản dựng và các phiên bản của bản dựng (build flavor).
  • Các phần phụ thuộc thư viện của dự án.

Gradle sẽ hợp nhất các tài nguyên trùng lặp theo thứ tự phân tầng ưu tiên như sau:

Phần phụ thuộc → Chính → Phiên bản của bản dựng → Loại bản dựng

Ví dụ: nếu một tài nguyên nào đó xuất hiện lặp lại trong cả tài nguyên chính và phiên bản của bản dựng thì Gradle sẽ chọn tài nguyên trong phiên bản của bản dựng.

Nếu các tài nguyên giống hệt nhau xuất hiện trong cùng một nhóm tài nguyên, Gradle không thể hợp nhất các tài nguyên đó và phát sinh lỗi hợp nhất tài nguyên. Điều này có thể xảy ra nếu bạn định nghĩa nhiều nhóm tài nguyên trong thuộc tính sourceSet của tệp build.gradle, ví dụ: nếu cả src/main/res/src/main/res2/ đều chứa các tài nguyên giống nhau.

Làm rối mã nguồn

Mục đích của tính năng làm rối mã nguồn là giảm kích thước của ứng dụng thông qua việc rút ngắn tên lớp, phương thức và trường trong ứng dụng. Sau đây là một ví dụ về cách làm rối mã nguồn bằng R8:

androidx.appcompat.app.ActionBarDrawerToggle$DelegateProvider -> a.a.a.b:
androidx.appcompat.app.AlertController -> androidx.appcompat.app.AlertController:
    android.content.Context mContext -> a
    int mListItemLayout -> O
    int mViewSpacingRight -> l
    android.widget.Button mButtonNeutral -> w
    int mMultiChoiceItemLayout -> M
    boolean mShowTitle -> P
    int mViewSpacingLeft -> j
    int mButtonPanelSideLayout -> K

Mặc dù tính năng làm rối mã nguồn sẽ không xoá mã khỏi ứng dụng, nhưng bạn có thể thấy kích thước ứng dụng giảm đáng kể nếu chứa tệp DEX được lập chỉ mục cho nhiều lớp, phương thức cũng như các trường trong ứng dụng. Tuy nhiên, việc làm rối mã nguồn sẽ đổi tên các phần khác nhau trong mã, đòi hỏi phải bổ sung thêm công cụ cho một số tác vụ nhất định, chẳng hạn như kiểm tra dấu vết ngăn xếp. Để tìm hiểu về dấu vết ngăn xếp sau khi làm rối mã nguồn, vui lòng đọc phần nội dung về cách giải mã một dấu vết ngăn xếp đã làm rối mã nguồn.

Ngoài ra, nếu bạn dùng những tên có thể dự đoán cho các phương thức và lớp trong mã của ứng dụng – chẳng hạn như khi sử dụng tính năng phản chiếu, bạn nên xem các chữ ký đó như là các điểm truy cập và chỉ định quy tắc lưu giữ những chữ ký này như mô tả trong phần hướng dẫn cách tuỳ chỉnh mã cần giữ lại. Các quy tắc này không những sẽ giúp R8 giữ lại mã đó trong DEX cuối cùng của ứng dụng mà còn giữ lại tên ban đầu của ứng dụng.

Giải mã dấu vết ngăn xếp đã làm rối mã nguồn

Sau khi R8 làm rối mã nguồn, bạn sẽ gặp khó khăn khi theo dõi dấu vết ngăn xếp (nếu không muốn nói là không thể) vì tên của lớp và phương thức có thể đã thay đổi. Để có được dấu vết ngăn xếp ban đầu, bạn nên truy vết ngược dấu vết ngăn xếp.

Tối ưu hoá mã

Để rút gọn tối đa ứng dụng, R8 sẽ kiểm tra mã ở cấp độ sâu hơn để xoá những mã nào không còn sử dụng hoặc nếu có thể, sẽ viết lại mã của bạn để giảm bớt các chi tiết rườm rà. Sau đây là một vài ví dụ về những cách để tối ưu hoá mã:

  • Nếu trong một câu lệnh if/else, lệnh rẽ nhánh else {} không bao giờ được thực hiện, R8 có thể xoá mã của nhánh else {} này.
  • Nếu mã của bạn gọi một phương thức tại một nơi duy nhất, R8 có thể thay thế nội tuyến (inline) phương thức này bằng cách chèn mã của phương thức tại vị trí của lệnh gọi phương thức đó.
  • Nếu R8 xác định một lớp nào chỉ có một lớp con duy nhất và bản thân lớp đó không được tạo bản sao (ví dụ: lớp cơ sở trừu tượng chỉ được một lớp triển khai cụ thể sử dụng), thì R8 có thể kết hợp hai lớp này và xoá một lớp khỏi ứng dụng.
  • Để tìm hiểu thêm, hãy đọc các bài đăng trên blog về tối ưu hoá R8 của tác giả Jake Wharton.

R8 không cho phép bạn tuỳ ý tắt hoặc bật tính năng tối ưu hoá hoặc sửa đổi hành vi của tính năng này. Trên thực tế, R8 bỏ qua mọi quy tắc ProGuard dùng để chỉnh sửa tính năng tối ưu hoá mặc định, chẳng hạn như -optimizations- optimizationpasses. Hạn chế này rất quan trọng vì R8 tiếp tục cải thiện, việc duy trì hành vi tiêu chuẩn để tối ưu hoá sẽ giúp nhóm Android Studio dễ dàng khắc phục và giải quyết mọi vấn đề có thể gặp phải.

Lưu ý là việc bật tính năng tối ưu hoá sẽ thay đổi dấu vết ngăn xếp cho ứng dụng. Chẳng hạn như việc cùng dòng sẽ xóa các khung ngăn xếp. Vui lòng xem phần truy vết ngược để tìm hiểu cách lấy dấu vết ngăn xếp ban đầu.

Cho phép tối ưu hoá linh hoạt hơn

R8 bao gồm một bộ tính năng tối ưu hoá bổ sung không được bật lên theo mặc định. Bạn có thể bật các tính năng tối ưu hoá bổ sung này bằng cách đưa nội dung sau vào tệp gradle.properties của dự án:

android.enableR8.fullMode=true

Các tính năng tối ưu hoá bổ sung này sẽ làm R8 trở nên khác biệt so với ProGuard, đòi hỏi bạn phải kèm theo các quy tắc ProGuard bổ sung để tránh các vấn đề về thời gian chạy. Ví dụ: giả sử trong mã có tham chiếu đến một lớp thông qua API phản chiếu trong Java (Java Reflection). Theo mặc định, R8 giả định rằng bạn có ý định kiểm tra và thao tác các đối tượng của lớp đó trong thời gian chạy, ngay cả khi mã của bạn thực sự không thực hiện điều này, và R8 sẽ tự động giữ lại lớp này cũng như trình khởi chạy tĩnh của lớp.

Tuy nhiên, khi sử dụng "chế độ đầy đủ chức năng" (full mode), R8 không giả định như vậy. Nếu R8 xác nhận mã của bạn không bao giờ sử dụng lớp này trong thời gian chạy thì R8 sẽ xoá lớp này khỏi DEX cuối cùng của ứng dụng. Nghĩa là, nếu muốn giữ lớp này cũng như trình khởi chạy tĩnh của lớp, bạn cần thêm quy tắc lưu giữ vào tệp quy tắc để thực hiện điều đó.

Nếu gặp sự cố gì khi sử dụng "chế độ đầy đủ chức năng" của R8, bạn hãy tham khảo trang Câu hỏi thường gặp về R8 để tìm giải pháp xử lý khả thi. Nếu không thể giải quyết được vấn đề, bạn hãy vui lòng gửi thông tin về lỗi này.

Truy vết dấu vết ngăn xếp

Mã do R8 xử lý sẽ thay đổi theo nhiều cách khiến dấu vết ngăn xếp trở nên khó hiểu hơn, vì dấu vết ngăn xếp sẽ không hoàn toàn tương ứng với mã nguồn. Đây có thể là trường hợp thay đổi số dòng khi thông tin gỡ lỗi không được lưu giữ. Điều này có thể là do các tính năng tối ưu hoá, chẳng hạn như chèn cùng dòng và vẽ đường viền. Yếu tố đóng góp lớn nhất là làm rối mã nguồn, nơi mà ngay cả các lớp lẫn phương thức cũng sẽ thay đổi tên.

Để khôi phục dấu vết ngăn xếp ban đầu, R8 cung cấp công cụ truy vết ngược dựa trên dòng lệnh, được đóng gói kèm với gói công cụ dòng lệnh.

Để hỗ trợ truy vết ngược các dấu vết ngăn xếp của ứng dụng, bạn nên đảm bảo bản dựng giữ lại đủ thông tin để truy vết bằng cách thêm các quy tắc sau vào tệp proguard-rules.pro của mô-đun:

-keepattributes LineNumberTable,SourceFile
-renamesourcefileattribute SourceFile

Thuộc tính LineNumberTable giữ lại thông tin vị trí trong các phương thức mà các vị trí đó được in trong dấu vết ngăn xếp. Thuộc tính SourceFile đảm bảo tất cả thời gian chạy tiềm năng đều thực sự in thông tin vị trí. Lệnh -renamesourcefileattribute đặt tên tệp nguồn trong dấu vết ngăn xếp thành SourceFile. Tên tệp nguồn thực tế ban đầu là không cần thiết khi truy vết, vì tệp ánh xạ đã chứa tệp nguồn ban đầu này.

R8 tạo một tệp mapping.txt mỗi lần chạy. Tệp này chứa thông tin cần thiết để ánh xạ các dấu vết ngăn xếp trở lại dấu vết ngăn xếp ban đầu. Android Studio lưu tệp này trong thư mục <module-name>/build/outputs/mapping/<build-type>/.

Khi phát hành ứng dụng trên Google Play, bạn có thể tải tệp mapping.txt lên cho từng phiên bản ứng dụng. Nếu phát hành ứng dụng bằng Android App Bundle, tệp này sẽ được tự động đưa vào như một phần nội dung của gói ứng dụng. Sau đó, Google Play sẽ truy ngược các dấu vết ngăn xếp xuất đến từ các sự cố do người dùng báo cáo, nhờ đó bạn có thể xem xét các dấu vết này trong Play Console. Để tìm hiểu thêm thông tin, vui lòng xem bài viết trên Trung tâm trợ giúp về cách gỡ rối mã nguồn cho các dấu vết ngăn xếp sự cố.

Khắc phục sự cố bằng R8

Phần này sẽ mô tả một số chiến lược để khắc phục sự cố khi bật tính năng rút gọn, làm rối và tối ưu hoá mã bằng R8. Nếu không tìm thấy giải pháp cho vấn đề của mình bên dưới, bạn hãy đọc trang Câu hỏi thường gặp về R8hướng dẫn khắc phục sự cố của ProGuard.

Tạo báo cáo về mã đã xoá (hoặc được giữ lại)

Để khắc phục một số vấn đề liên quan đến R8, bạn có thể xem báo cáo về toàn bộ mã mà R8 đã xoá khỏi ứng dụng của mình. Hãy thêm -printusage <output-dir>/usage.txt vào tệp các quy tắc tuỳ chỉnh cho mỗi mô-đun bạn muốn xuất hiện trong báo cáo này. Khi bật R8 và tạo ứng dụng, R8 sẽ xuất ra một báo cáo có đường dẫn và tên tệp như bạn đã chỉ định. Báo cáo về mã đã xoá sẽ có dạng như sau:

androidx.drawerlayout.R$attr
androidx.vectordrawable.R
androidx.appcompat.app.AppCompatDelegateImpl
    public void setSupportActionBar(androidx.appcompat.widget.Toolbar)
    public boolean hasWindowFeature(int)
    public void setHandleNativeActionModesEnabled(boolean)
    android.view.ViewGroup getSubDecor()
    public void setLocalNightMode(int)
    final androidx.appcompat.app.AppCompatDelegateImpl$AutoNightModeManager getAutoNightModeManager()
    public final androidx.appcompat.app.ActionBarDrawerToggle$Delegate getDrawerToggleDelegate()
    private static final boolean DEBUG
    private static final java.lang.String KEY_LOCAL_NIGHT_MODE
    static final java.lang.String EXCEPTION_HANDLER_MESSAGE_SUFFIX
...

Thay vào đó, nếu muốn xem báo cáo về các điểm truy cập được R8 xác định từ các quy tắc lưu giữ của dự án, bạn hãy thêm -printseeds <output-dir>/seeds.txt vào tệp quy tắc tuỳ chỉnh của mình. Khi bật R8 và tạo ứng dụng, R8 sẽ xuất ra một báo cáo có đường dẫn và tên tệp như bạn đã chỉ định. Báo cáo về các điểm truy cập được lưu giữ sẽ giống như sau:

com.example.myapplication.MainActivity
androidx.appcompat.R$layout: int abc_action_menu_item_layout
androidx.appcompat.R$attr: int activityChooserViewStyle
androidx.appcompat.R$styleable: int MenuItem_android_id
androidx.appcompat.R$styleable: int[] CoordinatorLayout_Layout
androidx.lifecycle.FullLifecycleObserverAdapter
...

Khắc phục sự cố liên quan đến việc rút gọn tài nguyên

Khi bạn rút gọn các tài nguyên, cửa sổ Build (Tạo) sẽ trình bày bản tóm tắt về các tài nguyên đã bị xoá khỏi ứng dụng. (Trước tiên, bạn cần phải nhấp vào Toggle view (Chuyển đổi chế độ xem) ở phía bên trái của cửa sổ để hiển thị văn bản đầu ra chi tiết từ Gradle.) Ví dụ:

:android:shrinkDebugResources
Removed unused resources: Binary resource data reduced from 2570KB to 1711KB: Removed 33%
:android:validateDebugSigning

Gradle cũng tạo một tệp chẩn đoán có tên resources.txt trong <module-name>/build/outputs/mapping/release/ (chung thư mục với các tệp đầu ra của ProGuard). Tệp này bao gồm chi tiết như tài nguyên nào tham chiếu các tài nguyên khác và tài nguyên nào được sử dụng hoặc bị xoá.

Ví dụ: để tìm hiểu tại sao @drawable/ic_plus_anim_016 vẫn còn trong ứng dụng, bạn hãy mở tệp resources.txt và tìm kiếm tên tệp đó. Bạn có thể thấy rằng tệp được tham chiếu từ một tài nguyên khác, như thể hiện bên dưới:

16:25:48.005 [QUIET] [system.out] &#64;drawable/add_schedule_fab_icon_anim : reachable=true
16:25:48.009 [QUIET] [system.out]     &#64;drawable/ic_plus_anim_016

Bây giờ, bạn cần biết tại sao có thể tiếp cận được @drawable/add_schedule_fab_icon_anim và nếu tìm kiếm ở phía trên, bạn sẽ thấy tài nguyên đó được liệt kê trong mục "Tài nguyên có thể tiếp cận ở mức cao nhất (root) là:". Điều này có nghĩa rằng có một tham chiếu mã tới add_schedule_fab_icon_anim (tức là mã nhận dạng tài nguyên có thể vẽ được tìm thấy trong mã có thể tiếp cận).

Nếu bạn không sử dụng tuỳ chọn kiểm tra nghiêm ngặt thì các mã nhận dạng tài nguyên sẽ được đánh dấu là có thể tiếp cận được nếu chứa các hằng số chuỗi có thể dùng để tạo tên tài nguyên cho các tài nguyên được tải động. Trong trường hợp đó, nếu tìm kiếm kết quả bản dựng cho tên tài nguyên, bạn có thể thấy thông báo như sau:

10:32:50.590 [QUIET] [system.out] Marking drawable:ic_plus_anim_016:2130837506
    used because it format-string matches string pool constant ic_plus_anim_%1$d.

Nếu thấy một trong những chuỗi này và chắc chắn rằng chuỗi đó không được dùng để tải động tài nguyên đã cho, thì bạn có thể sử dụng thuộc tính tools:discard để thông báo cho hệ thống xây dựng xoá chuỗi đó như mô tả trong phần hướng dẫn cách tuỳ chỉnh tài nguyên cần giữ lại.