Android Dex分包最全总结:含Facebook解决方案

当程序愈来愈大以后,出现了一个 dex 包装不下的状况,经过 MultiDex 的方法解决了这个问题,可是在低端机器上又出现了 INSTALL_FAILED_DEXOPT 的状况,那再解决这个问题吧。等解决完这个问题以后,发现须要填的坑愈来愈多了,文章讲的是我在分包处理中填的坑,好比 6553六、LinearAlloc、NoClassDefFoundError等等。java

INSTALL_FAILED_DEXOPT

INSTALL_FAILED_DEXOPT 出现的缘由大部分都是两种,一种是 65536 了,另一种是 LinearAlloc 过小了。二者的限制不一样,可是缘由倒是类似,那就是App太大了,致使没办法安装到手机上。node

65536

trouble writing output: Too many method references: 70048; max is 65536. 或者 UNEXPECTED TOP-LEVEL EXCEPTION:android

java.lang.IllegalArgumentException: method ID not in [0, 0xffff]: 65536
 at com.android.dx.merge.DexMerger$6.updateIndex(DexMerger.java:501)
 at com.android.dx.merge.DexMerger$IdMerger.mergeSorted(DexMerger.java:276)
 at com.android.dx.merge.DexMerger.mergeMethodIds(DexMerger.java:490)
 at com.android.dx.merge.DexMerger.mergeDexes(DexMerger.java:167)
 at com.android.dx.merge.DexMerger.merge(DexMerger.java:188)
 at com.android.dx.command.dexer.Main.mergeLibraryDexBuffers(Main.java:439)
 at com.android.dx.command.dexer.Main.runMonoDex(Main.java:287)
 at com.android.dx.command.dexer.Main.run(Main.java:230)
 at com.android.dx.command.dexer.Main.main(Main.java:199)
 at com.android.dx.command.Main.main(Main.java:103):Derp:dexDerpDebug FAILED 
复制代码

编译环境git

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:1.3.0'
    }
}

android {
    compileSdkVersion 23
    buildToolsVersion "25.0.3"
    //....
    defaultConfig {
        minSdkVersion 14
        targetSdkVersion 23
        //....
    }
}
复制代码

为何是65536

根据 StackOverFlow – Does the Android ART runtime have the same method limit limitations as Dalvik? 上面的说法,是由于 Dalvik 的 invoke-kind 指令集中,method reference index 只留了 16 bits,最多能引用 65535 个方法。Dalvik bytecode :程序员

  • 即便 dex 里面的引用方法数超过了 65536,那也只有前面的 65536 得的到调用。因此这个不是 dex 的缘由。其次,既然和 dex 没有关系,那在打包 dex 的时候为何会报错。咱们先定位 Too many 关键字,定位到了 MemberIdsSection :
public abstract class MemberIdsSection extends UniformItemSection {
  /** {@inheritDoc} */
    @Override
    protected void orderItems() {
        int idx = 0;

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

        for (Object i : items()) {
            ((MemberIdItem) i).setIndex(idx);
            idx++;
        }
    }

    private String getTooManyMembersMessage() {
        Map<String, AtomicInteger> membersByPackage = new TreeMap<String, AtomicInteger>();
        for (Object member : items()) {
            String packageName = ((MemberIdItem) member).getDefiningClass().getPackageName();
            AtomicInteger count = membersByPackage.get(packageName);
            if (count == null) {
                count = new AtomicInteger();
                membersByPackage.put(packageName, count);
            }
            count.incrementAndGet();
        }

        Formatter formatter = new Formatter();
        try {
            String memberType = this instanceof MethodIdsSection ? "method" : "field";
            formatter.format("Too many %s references: %d; max is %d.%n" +
                    Main.getTooManyIdsErrorMessage() + "%n" +
                    "References by package:",
                    memberType, items().size(), DexFormat.MAX_MEMBER_IDX + 1);
            for (Map.Entry<String, AtomicInteger> entry : membersByPackage.entrySet()) {
                formatter.format("%n%6d %s", entry.getValue().get(), entry.getKey());
            }
            return formatter.toString();
        } finally {
            formatter.close();
        }
    }
}
复制代码

items().size() > DexFormat.MAX_MEMBER_IDX + 1 ,那 DexFormat 的值是:github

public final class 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;
}
复制代码

dx 在这里作了判断,当大于 65536 的时候就抛出异常了。因此在生成 dex 文件的过程当中,当调用方法数不能超过 65535 。那咱们再跟一跟代码,发现 MemberIdsSection 的一个子类叫 MethodidsSection :小程序

public final class MethodIdsSection extends MemberIdsSection {}
复制代码

回过头来,看一下 orderItems() 方法在哪里被调用了,跟到了 MemberIdsSection 的父类 UniformItemSection :windows

