Dexpreopt và <uses-library> séc

Android 12 có các thay đổi về hệ thống xây 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 này đối với hệ thống xây dựng có thể phá vỡ các bản dựng. Hãy dùng trang này để chuẩn bị cho sự cố và làm theo công thức trên trang này để khắc phục và giảm thiểu sự cố.

Dexpreopt là quá trình biên dịch trước các thư viện và ứng dụng Java. Dexpreopt xảy ra trên máy chủ tại thời điểm tạo bản dựng (thay vì dexopt xảy ra trên thiết bị). Cấu trúc của các phần phụ thuộc của 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 xây dựng và thời gian chạy phải trùng nhau. CLC tại thời điểm tạo bản dựng là nội dung mà trình biên dịch dex2oat sử dụng tại thời gian dexpreopt (được ghi lại 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 này trong thời gian xây dựng và trong thời gian chạy phải trùng khớp vì cả tính chính xác lẫn hiệu suất. Để đảm bảo tính chính xác, cần phải 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 trong thời gian chạy khác với các phần phụ thuộ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ỏ về thời gian chạy. Hiệu suất cũng chịu ảnh hưởng của việc kiểm tra trong thời gian chạy để tìm các lớp trùng lặp.

Các trường hợp sử dụng chịu ả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 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ấu phần phần mềm dexpreopt và chạy dexopt. Đối với các lần khởi động tiếp theo, điều này không có vấn đề gì vì ứng dụng có thể được chuyển sang tệp dex ở chế độ nền và được lưu trữ trên ổ đĩa.

Những khu vực bị ảnh hưởng trên 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 thời gian chạy trên các thư viện Java khác. Android có hàng ngàn ứng dụng và hàng trăm ứng dụng trong số đó sử dụng thư viện dùng chung. Đố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 về điểm chèn quảng cáo

Hệ thống xây 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 xây dựng dexpreopt. Tuy nhiên, tệp kê khai không thể trực tiếp truy cập vào tệp kê khai và đọc các thẻ <uses-library> trong đó, vì hệ thống xây dựng không được phép đọc các tệp tuỳ ý khi tạo quy tắc bản dựng (vì lý do hiệu suất). Hơn nữa, tệp kê khai có thể được đóng gói bên trong tệp APK hoặc tệp tạo sẵn. Do đó, thông tin <uses-library> phải có trong các 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 bỏ qua các phần phụ thuộc 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 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 lỗi bản dựng (do CLC không khớp trong thời gian xây dựng) hoặc hồi quy CLC trong lần khởi động lần đầu (do lỗi CLC không khớp trong thời gian khởi động, sau đó là dexopt).

Lộ trình di chuyển

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

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

    PRODUCT_BROKEN_VERIFY_USES_LIBRARIES := true

    trong tệp makefile của sản phẩm. Thao tác này sẽ 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 sự cố). Tuy nhiên, đây là giải pháp tạm thời và có thể khiến CLC không khớp trong thời gian khởi động, theo sau là dexopt.

  2. Khắc phục các mô-đun không thành công trước khi bạn tắt tính năng kiểm tra thời gian xây dựng trên toàn hệ thống bằng cách thêm thông tin <uses-library> cần thiết vào tệp bản dựng (xem bài viết Khắc phục sự cố để biết thông tin chi tiết). Đối với hầu hết các mô-đun, bạn phải 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à dung lượng lưu trữ cho các cấu phần phần mềm bị từ chối khi khởi động.

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

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

Các bước kiểm tra <uses-library> tại thời điểm tạo bản dựng được thực thi trong Android 12.

Khắc phục sự cố

Các phần sau đây sẽ cho bạn biết cách khắc phục các loại sự cố 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 tại thời điểm tạo bản 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 đó 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 đề xuất, có nhiều giải pháp, tuỳ thuộc vào mức độ khẩn cấp:

  • Đối với một bản sửa lỗi tạm thời trên toàn sản phẩm, hãy đặt PRODUCT_BROKEN_VERIFY_USES_LIBRARIES := true trong tệp makefile của sản phẩm. Quá 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à bản dựng bị lỗi. Thay vào đó, nếu không kiểm tra được thì hệ thống xây dựng sẽ hạ cấp bộ lọc trình biên dịch dex2oat thành verify trong dexpreopt, từ đó tắt hoàn toàn tính năng biên dịch AOT cho mô-đun này.
  • Để khắc phục nhanh chóng toàn bộ dòng lệnh, hãy sử dụng biến môi trường RELAX_USES_LIBRARY_CHECK=true. Thuộc tính này có tác dụng tương tự như PRODUCT_BROKEN_VERIFY_USES_LIBRARIES, nhưng dùng để sử dụng trên dòng lệnh. Biến môi trường sẽ ghi đè biến sản phẩm.
  • Để biết giải pháp khắc phục nguyên nhân gốc rễ lỗi này, hãy cho hệ thống xây dựng biết về các thẻ <uses-library> trong tệp kê khai. Việc kiểm tra thông báo lỗi sẽ cho biết thư viện nào gây ra sự cố (cũng như việc kiểm tra AndroidManifest.xml hoặc tệp kê khai bên trong tệp 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) 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 cung cấp giải pháp, 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. Các thuộc tính này chấp nhận 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 (trong tệp kê khai) khác với tên mô-đun (trong hệ thống xây dựng) hay không. Nếu có, hãy khắc phục tạm thời 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 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.

