Tối ưu hoá mã và tài nguyên nền tảng bằng R8

Hệ thống tạo nền tảng Android (Soong) chạy trình biên dịch R8 trên các mục tiêu biên dịch mã byte DEX, bao gồm cả ứng dụng Android (android_app), các bài kiểm thử (android_test) và các thư viện Java có thể cài đặt bằng compile_dex: true (chẳng hạn như services.jar), để rút gọn, tối ưu hoá và loại bỏ mã cũng như tài nguyên không dùng đến. Trên các thư viện tĩnh (android_library và java_library tĩnh), Soong không chạy R8 trực tiếp mà sử dụng khối thuộc tính optimize để đính kèm và truyền các quy tắc giữ lại người dùng (export_proguard_flags_files: true) đến các mục tiêu DEX hạ lưu liên kết tĩnh chúng.

Đối với các kỹ sư nền tảng xây dựng hình ảnh hệ thống hoặc gói nhà cung cấp, việc loại bỏ các quy tắc giữ lại quá rộng mang lại lợi ích trực tiếp cho tình trạng hệ thống:

  • Dấu vết phân vùng hệ thống nhỏ hơn: R8 xoá các lớp, phương thức và tài nguyên không dùng đến trước khi đóng gói APK và JAR vào /system, /system_ext, /product và /vendor.
  • Cấu phần phần mềm được biên dịch có kích thước nhỏ hơn: Ít phương thức DEX hơn có nghĩa là các tệp .odex và .vdex do dex2oat tạo ra có kích thước nhỏ hơn trong thời gian tạo hoặc biên dịch trên thiết bị.
  • Giảm áp lực bộ nhớ trong thời gian chạy: Vì Android ánh xạ .odex, .vdex và các bảng tài nguyên APK vào bộ nhớ quy trình bằng cách sử dụng các lệnh gọi mmap được phân trang theo yêu cầu, các tệp nhị phân nhỏ hơn sẽ giảm các lỗi trang chính trong quá trình khởi động ứng dụng và giảm bộ nhớ mã và tài nguyên thường trú trên các quy trình. Để biết thêm thông tin về cách mã ứng dụng đã biên dịch ảnh hưởng đến bộ nhớ thiết bị, hãy xem bài viết Mã ứng dụng là bộ nhớ.
  • Tối ưu hoá toàn bộ chương trình hiệu quả hơn: Các ràng buộc giữ lại hẹp cho phép R8 nội tuyến các phương thức, huỷ ảo hoá giao diện và các lệnh gọi ảo, cắt tỉa các trường không dùng đến và truyền hằng số trên các lớp.

Định cấu hình chế độ tối ưu hoá R8 trong Soong

Trong các tệp Android.bp, hãy định cấu hình chế độ tối ưu hoá R8 bằng cách sử dụng khối thuộc tính optimize trên android_app và các mục tiêu java_library có thể cài đặt (hoặc trên android_library và các mô-đun java_library tĩnh để xuất các quy tắc giữ lại của người dùng):

android_app {
    name: "MySystemApp",
    srcs: ["src/**/*.java"],
    optimize: {
        obfuscate: true,
        shrink_resources: true,
    },
}

Bảng sau đây tóm tắt các thuộc tính optimize phổ biến nhất trong Soong (được xác định trong build/soong/java/dex.go):

Thuộc tính Mô tả
enabled Kiểm soát xem R8 có chạy trên mục tiêu hay không. Mặc định là true cho tất cả các mục tiêu android_app.
shrink Kiểm soát việc loại bỏ mã không dùng đến để xoá các lớp, trường và phương thức không thể truy cập. Mặc định là true cho các mục tiêu android_app (false cho các mô-đun kiểm thử và java_library độc lập, truyền -dontshrink trừ phi được đặt rõ ràng).
optimize Kiểm soát các hoạt động tối ưu hoá mã byte, chẳng hạn như chèn cùng dòng phương thức, hợp nhất lớp, truyền hằng số và xoá nhánh không hoạt động. Mặc định là true cho các mục tiêu android_app (do cờ bản phát hành RELEASE_R8_OPTIMIZE_BY_DEFAULT kiểm soát).
obfuscate Kiểm soát việc rút gọn và đổi tên giá trị nhận dạng. Mặc định là false để tương thích với các phiên bản trước, nhưng hãy đặt thành true bất cứ khi nào có thể để giảm kích thước DEX và mở khoá các chế độ tối ưu hoá sâu hơn (xem phần Bật tính năng làm rối mã nguồn bất cứ khi nào có thể).
shrink_resources Xoá các tài nguyên không dùng đến (các mục res/) khỏi APK được đóng gói sau khi rút gọn mã. Giá trị mặc định là false.
optimized_shrink_resources Chạy quy trình rút gọn mã và tài nguyên R8 tích hợp để R8 theo dõi đồng thời các tham chiếu mã và tài nguyên trong một lần truyền. Khi bạn đặt shrink_resources: true, giá trị mặc định sẽ là RELEASE_USE_OPTIMIZED_RESOURCE_SHRINKING_BY_DEFAULT (true trong các bản dựng nền tảng tiêu chuẩn).
proguard_flags_files Liệt kê các tệp .flags hoặc .pro dành riêng cho mô-đun có chứa các quy tắc tuỳ chỉnh để giữ lại. Tránh thêm các tệp tuỳ chỉnh khi chú thích hoặc giá trị mặc định tiêu chuẩn là đủ.
export_proguard_flags_files Truyền proguard_flags_files của thư viện này đến các mô-đun hạ lưu phụ thuộc vào thư viện này một cách tĩnh.
trace_references_from Liệt kê các mục tiêu đồng hành của thư viện Java mà mã byte tham chiếu vào mục tiêu này. R8 sẽ tự động theo dõi và giữ lại.

