关于Android的65535限制

一、65536是什么样的数?

2的16次方 或者 16进制的 0xFFFF

下边这个error是不是很熟悉

较高版本的Android构建系统下的提示(Android 7.0及以下):

Conversion to Dalvik format failed:
Unable to execute dex: method ID not in [0, 0xffff]: 65536

较高版本的Android构建系统的报错信息(Android 8.0)

trouble writing output:
Too many field references: 131000; max is 65536.
You may try using --multi-dex option.

二、为什么会出现64K的限制呢?

一般排查问题我们需要从问题本身入手,那么log是最重要的信息。

在构建流程中出现这种问题,根据提示我们大概猜到是因为方法数过多,而这些方法是存在于编译后的.class文件中的,而.class最后要存在于dex文件中。

那么如此分析的话,问题应该存在于dex的打包流程当中,这个需要以后深入了解一下。

根据前人的一些分析,我们来看看MemberIdsSection文件。
注意:
源码路径是 /dalvik/dx/src/com/android/dx/dex/file/MemberIdsSection.java
不是/dalvik/dexgen/src/com/android/dexgen/dex/file/MemberIdsSection.java
代码不多,如下:

1 /*
2 * Copyright (C) 2007 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.dx.dex.file;
18
19import com.android.dex.DexFormat;
20import com.android.dex.DexIndexOverflowException;
21
22import java.util.Formatter;
23import java.util.Map;
24import java.util.TreeMap;
25import java.util.concurrent.atomic.AtomicInteger;
26
27/**
28 * Member (field or method) refs list section of a {@code .dex} file.
29 */
30public abstract class MemberIdsSection extends UniformItemSection {
31
32    /**
33     * Constructs an instance. The file offset is initially unknown.
34     *
35     * @param name {@code null-ok;} the name of this instance, for annotation
36     * purposes
37     * @param file {@code non-null;} file that this instance is part of
38     */
39    public MemberIdsSection(String name, DexFile file) {
40        super(name, file, 4);
41    }
42
43    /** {@inheritDoc} */
44    @Override
45    protected void orderItems() {
46        int idx = 0;
47
48        if (items().size() > DexFormat.MAX_MEMBER_IDX + 1) {
49            throw new DexIndexOverflowException(getTooManyMembersMessage());
50        }
51
52        for (Object i : items()) {
53            ((MemberIdItem) i).setIndex(idx);
54            idx++;
55        }
56    }
57
58    private String getTooManyMembersMessage() {
59        Map membersByPackage = new TreeMap();
60        for (Object member : items()) {
61            String packageName = ((MemberIdItem) member).getDefiningClass().getPackageName();
62            AtomicInteger count = membersByPackage.get(packageName);
63            if (count == null) {
64                count = new AtomicInteger();
65                membersByPackage.put(packageName, count);
66            }
67            count.incrementAndGet();
68        }
69
70        Formatter formatter = new Formatter();
71        try {
72            String memberType = this instanceof MethodIdsSection ? "method" : "field";
73            formatter.format("Too many %1$s references to fit in one dex file: %2$d; max is %3$d.%n" +
74                            "You may try using multi-dex. If multi-dex is enabled then the list of " +
75                            "classes for the main dex list is too large.%n" +
76                    "References by package:",
77                    memberType, items().size(), DexFormat.MAX_MEMBER_IDX + 1);
78            for (Map.Entry entry : membersByPackage.entrySet()) {
79                formatter.format("%n%6d %s", entry.getValue().get(), entry.getKey());
80            }
81            return formatter.toString();
82        } finally {
83            formatter.close();
84        }
85    }
86
87}

在48行到49中,我们看到如下可能抛出异常的情况

if (items().size() > DexFormat.MAX_MEMBER_IDX + 1) {
     throw new DexIndexOverflowException(getTooManyMembersMessage());
}

getTooManyMembersMessage()函数内(72行到77行)有如下异常信息字符串构造

