Bỏ qua để đến nội dung

Kiểm thử, phân tích và gỡ lỗi tối ưu R8

Sau khi bật tối ưu app, bạn cần kiểm tra app vẫn chạy đúng, đo xem tối ưu có hiệu quả không, và biết cách lần ra nguyên nhân khi có lỗi. Bài này gom các công cụ cho từng việc đó.

Sau khi bật tối ưu, hãy xác nhận app vẫn hoạt động như mong muốn.

Kiểm thử ở máy local:

  • Đo mức cải thiện hiệu năng bằng benchmark: benchmark app trước và sau khi bật tối ưu.
  • Kiểm thử các hành trình người dùng quan trọng (critical user journey, CUJ): đảm bảo mọi CUJ chạy đúng, ví dụ người dùng đăng nhập được và làm được các việc quan trọng khác. Để kiểm thử bản build release, dùng UI Automator.

Kiểm thử trên production:

  • Theo dõi chỉ số hiệu năng app: dùng Android vitals trên Play Console và qua Google Play Developer Reporting API.
  • Phát hành bản cập nhật theo staged rollout: nếu keep rule áp dụng cho code ít khi chạy, rất khó kiểm thử ở local. Dùng staged rollout để thử thay đổi với một nhóm người dùng sớm. Để ý các crash mới xuất hiện có thể do keep rule. Cách nhận ra crash do R8 xem mục Sửa lỗi sau khi tối ưu bên dưới.

Vì tối ưu của R8 thay đổi code của app, bạn cần kiểm thử hành vi app thật kỹ. Nếu app chạy không như mong muốn, tập trung vào hai tình huống:

  • Tối ưu quá tay làm app crash: R8 tối ưu quá nhiều code.
  • Tối ưu không rõ ràng hoặc chưa đủ: R8 không tối ưu app nhiều như bạn mong đợi, hoặc bạn cần giải thích thêm về các tối ưu.

Nếu app crash sau khi tối ưu bằng R8, nguyên nhân thường là reflection bị hỏng. Các dấu hiệu nhận ra:

  • Bạn thấy một exception cho biết class, method hoặc field được dùng qua reflection. Thường là một trong: ClassNotFoundException, NoSuchMethodException, NoSuchFieldException, NoClassDefFoundError, NoSuchMethodError, NoSuchFieldError.
  • Bạn thấy code dùng reflection với import kotlin.reflect.* hoặc import java.lang.reflect.*.
  • Bạn thấy constructor của class được dùng kiểu Something::class.constructors.
  • Bạn thấy Class.forName(...).

Cách sửa: thêm keep rule.

Nếu chính code của bạn thực hiện reflection, bạn có thể gắn annotation Jetpack Keep thay vì thêm keep rule. Lint check của Jetpack Keep cũng báo các lời gọi reflection khác chưa có annotation, giúp bạn tránh các crash tương tự.

Vì rule được áp dụng từ cả app lẫn các library đi kèm, bạn có thể cần biết rõ hơn rule nào đang được áp dụng, hoặc vì sao R8 giữ lại một phần code mà bạn tưởng sẽ được tối ưu.

  • Không rõ rule nào được áp dụng. Rule từ library cũng áp dụng cho app, và global option từ library cũng lan sang app, nên bạn có thể không chắc rule nào đang có hiệu lực.

    Cách xử lý: xem báo cáo tất cả rule mà R8 áp dụng khi build, ở ./app/build/outputs/mapping/configuration.txt. Báo cáo này chứa mọi rule đã gộp từ mọi library và module dùng để cấu hình R8, và dùng được để tìm các rule kém hiệu quả.

  • Giữ quá nhiều code. R8 có thể giữ lại code mà bạn nghĩ sẽ bị xoá.

    Cách xử lý: dùng option -whyareyoukeeping để biết vì sao code được giữ. Output cho một đường đi từ code được giữ tới một entry point của app. Xem mục -whyareyoukeeping bên dưới và tài liệu của Guardsquare.

  • Khó đọc stack trace gốc. R8 thay đổi code theo nhiều cách khiến stack trace không còn khớp với code gốc, ví dụ số dòng và tên class, method bị đổi.

    Cách xử lý: từ Android Studio Otter 3 Feature Drop và AGP 9.0, Logcat tự deobfuscate stack trace. Nếu dùng Android Studio cũ hơn, bạn phải tự khôi phục stack trace gốc bằng công cụ dòng lệnh retrace, đi kèm gói command-line tools.

    Để dùng retrace, truyền cho lệnh đường dẫn tới mapping file và file stack trace. Mapping file tên là mapping.txt, được tự đóng gói kèm Android App Bundle (AAB). Xem thêm tài liệu retrace và bài trên Play Console Help Center về deobfuscate crash stack trace. Khi dùng Play và Firebase Crashlytics, dùng mapping.txt với các crash mà dịch vụ thu thập từ người dùng. Lệnh sau chạy retrace từ thư mục gốc của project:

    Terminal window
    $ANDROID_HOME/cmdline-tools/latest/bin/retrace app/build/outputs/mapping/$releaseVariant/mapping.txt trace.txt

