Android Dex分包之旅

當(dāng)程序越來越大之后拐揭,出現(xiàn)了一個(gè) dex 包裝不下的情況堂污,通過 MultiDex 的方法解決了這個(gè)問題盟猖,但是在低端機(jī)器上又出現(xiàn)了 INSTALL_FAILED_DEXOPT 的情況式镐,那再解決這個(gè)問題吧娘汞。等解決完這個(gè)問題之后你弦,發(fā)現(xiàn)需要填的坑越來越多了禽作,文章講的是我在分包處理中填的坑彻磁,比如 65536衷蜓、LinearAlloc磁浇、NoClassDefFoundError等等置吓。

INSTALL_FAILED_DEXOPT

INSTALL_FAILED_DEXOPT 出現(xiàn)的原因大部分都是兩種衍锚,一種是 65536 了戴质,另外一種是 LinearAlloc 太小了告匠。兩者的限制不同后专,但是原因卻是相似戚哎,那就是App太大了,導(dǎo)致沒辦法安裝到手機(jī)上崭捍。

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 

編譯環(huán)境

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

根據(jù) StackOverFlow – Does the Android ART runtime have the same method limit limitations as Dalvik? 上面的說法,是因?yàn)?Dalvik 的 invoke-kind 指令集中橄浓,method reference index 只留了 16 bits荸实,最多能引用 65535 個(gè)方法准给。Dalvik bytecode :

Op & Format Mnemonic Mnemonic / Syntax Arguments
6e..72 invoke-kind {vC, vD, vE, vF, vG}, meth@BBBB6e: A: argument word count (4 bits)B:
35c invoke-virtual6f: invoke-super70: invoke-direct71: invoke-static72: invoke-interface method reference index (16 bits)C..G: argument registers (4 bits each)
  • 即使 dex 里面的引用方法數(shù)超過了 65536露氮,那也只有前面的 65536 得的到調(diào)用。所以這個(gè)不是 dex 的原因恨统。其次畜埋,既然和 dex 沒有關(guān)系悠鞍,那在打包 dex 的時(shí)候?yàn)槭裁磿?huì)報(bào)錯(cuò)狞玛。我們先定位 Too many 關(guān)鍵字心肪,定位到了 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 在這里做了判斷锅减,當(dāng)大于 65536 的時(shí)候就拋出異常了怔匣。所以在生成 dex 文件的過程中每瞒,當(dāng)調(diào)用方法數(shù)不能超過 65535 剿骨。那我們?cè)俑桓a浓利,發(fā)現(xiàn) MemberIdsSection 的一個(gè)子類叫 MethodidsSection :

public final class MethodIdsSection extends MemberIdsSection {}

回過頭來嫡秕,看一下 orderItems() 方法在哪里被調(diào)用了淘菩,跟到了 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 在哪里被調(diào)用潮改,查到了 UniformItemSection 父類 Section :

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

那現(xiàn)在再跟一下 prepare() 汇在,查到 DexFile 中有調(diào)用:

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() 吧糕殉,因?yàn)槭?private 的阿蝶,直接在類中找調(diào)用的地方就可以了:

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() 方法吧羡洁,最終發(fā)現(xiàn)在 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;
    }
    //調(diào)用writeDex的地方
    private static int runMonoDex() throws IOException {
        //blablabla......
        outArray = writeDex(outputDex);
        //blablabla......
    }
    //調(diào)用runMonoDex的地方
    public static int run(Arguments arguments) throws IOException {
        if (args.multiDex) {
            return runMultiDex();
        } else {
            return runMonoDex();
        }
    }
}

args.multiDex 就是是否分包的參數(shù)辛蚊,那么問題找著了袋马,如果不選擇分包的情況下虑凛,引用方法數(shù)超過了 65536 的話就會(huì)拋出異常卧檐。

同樣分析第二種情況焰宣,根據(jù)錯(cuò)誤信息可以具體定位到代碼匕积,但是很奇怪的是 DexMerger 闪唆,我們沒有設(shè)置分包參數(shù)或者其他參數(shù)悄蕾,為什么會(huì)有 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 枣抱,是一個(gè) List 集合佳晶,那么我們看一下這個(gè)集合在哪里添加數(shù)據(jù)的:

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...
    }
    //調(diào)用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...
    }
    //調(diào)用FileBytesConsumer的地方
    private static void processOne(String pathname, FileNameFilter filter) {
        ClassPathOpener opener;

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

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

}