Bật tính năng làm rối mã nguồn bất cứ khi nào có thể

Trong Soong, obfuscate mặc định là false để tương thích với các phiên bản trước, nhưng bạn nên đặt obfuscate: true một cách rõ ràng trên android_app và các mục tiêu DEX độc lập bất cứ khi nào có thể:

  • Dấu vết .vdex và DEX nhỏ hơn: Việc đổi tên các gói, lớp, trường và phương thức thành các giá trị nhận dạng ngắn (a, b) sẽ thu hẹp nhóm chuỗi DEX (string_ids và string_data_item) cũng như các giá trị mô tả kiểu. Vì Android ánh xạ các tệp .vdex và .odex vào bộ nhớ xử lý, nên các bảng ký hiệu nhỏ hơn sẽ trực tiếp giảm cả kích thước phân vùng hệ thống và mức sử dụng bộ nhớ thời gian chạy.
  • Tối ưu hoá toàn bộ chương trình sâu hơn: Cho phép R8 đổi tên các giá trị nhận dạng sẽ mở khoá tính năng hợp nhất lớp, làm phẳng gói và loại bỏ các lớp trùng lặp trên các gói mà R8 phải bỏ qua để tránh xung đột tên.
  • Thay thế bằng biểu tượng toàn bộ dấu vết ngăn xếp: Việc bật tính năng làm rối mã nguồn trong Soong không làm giảm khả năng gỡ lỗi. Đối với mọi mục tiêu được R8 biên dịch, Soong sẽ phát ra một tệp ánh xạ proguard_dictionary trong thư mục trung gian của mô-đun, kết hợp tất cả các từ điển mô-đun vào cấu phần phần mềm proguard-dict.zip của bản dựng, nhúng một hàm băm ánh xạ (--map-id-template) vào tiêu đề DEX và ghi lại các thuộc tính lớp SourceFile (--source-file-template) để retrace có thể tự động biểu thị các dấu vết ngăn xếp.

Chỉ giữ lại obfuscate: false (hoặc giữ lại rõ ràng tên API công khai) cho:

  • Thư viện dùng chung trên bootclasspath hoặc đường dẫn lớp system_server: Các mô-đun (java_library hoặc java_sdk_library) hiển thị một bề mặt API bên ngoài được các mô-đun khác liên kết động trong thời gian chạy (chẳng hạn như các mục tiêu framework.jar, services.jar hoặc <uses-library>) phải giữ obfuscate: false hoặc giữ lại rõ ràng bề mặt API của chúng bằng các quy tắc @KeepForApi hoặc -keep (cùng với protect_api_surface: true cho các mục tiêu bootclasspath) để những đối tượng gọi được biên dịch dựa trên các phần giữ chỗ của chúng có thể phân giải tên lớp và tên thành viên trong thời gian chạy.

Trước khi bật obfuscate: true trên một ứng dụng, hãy xác minh các điều kiện tiên quyết sau đây để di chuyển:

  • Các phần phụ thuộc APK kiểm thử bên ngoài: Nếu một tệp APK android_test bên ngoài đặt instrumentation_for: "MyApp" và trực tiếp gọi các lớp hoặc phương thức nội bộ của MyApp, thì việc đổi tên các biểu tượng đó sẽ gây ra NoSuchMethodError hoặc NoClassDefFoundError tại thời gian chạy kiểm thử. Bạn nên cấu trúc bài kiểm thử dưới dạng một APK tự đo lường liên kết MyApp.impl một cách tĩnh, chú thích các lệnh gọi kiểm thử bằng @VisibleForTesting hoặc định cấu hình trace_references_from (xem Bước 2: Di chuyển các quy tắc giữ dựa trên kiểm thử) để các biểu tượng nội bộ cần thiết cho các bài kiểm thử được giữ lại trong khi làm rối phần còn lại của ứng dụng.
  • Phản chiếu dựa trên chuỗi và JNI: Nếu một mô-đun tra cứu các lớp, phương thức hoặc trường theo tên chuỗi chữ (Class.forName, getDeclaredMethod hoặc JNI FindClass và GetMethodID) mà không có quy tắc hoặc chú giải giữ lại, thì quá trình làm rối mã nguồn sẽ đổi tên các mục tiêu đó và làm gián đoạn quá trình tra cứu thời gian chạy. (Xin lưu ý rằng hoạt động phản chiếu không được chú thích cũng sẽ không thành công trong shrink: true và optimize: true.) Chú thích những điểm truy cập đó bằng Hướng dẫn về cách giữ lại chú thích (@UsesReflection, @UsedByReflection hoặc @UsedByNative) để R8 giữ lại tên của các điểm truy cập đó và làm rối mã nguồn phần còn lại của mô-đun.

Tuân thủ nguyên tắc không có cờ tuỳ chỉnh