Nếu không tự giải quyết được vấn đề với R8, hãy báo lỗi cho Google.

Ngoài keep rule, các loại rule khác và global option, bạn có thể dùng một số rule riêng để gỡ lỗi tối ưu.

Rule -checkdiscard kiểm tra R8 đã thực sự loại bỏ một class hoặc member mà bạn mong là bị xoá chưa. Nếu class hoặc member được chỉ định không bị loại bỏ, build thất bại.

Cú pháp:

-checkdiscard <class_specification>

Ở ví dụ sau, build thất bại nếu field userId hoặc method setLabel() của class com.example.models.User còn được giữ:

-checkdiscard class com.example.models.User{
private java.lang.String userId;
public void setLabel(java.lang.String);
}

Code của class vẫn có thể còn trong app nếu method của nó đã được inline vào class khác. Để chắc chắn code bị xoá hoàn toàn chứ không chỉ được inline, thêm một rule ngăn R8 tối ưu class đích, bằng cách kết hợp -checkdiscard với rule -keep,allowshrinking. Rule này chặn các tối ưu như class merging và inline. Nếu -checkdiscard qua được, không còn chút nội dung nào của các class khớp trong app đã tối ưu.

# Hoặc giữ hoặc xoá class, không đổi tên hay tối ưu kiểu khác
-keep,allowshrinking class com.example.foo { *; }
# Kiểm tra class cùng mọi field và method của nó đã bị xoá.
-checkdiscard class com.example.foo

Dùng -whyareyoukeeping để biết vì sao R8 giữ một class, field hoặc method trong bản build. Một phần tử có thể được giữ vì nhiều lý do, nhưng rule này chỉ cho một lý do: đường đi ngắn nhất tới phần tử từ một phần tử được giữ. Nếu bạn bỏ đường đi đó khỏi code, phần tử có thể vẫn được giữ vì một lý do khác.

Các lý do có thể là:

  • Một keep rule: từ app, từ library được dùng, hoặc rule do AAPT (Android Asset Packaging Tool) sinh ra. Lưu ý keep rule do AAPT sinh ra sẽ không có khi bật optimized resource shrinking.
  • Tham chiếu bắc cầu từ code hoặc resource được giữ: nếu code hoặc XML (như layout) được R8 giữ, mọi thứ chúng tham chiếu tĩnh cũng được giữ.

Cú pháp:

-whyareyoukeeping <class_specification>

Ví dụ:

-whyareyoukeeping class com.example.foo.MainActivity {
private void setLabel(...);
}

Output được in ra console.

Nếu không có rule nào giữ setLabel(), output là:

com.example.foo.MainActivity
|- is referenced in keep rule:
| /app/build/intermediates/aapt_proguard_file/release/processReleaseResources/aapt_rules.txt:4:1
Nothing is keeping void com.example.foo.MainActivity.setLabel()

Nếu có keep rule nhắm vào setLabel(), output tương tự:

com.example.foo.MainActivity
|- is referenced in keep rule:
| /app/proguard-rules.pro:23:1
void com.example.foo.MainActivity.setLabel()
|- is referenced in keep rule:
| /app/proguard-rules.pro:23:1

