Kiểm tra Dexpreopt và <uses-library>

Android 12 có những thay đổi về hệ thống bản dựng đối với quá trình biên dịch AOT của các tệp DEX (dexpreopt) cho các mô-đun Java có phần phụ thuộc <uses-library>. Trong một số trường hợp, những thay đổi về hệ thống bản dựng này có thể làm hỏng các bản dựng. Hãy sử dụng trang này để chuẩn bị cho các lỗi và làm theo các bước trên trang này để khắc phục và giảm thiểu các lỗi đó.

Dexpreopt là quy trình biên dịch trước thời gian của các thư viện và ứng dụng Java. Dexpreopt diễn ra trên máy chủ lưu trữ tại thời điểm tạo (khác với dexopt, diễn ra trên thiết bị). Cấu trúc của các phần phụ thuộc thư viện dùng chung mà một mô-đun Java (thư viện hoặc ứng dụng) sử dụng được gọi là ngữ cảnh trình tải lớp (CLC). Để đảm bảo tính chính xác của dexpreopt, CLC trong thời gian tạo và thời gian chạy phải trùng khớp. CLC thời gian xây dựng là những gì trình biên dịch dex2oat sử dụng tại thời điểm dexpreopt (được ghi trong tệp ODEX) và CLC thời gian chạy là ngữ cảnh mà mã được biên dịch trước được tải trên thiết bị.

Các CLC trong thời gian chạy và thời gian xây dựng này phải trùng khớp vì cả lý do về độ chính xác và hiệu suất. Để đảm bảo tính chính xác, bạn cần xử lý các lớp trùng lặp. Nếu các phần phụ thuộc của thư viện dùng chung tại thời gian chạy khác với các phần phụ thuộc được dùng để biên dịch, thì một số lớp có thể được phân giải theo cách khác, gây ra các lỗi nhỏ tại thời gian chạy. Hiệu suất cũng bị ảnh hưởng bởi các quy trình kiểm tra thời gian chạy đối với các lớp trùng lặp.

Các trường hợp sử dụng bị ảnh hưởng

Lần khởi động đầu tiên là trường hợp sử dụng chính chịu ảnh hưởng của những thay đổi này: nếu ART phát hiện thấy sự không khớp giữa CLC thời gian xây dựng và thời gian chạy, thì ART sẽ từ chối các cấu phần phần mềm dexpreopt và chạy dexopt thay thế. Đối với các lần khởi động tiếp theo, điều này không sao vì các ứng dụng có thể được dexopt ở chế độ nền và lưu trữ trên ổ đĩa.

Các khu vực bị ảnh hưởng của Android

Điều này ảnh hưởng đến tất cả các ứng dụng và thư viện Java có phần phụ thuộc trong thời gian chạy vào các thư viện Java khác. Android có hàng nghìn ứng dụng và hàng trăm ứng dụng trong số đó sử dụng các thư viện dùng chung. Các đối tác cũng chịu ảnh hưởng vì họ có các thư viện và ứng dụng riêng.

Thay đổi có thể gây lỗi

Hệ thống bản dựng cần biết các phần phụ thuộc <uses-library> trước khi tạo các quy tắc bản dựng dexpreopt. Tuy nhiên, nó không thể truy cập trực tiếp vào tệp kê khai và đọc các thẻ <uses-library> trong tệp đó, vì hệ thống bản dựng không được phép đọc các tệp tuỳ ý khi tạo các quy tắc bản dựng (vì lý do hiệu suất). Ngoài ra, tệp kê khai có thể được đóng gói bên trong một APK hoặc một bản dựng sẵn. Do đó, thông tin <uses-library> phải có trong tệp bản dựng (Android.bp hoặc Android.mk).

Trước đây, ART đã sử dụng một giải pháp tạm thời bỏ qua các phần phụ thuộc của thư viện dùng chung (được gọi là &-classpath). Điều này không an toàn và gây ra các lỗi nhỏ, vì vậy, giải pháp tạm thời này đã bị xoá trong Android 12.

