MultiDex源碼解析

1.產(chǎn)生背景

65535問題是一個(gè)應(yīng)用開發(fā)到一定階段后必定會(huì)遇到的一個(gè)問題匾荆,主要是因?yàn)樵陂_始設(shè)計(jì)的Dex文件格式中將method的引用限制為short進(jìn)行存儲(chǔ),導(dǎo)致超過數(shù)目后編譯失敗杆烁,后來google推出了一個(gè)MultiDex來解決這一個(gè)問題牙丽。

2.源碼分析

2.1MultiApplication

這個(gè)類是需要我們?nèi)ダ^承的,當(dāng)然也可以不用繼承兔魂,我們只需要實(shí)現(xiàn)以下的方法就行

MultiDex.install(this);
2.2MultiDex

首先這個(gè)類的static靜態(tài)塊中初始化了一些數(shù)據(jù)剩岳,

static {   
  SECONDARY_FOLDER_NAME = "code_cache" + File.separator + "secondary-dexes"; 
  installedApk = new HashSet();
  IS_VM_MULTIDEX_CAPABLE = isVMMultidexCapable(System.getProperty("java.vm.version"));
}

第一個(gè)參數(shù)是分dex的文件存放路徑,第二個(gè)是一個(gè)hashset入热,第三個(gè)調(diào)用的一個(gè)判斷是否multidex已經(jīng)支持的一個(gè)方法拍棕,傳入的參數(shù)則是虛擬機(jī)的版本信息晓铆。

