Tối ưu hoá cho tác giả thư viện

Là tác giả thư viện, bạn phải đảm bảo rằng nhà phát triển ứng dụng có thể dễ dàng kết hợp thư viện của bạn vào ứng dụng của họ mà vẫn duy trì trải nghiệm chất lượng cao cho người dùng cuối. Điều này có nghĩa là thư viện của bạn phải tương thích với tính năng tối ưu hoá Android (R8) mà không cần nhà phát triển thiết lập thêm, hoặc tài liệu cho biết thư viện có thể không phù hợp để sử dụng trên Android. Điều quan trọng là các thư viện dùng trên Android không được ngăn chặn các hoạt động tối ưu hoá ứng dụng quan trọng và phải tuân thủ các yêu cầu tối ưu hoá bổ sung.

Tài liệu này dành cho nhà phát triển thư viện đã phát hành, nhưng cũng có thể hữu ích cho nhà phát triển mô-đun thư viện nội bộ trong một ứng dụng lớn, được mô-đun hoá.

Nếu bạn là nhà phát triển ứng dụng và muốn tìm hiểu về cách tối ưu hoá ứng dụng Android, hãy xem bài viết Bật tính năng tối ưu hoá ứng dụng. Để tìm hiểu về những thư viện phù hợp, hãy xem phần Chọn thư viện một cách thông minh.

Tìm hiểu các loại quy tắc giữ lại

Có hai loại quy tắc giữ lại riêng biệt mà bạn có thể áp dụng trong thư viện:

  • Các quy tắc giữ lại của bên sử dụng phải chỉ định các quy tắc giữ lại bất cứ nội dung nào mà thư viện phản ánh. Nếu một thư viện sử dụng tính năng phản chiếu hoặc JNI để gọi vào mã của thư viện đó hoặc mã do ứng dụng khách xác định, thì các quy tắc này cần mô tả mã nào cần được giữ lại. Các thư viện nên đóng gói các quy tắc giữ lại của bên sử dụng thư viện. Các quy tắc này sử dụng cùng một định dạng với quy tắc giữ lại ứng dụng. Các quy tắc này được gói vào các cấu phần phần mềm thư viện (AAR hoặc JAR) và được sử dụng tự động trong quá trình tối ưu hoá ứng dụng Android khi thư viện được dùng. Các quy tắc này được duy trì trong tệp được chỉ định bằng thuộc tính consumerProguardFiles trong tệp build.gradle.kts (hoặc build.gradle). Để tìm hiểu thêm, hãy xem bài viết Viết quy tắc giữ lại người dùng.
  • Các quy tắc giữ lại bản dựng thư viện được áp dụng khi thư viện của bạn được tạo. Bạn chỉ cần các tệp này nếu quyết định tối ưu hoá một phần thư viện tại thời điểm xây dựng. Họ phải giữ cho API công khai của thư viện không bị xoá, nếu không, API công khai sẽ không có trong bản phân phối thư viện, nghĩa là nhà phát triển ứng dụng không thể sử dụng thư viện. Các quy tắc này được duy trì trong tệp được chỉ định bằng thuộc tính proguardFiles trong tệp build.gradle.kts (hoặc build.gradle). Để tìm hiểu thêm, hãy xem phần Tối ưu hoá bản dựng thư viện AAR.

Nguyên tắc và yêu cầu đối với việc tối ưu hoá

Cấu hình R8 trong thư viện có tác động chung đến kích thước và hiệu suất nhị phân cuối cùng của ứng dụng sử dụng. Ngoài các phương pháp hay nhất về quy tắc giữ lại, tác giả thư viện phải tuân thủ các yêu cầu cụ thể và cân nhắc các nguyên tắc bổ sung.

Tuân thủ các yêu cầu về tối ưu hoá

