راهکاری جهت مقابله با کاهش سرعت راه اندازی اپلیکیشن ناشی از multidex اندروید

راهکاری جهت مقابله با کاهش سرعت راه اندازی اپلیکیشن ناشی از multidex اندروید

محدودیت 65K متدها یکی از مشکلاتی است که بارها و بارها در انجمن های اندرویدی به بحث گذاشته شده و multidexing به عنوان روشی برای این مشکل پیشنهاد شده است. Multidexing راه حلی بسیار خوبی از سوی گوگل است، اما تاثیر چشمگیری بر روی کارآیی اپلیکیشن در هنگام راه اندازی دارد، از این رو در این مقاله به راه حلی جایگزین پرداخته می شود که تاثیرات منفی multidexing را ندارد.

اپلیکیشن های اندروید به زبان جاوا نوشته شده و به یک فایل class. تبدیل می شوند. این فایل در یک فایل منفرد classes.dex کامپایل می شود. این فایل dex همراه با تمامی منابع مورد نیاز در یک فایل apk جای می گیرد و بسته نهایی که از اپ استور دانلود و بر روی گوشی خود نصب می کنید آماده می شود.

یکی از محدودیت های مربوط به این نوع فرآیند کامپایل این است که سیستم محدودیتی 65K برای متدهای موجود در یک فایل dex قائل شده. در روزهای نخست روی کار آمدن اندروید اپلیکیشن هایی که محدودیت 65K برای متدها را رد می کردند از Proguard برای حذف کدهای استفاده نشده بهره می بردند و بدین ترتیب حجم کاسته می شد. اما این رویکرد نیز خود دارای محدودیت هایی است و تنها رسیدن به محدودیت تعریف شده را به تعویق می اندازد.

در راستای این مشکل گوگل اقدام به انتشار راه حلی برای کتابخانه های پشتیبانی اخیر اندروید جهت پرداختن به این محدودیت متدها نمود. این متد کاربردی است و شما را قادر به رهایی از مشکل محدودیت می نماید، اما تاثیر چشمگیری بر روی کارآیی می گذارد، بدین ترتیب که سرعت راه اندازی اپلیکیشن را کاهش می دهد.

راه اندازی multidex

Multidexing یک راه حل کامل و همراه با مستندسازی های خوب است. به منظور بکارگیری multidex در پروژه پیشنهاد می شود که نگاهی به Android Developer site بیندازید، my Github نیز یک پروژه نمونه را برای آشنایی بیشتر در اختیار شما قرار می دهد.

خطای NoClassDefFoundError

در هنگام راه اندازی multidexing برای پروژه احتمال دارد در حین اجرای اپلیکیشن با خطای ajava.lang.NoClassDefFoundError  مواجه شوید، این خطا به این معنا است که کلاس برای راه اندازی اپلیکیشن در فایل اصلی dex قرار نگرفته است. پلاگین اندروید برای Gradle در Android SDK Build Tools 21.1 یا نسخه های بالاتر از multidex پشتیبانی می کند. این پلاگین از Proguard به منظور آنالیز کردن پروژه و تولید لیستی از کلاس ها برای راه اندازی اپلیکیشن در فایل [buildDir]/intermediates/multi-dex/[buildType]/maindexlist.txt بهره می گیرد.

با این وجود این فهرست 100% دقیق نیست و احتمال دارد تعدادی از کلاس های لازم برای راه اندازی اپلیکیشن را جا انداخته باشد.

YesClassDefFound

به منظور حل این خطا باید کلاس ها را در فایل multidex.keep فهرست کنید تا کامپایلر بداند کدام کلاس ها را باید در فایل Dex اصلی نگه دارد:

- یک فایل multidex.keep در پوشه پروژه بسازید.

- کلاس های java.lang.NoClassDefFoundError  را در فایل multidex.keep  فهرست کنید. توجه داشته باشید که فایل maindexlist.txt  موجود در پوشه بیلد را مستقیما دستکاری نکنید، این فایل با هر بار کامپایل شدن اپلیکیشن بوجود می آید.

- اسکریپت زیر را به build.gradle در اپلیکیشن بیفزایید، این اسکریپت در حین کامپایل پروژه multidex.keep و maindexlist.txt تولید شده توسط Gradle را ترکیب می کند.

تاثیرگذاری راه اندازی اپلیکیشن با Multidex بر کارآیی

