הפעלת Multidex באפליקציות עם יותר מ-64K שיטות

אם לאפליקציה יש minSdk של API 20 או גרסה ישנה יותר, והאפליקציה והספריות שהיא מפנה אליהן חורגות מ-65,536 שיטות, תופיע שגיאת ה-build הבאה שמציינת שהאפליקציה הגיעה למגבלה של ארכיטקטורת ה-build של Android:

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

בגרסאות ישנות יותר של מערכת ה-build מדווחת שגיאה אחרת, שמעידה על אותה בעיה:

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

במצבי השגיאה האלה מוצג מספר נפוץ: 65536. המספר הזה מייצג את המספר הכולל של קובצי העזר שאפשר להפעיל באמצעות הקוד בתוך קובץ בייטקוד אחד של Dalvik Executable ‏ (DEX). בדף הזה מוסבר איך לעקוף את המגבלה הזו על ידי הפעלת הגדרת אפליקציה שנקראת multidex, שמאפשרת לאפליקציה ליצור ולקרוא כמה קובצי DEX.

מידע על מגבלת 64K קובצי העזר

קובצי אפליקציות ל-Android‏ (APK) מכילים קובצי בייטקוד להפעלה בפורמט Dalvik Executable‏ (DEX), שמכילים את הקוד המקומפל שמשמש להפעלת האפליקציה. המפרט של Dalvik Executable מגביל את המספר הכולל של השיטות שאפשר להפנות אליהן בקובץ DEX יחיד ל-65,536 – כולל שיטות של Android framework, שיטות של ספריות ושיטות בקוד שלכם.

בהקשר של מדעי המחשב, המונח קילו, או K, מציין 1,024 (או 2 בחזקת 10). ‫65,536 שווה ל-‎64x1024, ולכן המגבלה הזו נקראת <em>מגבלת ההפניות של 64K</em>.

תמיכה ב-multidex בגרסאות שקודמות ל-Android 5.0

בגרסאות של הפלטפורמה שקדמו ל-Android 5.0 (רמת API‏ 21), נעשה שימוש בסביבת זמן הריצה של Dalvik כדי להריץ את קוד האפליקציה. כברירת מחדל, Dalvik מגביל את האפליקציות לקובץ בייטקוד classes.dex אחד לכל APK. כדי לעקוף את המגבלה הזו, מוסיפים את ספריית ה-multidex לקובץ build.gradle או build.gradle.kts ברמת המודול:

מגניב

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")
}

הספרייה הזו הופכת לחלק מקובץ ה-DEX הראשי של האפליקציה, ואז מנהלת את הגישה לקובצי ה-DEX הנוספים ולקוד שהם מכילים. כדי לראות את הגרסאות הנוכחיות של הספרייה הזו, עיינו ב גרסאות של multidex.

מידע נוסף זמין בקטע הגדרת האפליקציה ל-multidex.

תמיכה ב-multidex ב-Android מגרסה 5.0 ואילך

ב-Android מגרסה 5.0 (רמת API‏ 21) ומעלה נעשה שימוש בסביבת זמן ריצה שנקראת ART, שתומכת באופן מובנה בטעינה של כמה קובצי DEX מקובצי APK. ‫ART מבצע קימפול מראש בזמן התקנת האפליקציה, סורק קובצי classesN.dex ומקמפל אותם לקובץ OAT אחד להפעלה במכשיר Android. לכן, אם הערך של minSdkVersion הוא 21 ומעלה, multidex מופעל כברירת מחדל ואין צורך בספריית multidex.

מידע נוסף על סביבת זמן הריצה של Android 5.0 זמין במאמר סביבת זמן ריצה ל-Android‏ (ART) ו-Dalvik.

