优秀的编程知识分享平台

网站首页 > 技术文章 正文

如何解决使用MultiDex方案导致系统版本5.0以下手机启动慢的问题

nanyue 2024-08-09 07:02:54 技术文章 6 ℃

虽然Android 5.0及其以上系统允许加载多dex了,但是对于低于5.0版本的系统来说,由于Android系统最开始只允许加载一个dex,而一个dex的方法数又不能65535,所以程序在启动时会出现超出方法数错误。

为了解决这个问题,Google提出了MultiDex方案。但是Android的小伙伴们引用了这个方案后会发现,虽然MultiDex方案解决了低版本多dex问题,但是启动速度变慢了。坑爹了。

那么,我们如何优化它,解决启动速度慢的问题呢?

我在《MultiDex源码解析》这篇文章中讲解了MultiDex原理,我们会发现之所以引入MultiDex方案启动速度会变慢,是因为Dalvik虚拟机在加载非主dex(classes2.dex, classes3.dex....)文件时,需要解压apk,并压缩dex文件,转成odex文件。这些都是耗时操作。那么,如果我们将启动的所有类都加载到主dex(classes.dex)中,是不是就能加快启动速度了呢?

那么,就需要我们先找出哪些类是启动时需要,但是却不在主dex中的。我们可以使用以下代码找出这些类。

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 static 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
     */
    private static 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
     */
    private static 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 static 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("#34;, ".class"));
                    }
                }
                return classList;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

调用MultiDexUtils的静态方法getLoadedExternalDexClasses可以找出这些类。

然后,我们将这些类放到multidex-config.txt文件中。

并将这个文件配置到app目录下的build.gradle文件中。

multiDexKeepFile file('multidex-config.txt')

这样,这些类就会打包到主dex中了。

最近发表
标签列表