Mô-đun nền tảng Android lý tưởng không có tệp proguard.flags hoặc keep.xml tuỳ chỉnh. Trong bản dựng nền tảng Android, phần lớn các quy tắc giữ lại tuỳ chỉnh đều dư thừa:

  • Đường cơ sở của nền tảng và AAPT2: Hầu hết các điểm truy cập đều được tự động giữ lại theo đường cơ sở của nền tảng chung (chẳng hạn như @Keep, các phương thức JNI native và @VisibleForTesting) hoặc do AAPT2 tạo từ AndroidManifest.xml và tài nguyên bố cục (xem phần Tìm hiểu các quy tắc lưu giữ mặc định của nền tảng).
  • Các lựa chọn thay thế được nhắm đến: Trong trường hợp phải giữ lại các điểm truy cập, hãy ưu tiên chú giải tại vị trí khai báo (keepanno, @VisibleForTesting), các quy tắc thư viện đã xuất (export_proguard_flags_files: true) hoặc liên kết kiểm thử tĩnh qua các tệp .flags riêng biệt (xem phần Kiểm tra và di chuyển các quy tắc giữ lại hiện có).

Trước khi thêm hoặc giữ lại tệp proguard.flags hoặc keep.xml tuỳ chỉnh, hãy xác minh xem quy tắc đó đã được xử lý bằng các đường cơ sở mặc định hay có thể di chuyển sang chú thích mã hay không.

Tìm hiểu các quy tắc mặc định về việc giữ lại dữ liệu trên nền tảng

Soong tự động truyền các quy tắc cơ sở toàn cầu sau đây đến mọi lệnh gọi R8 trên các mục tiêu biên dịch DEX (android_app, android_test và các mô-đun java_library có thể cài đặt), được định cấu hình trong build/soong/java/dex.go:

  • build/make/core/proguard.flags: Giữ lại các lớp và thành viên được chú thích bằng @com.android.internal.annotations.VisibleForTesting trên tất cả các gói, đồng thời giữ lại @VisibleForTesting (androidx.annotation.VisibleForTesting hoặc com.google.common.annotations.VisibleForTesting) trong các gói android.**, com.android.** và com.google.android.**. Cũng giữ lại @TestApi, @Keep (androidx.annotation, android.support.annotation, com.android.internal.annotations), @KeepForWeakReference, @WeaklyReferencedCallback và @dalvik.annotation.optimization.**.
  • build/make/core/proguard_basic_keeps.flags: Giữ lại các thuộc tính SourceFile cho dấu vết ngăn xếp, chú thích hiển thị thời gian chạy (RuntimeVisible*Annotations), các thuộc tính Exceptions và AnnotationDefault, các phương thức native, các thành viên Serializable, các phương thức @JavascriptInterface, các hàm khởi tạo Throwable(String), các trường Parcelable$CREATOR và các trường protobuf MessageLite.
  • build/make/core/proguard/kotlin.flags: Tắt các cảnh báo vô hại cho chú thích meta cụ thể của Kotlin (kotlin.Metadata và kotlin.annotation.{AnnotationRetention,AnnotationTarget,Retention,Target}) và loại bỏ chú thích DebugMetadata của Kotlin trong các bản dựng phát hành.
  • build/make/core/proguard/checknotnull.flags: Thay thế các lệnh gọi trợ giúp kiểm tra giá trị rỗng phổ biến (com.google.common.base.Preconditions.checkNotNull và dagger.internal.Preconditions.checkNotNull*) bằng các lệnh kiểm tra giá trị rỗng mã byte ngắn gọn. Tệp này cố ý bỏ qua Objects.requireNonNull để giữ lại các thông báo ngoại lệ rõ ràng trên các ranh giới API của khung.
  • build/make/core/proguard/enumvalues.flags: Giữ lại các phương thức values và valueOf trên các loại enum, trừ phi bị vô hiệu hoá theo cấu hình bản dựng.
  • Các quy tắc do AAPT2 tự động tạo: AAPT2 kiểm tra các tệp XML AndroidManifest.xml, bố cục XML và tệp XML ưu tiên đã hợp nhất để tạo các quy tắc giữ chính xác cho mọi Activity, Service, BroadcastReceiver, ContentProvider, BackupAgent, Application, lớp con View tuỳ chỉnh, lớp con Preference và phương thức android:onClick XML đã đăng ký.

Kiểm tra và di chuyển các quy tắc giữ lại hiện có

Khi kiểm tra các tệp proguard.flags hoặc keep.xml hiện có trong kho lưu trữ nền tảng, hãy đánh giá từng quy tắc theo thứ tự dựa trên hệ thống phân cấp gồm 4 bước sau:

Bước 1: Xoá các quy tắc dư thừa hoặc lỗi thời

Xoá các quy tắc đã được bao gồm trong các đường cơ sở toàn cầu, các quy tắc về bố cục và tệp kê khai AAPT2 hoặc các giá trị mặc định của R8, cũng như các quy tắc tham chiếu đến các lớp hoặc gói không còn tồn tại.

Nếu tất cả các quy tắc trong tệp .flags đều dư thừa, hãy xoá tệp đó và xoá proguard_flags_files khỏi Android.bp. Nếu khối optimize còn lại chỉ nêu lại các chế độ cài đặt mặc định, hãy xoá khối dư thừa và định dạng tệp bản dựng bằng bpfmt -w Android.bp. Khi dọn dẹp một gói, hãy kiểm tra xem các mô-đun đi kèm trong thư mục con (chẳng hạn như các biến thể mục tiêu Kotlin) có tham chiếu đến cùng một tệp cờ hay không và cập nhật chúng cùng nhau.