הערה: כשמפעילים את האפליקציה באמצעות Android Studio, ה-build עובר אופטימיזציה למכשירי היעד שבהם פורסים אותו. זה כולל הפעלה של multidex כשמכשירי היעד פועלים עם Android מגרסה 5.0 ואילך. מכיוון שהאופטימיזציה הזו חלה רק כשפורסים את האפליקציה באמצעות Android Studio, יכול להיות שעדיין תצטרכו להגדיר את גרסת ה-build להפצה ל-multidex כדי להימנע ממגבלת ה-64K.

איך להימנע מהמגבלה של 64K

לפני שמגדירים את האפליקציה כך שתאפשר שימוש ב-64,000 הפניות לשיטות או יותר, צריך לבצע פעולות כדי לצמצם את המספר הכולל של ההפניות שקוד האפליקציה קורא להן, כולל שיטות שמוגדרות על ידי קוד האפליקציה או ספריות כלולות.

האסטרטגיות הבאות יכולות לעזור לכם להימנע מהגעה למגבלת ההפניות של DEX:

בדיקת יחסי התלות הישירים והטרנזיטיביים של האפליקציה
כדאי לשקול אם הערך של כל ספרייה גדולה שאתם כוללים באפליקציה עולה על כמות הקוד שנוספת לאפליקציה. דפוס נפוץ אך בעייתי הוא לכלול ספרייה גדולה מאוד כי כמה שיטות שירות היו שימושיות. צמצום יחסי התלות של קוד האפליקציה יכול לעזור לכם להימנע ממגבלת ההפניות של DEX.
הסרת קוד שלא בשימוש באמצעות R8
מפעילים כיווץ קוד כדי להריץ את R8 בגרסאות ה-build להפצה. כדאי להפעיל כיווץ כדי לוודא שאתם לא שולחים קוד שלא בשימוש עם קובצי ה-APK. אם כיווץ הקוד מוגדר בצורה נכונה, הוא יכול גם להסיר קוד ומשאבים שלא בשימוש מהתלויות שלכם.

השיטות האלה יכולות לעזור לכם להקטין את הגודל הכולל של ה-APK, וכך לא תצטרכו להשתמש ב-multidex באפליקציה.

הגדרת האפליקציה ל-multidex

הערה: אם הערך של minSdkVersion מוגדר ל-21 ומעלה, multidex מופעל כברירת מחדל ואין צורך בספריית multidex.

אם הערך של minSdkVersion הוא 20 או פחות, צריך להשתמש בספריית multidex ולבצע את השינויים הבאים בפרויקט של האפליקציה:

  1. משנים את הקובץ build.gradle ברמת המודול כדי להפעיל את multidex ולהוסיף את ספריית multidex כתלות, כמו שמוצג כאן:

    מגניב

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

    Kotlin

    android {
        defaultConfig {
            ...
            minSdk = 15 
            targetSdk = 36
            multiDexEnabled = true
        }
        ...
    }
    
    dependencies {
        implementation("androidx.multidex:multidex:2.0.1")
    }
  2. בהתאם למצב שבו אתם מבטלים את המחלקה Application, מבצעים אחת מהפעולות הבאות:
    • אם לא משנים את המחלקה Application, עורכים את קובץ המניפסט כדי להגדיר את android:name בתג <application> באופן הבא:

      <?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>
    • אם אתם משנים את המחלקה Application, צריך לשנות אותה כך שתרחיב את MultiDexApplication, באופן הבא:

      Kotlin

      class MyApplication : MultiDexApplication() {...}

      Java

      public class MyApplication extends MultiDexApplication { ... }
    • אם אתם משנים את המחלקה Application אבל אי אפשר לשנות את המחלקה הבסיסית, אתם יכולים לשנות את השיטה attachBaseContext() ולקרוא ל-MultiDex.install(this) כדי להפעיל 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);
        }
      }

      זהירות: אל תפעילו את MultiDex.install() או כל קוד אחר באמצעות השתקפות או JNI לפני ש-MultiDex.install() מסתיים. מעקב multidex לא יפעל לפי הקריאות האלה, ויגרום לשגיאות ClassNotFoundException או לשגיאות אימות בגלל חלוקה שגויה של מחלקות בין קובצי DEX.