Sự thiếu hiệu quả trong các thư viện là một yếu tố chính góp phần làm tăng kích thước ứng dụng, lãng phí bộ nhớ, khởi động chậm và lỗi ANR (Ứng dụng không phản hồi). Thư viện phải tránh vi phạm các yêu cầu sau để không làm giảm đáng kể chất lượng ứng dụng và trải nghiệm người dùng.

  • Không có quy tắc giữ lại rộng hoặc trên toàn gói: Thư viện của bạn không được bao gồm các quy tắc giữ lại rộng, giữ lại hầu hết mã trong thư viện của bạn hoặc trong một thư viện khác. Các quy tắc giữ lại rộng có thể giải quyết sự cố trong thời gian ngắn, nhưng chúng làm tăng kích thước của tất cả ứng dụng sử dụng thư viện của bạn.

    Đừng thêm quy tắc lưu giữ trên toàn gói (chẳng hạn như -keep class com.mylibrary.** {*; }) cho các gói trong thư viện của bạn hoặc các thư viện được tham chiếu khác. Những quy tắc như vậy sẽ giới hạn việc tối ưu hoá cho các gói này trên tất cả những ứng dụng sử dụng thư viện của bạn.

  • Không có quy tắc chung không phù hợp: Đừng bao giờ sử dụng các lựa chọn chung như -dontobfuscate hoặc -allowaccessmodification.

  • Sử dụng codegen thay vì reflection bất cứ khi nào có thể: Khi có thể, hãy sử dụng tạo mã (codegen) thay vì reflection. Cả codegen và reflection đều là những phương pháp phổ biến để tránh mã nguyên mẫu khi lập trình, nhưng codegen tương thích hơn với một trình tối ưu hoá ứng dụng như R8.

    Với tính năng tạo mã, mã sẽ được phân tích và sửa đổi trong quá trình xây dựng. Vì không có bất kỳ nội dung sửa đổi lớn nào sau thời gian biên dịch, nên trình tối ưu hoá biết mã nào là cần thiết và mã nào có thể được xoá một cách an toàn.

    Với tính năng phản chiếu, mã được phân tích và thao tác trong thời gian chạy. Vì mã chưa thực sự hoàn tất cho đến khi thực thi, nên trình tối ưu hoá không biết mã nào có thể được xoá một cách an toàn. Thao tác này có thể sẽ xoá mã được dùng linh động thông qua tính năng phản chiếu trong thời gian chạy, khiến ứng dụng gặp sự cố cho người dùng.

    Nhiều thư viện hiện đại sử dụng codegen thay vì phản chiếu. Hãy xem KSP để biết điểm truy cập phổ biến mà Room, Hilt và nhiều thư viện khác sử dụng.

  • Hỗ trợ chế độ đầy đủ của R8: Thư viện của bạn không được gặp sự cố khi bật chế độ đầy đủ của R8. Chế độ đầy đủ của R8 là chế độ nên dùng cho R8 và là chế độ mặc định kể từ AGP 8.0, được phát hành ổn định vào năm 2023. Nếu thư viện của bạn gặp sự cố trong R8, thì giải pháp là xác định điểm truy cập JNI hoặc phản chiếu cụ thể và thêm quy tắc được nhắm mục tiêu, chứ không phải giữ lại toàn bộ gói.

Đề xuất khác

