一.简介

MultiDex优化至关重要,下面会讲解到,因为它比较耗时,所以它的优化对APP启动速度的影响是很大的。

 

 

 

二.apk编译流程

 

Android Studio 按下编译按钮后执行了什么呢。

 

1.打包资源文件,生成R.java文件(使用工具AAPT)。

2.编译 java 文件,生成对应.class文件(java compiler)。

3.class 文件转换成dex文件(dex)

 

将class文件转换成dex文件时,默认只会生成一个dex文件,单个dex文件中的方法数不能超过65536,不然编译会报错:

Unable to execute dex: method ID not in [0, 0xffff]: 65536

项目中,项目一大,方法数一般都是超过65536的。

 

解决办法就是:一个dex装不下,用多个dex来装。

 

gradle增加一行配置即可。

multiDexEnabled true
android {
    compileSdkVersion 30
    buildToolsVersion "30.0.0"

    defaultConfig {
        applicationId "com.example.mytest"
        minSdkVersion 16
        targetSdkVersion 30
        versionCode 1
        versionName "1.0"

        multiDexEnabled true

    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

}

添加了这行依赖,也就是将一个dex文件,分成多个dex文件。这样解决了编译问题,可是在5.0以下的手机可能存在问题。因为Android 5.0以下,ClassLoader加载类的时候只会从class.dex(主dex)里加载,ClassLoader不认识其它的class2.dex、class3.dex、...,当访问到不在主dex中的类的时候,就会报错:Class NotFound xxx,因此谷歌给出兼容方案 MultiDex。

 

 

 

 

三.MultiDex 原理

上面讲到,class文件转换成dex文件时,如果项目中方法太多会报错。所以要在项目中的Gradle中配置 multiDexEnabled true。即用MultiDex将class文件加载成的一个dex文件用多个dex来装。既然用到了MultiDex,我们就来简单的讲解以下MultiDex的原理。

 

1.使用

public class MyApplication extends Application {

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        MultiDex.install(base);
    }
}

 

 

2.源码

public static void install(Context context) {
    Log.i("MultiDex", "Installing application");
    if (IS_VM_MULTIDEX_CAPABLE) {
        Log.i("MultiDex", "VM has multidex support, MultiDex support library is disabled.");
    } else if (VERSION.SDK_INT < 4) {
        throw new RuntimeException("MultiDex installation failed. SDK " + VERSION.SDK_INT + " is unsupported. Min SDK version is " + 4 + ".");
    } else {
        try {
            ApplicationInfo applicationInfo = getApplicationInfo(context);
            if (applicationInfo == null) {
                Log.i("MultiDex", "No ApplicationInfo available, i.e. running on a test Context: MultiDex support library is disabled.");
                return;
            }

            doInstallation(context, new File(applicationInfo.sourceDir), new File(applicationInfo.dataDir), "secondary-dexes", "", true);
        } catch (Exception var2) {
            Log.e("MultiDex", "MultiDex installation failure", var2);
            throw new RuntimeException("MultiDex installation failed (" + var2.getMessage() + ").");
        }

        Log.i("MultiDex", "install done");
    }
}

源码可知

IS_VM_MULTIDEX_CAPABLE=true

如果这个条件成立,则不做操作。相当于install方法不执行。那么IS_VM_MULTIDEX_CAPABLE字段代表什么意思呢?

private static final boolean IS_VM_MULTIDEX_CAPABLE = isVMMultidexCapable(System.getProperty("java.vm.version"));

isVMMultidexCapable方法和System.getProperty("java.vm.version")方法是做什么的呢?

测试代码

public class MultiDexActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_multidex);
        String jvmVersion = System.getProperty("java.vm.version");
        boolean is = isVMMultidexCapable(jvmVersion);
        Log.d("MultiDexActivity", "jvmVersion----:" + jvmVersion);
        Log.d("MultiDexActivity", "is----:" + is);
    }

    public boolean isVMMultidexCapable(String versionString) {
        boolean isMultidexCapable = false;
        if (versionString != null) {
            Matcher matcher = Pattern.compile("(\\d+)\\.(\\d+)(\\.\\d+)?").matcher(versionString);
            if (matcher.matches()) {
                try {
                    int major = Integer.parseInt(matcher.group(1));
                    int minor = Integer.parseInt(matcher.group(2));
                    isMultidexCapable = major > 2 || major == 2 && minor >= 1;
                } catch (NumberFormatException var5) {
                }
            }
        }

        Log.i("MultiDex", "VM with version " + versionString + (isMultidexCapable ? " has multidex support" : " does not have multidex support"));
        return isMultidexCapable;
    }
}