跟了一圈又跟回來了垂攘,但是注意一個(gè)變量:fileNames[i]晒他,傳進(jìn)去這個(gè)變量陨仅,是個(gè)地址灼伤,最終在 processFileBytes 中處理后添加到 libraryDexBuffers 中狐赡,那跟一下這個(gè)變量:

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 && !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);
        }
    }
}

跟到這里發(fā)現(xiàn)是傳進(jìn)來的參數(shù),那我們?cè)倏纯?gradle 里面?zhèn)鞯氖鞘裁磪?shù)吧享郊,查看 Dex task :

public class Dex extends BaseTask {
    @InputFiles
    Collection<File> libraries
}
我們把這個(gè)參數(shù)打印出來:

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

打印出來發(fā)現(xiàn)是 build/intermediates/pre-dexed/ 目錄里面的 jar 文件炊琉,再把 jar 文件解壓發(fā)現(xiàn)里面就是 dex 文件了苔咪。所以 DexMerger 的工作就是合并這里的 dex 悼泌。

更改編譯環(huán)境

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

將 gradle 設(shè)置為 2.1.0-alpha3 之后馆里,在項(xiàng)目的 build.gradle 中即使沒有設(shè)置 multiDexEnabled true 也能夠編譯通過鸠踪,但是生成的 apk 包依舊是兩個(gè) dex 营密,我想的是可能為了設(shè)置 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= 用于控制每一個(gè) dex 的最大方法個(gè)數(shù)病往。

這個(gè)參數(shù)在查看 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...

更多細(xì)節(jié)可以查看源碼:Github – platform_dalvik/Main

FB 的工程師們?cè)?jīng)還想到過直接修改 LinearAlloc 的大小停巷,比如從 5M 修改到 8M: Under the Hood: Dalvik patch for Facebook for Android 叠穆。

dexopt && dex2oat

image.png

dexopt

當(dāng) Android 系統(tǒng)安裝一個(gè)應(yīng)用的時(shí)候,有一步是對(duì) Dex 進(jìn)行優(yōu)化示损,這個(gè)過程有一個(gè)專門的工具來處理检访,叫 DexOpt脆贵。DexOpt 是在第一次加載 Dex 文件的時(shí)候執(zhí)行的卖氨,將 dex 的依賴庫文件和一些輔助數(shù)據(jù)打包成 odex 文件筒捺,即 Optimised Dex系吭,存放在 cache/dalvik_cache 目錄下。保存格式為 apk路徑 @ apk名 @ classes.dex 躯枢。執(zhí)行 ODEX 的效率會(huì)比直接執(zhí)行 Dex 文件的效率要高很多闺金。

dex2oat

Android Runtime 的 dex2oat 是將 dex 文件編譯成 oat 文件败匹。而 oat 文件是 elf 文件掀亩,是可以在本地執(zhí)行的文件槽棍,而 Android Runtime 替換掉了虛擬機(jī)讀取的字節(jié)碼轉(zhuǎn)而用本地可執(zhí)行代碼炼七,這就被叫做 AOT(ahead-of-time)豌拙。dex2oat 對(duì)所有 apk 進(jìn)行編譯并保存在 dalvik-cache 目錄里按傅。PackageManagerService 會(huì)持續(xù)掃描安裝目錄唯绍,如果有新的 App 安裝則馬上調(diào)用 dex2oat 進(jìn)行編譯枝誊。

NoClassDefFoundError

現(xiàn)在 INSTALL_FAILED_DEXOPT 問題是解決了叶撒,但是有時(shí)候編譯完運(yùn)行的時(shí)候一打開 App 就 crash 了痊乾,查看 log 發(fā)現(xiàn)是某個(gè)類找不到引用哪审。

  • Build Tool 是如何分包的
    為什么會(huì)這樣呢?是因?yàn)?build-tool 在分包的時(shí)候只判斷了直接引用類舌狗。什么是直接引用類呢痛侍?舉個(gè)栗子:
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 三個(gè)類绘闷,其中 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 了。同理還要單例模式中拿到單例之后直接調(diào)用某個(gè)方法返回的是另外一個(gè)對(duì)象秉剑,并非單例對(duì)象侦鹏。

build tool 的分包操作可以查看 sdk 中 build-tools 文件夾下的 mainDexClasses 腳本略水,同時(shí)還發(fā)現(xiàn)了 mainDexClasses.rules 文件渊涝,該文件是主 dex 的匹配規(guī)則跨释。該腳本要求輸入一個(gè)文件組(包含編譯后的目錄或jar包)鳖谈,然后分析文件組中的類并寫入到–output所指定的文件中缆娃。實(shí)現(xiàn)原理也不復(fù)雜,主要分為三步:

  • 環(huán)境檢查疙驾,包括傳入?yún)?shù)合法性檢查它碎,路徑檢查以及proguard環(huán)境檢測(cè)等扳肛。
  • 使用mainDexClasses.rules規(guī)則挖息,通過Proguard的shrink功能套腹,裁剪無關(guān)類电禀,生成一個(gè)tmp.jar包尖飞。
  • 通過生成的tmp jar包政基,調(diào)用MainDexListBuilder類生成主dex的文件列表

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