public abstract class UniformItemSection extends Section {
    @Override
    protected final void prepare0() {
        DexFile file = getFile();

        orderItems();

        for (Item one : items()) {
            one.addContents(file);
        }
    }

    protected abstract void orderItems();
}
复制代码

再跟一下 prepare0 在哪里被调用,查到了 UniformItemSection 父类 Section :微信小程序

public abstract class Section {
    public final void prepare() {
        throwIfPrepared();
        prepare0();
        prepared = true;
    }

    protected abstract void prepare0();
}
复制代码

那如今再跟一下 prepare() ,查到 DexFile 中有调用:安全

public final class DexFile {
  private ByteArrayAnnotatedOutput toDex0(boolean annotate, boolean verbose) {
        classDefs.prepare();
        classData.prepare();
        wordData.prepare();
        byteData.prepare();
        methodIds.prepare();
        fieldIds.prepare();
        protoIds.prepare();
        typeLists.prepare();
        typeIds.prepare();
        stringIds.prepare();
        stringData.prepare();
        header.prepare();
        //blablabla......
    }
}
复制代码

那再看一下 toDex0() 吧,由于是 private 的,直接在类中找调用的地方就能够了:

public final class DexFile {
    public byte[] toDex(Writer humanOut, boolean verbose) throws IOException {
        boolean annotate = (humanOut != null);
        ByteArrayAnnotatedOutput result = toDex0(annotate, verbose);

        if (annotate) {
            result.writeAnnotationsTo(humanOut);
        }

        return result.getArray();
    }

    public void writeTo(OutputStream out, Writer humanOut, boolean verbose) throws IOException {
        boolean annotate = (humanOut != null);
        ByteArrayAnnotatedOutput result = toDex0(annotate, verbose);

        if (out != null) {
            out.write(result.getArray());
        }

        if (annotate) {
            result.writeAnnotationsTo(humanOut);
        }
    }
}
复制代码

先搜搜 toDex() 方法吧,最终发如今 com.android.dx.command.dexer.Main 中:

public class Main {
    private static byte[] writeDex(DexFile outputDex) {
        byte[] outArray = null;
        //blablabla......
        if (args.methodToDump != null) {
            outputDex.toDex(null, false);
            dumpMethod(outputDex, args.methodToDump, humanOutWriter);
        } else {
            outArray = outputDex.toDex(humanOutWriter, args.verboseDump);
        }
        //blablabla......
        return outArray;
    }
    //调用writeDex的地方
    private static int runMonoDex() throws IOException {
        //blablabla......
        outArray = writeDex(outputDex);
        //blablabla......
    }
    //调用runMonoDex的地方
    public static int run(Arguments arguments) throws IOException {
        if (args.multiDex) {
            return runMultiDex();
        } else {
            return runMonoDex();
        }
    }
}
复制代码

args.multiDex 就是是否分包的参数,那么问题找着了,若是不选择分包的状况下,引用方法数超过了 65536 的话就会抛出异常。

一样分析第二种状况,根据错误信息能够具体定位到代码,可是很奇怪的是 DexMerger ,咱们没有设置分包参数或者其余参数,为何会有 DexMerger ,并且依赖工程最终不都是 aar 格式的吗?那咱们仍是来跟一跟代码吧。

public class Main {
    private static byte[] mergeLibraryDexBuffers(byte[] outArray) throws IOException {
        ArrayList<Dex> dexes = new ArrayList<Dex>();
        if (outArray != null) {
            dexes.add(new Dex(outArray));
        }
        for (byte[] libraryDex : libraryDexBuffers) {
            dexes.add(new Dex(libraryDex));
        }
        if (dexes.isEmpty()) {
            return null;
        }
        Dex merged = new DexMerger(dexes.toArray(new Dex[dexes.size()]), CollisionPolicy.FAIL).merge();
        return merged.getBytes();
    }
}
复制代码

这里能够看到变量 libraryDexBuffers ,是一个 List 集合,那么咱们看一下这个集合在哪里添加数据的:

public class Main {
    private static boolean processFileBytes(String name, long lastModified, byte[] bytes) {
        boolean isClassesDex = name.equals(DexFormat.DEX_IN_JAR_NAME);
        //blablabla...
        } else if (isClassesDex) {
            synchronized (libraryDexBuffers) {
                libraryDexBuffers.add(bytes);
            }
            return true;
        } else {
        //blablabla...
    }
    //调用processFileBytes的地方
    private static class FileBytesConsumer implements ClassPathOpener.Consumer {

