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)生一些新的想法瓶颠。