在項(xiàng)目中沮明,可以直接運(yùn)行 gradle 的 task 荐健。

  • collect{flavor}{buildType}MultiDexComponents Task 。這個(gè) task 是獲取 AndroidManifest.xml 中 Application 圣贸、Activity 吁峻、Service 用含、 Receiver 啄骇、 Provider 等相關(guān)類缸夹,以及 Annotation 虽惭,之后將內(nèi)容寫到 build/intermediates/multi-dex/{flavor}/{buildType}/maindexlist.txt 文件中去芽唇。

  • packageAll{flavor}DebugClassesForMultiDex Task 匆笤。該 task 是將所有類打包成 jar 文件存在 build/intermediates/multi-dex/{flavor}/debug/allclasses.jar 炮捧。 當(dāng) BuildType 為 Release 的時(shí)候寓盗,執(zhí)行的是 proguard{flavor}Release Task,該 task 將 proguard 混淆后的類打包成 jar 文件存在 build/intermediates/classes-proguard/{flavor}/release/classes.jar

  • shrink{flavor}{buildType}MultiDexComponents Task 蘸吓。該 task 會(huì)根據(jù) maindexlist.txt 生成 componentClasses.jar 库继,該 jar 包里面就只有 maindexlist.txt 里面的類,該 jar 包的位置在 build/intermediates/multi-dex/{flavor}/{buildType}/componentClasses.jar

  • create{flavor}{buildType}MainDexClassList Task 艺谆。該 task 會(huì)根據(jù)生成的 componentClasses.jar 去找這里面的所有的 class 中直接依賴的 class 静汤,然后將內(nèi)容寫到 build/intermediates/multi-dex/{flavor}/{buildType}/maindexlist.txt 中虫给。最終這個(gè)文件里面列出來的類都會(huì)被分配到第一個(gè) 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= 參數(shù)是一個(gè)類列表的文件药蜻,在該文件中的類會(huì)被打包在第一個(gè) dex 中语泽。

multidex.keep 里面列上需要打包到第一個(gè) dex 的 class 文件湿弦,注意颊埃,如果需要混淆的話需要寫混淆之后的 class 班利。

Application Not Responding

因?yàn)榈谝淮芜\(yùn)行(包括清除數(shù)據(jù)之后)的時(shí)候需要 dexopt 罗标,然而 dexopt 是一個(gè)比較耗時(shí)的操作闯割,同時(shí) MultiDex.install() 操作是在 Application.attachBaseContext() 中進(jìn)行的宙拉,占用的是UI線程谢澈。那么問題來了,當(dāng)我的第二個(gè)包牛郑、第三個(gè)包很大的時(shí)候淹朋,程序就阻塞在 MultiDex.install() 這個(gè)地方了瑞你,一旦超過規(guī)定時(shí)間者甲,那就 ANR 了虏缸。那怎么辦刽辙?放子線程宰缤?如果 Application 有一些初始化操作慨灭,到初始化操作的地方的時(shí)候都還沒有完成 install + dexopt 的話氧骤,那不是又 NoClassDefFoundError 了嗎筹陵?同時(shí) ClassLoader 放在哪個(gè)線程都讓主線程掛起镊尺。好了庐氮,那在 multidex.keep 的加上相關(guān)的所有的類吧旭愧。好像這樣成了输枯,但是第一個(gè) dex 又大起來了桃熄,而且如果用戶操作快瞳收,還沒完成 install + dexopt 但是已經(jīng)把 App 所以界面都打開了一遍螟深。界弧。垢箕。雖然這不現(xiàn)實(shí)条获。帅掘。

微信加載方案

