dex分包引起的app首次启动过慢问题

dex分包引起的app首次启动过慢问题

android系统中对app引用的方法(包含android框架方法、库方法、以及自己代码的方法)数量有64k的限制。如果引用的方法数超过64kb,则需要引入multidex对apk进行dex分包处理。默认情况下,Dalvik会限制app只有一个class.dex字节码文件,multidex则可以取消这个限制。multidex会成为主dex文件的一部分,然后对其他分dex文件及其所访问的代码进行管理。在app首次启动的时候,主dex文件会进行复杂的计算来确定主dex文件所应该包含那些class文件,引起app首次启动过慢的原因就这里。解决这个问题需要引入multidex.keep文件,来告诉主dex文件首次启动时不需要计算主dex包含的class文件,直接取multidex.keep列出来的文件即可。需要如下步骤,

  1. 在build.gradle同目录中新建multidex.keep文件
  2. 在build.gradle文件中指明主dex文件该包含的class文件应该从multidex.keep文件获取
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"
            }
        }
    }

3.在multidex.keep文件中添加主dex文件所需的class文件。主dex文件所需的class文件可以通过以下方法获取。调用MultiDexUtils的getLoadedExternalDexClasses方法即可获取所需的class文件的list。

import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.os.Build;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;

import dalvik.system.DexFile;

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 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 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 getExternalDexClasses(Context context) throws PackageManager.NameNotFoundException, IOException {
        final List 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 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 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 getLoadedExternalDexClasses(Context context) {
        try {
            final List externalDexClasses = getExternalDexClasses(context);
            if (externalDexClasses != null && !externalDexClasses.isEmpty()) {
                final ArrayList 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;
    }
}

4.再把list数组写到手机sd卡的txt文件中,把txt文件的内容复制到multidex.keep即可。

MultiDexUtils dexUtils = new MultiDexUtils();
        List des = dexUtils.getLoadedExternalDexClasses(this);

        String sdCardDir = Environment.getExternalStorageDirectory().getAbsolutePath();
        File saveFile = new File(sdCardDir, "aaaa.txt");
        try {
            FileOutputStream outStream = new FileOutputStream(saveFile);
            outStream.write(listToString(des).getBytes());
            outStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
public static String listToString(List stringList){
        if(stringList==null) {
            return null;
        }
        StringBuilder result = new StringBuilder();
        boolean flag=false;
        for(String string : stringList) {
            if(flag) {
                result.append("\n");
            }else{
                flag=true;
            }
            result.append(string);
        }
        return result.toString();
    }

经过我公司的app测试,可优化app首次启动从4秒左右减到1秒左右。优化还是挺明显的。

你可能感兴趣的:(android)