Matcher matcher = Pattern.compile("(\\d+)\\.(\\d+)(\\.\\d+)?").matcher(versionString);
if(matcher.matches()) {
...
int e = Integer.parseInt(matcher.group(1));
int minor = Integer.parseInt(matcher.group(2));
isMultidexCapable = e > 2 || e == 2 && minor >= 1;
...

如上所示,是通過一個(gè)正則來進(jìn)行判斷的绰播,根據(jù)對(duì)多個(gè)手機(jī)版本的測試骄噪,在4.4.4的機(jī)型上版本為1.6.0,在5.1和6.0的機(jī)型上均為2.1.0蠢箩,推斷在5.0以下的機(jī)型返回false链蕊,5.0及以上的返回true。
接下來就是install部分的代碼了谬泌,在貼代碼前先提出幾個(gè)問題滔韵,app在安裝中做了什么事情,安裝后存放的路徑在哪
install時(shí)分為幾個(gè)步驟

3.1判斷是否進(jìn)入MultiDex主流程
if(IS_VM_MULTIDEX_CAPABLE) {  
  Log.i("MultiDex", "VM has multidex support, MultiDex support library is disabled.");
} else if(VERSION.SDK_INT < 4) {  
  throw new RuntimeException("Multi dex installation failed. SDK " + VERSION.SDK_INT + " is unsupported. Min SDK version is " + 4 + ".");
} else {

這里調(diào)用的就是上面的虛擬機(jī)版本并且不支持小于4的情況

3.2判斷是否安裝過
Set var2 = installedApk;
    synchronized(installedApk) {  
    String apkPath = e.sourceDir; 
    if(installedApk.contains(apkPath)) {      
        return; 
    }
    installedApk.add(apkPath);

如果安裝過就直接返回掌实,這里installedAPK是靜態(tài)塊中直接初始化的陪蜻,默認(rèn)就是空,而且沒有賦值的地方贱鼻,肯定會(huì)跳過這個(gè)過程宴卖,所以這里還不了解實(shí)際的意義是什么。

3.3清除老的插件列表

清除老的Dex文件只是為了防止重新加載邻悬,這里只是傳入了一個(gè)dex的文件目錄然后進(jìn)行遞歸刪除文件症昏,最后刪除整個(gè)文件夾,代碼比較簡單就不貼出來了父丰。

3.4MultiDex文件提取
File dexDir = new File(e.dataDir, SECONDARY_FOLDER_NAME);
List files = MultiDexExtractor.load(context, e, dexDir, false);

這里首先拼接一個(gè)完整的dex的路徑
dataDir是安裝后存放數(shù)據(jù)的地方 也就是data/data/packageName
SECONDARY_FOLDER_NAME則是安裝完dex存在的地方肝谭,拼接出來的完整路徑dexDir就是
data/data/packageName/code_cache/secondary-dexes
然后將這個(gè)路徑以及applicationInfo,是否強(qiáng)制重新加載傳遞給MultiDexExtractor蛾扇,這個(gè)是提取dex的核心代碼

static List<File> load(Context context, ApplicationInfo applicationInfo, File dexDir, boolean forceReload) throws IOException {
File sourceApk = new File(applicationInfo.sourceDir);
    long currentCrc = getZipCrc(sourceApk);
    List files;
    if(!forceReload && !isModified(context, sourceApk, currentCrc)) {
        try {
            files = loadExistingExtractions(context, sourceApk, dexDir);
        } catch (IOException var9) {
            files = performExtractions(sourceApk, dexDir);
            putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1);
        }
    } else {
        files = performExtractions(sourceApk, dexDir);
        putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1);
    }
    return files;
}

逐行分析攘烛,isModified根據(jù)CRC校驗(yàn)apk是否和安裝時(shí)的一樣,forceReload默認(rèn)傳進(jìn)來為false屁桑,當(dāng)在加載失敗的時(shí)候會(huì)走到MultiDex的catch方法中医寿,然后傳入進(jìn)來的就是true,一般就是不重新提取栏赴,所以是直接走到loadExistingExtractions方法

private static List<File> loadExistingExtractions(Context context, File sourceApk, File dexDir) throws IOException {
    String extractedFilePrefix = sourceApk.getName() + ".classes";
    int totalDexNumber = getMultiDexPreferences(context).getInt("dex.number", 1);
    ArrayList files = new ArrayList(totalDexNumber);
    for(int secondaryNumber = 2; secondaryNumber <= totalDexNumber; ++secondaryNumber) {
        String fileName = extractedFilePrefix + secondaryNumber + ".zip";
        File extractedFile = new File(dexDir, fileName);
        if(!extractedFile.isFile()) {
            throw new IOException("Missing extracted secondary dex file \'" + extractedFile.getPath() + "\'");
        }
        files.add(extractedFile);
        if(!verifyZipFile(extractedFile)) {
            throw new IOException("Invalid ZIP file.");
        }
    }
    return files;
}

sourceApk是外部傳進(jìn)來的蘑斧,初始值是applicationInfo的sourceDir,getName后得到的就是apkName.apk
然后在for循環(huán)中根據(jù)分dex的count進(jìn)行遍歷须眷,經(jīng)過fileName竖瘾,dexDir文件拼接最后產(chǎn)生的files列表的全稱就是data/data/packageName/code_cache/secondary-dexes/data/data/apkName.apk.classesN.zip
但是如果提取失敗,或者文件校驗(yàn)不成功花颗,便會(huì)強(qiáng)制進(jìn)行performExtractions捕传。

private static List<File> performExtractions(File sourceApk, File dexDir) throws IOException { 
    String extractedFilePrefix = sourceApk.getName() + ".classes";
    prepareDexDir(dexDir, extractedFilePrefix);
    ArrayList files = new ArrayList();
    ZipFile apk = new ZipFile(sourceApk);
    try {
            int e = 2;
            for(ZipEntry dexFile = apk.getEntry("classes" + e + ".dex");
            dexFile != null;
            dexFile = apk.getEntry("classes" + e + ".dex")) {
            String fileName = extractedFilePrefix + e + ".zip";
            File extractedFile = new File(dexDir, fileName);
            files.add(extractedFile);
            int numAttempts = 0;
            boolean isExtractionSuccessful = false;
            while(numAttempts < 3 && !isExtractionSuccessful) {
                ++numAttempts;
                extract(apk, dexFile, extractedFile, extractedFilePrefix);
                isExtractionSuccessful = verifyZipFile(extractedFile);
                if(!isExtractionSuccessful) {
                    extractedFile.delete();
                    if(extractedFile.exists()) {
                    }
               }
            }
            if(!isExtractionSuccessful) {
                throw new IOException("Could not create zip file " + extractedFile.getAbsolutePath() + " for secondary dex (" + e + ")");
            }
            ++e;
        }
    } finally {
        try {
            apk.close();
        } catch (IOException var16) {
            Log.w("MultiDex", "Failed to close resource", var16);
        }
    }
    return files;
}

首先調(diào)用的是prepareDexDir方法

File cache = dexDir.getParentFile();
mkdirChecked(cache);
mkdirChecked(dexDir);
FileFilter filter = new FileFilter() {
    public boolean accept(File pathname) {
        return !pathname.getName().startsWith(extractedFilePrefix); 
   }};

在里面初始化了兩級(jí)文件夾 code_cache和secondary-dexes,然后過濾掉不是extractedFilePrefix開頭的文件并將其刪除扩劝,通過ZipFile的構(gòu)造函數(shù)中傳入源文件apk

int e = 2;
for(ZipEntry dexFile = apk.getEntry("classes" + e + ".dex");dexFile != null;dexFile = apk.getEntry("classes" + e + ".dex")) {
    String fileName = extractedFilePrefix + e + ".zip";
    File extractedFile = new File(dexDir, fileName);
    files.add(extractedFile);
    int numAttempts = 0;
    boolean isExtractionSuccessful = false;
    while(numAttempts < 3 && !isExtractionSuccessful) {
        ++numAttempts;
        extract(apk, dexFile, extractedFile, extractedFilePrefix);
        isExtractionSuccessful = verifyZipFile(extractedFile);
        if(!isExtractionSuccessful) {
            extractedFile.delete();
            if(extractedFile.exists()) {
            }
        }
    }
    if(!isExtractionSuccessful) {
        throw new IOException("Could not create zip file " + extractedFile.getAbsolutePath() + " for secondary dex (" + e + ")");
    }
    ++e;

for(1;2;3){4}庸论,for循環(huán)的順序就是1-2-4-3這樣的职辅,所以首先是從ZipFile的getEntry中取出classes2.dex這個(gè)dexFile,經(jīng)過熟悉的兩步拼接成一個(gè)data/data/packageName/code_cache/secondary-dexes/data/data/apkName.apk.classesN.zip聂示,然后調(diào)用extract這個(gè)方法域携。

private static void extract(ZipFile apk, ZipEntry dexFile, File extractTo, String extractedFilePrefix) throws IOException, FileNotFoundException {
    InputStream in = apk.getInputStream(dexFile);
    ZipOutputStream out = null;
    File tmp = File.createTempFile(extractedFilePrefix, ".zip", extractTo.getParentFile());
    try {
        out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(tmp)));
        try {
            ZipEntry classesDex = new ZipEntry("classes.dex");
            classesDex.setTime(dexFile.getTime());
            out.putNextEntry(classesDex);
            byte[] buffer = new byte[16384];
            for(int length = in.read(buffer); length != -1; length = in.read(buffer)) {
                out.write(buffer, 0, length);
            }
            out.closeEntry();
        } finally {
            out.close();
        }
        if(!tmp.renameTo(extractTo)) {
            throw new IOException("Failed to rename \"" + tmp.getAbsolutePath() + "\" to \"" + extractTo.getAbsolutePath() + "\"");
        }
    } finally {
        closeQuietly(in);
        tmp.delete();
    }}