String memberType = this instanceof MethodIdsSection ? "method" : "field";
           formatter.format("Too many %1$s references to fit in one dex file: %2$d; max is %3$d.%n" +
                            "You may try using multi-dex. If multi-dex is enabled then the list of " +
                            "classes for the main dex list is too large.%n" +
                    "References by package:",
                    memberType, items().size(), DexFormat.MAX_MEMBER_IDX + 1);

同时我们还要注意DexFormat类,

    /**
     * Maximum addressable field or method index.
     * The largest addressable member is 0xffff, in the "instruction formats" spec as field@CCCC or
     * meth@CCCC.
     */
   public static final int MAX_MEMBER_IDX = 0xFFFF;

根据注释,我们来到Dalvik 字节码,根据表格中的解释如下图:

Dalvik

可以看到类型索引(16 位),由此可以知道,无论是方法数还是字段数都不能超过65536,这也就是为什么在构建流程中出现65536的报错信息。

由此可以得出结论:

invoke-kind (调用各类方法)指令中,方法引用索引数是 16 位的,也就是最多调用 2^16 = 65536 个方法,这就是 DexFormat 中 MAX_MEMBER_IDX 为 0xFFFF 的原因。
所以单个dex的方法或者字段数量不能超过65536

也可以简单的说:Apk 文件本质上是个压缩文件,它里面包含的class.dex文件是可执行的Dalvik字节码文件,这个.dex文件中存放的是所有编译后的Java代码。Dalvil可执行文件规范限制了(事实上是最初设计上的一个失误)单个.dex文件最多能引用的方法数是65536个,这其中包含了Android Framework、APP引用的第三方函数库以及APP自身的方法。

三、Android 官方是如何解决65536问题的

android 5.0 (API level 21)之前,系统使用的是Dalvik虚拟机来执行Android应用,默认情况下,Dalvik为每个APK只生成一个classes.dex文件,为了避免单个.dex文件方法数超过64K的问题,我们需要拆分这个单一的classes.dex文件,拆分后可能存在类似于classes.dex、classes2.dex等多个.dex文件。在应用启动后,会先加载classes.dex文件,我们称之为主(Primary)dex文件,应用启动后才会依次加载其他.dex文件,这些统称为从(Secondary)dex文件。为了规避这个64K限制,Google推出了一个名为MultiDex Support Library的函数库,当我们下载了Android Support Libraries之后,可以在/extras/android/support/multidex/目录中找到这个函数库。

从API21及之后的版本Android使用名为ART的虚拟机来代替Dalvik虚拟机,ART天然支持从APK文件中加载多个.dex文件,在应用安装期间,他会执行一个预编译操作,扫描APK中的classes(..N).dex文件并将它们编译成一个单一的.oat文件,在应用运行时去加载这个.oat文件,而不是一个个的加载.dex文件。

四、最好还是避免出现64K限制

当app开始触碰到64K方法数的天花板时,不建议立即使用MultiDex Support Library来将apk中的单一.dex拆分成多个,从而规避64K方法数限制引起的编译错误问题。
最佳实践是永远保持应用的方法数低于64K,永远没有机会使用MultiDex Support Library。
因为使用MultiDex是下下策,在大多数情况下会降低应用的性能。

减少应用方法数的方法:

  • 检查应用的直接和简洁第三方依赖
  • 使用Proguard移除无用的代码:配置并在Release版本中使用ProGuard,它通过分析字节码压缩功能,能够检测并移除没有使用到的类、字段、方法和属性

如果使用以上方法没法将方法数限制在64K以下,那么就需要使用MultiDex,需要在应用module的build.gradle文件的defaultConfig中添加multiDexEnabled true,接着引入MultiDexApplication 初始化MultiDex。

五、MultiDex局限性