首次加載在地球中頁中, 并用線程去加載(但是 5.0 之前加載 dex 時(shí)還是會(huì)掛起主線程一段時(shí)間(不是全程都掛起))锄开。

  • dex 形式
    微信是將包放在 assets 目錄下的萍悴,在加載 Dex 的代碼時(shí)癣诱,實(shí)際上傳進(jìn)去的是 zip撕予,在加載前需要驗(yàn)證 MD5实抡,確保所加載的 Dex 沒有被篡改。

  • dex 類分包規(guī)則
    分包規(guī)則即將所有 Application赏淌、ContentProvider 以及所有 export 的 Activity六水、Service 掷贾、Receiver 的間接依賴集都必須放在主 dex想帅。

  • 加載 dex 的方式
    加載邏輯這邊主要判斷是否已經(jīng) dexopt博脑,若已經(jīng) dexopt叉趣,即放在 attachBaseContext 加載疗杉,反之放于地球中用線程加載烟具。怎么判斷朝聋?因?yàn)樵谖⑿胖屑胶郏襞袛?revision 改變,即將 dex 以及 dexopt 目錄清空僻他。只需簡單判斷兩個(gè)目錄 dex 名稱吨拗、數(shù)量是否與配置文件的一致哨鸭。

總的來說兔跌,這種方案用戶體驗(yàn)較好,缺點(diǎn)在于太過復(fù)雜华望,每次都需重新掃描依賴集赖舟,而且使用的是比較大的間接依賴集宾抓。

Facebook 加載方案

Facebook的思路是將 MultiDex.install() 操作放在另外一個(gè)經(jīng)常進(jìn)行的幢泼。

  • dex 形式

    與微信相同讲衫。

  • dex 類分包規(guī)則

    Facebook 將加載 dex 的邏輯單獨(dú)放于一個(gè)單獨(dú)的 nodex 進(jìn)程中招驴。

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

所有的依賴集為 Application别厘、NodexSplashActivity 的間接依賴集即可触趴。

  • 加載 dex 的方式

    因?yàn)?NodexSplashActivity 的 intent-filter 指定為 Main 和LAUNCHER 雕蔽,所以一打開 App 首先拉起 nodex 進(jìn)程批狐,然后打開 NodexSplashActivity 進(jìn)行 MultiDex.install() 嚣艇。如果已經(jīng)進(jìn)行了 dexpot 操作的話就直接跳轉(zhuǎn)主界面食零,沒有的話就等待 dexpot 操作完成再跳轉(zhuǎn)主界面。

這種方式好處在于依賴集非常簡單娜搂,同時(shí)首次加載 dex 時(shí)也不會(huì)卡死百宇。但是它的缺點(diǎn)也很明顯携御,即每次啟動(dòng)主進(jìn)程時(shí)啄刹,都需先啟動(dòng) nodex 進(jìn)程誓军。盡管 nodex 進(jìn)程邏輯非常簡單谭企,這也需100ms以上债查。

美團(tuán)加載方案

  • dex 形式
    在 gradle 生成 dex 文件的這步中盹廷,自定義一個(gè) task 來干預(yù) dex 的生產(chǎn)過程俄占,從而產(chǎn)生多個(gè) dex 缸榄。
tasks.whenTaskAdded { task ->
   if (task.name.startsWith('proguard') && (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') && (task.name.endsWith('Debug') || task.name.endsWith('Release'))) {
       task.doFirst {
           ensureMultiDexInApk();
       }
   }
} 
  • dex 類分包規(guī)則
    把 Service甚带、Receiver鹰贵、Provider 涉及到的代碼都放到主 dex 中碉输,而把 Activity 涉及到的代碼進(jìn)行了一定的拆分敷钾,把首頁 Activity闰非、Laucher Activity 财松、歡迎頁的 Activity 辆毡、城市列表頁 Activity 等所依賴的 class 放到了主 dex 中舶掖,把二級(jí)眨攘、三級(jí)頁面的 Activity 以及業(yè)務(wù)頻道的代碼放到了第二個(gè) dex 中鲫售,為了減少人工分析 class 的依賴所帶了的不可維護(hù)性和高風(fēng)險(xiǎn)性情竹,美團(tuán)編寫了一個(gè)能夠自動(dòng)分析 class 依賴的腳本秦效, 從而能夠保證主 dex 包含 class 以及他們所依賴的所有 class 都在其內(nèi)阱州,這樣這個(gè)腳本就會(huì)在打包之前自動(dòng)分析出啟動(dòng)到主 dex 所涉及的所有代碼苔货,保證主 dex 運(yùn)行正常阱冶。
  • 加載 dex 的方式
    通過分析 Activity 的啟動(dòng)過程木蹬,發(fā)現(xiàn) Activity 是由 ActivityThread 通過 Instrumentation 來啟動(dòng)的镊叁,那么是否可以在 Instrumentation 中做一定的手腳呢晦譬?通過分析代碼 ActivityThread 和 Instrumentation 發(fā)現(xiàn)敛腌,Instrumentation 有關(guān) Activity 啟動(dòng)相關(guān)的方法大概有:execStartActivity像樊、 newActivity 等等生棍,這樣就可以在這些方法中添加代碼邏輯進(jìn)行判斷這個(gè) class 是否加載了涂滴,如果加載則直接啟動(dòng)這個(gè) Activity柔纵,如果沒有加載完成則啟動(dòng)一個(gè)等待的 Activity 顯示給用戶首量,然后在這個(gè) Activity 中等待后臺(tái)第二個(gè) dex 加載完成加缘,完成后自動(dòng)跳轉(zhuǎn)到用戶實(shí)際要跳轉(zhuǎn)的 Activity拣宏;這樣在代碼充分解耦合勋乾,以及每個(gè)業(yè)務(wù)代碼能夠做到顆粒化的前提下学歧,就做到第二個(gè) dex 的按需加載了枝笨。

美團(tuán)的這種方式對(duì)主 dex 的要求非常高横浑,因?yàn)榈诙€(gè) dex 是等到需要的時(shí)候再去加載徙融。重寫Instrumentation 的 execStartActivity 方法欺冀,hook 跳轉(zhuǎn) Activity 的總?cè)肟谧雠袛嘟呕绻?dāng)前第二個(gè) dex 還沒有加載完成,就彈一個(gè) loading Activity等待加載完成蛛芥。