Do đó, các mô-đun Java không cung cấp thông tin <uses-library> chính xác trong tệp bản dựng có thể gây ra sự cố khi tạo bản dựng (do CLC không khớp trong thời gian tạo bản dựng) hoặc sự cố hồi quy trong lần khởi động đầu tiên (do CLC không khớp trong thời gian khởi động, sau đó là dexopt).

Đường dẫn di chuyển

Hãy làm theo các bước sau để khắc phục bản dựng bị lỗi:

  1. Tắt chế độ kiểm tra thời gian xây dựng cho một sản phẩm cụ thể trên toàn cầu bằng cách đặt

    PRODUCT_BROKEN_VERIFY_USES_LIBRARIES := true

    trong tệp makefile của sản phẩm. Điều này giúp khắc phục các lỗi bản dựng (ngoại trừ các trường hợp đặc biệt được liệt kê trong phần Khắc phục các lỗi làm gián đoạn). Tuy nhiên, đây chỉ là một giải pháp tạm thời và có thể gây ra sự cố không khớp CLC khi khởi động, sau đó là dexopt.

  2. Hãy sửa các mô-đun bị lỗi trước khi bạn vô hiệu hoá chế độ kiểm tra thời gian xây dựng trên toàn cầu bằng cách thêm thông tin <uses-library> cần thiết vào tệp bản dựng của các mô-đun đó (xem phần Khắc phục các lỗi để biết thông tin chi tiết). Đối với hầu hết các mô-đun, bạn cần thêm một vài dòng trong Android.bp hoặc trong Android.mk.

  3. Tắt tính năng kiểm tra thời gian xây dựng và dexpreopt cho các trường hợp có vấn đề, trên cơ sở từng mô-đun. Tắt dexpreopt để bạn không lãng phí thời gian tạo bản dựng và bộ nhớ cho các cấu phần phần mềm bị từ chối khi khởi động.

  4. Bật lại chế độ kiểm tra thời gian xây dựng trên toàn cầu bằng cách huỷ đặt PRODUCT_BROKEN_VERIFY_USES_LIBRARIES đã được đặt ở Bước 1; bản dựng sẽ không thất bại sau thay đổi này (do các bước 2 và 3).

  5. Lần lượt sửa từng mô-đun mà bạn đã tắt ở Bước 3, sau đó bật lại dexpreopt và chế độ kiểm tra <uses-library>. Báo cáo lỗi nếu cần.

Các quy trình kiểm tra <uses-library> trong thời gian xây dựng được thực thi trong Android 12.

Khắc phục các lỗi

Các phần sau đây sẽ hướng dẫn bạn cách khắc phục các loại lỗi cụ thể.

Lỗi bản dựng: CLC không khớp

Hệ thống xây dựng sẽ kiểm tra tính nhất quán trong thời gian xây dựng giữa thông tin trong tệp Android.bp hoặc Android.mk và tệp kê khai. Hệ thống xây dựng không thể đọc tệp kê khai, nhưng có thể tạo các quy tắc xây dựng để đọc tệp kê khai (trích xuất tệp kê khai từ APK nếu cần) và so sánh các thẻ <uses-library> trong tệp kê khai với thông tin <uses-library> trong tệp bản dựng. Nếu quy trình kiểm tra không thành công, lỗi sẽ có dạng như sau:

error: mismatch in the <uses-library> tags between the build system and the manifest:
    - required libraries in build system: []
                     vs. in the manifest: [org.apache.http.legacy]
    - optional libraries in build system: []
                     vs. in the manifest: [com.x.y.z]
    - tags in the manifest (.../X_intermediates/manifest/AndroidManifest.xml):
        <uses-library android:name="com.x.y.z"/>
        <uses-library android:name="org.apache.http.legacy"/>

note: the following options are available:
    - to temporarily disable the check on command line, rebuild with RELAX_USES_LIBRARY_CHECK=true (this will set compiler filter "verify" and disable AOT-compilation in dexpreopt)
    - to temporarily disable the check for the whole product, set PRODUCT_BROKEN_VERIFY_USES_LIBRARIES := true in the product makefiles
    - to fix the check, make build system properties coherent with the manifest
    - see build/make/Changes.md for details

