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

אם באפליקציה שלכם minSdk של API 20 או גרסה מוקדמת יותר, והאפליקציה והספריות שהיא מפנה אליהן חורגות מ-65,536 שיטות, תופיע שגיאת הבנייה הבאה שמציינת שהאפליקציה הגיעה למגבלה של ארכיטקטורת הבנייה של 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. המספר הזה מייצג את המספר הכולל של ההפניות שאפשר להפעיל באמצעות הקוד בקובץ בייטקוד (bytecode) אחד של Dalvik Executable ‏(DEX). בדף הזה מוסבר איך לעקוף את המגבלה הזו באמצעות הפעלת הגדרת אפליקציה שנקראת multidex, שמאפשרת לאפליקציה ליצור ולקרוא כמה קובצי DEX.

מידע על מגבלת ההפניה של 64K

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

בהקשר של מדעי המחשב, המונח קילו, או K, מציין 1,024 (או 2^10). מכיוון ש-65,536 שווה ל-64x1024, המגבלה הזו נקראת _מגבלת ההפניה של 64K_.

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

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

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

הספרייה הזו הופכת לחלק מקובץ ה-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 Runtime (ART) and Dalvik.

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

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

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

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

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

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

הגדרת האפליקציה לשימוש ב-multidex

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

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

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

    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. בהתאם להגדרה של Application class, מבצעים אחת מהפעולות הבאות:
    • אם לא מבטלים את ברירת המחדל של המחלקה 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 class אבל אי אפשר לשנות את מחלקת הבסיס, אתם צריכים לבטל את ההגדרה של השיטה 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() יסתיים. המעקב אחר קובצי DEX מרובים לא יכלול את הקריאות האלה, ולכן יופיעו שגיאות ClassNotFoundException או שגיאות אימות בגלל חלוקת מחלקות שגויה בין קובצי DEX.

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

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

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

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

  • ההתקנה של קובצי 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 לאפליקציה עם כמה קובצי DEX, כלי הבנייה מבצעים תהליך מורכב של קבלת החלטות כדי לקבוע אילו מחלקות נדרשות בקובץ ה-DEX הראשי, כדי שהאפליקציה תוכל להתחיל לפעול בהצלחה. אם לא מספקים בקובץ ה-DEX הראשי מחלקה שנדרשת במהלך ההפעלה, האפליקציה קורסת עם השגיאה java.lang.NoClassDefFoundError.

כלי הבנייה מזהים את נתיבי הקוד של קוד שאליו יש גישה ישירה מקוד האפליקציה. עם זאת, הבעיה הזו יכולה להתרחש כשנתיבי הקוד פחות גלויים, למשל כשספרייה שבה אתם משתמשים כוללת תלות מורכבת. לדוגמה, אם הקוד משתמש באינטרוספקציה או בהפעלת שיטות Java מקוד מקורי, יכול להיות שהמחלקות האלה לא יזוהו כנדרשות בקובץ ה-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, באופן הבא:

Groovy

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

Kotlin

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

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

הגדרת multidex דורשת זמן עיבוד ארוך יותר באופן משמעותי, כי מערכת ה-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) ומעלה. עם זאת, אם אתם מריצים Gradle builds משורת הפקודה, אתם צריכים להגדיר את minSdkVersion ל-21 ומעלה כדי להפעיל pre-dexing.

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

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

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

טיפ: אם יש לכם וריאציות שונות של build לצרכים שונים של multidex, אתם יכולים לספק קובץ מניפסט שונה לכל וריאציה, כך שרק הקובץ לרמת 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);
  ...
}