Mutidex源码分析和优化

Mutidex源码分析和优化

因为单个DEX文件被限制在65535之内,在Android5.0以上的时候,Android系统开始支持多Dex,所以我们可以直接在build.gradle配置multiDexEnabled true就可以了,然后Android5.0以下仅仅支持单Dex,所以Google提供了相关的兼容库对Android5.0以下的系统进行兼容,这个兼容库就是Mutidex,接下我们将从如何兼容Dex讲起,然后了解相关的原理,最终提供启动时间的优化策略。

Mutidex的使用

为了解决Android不支持多Dex的问题,Google官方引入了Mutidex,下面我们来了解一下Mutidex如何使用。

导入依赖包

dependencies {
	implementation 'com.android.support:multidex:1.0.1'
}

使用Mutidex的方法

使用Mutidex的方法有两种方法:

  • 使用Application的时候继承MultiDexApplication
  • 在自己实现的Application中的方法attachBaseContext配置Mutidex安装

因为JAVA支持的是单继承的关系,所以我们比较常用第二种方法。代码示例如下:

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

Mutidex的原理

在进行Mutidex的源码分析前,我们应当有一个概念就是多Dex文件是如何命名的,所以我们通过AndroidStudio打开我们我APK文件,目录如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BhJuOBKY-1571450911640)(https://i.imgur.com/OtMYx7o.png)]
从图中我们可以看出,DEX文件的命名规范是classes.dex为主DEX文件,其余DEX为classes2.dex、classes3.dex、classes4.dex…以此类推为classes[序列号].dex。

不管是使用那种方法来实现Mutidex最终Mutidex都会调用MultiDex.install(this),接下来我们将从这个作为突破口,开始解析MultiDex的实现原理。

Mutidex是如何提取多个Class文件的

我们先来看MultiDex.install(this)的实现代码:

public static void install(Context context) {
    if (IS_VM_MULTIDEX_CAPABLE) {
		// 如何该系统支持多DEX文件运行 不做任何处理
        Log.i("MultiDex", "VM has multidex support, MultiDex support library is disabled.");
    } else if (VERSION.SDK_INT < 4) {
		// 不做Android4以下的兼容 直接抛出异常
        throw new RuntimeException("MultiDex installation failed. SDK " + VERSION.SDK_INT + " is unsupported. Min SDK version is " + 4 + ".");
    } else {
        ApplicationInfo applicationInfo = getApplicationInfo(context);
        if (applicationInfo == null) {
			// 获取不到Application的信息,直接返回不做处理
            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", "");
        Log.i("MultiDex", "install done");
    }
}

在这段代码中,我们总结以下逻辑就是:

  1. 如果当前Android系统支持多DEX,不需要做任何处理
  2. 如果当前Android系统为Android4以下,MultiDex不支持兼容抛出异常
  3. 如果获取不到ApplicationInfo不做任何处理
  4. 最终调用MultiDex.doInstallation()开始安装

下面我们来看看MultiDex.doInstallation()的实现:

// 已安装APK的列表
private static final Set installedApk = new HashSet();

private static void doInstallation(Context mainContext, File sourceApk,
	File dataDir, String secondaryFolderName, String prefsKeyPrefix) throws ...{
	// 以installedApk列表来同步
    synchronized(installedApk) {
		// 如果没有在安装列表里面的APK才进行安装
        if (!installedApk.contains(sourceApk)) {
            installedApk.add(sourceApk);
            if (VERSION.SDK_INT > 20) {
               // 如何Android5.0以上的系统不确保功能正常
            }
			
			// 获取程序的ClassLoader否则不做任何处理
            ClassLoader loader;
            try {
                loader = mainContext.getClassLoader();
            } catch (RuntimeException var11) {
                return;
            }
			// 清理旧的Dex文件
            clearOldDexDir(mainContext);
			// 获取到Dex文件目录
            File dexDir = getDexDir(mainContext, dataDir, secondaryFolderName);
			// 提取到程序中所有的Dex压缩文件
            List files = MultiDexExtractor.load(mainContext, sourceApk, dexDir, prefsKeyPrefix, false);
			// 将Dex安装
            nstallSecondaryDexes(loader, dexDir, files);
        }
    }
}

上面示例代码我删除了一些不是很重要异常和判断,以方便我们梳理逻辑,主要的流程为:

  1. 获取到程序的 ClassLoader
  2. 清理旧的Dex文件目
  3. 获取到Dex文件目录文件
  4. 从程序到提取出所有的Dex压缩文件(本章核心)
  5. 安装Dex文件

当我们跟踪到MultiDex.doInstallation()一条很清晰的流程就出来了,简单来说就是MultiDex从APK目录到抽取出所有的Classes.dex文件,然后将这些文件安装到程序,就能实现多DEX的运行了,为了更深入的理解,接下来我们将继续跟踪MultiDexExtractor.load方法。

static List load(Context context, File sourceApk, File dexDir, String prefsKeyPrefix, boolean forceReload) throws ... {
    ....
    List files;
    try {
       	// 文件加锁
        if (!forceReload && !isModified(context, sourceApk, currentCrc, prefsKeyPrefix)) {
            try {
				// 加载缓存过的Classes.dex压缩文件
                files = loadExistingExtractions(context, sourceApk, dexDir, prefsKeyPrefix);
            } catch (IOException var21) {
               	// 加载已缓存的Classes.dex压缩文件失败,重新提取
                files = performExtractions(sourceApk, dexDir);
				// 保存在本地缓存
                putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(sourceApk), currentCrc, files);
            }
        } else {
           	// 强制重新提取或者Classes.dex压缩文件被修改
            files = performExtractions(sourceApk, dexDir);
			// 保存在本地缓存
            putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(sourceApk), currentCrc, files);
        }
    } finally {
        // 解锁和关闭文件流
    }

  	return files;
}