        @Override
        public boolean processFileBytes(String name, long lastModified,
                byte[] bytes)   {
            return Main.processFileBytes(name, lastModified, bytes);
        }
        //blablabla...
    }
    //调用FileBytesConsumer的地方
    private static void processOne(String pathname, FileNameFilter filter) {
        ClassPathOpener opener;

        opener = new ClassPathOpener(pathname, true, filter, new FileBytesConsumer());

        if (opener.process()) {
          updateStatus(true);
        }
    }
    //调用processOne的地方
    private static boolean processAllFiles() {
        //blablabla...
        // forced in main dex
        for (int i = 0; i < fileNames.length; i++) {
            processOne(fileNames[i], mainPassFilter);
        }
        //blablabla...
    }
    //调用processAllFiles的地方
    private static int runMonoDex() throws IOException {
        //blablabla...
        if (!processAllFiles()) {
            return 1;
        }
        //blablabla...
    }

}
复制代码

跟了一圈又跟回来了,可是注意一个变量:fileNames[i],传进去这个变量,是个地址,最终在 processFileBytes 中处理后添加到 libraryDexBuffers 中,那跟一下这个变量:

public class Main {
    private static boolean processAllFiles() {
        //blablabla...
        String[] fileNames = args.fileNames;
        //blablabla...
    }
    public void parse(String[] args) {
        //blablabla...
        }else if(parser.isArg(INPUT_LIST_OPTION + "=")) {
            File inputListFile = new File(parser.getLastValue());
            try{
                inputList = new ArrayList<String>();
                readPathsFromFile(inputListFile.getAbsolutePath(), inputList);
            } catch(IOException e) {
                System.err.println("Unable to read input list file: " + inputListFile.getName());
                throw new UsageException();
            }
        } else {
        //blablabla...
        fileNames = parser.getRemaining();
        if(inputList != null &amp;&amp; !inputList.isEmpty()) {
            inputList.addAll(Arrays.asList(fileNames));
            fileNames = inputList.toArray(new String[inputList.size()]);
        }
    }

    public static void main(String[] argArray) throws IOException {
        Arguments arguments = new Arguments();
        arguments.parse(argArray);

        int result = run(arguments);
        if (result != 0) {
            System.exit(result);
        }
    }
}
复制代码

跟到这里发现是传进来的参数,那咱们再看看 gradle 里面传的是什么参数吧,查看 Dex task :

public class Dex extends BaseTask {
    @InputFiles
    Collection<File> libraries
}
咱们把这个参数打印出来:

afterEvaluate {
    tasks.matching {
        it.name.startsWith('dex')
    }.each { dx ->
        if (dx.additionalParameters == null) {
            dx.additionalParameters = []
        }
        println dx.libraries
    }
}
复制代码

打印出来发现是 build/intermediates/pre-dexed/ 目录里面的 jar 文件,再把 jar 文件解压发现里面就是 dex 文件了。因此 DexMerger 的工做就是合并这里的 dex 。

更改编译环境

buildscript {
    //...
    dependencies {
        classpath 'com.android.tools.build:gradle:2.1.0-alpha3'
    }
}
复制代码

将 gradle 设置为 2.1.0-alpha3 以后,在项目的 build.gradle 中即便没有设置 multiDexEnabled true 也可以编译经过,可是生成的 apk 包依旧是两个 dex ,我想的是可能为了设置 instantRun 。

解决 65536

Google MultiDex 解决方案:

在 gradle 中添加 MultiDex 的依赖:

dependencies { compile 'com.android.support:MultiDex:1.0.0' }
复制代码

在 gradle 中配置 MultiDexEnable :

android {
    buildToolsVersion "21.1.0"
    defaultConfig {
        // Enabling MultiDex support.
        MultiDexEnabled true
  }
}
复制代码

在 AndroidManifest.xml 的 application 中声明:

<application
  android:name="android.support.multidex.MultiDexApplication">
<application/>
复制代码

若是有本身的 Application 了,让其继承于 MultiDexApplication 。

若是继承了其余的 Application ,那么能够重写 attachBaseContext(Context):

@Override 
protected void attachBaseContext(Context base) {
    super.attachBaseContext(base);
    MultiDex.install(this);
}
复制代码

LinearAlloc

gradle:

afterEvaluate { 
  tasks.matching { 
    it.name.startsWith('dex') 
  }.each { dx -> 
    if (dx.additionalParameters == null) { 
      dx.additionalParameters = []
    }  
    dx.additionalParameters += '--set-max-idx-number=48000' 
  } 
}
复制代码

--set-max-idx-number= 用于控制每个 dex 的最大方法个数。

这个参数在查看 dx.jar 找到:

//blablabla...
} else if (parser.isArg("--set-max-idx-number=")) { // undocumented test option
  maxNumberOfIdxPerDex = Integer.parseInt(parser.getLastValue());
} else if(parser.isArg(INPUT_LIST_OPTION + "=")) {
//blablabla...
复制代码

更多细节能够查看源码:Github – platform_dalvik/Main

FB 的工程师们曾经还想到过直接修改 LinearAlloc 的大小,好比从 5M 修改到 8M: Under the Hood: Dalvik patch for Facebook for Android 。

dexopt && dex2oat

Picture

dexopt

当 Android 系统安装一个应用的时候,有一步是对 Dex 进行优化,这个过程有一个专门的工具来处理,叫 DexOpt。DexOpt 是在第一次加载 Dex 文件的时候执行的,将 dex 的依赖库文件和一些辅助数据打包成 odex 文件,即 Optimised Dex,存放在 cache/dalvik_cache 目录下。保存格式为 apk路径 @ apk名 @ classes.dex 。执行 ODEX 的效率会比直接执行 Dex 文件的效率要高不少。

dex2oat

Android Runtime 的 dex2oat 是将 dex 文件编译成 oat 文件。而 oat 文件是 elf 文件,是能够在本地执行的文件,而 Android Runtime 替换掉了虚拟机读取的字节码转而用本地可执行代码,这就被叫作 AOT(ahead-of-time)。dex2oat 对全部 apk 进行编译并保存在 dalvik-cache 目录里。PackageManagerService 会持续扫描安装目录,若是有新的 App 安装则立刻调用 dex2oat 进行编译。

NoClassDefFoundError

如今 INSTALL_FAILED_DEXOPT 问题是解决了,可是有时候编译完运行的时候一打开 App 就 crash 了,查看 log 发现是某个类找不到引用。

  • Build Tool 是如何分包的 为何会这样呢?是由于 build-tool 在分包的时候只判断了直接引用类。什么是直接引用类呢?举个栗子:
public class MainActivity extends Activity {
    protected void onCreate(Bundle savedInstanceState) {
        DirectReferenceClass test = new DirectReferenceClass();
    }
}

public class DirectReferenceClass {
    public DirectReferenceClass() {
        InDirectReferenceClass test = new InDirectReferenceClass();
    }
}

public class InDirectReferenceClass {
    public InDirectReferenceClass() {

    }
}
复制代码

上面有 MainActivity、DirectReferenceClass 、InDirectReferenceClass 三个类,其中 DirectReferenceClass 是 MainActivity 的直接引用类,InDirectReferenceClass 是 DirectReferenceClass 的直接引用类。而 InDirectReferenceClass 是 MainActivity 的间接引用类(即直接引用类的全部直接引用类)。

若是咱们代码是这样写的:

public class HelloMultiDexApplication extends Application {
    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        DirectReferenceClass test = new DirectReferenceClass();
        MultiDex.install(this);
    }
}
复制代码

这样直接就 crash 了。同理还要单例模式中拿到单例以后直接调用某个方法返回的是另一个对象,并不是单例对象。

build tool 的分包操做能够查看 sdk 中 build-tools 文件夹下的 mainDexClasses 脚本,同时还发现了 mainDexClasses.rules 文件,该文件是主 dex 的匹配规则。该脚本要求输入一个文件组(包含编译后的目录或jar包),而后分析文件组中的类并写入到–output所指定的文件中。实现原理也不复杂,主要分为三步:

  • 环境检查,包括传入参数合法性检查,路径检查以及proguard环境检测等。

  • 使用mainDexClasses.rules规则,经过Proguard的shrink功能,裁剪无关类,生成一个tmp.jar包。

  • 经过生成的tmp jar包,调用MainDexListBuilder类生成主dex的文件列表

Gradle 打包流程中是如何分包的

在项目中,能够直接运行 gradle 的 task 。

  • collect{flavor}{buildType}MultiDexComponents Task 。这个 task 是获取 AndroidManifest.xml 中 Application 、Activity 、Service 、 Receiver 、 Provider 等相关类,以及 Annotation ,以后将内容写到 build/intermediates/multi-dex/{flavor}/{buildType}/maindexlist.txt 文件中去。

  • packageAll{flavor}DebugClassesForMultiDex Task 。该 task 是将全部类打包成 jar 文件存在 build/intermediates/multi-dex/{flavor}/debug/allclasses.jar 。 当 BuildType 为 Release 的时候,执行的是 proguard{flavor}Release Task,该 task 将 proguard 混淆后的类打包成 jar 文件存在 build/intermediates/classes-proguard/{flavor}/release/classes.jar

  • shrink{flavor}{buildType}MultiDexComponents Task 。该 task 会根据 maindexlist.txt 生成 componentClasses.jar ,该 jar 包里面就只有 maindexlist.txt 里面的类,该 jar 包的位置在 build/intermediates/multi-dex/{flavor}/{buildType}/componentClasses.jar

  • create{flavor}{buildType}MainDexClassList Task 。该 task 会根据生成的 componentClasses.jar 去找这里面的全部的 class 中直接依赖的 class ,而后将内容写到 build/intermediates/multi-dex/{flavor}/{buildType}/maindexlist.txt 中。最终这个文件里面列出来的类都会被分配到第一个 dex 里面。

解决 NoClassDefFoundError

gradle :