چنانچه از Multidex استفاده می کنید لازم است از تاثیراتی که بر کارآیی اپلیکیشن در هنگام راه اندازی می گذارد نیز آگاه باشید. زمان راه اندازی را می توان به صورت زمانی که کاربر بر روی آیکون اپلیکیشن کلیک می کند و زمانی که تمامی تصاویر دانلود شده و نمایش داده می شوند تعریف کرد. زمانی که Multidex فعال باشد، زمان راه اندازی اپلیکیشن به میزان 15% بیشتر ازحالت معمولی بر روی گوشی هایی اندروید با نسخه 4.4 یا کمتر می باشد.

نسخه های اندروید 5.0 و بالاتر از ران تایمی به نام ART استفاده می کنند که به طور نیتیو از بارگذاری فایل های dex چندگانه از فایل های APK  اپلیکیشن پشتیبانی می کند. با این وجود گوشی هایی با نسخه کمتر از 5 هنگام بارگذاری کلاس های خارج از فایل dex اصلی با سربار اضافی مواجه می شوند.

android.applicationVariants.all { variant ->

task "fix${variant.name.capitalize()}MainDexClassList" << {

logger.info "Fixing main dex keep file for $variant.name"

File keepFile = new File("$buildDir/intermediates/multi-dex/$variant.buildType.name/maindexlist.txt")

keepFile.withWriterAppend { w ->

// Get a reader for the input file

w.append('\n')

new File("${projectDir}/multidex.keep").withReader { r ->

// And write data from the input into the output

w << r << '\n'

}

logger.info "Updated main dex keep file for ${keepFile.getAbsolutePath()}\n$keepFile.text"

}

}

}

tasks.whenTaskAdded { task ->

android.applicationVariants.all { variant ->

if (task.name == "create${variant.name.capitalize()}MainDexClassList") {

task.finalizedBy "fix${variant.name.capitalize()}MainDexClassList"

}

}

}

تاثیر multidex بر کارآیی اپلیکیشن در هنگام راه اندازی

برخی از کلاس ها در بازه زمانی بین شروع اپلیکیشن و نمایش تمامی عکس ها توسط Proguard شناسایی نمی شوند در حالی که در فایل dex اصلی ذخیره شده اند. سوالی که مطرح می شود این است که چگونه از لود شدن این کلاس ها در حین راه اندازی اپلیکیشن مطلع شویم.

خوشبختانه متدی به نام findLoadedClass در ClassLoader وجود دارد، کار لازم بررسی ران تایم پس از پایان راه اندازی اپلیکیشن می باشد. پس از آن هر کلاسی که در فایل dex دوم ذخیره شده و در حین راه اندازی اپلیکیشن بارگذاری می شود به فایل dex اصلی جابجا شده و نام کلاس در فایل multidex.keep افزوده می شود. My sample project این جزئیات را به تصویر کشیده است، اما می توانید کار را به صورت زیر نیز به انجام برسانید:

اجرای getLoadedExternalDexClasses  در کلاس زیر تا زمانی که راه اندازی اپلیکیشن پایان یافته باشد.

افزودن لیستی به فایل multidex.keep  توسط متد بالا و کامپایل مجدد.