其中**MultiDexExtractor.performExtractions(sourceApk, dexDir)**是重点方法,我们看一下:

private static List performExtractions(File sourceApk, File dexDir) throws IOException {
   	...	
	// APK的压缩文件
    ZipFile apk = new ZipFile(sourceApk);

    try {
		// 标记序列号
        int secondaryNumber = 2;
		// 遍历APK压缩文件里面的dex文件
        for(ZipEntry dexFile = apk.getEntry("classes" + secondaryNumber + ".dex"); dexFile != null; dexFile = apk.getEntry("classes" + secondaryNumber + ".dex")) {
           	...
			// 失败可以重试三次
            while(numAttempts < 3 && !isExtractionSuccessful) {
                ++numAttempts;
				// 压缩DEX文件
                extract(apk, dexFile, extractedFile, extractedFilePrefix);

                try {
                    extractedFile.crc = getZipCrc(extractedFile);
                    isExtractionSuccessful = true;
                } catch (IOException var19) {
                    isExtractionSuccessful = false;
                }

               	// 压缩失败删除临时文件
                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 {
      //关闭流
    }

    return files;
}

从代码中可以得出整个方法的核心就是对DEX文件进行压缩,然后进行返回,至此整个流程结束。

安装DEX压缩文件

前面我们介绍了从APK压缩包中提取DEX压缩文件的流程,接下来我们可以看一下Mutidex是如何进行安装的。

private static void installSecondaryDexes(ClassLoader loader, File dexDir, List files) throws ... {
    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, dexDir);
        } else {
            MultiDex.V4.install(loader, files);
        }
    }

}

以上代码是**Mutidex.installSecondaryDexes()方法中的源码,我们再看看MultiDex.V19.install()**里面的方法。

    private static void install(ClassLoader loader, List additionalClassPathEntries, File optimizedDirectory) throws ... {
		// 反射出ClassLoader的pathList字段
        Field pathListField = MultiDex.findField(loader, "pathList");
        Object dexPathList = pathListField.get(loader);
        ArrayList suppressedExceptions = new ArrayList();
		// 扩展数组并进行安装
        MultiDex.expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, new ArrayList(additionalClassPathEntries), optimizedDirectory, suppressedExceptions));
        ....
		//异常处理
    }

到了这一步我们就进行不下去了,需要了解ClassLoader的原理才能更加深入了解这些方法的作用,我这里直接说一下这些方法的功能:

  • makeDexElements是使用反射调用Elements的方法,使得多dex文件转为Element数组
  • MultiDex.expandFieldArray()是使用反射调用ClassLoader扩展里面的一个字段 dexElements[]的数据,这个数组之前维护的就是主dex文件

