Android打包相关知识整理(二)

·  阅读 370

Android打包相关知识整理(二)

本篇主要分析Android Multidex的原理,通过Multidex来认识Android的类加载机。

Multidex

Dalvik执行的是由多个.class文件合并成的一个.dex文件,而在这其中,会对.dex包中所有的方法通过方法ID做一个索引,存储在一个链表中,而这个链表的长度用一个short类型来保存。short类型占两个字节,所以一个short最大值应该是65536,因此如果我们一个包中的方法数如果太多,以至于链表索引的长度超过了65546,就会有问题。虽然在新版本中Google已经修复了这一问题,但是为了兼容低版本系统,我们需要在方法数超过这一限制时做一些处理。
解决方法有很多,其中有一条就是分割dex,即如果一个dex放不下,就多分几个dex包,这样就不会有问题了。Google官方推出了multidex方案,可以有效的进行dex分包。multidex的原理很简单,在打包时主动分包,分为一个主dex包和多个次dex包,然后一起打包成apk。在应用启动时,会首先加载主dex包作为入口,然后依次加载次dex包。

配置使用

配置使用很简单,分为两步:

  1. 修改Gradle配置,使项目支持multidex。
android {
    compileSdkVersion 27
    defaultConfig {
        ...
        minSdkVersion 14
        targetSdkVersion 27
        ...
        // Enabling multidex support.
        multiDexEnabled true
    }
    ...
}
dependencies {
  compile 'com.android.support:multidex:1.0.3'
}
复制代码
  1. 在代码中启动MultiDex。

具体来讲,有三种方式。第一种,通过在AndroidManifest文件中指定使用MultidexApplication

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.android.multidex.myapplication">
    <application
        ...
        android:name="android.support.multidex.MultiDexApplication">
        ...
    </application>
</manifest>

复制代码

第二种,自定义Application并继承MultiDexApplication

public class DemoApp extends MultiDexApplication {

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

}
复制代码

第三种,自定义Application,然后在attachBaseContext方法中启用MultiDex

public class BaseApplication extends Application {

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

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

}
复制代码

其实上述三种方法最终目的都是调用MultiDex.install()这一方法。

dex包拆分

dex拆包的步骤:

  1. 扫描整个工程代码,得到一个记录了标记主、从dex的main-dex-list。

  2. 根据main-dex-list,对把项目编译后的.class文件按主、从分开。

  3. 分别将主、从.class文件打包为主、从.dex文件。

生成mai-dex-list的工具在Android Sdk的Build Tools中,是一个名字叫mainDexClasses的脚本文件。其核心部分为先生成一个jar包,然后连同所有的文件一起作为参数,调用com.android.multidex.MainDexListBuilder:

java -cp "$jarpath" com.android.multidex.MainDexListBuilder ${disableKeepAnnotated} "${tmpOut}" ${@} ||  exit 11
复制代码

MainDexListBuilder源码可以在这里查看:MainDexListBuilder.java
主要做的工作就是将符合keep规则的部分加入到主dex中。因为分包之后的合包必须先执行主包再一一合包次包,因此有些必需的文件就得装在主dex包中,这里的keep规则就是为了能够将这部分文件做保护,这样在生成main-dex-list时就会默认放入主dex包中了。

dex包合并

初始化

根据前面MultiDex配置的方法,那么MultiDe在合并的时候就是在首次安装运行时,将从dex包逐一安装,入口就是Application中的这行代码:

    MultiDex.install(this);
复制代码

在install中:

public static void install(Context context) {
        Log.i("MultiDex", "Installing application");
        //这里判断一下当前系统是否支持MultiDex,如果已经支持,就不用MultiDex自己合并了
        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 {
                //获取Application对象的引用
                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");
        }
    }
复制代码

所以具体的安装是在doInstallation()中执行的,在调用这个方法时传入的主要参数有一个context引用,一个存放base apk的目录,一个app的data目录,一个字符串secondary-dexes作为从dex目录。

总体过程

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 {
        //这里的installedApk是一个HashSet,应该是保存当前以及安装的dex包
        Set var6 = installedApk;
        synchronized(installedApk) {
            //如果souceApk不存在,就将他加入到这个HashSet中
            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") + "\"");
                }
                //用当前的Context获取一个ClassLoader对象的引用
                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);
                    }
                    //创造一个dex目录
                    File dexDir = getDexDir(mainContext, dataDir, secondaryFolderName);
                    //创造一个MultiDexExtractor对象
                    MultiDexExtractor extractor = new MultiDexExtractor(sourceApk, dexDir);
                    IOException closeException = null;

                    try {
                        //调用MultiDexExtractor对象的load()方法,获取一个文件列表,这个文件列表应该是其他的从dex
                        List files = extractor.load(mainContext, prefsKeyPrefix, false);

                        try {
                            //安装这些从dex包
                            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;
                    }
                }
            }
        }
    }
