Xử lý các vấn đề ProGuard trên Android (Phần 1)

Tại sao là ProGuard

ProGuard là một công cụ rút gọn (shrink), tối ưu hoá (optimize) và làm mờ (obfuscate) code. Mặc dù cũng có các công cụ khác cho developer, ProGuard thì có sẵn là một phần của Android Gradle build process và gửi kèm với SDK.

Có nhiều lý do tại sao bạn có thể muốn bật ProGuard khi xây dựng ứng dụng của mình. Một số developer quan tâm đến phần làm rõ code nhiều hơn, nhưng đối với tôi, lợi ích chính là loại bỏ tất cả code không sử dụng mà bạn gửi vào APK của mình như một phần của file classes.dex. Ví dụ biểu đồ phân phối kích thước (size) của một ứng dụng Android:

Có rất nhiều lợi ích hữu hình khi làm cho kích thước và ứng dụng của bạn nhỏ đi, chẳng hạn như tăng sự duy trì và hài lòng của người dùng, thời gian tải xuống và cài đặt nhanh hơn, tiếp cận người dùng trên các thiết bị cấp thấp hơn, đặc biệt là các thị trường mới nổi. Thậm chí có một số trường hợp khi bạn bắt buộc phải giới hạn kích thước ứng dụng, chẳng hạn giới hạn 4MB cho ứng dụng Instant App, thì ProGuard là không thể thiếu.

Nếu không đủ để thuyết phục bạn, hãy xem xét việc loại bỏ code không sử dụng và làm mờ tất cả các tên có hiệu ứng tích luỹ và có thể mở khoá tối ưu hoá hơn nữa:

  • Trên một số version của Android, DEX code được biên dịch thành mã máy (machine code) hoặc ở thời gian cài đặt (install time) hoặc trong thời gian chạy (runtime). Cả DEX code ban đầu và code tối ưu vẫn còn trên thiết bị mọi lúc, vậy bài toán đơn giản là: giảm code bằng thời gian biên dịch ngắn hơn và giảm dung lượng được sử dụng.
  • Một điều mà ProGuard có thể làm, có ảnh hưởng lớn đến kích thước code, là thay đổi tất cả các định danh (package, class and class members) để sử dụng tên ngắn, chẳng hạn như a.Aa.a.B. Quá trình này được gọi là giấu code hay làm mờ code (obfuscation). Giấu code làm giảm kích thước code theo 2 cách: các chuỗi string thực tế đại diện cho các tên (name) ngắn hơn, và hơn thế nữa, chúng có cơ hội sử dụng lại các method và các field khác nhau nếu chúng chia sẻ cùng một chữ ký, làm giảm tổng số các mục trong chuỗi string.
  • Sử dụng ProGuard là điều kiện tiên quyết để kích hoạt thu hẹp tài nguyên (resources shrinker). Thu hẹp tài nguyên sẽ loại bỏ các tài nguyên không được tham chiếu từ code trong project của bạn (chẳng hạn như images, thường là phần lớn nhất của APK).
  • Xoá code cũng có thể giúp bạn tránh khỏi vấn đề 64K dex method. Chỉ cần bằng cách đóng gói các method mà code của bạn thực sự sử dụng vào APK, đặc biệt là khi bạn tính đến các thư viện bên thứ ba, bạn có thể giảm bớt nhu cầu sử dụng Multidex trong ứng dụng của bạn.

Tôi nghĩ rằng mọi ứng dụng Android nên sử dụng code shrinking.

Nhưng trước khi đi vào thực hiện, tiếp tục đọc để tìm hiểu về những điều có thể phá vỡ (break) trong ứng dụng của bạn, đôi khi theo những cách rất tinh tế, khi bạn kích hoạt (enable) ProGuard. Mặc dù một số lỗi sẽ ngăn không cho bạn build APK, nhưng cũng có những lỗi bạn chỉ có thể bắt gặp khi chạy ứng dụng, vì vậy hãy chắc chắn test ứng dụng của bạn.

Cách sử dụng ProGuard

Bật ProGuard trong project của bạn đơn giản chỉ bằng cách thêm các dòng này (nếu chúng chưa có) vào file build.gradle trong module chính của ứng dụng.

buildTypes {
/* you will normally want to enable ProGuard only for your release
builds, as it’s an additional step that makes the build slower and can make debugging more difficult */
  
  release {
    minifyEnabled true
    proguardFiles getDefaultProguardFile(‘proguard-android.txt’), ‘proguard-rules.pro’
  }
}