Như thông báo lỗi cho biết, có nhiều giải pháp, tuỳ thuộc vào mức độ khẩn cấp:

  • Để khắc phục tạm thời trên toàn bộ sản phẩm, hãy đặt PRODUCT_BROKEN_VERIFY_USES_LIBRARIES := true trong tệp makefile của sản phẩm. Quy trình kiểm tra tính nhất quán trong thời gian xây dựng vẫn được thực hiện, nhưng lỗi kiểm tra không có nghĩa là lỗi xây dựng. Thay vào đó, lỗi kiểm tra khiến hệ thống xây dựng hạ cấp bộ lọc trình biên dịch dex2oat xuống verify trong dexpreopt, việc này sẽ vô hiệu hoá hoàn toàn quá trình biên dịch AOT cho mô-đun này.
  • Để có một bản sửa lỗi dòng lệnh nhanh chóng, trên toàn cầu, hãy sử dụng biến môi trường RELAX_USES_LIBRARY_CHECK=true. Lệnh này có tác dụng tương tự như PRODUCT_BROKEN_VERIFY_USES_LIBRARIES, nhưng được dùng trên dòng lệnh. Biến môi trường sẽ ghi đè biến sản phẩm.
  • Để có giải pháp khắc phục nguyên nhân gốc của lỗi, hãy cho hệ thống bản dựng biết về thẻ <uses-library> trong tệp kê khai. Việc kiểm tra thông báo lỗi cho thấy thư viện nào gây ra vấn đề (cũng như việc kiểm tra AndroidManifest.xml hoặc tệp kê khai bên trong một APK có thể được kiểm tra bằng "aapt dump badging $APK | grep uses-library").

Đối với các mô-đun Android.bp:

  1. Tìm thư viện bị thiếu trong thuộc tính libs của mô-đun. Nếu có, Soong thường tự động thêm các thư viện như vậy, ngoại trừ trong những trường hợp đặc biệt sau:

    • Thư viện này không phải là thư viện SDK (được xác định là java_library thay vì java_sdk_library).
    • Thư viện có tên thư viện khác (trong tệp kê khai) so với tên mô-đun (trong hệ thống xây dựng).

    Để tạm thời khắc phục vấn đề này, hãy thêm provides_uses_lib: "<library-name>" vào định nghĩa thư viện Android.bp. Để có giải pháp lâu dài, hãy khắc phục vấn đề cơ bản: chuyển đổi thư viện thành thư viện SDK hoặc đổi tên mô-đun của thư viện.

  2. Nếu bước trước không giải quyết được vấn đề, hãy thêm uses_libs: ["<library-module-name>"] cho các thư viện bắt buộc hoặc optional_uses_libs: ["<library-module-name>"] cho các thư viện không bắt buộc vào định nghĩa Android.bp của mô-đun. Những thuộc tính này chấp nhận một danh sách tên mô-đun. Thứ tự tương đối của các thư viện trong danh sách phải giống với thứ tự trong tệp kê khai.

Đối với các mô-đun Android.mk:

  1. Kiểm tra xem thư viện có tên thư viện khác (trong tệp kê khai) so với tên mô-đun (trong hệ thống xây dựng) hay không. Nếu có, hãy tạm thời khắc phục vấn đề này bằng cách thêm LOCAL_PROVIDES_USES_LIBRARY := <library-name> vào tệp Android.mk của thư viện hoặc thêm provides_uses_lib: "<library-name>" vào tệp Android.bp của thư viện (cả hai trường hợp đều có thể xảy ra vì mô-đun Android.mk có thể phụ thuộc vào thư viện Android.bp). Để có giải pháp lâu dài, hãy khắc phục vấn đề cơ bản: đổi tên mô-đun thư viện.

  2. Thêm LOCAL_USES_LIBRARIES := <library-module-name> cho các thư viện bắt buộc; thêm LOCAL_OPTIONAL_USES_LIBRARIES := <library-module-name> cho các thư viện không bắt buộc vào định nghĩa Android.mk của mô-đun. Các thuộc tính này chấp nhận một danh sách tên mô-đun. Thứ tự tương đối của các thư viện trong danh sách phải giống như trong tệp kê khai.