MultiDex Support Library是一个不得已而为之的方案,它本身不是完美的,将它集成到项目中,需要经过完整的测试才能上线,可能会出现应用性能下降等问题,具体局限性如下:

  • 应用首次启动时Dalvik虚拟机会对所有的.dex文件执行dexopt操作,生成ODEX文件,这个过程很复杂且非常耗时,如果应用的从dex文件太大,可能会导致出现ANR。
  • 在Android 4.0(API level 14)之前的系统上,由于Dalvik linearAlloc的bug,使用MultiDex的应用可能启动失败。
  • 引入MultiDex机制时,必然会存在主dex文件和从dex文件,应用启动所需要的类都必须放到主dex文件中,否则会出现NoClassDefFoundError的错误。

六、MeltiDex源码分析

基于com.android.support:multidex:1.0.3版本分析

6.1 判断安卓虚拟机的逻辑

程序入口 MultiDex.install();

public static void install(Context context) {
        Log.i("MultiDex", "Installing application");
        if (IS_VM_MULTIDEX_CAPABLE) {
          //判断VM是否支持Multidex,如果是ART虚拟机默认情况下会启用 MultiDex,并且不需要MultiDex支持库

          Log.i("MultiDex", "VM has multidex support, MultiDex support library is disabled.");
        } else if (VERSION.SDK_INT < 4) {
            //最低兼容SDK版本是4,这样的手机基本都是看不着了吧
            throw new RuntimeException("MultiDex installation failed. SDK " + VERSION.SDK_INT + " is unsupported. Min SDK version is " + 4 + ".");
        } else {
              //执行到这里说明当前是Dalvik虚拟机,该进行多dex拆分
                     doInstallation();
                     ...........
}
}

我们再看看IS_VM_MULTIDEX_CAPABLE如何定义,做了什么逻辑:

//System.getProperty("java.vm.version")  获取当前虚拟机版本 例:2.1.0
private static final boolean IS_VM_MULTIDEX_CAPABLE = isVMMultidexCapable(System.getProperty("java.vm.version"));
    /**
     * 1、通过正则表达式将版本号分成major(主版本号)和minor(次版本号)。
     * 2、通过判断主版本和次版本是否大于一个常量来判定虚拟机是否支持MultiDex。
     */
static 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;
    }

isVMMultidexCapable()返回true 说明是ART虚拟机自身就支持MultiDex不需要再做任何处理,flase说明是Dalvik虚拟机自身不支持 MultiDex ,该执行doInstallation()

6.1.1有争议的一个问题

网上搜索很多技术大佬的博客,大部分说:可以通过调用 System.getProperty(“java.vm.version”)来检测当前使用的是哪个虚拟机,如果使用的是ART虚拟机的话,属性值会大于等于2.0.0(重点就是这个=2.0.0)

在这里我纠正下,正确的说法是:如果使用的是ART虚拟机的话,属性值应该大于等于2.1.0(>=2.1.0),有什么依据这样说?用真机(模拟器)安卓系统4.4测试,会发现 System.getProperty(“java.vm.version”)=2.0.0 ,把结果带入isVMMultidexCapable()方法里,返回的是false,说明当前应该(api 4.4版本)是Dalvik虚拟机才对,有悖于技术大佬博客上面所说的等于2.0.0就是ART虚拟机。

6.2 Dex解压和压缩