Ngoài các yêu cầu về tối ưu hoá, sau đây là các đề xuất bổ sung.

  • Đừng sử dụng -repackageclasses trong tệp quy tắc giữ lại của bên sử dụng thư viện. Tuy nhiên, để tối ưu hoá bản dựng thư viện, bạn có thể sử dụng -repackageclasses với tên gói nội bộ, chẳng hạn như <your.library.package>.internal, trong tệp quy tắc giữ bản dựng của thư viện. Điều này có thể cải thiện hiệu quả của thư viện trong các ứng dụng chưa được tối ưu hoá. Tuy nhiên, việc này thường không cần thiết vì các ứng dụng cũng cần được tối ưu hoá.
  • Khai báo mọi thuộc tính bạn cần để thư viện hoạt động trong các tệp quy tắc lưu giữ của thư viện, ngay cả khi có thể có sự trùng lặp với các thuộc tính được xác định trong proguard-android-optimize.txt.
  • Nếu bạn yêu cầu các thuộc tính sau trong quá trình phân phối thư viện, hãy duy trì các thuộc tính đó trong tệp quy tắc giữ lại bản dựng của thư viện và không duy trì trong tệp quy tắc giữ lại người dùng của thư viện:
    • AnnotationDefault
    • EnclosingMethod
    • Exceptions
    • InnerClasses
    • RuntimeInvisibleAnnotations
    • RuntimeInvisibleParameterAnnotations
    • RuntimeInvisibleTypeAnnotations
    • RuntimeVisibleAnnotations
    • RuntimeVisibleParameterAnnotations
    • RuntimeVisibleTypeAnnotations
    • Signature
  • Tác giả thư viện nên giữ thuộc tính RuntimeVisibleAnnotations trong các quy tắc giữ lại của bên sử dụng thư viện nếu chú thích được dùng trong thời gian chạy.
  • Tác giả thư viện không nên sử dụng các lựa chọn chung sau đây trong các quy tắc giữ lại của bên sử dụng thư viện:
    • -include
    • -basedirectory
    • -injars
    • -outjars
    • -libraryjars
    • -repackageclasses
    • -flattenpackagehierarchy
    • -allowaccessmodification
    • -renamesourcefileattribute
    • -ignorewarnings
    • -addconfigurationdebugging
    • -printconfiguration
    • -printmapping
    • -printusage
    • -printseeds
    • -applymapping
    • -obfuscationdictionary
    • -classobfuscationdictionary
    • -packageobfuscationdictionary

Khi nào nên suy ngẫm

Nếu phải sử dụng phản chiếu, bạn chỉ nên phản chiếu vào một trong những mục sau:

  • Các loại được nhắm mục tiêu cụ thể (các lớp con hoặc trình triển khai giao diện cụ thể)
  • Mã sử dụng một chú thích thời gian chạy cụ thể

Việc sử dụng phản chiếu theo cách này sẽ giới hạn chi phí thời gian chạy và cho phép viết quy tắc giữ lại người dùng được nhắm mục tiêu.

Hình thức phản chiếu cụ thể và có mục tiêu này là một mẫu mà bạn có thể thấy trên cả khung Android (ví dụ: khi tăng cường hoạt động, khung hiển thị và đối tượng có thể vẽ) và thư viện AndroidX (ví dụ: khi tạo WorkManager ListenableWorkers hoặc RoomDatabases). Ngược lại, phản chiếu mở của Gson không phù hợp để sử dụng trong các ứng dụng Android.

Quan niệm sai lầm phổ biến

Một số quan niệm sai lầm phổ biến có thể khiến bạn định cấu hình R8 không chính xác. Trong đó bao gồm:

  • Hiểu sai về tính năng tối ưu hoá của R8: Trái với hiểu biết phổ biến, tính năng tối ưu hoá của R8 không chỉ giới hạn ở việc làm rối mã nguồn mà còn bao gồm cả việc rút gọn mã và tối ưu hoá logic bằng kỹ thuật hợp nhất lớp và nội tuyến phương thức. Để biết thêm thông tin, hãy xem bài viết Tổng quan về tính năng tối ưu hoá R8.

  • Bỏ qua việc tối ưu hoá các thư viện bị làm rối mã nguồn: Một lỗi phổ biến là bỏ qua việc tối ưu hoá một thư viện vì thư viện đó đã được tối ưu hoá hoặc làm rối mã nguồn khi được biên dịch thành tệp AAR (Android Archive) hoặc JAR (Java Archive). Các hoạt động tối ưu hoá trong thời gian xây dựng thư viện bị giới hạn và ứng dụng của bạn không nên tắt tính năng tối ưu hoá thư viện bằng cách đưa thư viện vào một quy tắc giữ lại. Để biết thêm thông tin, hãy xem bài viết Tối ưu hoá bản dựng thư viện AAR.

  • Hiểu sai về tuỳ chọn -keep Quy tắc -keep ngăn R8 chạy bất kỳ bước tối ưu hoá nào. Để biết thêm thông tin, hãy xem bài viết Chọn quy tắc giữ lại phù hợp.