复制代码

梳理一下,就是首先看一下sourceApk是否已经安装,如果没有则安装,然后用Context获取一个ClasLoader,清除掉旧的缓存,获取从dex包,然后用前面获取的那个ClassLoader去加载这些从dex包。然后依次来看一下那几个重要的方法。
首先是clearOldDexDir():

 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;
            //遍历所有的子文件,依次调用delete()方法删除
            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());
            }
        }

    }
复制代码

所以这里是清除了/data/data//files/secondary-dexes这一目录下的所有文件。
然后是getDexDir():

private static File getDexDir(Context context, File dataDir, String secondaryFolderName) throws IOException {
    
        File cache = new File(dataDir, "code_cache");

        try {//创建这个code_cache目录
            mkdirChecked(cache);
        } catch (IOException var5) {
            //如果创建失败,就创建这个files/code_cache目录
            cache = new File(context.getFilesDir(), "code_cache");
            mkdirChecked(cache);
        }
        //在刚刚创建的files/code_cache目录下再创建一个secondary-dexes目录
        File dexDir = new File(cache, secondaryFolderName);
        mkdirChecked(dexDir);
        return dexDir;
    }
复制代码

总结一下,这里先创建一个/data/data//code_cache目录,如果创建失败,改为创建一个/data/data//files/code_cache目录,然后再在这一目录下创建一个secondary目录,因此总的来说创建了一个/data/data//files/code_cache/secondary-dexes或者/data/data//code_cache/secondary-dexes目录,并返回。这样也跟刚才清除目录项对应了,clear的时候清除的就是创建失败时再次创建的这一目录。
再看创建一个extractor.load()

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 {
                    //提取之前已经存在的files
                    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);
                    //如果失败,通过performExtractions()方法获取
                    files = this.performExtractions();
                    //存储apk信息???
                    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.");
                }
                //也是需要调用performExtraction()方法提取
                files = this.performExtractions();
                putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(this.sourceApk), this.sourceCrc, files);
            }

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

所以load()方法首先要确保当前操作获取到的文件锁是有效的,应该是为了确保多进程操作的安全。然后根据传入的参数forceReload以及是否有修改,来决定如何提取文件。如果可以,就会获取Existing文件,在此推测是缓存文件。否则,就要通过performExtractions()方法来获取并通过putStoreApkInfo()方法保存了。
看看this.loadExistingExtractions()方法是否符合推测:

private List<MultiDexExtractor.ExtractedDex> loadExistingExtractions(Context context, String prefsKeyPrefix) throws IOException {
        Log.i("MultiDex", "loading existing secondary dex files");
        //构造一个apkName.classes为前缀
        String extractedFilePrefix = this.sourceApk.getName() + ".classes";
        //获取一个SP对象
        SharedPreferences multiDexPreferences = getMultiDexPreferences(context);
        //获取总的dex数量,即totalDexNumber
        int totalDexNumber = multiDexPreferences.getInt(prefsKeyPrefix + "dex.number", 1);
        //一个存放从dex文件的list 数量为总数量-1 也就是去掉了主dex文件
        List<MultiDexExtractor.ExtractedDex> files = new ArrayList(totalDexNumber - 1);
        //从第二个开始遍历,也就是跳过了第一个主dex
        for(int secondaryNumber = 2; secondaryNumber <= totalDexNumber; ++secondaryNumber) {
            //以前缀+当前的数字+.zip为文件名,比如 xxx.classes2.zip
            String fileName = extractedFilePrefix + secondaryNumber + ".zip";
            //获取这一文件
            MultiDexExtractor.ExtractedDex extractedFile = new MultiDexExtractor.ExtractedDex(this.dexDir, fileName);
            if (!extractedFile.isFile()) {//不存在 就会报错
                throw new IOException("Missing extracted secondary dex file '" + extractedFile.getPath() + "'");
            }
            //存在 就会先做一些验证工作
            extractedFile.crc = getZipCrc(extractedFile);
            long expectedCrc = multiDexPreferences.getLong(prefsKeyPrefix + "dex.crc." + secondaryNumber, -1L);
            long expectedModTime = multiDexPreferences.getLong(prefsKeyPrefix + "dex.time." + secondaryNumber, -1L);
            long lastModified = extractedFile.lastModified();
            if (expectedModTime != lastModified || expectedCrc != extractedFile.crc) {
            //验证不通过 会报错
                throw new IOException("Invalid extracted dex: " + extractedFile + " (key \"" + prefsKeyPrefix + "\"), expected modification time: " + expectedModTime + ", modification time: " + lastModified + ", expected crc: " + expectedCrc + ", file crc: " + extractedFile.crc);
            }
            //最终 验证通过 把这一文件加入到list中
            files.add(extractedFile);
        }
        //最后返回那个构建的从dex文件list
        return files;
    }
