Bật multidex cho các ứng dụng có hơn 64K phương thức

Nếu ứng dụng của bạn có minSdk là API 20 trở xuống và ứng dụng đó cũng như thư viện mà ứng dụng đó tham chiếu vượt quá 65.536 phương thức, bạn sẽ gặp lỗi bản dựng sau đây (cho biết ứng dụng của bạn đã đạt đến giới hạn của cấu trúc bản dựng Android):

trouble writing output:
Too many field references: 131000; max is 65536.
You may try using --multi-dex option.

Các phiên bản cũ hơn của hệ thống bản dựng báo cáo một lỗi khác, đó là dấu hiệu của cùng một vấn đề:

Conversion to Dalvik format failed:
Unable to execute dex: method ID not in [0, 0xffff]: 65536

Cả hai điều kiện lỗi này đều cho thấy một số chung: 65536. Con số này đại diện cho tổng số lượt tham chiếu có thể được gọi bằng mã trong một tệp mã byte Dalvik Executable (DEX) duy nhất. Trang này giải thích cách vượt qua giới hạn này bằng cách bật cấu hình ứng dụng được gọi là multidex. Cấu hình này cho phép ứng dụng của bạn xây dựng và đọc nhiều tệp DEX.

Giới thiệu về giới hạn tham chiếu 64K

Tệp ứng dụng Android (APK) chứa các tệp mã byte ở dạng tệp Dalvik có thể thực thi (DEX). Tệp này chứa mã đã biên dịch dùng để chạy ứng dụng của bạn. Thông số có thể thực thi Dalvik giới hạn tổng số phương thức có thể được tham chiếu trong một tệp DEX ở mức 65.536 — bao gồm cả các phương thức khung Android, phương thức thư viện và phương thức trong mã riêng bạn.

Trong bối cảnh khoa học máy tính, thuật ngữ Kilo hay còn gọi là K, biểu thị 1024 (hoặc 2^10). Vì 65.536 bằng 64 X 1024, nên giới hạn này được gọi là "giới hạn tham chiếu 64K".

Hỗ trợ Multidex trước Android 5.0

Các phiên bản của nền tảng trước Android 5.0 (API cấp độ 21) sử dụng thời gian chạy Dalvik để thực thi mã ứng dụng. Theo mặc định, Dalvik giới hạn các ứng dụng thành một tệp mã byte classes.dex duy nhất cho mỗi APK. Để khắc phục hạn chế này, hãy thêm thư viện multidex vào tệp build.gradle hoặc build.gradle.kts ở cấp mô-đun:

Groovy

dependencies {
    def multidex_version = "2.0.1"
    implementation "androidx.multidex:multidex:$multidex_version"
}

Kotlin

dependencies {
    val multidex_version = "2.0.1"
    implementation("androidx.multidex:multidex:$multidex_version")
}

Thư viện này trở thành một phần của tệp DEX chính của ứng dụng, sau đó quản lý quyền truy cập vào các tệp DEX bổ sung và mã mà các tệp này chứa. Để xem các phiên bản hiện tại của thư viện này, hãy xem phần phiên bản multidex.

Để biết thêm thông tin, hãy xem phần nội dung về cách định cấu hình ứng dụng dành cho multidex.

Hỗ trợ multidex cho Android 5.0 trở lên

Android 5.0 (API cấp độ 21) trở lên sử dụng thời gian chạy có tên là ART (vốn hỗ trợ việc tải nhiều tệp DEX qua các tệp APK). ART thực hiện quá trình biên dịch trước tại thời điểm cài đặt ứng dụng, quét tìm các tệp classesN.dex và biên dịch các tệp đó thành một tệp OAT duy nhất để thực thi trên thiết bị Android. Do đó, nếu minSdkVersion của bạn là từ 21 trở lên, thì multidex sẽ được bật theo mặc định và bạn không cần có thư viện multidex.