綜合加載方案

微信的方案需要將 dex 放于 assets 目錄下仅淑,在打包的時(shí)候太過負(fù)責(zé)涯竟;Facebook 的方案每次進(jìn)入都是開啟一個(gè) nodex 進(jìn)程庐船,而我們希望節(jié)省資源的同時(shí)快速打開 App筐钟;美團(tuán)的方案確實(shí)很 hack篓冲,但是對(duì)于項(xiàng)目已經(jīng)很龐大壹将,耦合度又比較高的情況下并不適合诽俯。所以這里嘗試結(jié)合三個(gè)方案惊畏,針對(duì)自己的項(xiàng)目來進(jìn)行優(yōu)化颜启。

  • dex 形式
    第一缰盏,為了能夠繼續(xù)支持 Android 2.x 的機(jī)型口猜,我們將每個(gè)包的方法數(shù)控制在 48000 個(gè),這樣最后分出來 dex 包大約在 5M 左右川抡;第二崖堤,為了防止 NoClassDefFoundError 的情況密幔,我們找出來啟動(dòng)頁胯甩、引導(dǎo)頁、首頁比較在意的一些類缰雇,比如 Fragment 等(因?yàn)樵谏?maindexlist.txt 的時(shí)候只會(huì)找 Activity 的直接引用,比如首頁 Activity 直接引用 AFragemnt愚战,但是 AFragment 的引用并沒有去找)。

  • dex 類分包規(guī)則
    第一個(gè)包放 Application梗摇、Android四大組件以及啟動(dòng)頁伶授、引導(dǎo)頁糜烹、首頁的直接引用的 Fragment 的引用類疮蹦,還放了推送消息過來點(diǎn)擊 Notification 之后要展示的 Activity 中的 Fragment 的引用類愕乎。
    Fragment 的引用類是寫了一個(gè)腳本感论,輸入需要找的類然后將這些引用類寫到 multidex.keep 文件中紊册,如果是 debug 的就直接在生成的 jar 里面找囊陡,如果是 release 的話就通過 mapping.txt 找,找不到的話再去 jar 里面找,所以在 gradle 打包的過程中我們?nèi)藶楦蓴_一下:

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