Lỗi khi tạo: đường dẫn thư viện không xác định

Nếu hệ thống tạo bản dựng không tìm thấy đường dẫn đến tệp <uses-library> DEX (đường dẫn thời gian tạo trên máy chủ lưu trữ hoặc đường dẫn cài đặt trên thiết bị), thì hệ thống thường sẽ không tạo được bản dựng. Nếu không tìm thấy đường dẫn, có thể thư viện được định cấu hình theo một cách không mong muốn. Tạm thời khắc phục bản dựng bằng cách tắt dexpreopt cho mô-đun có vấn đề.

Android.bp (thuộc tính mô-đun):

enforce_uses_libs: false,
dex_preopt: {
    enabled: false,
},

Android.mk (biến mô-đun):

LOCAL_ENFORCE_USES_LIBRARIES := false
LOCAL_DEX_PREOPT := false

Báo cáo lỗi để điều tra mọi trường hợp không được hỗ trợ.

Lỗi khi tạo: thiếu phần phụ thuộc thư viện

Việc cố gắng thêm <uses-library> X từ tệp kê khai của mô-đun Y vào tệp bản dựng cho Y có thể dẫn đến lỗi bản dựng do thiếu phần phụ thuộc X.

Đây là thông báo lỗi mẫu cho các mô-đun Android.bp:

"Y" depends on undefined module "X"

Đây là thông báo lỗi mẫu cho các mô-đun Android.mk:

'.../JAVA_LIBRARIES/com.android.X_intermediates/dexpreopt.config', needed by '.../APPS/Y_intermediates/enforce_uses_libraries.status', missing and no known rule to make it

Một nguồn phổ biến gây ra những lỗi như vậy là khi một thư viện có tên khác với tên mô-đun tương ứng trong hệ thống bản dựng. Ví dụ: nếu mục nhập <uses-library> trong tệp kê khai là com.android.X, nhưng tên của mô-đun thư viện chỉ là X, thì sẽ xảy ra lỗi. Để giải quyết trường hợp này, hãy cho hệ thống bản dựng biết rằng mô-đun có tên X cung cấp một <uses-library> có tên com.android.X.

Đây là ví dụ về các thư viện Android.bp (thuộc tính mô-đun):

provides_uses_lib: “com.android.X”,

Đây là ví dụ về các thư viện Android.mk (biến mô-đun):

LOCAL_PROVIDES_USES_LIBRARY := com.android.X

CLC không khớp trong thời gian khởi động

Khi khởi động lần đầu, hãy tìm trong logcat các thông báo liên quan đến lỗi CLC không khớp, như minh hoạ bên dưới:

$ adb wait-for-device && adb logcat \
  | grep -E 'ClassLoaderContext [a-z ]+ mismatch' -A1

Đầu ra có thể có các thông báo ở dạng như sau:

[...] W system_server: ClassLoaderContext shared library size mismatch Expected=..., found=... (PCL[]... | PCL[]...)
[...] I PackageDexOptimizer: Running dexopt (dexoptNeeded=1) on: ...

Nếu bạn nhận được cảnh báo CLC không khớp, hãy tìm lệnh dexopt cho mô-đun bị lỗi. Để khắc phục vấn đề này, hãy đảm bảo rằng quy trình kiểm tra thời gian xây dựng cho mô-đun sẽ diễn ra. Nếu cách đó không hiệu quả, thì có thể trường hợp của bạn là một trường hợp đặc biệt không được hệ thống xây dựng hỗ trợ (chẳng hạn như một ứng dụng tải một APK khác, chứ không phải một thư viện). Hệ thống bản dựng không xử lý tất cả các trường hợp, vì tại thời điểm xây dựng, không thể biết chắc chắn ứng dụng sẽ tải những gì trong thời gian chạy.