Để biết thêm thông tin về môi trường thời gian chạy của Android 5.0, hãy đọc nội dung về Android Runtime (ART) và Dalvik.

Lưu ý: Khi chạy ứng dụng của bạn bằng Android Studio, bản dựng sẽ được tối ưu hoá cho các thiết bị mục tiêu mà bạn triển khai. Trong đó có việc bật multidex khi các thiết bị mục tiêu đang chạy Android 5.0 trở lên. Vì tính năng tối ưu hoá này chỉ được áp dụng khi triển khai ứng dụng bằng Android Studio, nên có thể bạn vẫn cần phải định cấu hình bản phát hành cho multidex để tránh giới hạn 64K.

Tránh giới hạn 64K

Trước khi định cấu hình ứng dụng để cho phép sử dụng các tệp đối chiếu phương thức 64K trở lên, bạn nên thực hiện các bước để giảm tổng số tệp đối chiếu được mã ứng dụng của bạn gọi, bao gồm cả các phương thức được xác định theo mã ứng dụng của bạn hoặc thư viện đi kèm.

Các chiến lược sau đây có thể giúp bạn tránh đạt đến giới hạn tham chiếu DEX:

Xem xét các phần phụ thuộc trực tiếp và bắc cầu của ứng dụng
Hãy cân nhắc xem giá trị của bất cứ phần phụ thuộc thư viện lớn nào mà bạn đưa vào ứng dụng có lớn hơn số lượng mã thêm vào ứng dụng hay không. Một cách làm phổ biến nhưng có vấn đề là đưa vào một thư viện rất lớn vì có một số phương thức hữu ích. Thông thường, việc giảm phần phụ thuộc mã ứng dụng có thể giúp bạn tránh giới hạn tệp tham chiếu DEX.
Xoá mã không dùng đến bằng R8
Bật tính năng rút gọn mã để chạy R8 trong bản phát hành. Bật tính năng rút gọn để đảm bảo rằng bạn không vận chuyển mã không sử dụng đến tệp APK. Nếu bạn định cấu hình rút gọn mã đúng cách, tính năng này cũng có thể xoá mã và tài nguyên không dùng đến khỏi các phần phụ thuộc.

Việc sử dụng những kỹ thuật này có thể giúp bạn giảm kích thước tổng thể của tệp APK và tránh dùng multidex trong ứng dụng.

Định cấu hình ứng dụng của bạn cho multidex

Lưu ý: Nếu minSdkVersion của bạn được thiết lập thành 21 trở lên, thì multidex sẽ được bật theo mặc định và bạn không cần có thư viện multidex.