测试结果

D/MultiDexActivity: jvmVersion----:2.1.0


D/MultiDexActivity: is----:true

System.getProperty("java.vm.version"):虚拟机实现的版本。

isVMMultidexCapable方法:判断当前设备是否支持多个dex文件。

 

 

小结1

Android 5.0以上VM基本支持多dex。也就是说其实MultiDex.install(base);这个方法在Android 5.0以上的系统是不会起什么作用的。因为Android 5.0以上的系统本身支持多个dex文件加载。

 

 

继续

VERSION.SDK_INT < 4

如果这个条件成立,抛出异常。即Android系统版本小于4时,抛出异常。因为现在Android系统版本小于4的设备已经可以忽略。所以这个条件忽略。

 

继续

也就是说,如果设备不支持多个dex文件,且Android系统版本大于4。其实也就是Android5.0版本下的系统会走到下面的代码。

ApplicationInfo applicationInfo = getApplicationInfo(context);
if (applicationInfo == null) {
      Log.i("MultiDex", "No ApplicationInfo available, i.e. running on a test Context: MultiDex support library is disabled.");
      return;
}

doInstallation(context, new File(applicationInfo.sourceDir), new File(applicationInfo.dataDir), "secondary-dexes", "", true);

也就是,先获取ApplicationInfo对象,如果此对象不为空。则执行doInstallation()方法。

 

 

小结2

Android5.0系统及以上的版本,因为默认支持加载多个dex文件。所以MultiDex.install(base)方法只在Android5.0以下的版本起作用。

 

继续

doInstallation()方法源码

private static void doInstallation(Context mainContext, File sourceApk, File dataDir, String secondaryFolderName, String prefsKeyPrefix, boolean reinstallOnPatchRecoverableException) throws IOException, IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException, SecurityException, ClassNotFoundException, InstantiationException {
    synchronized(installedApk) {
        if (!installedApk.contains(sourceApk)) {
            installedApk.add(sourceApk);
            if (VERSION.SDK_INT > 20) {
                    Log.w("MultiDex", "MultiDex is not guaranteed to work in SDK version " + VERSION.SDK_INT + ": SDK version higher than " + 20 + " should be backed by " + "runtime with built-in multidex capabilty but it's not the " + "case here: java.vm.version=\"" + System.getProperty("java.vm.version") + "\"");
            }

            ClassLoader loader;
            try {
                loader = mainContext.getClassLoader();
            } catch (RuntimeException var25) {
                Log.w("MultiDex", "Failure while trying to obtain Context class loader. Must be running in test mode. Skip patching.", var25);
                return;
            }

            if (loader == null) {
                Log.e("MultiDex", "Context class loader is null. Must be running in test mode. Skip patching.");
            } else {
                try {
                    clearOldDexDir(mainContext);
                } catch (Throwable var24) {
                    Log.w("MultiDex", "Something went wrong when trying to clear old MultiDex extraction, continuing without cleaning.", var24);
                }

                File dexDir = getDexDir(mainContext, dataDir, secondaryFolderName);
                MultiDexExtractor extractor = new MultiDexExtractor(sourceApk, dexDir);
                IOException closeException = null;

                try {
                    List files = extractor.load(mainContext, prefsKeyPrefix, false);

                    try {
                        installSecondaryDexes(loader, dexDir, files);
                    } catch (IOException var26) {
                        if (!reinstallOnPatchRecoverableException) {
                            throw var26;
                        }

                        Log.w("MultiDex", "Failed to install extracted secondary dex files, retrying with forced extraction", var26);
                        files = extractor.load(mainContext, prefsKeyPrefix, true);
                        installSecondaryDexes(loader, dexDir, files);
                    }
                } finally {
                    try {
                        extractor.close();
                    } catch (IOException var23) {
                        closeException = var23;
                    }

                }

                if (closeException != null) {
                    throw closeException;
                }
            }
        }
    }
}