public class MultiDexUtils {

private static final String EXTRACTED_NAME_EXT = ".classes";

private static final String EXTRACTED_SUFFIX = ".zip";

private static final String SECONDARY_FOLDER_NAME = "code_cache" + File.separator +

"secondary-dexes";

private static final String PREFS_FILE = "multidex.version";

private static final String KEY_DEX_NUMBER = "dex.number";

private SharedPreferences getMultiDexPreferences(Context context) {

return context.getSharedPreferences(PREFS_FILE,

Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB

? Context.MODE_PRIVATE

: Context.MODE_PRIVATE | Context.MODE_MULTI_PROCESS);

}

/**

* get all the dex path

*

* @param context the application context

* @return all the dex path

* @throws PackageManager.NameNotFoundException

* @throws IOException

*/

public List<String> getSourcePaths(Context context) throws PackageManager.NameNotFoundException, IOException {

final ApplicationInfo applicationInfo = context.getPackageManager().getApplicationInfo(context.getPackageName(), 0);

final File sourceApk = new File(applicationInfo.sourceDir);

final File dexDir = new File(applicationInfo.dataDir, SECONDARY_FOLDER_NAME);

final List<String> sourcePaths = new ArrayList<>();

sourcePaths.add(applicationInfo.sourceDir); //add the default apk path

//the prefix of extracted file, ie: test.classes

final String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT;

//the total dex numbers

final int totalDexNumber = getMultiDexPreferences(context).getInt(KEY_DEX_NUMBER, 1);

for (int secondaryNumber = 2; secondaryNumber <= totalDexNumber; secondaryNumber++) {

//for each dex file, ie: test.classes2.zip, test.classes3.zip...

final String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX;

final File extractedFile = new File(dexDir, fileName);

if (extractedFile.isFile()) {

sourcePaths.add(extractedFile.getAbsolutePath());

//we ignore the verify zip part

} else {

throw new IOException("Missing extracted secondary dex file '" +

extractedFile.getPath() + "'");

}

}

return sourcePaths;

}

/**

* get all the external classes name in "classes2.dex", "classes3.dex" ....

*

* @param context the application context

* @return all the classes name in the external dex

* @throws PackageManager.NameNotFoundException

* @throws IOException

*/

public List<String> getExternalDexClasses(Context context) throws PackageManager.NameNotFoundException, IOException {

final List<String> paths = getSourcePaths(context);

if(paths.size() <= 1) {

// no external dex

return null;

}

// the first element is the main dex, remove it.

paths.remove(0);

final List<String> classNames = new ArrayList<>();

for (String path : paths) {

try {

DexFile dexfile = null;

if (path.endsWith(EXTRACTED_SUFFIX)) {

//NOT use new DexFile(path), because it will throw "permission error in /data/dalvik-cache"

dexfile = DexFile.loadDex(path, path + ".tmp", 0);

} else {

dexfile = new DexFile(path);

}

final Enumeration<String> dexEntries = dexfile.entries();

while (dexEntries.hasMoreElements()) {

classNames.add(dexEntries.nextElement());

}

} catch (IOException e) {

throw new IOException("Error at loading dex file '" +

path + "'");

}

}

return classNames;

}

/**

* Get all loaded external classes name in "classes2.dex", "classes3.dex" ....

* @param context

* @return get all loaded external classes

*/

public List<String> getLoadedExternalDexClasses(Context context) {

try {

final List<String> externalDexClasses = getExternalDexClasses(context);

if (externalDexClasses != null && !externalDexClasses.isEmpty()) {

final ArrayList<String> classList = new ArrayList<>();

final java.lang.reflect.Method m = ClassLoader.class.getDeclaredMethod("findLoadedClass", new Class[]{String.class});

m.setAccessible(true);

final ClassLoader cl = context.getClassLoader();

for (String clazz : externalDexClasses) {

if (m.invoke(cl, clazz) != null) {

classList.add(clazz.replaceAll("\\.", "/").replaceAll("$", ".class"));

}

}

return classList;

}

} catch (Exception e) {

e.printStackTrace();

}

return null;

}

}

نتایج

در زیر بهبودهای کارآیی در راه اندازی اپلیکیشن که بر روی گوشی های متعددی مشاهده شده به نمایش گذاشته شده است. ستون اول زمان راه اندازی پایه بدون استفاده از multidexing می باشد، شما قادر به مشاهده افزایش چشمگیری در ستون دوم، یعنی شرایطی که multidexing فعال شده می باشید. ستون سوم زمان راه اندازی اپلیکیشن با فعال سازی multidexing و بهبودهای شرح داده شده در بالا را به نمایش گذاشته است. همانطور که در نمودار مشخص است زمان راه اندازی اپلیکیشن به شرایط اولیه و حتی سریع تر از آن ارتقا پیدا کرده، با این اوصاف به کارگیری ترفندهای گفته شده کاربردی خواهد بود.

blog_15691_1

multidexing کارآیی را تحت تاثیر قرار می دهد، بنابراین تنها در شرایطی که واقعا استفاده از آن تنها راه حل است آن را مورد استفاده قرار دهید، در این روش جهت رفع خطاهای غیرمعمول به افزودن کدهایی نیاز دارید. به جای استفاده از multidexing در صورت دستیابی به محدودیت 65K برای متدها اقدام به سنجش معیارها و بررسی SDK کنید و کدهای بی استفاده را حذف نمایید. تنها زمانی که راهکار دیگری وجود نداشته باشد استفاده از multidexing جایز است، در این شرایط کیفیت کد و استانداردها به طور چشمگیری بهبود می یابد. به جای استفاده از multidexing بهتر است کدهای خود را نظم دهید، هر آنچه اضافه است را حذف کنید، از کامپوننت های موجود استفاده مجدد کنید و یا کد خود را ریفکتورینگ کنید تا از چنگ محدودیت 65K خلاص شوید.

 

https://medium.com برگرفته از

اینها را هم بخوانید