复制代码

首先获取一个SP对象,里面保存了dex文件数量相关的数据。然后通过遍历,获取所有的dex文件,加入到一个list钟返回。如果期间发现获取不到,则抛出一个异常。结合前面的代码,抛出异常后就需要通过performExtractions()方法重新提取一次:

private List<MultiDexExtractor.ExtractedDex> performExtractions() throws IOException {
        //同样的,先构造一个apkName.classes的前缀
        String extractedFilePrefix = this.sourceApk.getName() + ".classes";
        this.clearDexDir();
        List<MultiDexExtractor.ExtractedDex> files = new ArrayList();
        //获取一个zipfile 这就是原始apk
        ZipFile apk = new ZipFile(this.sourceApk);

        try {
            int secondaryNumber = 2;
            //让number从2开始累加 做一个遍历 期间的dexFile文件是apk通过classes+number+.dex为文件名获取的
            for(ZipEntry dexFile = apk.getEntry("classes" + secondaryNumber + ".dex"); dexFile != null; dexFile = apk.getEntry("classes" + secondaryNumber + ".dex")) {
                //构造一个文件名 apkName+.classes+number+.zip
                String fileName = extractedFilePrefix + secondaryNumber + ".zip";
                //这里的dexDir就是前面创建的缓存目录 xxx/secondary-dexs,这里是用这两个参数获取到dex文件
                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;
                    //提取dex文件的核心方法
                    extract(apk, dexFile, extractedFile, extractedFilePrefix);

                    try {
                        //获取crc
                        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;
    }
复制代码

可以看到,会从2开始,不断的构造一个dex文件,然后提取这一文件到dexDir中,提取的核心方法为extract():

private static void extract(ZipFile apk, ZipEntry dexFile, File extractTo, String extractedFilePrefix) throws IOException, FileNotFoundException {
        //这里的dexFile就是源dex文件 所以这里先获取了他的文件输入流
        InputStream in = apk.getInputStream(dexFile);
        ZipOutputStream out = null;
        //以一个tmp文件作为交换文件
        File tmp = File.createTempFile("tmp-" + extractedFilePrefix, ".zip", extractTo.getParentFile());
        Log.i("MultiDex", "Extracting " + tmp.getPath());

        try {
            //压缩文件流
            out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(tmp)));

            try {
                ZipEntry classesDex = new ZipEntry("classes.dex");
                classesDex.setTime(dexFile.getTime());
                out.putNextEntry(classesDex);
                byte[] buffer = new byte[16384];
                //文件写入的过程
                for(int length = in.read(buffer); length != -1; length = in.read(buffer)) {
                    out.write(buffer, 0, length);
                }

                out.closeEntry();
            } finally {
                out.close();
            }

            if (!tmp.setReadOnly()) {
                throw new IOException("Failed to mark readonly \"" + tmp.getAbsolutePath() + "\" (tmp of \"" + extractTo.getAbsolutePath() + "\")");
            }

            Log.i("MultiDex", "Renaming to " + extractTo.getPath());
            //将交换文件转换为目标文件,即secondary-dexs目录下的相对应的文件
            if (!tmp.renameTo(extractTo)) {
                throw new IOException("Failed to rename \"" + tmp.getAbsolutePath() + "\" to \"" + extractTo.getAbsolutePath() + "\"");
            }
        } finally {
            closeQuietly(in);
            tmp.delete();
        }

    }
复制代码

具体的提取过程到这里就结束了,总结一下,就是通过load()方法,将源dex文件一一写入到新的secondary-dexs目录下对应的文件中,这一过程会将源apk文件进行一个解压,且只会从第二个dex文件开始,因为第一个就是主dex文件,主dex是已经安装好了的。

install具体过程

好了,现在所有的次dex包都已经提取出来,接下来就是安装了,安装的入口就是前面的installSecondaryDexes()方法:

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包时,会根据不同的sdk版本有不同的方法。就以sdk>=19举例,他的install()方法如下:

static void install(ClassLoader loader, List<? extends File> additionalClassPathEntries, File optimizedDirectory) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException {
            //通过反射 获取ClassLoader 的 pathList 字段
            Field pathListField = MultiDex.findField(loader, "pathList");
            Object dexPathList = pathListField.get(loader);
            ArrayList<IOException> suppressedExceptions = new ArrayList();
            //先调用makeDexElements方法 把从Dex文件们变成Element对象
            //然后调用expandFieldArray方法 把刚才得到的Element对象们添加到dexPathList的dexElements字段的后面(这个字段是个数组)
            MultiDex.expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, new ArrayList(additionalClassPathEntries), optimizedDirectory, suppressedExceptions));
            //添加一些IO异常信息
            if (suppressedExceptions.size() > 0) {
                Iterator var6 = suppressedExceptions.iterator();

                while(var6.hasNext()) {
                    IOException e = (IOException)var6.next();
                    Log.w("MultiDex", "Exception in makeDexElement", e);
                }

                Field suppressedExceptionsField = MultiDex.findField(dexPathList, "dexElementsSuppressedExceptions");
                IOException[] dexElementsSuppressedExceptions = (IOException[])((IOException[])suppressedExceptionsField.get(dexPathList));
                if (dexElementsSuppressedExceptions == null) {
                    dexElementsSuppressedExceptions = (IOException[])suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
                } else {
                    IOException[] combined = new IOException[suppressedExceptions.size() + dexElementsSuppressedExceptions.length];
                    suppressedExceptions.toArray(combined);
                    System.arraycopy(dexElementsSuppressedExceptions, 0, combined, suppressedExceptions.size(), dexElementsSuppressedExceptions.length);
                    dexElementsSuppressedExceptions = combined;
                }

                suppressedExceptionsField.set(dexPathList, dexElementsSuppressedExceptions);
                IOException exception = new IOException("I/O exception during makeDexElement");
                exception.initCause((Throwable)suppressedExceptions.get(0));
                throw exception;
            }
        }