如果上一步判断是Dalvik虚拟机,执行到了doInstallation()

   /**
    * applicationInfo.sourceDir获取应用APK所在目录  /data/app/{packageName}-s_ZR1N24kyfFdRoazc7SLw==/base.apk
    * applicationInfo.dataDir获取数据所在目录   /data/user/0/{packageName}
   */
  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 {
        Set var6 = installedApk;
        //考虑到多线程并发下加锁,保证执行一次
        synchronized(installedApk) {
            //如果应用 没有安装,把installedApk添加到集合中,安装应用的路径:/data/app/packageName/base.apk
            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(); //上下文对象中获取ClassLoader对象,提取出来的Dex需要通过ClassLoader真正的被加载执行;
                } 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) {//说明获取ClassLoader 对象失败
                    Log.e("MultiDex", "Context class loader is null. Must be running in test mode. Skip patching.");
                } else {
                    try {
                        clearOldDexDir(mainContext);//清理老的缓存DEX文件
                    } catch (Throwable var24) {
                        Log.w("MultiDex", "Something went wrong when trying to clear old MultiDex extraction, continuing without cleaning.", var24);
                    }
                    //创建一个存放dex的目录   getDexDir()有详细的注释
                    File dexDir = getDexDir(mainContext, dataDir, secondaryFolderName);

                    // 把APK中的dex提取到dexDir目录中,返回的files集合有可能为空,表示没有secondaryDex
                    //apk路径:data/app/packageName/base.apk
                    //dexDir 路径: data/user/0/packageName/code_cache/secondary-dexes
                    MultiDexExtractor extractor = new MultiDexExtractor(sourceApk, dexDir);
                    IOException closeException = null;

                    try {
                   //  调用MultiDexExtractor.load方法,第一次是没有缓存的,需要IO操作,会非常耗时 返回dex文件列表
                        List files = extractor.load(mainContext, prefsKeyPrefix, false);

                        try {
                            installSecondaryDexes(loader, dexDir, files);  //安装提取出来的Dex文件。
                        } catch (IOException var26) {
                            if (!reinstallOnPatchRecoverableException) {
                                throw var26;
                            }
                            //出现异常 重新提取dex文件,并安装提取出来的dex文件
                            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;
                    }
                }
            }
        }
    }

上面代码进行各种预校验以及获取需要的信息,重点方法MultiDexExtractor.load():提取dex

 List 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;

            //forceReload判断文件是否重新加载,isModified()是判断sourceApk文件是否做过修改(简单点说这个条件就是没有覆盖安装过)
            if (!forceReload && !isModified(context, this.sourceApk, this.sourceCrc, prefsKeyPrefix)) {
                try {
                    files = this.loadExistingExtractions(context, prefsKeyPrefix);//加载之前已经解压过的dex(可以理解缓存过的)
                } catch (IOException var6) {
                    Log.w("MultiDex", "Failed to reload existing extracted secondary dex files, falling back to fresh extraction", var6);
                    files = this.performExtractions();    //出现异常重新执行提取dex
                    putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(this.sourceApk), this.sourceCrc, files);//出现异常保存apk时间戳 Crc码等信息缓存下来用于下次比对。
                }
            } else {
                if (forceReload) {
                    Log.i("MultiDex", "Forced extraction must be performed.");
                } else {
                    Log.i("MultiDex", "Detected that extraction must be performed.");
                }
                files = this.performExtractions();//走到else{}说明 没有缓存,本质上提取的是dex文件
                putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(this.sourceApk), this.sourceCrc, files);//把apk 信息缓存下来
            }

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

看上面的代码是不是有点懵,我先给大家梳理下大概的逻辑:
load()方法里面有两种逻辑,缓存过的loadExistingExtractions() 和没缓存过的performExtractions()
第一次获取dex,没有缓存过任何信息,应先执行performExtractions()。这是一个IO耗时操作(下面会细说),完成这个操作后把信息缓存下来(因为IO很耗时 不能每次都去操作),下一次则读取缓存的loadExistingExtractions(),速度会更快些。