R8 Configuration Analyzer là công cụ giúp bạn tận dụng tối đa lợi ích hiệu năng của R8 bằng cách cho thấy chi tiết chất lượng cấu hình của app. Bạn theo dõi và cải thiện tối ưu qua các chỉ số chính, cụ thể là điểm shrinking, optimization và obfuscation, cho biết bao nhiêu phần trăm codebase được phép tối ưu. Bằng cách tìm ra các keep rule quá rộng hoặc thừa, kể cả rule đến từ library bên thứ ba, công cụ giúp bạn tinh chỉnh cấu hình để R8 tối ưu được càng nhiều class, field và method càng tốt.

Từ AGP 9.3.0, bạn tạo báo cáo bằng một Gradle task riêng hoặc để nó tự sinh trong lúc build R8.

Chạy Gradle task riêng (nên dùng ở local). Khi đang chỉnh keep rule liên tục, dùng task riêng để nhanh chóng xem tác động của thay đổi mà không phải build đầy đủ APK hay Bundle:

Terminal window
./gradlew :app:analyzeReleaseR8Config

Vì task bỏ qua bước tạo APK hay Bundle, vòng phản hồi ngắn hơn nhiều. Bạn phân tích nhanh keep rule ảnh hưởng tới điểm shrinking, optimization và obfuscation thế nào rồi chỉnh ngay. Báo cáo HTML nằm ở app/build/reports/r8/r8-config-analyzer-release.html.

Tự sinh trong lúc build thường. Khi chạy build release đầy đủ có R8 (như assembleRelease), báo cáo tự được tạo ở build/outputs/mapping/release/configanalyzer.html. Để tắt việc tự sinh trong build thường, đặt Gradle property:

android.experimental.r8.enableR8ConfigurationAnalyzer=false

Với AGP 9.2 trở xuống. Cập nhật bản R8 ở local lên mới nhất bằng cách thêm nó làm classpath dependency trong settings.gradle hoặc settings.gradle.kts:

pluginManagement {
repositories {
google()
mavenCentral()
}
buildscript {
dependencies {
// Cập nhật lên phiên bản R8 mới hơn.
classpath("com.android.tools:r8:9.4.14")
}
}
}

Cách này dùng R8 mới nhất kèm configuration analyzer mới nhất, và có thể chỉ làm tạm khi phân tích cấu hình R8.

Sau đó đặt system property com.android.tools.r8.dumpkeepradiushtmltodirectory khi chạy một Gradle task có build bằng R8:

Terminal window
./gradlew assembleRelease \
-Dcom.android.tools.r8.dumpkeepradiushtmltodirectory=<output_directory>

Ví dụ tạo báo cáo HTML trong thư mục /tmp/r8analysis:

Terminal window
# Tạo thư mục /tmp/r8analysis.
mkdir -p /tmp/r8analysis
# Tạo báo cáo trong thư mục /tmp/r8analysis.
./gradlew assembleRelease \
-Dcom.android.tools.r8.dumpkeepradiushtmltodirectory=/tmp/r8analysis

Báo cáo cho thấy cấu hình R8 của app và tác động của từng keep rule. Dùng các điểm sau để biết bao nhiêu phần codebase R8 được phép tối ưu.

Phần tóm tắt của báo cáo R8 Configuration Analyzer, hiển thị điểm shrinking, optimization và obfuscation

Hình 1. Ví dụ phần tóm tắt của báo cáo.

Điểm shrinking. Khi shrink app, R8 giảm kích thước tổng bằng cách tìm và xoá code, resource không dùng. Điểm shrinking là phần trăm class, field và method được phép shrink. Ví dụ điểm 66% nghĩa là R8 shrink được 66% codebase.

Điểm optimization. R8 tối ưu như inline method và class merging, giúp app khởi động nhanh hơn và dùng ít bộ nhớ hơn. Điểm optimization là phần trăm class, field và method được phép tối ưu. Ví dụ điểm 66% nghĩa là R8 chỉ tối ưu được 66% codebase.

Điểm obfuscation. Bằng cách đổi tên class, field và method thành tên ngắn hơn, R8 giảm lượng metadata của app để tiết kiệm bộ nhớ. Điểm obfuscation là phần trăm code được phép obfuscate.

