一.简介
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;
}
这个方法大概的逻辑是解压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;
}
}