doInstallation()方法中。首先再次判断系统版本是否大于20。即系统版本是5.0即以上。不做处理。再次证明Android5.0版本及以上。默认就支持加载多个dex文件。

然后获取ClassLoader对象。如果此对象不为空。继续。

 

清空旧的dex文件

clearOldDexDir(mainContext);
private static void clearOldDexDir(Context context) throws Exception {
    File dexDir = new File(context.getFilesDir(), "secondary-dexes");
    if (dexDir.isDirectory()) {
        Log.i("MultiDex", "Clearing old secondary dex dir (" + dexDir.getPath() + ").");
        File[] files = dexDir.listFiles();
        if (files == null) {
            Log.w("MultiDex", "Failed to list secondary dex dir content (" + dexDir.getPath() + ").");
            return;
        }

        File[] var3 = files;
        int var4 = files.length;

        for(int var5 = 0; var5 < var4; ++var5) {
            File oldFile = var3[var5];
            Log.i("MultiDex", "Trying to delete old file " + oldFile.getPath() + " of size " + oldFile.length());
            if (!oldFile.delete()) {
                Log.w("MultiDex", "Failed to delete old file " + oldFile.getPath());
            } else {
                Log.i("MultiDex", "Deleted old file " + oldFile.getPath());
            }
        }

        if (!dexDir.delete()) {
            Log.w("MultiDex", "Failed to delete secondary dex dir " + dexDir.getPath());
        } else {
            Log.i("MultiDex", "Deleted old secondary dex dir " + dexDir.getPath());
        }
    }

}

 

获取非主dex文件

File dexDir = getDexDir(mainContext, dataDir, secondaryFolderName);
MultiDexExtractor extractor = new MultiDexExtractor(sourceApk, dexDir);

 

然后 加载dex文件 这个方法第一次执行时没有缓存,会非常耗时的。

List files = extractor.load(mainContext, prefsKeyPrefix, false);
List<? extends File> load(Context context, String prefsKeyPrefix, boolean forceReload) throws IOException {
    Log.i("MultiDex", "MultiDexExtractor.load(" + this.sourceApk.getPath() + ", " + forceReload + ", " + prefsKeyPrefix + ")");
    if (!this.cacheLock.isValid()) {
        throw new IllegalStateException("MultiDexExtractor was closed");
    } else {
        List files;
        if (!forceReload && !isModified(context, this.sourceApk, this.sourceCrc, prefsKeyPrefix)) {//有缓存
            try {
                //获取缓存的dex文件
                files = this.loadExistingExtractions(context, prefsKeyPrefix);
            } catch (IOException var6) {
                Log.w("MultiDex", "Failed to reload existing extracted secondary dex files, falling back to fresh extraction", var6);
                //如果 catch住 即有异常 可能是dex文件损坏了,重新去解压apk读取,跟else代码块一样
                files = this.performExtractions();
                //保存信息到SharedPreferences
                putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(this.sourceApk), this.sourceCrc, files);
            }
        } else {//没有缓存
            if (forceReload) {
                Log.i("MultiDex", "Forced extraction must be performed.");
            } else {
                Log.i("MultiDex", "Detected that extraction must be performed.");
            }

            //去解压apk读取
            files = this.performExtractions();
            //保存信息到SharedPreferences
            putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(this.sourceApk), this.sourceCrc, files);
        }

        Log.i("MultiDex", "load found " + files.size() + " secondary dex files");
        return files;
    }
}

 

解压APK方法