Để tăng điểm và mở khoá tối ưu tốt hơn, hãy tinh chỉnh keep rule sao cho chúng không chặn R8 tối ưu một cách không cần thiết. Bạn chỉ nên giữ class, method hoặc field được truy cập bằng reflection. Dùng phần Keep Rule Analysis cho việc này.

Phần Keep Rule Analysis của báo cáo, liệt kê từng keep rule và phần trăm code bị rule đó chặn tối ưu

Hình 2. Ví dụ phần phân tích keep rule.

Bấm vào một rule để mở màn hình phân tích chi tiết.

Màn hình chi tiết của một keep rule, cho biết các class, field, method bị rule chặn tối ưu và loại tối ưu bị chặn

Hình 3. Ví dụ phân tích chi tiết một keep rule.

Các bước tinh chỉnh keep rule để mở khoá hết tiềm năng tối ưu của R8:

  1. Với mỗi keep rule, xem trong Configuration Analyzer phần trăm class, field và method mà R8 không tối ưu được. Dùng thông tin này để tìm những keep rule chặn tối ưu trên nhiều class, field hoặc method. Báo cáo cũng liệt kê các thuộc tính tối ưu mà mỗi rule chặn.
  2. Nếu thấy một rule chặn tối ưu trên rất nhiều class, hãy xem nó chặn những class, field, method nào để biết rule có đang giữ những thứ không hề được gọi động qua reflection không.
  3. Giảm phần tối ưu bị chặn bằng cách chỉ nhắm vào class, field hoặc method thật sự cần, qua việc chọn đúng keep option và làm theo best practice.
  4. Tìm hiểu và chạy các test bao phủ class, field, method bị ảnh hưởng bởi keep rule, rồi tinh chỉnh rule.

Library bên thứ ba thường có consumer keep rule riêng để chạy được với R8. Vì tác giả library không đoán trước được cách bạn dùng, đôi khi họ viết rule thận trọng, phạm vi rộng, chặn tối ưu trên nhiều class, field, method hơn cần thiết. Điều này có thể khiến R8 không tối ưu được những phần app chẳng liên quan gì tới việc library thực sự chạy. Configuration Analyzer giúp bạn tìm các library đưa vào rule ảnh hưởng xấu tới tối ưu của app.

Dùng analyzer để xem tác động tổng hợp của mọi consumer keep rule đã gộp. Phân tích tác động của từng rule đến từ library bên thứ ba giúp bạn lần ra library nào đang chặn nhiều tối ưu trong app.

Để xử lý keep rule do library bên thứ ba đưa vào:

  • Nếu library có rule quá rộng, nên liên hệ người duy trì library kèm dữ liệu từ báo cáo để cho thấy rule hiện tại ảnh hưởng tới điểm tối ưu của app thế nào. Với library bên ngoài, tìm xem đã có bug tương tự chưa trước khi tạo issue mới.
  • Nếu cần, bạn có thể thử mức cải thiện bằng cách lọc rule của một library cụ thể: nhập rule của library vào project, loại các rule rộng, rồi chạy lại analyzer để đo mức lợi về kích thước và hiệu năng.

Có lúc nhiều keep rule chồng lên nhau và một trong số đó chặn nhiều tối ưu hơn cần thiết. Ví dụ codebase có hai keep rule:

# Chặn tối ưu trên cả package
# Xoá rule này để cải thiện tối ưu
-keep class com.example.package.** { *; }
# Chặn tối ưu trên một class trong package
-keep class com.example.package.Myclass

Rule đầu chặn tối ưu trên cả package nên bao trùm rule thứ hai, vốn chỉ nhắm vào một class nằm trong package đó. Khi rule chồng nhau, một rule có thể chặn nhiều tối ưu hơn cần thiết. Tinh chỉnh các rule chồng nhau giúp tối đa hoá tối ưu của R8 và dọn bớt nợ kỹ thuật: cấu hình gọn lại, chỉ giữ code thật sự cần, và mở khoá hết khả năng tối ưu của R8.

Báo cáo liệt kê các keep rule bị một rule rộng hơn bao trùm

Hình 4. Ví dụ các rule bị bao trùm trong báo cáo.