詳細(xì)代碼可見:Github — PhotoNoter/gradle

  • 加載 dex 的方式
    在防止 ANR 方面痢畜,我們采用了 Facebook 的思路丁稀。但是稍微有一點(diǎn)區(qū)別线衫,差別在于我們并不在一開啟 App 的時(shí)候就去起進(jìn)程授账,而是一開啟 App 的時(shí)候在主進(jìn)程里面判斷是否 dexopt 過沒,沒有的話再去起另外的進(jìn)程的 Activity 專門做 dexopt 操作 敛助。一旦拉起了去做 dexopt 的進(jìn)程纳击,那么讓主進(jìn)程進(jìn)入一個(gè)死循環(huán)焕数,一直等到 dexopt 進(jìn)程結(jié)束再結(jié)束死循環(huán)往下走刨啸。那么問題來了加匈,第一雕拼,主進(jìn)程進(jìn)入死循環(huán)會(huì) ANR 嗎啥寇?第二辑甜,如何判斷是否 dexopt 過磷醋;第三邓线,為了界面友好骇陈,dexopt 的進(jìn)程該怎么做你雌;第四婿崭,主進(jìn)程怎么知道 dexopt 進(jìn)程結(jié)束了逛球,也就是怎么去做進(jìn)程間通信颤绕。

  • 一個(gè)一個(gè)問題的解決奥务,先第一個(gè):因?yàn)楫?dāng)拉起 dexopt 進(jìn)程之后挡篓,我們?cè)?dexopt 進(jìn)程的 Activity 中進(jìn)行 MultiDex.install() 操作帚称,此時(shí)主進(jìn)程不再是前臺(tái)進(jìn)程了官研,所以不會(huì) ANR 。

  • 第二個(gè)問題:因?yàn)榈谝淮螁?dòng)是什么數(shù)據(jù)都沒有的闯睹,那么我們就建立一個(gè) SharedPreference 戏羽,啟動(dòng)的時(shí)候先去從這里獲取數(shù)據(jù),如果沒有數(shù)據(jù)那么也就是沒有 dexopt 過楼吃,如果有數(shù)據(jù)那么肯定是 dexopt 過的始花,但是這個(gè) SharedPreference 我們得保證我們的程序只有這個(gè)地方可以修改,其他地方不能修改孩锡。

  • 第三個(gè)問題:因?yàn)?App 的啟動(dòng)也是一張圖片浇垦,所以在 dexopt 的 Activity 的 layout 中,我們就把這張圖片設(shè)置上去就好了仍劈,當(dāng)關(guān)閉 dexopt 的 Activity 的時(shí)候讹弯,我們得關(guān)閉 Activity 的動(dòng)畫。同時(shí)為了不讓 dexopt 進(jìn)程發(fā)生 ANR 莫其,我們將 MultiDex.install() 過程放在了子線程中進(jìn)行。

  • 第四個(gè)問題:Linux 的進(jìn)程間通信的方式有很多,Android 中還有 Binder 等爽彤,那么我們這里采用哪種方式比較好呢蹬跃?首先想到的是既然 dexopt 進(jìn)程結(jié)束了自然在主進(jìn)程的死循環(huán)中去判斷 dexopt 進(jìn)程是否存在丹喻。但是在實(shí)際操作中發(fā)現(xiàn)柄慰,dexopt 雖然已經(jīng)退出了,但是進(jìn)程并沒有馬上被回收掉蠢挡,所以這個(gè)方法走不通。那么用 Broadcast 廣播可以嗎腹尖?可是可以,但是增加了 Application 的負(fù)擔(dān)断凶,在拉起 dexopt 進(jìn)程前還得注冊(cè)一個(gè)動(dòng)態(tài)廣播介汹,接收到廣播之后還得注銷掉,所以這個(gè)也沒有采用撼港。那么最終采用的方式是判斷文件是否存在蒙揣,在拉起 dexopt 進(jìn)程前在某個(gè)安全的地方建立一個(gè)臨時(shí)文件,然后死循環(huán)判斷這個(gè)文件是否存在瓷炮,在 dexopt 進(jìn)程結(jié)束的時(shí)候刪除這個(gè)臨時(shí)文件,那么在主進(jìn)程的死循環(huán)中發(fā)現(xiàn)此文件不存在了土榴,就直接跳出循環(huán),繼續(xù) Application 初始化操作糯笙。

public class NoteApplication extends Application {
@Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        //開啟dex進(jìn)程的話也會(huì)進(jìn)入application
        if (isDexProcess()) {
            return;
        }
        doInstallBeforeLollipop();
        MultiDex.install(this);
    }

    @Override
    public void onCreate() {
        super.onCreate();
        if (isDexProcess()) {
            return;
        }
      //其他初始化
    }
    
  private void doInstallBeforeLollipop() {
        //滿足3個(gè)條件够庙,1.第一次安裝開啟剔难,2.主進(jìn)程衫嵌,3.API<21(因?yàn)?1之后ART的速度比dalvik快接近10倍(畢竟5.0之后的手機(jī)性能也要好很多))
        if (isAppFirstInstall() && !isDexProcessOrOtherProcesses() && 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();
            }
        }
    }

詳細(xì)代碼可見:Github — PhotoNoter/NoteApplication

總的來說桦锄,這種方式好處在于依賴集非常簡單,同時(shí)它的集成方式也是非常簡單碍粥,我們無須去修改與加載無關(guān)的代碼枕面。但是當(dāng)沒有啟動(dòng)過 App 的時(shí)候枕荞,被推送全家桶喚醒或者收到了廣播,雖然這里都是沒有界面的過程,但是運(yùn)用了這種加載方式的話會(huì)彈出 dexopt 進(jìn)程的 Activity,用戶看到會(huì)一臉懵比的虱而。
推薦插件: 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 里面會(huì)有 Application、Activity、Service少梁、Receiver著洼、Provider葵陵、Instrumentation绊困、BackupAgent 和 Annotation取视。當(dāng)這些類以及直接引用類比較多的時(shí)候折欠,都要塞進(jìn)主 dex 那先,就引發(fā)了 main dex capacity exceeded build error 。

