Android开发经验谈Android技术知识Android收藏集

Android Dex分包之旅

2019-04-25  本文已影响7人  Android高级开发

当程序越来越大之后,出现了一个 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 :

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

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 发现是某个类找不到引用。

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所指定的文件中。实现原理也不复杂,主要分为三步:

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

在项目中,可以直接运行 gradle 的 task 。

解决 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 时还是会挂起主线程一段时间(不是全程都挂起))。

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

Facebook 加载方案

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

与微信相同。

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

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

所有的依赖集为 Application、NodexSplashActivity 的间接依赖集即可。

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

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

美团加载方案

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 的要求非常高,因为第二个 dex 是等到需要的时候再去加载。重写Instrumentation 的 execStartActivity 方法,hook 跳转 Activity 的总入口做判断,如果当前第二个 dex 还没有加载完成,就弹一个 loading Activity等待加载完成。

综合加载方案

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

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

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,用户看到会一脸懵比的。
推荐插件: https://github.com/TangXiaoLv/Android-Easy-MultiDex

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:最终方案?
示例代码参考 :
https://github.com/yuyang272999712/NewFeaturesTest/blob/master/app/src/main/java/com/yuyang/fitsystemwindowstestdrawer/MyApplication.java

上一篇下一篇

猜你喜欢

热点阅读