Tuy nhiên, nếu minSdkVersion của bạn được thiết lập thành 20 trở xuống, thì bạn phải sử dụng thư viện multidex và thực hiện những sửa đổi sau đối với dự án ứng dụng:

  1. Sửa đổi tệp build.gradle ở cấp mô-đun để bật multidex và thêm thư viện multidex dưới dạng phần phụ thuộc, như minh hoạ dưới đây:

    Groovy

    android {
        defaultConfig {
            ...
            minSdkVersion 15 
            targetSdkVersion 33
            multiDexEnabled true
        }
        ...
    }
    
    dependencies {
        implementation "androidx.multidex:multidex:2.0.1"
    }

    Kotlin

    android {
        defaultConfig {
            ...
            minSdk = 15 
            targetSdk = 33
            multiDexEnabled = true
        }
        ...
    }
    
    dependencies {
        implementation("androidx.multidex:multidex:2.0.1")
    }
  2. Tuỳ thuộc vào việc bạn có ghi đè lớp Application hay không, hãy thực hiện một trong những thao tác sau:
    • Nếu bạn không ghi đè lớp Application, hãy chỉnh sửa tệp kê khai của bạn để đặt android:name trong thẻ <application> như sau:

      <?xml version="1.0" encoding="utf-8"?>
      <manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.example.myapp">
          <application
                  android:name="androidx.multidex.MultiDexApplication" >
              ...
          </application>
      </manifest>
    • Nếu bạn ghi đè lớp Application, hãy thay đổi lớp này để mở rộng MultiDexApplication như sau:

      Kotlin

      class MyApplication : MultiDexApplication() {...}

      Java

      public class MyApplication extends MultiDexApplication { ... }
    • Nếu bạn ghi đè Application nhưng không thể thay đổi lớp cơ sở, sau đó ghi đè lớp attachBaseContext() phương thức và gọi MultiDex.install(this) để bật multidex:

      Kotlin

      class MyApplication : SomeOtherApplication() {
      
          override fun attachBaseContext(base: Context) {
              super.attachBaseContext(base)
              MultiDex.install(this)
          }
      }

      Java

      public class MyApplication extends SomeOtherApplication {
        @Override
        protected void attachBaseContext(Context base) {
           super.attachBaseContext(base);
           MultiDex.install(this);
        }
      }

      Lưu ý: Đừng thực thi MultiDex.install() hoặc bất cứ mã nào khác thông qua tính năng phản chiếu hoặc JNI trước khi MultiDex.install() hoàn tất. Quá trình theo dõi multidex sẽ không tuân theo các lệnh gọi đó, gây ra ClassNotFoundException hoặc xác minh lỗi do phân vùng lớp không hợp lệ giữa các tệp DEX.

Giờ đây, khi bạn xây dựng ứng dụng, các công cụ bản dựng Android sẽ tạo một tệp DEX sơ cấp (classes.dex) và hỗ trợ các tệp DEX (classes2.dex, classes3.dex, v.v.) khi cần. Sau đó, hệ thống bản dựng sẽ kết hợp tất cả các tệp DEX vào tệp APK của bạn.

Trong thời gian chạy, thay vì chỉ tìm kiếm trong tệp classes.dex chính, các API multidex sẽ sử dụng một trình tải lớp đặc biệt để tìm kiếm tất cả các tệp DEX có sẵn cho phương thức của bạn.

Các hạn chế của thư viện multidex

Thư viện multidex có một số hạn chế đã biết. Khi kết hợp thư viện này vào cấu hình bản dựng ứng dụng, hãy cân nhắc những điều sau:

  • Việc cài đặt các tệp DEX trong quá trình khởi động vào phân vùng dữ liệu của thiết bị rất phức tạp và có thể dẫn đến lỗi Ứng dụng không phản hồi (ANR) nếu tệp DEX thứ cấp có kích thước lớn. Để tránh vấn đề này, hãy bật tính năng rút gọn mã để giảm thiểu kích thước của các tệp DEX và xoá các phần mã không dùng đến.
  • Khi chạy trên các phiên bản trước Android 5.0 (API cấp độ 21), việc sử dụng multidex là không đủ để xử lý giới hạn phân bổ tuyến tính (vấn đề 37008143). Giới hạn này đã tăng trong Android 4.0 (API cấp độ 14), nhưng điều đó không giải quyết được triệt để vấn đề này.

    Trong các phiên bản Android thấp hơn 4.0, bạn có thể đạt đến giới hạn phân bổ tuyến tính trước khi đạt đến giới hạn chỉ số DEX. Vì vậy, nếu bạn đang nhắm đến các cấp độ API thấp hơn 14, hãy kiểm tra kỹ các phiên bản của nền tảng đó vì ứng dụng của bạn có thể gặp vấn đề khi khởi động hoặc khi một nhóm lớp cụ thể được tải.

    Tính năng rút gọn mã có thể làm giảm hoặc loại bỏ những vấn đề này.

Khai báo các loại bắt buộc trong tệp DEX chính