Các bước tối ưu rule bị bao trùm:

  1. Tìm các rule bao trùm bằng R8 Configuration Analyzer.
  2. Xác định chính xác class, field hoặc method nào trong codebase thật sự dựa vào reflection và cần giữ bằng keep rule. Biết được điều này giúp bạn tinh chỉnh rule.
  3. Dùng analyzer so sánh tác động của từng rule cùng nhắm vào một class, field hoặc method. Có thể dùng phần trăm tối ưu bị chặn của mỗi rule để biết rule nào rộng, rule nào hẹp.
    • Nếu rule hẹp đã viết chính xác (chỉ giữ đúng member hoặc class bị truy cập qua reflection), hãy xoá rule rộng. Như vậy phần còn lại của package được R8 tối ưu một cách an toàn.
    • Nếu rule rộng đang nhắm đúng class, giữ rule rộng và xoá rule hẹp, vì rule hẹp chỉ là thừa. Nhớ tinh chỉnh rule rộng để chỉ nhắm vào các class, field hoặc method bạn đã xác định.

Kiểm tra và kiểm thử thay đổi: chạy lại analyzer để chắc xung đột đã được giải quyết. Sau đó build bản release và kiểm thử để chắc codebase chạy như mong đợi.

Với analyzer, bạn có thể rà soát codebase có hệ thống để tìm và cắt bỏ các keep rule lỗi thời làm rối cấu hình. Analyzer chỉ ra hai nguồn rule thừa chính:

  • Rule không dùng: rule không khớp với class, method hay field nào trong bản build hiện tại. Chúng thường sót lại sau khi refactor code, gỡ dependency, hoặc từ cấu hình chép ở đâu đó không còn phù hợp, làm cấu hình phức tạp vô ích.
  • Rule trùng lặp: các rule nhắm vào cùng class, field, method, hoặc cùng một rule được khai báo lặp lại trong một file hay nhiều file keep rule.

Cả hai loại đều làm cấu hình rối, khó bảo trì và khó debug. Tìm ra chúng giúp bạn dọn sạch cấu hình.

Khi bật tối ưu app, hành vi mặc định với resource khác nhau theo phiên bản:

  • Với DSL mới từ AGP 9.3, resource shrinking được bật mặc định khi bật tối ưu. DSL cũ (bật riêng tối ưu code và resource) vẫn được hỗ trợ.
  • Với AGP dưới 9.3, thiết lập isShrinkResources = true bảo công cụ tối ưu xoá resource không dùng để giảm kích thước app. Resource shrinking chỉ hoạt động cùng code shrinking, nên khi tối ưu resource thì cũng phải đặt isMinifyEnabled = true.

Cấu hình bật tối ưu xem bài R8: Bật tối ưu app.

Nếu muốn giữ hoặc bỏ một số resource cụ thể, tạo một file XML keep trong resource của project, ví dụ res/raw/keep_my_package.xml. File keep gồm:

  • Thẻ <resources>: chứa mọi phần tử resource con và các attribute keep/discard.
  • Attribute tools:keep: nhận danh sách tên resource cần giữ, cách nhau bằng dấu phẩy.
  • Attribute tools:discard: nhận danh sách tên resource cần bỏ, cách nhau bằng dấu phẩy.

Dùng dấu * làm wildcard để nhắc tới nhiều resource trong cùng thư mục:

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

Khai báo resource cần bỏ nghe có vẻ thừa khi bạn có thể xoá chúng, nhưng nó hữu ích khi dùng build variant.

Để chỉ xoá resource ở một số build variant, đặt mọi resource vào thư mục chung của project, rồi tạo một file keep_my_package_build_variant.xml khác nhau trong thư mục resource của từng variant. Trong file keep, tự liệt kê các resource cần xoá khi một resource có vẻ được dùng trong code (nên shrinker không xoá) nhưng bạn biết nó sẽ không được dùng ở variant đó.

Công cụ tối ưu chỉ xoá resource không được code của app tham chiếu, nghĩa là nó sẽ không xoá alternative resource cho các cấu hình thiết bị khác nhau.

Dùng thuộc tính resConfigs của Android Gradle trong build.gradle của app module để xoá các file alternative resource mà app không cần.