afterEvaluate { 
  tasks.matching { 
    it.name.startsWith('dex') 
  }.each { dx -> 
    if (dx.additionalParameters == null) { 
      dx.additionalParameters = []
    }  
    dx.additionalParameters += '--set-max-idx-number=48000' 
    dx.additionalParameters += "--main-dex-list=$projectDir/multidex.keep".toString()
  } 
}
复制代码

--main-dex-list= 参数是一个类列表的文件,在该文件中的类会被打包在第一个 dex 中。

multidex.keep 里面列上须要打包到第一个 dex 的 class 文件,注意,若是须要混淆的话须要写混淆以后的 class 。

Application Not Responding

由于第一次运行(包括清除数据以后)的时候须要 dexopt ,然而 dexopt 是一个比较耗时的操做,同时 MultiDex.install() 操做是在 Application.attachBaseContext() 中进行的,占用的是UI线程。那么问题来了,当个人第二个包、第三个包很大的时候,程序就阻塞在 MultiDex.install() 这个地方了,一旦超过规定时间,那就 ANR 了。那怎么办?放子线程?若是 Application 有一些初始化操做,到初始化操做的地方的时候都尚未完成 install + dexopt 的话,那不是又 NoClassDefFoundError 了吗?同时 ClassLoader 放在哪一个线程都让主线程挂起。好了,那在 multidex.keep 的加上相关的全部的类吧。好像这样成了,可是第一个 dex 又大起来了,并且若是用户操做快,还没完成 install + dexopt 可是已经把 App 因此界面都打开了一遍。。。虽然这不现实。。

微信加载方案

首次加载在地球中页中, 并用线程去加载(可是 5.0 以前加载 dex 时仍是会挂起主线程一段时间(不是全程都挂起))。

  • dex 形式

微信是将包放在 assets 目录下的,在加载 Dex 的代码时,实际上传进去的是 zip,在加载前须要验证 MD5,确保所加载的 Dex 没有被篡改。

  • dex 类分包规则

分包规则即将全部 Application、ContentProvider 以及全部 export 的 Activity、Service 、Receiver 的间接依赖集都必须放在主 dex。

  • 加载 dex 的方式

加载逻辑这边主要判断是否已经 dexopt,若已经 dexopt,即放在 attachBaseContext 加载,反之放于地球中用线程加载。怎么判断?由于在微信中,若判断 revision 改变,即将 dex 以及 dexopt 目录清空。只需简单判断两个目录 dex 名称、数量是否与配置文件的一致。

总的来讲,这种方案用户体验较好,缺点在于太过复杂,每次都需从新扫描依赖集,并且使用的是比较大的间接依赖集。

Facebook 加载方案

Facebook的思路是将 MultiDex.install() 操做放在另一个常常进行的。

  • dex 形式

与微信相同。

  • dex 类分包规则

Facebook 将加载 dex 的逻辑单独放于一个单独的 nodex 进程中。

<activity 
android:exported="false"
android:process=":nodex"android:name="com.facebook.nodex.startup.splashscreen.NodexSplashActivity">
复制代码

全部的依赖集为 Application、NodexSplashActivity 的间接依赖集便可。

  • 加载 dex 的方式

由于 NodexSplashActivity 的 intent-filter 指定为 Main 和LAUNCHER ,因此一打开 App 首先拉起 nodex 进程,而后打开 NodexSplashActivity 进行 MultiDex.install() 。若是已经进行了 dexpot 操做的话就直接跳转主界面,没有的话就等待 dexpot 操做完成再跳转主界面。

这种方式好处在于依赖集很是简单,同时首次加载 dex 时也不会卡死。可是它的缺点也很明显,即每次启动主进程时,都需先启动 nodex 进程。尽管 nodex 进程逻辑很是简单,这也需100ms以上。

美团加载方案

  • dex 形式 在 gradle 生成 dex 文件的这步中,自定义一个 task 来干预 dex 的生产过程,从而产生多个 dex 。