Khi tạo từng tệp DEX cho một ứng dụng multidex, các công cụ xây dựng sẽ thực hiện việc ra quyết định phức tạp để xác định loại nào là cần thiết trong tệp DEX chính để ứng dụng của bạn có thể khởi động thành công. Nếu tất cả các lớp bắt buộc trong quá trình khởi động không được cung cấp trong tệp DEX chính, thì ứng dụng của bạn sẽ gặp lỗi java.lang.NoClassDefFoundError.

Các công cụ bản dựng nhận ra đường dẫn mã đối với mã được truy cập trực tiếp qua mã ứng dụng của bạn. Tuy nhiên, vấn đề này có thể xảy ra khi đường dẫn mã hiển thị ít hơn, chẳng hạn như khi thư viện bạn sử dụng có phần phụ thuộc phức tạp. Ví dụ: nếu mã sử dụng phương thức gọi nội dung hoặc gọi của phương thức Java từ mã gốc, thì có thể các lớp đó không được nhận dạng là bắt buộc trong tệp DEX chính.

Nếu nhận được java.lang.NoClassDefFoundError, bạn phải chỉ định thủ công các lớp bổ sung cần thiết trong tệp DEX chính bằng cách khai báo các lớp đó với thuộc tính multiDexKeepProguard trong loại bản dựng. Nếu một lớp được so khớp trong tệp multiDexKeepProguard, thì lớp đó sẽ được thêm vào tệp DEX chính.

Thuộc tính multiDexKeepProguard

Tệp multiDexKeepProguard sử dụng định dạng giống như ProGuard và hỗ trợ toàn bộ ngữ pháp ProGuard. Để biết thêm thông tin về cách tuỳ chỉnh nội dung được lưu giữ trong ứng dụng, hãy xem nội dung Tuỳ chỉnh mã cần giữ lại.

Tệp mà bạn chỉ định trong multiDexKeepProguard phải chứa các tuỳ chọn -keep trong mọi cú pháp ProGuard hợp lệ Ví dụ: -keep com.example.MyClass.class. Bạn có thể tạo tệp có tên multidex-config.pro trông như sau:

-keep class com.example.MyClass
-keep class com.example.MyClassToo

Nếu bạn muốn chỉ định tất cả các lớp trong một gói, tệp sẽ có dạng như sau:

-keep class com.example.** { *; } // All classes in the com.example package

Sau đó, bạn có thể khai báo tệp đó cho loại bản dựng như sau:

Groovy

android {
    buildTypes {
        release {
            multiDexKeepProguard file('multidex-config.pro')
            ...
        }
    }
}

Kotlin

android {
    buildTypes {
        getByName("release") {
            multiDexKeepProguard = file("multidex-config.pro")
            ...
        }
    }
}

Tối ưu hoá multidex trong các bản dựng phát triển

Cấu hình multidex đòi hỏi phải tăng đáng kể thời gian quy trình xây dựng vì hệ thống xây dựng phải đưa ra các quyết định phức tạp về việc phải đưa lớp nào vào tệp DEX sơ cấp và có thể đưa lớp nào vào tệp DEX thứ cấp. Tức là các bản dựng tăng dần sử dụng multidex thường mất nhiều thời gian hơn và có nguy cơ làm chậm quá trình phát triển của bạn.

Để giảm thiểu thời gian tạo bản dựng tăng dần, hãy sử dụng tính năng chuyển sang định dạng .dex để sử dụng lại kết quả multidex trong các bản dựng. Việc chuyển sang định dạng .dex phụ thuộc vào định dạng ART chỉ có trên Android 5.0 (API cấp độ 21) trở lên. Nếu đang sử dụng Android Studio, IDE sẽ tự động sử dụng tính năng chuyển sang định dạng .dex khi bạn triển khai ứng dụng trên một thiết bị chạy Android 5.0 (API cấp độ 21) trở lên. Tuy nhiên, nếu bạn đang chạy các bản dựng Gradle qua dòng lệnh, bạn cần phải thiết lập minSdkVersion thành 21 trở lên để bật tính năng chuyển sang định dạng .dex.

