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進階實踐技術;但願能幫助到你們,也節省你們在網上搜索資料的時間來學習,也能夠分享動態給身邊好友一塊兒學習!)

相關文章
相關標籤/搜索