复制代码

在install中,主要就是通过反射机制,从ClassLoader中获取一个叫做pathlist的Field,再将dex包们通过makeElements()方法产生一个Element数组,然后调用expandFieldArray()方法,按字面意思应该是对数组进行了扩展。最后,查看是否有异常需要抛出。

在Android中,类加载器有两种,DexClassLoader和PathClassLoader,这两个加载器都是继承自BaseClassLoader的。pathlist就是定义在BaseClassLoader中的:

/**
 * Base class for common functionality between various dex-based
 * {@link ClassLoader} implementations.
 */
public class BaseDexClassLoader extends ClassLoader {
    private final DexPathList pathList;

}
复制代码

再看看这个DexPathList具体是什么:

/**
 * A pair of lists of entries, associated with a {@code ClassLoader}.
 * One of the lists is a dex/resource path &mdash; typically referred
 * to as a "class path" &mdash; list, and the other names directories
 * containing native code libraries. Class path entries may be any of:
 * a {@code .jar} or {@code .zip} file containing an optional
 * top-level {@code classes.dex} file as well as arbitrary resources,
 * or a plain {@code .dex} file (with no possibility of associated
 * resources).
 *
 * <p>This class also contains methods to use these lists to look up
 * classes and resources.</p>
 */
/*package*/ final class DexPathList {
}
复制代码

所以他相当于是封装了一个存放dex路径的list。回到install方法中,按步骤看一下。首先是makeElements():

/**
 * A wrapper around
 * {@code private static final dalvik.system.DexPathList#makeDexElements}.
 */
private static Object[] makeDexElements(
        Object dexPathList, ArrayList<File> files, File optimizedDirectory,
        ArrayList<IOException> suppressedExceptions)
                throws IllegalAccessException, InvocationTargetException,
                NoSuchMethodException {
    Method makeDexElements =
            findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class,
                    ArrayList.class);

    return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory,
            suppressedExceptions);
}
复制代码

看注释就知道,这里也是通过反射,调用了前面DexPathList的makeDexElements()方法,那就来看看吧:

/**
 * Makes an array of dex/resource path elements, one per element of
 * the given array.
 */
private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
                                         List<IOException> suppressedExceptions,
                                         ClassLoader loader) {
    return makeElements(files, optimizedDirectory, suppressedExceptions, false, loader);
}
复制代码

创建了一个dex/resource的数组:

private static Element[] makeElements(List<File> files, File optimizedDirectory,
                                      List<IOException> suppressedExceptions,
                                      boolean ignoreDexFiles,
                                      ClassLoader loader) {
  //创建一个Element数组
    Element[] elements = new Element[files.size()];
    int elementsPos = 0;
    /*
     * Open all files and load the (direct or contained) dex files
     * up front.
     */
     //遍历 打开所有的文件
    for (File file : files) {
        File zip = null;
        File dir = new File("");
        DexFile dex = null;
        String path = file.getPath();
        String name = file.getName();
        //这个zipSeparator的值为  !/
        if (path.contains(zipSeparator)) {
            String split[] = path.split(zipSeparator, 2);
            zip = new File(split[0]);
            dir = new File(split[1]);
        } else if (file.isDirectory()) {
            // We support directories for looking up resources and native libraries.
            // Looking up resources in directories is useful for running libcore tests.
            elements[elementsPos++] = new Element(file, true, null, null);
        } else if (file.isFile()) {
        //根据前面的源码 这里的ignoreDexFiles为false,而DEX_SUFFIX的值为 .dex
            if (!ignoreDexFiles && name.endsWith(DEX_SUFFIX)) {
                // Raw dex file (not inside a zip/jar).
                try {
                    //找到了dex文件,尝试加载
                    dex = loadDexFile(file, optimizedDirectory, loader, elements);
                } catch (IOException suppressed) {
                    System.logE("Unable to load dex file: " + file, suppressed);
                    suppressedExceptions.add(suppressed);
                }
            } else {
                zip = file;

                if (!ignoreDexFiles) {
                    try {
                        dex = loadDexFile(file, optimizedDirectory, loader, elements);
                    } catch (IOException suppressed) {
                        /*
                         * IOException might get thrown "legitimately" by the DexFile constructor if
                         * the zip file turns out to be resource-only (that is, no classes.dex file
                         * in it).
                         * Let dex == null and hang on to the exception to add to the tea-leaves for
                         * when findClass returns null.
                         */
                        suppressedExceptions.add(suppressed);
                    }
                }
            }
        } else {
            System.logW("ClassLoader referenced unknown path: " + file);
        }
        //加载完毕 将获取到的zip或者dex先封装为一个Element对象,再将这个Element对象放入前面的Element数组中
        if ((zip != null) || (dex != null)) {
            elements[elementsPos++] = new Element(dir, false, zip, dex);
        }
    }
    if (elementsPos != elements.length) {
        elements = Arrays.copyOf(elements, elementsPos);
    }
    return elements;
}
复制代码

然后是expandFieldArray()方法:

    /**
     * Replace the value of a field containing a non null array, by a new array containing the
     * elements of the original array plus the elements of extraElements.
     * @param instance the instance whose field is to be modified.
     * @param fieldName the field to modify.
     * @param extraElements elements to append at the end of the array.
     */
    private static void expandFieldArray(Object instance, String fieldName,
            Object[] extraElements) throws NoSuchFieldException, IllegalArgumentException,
            IllegalAccessException {
        Field jlrField = findField(instance, fieldName);
        Object[] original = (Object[]) jlrField.get(instance);
        Object[] combined = (Object[]) Array.newInstance(
                original.getClass().getComponentType(), original.length + extraElements.length);
        System.arraycopy(original, 0, combined, 0, original.length);
        System.arraycopy(extraElements, 0, combined, original.length, extraElements.length);
        jlrField.set(instance, combined);
    }
复制代码

结合注释就知道,这里是用一个包含了原来的elements的新的数组,加入了新的elemets。这里也是反射,实际发生了变化的是DexPathList的数组:

 /**
     * List of dex/resource (class path) elements.
     * Should be called pathElements, but the Facebook app uses reflection
     * to modify 'dexElements' (http://b/7726934).
     */
    private Element[] dexElements;
复制代码

总结一下,具体的安装过程就是将Dex包们封装为Element对象,再将这些Element对象插入到原来的Element数组中,充当为新的数组,而这整个过程都是通过反射来做的,具体的执行者就是DexPathList

总结

上面都是一些工具方法,主要用途就是通过反射,获取对象,调用指定方法。
dex合并就到这里了,总结一下合并的步骤:

  1. 检查当前系统能够自动合并包,以及如果不支持的话系统版本是否支持multidex。
  2. 清除旧的备用缓存目录,之所以叫备用是因为在创建目录时创建失败才会创建备用目录,这个备用目录是需要删除的。
  3. 创建新的缓存目录,然后提取从dex文件至缓存目录中。
  4. 安装缓存目录中断dex文件,这时根据当前系统的版本有不同的安装方法,但大体上逻辑是一样的。
分类:
Android
标签:
分类:
Android
标签:
收藏成功!
已添加到「」, 点击更改