private List performExtractions() throws IOException {  
        //  格式:base.apk.classes"
        String extractedFilePrefix = this.sourceApk.getName() + ".classes";
        this.clearDexDir();  //清理dex文件
        List files = new ArrayList();

        ZipFile apk = new ZipFile(this.sourceApk);// 把.apk转换成.zip

        try {
            int secondaryNumber = 2;
            //apk本质上就是归档文件上面步骤已经把apk变成了zip文件 ,for循环遍历zip文件 ,获取的dex文件
            // classes2.dex  classesN.dex
            for(ZipEntry dexFile = apk.getEntry("classes" + secondaryNumber + ".dex"); dexFile != null; dexFile = apk.getEntry("classes" + secondaryNumber + ".dex")) {
                //获取的应该是base.apk.classes2.zip
                String fileName = extractedFilePrefix + secondaryNumber + ".zip";
                //创建base.apk.classes2.zip 文件
                MultiDexExtractor.ExtractedDex extractedFile = new MultiDexExtractor.ExtractedDex(this.dexDir, fileName);//
                // 添加到文件列表(base.apk.classes2.zip 添加到/data/user/0/packageName/files/code_cache/secondary-dexes文件下)
                files.add(extractedFile);
                Log.i("MultiDex", "Extraction is needed for file " + extractedFile);
                int numAttempts = 0;
                boolean isExtractionSuccessful = false; //是否提取成功

                while(numAttempts < 3 && !isExtractionSuccessful) {
                    ++numAttempts;

                    //将classes2.dex文件写到压缩文件classes2.zip里去,最多重试三次
                    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; //返回dex的压缩文件列表
    }

上面的逻辑就是解压apk(apk来自applicationInfo.sourceDir()),遍历出里面的dex文件,例如 classes.dex, classesN.dex,然后又压缩成classes.zip,classesN.zip,然后返回zip文件列表。

总结:第一次加载才会执行耗时IO操作,第二次进来读取缓存中保存的dex信息,直接返回文件列表,所以第一次启动的时候比较耗时。

6.3 安装dex

dex列表已经返回了,该执行 installSecondaryDexes();进行dex安装

private static void installSecondaryDexes(ClassLoader loader, File dexDir, List files) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException, SecurityException, ClassNotFoundException, InstantiationException {
         //针对不同的api  分别进行逻辑处理
        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);
            }
        }
    }

看下 api19(v14 v4这是对不同版本做了处理 )dex安装处理了什么逻辑 MultiDex.V19.install()

 private static final class V19 {
        private V19() {
        }

        static void install(ClassLoader loader, List additionalClassPathEntries, File optimizedDirectory) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException {
            //反射获取ClassLoader 的 pathList 字段
            Field pathListField = MultiDex.findField(loader, "pathList");
            Object dexPathList = pathListField.get(loader);
            ArrayList suppressedExceptions = new ArrayList();
            //生成的Dex文件对应的Element数组
            //将Element数组插入到原有的dexElments数组后面
            MultiDex.expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, new ArrayList(additionalClassPathEntries), optimizedDirectory, suppressedExceptions));
            if (suppressedExceptions.size() > 0) {
                Iterator var6 = suppressedExceptions.iterator();

                while(var6.hasNext()) {
                    IOException e = (IOException)var6.next();
                    Log.w("MultiDex", "Exception in makeDexElement", e);
                }
                //反射获取到dexElements字段
                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;
            }
        }

        private static Object[] makeDexElements(Object dexPathList, ArrayList files, File optimizedDirectory, ArrayList suppressedExceptions) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
            Method makeDexElements = MultiDex.findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class, ArrayList.class);
            return (Object[])((Object[])makeDexElements.invoke(dexPathList, files, optimizedDirectory, suppressedExceptions));
        }
    }

上面代码做了以下的几个操作

  1. 反射获取到 pathList 字段

  2. 找到pathList 字段对应的类的makeDexElements 方法(也用到了反射)

  3. 通过MultiDex.expandFieldArray 这个方法扩展 dexElements 数组

就是创建一个新的数组,把主dex要增加的内容(classesdex2、classesdexN)拷贝进去,反射替换原来的dexElements为新的数组。

6.4详细源码注释

github地址有需要可以去clone :https://github.com/AndroidProg/MultiDexSources

七、使用 MultiDex 后首次启动 app 有什么优化方向

简单来说,安装完成并初次启动APP的时候,5.0以下某些低端机会出现ANR或者长时间卡顿不进入引导页,而罪魁祸首是MultiDex.install(Context context)的dexopt过程耗时过长。因此需要在初次启动时做特别处理。