Ngữ cảnh trình tải lớp

CLC là một cấu trúc dạng cây mô tả hệ phân cấp trình tải lớp. Hệ thống bản dựng sử dụng CLC theo nghĩa hẹp (chỉ bao gồm các thư viện, không phải APK hoặc trình tải lớp tuỳ chỉnh): đó là một cây thư viện đại diện cho bao đóng bắc cầu của tất cả các phần phụ thuộc <uses-library> của một thư viện hoặc ứng dụng. Các phần tử cấp cao nhất của CLC là các phần phụ thuộc <uses-library> trực tiếp được chỉ định trong tệp kê khai (đường dẫn lớp). Mỗi nút của cây CLC là một nút <uses-library> có thể có các nút con <uses-library> riêng.

Vì các phần phụ thuộc <uses-library> là một đồ thị có hướng không có chu trình và không nhất thiết phải là một cây, nên CLC có thể chứa nhiều cây con cho cùng một thư viện. Nói cách khác, CLC là biểu đồ phần phụ thuộc "mở" thành một cây. Việc trùng lặp chỉ diễn ra ở cấp độ logic; các trình tải lớp cơ bản thực tế không bị trùng lặp (tại thời gian chạy, mỗi thư viện có một thực thể trình tải lớp duy nhất).

CLC xác định thứ tự tra cứu của các thư viện khi phân giải các lớp Java mà thư viện hoặc ứng dụng sử dụng. Thứ tự tra cứu rất quan trọng vì các thư viện có thể chứa các lớp trùng lặp và lớp được phân giải thành kết quả trùng khớp đầu tiên.

CLC trên thiết bị (thời gian chạy)

PackageManager (trong frameworks/base) tạo một CLC để tải mô-đun Java trên thiết bị. Thao tác này sẽ thêm các thư viện được liệt kê trong thẻ <uses-library> trong tệp kê khai của mô-đun dưới dạng các phần tử CLC cấp cao nhất.

Đối với mỗi thư viện được dùng, PackageManager sẽ nhận tất cả các phần phụ thuộc <uses-library> của thư viện đó (được chỉ định dưới dạng thẻ trong tệp kê khai của thư viện đó) và thêm một CLC lồng nhau cho mỗi phần phụ thuộc. Quá trình này tiếp tục đệ quy cho đến khi tất cả các nút lá của cây CLC được tạo là các thư viện không có phần phụ thuộc <uses-library>.

PackageManager chỉ nhận biết các thư viện dùng chung. Định nghĩa về shared trong cách sử dụng này khác với nghĩa thông thường (như trong shared so với static). Trong Android, các thư viện dùng chung Java là những thư viện có trong cấu hình XML được cài đặt trên thiết bị (/system/etc/permissions/platform.xml). Mỗi mục chứa tên của một thư viện dùng chung, đường dẫn đến tệp jar DEX của thư viện đó và danh sách các phần phụ thuộc (các thư viện dùng chung khác mà thư viện này sử dụng trong thời gian chạy và chỉ định trong thẻ <uses-library> trong tệp kê khai).

Nói cách khác, có 2 nguồn thông tin cho phép PackageManager xây dựng CLC trong thời gian chạy: thẻ <uses-library> trong tệp kê khai và các phần phụ thuộc của thư viện dùng chung trong cấu hình XML.

CLC trên máy chủ (thời gian xây dựng)

CLC không chỉ cần thiết khi tải một thư viện hoặc ứng dụng mà còn cần thiết khi biên dịch một thư viện hoặc ứng dụng. Quá trình biên dịch có thể diễn ra trên thiết bị (dexopt) hoặc trong quá trình tạo (dexpreopt). Vì dexopt diễn ra trên thiết bị, nên nó có cùng thông tin với PackageManager (các phần phụ thuộc của thư viện dùng chung và tệp kê khai). Tuy nhiên, Dexpreopt diễn ra trên máy chủ lưu trữ và trong một môi trường hoàn toàn khác, đồng thời phải lấy cùng một thông tin từ hệ thống xây dựng.