tasks.whenTaskAdded { task ->
   if (task.name.startsWith('proguard') &amp;&amp; (task.name.endsWith('Debug') || task.name.endsWith('Release'))) {
       task.doLast {
           makeDexFileAfterProguardJar();
       }
       task.doFirst {
           delete "${project.buildDir}/intermediates/classes-proguard";

           String flavor = task.name.substring('proguard'.length(), task.name.lastIndexOf(task.name.endsWith('Debug') ? "Debug" : "Release"));
           generateMainIndexKeepList(flavor.toLowerCase());
       }
   } else if (task.name.startsWith('zipalign') &amp;&amp; (task.name.endsWith('Debug') || task.name.endsWith('Release'))) {
       task.doFirst {
           ensureMultiDexInApk();
       }
   }
} 
复制代码
  • dex 类分包规则 把 Service、Receiver、Provider 涉及到的代码都放到主 dex 中,而把 Activity 涉及到的代码进行了必定的拆分,把首页 Activity、Laucher Activity 、欢迎页的 Activity 、城市列表页 Activity 等所依赖的 class 放到了主 dex 中,把二级、三级页面的 Activity 以及业务频道的代码放到了第二个 dex 中,为了减小人工分析 class 的依赖所带了的不可维护性和高风险性,美团编写了一个可以自动分析 class 依赖的脚本, 从而可以保证主 dex 包含 class 以及他们所依赖的全部 class 都在其内,这样这个脚本就会在打包以前自动分析出启动到主 dex 所涉及的全部代码,保证主 dex 运行正常。

  • 加载 dex 的方式 经过分析 Activity 的启动过程,发现 Activity 是由 ActivityThread 经过 Instrumentation 来启动的,那么是否能够在 Instrumentation 中作必定的手脚呢?经过分析代码 ActivityThread 和 Instrumentation 发现,Instrumentation 有关 Activity 启动相关的方法大概有:execStartActivity、 newActivity 等等,这样就能够在这些方法中添加代码逻辑进行判断这个 class 是否加载了,若是加载则直接启动这个 Activity,若是没有加载完成则启动一个等待的 Activity 显示给用户,而后在这个 Activity 中等待后台第二个 dex 加载完成,完成后自动跳转到用户实际要跳转的 Activity;这样在代码充分解耦合,以及每一个业务代码可以作到颗粒化的前提下,就作到第二个 dex 的按需加载了。

美团的这种方式对主 dex 的要求很是高,由于第二个 dex 是等到须要的时候再去加载。重写Instrumentation 的 execStartActivity 方法,hook 跳转 Activity 的总入口作判断,若是当前第二个 dex 尚未加载完成,就弹一个 loading Activity等待加载完成。

综合加载方案

微信的方案须要将 dex 放于 assets 目录下,在打包的时候太过负责;Facebook 的方案每次进入都是开启一个 nodex 进程,而咱们但愿节省资源的同时快速打开 App;美团的方案确实很 hack,可是对于项目已经很庞大,耦合度又比较高的状况下并不适合。因此这里尝试结合三个方案,针对本身的项目来进行优化。

  • dex 形式 第一,为了可以继续支持 Android 2.x 的机型,咱们将每一个包的方法数控制在 48000 个,这样最后分出来 dex 包大约在 5M 左右;第二,为了防止 NoClassDefFoundError 的状况,咱们找出来启动页、引导页、首页比较在乎的一些类,好比 Fragment 等(由于在生成 maindexlist.txt 的时候只会找 Activity 的直接引用,好比首页 Activity 直接引用 AFragemnt,可是 AFragment 的引用并无去找)。

  • dex 类分包规则 第一个包放 Application、Android四大组件以及启动页、引导页、首页的直接引用的 Fragment 的引用类,还放了推送消息过来点击 Notification 以后要展现的 Activity 中的 Fragment 的引用类。 Fragment 的引用类是写了一个脚本,输入须要找的类而后将这些引用类写到 multidex.keep 文件中,若是是 debug 的就直接在生成的 jar 里面找,若是是 release 的话就经过 mapping.txt 找,找不到的话再去 jar 里面找,因此在 gradle 打包的过程当中咱们人为干扰一下:

tasks.whenTaskAdded { task ->
    if (task.name.startsWith("create") &amp;&amp; task.name.endsWith("MainDexClassList")) {
        task.doLast {
            def flavorAndBuildType = task.name.substring("create".length(), task.name.length() - "MainDexClassList".length())
            autoSplitDex.configure {
                description = flavorAndBuildType
            }
            autoSplitDex.execute()
        }
    } 
}
复制代码