Cấu hình của ProGuard được thực hiện thông qua một file cấu hình riêng. Mặc định được cung cấp với Android Gradle Plugin là file proguard-android.txt (được lấy trong thư mục Sdk tools Sdk/tools/ProGuard/ProGuard-android.txt, tuy nhiên đối với version Sdk tools mới hơn và Android Gradle plugin 2.2.0+, nó được unzip từ Android plugin jar trong khi build, bạn có thể tìm thấy theo đường dẫn <your_project>/build/intermediates/ProGuard-files/). Sau đó, chúng ta có thể thêm một số tuỳ chọn có liên quan đến dự án của mình vào file proguard-rules.pro.

Bạn có thể tìm thấy hướng dẫn sử dụng mô tả tất cả các lựa chọn có thể có trên trang ProGuard. Trước khi bạn tìm hiểu sâu hơn về cấu hình, tốt nhất là nên bắt đầu với một sự hiểu biết cơ bản về cách ProGuard hoạt động và tại sao chúng ta cần chỉ định các tuỳ chọn bổ sung.

Tóm lại, ProGuard lấy các file class trong project của bạn làm đầu vào, sau đó truy tìm đến tất cả các điểm truy cập (entry point) có thể của ứng dụng và tính toán map của tất cả các code có thể truy cập từ các điểm truy cập (entry point), sau đó loại bỏ hết phần còn lại (dead code, hoặc code mà không bao giờ có thể chạy bởi vì nó không bao giờ được gọi).

Khi đọc hướng dẫn sử dụng ProGuard, bạn nên bỏ qua phần input/output, vì Android Gradle Plugin sẽ chỉ định input (các class của bạn) và các library jars (các class của Android framework mà bạn đang xây dựng ứng dụng của mình).

Phần quan trọng của việc cấu hình chính xác ProGuard là để cho nó biết được những phần nào trong code của bạn được truy cập khi runtime và không nên loại bỏ (và cũng nên để nguyên tên của chúng khi obfuscation được bật). Khi các class hoặc các method chỉ được truy cập động (sử dụng reflection), ProGuard đôi khi không thể xác định được vòng đời (liveness) của chúng khi xây dựng bản đồ các code được sử dụng và sẽ xoá các class này một cách sai lầm. Điều này có thể xảy ra bất cứ khi nào bạn chỉ tham chiếu tới code từ tài nguyên XML.

Trong quá trình build Android điển hình, AAPT (công cụ xử lý tài nguyên) tạo ra một file ProGuard rule bổ sung. Nó add thêm các keep rules rõ ràng cho các điểm nhập (entry point) của ứng dụng Android, vì vậy, tất cả các Activity, Service, BroadcastReceiver và ContentProvider được khai báo trong AndroidMinifest phải được giữ nguyên. Đó là lý do vì sao trong hình trên, class MyActivity không bị loại bỏ hay đổi tên.

AAPT cũng sẽ giữ (keep) tất cả các View (và các contructor của chúng) mà được sử dụng trong XML layout và một số class khác như chuyển tiếp tham chiếu từ tài nguyên animation transition. Bạn có thể kiểm tra file cấu hình do AAPT tạo ra sau khi build ứng dụng bằng các mở file <your_project>/<app_module>/build/intermediates/proguard-rules/<variant>/aapt_rules.txt:

Chúng ta sẽ tiếp tục tìm hiểu thêm về những keep rule ở phần sau của bài viết này, nhưng trước hết, chúng ta phải tìm hiểu sẽ làm gì khi bật ProGuard gây ra lỗi build ứng dụng của bạn.

Khi bật ProGuard gây ra lỗi build

Trước khi bạn test mọi thứ có hoạt động chính xác khi chạy với ProGuard hay không, bạn phải build ứng dụng. Thật không may, ProGuard có thể làm thất bại quá trình build của bạn và phát ra các cảnh báo warning lúc compile khi nó phát hiện ra các vấn đề với code của bạn, chẳng hạn như tham chiếu tới những class không tìm thấy.

Chìa khoá để sửa những lỗi build này là nhìn vào những message thông báo lỗi, hiểu về những cảnh báo warning và giải quyết chúng, thường là bằng cách sửa các phụ thuộc (dependencies) hoặc thêm vào -dontwarn rule vào file cấu hình ProGuard.

Một trong những lý do warning có thể xuất hiện là khi một trong những phụ thuộc (dependencies) của bạn được compile với JAR mà không có đường dẫn build (build path), chẳng hạn như sử dụng phụ thuộc provided. Đôi khi những đường dẫn code sử dụng những phụ thuộc không thực sự được gọi khi chạy code thư viện trên Android. Hãy xem ví dụ thực tế dưới đây: Build output khi build project có phụ thuộc OkHttp 3.8.0