為了解決這個(gè)問題,當(dāng)執(zhí)行 Create{flavor}{buildType}ManifestKeepList task 之前將其中的 activity 去掉,之后會(huì)發(fā)現(xiàn) /build/intermediates/multi_dex/{flavor}/{buildType}/manifest_keep.txt 文件中已經(jīng)沒有 Activity 相關(guān)的類了。

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()
詳細(xì)可以看 CreateManifestKeepList 的源碼:Github – CreateManifestKeepList

Too many classes in –main-dex-list
沒錯(cuò)狡恬,還是 Too many classes in –main-dex-list 的錯(cuò)誤兔乞。在美團(tuán)的自動(dòng)拆包中講到:

實(shí)際應(yīng)用中我們還遇到另外一個(gè)比較棘手的問題锚国, 就是Field的過多的問題煎楣,F(xiàn)ield過多是由我們目前采用的代碼組織結(jié)構(gòu)引入的,我們?yōu)榱朔奖愣鄻I(yè)務(wù)線谦去、多團(tuán)隊(duì)并發(fā)協(xié)作的情況下開發(fā)妆丘,我們采用的aar的方式進(jìn)行開發(fā)宣脉,并同時(shí)在aar依賴鏈的最底層引入了一個(gè)通用業(yè)務(wù)aar,而這個(gè)通用業(yè)務(wù)aar中包含了很多資源,而ADT14以及更高的版本中對(duì)Library資源處理時(shí),Library的R資源不再是static final的了,詳情請(qǐng)查看google官方說明,這樣在最終打包時(shí)Library中的R沒法做到內(nèi)聯(lián),這樣帶來了R field過多的情況,導(dǎo)致需要拆分多個(gè)Secondary DEX,為了解決這個(gè)問題我們采用的是在打包過程中利用腳本把Libray中R field(例如ID、Layout、Drawable等)的引用替換成常量,然后刪去Library中R.class中的相應(yīng)Field。

同樣,hu關(guān)于這個(gè)問題可以參考這篇大神的文章:當(dāng)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 分包之旅技術(shù)分享疑難解答

Q1:Facebook mutidex 方案為何要多起一個(gè)進(jìn)程互站,如果采用單進(jìn)程 線程去處理呢标捺?
答:install能不能放到線程里做闺兢?如果開新線程加載悔耘,而主線程繼續(xù)Application初始化—-——導(dǎo)致如果異步化看峻,multidex安裝沒有結(jié)束意味著dex還沒加載進(jìn)來冯勉,這時(shí)候如果進(jìn)程需要seconday.dex里的classes信息不就悲劇了—-某些類強(qiáng)行使用就會(huì)報(bào)NoClassDefFoundError.
FaceBook多dex分包方案
安裝完成之后第一次啟動(dòng)時(shí),是secondary.dex的dexopt花費(fèi)了更多的時(shí)間,認(rèn)識(shí)到這點(diǎn)非常重要轩猩,使得問題轉(zhuǎn)化為:在不阻塞UI線程的前提下彤委,完成dexopt,以后都不需要再次dexopt,所以可以在UI線程install dex了
我們現(xiàn)在想做到的是:既希望在Application的attachContext()方法里同步加載secondary.dex,又不希望卡住UI線程
FB的方案就是:
讓Launcher Activity在另外一個(gè)進(jìn)程啟動(dòng)拯欧,但是Multidex.install還是在Main Process中開啟该贾,雖然邏輯上已經(jīng)不承擔(dān)dexopt的任務(wù)
這個(gè)Launcher Activity就是用來異步觸發(fā)dexopt的 ,load完成就啟動(dòng)Main Activity;如果已經(jīng)loaded揩抡,則直接啟動(dòng)Main Process
Multidex.install所引發(fā)的合并耗時(shí)操作刃泌,是在前臺(tái)進(jìn)程的異步任務(wù)中執(zhí)行的,所以沒有anr的風(fēng)險(xiǎn)