详细代码可见:Github — PhotoNoter/gradle

  • 加载 dex 的方式 在防止 ANR 方面,咱们采用了 Facebook 的思路。可是稍微有一点区别,差异在于咱们并不在一开启 App 的时候就去起进程,而是一开启 App 的时候在主进程里面判断是否 dexopt 过没,没有的话再去起另外的进程的 Activity 专门作 dexopt 操做 。一旦拉起了去作 dexopt 的进程,那么让主进程进入一个死循环,一直等到 dexopt 进程结束再结束死循环往下走。那么问题来了,第一,主进程进入死循环会 ANR 吗?第二,如何判断是否 dexopt 过;第三,为了界面友好,dexopt 的进程该怎么作;第四,主进程怎么知道 dexopt 进程结束了,也就是怎么去作进程间通讯。

  • 一个一个问题的解决,先第一个:由于当拉起 dexopt 进程以后,咱们在 dexopt 进程的 Activity 中进行 MultiDex.install() 操做,此时主进程再也不是前台进程了,因此不会 ANR 。

  • 第二个问题:由于第一次启动是什么数据都没有的,那么咱们就创建一个 SharedPreference ,启动的时候先去从这里获取数据,若是没有数据那么也就是没有 dexopt 过,若是有数据那么确定是 dexopt 过的,可是这个 SharedPreference 咱们得保证咱们的程序只有这个地方能够修改,其余地方不能修改。

  • 第三个问题:由于 App 的启动也是一张图片,因此在 dexopt 的 Activity 的 layout 中,咱们就把这张图片设置上去就行了,当关闭 dexopt 的 Activity 的时候,咱们得关闭 Activity 的动画。同时为了避免让 dexopt 进程发生 ANR ,咱们将 MultiDex.install() 过程放在了子线程中进行。

  • 第四个问题:Linux 的进程间通讯的方式有不少,Android 中还有 Binder 等,那么咱们这里采用哪一种方式比较好呢?首先想到的是既然 dexopt 进程结束了天然在主进程的死循环中去判断 dexopt 进程是否存在。可是在实际操做中发现,dexopt 虽然已经退出了,可是进程并无立刻被回收掉,因此这个方法走不通。那么用 Broadcast 广播能够吗?但是能够,可是增长了 Application 的负担,在拉起 dexopt 进程前还得注册一个动态广播,接收到广播以后还得注销掉,因此这个也没有采用。那么最终采用的方式是判断文件是否存在,在拉起 dexopt 进程前在某个安全的地方创建一个临时文件,而后死循环判断这个文件是否存在,在 dexopt 进程结束的时候删除这个临时文件,那么在主进程的死循环中发现此文件不存在了,就直接跳出循环,继续 Application 初始化操做。