private List<MultiDexExtractor.ExtractedDex> performExtractions() throws IOException {
    String extractedFilePrefix = this.sourceApk.getName() + ".classes";
    this.clearDexDir();
    List<MultiDexExtractor.ExtractedDex> files = new ArrayList();

    //apk转为zip格式
    ZipFile apk = new ZipFile(this.sourceApk);

    try {
        int secondaryNumber = 2;

        //apk已经是改为zip格式了,解压遍历zip文件,获取里面的dex文件
        for(ZipEntry dexFile = apk.getEntry("classes" + secondaryNumber + ".dex"); dexFile != null; dexFile = apk.getEntry("classes" + secondaryNumber + ".dex")) {
            String fileName = extractedFilePrefix + secondaryNumber + ".zip";
            MultiDexExtractor.ExtractedDex extractedFile = new MultiDexExtractor.ExtractedDex(this.dexDir, fileName);
            files.add(extractedFile);
            Log.i("MultiDex", "Extraction is needed for file " + extractedFile);
            int numAttempts = 0;
            boolean isExtractionSuccessful = false;

            while(numAttempts < 3 && !isExtractionSuccessful) {
                ++numAttempts;
                extract(apk, dexFile, extractedFile, extractedFilePrefix);

                try {
                    extractedFile.crc = getZipCrc(extractedFile);
                    isExtractionSuccessful = true;
                } catch (IOException var18) {
                    isExtractionSuccessful = false;
                    Log.w("MultiDex", "Failed to read crc from " + extractedFile.getAbsolutePath(), var18);
                }

                Log.i("MultiDex", "Extraction " + (isExtractionSuccessful ? "succeeded" : "failed") + " '" + extractedFile.getAbsolutePath() + "': length " + extractedFile.length() + " - crc: " + extractedFile.crc);
                if (!isExtractionSuccessful) {
                    extractedFile.delete();
                    if (extractedFile.exists()) {
                        Log.w("MultiDex", "Failed to delete corrupted secondary dex '" + extractedFile.getPath() + "'");
                    }
                }
            }

            if (!isExtractionSuccessful) {
                throw new IOException("Could not create zip file " + extractedFile.getAbsolutePath() + " for secondary dex (" + secondaryNumber + ")");
            }

            ++secondaryNumber;
        }
    } finally {
        try {
            apk.close();
        } catch (IOException var17) {
            Log.w("MultiDex", "Failed to close resource", var17);
        }

    }

    return files;
}

Android V8 引擎优化 android dex优化_MultiDex  65536

这个方法大概的逻辑是解压apk,遍历出里面的dex文件,例如class2.dex,class3.dex,然后又压缩成class2.zip,class3.zip...,然后返回zip文件列表。

 

安装dex文件

installSecondaryDexes(loader, dexDir, files);
private static void installSecondaryDexes(ClassLoader loader, File dexDir, List<? extends File> files) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException, SecurityException, ClassNotFoundException, InstantiationException {
    if (!files.isEmpty()) {
        if (VERSION.SDK_INT >= 19) {
            MultiDex.V19.install(loader, files, dexDir);
        } else if (VERSION.SDK_INT >= 14) {
            MultiDex.V14.install(loader, files);
        } else {
            MultiDex.V4.install(loader, files);
        }
     }

}

也就是说,安装dex文件时,不同系统的方法有差别。

 

 

 

 

四.优化方案

上述可知,项目中方法太多时,要将class文件加载成多个dex文件。本身只需Gradle配置一行代码即可。可是有的设备不支持加载多个dex文件,所以谷歌推荐MultiDex 解决。由上述代码可知 MultiDex.install(this);在不同的设备上有不同的表现形式,所以就会有不同的耗时,耗时太多对启动速度有很大的影响,所以要优化。

 

代码

package com.example.mytest;

import android.os.Bundle;
import android.util.Log;

import androidx.appcompat.app.AppCompatActivity;
import androidx.multidex.MultiDex;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        long starttime = System.currentTimeMillis();

        MultiDex.install(this);

        long endtime = System.currentTimeMillis();

        long time = endtime - starttime;
        Log.d("MainActivity", "time----:" + time);
    }


}

 

结果

高端机运行多次

D/MainActivity: time----:0

 

Android 4.4的机器运行多次

D/MainActivity: time----:1000

也就是说在某些设备上还是耗时的,源码中有介绍(设备本身不支持多dex文件的,要使用ClassLoader做各种操作)。

 

 

下面讲一下优化方案。

 

1.子线程执行

比如在闪屏页Activity中,子线程执行MultiDex.install(base);方法。

