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

אם לאפליקציה יש minSdk של API 20 ומטה וגם לאפליקציה הספריות שהיא מפנה אליה חורגות מ-65,536 methods, ומקבלים את שגיאת ה-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) מכילים קובצי 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 מגביל אפליקציות לקובץ bytecode יחיד של classes.dex לכל קובץ APK. כדי להגיע ליעד הזה מגבלה, מוסיפים את ספריית multidex ל-build.gradle ברמת המודול, או build.gradle.kts file:

GroovyKotlin
dependencies {
    def multidex_version = "2.0.1"
    implementation "androidx.multidex:multidex:$multidex_version"
}
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 כדי להימנע מהמגבלה של 64K.

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

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

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

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

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

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

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

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

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

    GroovyKotlin
    android {
        defaultConfig {
            ...
            minSdkVersion 15 
            targetSdkVersion 33
            multiDexEnabled true
        }
        ...
    }
    
    dependencies {
        implementation "androidx.multidex:multidex:2.0.1"
    }
    android {
        defaultConfig {
            ...
            minSdk = 15 
            targetSdk = 33
            multiDexEnabled = true
        }
        ...
    }
    
    dependencies {
        implementation("androidx.multidex:multidex:2.0.1")
    }
  2. אם משנים את הערך בשדה Application, class, מבצעים אחת מהפעולות הבאות:
    • אם לא משנים את Application class, לערוך את קובץ המניפסט כדי להגדיר את 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, באופן הבא:

      KotlinJava
      class MyApplication : MultiDexApplication() {...}
      public class MyApplication extends MultiDexApplication { ... }
    • אם מבטלים את הגדרת ברירת המחדל של הכיתה Application אבל אי אפשר לשנות את הכיתה הבסיסית, מבטלים במקום זאת את הגדרת ברירת המחדל של השיטה attachBaseContext() ומפעילים את MultiDex.install(this) כדי להפעיל את multidex:

      KotlinJava
      class MyApplication : SomeOtherApplication() {
      
          override fun attachBaseContext(base: Context) {
              super.attachBaseContext(base)
              MultiDex.install(this)
          }
      }
      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 במהלך ההפעלה במחיצה של נתוני המכשיר היא מורכבת, ועלולה לגרום לשגיאות מסוג Application Not Responding (ANR) אם קובצי ה-DEX המשניים גדולים. כדי למנוע את הבעיה הזו, מפעילים כיווץ קוד כדי למזער את הגודל של קובצי ה-DEX ולהסיר קטעי קוד שלא בשימוש.
  • כשמפעילים גרסאות שקודמות ל-Android 5.0 (רמת API 21), באמצעות Multidex אינו מספיק כדי לעקוף את המגבלה ליניארית (בעיה 37008143). המגבלה הזו הוגדלה ב-Android 4.0 (רמת API 14), אבל הבעיה לא נפתרה לגמרי.

    בגרסאות ישנות יותר מ-Android 4.0, יכול להיות שתגיעו למגבלת linearalloc לפני שתגיעו למגבלת האינדקס של 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, באופן הבא:

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

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

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

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

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

GroovyKotlin
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"
}
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 מ-Android Studio או מהפקודה קוראים את המאמר אופטימיזציה של מהירות ה-build. מידע נוסף על השימוש בווריאציות build זמין בכתובת הגדרה של וריאציות build

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

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

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

KotlinJava
fun onCreate(arguments: Bundle) {
  MultiDex.install(targetContext)
  super.onCreate(arguments)
  ...
}
public void onCreate(Bundle arguments) {
  MultiDex.install(getTargetContext());
  super.onCreate(arguments);
  ...
}