第一步拿到dexFile的流,創(chuàng)建一個(gè)tmp的zip臨時(shí)文件鱼喉,將dexFile的數(shù)據(jù)寫入到臨時(shí)的zip文件中秀鞭,并存入一份時(shí)間戳,這里有三次重試機(jī)會(huì)扛禽,每次調(diào)用一次便將numAttempts++锋边,如果仍然不成功,就將這個(gè)文件刪除编曼。
到此階段豆巨,無論是直接加載dex還是重新提取都走完了自己的階段,然后就是最終MultiDex的installSecondaryDexes方法灵巧,分為三個(gè)版本的加載搀矫,分別是19,14和14以下,其中14和19版本的代碼大同小異刻肄,在19上只是多了一個(gè)suppressedExceptions瓤球,這個(gè)在stackoverflow上有人給了一個(gè)定義

Java 7 has a new feature called "suppressed exceptions", because of "the addition of ARM" (support for ARM CPUs?).

主要是用于兼容ARM平臺(tái)的cpu,做一些特定的事情

An exception can be thrown from the block of code associated with the try-with-resources statement. In the example writeToFileZipFileContents, an exception can be thrown from the try block, and up to two exceptions can be thrown from the try-with-resources statement when it tries to close the ZipFile and BufferedWriter objects. If an exception is thrown from the try block and one or more exceptions are thrown from the try-with-resources statement, then those exceptions thrown from the try-with-resources statement are suppressed, and the exception thrown by the block is the one that is thrown by the writeToFileZipFileContents method. You can retrieve these suppressed exceptions by calling the Throwable.getSuppressed method from the exception thrown by the try block.

真正的install只有三行代碼

Field pathListField = MultiDex.findField(loader, "pathList");
Object dexPathList = pathListField.get(loader);
MultiDex.expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, new ArrayList(additionalClassPathEntries), optimizedDirectory));

BaseDexClassLoader.java
通過反射拿到上面這個(gè)類中定義的DexPathList的pathList這個(gè)實(shí)例