Bước 2: Di chuyển các quy tắc giữ lại dựa trên kiểm thử

Di chuyển các quy tắc giữ dựa trên kiểm thử ra khỏi các tệp .flags tuỳ chỉnh của phiên bản phát hành công khai bằng một trong các mẫu sau:

  • Liên kết thư viện triển khai vào các kiểm thử (static_libs): Khi android_test thực hiện các chi tiết triển khai nội bộ hoặc riêng tư theo gói của một ứng dụng, cấu trúc nền tảng được đề xuất là đặt các tệp nguồn ứng dụng trong android_library (MyApp.impl) và liên kết tĩnh MyApp.impl vào cả MyApp và MyAppTests:

    android_library {
        name: "MyApp.impl",
        srcs: ["src/**/*.java"],
        manifest: "AndroidManifest.xml",
    }
    
    android_app {
        name: "MyApp",
        static_libs: ["MyApp.impl"],
        optimize: {
            obfuscate: true,
            shrink_resources: true,
        },
    }
    
    android_test {
        name: "MyAppTests",
        srcs: ["tests/**/*.java"],
        static_libs: ["MyApp.impl"],
    }
    

    Mẫu 3 mô-đun này cho phép MyAppTests chạy dưới dạng một thử nghiệm tự đo lường với quyền truy cập đầy đủ vào các lớp nội bộ, cho phép MyApp bật obfuscate: true một cách tự do mà không làm hỏng các thử nghiệm, tránh vận chuyển các điểm truy cập chỉ dành cho thử nghiệm trong APK phát hành công khai và loại bỏ việc tạo lại MyApp khi mã kiểm thử thay đổi.

  • Chú thích các lệnh gọi kiểm thử bằng @VisibleForTesting: Nếu một APK kiểm thử bên ngoài gọi một số ít phương thức hoặc hàm khởi tạo nội bộ trên mục tiêu android_app và việc tái cấu trúc thành thư viện .impl là không thực tế, hãy chú thích những khai báo đó bằng @VisibleForTesting. Đường cơ sở build/make/core/proguard.flags toàn cầu sẽ tự động giữ lại @VisibleForTesting mục trong các gói android.**, com.android.** và com.google.android.** mà không cần các tệp .flags tuỳ chỉnh. (Đối với các mô-đun của nhà cung cấp bên ngoài các không gian tên này, hãy sử dụng @UsedByReflection hoặc các quy tắc thư viện đã xuất.)

  • Sử dụng trace_references_from trên các ranh giới thư viện trên nền tảng: Khi các hoạt động kiểm thử không thể liên kết tĩnh việc triển khai đích (ví dụ: các hoạt động kiểm thử thực thi các dịch vụ máy chủ hệ thống hoặc các tệp JAR nền tảng như framework-connectivity), hãy định cấu hình trace_references_from trên mô-đun đích trỏ đến một mô-đun đồng hành thư viện Java chứa các nguồn kiểm thử. R8 theo dõi và giữ lại tất cả các lớp và thành phần mà mã byte đồng hành tham chiếu.

Bước 3: Xuất quy tắc từ thư viện sở hữu

Nếu java_library hoặc android_library dùng chung yêu cầu các quy tắc giữ lại cho hoạt động phản chiếu nội bộ hoặc lệnh gọi lại JNI của riêng nó, hãy xác định các quy tắc trên mục tiêu thư viện và đặt export_proguard_flags_files: true:

java_library {
    name: "my-shared-library",
    srcs: ["src/**/*.java"],
    optimize: {
        proguard_flags_files: ["proguard.flags"],
        export_proguard_flags_files: true,
    },
}

Tất cả các mục tiêu android_app và thư viện ở hạ nguồn liên kết tĩnh my-shared-library đều tự động kế thừa những quy tắc đó, vì vậy, các ứng dụng ở hạ nguồn không cần sao chép các quy tắc đó. Khi nhiều mô-đun trên các thư mục khác nhau phải dùng chung một bộ quy tắc không liên kết với một thư viện mã duy nhất, hãy xuất bản tệp .flags thông qua một filegroup rõ ràng trong Android.bp để các mô-đun có thể tham chiếu :my-shared-flags một cách rõ ràng trên các ranh giới gói. Để biết hướng dẫn chung về cách tạo quy tắc giữ lại của bên sử dụng thư viện mà không hạn chế việc tối ưu hoá ứng dụng hạ nguồn, hãy xem phần Tối ưu hoá cho tác giả thư viện.

Bước 4: Chú thích các khai báo trong mã nguồn