Xác thực chất lượng của quy tắc giữ lại của người tiêu dùng

Để xác thực rằng thư viện của bạn không ngăn chặn quá trình tối ưu hoá mã quá mức khi được nhúng trong một ứng dụng, hãy sử dụng mẫu thư viện hoặc ứng dụng kiểm thử tích hợp. Bật R8 và kiểm tra chất lượng cấu hình R8 tổng thể của ứng dụng bằng Trình phân tích cấu hình R8.

Nếu các quy tắc của bên sử dụng thư viện được cho là hạn chế việc tối ưu hoá trên phần lớn mã của thư viện, thì điều này cho thấy các quy tắc của bên sử dụng thư viện cần được tinh chỉnh. Thu hẹp các quy tắc của người dùng và kiểm thử mọi khía cạnh bị ảnh hưởng của thư viện trong một ứng dụng có bật R8.

Định cấu hình việc đóng gói quy tắc

Để đảm bảo các quy tắc lưu giữ người dùng được áp dụng chính xác, bạn phải đóng gói các quy tắc đó một cách thích hợp tuỳ thuộc vào định dạng thư viện của bạn.

Nếu bạn sử dụng Jetpack Keep để khai báo việc sử dụng JNI và phản chiếu của thư viện, thì trình bổ trợ Gradle của thư viện có thể đóng gói các quy tắc đã tạo dưới dạng quy tắc giữ lại của người dùng cho bạn.

Thư viện AAR

Để thêm quy tắc người dùng cho thư viện AAR, hãy sử dụng tuỳ chọn consumerProguardFiles trong tập lệnh bản dựng của mô-đun thư viện Android. Để biết thêm thông tin, hãy xem hướng dẫn của chúng tôi về cách tạo mô-đun thư viện.

Kotlin

android {
    defaultConfig {
        consumerProguardFiles("consumer-proguard-rules.pro")
    }
    ...
}

Groovy

android {
    defaultConfig {
        consumerProguardFiles 'consumer-proguard-rules.pro'
    }
    ...
}

Thư viện JAR

Để gói các quy tắc với thư viện Kotlin hoặc Java được phân phối dưới dạng JAR, hãy đặt tệp quy tắc của bạn vào thư mục META-INF/proguard/ của JAR cuối cùng, với bất kỳ tên tệp nào. Ví dụ: nếu mã của bạn nằm trong <libraryroot>/src/main/kotlin, hãy đặt tệp quy tắc người dùng tại <libraryroot>/src/main/resources/META-INF/proguard/consumer-proguard-rules.pro và các quy tắc sẽ được đóng gói ở đúng vị trí trong tệp JAR đầu ra.

Xác minh rằng JAR cuối cùng gói các quy tắc đúng cách bằng cách kiểm tra để đảm bảo các quy tắc nằm trong thư mục META-INF/proguard.

Tối ưu hoá bản dựng thư viện AAR (nâng cao)

Nhìn chung, bạn không cần tối ưu hoá trực tiếp bản dựng thư viện vì các hoạt động tối ưu hoá có thể thực hiện tại thời gian tạo bản dựng thư viện rất hạn chế. Là nhà phát triển thư viện, bạn cần cân nhắc nhiều giai đoạn tối ưu hoá và duy trì hành vi, cả tại thời điểm xây dựng thư viện và ứng dụng, trước khi tối ưu hoá thư viện đó.

Nếu bạn vẫn muốn tối ưu hoá thư viện tại thời điểm xây dựng, thì Trình bổ trợ Android cho Gradle sẽ hỗ trợ việc này.

Kotlin