Do đó, CLC thời gian xây dựng mà dexpreopt sử dụng và CLC thời gian chạy mà PackageManager sử dụng là giống nhau, nhưng được tính theo hai cách khác nhau.

CLC thời gian xây dựng và thời gian chạy phải trùng khớp, nếu không, mã được biên dịch AOT do dexpreopt tạo sẽ bị từ chối. Để kiểm tra tính bình đẳng của CLC trong thời gian xây dựng và thời gian chạy, trình biên dịch dex2oat sẽ ghi lại CLC trong thời gian xây dựng trong các tệp *.odex (trong trường classpath của tiêu đề tệp OAT). Để tìm CLC đã lưu trữ, hãy dùng lệnh sau:

oatdump --oat-file=<FILE> | grep '^classpath = '

Sự không khớp CLC trong thời gian xây dựng và thời gian chạy được báo cáo trong logcat trong quá trình khởi động. Tìm kiếm bằng lệnh sau:

logcat | grep -E 'ClassLoaderContext [a-z ]+ mismatch'

Sự không khớp này ảnh hưởng xấu đến hiệu suất, vì nó buộc thư viện hoặc ứng dụng phải được dexopt hoặc chạy mà không có các hoạt động tối ưu hoá (ví dụ: mã của ứng dụng có thể cần được trích xuất trong bộ nhớ từ APK, một thao tác rất tốn kém).

Thư viện dùng chung có thể là tuỳ chọn hoặc bắt buộc. Theo quan điểm của dexpreopt, một thư viện bắt buộc phải có tại thời điểm tạo bản dựng (nếu không có thì sẽ xảy ra lỗi bản dựng). Thư viện không bắt buộc có thể xuất hiện hoặc không xuất hiện tại thời gian xây dựng: nếu xuất hiện, thư viện này sẽ được thêm vào CLC, truyền đến dex2oat và được ghi lại trong tệp *.odex. Nếu thiếu một thư viện không bắt buộc, thì thư viện đó sẽ bị bỏ qua và không được thêm vào CLC. Nếu có sự không khớp giữa trạng thái thời gian tạo và thời gian chạy (thư viện không bắt buộc có trong một trường hợp nhưng không có trong trường hợp khác), thì CLC thời gian tạo và thời gian chạy sẽ không khớp và mã đã biên dịch sẽ bị từ chối.

Thông tin chi tiết về hệ thống bản dựng nâng cao (trình sửa tệp kê khai)

Đôi khi, thẻ <uses-library> bị thiếu trong tệp kê khai nguồn của một thư viện hoặc ứng dụng. Ví dụ: điều này có thể xảy ra nếu một trong các phần phụ thuộc bắc cầu của thư viện hoặc ứng dụng bắt đầu sử dụng một thẻ <uses-library> khác và tệp kê khai của thư viện hoặc ứng dụng không được cập nhật để đưa thẻ đó vào.

Soong có thể tự động tính toán một số thẻ <uses-library> còn thiếu cho một thư viện hoặc ứng dụng nhất định, dưới dạng các thư viện SDK trong bao đóng phần phụ thuộc bắc cầu của thư viện hoặc ứng dụng. Cần có bao đóng vì thư viện (hoặc ứng dụng) có thể phụ thuộc vào một thư viện tĩnh phụ thuộc vào một thư viện SDK và có thể lại phụ thuộc bắc cầu thông qua một thư viện khác.

Không phải tất cả thẻ <uses-library> đều có thể được tính theo cách này, nhưng khi có thể, bạn nên để Soong tự động thêm các mục nhập tệp kê khai; cách này ít xảy ra lỗi hơn và giúp đơn giản hoá việc duy trì. Ví dụ: khi nhiều ứng dụng sử dụng một thư viện tĩnh thêm một phần phụ thuộc <uses-library> mới, tất cả các ứng dụng phải được cập nhật, điều này gây khó khăn cho việc duy trì.