Khi một lớp, phương thức, hàm khởi tạo hoặc trường được truy cập thông qua tính năng phản chiếu hoặc JNI và không được AAPT2 hoặc các đường cơ sở toàn cầu bao gồm, hãy thay thế các quy tắc -keep riêng biệt trong tệp .flags bằng chú giải nguồn trong Hướng dẫn về chú giải giữ lại (xem tài liệu tham khảo keepanno Javadoc):

  • @UsesReflection: Ưu tiên chú thích này khi bạn kiểm soát mã thực hiện hoạt động phản chiếu. Đặt chú thích này tại vị trí gọi phản chiếu để khai báo những lớp, phương thức hoặc trường đích được truy cập một cách linh động. Vì @UsesReflection tự động mã hoá một điều kiện tiên quyết rằng bản thân vị trí gọi được chú thích có thể truy cập được, nên R8 sẽ cắt tỉa cả phương thức gọi và đích phản chiếu nếu vị trí gọi không được dùng.
  • @UsedByReflection: Đặt trên các lớp, phương thức, trường hoặc hàm khởi tạo được mã hoặc thư viện bên ngoài khởi tạo hoặc gọi một cách phản chiếu, chẳng hạn như các lớp được tải bằng Class.forName từ các khoá Bundle hoặc Settings, hoặc các lớp trình bổ trợ được tải trên các trình tải lớp động. Chỉ định kind (chẳng hạn như KeepItemKind.CLASS_AND_METHODS), preconditions (@KeepCondition) và các ràng buộc tham số để R8 chỉ giữ lại hợp đồng phản chiếu chính xác và vẫn có thể tối ưu hoá hoặc cắt tỉa các thành phần không dùng đến của lớp. Đừng thêm @UsedByReflection vào các thành phần đã đăng ký trong tệp kê khai như JobService hoặc BroadcastReceiver, vì AAPT2 đã giữ các thành phần này.
  • @UsedByNative: Đặt trên các phương thức hoặc trường được truy cập từ mã JNI C hoặc C++ bằng cách sử dụng GetMethodID, GetStaticMethodID hoặc GetFieldID.
  • @KeepForApi: Đặt trên các lớp hoặc thành phần API của thư viện phải giữ nguyên khi bản thân thư viện bị thu nhỏ trước khi phân phối.
  • @Keep (androidx.annotation.Keep): Sử dụng làm phương án dự phòng khi keepanno không áp dụng. Việc áp dụng @Keep cho một lớp sẽ giữ lại lớp và tất cả các thành phần của lớp đó vô điều kiện. Việc áp dụng @Keep cho một phương thức hoặc trường sẽ hoạt động như một điểm truy cập vô điều kiện, giữ lại thành phần và lớp chứa của thành phần đó ngay cả khi lớp không bao giờ được khởi tạo, trong khi keepanno thể hiện khả năng tiếp cận có điều kiện.

Cách sử dụng chú thích keepanno R8 trong một mô-đun Soong:

  1. Thêm "keepanno-annotations" vào libs trong Android.bp:

    libs: [
        "keepanno-annotations",
    ],
    
  2. Nhập com.android.tools.r8.keepanno.annotations.* và chú thích khai báo hoặc vị trí gọi trong mã nguồn Java hoặc Kotlin:

    import com.android.tools.r8.keepanno.annotations.KeepItemKind;
    import com.android.tools.r8.keepanno.annotations.UsedByReflection;
    
    public final class CustomPluginController {
        @UsedByReflection(
            description = "Instantiated via Class.forName from plugin config",
            kind = KeepItemKind.CLASS_AND_METHODS)
        public CustomPluginController(Context context) {
            // ...
        }
    }
    

    R8 dịch chú thích keepanno trực tiếp thành mô hình quy tắc giữ lại nội bộ và loại bỏ chú thích khỏi đầu ra DEX cuối cùng, đồng thời không làm tăng chi phí mã byte thời gian chạy.

Tránh các lỗi thường gặp

Các phần sau đây mô tả những lỗi thường gặp về proguard.flags và keep.xml trong mã nền tảng cũng như cách khắc phục các lỗi đó.

Quy tắc giữ lại thành phần chung

# Don't do this:
-keep class * extends android.app.Activity
-keep class * extends android.app.Service
-keep class * extends android.content.BroadcastReceiver
  • Tại sao quy tắc này ảnh hưởng đến hiệu suất: Quy tắc này hoàn toàn dư thừa với AAPT2. Vì sử dụng ký tự đại diện mà không kiểm tra xem thành phần có được đăng ký trong AndroidManifest.xml đã hợp nhất cuối cùng hay không, nên nó buộc R8 giữ lại mọi lớp con Activity, Service hoặc BroadcastReceiver được tìm thấy ở bất kỳ đâu trên đường dẫn lớp, bao gồm cả các thành phần thư viện không dùng đến và các hoạt động gỡ lỗi bị vô hiệu hoá.
  • Cách khắc phục được đề xuất: Xoá quy tắc. AAPT2 kiểm tra tệp kê khai sáp nhập và tạo các quy tắc -keep chính xác cho các thành phần đã đăng ký.

Quy tắc giữ trình xử lý lượt nhấp XML mở rộng

# Don't do this:
-keepclassmembers class * {
    public void *(android.view.View);
}
  • Lý do gây ảnh hưởng đến hiệu suất: Việc giữ lại mọi phương thức public void *(View) trên mọi lớp trong mô-đun sẽ ngăn R8 loại bỏ hoặc nội tuyến bất kỳ phương thức nào chấp nhận một tham số View duy nhất.
  • Cách khắc phục được đề xuất: Xoá quy tắc. AAPT2 quét các tệp XML bố cục và tạo các quy tắc giữ mục tiêu cho các phương thức được tham chiếu theo các thuộc tính android:onClick.

Các quy tắc giữ lại phương thức getter và phương thức setter Broad View