public class NoteApplication extends Application {
@Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        //开启dex进程的话也会进入application
        if (isDexProcess()) {
            return;
        }
        doInstallBeforeLollipop();
        MultiDex.install(this);
    }

    @Override
    public void onCreate() {
        super.onCreate();
        if (isDexProcess()) {
            return;
        }
      //其余初始化
    }

  private void doInstallBeforeLollipop() {
        //知足3个条件,1.第一次安装开启,2.主进程,3.API<21(由于21以后ART的速度比dalvik快接近10倍(毕竟5.0以后的手机性能也要好不少))
        if (isAppFirstInstall() &amp;&amp; !isDexProcessOrOtherProcesses() &amp;&amp; Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
            try {
                createTempFile();
                startDexProcess();
                while (true) {
                    if (existTempFile()) {
                        try {
                            Thread.sleep(50);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    } else {
                        setAppNoteFirstInstall();
                        break;
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
复制代码

详细代码可见:Github — PhotoNoter/NoteApplication

总的来讲,这种方式好处在于依赖集很是简单,同时它的集成方式也是很是简单,咱们无须去修改与加载无关的代码。可是当没有启动过 App 的时候,被推送全家桶唤醒或者收到了广播,虽然这里都是没有界面的过程,可是运用了这种加载方式的话会弹出 dexopt 进程的 Activity,用户看到会一脸懵比的。 推荐插件: github.com/TangXiaoLv/…

Too many classes in –main-dex-list
UNEXPECTED TOP-LEVEL EXCEPTION:com.android.dex.DexException: Too many classes in –main-dex-list, main dex capacity exceeded at 
com.android.dx.command.dexer.Main.processAllFiles(Main.java:494) at 
com.android.dx.command.dexer.Main.runMultiDex(Main.java:332) at 
com.android.dx.command.dexer.Main.run(Main.java:243) at 
com.android.dx.command.dexer.Main.main(Main.java:214) at 
com.android.dx.command.Main.main(Main.java:106)
复制代码

经过 sdk 的 mainDexClasses.rules 知道主 dex 里面会有 Application、Activity、Service、Receiver、Provider、Instrumentation、BackupAgent 和 Annotation。当这些类以及直接引用类比较多的时候,都要塞进主 dex ,就引起了 main dex capacity exceeded build error 。

为了解决这个问题,当执行 Create{flavor}{buildType}ManifestKeepList task 以前将其中的 activity 去掉,以后会发现 /build/intermediates/multi_dex/{flavor}/{buildType}/manifest_keep.txt 文件中已经没有 Activity 相关的类了。

def patchKeepSpecs() {
def taskClass = "com.android.build.gradle.internal.tasks.multidex.CreateManifestKeepList";
def clazz = this.class.classLoader.loadClass(taskClass)
def keepSpecsField = clazz.getDeclaredField("KEEP_SPECS")
keepSpecsField.setAccessible(true)
def keepSpecsMap = (Map) keepSpecsField.get(null)
if (keepSpecsMap.remove("activity") != null) {
println "KEEP_SPECS patched: removed 'activity' root"
} else {
println "Failed to patch KEEP_SPECS: no 'activity' root found"
}
}
复制代码

patchKeepSpecs() 详细能够看 CreateManifestKeepList 的源码:Github – CreateManifestKeepList

Too many classes in –main-dex-list 没错,仍是 Too many classes in –main-dex-list 的错误。在美团的自动拆包中讲到:

实际应用中咱们还遇到另一个比较棘手的问题, 就是Field的过多的问题,Field过可能是由咱们目前采用的代码组织结构引入的,咱们为了方便多业务线、多团队并发协做的状况下开发,咱们采用的aar的方式进行开发,并同时在aar依赖链的最底层引入了一个通用业务aar,而这个通用业务aar中包含了不少资源,而ADT14以及更高的版本中对Library资源处理时,Library的R资源再也不是static final的了,详情请查看google官方说明,这样在最终打包时Library中的R无法作到内联,这样带来了R field过多的状况,致使须要拆分多个Secondary DEX,为了解决这个问题咱们采用的是在打包过程当中利用脚本把Libray中R field(例如ID、Layout、Drawable等)的引用替换成常量,而后删去Library中R.class中的相应Field。

一样,hu关于这个问题能够参考这篇大神的文章:当Field邂逅65535 。

DexException: Library dex files are not supported in multi-dex mode
com.android.dex.DexException: Library dex files are not supported in multi-dex mode
 at com.android.dx.command.dexer.Main.runMultiDex(Main.java:322)
 at com.android.dx.command.dexer.Main.run(Main.java:228)
 at com.android.dx.command.dexer.Main.main(Main.java:199)
 at com.android.dx.command.Main.main(Main.java:103)
复制代码

解决:

android {
dexOptions {
preDexLibraries = false
}
}
OutOfMemoryError: Java heap space
UNEXPECTED TOP-LEVEL ERROR:
java.lang.OutOfMemoryError: Java heap space
复制代码

解决:

android {
dexOptions {
javaMaxHeapSize "2g"
}
}
复制代码

Android 分包之旅技术分享疑难解答

Q1:Facebook mutidex 方案为什么要多起一个进程,若是采用单进程 线程去处理呢?

答:install能不能放到线程里作?若是开新线程加载,而主线程继续Application初始化—-——致使若是异步化,multidex安装没有结束意味着dex还没加载进来,这时候若是进程须要seconday.dex里的classes信息不就悲剧了—-某些类强行使用就会报NoClassDefFoundError.

FaceBook多dex分包方案

安装完成以后第一次启动时,是secondary.dex的dexopt花费了更多的时间,认识到这点很是重要,使得问题转化为:在不阻塞UI线程的前提下,完成dexopt,之后都不须要再次dexopt,因此能够在UI线程install dex了 咱们如今想作到的是:既但愿在Application的attachContext()方法里同步加载secondary.dex,又不但愿卡住UI线程

FB的方案就是:

让Launcher Activity在另一个进程启动,可是Multidex.install仍是在Main Process中开启,虽然逻辑上已经不承担dexopt的任务 这个Launcher Activity就是用来异步触发dexopt的 ,load完成就启动Main Activity;若是已经loaded,则直接启动Main Process Multidex.install所引起的合并耗时操做,是在前台进程的异步任务中执行的,因此没有anr的风险

Q2:当没有启动过 App 的时候,被推送全家桶唤醒或者收到了广播(App已经处于不是第一次启动过)

会唤醒,并且会出现dexopt的独立进程页面activity,一闪而过用户会懵逼… 改进采用新的思路会唤起新进程,可是该进程只会触发一次… 如何保证只触发一次? 咱们先判断是否第一次安装启动应用,当应用不是第一次安装启动时,咱们直接启动闪屏页,而且结束掉子进程便可。

Q3:处于第一次安装成功以后,app收到推送全家桶是否会被唤醒?

不会,由于须要首次在application执行过一次推送的init代码才会被唤醒

Q4:最终方案?

示例代码参考

读者福利:

好了,写到这里也结束了,在文章最后放上一个小小的福利,如下为小编本身在学习过程当中整理出的一个学习思路及方向,从事互联网开发,最主要的是要学好技术,而学习技术是一条慢长而艰苦的道路,不能靠一时激情,也不是熬几天几夜就能学好的,必须养成平时努力学习的习惯,更加须要准确的学习方向达到有效的学习效果。 因为内容较多就只放上一个大概的大纲,以后还有免费的高级UI、性能优化、移动架构师、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter全方面的Android进阶实践技术资料。

下面是部分资料截图,特别适合有3-5年开发经验的Android程序员们学习。

资料免费领取方式:点赞+加群Android架构设计(185873940)

  • Android前沿技术—组件化框架设计大纲

  • 全套体系化高级架构视频——组件化;视频+源码+笔记

本人Java开发4年Android开发5年,按期分享Android高级技术及经验分享,欢迎你们关注~(分享内容包括不限于高级UI、性能优化、架构师课程、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter等全方面的Android进阶实践技术;但愿能帮助到你们,也节省你们在网上搜索资料的时间来学习,也能够分享动态给身边好友一块儿学习!)

相关文章
相关标签/搜索