private static Object[] makeDexElements(Object dexPathList, ArrayList<File> files, File optimizedDirectory) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
    Method makeDexElements = MultiDex.findMethod(dexPathList, "makeDexElements", new Class[]{ArrayList.class, File.class});
    return (Object[])((Object[])makeDexElements.invoke(dexPathList, new Object[]{files, optimizedDirectory}));
}

然后再次通過反射調(diào)用DexPathList的MakeDexElements方法敏弃,這里實(shí)際上是產(chǎn)生了一個(gè)Elements數(shù)組卦羡,包含.jar.zip.apk等文件。最終實(shí)際上我們通過classloader加載的就是這個(gè)列表麦到。
整個(gè)加載過程到這里就分析完了绿饵,其實(shí)還有很多細(xì)節(jié)沒有理清,以后深入理解后可能能產(chǎn)生一些新的想法瓶颠。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末拟赊,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子粹淋,更是在濱河造成了極大的恐慌吸祟,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,907評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件桃移,死亡現(xiàn)場離奇詭異屋匕,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)借杰,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,987評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門过吻,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人蔗衡,你說我怎么就攤上這事纤虽∪槿疲” “怎么了?”我有些...
    開封第一講書人閱讀 164,298評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵逼纸,是天一觀的道長刷袍。 經(jīng)常有香客問我,道長樊展,這世上最難降的妖魔是什么呻纹? 我笑而不...
    開封第一講書人閱讀 58,586評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮专缠,結(jié)果婚禮上雷酪,老公的妹妹穿的比我還像新娘。我一直安慰自己涝婉,他們只是感情好哥力,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,633評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著墩弯,像睡著了一般吩跋。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上渔工,一...
    開封第一講書人閱讀 51,488評(píng)論 1 302
  • 那天锌钮,我揣著相機(jī)與錄音,去河邊找鬼引矩。 笑死梁丘,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的旺韭。 我是一名探鬼主播氛谜,決...
    沈念sama閱讀 40,275評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼区端!你這毒婦竟也來了值漫?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,176評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤织盼,失蹤者是張志新(化名)和其女友劉穎杨何,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體悔政,經(jīng)...
    沈念sama閱讀 45,619評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡晚吞,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,819評(píng)論 3 336
  • 正文 我和宋清朗相戀三年延旧,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了谋国。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,932評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡迁沫,死狀恐怖芦瘾,靈堂內(nèi)的尸體忽然破棺而出捌蚊,到底是詐尸還是另有隱情,我是刑警寧澤近弟,帶...
    沈念sama閱讀 35,655評(píng)論 5 346
  • 正文 年R本政府宣布缅糟,位于F島的核電站,受9級(jí)特大地震影響祷愉,放射性物質(zhì)發(fā)生泄漏窗宦。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,265評(píng)論 3 329
  • 文/蒙蒙 一二鳄、第九天 我趴在偏房一處隱蔽的房頂上張望赴涵。 院中可真熱鬧,春花似錦订讼、人聲如沸髓窜。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,871評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽寄纵。三九已至,卻和暖如春脖苏,著一層夾襖步出監(jiān)牢的瞬間程拭,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,994評(píng)論 1 269
  • 我被黑心中介騙來泰國打工棍潘, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留哺壶,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,095評(píng)論 3 370
  • 正文 我出身青樓蜒谤,卻偏偏與公主長得像山宾,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子鳍徽,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,884評(píng)論 2 354

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

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,129評(píng)論 25 707
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理资锰,服務(wù)發(fā)現(xiàn),斷路器阶祭,智...
    卡卡羅2017閱讀 134,656評(píng)論 18 139
  • “寒冬落魄你不在濒募,春暖花開你是誰” 01 朋友問我鞭盟,為什么每次發(fā)很多信息給男...
    白格姨媽閱讀 6,810評(píng)論 0 3
  • 還記得大一期末考試考完的晚上,我想寫一下這半年瑰剃,我在大學(xué)做了什么齿诉。那個(gè)時(shí)候我還是挺有激情的人,對(duì)什么東西都想去,而...
    打啊個(gè)大西瓜閱讀 111評(píng)論 0 0
  • 今天看到這個(gè)標(biāo)題焕议,真的不知道如何下筆。曾經(jīng)的愛人弧关,現(xiàn)在變成了最熟悉的陌生人盅安。 愛情就像手中沙,握得越緊世囊,流失的...
    仙劍貓閱讀 226評(píng)論 0 3