עכשיו, כשאתם יוצרים את האפליקציה, כלי ה-build של Android יוצרים קובץ DEX ראשי (classes.dex) וקובצי DEX תומכים (classes2.dex, ‏ classes3.dex וכו') לפי הצורך. לאחר מכן, מערכת ה-build אורזת את כל קובצי ה-DEX ב-APK.

בזמן הריצה, במקום לחפש רק בקובץ classes.dex הראשי, ממשקי ה-API של multidex משתמשים ב-class loader מיוחד כדי לחפש את השיטות בכל קובצי ה-DEX הזמינים.

המגבלות של ספריית multidex

לספריית multidex יש כמה מגבלות ידועות. כשמשלבים את הספרייה בתצורת ה-build של האפליקציה, חשוב להביא בחשבון את הנקודות הבאות:

  • ההתקנה של קובצי DEX במהלך ההפעלה במחיצת הנתונים של המכשיר היא מורכבת, ויכולה לגרום לשגיאות מסוג "האפליקציה לא מגיבה" (ANR) אם קובצי ה-DEX המשניים גדולים. כדי להימנע מהבעיה הזו, הפעילו כיווץ קוד כדי להקטין את הגודל של קובצי DEX ולהסיר חלקים בקוד שלא בשימוש.
  • כשמפעילים גרסאות שקודמות ל-Android 5.0 (רמת API‏ 21), השימוש ב-multidex לא מספיק כדי לעקוף את מגבלת linearalloc (בעיה 37008143). המגבלה הזו הוגדלה ב-Android 4.0 (רמת API ‏14), אבל זה לא פתר את הבעיה לגמרי.

    בגרסאות ישנות יותר מ-Android 4.0, יכול להיות שתגיעו למגבלת ה-linearalloc לפני שתגיעו למגבלת האינדקס של DEX. לכן, אם אתם מטרגטים רמות API נמוכות מ-14, כדאי לבדוק ביסודיות את הגרסאות האלה של הפלטפורמה, כי יכול להיות שיהיו בעיות באפליקציה בזמן ההפעלה או כשקבוצות מסוימות של מחלקות נטענות.

    כיווץ הקוד יכול לצמצם או אולי אפילו למנוע את הבעיות האלה.

הצהרה על המחלקות הנדרשות בקובץ ה-DEX הראשי

כשיוצרים כל קובץ DEX לאפליקציית multidex, כלי ה-build מבצעים תהליך מורכב של קבלת החלטות כדי לקבוע אילו מחלקות נדרשות בקובץ ה-DEX הראשי, כדי שהאפליקציה תוכל לפעול בהצלחה. אם מחלקה שנדרשת במהלך ההפעלה לא מסופקת בקובץ ה-DEX הראשי, האפליקציה קורסת עם השגיאה java.lang.NoClassDefFoundError.

כלי ה-build מזהים את נתיבי הקוד של קוד שניגשים אליו ישירות מקוד האפליקציה. עם זאת, הבעיה הזו יכולה להתרחש כשנתיבי הקוד פחות גלויים, למשל כשבספרייה שבה אתם משתמשים יש יחסי תלות מורכבים. לדוגמה, אם הקוד משתמש באינטרוספקציה או בהפעלה של שיטות Java מקוד Native, יכול להיות שהמחלקות האלה לא יזוהו כנדרשות בקובץ ה-DEX הראשי.

אם מקבלים את השגיאה java.lang.NoClassDefFoundError, צריך לציין באופן ידני את המחלקות הנוספות שנדרשות בקובץ ה-DEX הראשי, על ידי הצהרה עליהן באמצעות המאפיין multiDexKeepProguard בסוג ה-build. אם יש התאמה למחלקה בקובץ multiDexKeepProguard, המחלקה הזו מתווספת לקובץ ה-DEX הראשי.

המאפיין multiDexKeepProguard

הקובץ multiDexKeepProguard משתמש באותו פורמט כמו ProGuard ותומך בכל הדקדוק של ProGuard. מידע נוסף על התאמה אישית של מה שנשמר באפליקציה זמין במאמר התאמה אישית של הקוד שרוצים לשמור.

הקובץ שציינת ב-multiDexKeepProguard צריך להכיל -keep אפשרויות בכל תחביר ProGuard חוקי. לדוגמה, -keep com.example.MyClass.class. אפשר ליצור קובץ בשם multidex-config.pro שנראה כך:

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

אם רוצים לציין את כל המחלקות בחבילה, הקובץ נראה כך:

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

לאחר מכן, אפשר להצהיר על הקובץ הזה לסוג build, באופן הבא:

מגניב

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

Kotlin

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

אופטימיזציה של multidex בגרסאות build לפיתוח

הגדרת multidex דורשת זמן עיבוד ארוך משמעותית של ה-build, כי מערכת ה-build צריכה לקבל החלטות מורכבות לגבי המחלקות שצריך לכלול בקובץ ה-DEX הראשי והמחלקות שאפשר לכלול בקובצי ה-DEX המשניים. כלומר, בדרך כלל לוקח יותר זמן ליצור גרסאות build מצטברות באמצעות multidex, וזה עלול להאט את תהליך הפיתוח.

כדי לקצר את זמני ה-build המצטברים, אפשר להשתמש ב-pre-dexing כדי לעשות שימוש חוזר בפלט של multidex בין גרסאות build. ה-Pre-dexing מסתמך על פורמט ART שזמין רק ב-Android מגרסה 5.0 (רמת API‏ 21) ואילך. אם אתם משתמשים ב-Android Studio, סביבת הפיתוח המשולבת (IDE) משתמשת באופן אוטומטי ב-pre-dexing כשאתם פורסים את האפליקציה במכשיר עם Android בגרסה 5.0 (רמת API‏ 21) ומעלה. עם זאת, אם אתם מריצים גרסאות build של Gradle משורת הפקודה, אתם צריכים להגדיר את minSdkVersion ל-21 ומעלה כדי להפעיל את ה-pre-dexing.

כדי לשמור את ההגדרות של גרסת ה-build בסביבת הייצור, אפשר ליצור שתי גרסאות של האפליקציה באמצעות גרסאות המוצר – גרסה אחת עם גרסת פיתוח וגרסה אחת עם גרסת הפצה – עם ערכים שונים של minSdkVersion, כמו בדוגמה הבאה:

מגניב

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")
}

במאמר אופטימיזציה של מהירות ה-build מפורטות אסטרטגיות נוספות שיעזרו לכם לשפר את מהירות ה-build מ-Android Studio או משורת הפקודה. מידע נוסף על השימוש בווריאנטים של build זמין במאמר הגדרת וריאנטים של build.

טיפ: אם יש לכם וריאנטים שונים של build לצרכים שונים של multidex, אתם יכולים לספק קובץ מניפסט שונה לכל וריאנט build, כך שרק הקובץ לרמת API‏ 20 ומטה ישנה את שם התג <application>. אפשר גם ליצור מחלקת משנה שונה של Application לכל וריאנט, כך שרק מחלקת המשנה לרמת API ‏20 ומטה מרחיבה את המחלקה MultiDexApplication או קוראת ל-MultiDex.install(this).

בדיקת אפליקציות multidex

כשכותבים בדיקות אינסטרומנטציה לאפליקציות multidex, לא צריך הגדרות נוספות אם משתמשים באינסטרומנטציה של MonitoringInstrumentation או של AndroidJUnitRunner. אם אתם משתמשים ב-Instrumentation אחר, אתם צריכים לשנות את השיטה onCreate() שלו באמצעות הקוד הבא:

Kotlin

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

Java

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