# Don't do this:
-keep public class * extends android.view.View {
    public <init>(android.content.Context);
    public <init>(android.content.Context, android.util.AttributeSet);
    public <init>(android.content.Context, android.util.AttributeSet, int);
    public void set*(...);
    public *** get*();
}
  • Lý do gây ảnh hưởng đến hiệu suất: Quy tắc này buộc R8 phải giữ lại mọi phương thức getter và setter trên mọi lớp con View trong ứng dụng và tất cả các thư viện được liên kết (bao gồm cả thư viện AndroidX và Material), chặn việc xoá phương thức không dùng đến và nội tuyến trên mã giao diện người dùng.
  • Cách khắc phục được đề xuất: Xoá quy tắc. AAPT2 đã giữ lại các hàm khởi tạo cho các lớp View tuỳ chỉnh được tăng cường từ các tệp XML bố cục. Nếu mã tạo ảnh động cho một thuộc tính của thành phần hiển thị bằng cách sử dụng tên chuỗi phản chiếu, chẳng hạn như ObjectAnimator.ofFloat(view, "translationZ", ...), hãy thay thế tên chuỗi bằng một tham chiếu thuộc tính được nhập (View.TRANSLATION_Z hoặc một cách triển khai FloatProperty hoặc IntProperty tuỳ chỉnh) để tránh hoàn toàn việc phản chiếu. Nếu không thể tránh truy cập vào thuộc tính phản chiếu, hãy chú thích phương thức getter hoặc setter cụ thể bằng @UsedByReflection.

Tính năng tối ưu hoá hoặc làm rối mã nguồn toàn bộ sẽ bị vô hiệu hoá

# Don't do this in proguard.flags:
-dontoptimize
-dontshrink
-dontobfuscate
  • Lý do khiến hiệu suất bị ảnh hưởng: Việc đặt -dontoptimize hoặc -dontshrink bên trong tệp .flags sẽ âm thầm ghi đè các chế độ cài đặt Android.bp của mô-đun và tắt các lượt tối ưu hoá trên toàn bộ mục tiêu. Tệ hơn nữa, nếu một thư viện xuất tệp .flags chứa -dontoptimize, thì thư viện đó sẽ tắt tính năng tối ưu hoá R8 cho mọi android_app ở hạ lưu liên kết thư viện.
  • Cách khắc phục được đề xuất: Xoá -dontoptimize, -dontshrink và -dontobfuscate khỏi tệp .flags. Kiểm soát rõ ràng hành vi tối ưu hoá trong Android.bp bằng cách sử dụng khối optimize (shrink, optimize, obfuscate) trên mục tiêu lá.

Cờ chẩn đoán trong các quy tắc đã cam kết

# Don't do this in proguard.flags:
-verbose
-printmapping proguard.map
-printusage usage.txt
-printconfiguration config.txt
  • Lý do gây ảnh hưởng đến bản dựng: Cờ chẩn đoán tràn nhật ký bản dựng hoặc cố gắng ghi tệp đầu ra vào các đường dẫn cục bộ trong quá trình tạo bản dựng Soong có hộp cát.
  • Cách khắc phục được đề xuất: Xoá các cờ này khỏi các tệp .flags đã cam kết. Soong tự động ghi đầu ra về việc sử dụng và liên kết R8, chẳng hạn như proguard_dictionary và proguard_usage.zip, vào thư mục trung gian của mô-đun trong out/soong/.intermediates/.

Các quy tắc giữ lại trong bản phát hành chính cho mã kiểm thử

  • Lý do khiến hiệu suất bị ảnh hưởng: Việc chỉ thêm các quy tắc -keep tuỳ chỉnh cho quyền truy cập kiểm thử buộc mã đó phải duy trì trạng thái không bị làm rối mã nguồn và được giữ lại trong các bản dựng phát hành công khai. Mặc dù việc chú thích các phương thức bằng @VisibleForTesting hoặc định cấu hình trace_references_from giúp tránh duy trì các quy tắc -keep theo cách thủ công, nhưng những biểu tượng đó vẫn được gửi trong tệp nhị phân sản xuất.
  • Giải pháp được đề xuất: Để loại bỏ hoàn toàn các điểm truy cập chỉ dành cho kiểm thử khỏi APK phát hành chính thức, hãy cấu trúc ứng dụng thành một thư viện .impl và liên kết thư viện đó vào một APK kiểm thử tự đo lường bằng static_libs, như mô tả trong Bước 2: Di chuyển các quy tắc giữ dựa trên kiểm thử.

Ký tự đại diện chung chung dùng để khớp với mọi tài nguyên trong keep.xml