Q2:當(dāng)沒有啟動(dòng)過 App 的時(shí)候,被推送全家桶喚醒或者收到了廣播(App已經(jīng)處于不是第一次啟動(dòng)過)
會(huì)喚醒除抛,而且會(huì)出現(xiàn)dexopt的獨(dú)立進(jìn)程頁面activity护蝶,一閃而過用戶會(huì)懵逼...
改進(jìn)采用新的思路會(huì)喚起新進(jìn)程堤魁,但是該進(jìn)程只會(huì)觸發(fā)一次...
如何保證只觸發(fā)一次赏表? 我們先判斷是否第一次安裝啟動(dòng)應(yīng)用攻泼,當(dāng)應(yīng)用不是第一次安裝啟動(dòng)時(shí)骡男,我們直接啟動(dòng)閃屏頁,并且結(jié)束掉子進(jìn)程即可。

Q3:處于第一次安裝成功之后传趾,app收到推送全家桶是否會(huì)被喚醒榕订?
不會(huì),因?yàn)樾枰状卧赼pplication執(zhí)行過一次推送的init代碼才會(huì)被喚醒
Q4:最終方案?
示例代碼參考 :

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末舶替,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌吼虎,老刑警劉巖洒疚,帶你破解...
    沈念sama閱讀 218,755評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件喊括,死亡現(xiàn)場(chǎng)離奇詭異劫拢,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人盲厌,你說我怎么就攤上這事岁钓。” “怎么了?”我有些...
    開封第一講書人閱讀 165,138評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我芬为,道長,這世上最難降的妖魔是什么蜗帜? 我笑而不...
    開封第一講書人閱讀 58,791評(píng)論 1 295
  • 正文 為了忘掉前任窥妇,我火速辦了婚禮展氓,結(jié)果婚禮上族檬,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好钉蒲,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪灶轰。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,631評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音沃粗,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛娃磺,可吹牛的內(nèi)容都是我干的逼庞。 我是一名探鬼主播司倚,決...
    沈念sama閱讀 40,362評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起耸彪,我...
    開封第一講書人閱讀 39,264評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤纸镊,失蹤者是張志新(化名)和其女友劉穎罐呼,沒想到半個(gè)月后呐萌,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體茫叭,經(jīng)...
    沈念sama閱讀 45,724評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了弹囚。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片灸异。...
    茶點(diǎn)故事閱讀 40,040評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡窜骄,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出夭坪,到底是詐尸還是另有隱情讼撒,我是刑警寧澤钳幅,帶...
    沈念sama閱讀 35,742評(píng)論 5 346
  • 正文 年R本政府宣布牡属,位于F島的核電站措伐,受9級(jí)特大地震影響矗蕊,放射性物質(zhì)發(fā)生泄漏扇雕。R本人自食惡果不足惜玻侥,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評(píng)論 3 330
  • 文/蒙蒙 一决摧、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧凑兰,春花似錦掌桩、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至矢门,卻和暖如春盆色,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背祟剔。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評(píng)論 1 270
  • 我被黑心中介騙來泰國打工隔躲, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人物延。 一個(gè)月前我還...
    沈念sama閱讀 48,247評(píng)論 3 371
  • 正文 我出身青樓宣旱,卻偏偏與公主長得像,于是被迫代替她去往敵國和親叛薯。 傳聞我的和親對(duì)象是個(gè)殘疾皇子浑吟,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • 前言 最近開發(fā)中我們發(fā)現(xiàn),我們的產(chǎn)品在Android設(shè)備版本低于5.0以下第一次安裝啟動(dòng)會(huì)出現(xiàn)黑屏耗溜、ANR等情況组力。...
    miraclehen閱讀 3,559評(píng)論 2 11
  • 為什么要分包? 1抖拴、65536問題 導(dǎo)致因素隨著項(xiàng)目apk的龐大以及加入更多的第三方庫燎字,app的方法數(shù)已經(jīng)超過了6...
    會(huì)撒嬌的犀犀利閱讀 2,324評(píng)論 1 15
  • 為什么需要對(duì)Dex進(jìn)行分包 Android在安裝應(yīng)用的過程中,系統(tǒng)會(huì)運(yùn)行一個(gè)名為DexOpt的程序?yàn)樵搼?yīng)用在當(dāng)前機(jī)...
    Boreas_su閱讀 4,289評(píng)論 0 9
  • Tinker 熱補(bǔ)丁接入過程中的坑0⒄:蜓堋! =============== Tinker 介紹 官方接入說明 gra...
    朱立志閱讀 2,108評(píng)論 0 2
  • 最近項(xiàng)目apk方法數(shù)即將達(dá)到65536上限洒放,雖然通過瘦身減少了一些方法數(shù)蛉鹿,但是隨著更多sdk的接入,終究還是避免不...
    the_q閱讀 16,472評(píng)論 6 39