Để giữ nguyên chế độ cài đặt cho bản phát hành chính thức, bạn có thể tạo hai phiên bản ứng dụng bằng phiên bản sản phẩm (một phiên bản có phiên bản phát triển và một phiên bản có phiên bản phát hành) với các giá trị riêng biệt cho minSdkVersion, như minh hoạ:

Groovy

android {
    defaultConfig {
        ...
        multiDexEnabled true
        // The default minimum API level you want to support.
        minSdkVersion 15
    }
    productFlavors {
        // Includes settings you want to keep only while developing your app.
        dev {
            // Enables pre-dexing for command-line builds. When using
            // Android Studio 2.3 or higher, the IDE enables pre-dexing
            // when deploying your app to a device running Android 5.0
            // (API level 21) or higher, regardless of minSdkVersion.
            minSdkVersion 21
        }
        prod {
            // If you've configured the defaultConfig block for the production version of
            // your app, you can leave this block empty and Gradle uses configurations in
            // the defaultConfig block instead. You still need to include this flavor.
            // Otherwise, all variants use the "dev" flavor configurations.
        }
    }
    buildTypes {
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'),
                                                 'proguard-rules.pro'
        }
    }
}
dependencies {
    implementation "androidx.multidex:multidex:2.0.1"
}

Kotlin

android {
    defaultConfig {
        ...
        multiDexEnabled = true
        // The default minimum API level you want to support.
        minSdk = 15
    }
    productFlavors {
        // Includes settings you want to keep only while developing your app.
        create("dev") {
            // Enables pre-dexing for command-line builds. When using
            // Android Studio 2.3 or higher, the IDE enables pre-dexing
            // when deploying your app to a device running Android 5.0
            // (API level 21) or higher, regardless of minSdkVersion.
            minSdk = 21
        }
        create("prod") {
            // If you've configured the defaultConfig block for the production version of
            // your app, you can leave this block empty and Gradle uses configurations in
            // the defaultConfig block instead. You still need to include this flavor.
            // Otherwise, all variants use the "dev" flavor configurations.
        }
    }
    buildTypes {
        getByName("release") {
            isMinifyEnabled = true
            proguardFiles(getDefaultProguardFile("proguard-android.txt"),
                                                 "proguard-rules.pro")
        }
    }
}

dependencies {
    implementation("androidx.multidex:multidex:2.0.1")
}

Để tìm hiểu thêm về các chiến lược giúp cải thiện tốc độ xây dựng (qua Android Studio hoặc dòng lệnh), hãy đọc nội dung Tối ưu hoá tốc độ xây dựng. Để biết thêm thông tin về việc sử dụng các biến thể bản dựng, hãy xem nội dung Định cấu hình biến thể bản dựng.

Lưu ý: Nếu có nhiều biến thể bản dựng tuỳ theo nhu cầu multidex, bạn có thể cung cấp một tệp kê khai riêng cho mỗi biến thể để chỉ tệp của API cấp 20 trở xuống thay đổi tên thẻ <application>. Bạn cũng có thể tạo một lớp con Application riêng cho mỗi biến thể để chỉ lớp con cho API cấp 20 trở xuống mới mở rộng lớp MultiDexApplication hoặc gọi MultiDex.install(this).

Thử nghiệm ứng dụng multidex

Khi viết kiểm thử đo lường cho các ứng dụng multidex, bạn không cần phải định cấu hình thêm nếu sử dụng khả năng đo lường MonitoringInstrumentation (hoặc AndroidJUnitRunner). Nếu sử dụng một Instrumentation khác, bạn phải ghi đè phương thức onCreate() của nó bằng đoạn mã sau đây:

Kotlin

fun onCreate(arguments: Bundle) {
  MultiDex.install(targetContext)
  super.onCreate(arguments)
  ...
}

Java

public void onCreate(Bundle arguments) {
  MultiDex.install(getTargetContext());
  super.onCreate(arguments);
  ...
}