<!-- Don't do this in res/raw/keep.xml: -->
<resources xmlns:tools="http://schemas.android.com/tools"
    tools:keep="@raw/*,@drawable/*,@string/*" />
  • Tại sao điều này ảnh hưởng đến hiệu suất: Ký tự đại diện bao quát giữ lại mọi tài nguyên thuộc loại phù hợp trong mô-đun và các phần phụ thuộc của mô-đun, làm mất tác dụng của việc giảm kích thước tài nguyên (shrink_resources: true) và tăng kích thước của tệp APK và bảng resources.arsc được ánh xạ.
  • Cách khắc phục được đề xuất:
    • Ưu tiên các tham chiếu tài nguyên tĩnh hơn keep.xml: Tránh sử dụng phương thức Resources.getIdentifier để tra cứu một tập hợp tài nguyên có giới hạn một cách linh động, chẳng hạn như các chuỗi thử nghiệm được đánh số hoặc các đối tượng có thể vẽ theo chủ đề. Thay vào đó, hãy sử dụng câu lệnh switch tại thời gian biên dịch hoặc ánh xạ qua các hằng số tĩnh R.id, R.string hoặc R.drawable. Các tham chiếu tĩnh cho phép R8 và AAPT2 theo dõi chính xác các tài nguyên đang hoạt động, loại bỏ chi phí tra cứu chuỗi thời gian chạy và loại bỏ hoàn toàn nhu cầu về keep.xml.
    • Liệt kê tên tài nguyên cụ thể: Nếu cần tra cứu động, chẳng hạn như bằng trình đọc giấy phép của bên thứ ba, hãy liệt kê chính xác giá trị nhận dạng tài nguyên trong tools:keep (ví dụ: tools:keep="@raw/third_party_licenses").
    • Xoá các tệp keep.xml dư thừa: Nếu các tài nguyên được liệt kê đã được tham chiếu tĩnh trong mã (R.raw.foo) hoặc XML (@raw/foo), trình rút gọn sẽ tự động giữ lại các tài nguyên đó, vì vậy, bạn có thể xoá res/raw/keep.xml.

Xác minh và kiểm tra các thay đổi về quy tắc

Bất cứ khi nào bạn xoá hoặc thu hẹp các quy tắc giữ lại trong proguard.flags hoặc keep.xml, hãy xác minh rằng mô-đun được tạo một cách rõ ràng, vượt qua các kiểm thử đơn vị và kiểm thử đo lường, đồng thời giữ lại tất cả các điểm truy cập bắt buộc.

Tạo và kiểm thử các mô-đun

  1. Tạo mô-đun một cách rõ ràng để xác minh rằng R8 và AAPT2 hoàn tất mà không có cảnh báo tham chiếu bị thiếu:

    m <MODULE_NAME>
    
  2. Chạy bài kiểm thử đơn vị và bài kiểm thử đo lường cho mô-đun bằng cách sử dụng atest:

    atest <TEST_MODULE_NAME>
    
  3. Đối với các ứng dụng hệ thống, dịch vụ đặc quyền hoặc mô-đun phụ thuộc vào phần cứng, hãy chạy các kiểm thử đo lường và kiểm thử giao diện người dùng trên các thiết bị thực mục tiêu hoặc trong phòng kiểm thử thiết bị để thực hiện các đường dẫn phản chiếu thời gian chạy, các liên kết IPC và việc tăng tài nguyên.

Kiểm tra các khác biệt về DEX và tài nguyên

So sánh APK hoặc JAR đã biên dịch trước và sau khi thay đổi quy tắc để xác nhận rằng R8 xoá mã và tài nguyên không dùng đến mà không loại bỏ các điểm truy cập dự kiến:

  • Sử dụng apkanalyzer hoặc dexdump để kiểm tra các lớp, phương thức và trường được giữ lại trong tệp APK hoặc DEX đầu ra:

    apkanalyzer dex packages $OUT/system/priv-app/<APP_NAME>/<APP_NAME>.apk
    
  • Khi sửa đổi keep.xml hoặc bật shrink_resources, hãy sử dụng aapt2 dump resources để xác minh rằng bản dựng loại bỏ các tài nguyên không dùng đến và giữ lại các tài nguyên bắt buộc:

    aapt2 dump resources $OUT/system/priv-app/<APP_NAME>/<APP_NAME>.apk
    

Phân tích bán kính giữ lại và quy tắc bao hàm bằng R8

Trình biên dịch R8 mã nguồn mở có một trình phân tích Keep Radius (cũng được hiển thị trong Android Studio và Gradle dưới dạng Trình phân tích cấu hình R8) để đo lường chính xác mức tác động của từng quy tắc giữ lại trong quá trình biên dịch. Soong tích hợp trình phân tích này trực tiếp vào bản dựng nền tảng Android (được định cấu hình trong build/soong/java/dex.go).