解决这个问题的思路以及详细解释,大致分为3步:

  • Application.attachBaseContext(Context base)中,判断是否初次启动,以及系统版本是否小于5.0,如果是,跳到2;否则,直接执行MultiDex.install(Context context)
  • 开启一个新进程,在这个进程中执行MultiDex.install(Context context)。执行完毕,唤醒主进程,自身结束。主进程在开启新进程后,自身是挂起的,直到被唤醒。
  • 唤醒的主进程继续执行初始化操作。

这个解决思路对现有代码几乎是没有改动的,并且简单粗暴:既然不能直接调用MultiDex.install(Context context),那么干脆开启新进程调用,因为开启新进程时,主进程已经成为后台进程,即使挂起也不会ANR。
开启新进程加载太麻烦,能不能开新线程加载,而主线程继续Application初始化?显然不行。因为multidex安装没有结束,意味着dex还没加载进来,某些类强行使用就会报NoClassDefFoundError。

方案中有一个小难点是:主进程如何得知加载进程完成加载?我们使用标记来记录。MODE_MULTI_PROCESS标记使得SharedPreference得以进程间共享,主进程轮询sp文件即可。但是,这个标记在6.0被废除,Google不保证行为的准确性。而且……我们有一万个理由,能不写轮询就不写。那么我们该如何优雅的实现呢?
没错,今天我们使用Messenger的方式来实现:
话不多说,直接上终极版解决代码:
BaseApplication.class

public abstract class BaseApplication extends Application{

    @SuppressWarnings("MismatchedReadAndWriteOfArray")
    private static final byte[] lock = new byte[0];

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        //Android 5.0以上不需要特别处理
        if (!isAsyncLaunchProcess() && Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
            if (needWait(base)) {
                /*
                    第一次启动APP由于MultiDex将会非常缓慢,某些低端机可能ANR。
                    因此这里的做法是挂起主进程,开启:async_launch进程执行dexopt。
                    dexopt执行完毕,主进程重新变为前台进程,继续执行初始化。
                    主进程在这过程中变成后台进程,因此阻塞将不会引起ANR。
                 */
                DexInstallDeamonThread thread = new DexInstallDeamonThread(this, base);
                thread.start();

                //阻塞等待:async_launch完成加载
                synchronized (lock) {
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

                thread.exit();
                Log.d("BaseApplication", "dexopt finished. alloc MultiDex.install()");
            } else {
                MultiDex.install(this);
            }
        }
    }

    public boolean isAsyncLaunchProcess() {
        String processName = SystemUtils.getCurrentProcessName(this);
        return processName != null && processName.contains(":async_launch");
    }

    @SuppressWarnings("deprecation")
    private boolean needWait(Context context) {
        //这里实现不唯一,读取一个全局的标记,判断是否初次启动APP
        //这个标记应当随着版本升级而重置
        SharedPreferences sp = SPUtils.getVersionSharedPreferences(context);
        return sp.getBoolean(BaseSPKey.FIRST_LAUNCH, true);
    }

    private static class DexInstallDeamonThread extends Thread {

        private Handler handler;

        private Context application;

        private Context base;

        private Looper looper;

        public DexInstallDeamonThread(Context application, Context base) {
            this.application = application;
            this.base = base;
        }

        @SuppressLint("HandlerLeak")
        @Override
        public void run() {
            Looper.prepare();
            looper = Looper.myLooper();
            handler = new Handler() {

                @SuppressWarnings("deprecation")
                @Override
                public void handleMessage(Message msg) {
                    synchronized (lock) {
                        lock.notify();
                    }
                    SPUtils
                        .getVersionSharedPreferences(application)
                        .edit()
                        .putBoolean(BaseSPKey.FIRST_LAUNCH, false)
                        .apply();
                }
            };

            Messenger messenger = new Messenger(handler);
            Intent intent = new Intent(base, LoadResActivity.class);
            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            intent.putExtra("MESSENGER", messenger);
            base.startActivity(intent);
            Looper.loop();
        }