Thư viện Okhttp đã thêm một annotations mới (javax.annotation.Nullable) vào các class của nó ở version 3.8.0. Bởi vì chúng sử dụng phụ thuộc vào thời gian biên dịch, các annotation không tự nó đưa vào bản build cuối của một ứng dụng mà phụ thuộc Okhttp (trừ khi ứng dụng thêm vào com.google.code.findbugs:jsr305 một cách rõ ràng) và ProGuard sẽ cảnh báo về những class bị thiếu.

Bởi vì chúng ta biết các annatation class này không được sử dụng trong thời gian chạy runtime, chúng ta có thể bỏ qua cảnh báo đó một cách an toàn bằng cách thêm -dontwarn vào file cấu hình ProGuard, như đã được gợi ý trong manual của Okhttp:

-dontwarn javax.annotation.Nullable
-dontwarn javax.annotation.ParametersAreNonnullByDefault

Bạn nên làm tương tự như trên đối với tất cả những warning bạn thấy trong output, sau đó build lại cho tới khi thành công. Phần quan trọng là bạn phải hiểu được tại sao lại có cảnh báo warning, và nó là an toàn để bỏ qua hay thực sự thiếu một số class khi build.

Bây giờ bạn có thể bị cám dỗ là chỉ cần bỏ qua tất cả các cảnh báo warning bằng cách sử dụng tuỳ chọn ignorewarnings nhưng nó hiếm khi là một ý tưởng tốt. Trong một số trường hợp, cảnh báo warning sẽ cho bạn biết về các lỗi sẽ ngăn ứng dụng của bạn hoạt động và các vấn đề khác về cấu hình của bạn.

Bạn cũng có thể muốn đọc các note của ProGuard (là những message có độ ưu tiên thấp hơn warning). Mặc dù nó không gây ra break quá trình build ứng dụng, nhưng chúng có thể gây ra những lỗi crash trong thời gian chạy runtime. Điều này có thể xảy ra khi ProGuard loại bỏ quá nhiều.

Khi ProGuard loại bỏ quá nhiều

Trong một số trường hợp ProGuard không thể biết rằng một class hay method đang được sử dụng, chẳng hạn như nó chỉ được tham chiếu bởi reflection hoặc từ XML. Để ngăn không cho code đó bị bỏ đi hoặc đổi tên, bạn phải thêm keep vào file cấu hình ProGuard. Tuỳ thuộc vào developer, để tìm ra những phần nào trong code của bạn có thể là vấn đề và cung cấp các rule cần thiết.

Nhận về lỗi ClassNotFoundExceptionMethodNotFoundException trong thời gian chạy runtime là một dấu hiệu chắc chắn bạn đã thiếu class hoặc method, vì ProGuard đã xoá chúng hoặc chúng bị thiếu ngay từ đầu do các phụ thuộc depedencies đã cấu hình sai. Điều rất quan trọng là phải kiểm tra kỹ lưỡng bản release (khi bật ProGuard) của ứng dụng của bạn và giải quyết các lỗi này.

Có nhiều flavors khác nhau của tuỳ chọn keep mà bạn có thể sử dụng để cấu hình ProGuard:

  • keep - bảo toàn tất cả các class và method tương ứng với đặc tả class
  • keepclassmembers - quy định các member sẽ được lưu giữ, nhưng chỉ khi class cha của chúng đang được lưu giữ vì một số lý do khác (nó có thể truy cập từ một điểm vào hoặc keep bởi những rule khác)
  • keepclasseswithmembers - sẽ giữ class và các member của nó, nhưng chỉ khi tất cả các member được liệt kê trong đặc tả của class đều có mặt

Bạn nên quen thuộc với cú pháp đặc tả class trong ProGuard. Cũng có các version của 3 rule keep mà chỉ ngăn chặn việc đổi tên, nhưng không ngăn chặn việc rút gọn (shrinking). Bạn có thể xem tổng quan về tất cả các tuỳ chọn keep trong table trên website ProGuard.

Là một sự thay thế cho việc viết các rule phức tạp của ProGuard, bạn chỉ cần thêm annatation @keep trên các classes/methods/fields mà bạn không muốn xoá bỏ, đổi tên bởi ProGuard. Bạn cần file cấu hình mặc định Android ProGuard được bao gồm trong build project của bạn để sử dụng kỹ thuật này.

(Còn tiếp)