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 yêu cầu nhà phát triển thiết lập thêm, hoặc bạn phải ghi lại rằng 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 các thư viện đã xuất bản, nhưng cũng có thể hữu ích cho nhà phát triển các mô-đun thư viện nội bộ trong một ứng dụng lớn, theo mô-đun.

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 phần 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 để sử dụng, hãy xem phần Chọn thư viện một cách khôn ngoan.

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

Có 2 loại quy tắc giữ 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 thư viện 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 một ứng dụng khách xác định, thì các quy tắc này cần mô tả mã 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 của ứ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à tự động được sử dụ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 của người tiêu 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 những tệp này nếu quyết định tối ưu hoá một phần thư viện tại thời gian tạo. Chúng phải ngăn API công khai của thư viện bị xoá, nếu không API công khai sẽ không có trong bản phân phối thư viện, tức 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 tối ưu hoá

Cấu hình R8 trong các 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ề việc 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 (lỗi Ứng dụng không phản hồi). Các thư viện phải tránh vi phạm những yêu cầu sau đây để 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 trên diện rộng hoặc trên toàn gói: Thư viện của bạn không được có các quy tắc giữ lại trên diện 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ữ diện rộng có thể giải quyết sự cố trong ngắn hạn, nhưng chúng sẽ làm tăng kích thước ứng dụng của tất cả các ứng dụng sử dụng thư viện của bạn.

    Đừng thêm các 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 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. 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 codegen, 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ỳ sửa đổi lớn nào sau thời gian biên dịch, trình tối ưu hoá sẽ biết mã nào 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ã sẽ đượ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. Việc này có thể sẽ xoá mã được dùng linh hoạt 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ì reflection. Hãy xem KSP để biết một điểm truy cập chung, được Room, Dagger2 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 sẽ không gặp sự cố khi bật chế độ đầy đủ của R8. Chế độ đầy đủ của R8 là chế độ nên dùng để sử dụng 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, giải pháp là xác định điểm truy cập phản chiếu hoặc JNI cụ thể và thêm một quy tắc nhắm đến 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ề việc tối ưu hoá, bạn nên làm thêm những việc sau đây.

  • Đừng sử dụng -repackageclasses trong tệp quy tắc giữ nguyên của người 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ữ lại 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 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ữ lại 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 việc phản ánh là phù hợp

Nếu phải sử dụng tính năng phản chiếu, bạn chỉ nên phản chiếu vào một trong hai trường hợp sau:

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

Việc sử dụng tính nă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 ghi các quy tắc giữ lại người dùng được nhắm đến.

Dạng 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 trong cả khung Android (ví dụ: khi tăng cường các hoạt động, khung hiển thị và đối tượng có thể vẽ) và các thư viện AndroidX (ví dụ: khi tạo WorkManager ListenableWorkers hoặc RoomDatabases). Ngược lại, hoạt động 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ề các hoạt động tối ưu hoá của R8: Trái với quan điểm phổ biến, các hoạt độ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 các 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ề hoạt độ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: Lỗi thường gặp là bỏ qua một thư viện khỏi quá trình tối ưu hoá, 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 AAR (Android Archive) hoặc JAR (Java Archive). Các hoạt động tối ưu hoá trong thời gian tạo thư viện bị hạn chế và ứng dụng của bạn không được vô hiệu hoá hoạt độ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ề lựa chọn -keep Quy tắc -keep ngăn R8 chạy bất kỳ lượt tối ưu hoá nào. Để biết thêm thông tin, hãy xem bài viết Chọn chế độ giữ lại phù hợp.

Đị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.

Thư viện AAR

Để thêm các quy tắc dành cho người dùng cho một thư viện AAR, hãy dùng lựa 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 ở <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 gói ở đúng vị trí trong tệp JAR đầu ra.

Xác minh rằng các quy tắc gói JAR cuối cùng hoạt động chính xác bằng cách kiểm tra xem các quy tắc có nằm trong thư mục META-INF/proguard hay không.

Tối ưu hoá quy trình tạo 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 trong thời gian tạo bản dựng thư viện rất hạn chế. Là một nhà phát triển thư viện, bạn cần suy nghĩ về nhiều giai đoạn tối ưu hoá và duy trì hành vi, cả ở thời gian tạo 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 gian tạo bản 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"
        }
    }
}

Xin lưu ý rằng hành vi của proguardFiles khác biệt rất nhiều so với consumerProguardFiles:

  • proguardFiles được dùng trong thời gian tạo bản dựng, thường là 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 tạo thư viện. Tối thiểu, đây là API công khai của bạn.
  • 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ương pháp phản chiếu để tạo các lớp nội bộ, thì bạn có thể cần xác định các quy tắc giữ lại trong cả proguardFilesconsumerProguardFiles.

Nếu bạn sử dụng -repackageclasses trong bản dựng thư viện, hãy đóng gói lại các lớp vào một gói con bên trong gói 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 những 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 đến, 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 một AAR hoặc trong thư mục META-INF/com.android.tools của một 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ưới dạng r8-from-<X>-upto-<Y> để cho biết phiên bản R8 mà các quy tắc được viết cho. 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 mọi tên tệp và đuôi tệp.

Xin lưu ý rằng phần -from-<X>-upto-<Y> là không bắt buộc, phiên bản <Y>độ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.0r8-from-8.2.0 là tên thư mục đại diện cho một nhóm quy tắc R8 được nhắm đến. 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 cho đến phiên bản 8.2.0 (không bao gồm).

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).