        public void exit() {
            if (looper != null) looper.quit();
        }
    }
}

LoadResActivity.class

public class LoadResActivity extends AppCompatActivity {

    private Messenger messenger;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        super.onCreate(savedInstanceState);
        getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
        overridePendingTransition(R.anim.null_anim, R.anim.null_anim);
        setContentView(R.layout.activity_load_res);

        Log.d("LoadResActivity", "start install");
        Intent from = getIntent();
        messenger = from.getParcelableExtra("MESSENGER");

        LoadDexTask dexTask = new LoadDexTask();
        dexTask.execute();
    }

    class LoadDexTask extends AsyncTask {

        @Override
        protected Void doInBackground(Void... params) {
            try {
                MultiDex.install(getApplication());
                Log.d("LoadResActivity", "finish install");
                messenger.send(new Message());
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }

        @Override
        protected void onPostExecute(Void o) {
            finish();
            System.exit(0);
        }
    }

    @Override
    public void onBackPressed() {
        //无法退出
    }
}

app/AndroidManifest.xml加入:


SystemUtils.class

public class SystemUtils {

    //...

    /**
     * 获取当前进程名。
     */
    public static String getCurrentProcessName(Context context) {
        ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
        if (activityManager == null) {
            return null;
        }
        for (ActivityManager.RunningAppProcessInfo processInfo : activityManager.getRunningAppProcesses()) {
            if (processInfo.pid == Process.myPid()) {
                return processInfo.processName;
            }
        }
        return null;
    }
}

最后,因为有大量代码在install之前执行,因此必须把代码中用到的类放到主dex中(需要build tool版本大于21):

app/build.gradle

android {

    //...

    defaultConfig {
        //...

        //定义main dex中必须保留的类
        multiDexKeepProguard file('mainDexClasses.pro')
    }
}

app/mainDexClasses.pro

#这里只是示例,需要把所有install之前用到的类写进来

-keep public class * extends java.lang.Thread { *; }

-keep public class com.synaric.common.utils.SystemUtils { *; }

-keep public class com.synaric.common.utils.SPUtils { *; }

-keep interface android.content.SharedPreferences { *; }

-keep class android.os.Handler { *; }

-keep class com.synaric.common.BaseSPKey { *; }

-keep class android.os.Messenger { *; }

-keep class android.content.Intent { *; }

八、如何将指定的 class 打进 mainDex


defaultConfig {
    //分包1
    multiDexEnabled true
    multiDexKeepProguard file('multiDexKeep.pro') // keep specific classes using proguard syntax
    multiDexKeepFile file('maindexlist.txt') // keep specific classes
}

这样才是真正可以的指定需要的类到maindex的方式,至于配置哪些类到maindex的文件maindexlist.txt可以在app\build\intermediates\multi-dex\debug目录下的mainfest_keep.txt文件找到,然后把自己的类复制粘贴到里面去就可以了。这个文件大致这样:



retrofit2/http/PATCH.class
android/support/design/widget/FloatingActionButtonIcs.class
android/support/design/R$layout.class
android/support/v4/view/NestedScrollingParent.class
android/support/v4/media/session/MediaControllerCompat.class
android/support/annotation/IdRes.class
android/support/v4/view/ViewPager$4.class
android/support/design/widget/Snackbar$2.class
retrofit2/http/Field.class
android/support/annotation/ColorInt.class

感谢
https://www.jianshu.com/p/e4bed5790407
https://blog.csdn.net/weixin_42009516/article/details/79986263
https://www.jianshu.com/p/6c02935a84f7
https://www.jianshu.com/p/c2d7b76ff063
https://blog.csdn.net/qq_17265737/article/details/79074494

你可能感兴趣的:(关于Android的65535限制)