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

当程序越来越大之后,呈现了一个 dex 包装不下的状况,经过 MultiDex 的办法处理了这个问题,可是在低端机器上又呈现了 INSTALL_FAILED_DEXOPT 的状况,那再处理这个问题吧。等处理完这个问题之后,发现需求填的坑越来越多了,文章讲的是我在分包处理中填的坑,比方 65536、LinearAlloc、NoClassDefFoundError等等。

INSTALL_FAILED_DEXOPT

INSTALL_FAILED_DEXOPT 呈现的原因大部分都是两种,一种是 65536 了,别的一种是 LinearAlloc 太小了。两者的限制不同,可是原因却是相似,那便是App太大了,导致没办法装置到手机上。

65536

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

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 

编译环境

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 的值是:

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 :

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

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

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();
            }
        }
    }

总的来说,这种办法好处在于依靠集十分简略,一起它的集成办法也是十分简略,咱们无须去修正与加载无关的代码。可是当没有发动过 App 的时分,被推送全家桶唤醒或许收到了播送,虽然这儿都是没有界面的进程,可是运用了这种加载办法的话会弹出 dexopt 进程的 Activity,用户看到会一脸懵比的。 引荐插件: github.com/TangXiaoLv/…

Too many classes inmain-dex-list
UNEXPECTED TOP-LEVEL EXCEPTION:com.android.dex.DexException: Too many classes inmain-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代码才会被唤醒

更多Android进阶指南 能够具体Vx重视公众号:Android老皮 解锁 《Android十大板块文档》

1.Android车载运用开发系统学习指南(附项目实战)

2.Android Framework学习指南,助力成为系统级开发高手

3.2023最新Android中高档面试题汇总+解析,离别零offer

4.企业级Android音视频开发学习道路+项目实战(附源码)

5.Android Jetpack从入门到通晓,构建高质量UI界面

6.Flutter技能解析与实战,跨渠道首要之选

7.Kotlin从入门到实战,全方面提高架构根底

8.高档Android插件化与组件化(含实战教程和源码)

9.Android 功能优化实战+360全方面功能调优

10.Android零根底入门到通晓,高手进阶之路

敲代码不易,重视一下吧。ღ( ・ᴗ・` )