Cách phân tích các quy tắc giữ lại cho một mô-đun nền tảng hoặc trên toàn bộ bản dựng:

  1. Chạy bản dựng bằng biến môi trường R8_DUMP_KEEP_RADIUS=true:

    R8_DUMP_KEEP_RADIUS=true m <MODULE_NAME>
    

    Khi R8_DUMP_KEEP_RADIUS=true được thiết lập, Soong sẽ hướng dẫn R8 ghi lại các chỉ số quy tắc giữ lại trong tệp r8keepradius.pb trung gian cho từng mô-đun đã biên dịch trong out/soong/.intermediates/. Đối với mỗi quy tắc giữ lại và chú thích keepanno, R8 sẽ ghi lại:

    • Bán kính giữ lại tức thì: Các lớp, trường và phương thức chính xác được quy tắc giữ lại, cùng với các điều kiện ràng buộc cụ thể mà quy tắc đó thực thi đối với việc thu nhỏ, tối ưu hoá hoặc làm rối mã nguồn.
    • Quy tắc bao hàm: Những quy tắc lưu giữ hoặc chú thích khác đã lưu giữ cùng các mục. Nếu một quy tắc tuỳ chỉnh hoàn toàn được bao hàm bởi một quy tắc AAPT2 hoặc một quy tắc cơ sở toàn cầu, thì bạn có thể xoá quy tắc đó một cách an toàn.
    • Quy tắc trên toàn gói và quy tắc chung: Quy tắc sử dụng ký tự đại diện trên toàn gói hoặc áp dụng chỉ thị cấu hình chung.
  2. Chuyển đổi đầu ra r8keepradius.pb thành báo cáo HTML tương tác bằng cách sử dụng KeepRadiusHtmlReportGenerator (được gói trong prebuilts/r8/r8.jar):

    # Generate an HTML report for a single module
    INTERMEDIATES=out/soong/.intermediates/packages/apps/<APP_NAME>/<APP_NAME>
    java -cp prebuilts/r8/r8.jar \
        com.android.tools.r8.keepradius.KeepRadiusHtmlReportGenerator \
        $INTERMEDIATES/android_common/r8keepradius.pb \
        keep_radius_report.html
    
    # Scan all built modules under out/soong/.intermediates and generate
    # per-module HTML reports plus an aggregate keepradius.html summary
    java -cp prebuilts/r8/r8.jar \
        com.android.tools.r8.keepradius.KeepRadiusHtmlReportGenerator \
        out/soong/.intermediates \
        out/keep_radius_reports
    

    Khi được cung cấp một thư mục, KeepRadiusHtmlReportGenerator sẽ duyệt qua tất cả các tệp *keepradius*.pb, tạo báo cáo HTML cho từng mô-đun và tạo out/keep_radius_reports/keepradius.html tóm tắt số lượng mục trực tiếp, số lượng mục được giữ lại và các quy tắc giữ lại có bán kính lớn nhất trong bản dựng. Để biết thêm thông tin về cách diễn giải điểm số rút gọn, tối ưu hoá và làm rối mã nguồn trong báo cáo đã tạo, hãy xem phần Sử dụng Trình phân tích cấu hình R8.

Soong cũng hỗ trợ 2 biến môi trường chẩn đoán R8 bổ sung trong build/soong/java/dex.go:

  • R8_DUMP_INPUT=true: Ghi r8inputs.zip vào thư mục trung gian của mô-đun chứa tất cả các JAR đầu vào, JAR thư viện và cấu hình ProGuard đã hợp nhất để tái tạo R8 độc lập.
  • R8_DUMP_PERFETTO_TRACE=true: Ghi r8trace.ptrace vào thư mục trung gian của mô-đun để kiểm tra các lượt biên dịch R8 trong Perfetto.

Xác thực các quy tắc dành cho người dùng thư viện

Khi java_library hoặc android_library xuất các quy tắc giữ lại cho người dùng hạ nguồn (export_proguard_flags_files: true), những quy tắc đó không được bao gồm các cờ chung làm thay đổi hoặc vô hiệu hoá hoạt động tối ưu hoá cho ứng dụng sử dụng.

R8 cung cấp một trình phân tích quy tắc giữ nguồn mở (ProcessKeepRules), được hiển thị trong bản dựng nền tảng Android dưới dạng công cụ lưu trữ process-keep-rules (được xác định trong prebuilts/r8/Android.bp):

# Build the R8 keep rules validator host binary
m process-keep-rules

# Validate one or more ProGuard configuration files
out/host/linux-x86/bin/process-keep-rules <PROGUARD_FLAGS_PATH>

Công cụ process-keep-rules sẽ phân tích cú pháp từng tệp cấu hình và gặp lỗi với thông tin chẩn đoán về tệp và dòng nếu gặp phải các chỉ thị không được phép trong các quy tắc của người dùng thư viện. Các chỉ thị không được phép bao gồm tối ưu hoá toàn cục, thu hẹp hoặc vô hiệu hoá việc làm rối mã nguồn (chẳng hạn như -dontoptimize hoặc -dontshrink), đóng gói lại và các cờ sửa đổi quyền truy cập, cờ chẩn đoán hoặc cờ ánh xạ, và -keepattributes ở cấp ứng dụng.

Kiểm tra cây nguồn bằng pgaudit.py

Để kiểm tra các thư mục nguồn chưa được tạo cùng với trình phân tích thời gian tạo của R8, cây nền tảng Android sẽ bao gồm tập lệnh pgaudit.py trong build/make/core/proguard/tools/. Công cụ phân tích tĩnh này quét các tệp Android.bp, proguard.flags và keep.xml trong một kho lưu trữ để gắn cờ các quy tắc đã được AAPT2 hoặc đường cơ sở nền tảng toàn cầu, ký tự đại diện rộng, các chế độ ghi đè -dontoptimize và các quy tắc chỉ giữ lại để kiểm thử bao gồm:

# Audit a single package directory
./build/make/core/proguard/tools/pgaudit.py packages/apps/Provision/

# Scan a subsystem tree and write a structured JSON report
./build/make/core/proguard/tools/pgaudit.py packages/ --json=audit_report.json

Các nhóm Nền tảng và OEM có thể kết hợp pgaudit.py để phân loại nhanh cây nguồn, process-keep-rules để xác thực các quy tắc thư viện đã xuất và R8_DUMP_KEEP_RADIUS=true với KeepRadiusHtmlReportGenerator để đo bán kính lưu giữ chính xác của lớp, trường và phương thức của mọi quy tắc trong bản dựng.