android {
    buildTypes {
        release {
            isMinifyEnabled = true
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
        configureEach {
            consumerProguardFiles("consumer-rules.pro")
        }
    }
}

Groovy

android {
    buildTypes {
        release {
            minifyEnabled true
            proguardFiles
                getDefaultProguardFile('proguard-android-optimize.txt'),
                'proguard-rules.pro'
        }
        configureEach {
            consumerProguardFiles "consumer-rules.pro"
        }
    }
}

Lưu ý rằng hành vi của proguardFiles rất khác với consumerProguardFiles:

  • proguardFiles được dùng tại thời điểm xây dựng, thường cùng với getDefaultProguardFile("proguard-android-optimize.txt"), để xác định phần nào của thư viện cần được giữ lại trong quá trình xây dựng thư viện. Tối thiểu, đây là API công khai của bạn.
  • Ngược lại, consumerProguardFiles được đóng gói vào thư viện để ảnh hưởng đến những hoạt động tối ưu hoá diễn ra sau đó, trong quá trình tạo một ứng dụng sử dụng thư viện của bạn.

Ví dụ: nếu thư viện của bạn sử dụng phản chiếu để tạo các lớp nội bộ, thì bạn có thể cần xác định quy tắc giữ lại trong cả proguardFiles và consumerProguardFiles.

Nếu bạn sử dụng -repackageclasses trong bản dựng của thư viện, hãy đóng gói lại các lớp vào một gói phụ bên trong gói của thư viện. Ví dụ: sử dụng -repackageclasses 'com.example.mylibrary.internal' thay vì -repackageclasses 'internal'.

Hỗ trợ nhiều phiên bản R8 (nâng cao)

Bạn có thể điều chỉnh các quy tắc để nhắm đến các phiên bản cụ thể của R8. Điều này giúp thư viện của bạn hoạt động tối ưu trong các dự án sử dụng phiên bản R8 mới hơn, đồng thời cho phép tiếp tục sử dụng các quy tắc hiện có trong các dự án có phiên bản R8 cũ hơn.

Để chỉ định các quy tắc R8 được nhắm mục tiêu, bạn cần đưa các quy tắc đó vào thư mục META-INF/com.android.tools bên trong classes.jar của AAR hoặc trong thư mục META-INF/com.android.tools của JAR.

In an AAR library:
    proguard.txt (legacy location, the file name must be "proguard.txt")
    classes.jar
    └── META-INF
        └── com.android.tools (location of targeted R8 rules)
            ├── r8-from-<X>-upto-<Y>/<R8-rule-files>
            └── ... (more directories with the same name format)

In a JAR library:
    META-INF
    ├── proguard/<ProGuard-rule-files> (legacy location)
    └── com.android.tools (location of targeted R8 rules)
        ├── r8-from-<X>-upto-<Y>/<R8-rule-files>
        └── ... (more directories with the same name format)

Trong thư mục META-INF/com.android.tools, có thể có nhiều thư mục con có tên ở dạng r8-from-<X>-upto-<Y> để cho biết quy tắc được viết cho phiên bản R8 nào. Mỗi thư mục con có thể có một hoặc nhiều tệp chứa các quy tắc R8, với tên tệp và đuôi tệp bất kỳ.

Xin lưu ý rằng các phần -from-<X> và -upto-<Y> là không bắt buộc, phiên bản <Y> là độc quyền và các dải phiên bản thường liên tục nhưng cũng có thể trùng lặp.

Ví dụ: r8, r8-upto-8.0.0, r8-from-8.0.0-upto-8.2.0 và r8-from-8.2.0 là tên thư mục đại diện cho một tập hợp các quy tắc R8 được nhắm mục tiêu. Mọi phiên bản R8 đều có thể sử dụng các quy tắc trong thư mục r8. R8 có thể sử dụng các quy tắc trong thư mục r8-from-8.0.0-upto-8.2.0 từ phiên bản 8.0.0 trở lên nhưng không bao gồm phiên bản 8.2.0.

Trình bổ trợ Android cho Gradle sử dụng thông tin đó để chọn tất cả các quy tắc mà phiên bản R8 hiện tại có thể sử dụng. Nếu một thư viện không chỉ định các quy tắc R8 được nhắm đến, thì trình bổ trợ Android cho Gradle sẽ chọn các quy tắc từ các vị trí cũ (proguard.txt cho AAR hoặc META-INF/proguard/<ProGuard-rule-files> cho JAR).