需要注意的是闪屏页的Activity,包括闪屏页中引用到的其它类必须在主dex中,不然在MultiDex.install之前加载这些不在主dex中的类会报错Class Not Found。

 

Gradle额外配置

defaultConfig {
    
    ....
  
    multiDexEnabled true
    multiDexKeepProguard file('multiDexKeep.pro') //打包到主dex的这些类的混淆规制,没特殊需求就给个空文件。
    multiDexKeepFile file('maindexlist.txt') //指定哪些类要放到主dex中 maindexlist.txt文件
}

maindexlist.txt 文件指定哪些类要打包到主dex中,内容格式如下

com/example/mytest/MainActivity.class

如果,少指定了一些必须放在主dex文件中的类。可能会出现NoClassDefFoundError的异常。

一般都是该类没有在主dex中,要在maindexlist.txt 将配置指定在主dex。

还有就是 第三方库中的ContentProvider必须指定在主dex中。因为ContentProvider的onCreate方法在Application的attachBaseContext()方法和onCreate()方法之间。

 

举例

 

Application

public class MyApplication extends Application {

    @Override
    protected void attachBaseContext(final Context base) {
        super.attachBaseContext(base);
        MultiDex.install(base);
        Log.d("MyApplication","MyApplication类attachBaseContext方法执行!!!");
    }

    @Override
    public void onCreate() {
        super.onCreate();
        Log.d("MyApplication","MyApplication类onCreate方法执行!!!");
    }
}

 

ContentProvider

public class MyContentProvider extends ContentProvider {

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        throw new UnsupportedOperationException("Not yet implemented");
    }

    @Override
    public String getType(Uri uri) {
        throw new UnsupportedOperationException("Not yet implemented");
    }

    @Override
    public Uri insert(Uri uri, ContentValues values) {
        throw new UnsupportedOperationException("Not yet implemented");
    }

    @Override
    public boolean onCreate() {
        Log.d("MyApplication","ContentProvider类onCreate方法执行!!!");
        return false;
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
        throw new UnsupportedOperationException("Not yet implemented");
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        throw new UnsupportedOperationException("Not yet implemented");
    }
}

 

冷启动

D/MyApplication: MyApplication类attachBaseContext方法执行!!!


D/MyApplication: ContentProvider类onCreate方法执行!!!


D/MyApplication: MyApplication类onCreate方法执行!!!

证明ContentProvider类onCreate方法在Application类attachBaseContext方法和Application类onCreate方法之间执行。

 

 

小结1

在 闪屏页 中开启子线程 执行  MultiDex.install(base);方法 需要注意两点

<1> MultiDex加载逻辑放在闪屏页的话,闪屏页中引用到的类都要配置在主dex。

<2> ContentProvider必须在主dex,一些第三方库自带ContentProvider,维护比较麻烦,要一个一个配置。

 

当然,一种处理的方法就是在Application的attachBaseContext方法中执行 MultiDex.install(base);方法。就在UI线程操作。

package com.example.mytest;

import android.app.ActivityManager;
import android.app.Application;
import android.content.Context;
import android.text.TextUtils;
import android.util.Log;

import androidx.multidex.MultiDex;

import java.util.List;

public class MyApplication extends Application {

    private boolean isMainProcess = true;

    @Override
    protected void attachBaseContext(final Context base) {
        super.attachBaseContext(base);
        if (null == base) {
            return;
        }

        isMainProcess = (base != null && TextUtils.equals(base.getPackageName(), getCurrentProcessName(base)));

        if (isMainProcess) {
             MultiDex.install(base);
             Log.d("MyApplication", "执行MultiDex.install(base);方法");
        }
    }

    @Override
    public void onCreate() {
        super.onCreate();
    }

    /**
     * 获取当前进程
     */

    private String getCurrentProcessName(Context context) {
        int pid = android.os.Process.myPid();
        String processName = "";
        ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
        List<ActivityManager.RunningAppProcessInfo> list = manager.getRunningAppProcesses();
        if (list != null) {
            for (ActivityManager.RunningAppProcessInfo processInfo : list) {
                if (processInfo != null && processInfo.pid == pid) {
                    processName = processInfo.processName;
                }
            }
        }
        return processName;
    }

}