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

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

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

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

תמיכה ב-Multidex לפני Android 5.0

בגרסאות של הפלטפורמה שקדמו ל-Android 5.0‏ (רמת API 21), סביבת זמן הריצה של Dalvik משמשת להרצת קוד האפליקציה. כברירת מחדל, Dalvik מגביל אפליקציות לקובץ בייטקוד אחד (classes.dex) בכל 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) ו-Dalvik.

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

הימנעות מהמגבלה של 64KB

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

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

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

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

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

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

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

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

    מגניב

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

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

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

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

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

    צמצום קוד יכול לצמצם את הבעיות האלה, ואולי גם למנוע אותן.

הצהרת כיתות שנדרשות בקובץ ה-DEX הראשי

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

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

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

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

כדי לשמור את ההגדרות של 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, תוכלו לספק קובץ מניפסט שונה לכל וריאנט, כך שרק הקובץ של API ברמה 20 ומטה ישנה את שם התג <application>. אפשר גם ליצור לכל וריאנט תת-סוג Application שונה, כך שרק תת-הסוג של רמת ה-API 20 ומטה ירחיב את הכיתה MultiDexApplication או יבצע קריאה ל-MultiDex.install(this).

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

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

Kotlin

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

Java

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