在这里我们再总结一下整个MultiDex的流程:

  1. 过滤已经支持多DEX的Android系统、未兼容的Android4以下的系统
  2. 清理旧的DEX文件目录,获取新的DEX文件目录
  3. 提取出所有的DEX文件做压缩,并且做缓存记录,下次直接取缓存
  4. 开始安装
  5. 安装多DEX通过反射操作ClassLoader的字段dexElements[],扩充其数组

ClassLoader的原理(加载Class流程)

以Android5.0源代码的为蓝本。

不管是 PathClassLoader还是DexClassLoader,都继承自BaseDexClassLoader,加载类的代码在BaseDexClassLoader

public class BaseDexClassLoader extends ClassLoader {
	
	private final DexPathList pathList;


	public BaseDexClassLoader(String dexPath, File optimizedDirectory,
        	String libraryPath, ClassLoader parent) {
    	super(parent);
    	this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
	}

	@Override
	protected Class findClass(String name) throws ClassNotFoundException {
    	List suppressedExceptions = new ArrayList();
    	Class c = pathList.findClass(name, suppressedExceptions);
    	...
    	return c;
	}
	...	
}

BaseDexClassLoader.findClass()中可以看到,查找Class使用的是pathList的成员变量进行处理,而pathList是在BaseDexClassLoader的构造函数里面进行初始化的,其对象是DexPathList,那么接下来我们看看DexPathList.findClass()

final class DexPathList {
	private final Element[] dexElements;

	public Class findClass(String name, List suppressed) {
    	for (Element element : dexElements) {
       	 	DexFile dex = element.dexFile;

        	if (dex != null) {
            	Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
            	if (clazz != null) {
               	 return clazz;
            }
        }
    }
    if (dexElementsSuppressedExceptions != null) {
        suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
    }
    return null;
}
}

从代码中可知在最终BaseDexClassLoader.findClass()是通过遍历DexPathList.dexElements数组的元素来查找Class.

下面是对加载Class过程的总结:

BaseDexClassLoader
@Override
protected Class findClass(String name) throws ClassNotFoundException { 
	Class clazz = pathList.findClass(name);
	if (clazz == null) { 
    	throw new ClassNotFoundException(name); 
	} 
	return clazz;
}
DexPathList
public Class findClass(String name) { 
	for (Element element : dexElements) { 
    	DexFile dex = element.dexFile;
    	if (dex != null) { 
        	Class clazz = dex.loadClassBinaryName(name, definingContext); 
      	if (clazz != null) { 
          	return clazz; 
      	} 
    	} 
} 
return null;
}
DexFile
public Class loadClassBinaryName(String name, ClassLoader loader) { 
	return defineClass(name, loader, mCookie);
}
private native static Class defineClass(String name, ClassLoader loader, int cookie);

可以看出,BaseDexClassLoader中有个pathList对象,pathList中包含一个DexFile的数组dexElements,由上面分析知道,dexPath传入的原始dex(.apk,.zip,.jar等)文件在optimizedDirectory文件夹中生成相应的优化后的odex文件,dexElements数组就是这些odex文件的集合,如果不分包一般这个数组只有一个Element元素,也就只有一个DexFile文件,而对于类加载呢,就是遍历这个集合,通过DexFile去寻找。最终调用native方法的defineClass。

Muldex优化要点

学习过性能优化的同学应该都知道,SharedPreferences会导致UI暂停工作等待SharedPreferences完成后才进行,也就是说SharedPreferences保存数据如果使用SharedPreferences.Editor.commit()进行提交是同步行为,在我们学习Muldex原理的流程中缓存就是使用的SharedPreferences,所以Muldex在Android5.0以下会导致性能问题,如果我们需要让我们的APP更加稳定,那么我们就需要对其进行优化处理。

通过反编译今日头条APP的源码分析后发现,今日头条APP的优化思路是这样的:

  1. 在attachBaseContext处于主线程并且不支持多DEX的情况下才进行处理
  2. 创建一个临时文件跨进程标记当前正在处理Multdex
  3. 启动另一个进程的Activity,这个Activity有两个功能:其一是加载MultiDex,其二是显示界面加载给用户反馈
  4. 循环遍历临时文件是否存在,在这里卡住进程,只有当另一个线程的MultiDex安装完毕才进行释放
  5. 在主进程进行MultiDex安装操作,以为加载过一次有缓存,这次加载很快

基本的流程讲完,如果想要看代码样例可以打开:https://github.com/345509960/AndroidDemo/tree/master/MultiDexTest

你可能感兴趣的:(Android源码分析,Multidex,Android)