Ví dụ, nếu dùng một library có resource ngôn ngữ (như Google Play Services), app sẽ chứa mọi chuỗi đã dịch cho thông báo trong library, dù phần còn lại của app có được dịch sang các ngôn ngữ đó hay không. Để chỉ giữ các ngôn ngữ app chính thức hỗ trợ, khai báo chúng bằng resConfigs. Resource của mọi ngôn ngữ không được khai báo sẽ bị xoá.

Ví dụ giới hạn resource ngôn ngữ chỉ còn tiếng Anh và tiếng Pháp:

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

hoặc

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

Khi phát hành app bằng định dạng Android App Bundle (AAB), mặc định người dùng chỉ tải về các ngôn ngữ được cấu hình trên thiết bị khi cài app. Tương tự, chỉ resource khớp mật độ màn hình và native library khớp ABI của thiết bị mới có trong bản tải về. Xem Re-enable or disable types of configuration APKs.

Với app cũ phát hành bằng APK (tạo trước tháng 8/2021), bạn có thể chọn resource mật độ màn hình hoặc ABI nào đưa vào APK bằng cách build nhiều APK cho các cấu hình thiết bị khác nhau.

Mặc định Android Gradle plugin (AGP) gộp các resource trùng tên, như drawable cùng tên nằm ở các thư mục resource khác nhau. Hành vi này không do shrinkResources điều khiển và không tắt được, vì nó cần để tránh lỗi khi nhiều resource có cùng tên mà code đang tham chiếu.

Việc gộp chỉ xảy ra khi hai file trở lên có cùng tên, cùng loại và cùng qualifier. AGP chọn file mà nó xác định là lựa chọn tốt nhất trong các bản trùng (theo thứ tự ưu tiên bên dưới) và chỉ đưa resource đó cho AAPT để có mặt trong build cuối.

AGP tìm resource trùng ở:

  • Resource chính, thuộc main source set, thường ở src/main/res/
  • Variant overlay, từ build type và build flavor
  • Các library project dependency

AGP gộp resource trùng theo thứ tự ưu tiên tăng dần:

Dependency → Main → Build flavor → Build type

Ví dụ resource trùng có ở cả resource chính lẫn một build flavor thì Gradle chọn resource trong build flavor.

Nếu resource trùng nằm trong cùng một source set, Gradle không gộp được và báo lỗi resource merge. Lỗi này có thể xảy ra nếu bạn khai báo nhiều source set trong thuộc tính sourceSet của build.gradle, ví dụ cả src/main/res/ và src/main/res2/ đều có resource giống nhau.

Khi shrink resource, cửa sổ Build hiện tóm tắt các resource đã bị xoá khỏi app. (Bấm Toggle view ở bên trái cửa sổ để xem output chi tiết dạng chữ từ Gradle.) Ví dụ:

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

Gradle cũng tạo file chẩn đoán resources.txt trong <module-name>/build/outputs/mapping/release/ (cùng thư mục với các file output của ProGuard). File này cho biết những thông tin như resource nào tham chiếu resource nào, resource nào được dùng hay bị xoá.

Ví dụ, để biết vì sao @drawable/ic_plus_anim_016 vẫn còn trong app, mở resources.txt và tìm tên file đó. Có thể bạn sẽ thấy nó được một resource khác tham chiếu:

16:25:48.005 [QUIET] [system.out] @drawable/add_schedule_fab_icon_anim : reachable=true
16:25:48.009 [QUIET] [system.out] @drawable/ic_plus_anim_016

Giờ bạn cần biết vì sao @drawable/add_schedule_fab_icon_anim reachable. Tìm ngược lên trên, bạn sẽ thấy resource này nằm dưới tiêu đề “The root reachable resources are:” trong resources.txt. Nghĩa là có một tham chiếu trong code tới add_schedule_fab_icon_anim, tức ID R.drawable của nó được tìm thấy trong code reachable.

Nếu không dùng strict checking, resource ID có thể bị đánh dấu reachable khi có các hằng chuỗi trông như có thể dùng để ghép tên resource cho việc nạp resource động. Khi đó, tìm tên resource trong output build có thể thấy thông báo như:

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

Nếu thấy một chuỗi như vậy và chắc chắn chuỗi không được dùng để nạp resource đó động, dùng attribute tools:discard trong file keep để bảo build system xoá resource.