Lỗi bản dựng: đường dẫn thư viện không xác định

Nếu hệ thống xây dựng không tìm thấy đường dẫn đến tệp jar DEX <uses-library> (đường dẫn tại thời điểm tạo bản dựng trên máy chủ hoặc đường dẫn cài đặt trên thiết bị), thì bản dựng thường không thành công. Việc không tìm thấy đường dẫn có thể cho biết rằng thư viện được định cấu hình theo 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

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

Lỗi bản dựng: 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 các 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 được đặt tên trong hệ thống xây 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ì điều này sẽ gây ra lỗi. Để giải quyết trường hợp này, hãy cho hệ thống xây dựng biết rằng mô-đun có tên X cung cấp <uses-library> có tên com.android.X.

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

provides_uses_lib: “com.android.X”,

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

LOCAL_PROVIDES_USES_LIBRARY := com.android.X

CLC không khớp tại thời điểm khởi động

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

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

Kết quả có thể 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 không khớp CLC, hãy tìm lệnh dexopt cho mô-đun lỗi. Để khắc phục lỗi 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 đã thành công. Nếu cách này không hiệu quả, thì trường hợp của bạn có thể là một trường hợp đặc biệt mà hệ thống xây dựng không hỗ trợ (chẳng hạn như một ứng dụng tải một tệp APK khác, chứ không phải thư viện). Hệ thống xây dựng không xử lý mọi trường hợp, vì tại thời điểm xây dựng, bạn không thể biết chắc chắn nội dung mà ứng dụng sẽ tải trong thời gian chạy.

Ngữ cảnh của 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 xây dựng sử dụng CLC theo nghĩa hẹp (chỉ bao gồm các thư viện, chứ không phải tệp APK hoặc trình tải lớp tuỳ chỉnh): đây là một cây thư viện thể hiện trạng thái đóng bắc cầu của tất cả phần phụ thuộc <uses-library> của 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 biểu đồ không tuần hoàn có hướng 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ở ra" thành một cây. Việc sao chép chỉ ở mức độ logic; các trình tải lớp cơ bản thực tế không được sao chép (trong thời gian chạy, có một thực thể trình tải lớp duy nhất cho mỗi thư viện).

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 là rất quan trọng vì thư viện có thể chứa các lớp trùng lặp và lớp được phân giải cho kết quả khớp đầu tiên.

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

PackageManager (trong frameworks/base) tạo một CLC để tải mô-đun Java trên thiết bị. Tệp 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 đã sử dụng, PackageManager sẽ nhận tất cả phần phụ thuộc <uses-library> (được chỉ định là thẻ trong tệp kê khai của thư viện đó) và thêm 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 đều là thư viện không có phần phụ thuộc <uses-library>.

PackageManager chỉ nhận biết được thư viện dùng chung. Định nghĩa về "được chia sẻ" trong cách sử dụng này khác với ý nghĩa thông thường của nó (như trong dạng chia sẻ so với tĩnh). Trong Android, thư viện dùng chung Java là những thư viện được liệt kê trong các 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 DEX jar 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 các thẻ <uses-library> trong tệp kê khai).

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

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

CLC không chỉ cần thiết khi tải 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ì quá trình dexopt diễn ra trên thiết bị, nên hoạt động này có cùng thông tin với PackageManager (tệp kê khai và phần phụ thuộc thư viện dùng chung). 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 nhận thông tin tương tự từ hệ thống xây dựng.

Do đó, CLC tại thời gian xây dựng mà dexpreopt sử dụng và CLC tại 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ã do AOT biên dịch do dexpreopt tạo ra sẽ bị từ chối. Để kiểm tra sự bằng nhau của CLC tại 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 tại 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 sử dụng lệnh sau:

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

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

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

Việc không khớp sẽ ả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 loại bỏ hoặc chạy mà không tối ưu hoá (ví dụ: có thể mã của ứng dụng 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 chia sẻ có thể là thư viện không bắt buộc hoặc bắt buộc. Từ quan điểm dexpreopt, 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ì đó là lỗi bản dựng). Thư viện không bắt buộc có thể có hoặc không có tại thời điểm tạo bản dựng: nếu có, thư viện này sẽ được thêm vào CLC, chuyển đến dex2oat và được ghi lại trong tệp *.odex. Nếu không có 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 xây dựng 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 xây dựng và thời gian chạy không khớp nhau và mã đã biên dịch bị từ chối.

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

Đôi khi, tệp kê khai nguồn của thư viện hoặc ứng dụng bị thiếu thẻ <uses-library>. Điều này có thể xảy ra, ví dụ: 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 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> bị thiếu cho một thư viện hoặc ứng dụng nhất định, vì các thư viện SDK trong quá trình đóng phần phụ thuộc bắc cầu của thư viện hoặc ứng dụng. Việc đóng là cần thiết 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ũng 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 toán 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 bị lỗi hơn và đơn giản hoá việc bảo 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ả ứng dụng phải được cập nhật, điều này rất khó duy trì.