在早起的Android開發過程中,隨着項目的增加開發者可能會遇到65535的問題,也就是程序中的方法達到上限,針對此問題官方給出的解決方案是Multidex,使用Multidex將程序中的代碼打包成多個dex文件,從而讓系統的避免方法上限的問題;
1、使用
- 使用配置
- 在build.gradle中添加支持Multidex
defaultConfig{
multiDexEnabled true
}
- 添加Multidex依賴
implementation 'com.android.support:multidex:1.0.3'
- 在Application中初始化Multidex
MultiDex.install(this);
- 反編譯打包後的APK查看生成的Dex中的類
- 此時主classes.dex中的類
- 分包(配置主multiDex中的類)
- 在app目錄下,創建MultiDex的配置文件multidex-config.txt
- 在配置文件中聲明主Dex中的類
com/alex/kotlin/jniapplication/MainActivity.class
com/alex/kotlin/jniapplication/MainActivity2.class
- 在buildTypes中設置配置文件
multiDexKeepFile file('multidex-config.txt')
- 再次打包APK,反編譯APK查看此時生成的Dex中的類
2、multiDex源碼分析
在上面的使用過程中,體驗了將Android一個項目打包到連個dex的過程,且可以設置配置文件制定主dex的類文件,因爲主dex可能會影響到程序的啓動問題,也是在啓動時就加載到內存中的,那其他的dex是如何工作的呢?下面就從源碼角度看看MultiDex如何完成其他dex加載的;
- MultiDex.install(this);
public static void install(Context context) {
if (IS_VM_MULTIDEX_CAPABLE) {
} else if (VERSION.SDK_INT < 4) { //Multidex 最小支持版本SDK_INT 爲 4
throw new RuntimeException("MultiDex installation failed. SDK " + VERSION.SDK_INT + " is unsupported. Min SDK version is " + 4 + ".");
} else {
try {
ApplicationInfo applicationInfo = getApplicationInfo(context); //獲取程序的ApplicationInfo
//分別傳入apk安裝文件和apk的信息文件
doInstallation(context, new File(applicationInfo.sourceDir), new File(applicationInfo.dataDir), "secondary-dexes", "", true);
} catch (Exception var2) {}
}
}
- 首先獲取程序的ApplicationInfo並創建程序的sourceDir 和 dataDir 文件,調用doInstallation()加載文件信息,其中sourceDir爲程序APK的安裝目錄,要被加載的dex文件就在此文件夾下,dataDir時解析apk目錄下dex文件後的緩存目錄,所有的dex文件都會被寫入目錄下
- doInstallation()
private static void doInstallation(Context mainContext, File sourceApk, File dataDir, String secondaryFolderName, String prefsKeyPrefix, boolean reinstallOnPatchRecoverableException) {
synchronized(installedApk) {
if (!installedApk.contains(sourceApk)) {//判讀此APk是否被加載過,加載過會自動跳過
installedApk.add(sourceApk); //靜態集合保存被加載的apk
ClassLoader loader;
loader = mainContext.getClassLoader(); // 獲取系統的ClassLoader實例
clearOldDexDir(mainContext); // 清除file/下的臨時文件信息,
//在dataDir文件夾下創建加載dex文件的緩存目錄 code_cache/secondary-dexes
//如果dataDir創建失敗會創建getFilesDir()爲緩存目錄,上面清除的文件地址
File dexDir = getDexDir(mainContext, dataDir, secondaryFolderName);
MultiDexExtractor extractor = new MultiDexExtractor(sourceApk, dexDir); //創建MultiDexExtractor實例,保存源文件目錄和緩存目錄
try {
List files = extractor.load(mainContext, prefsKeyPrefix, false); //(1)加載dex文件
try {
installSecondaryDexes(loader, dexDir, files); //(2)安裝dex文件
} catch (IOException var26) {
files = extractor.load(mainContext, prefsKeyPrefix, true); //第一次加載出現異常會強制重新加載,參數true
installSecondaryDexes(loader, dexDir, files);
}
} finally {
try {
extractor.close();
} catch (IOException var23) {
closeException = var23;
}
}
}
}
總結:
- 首先根據installedApk判斷此APK文件夾是否加載過,如果加載過直接返回,未加載執行加載程序並緩存文件夾
- 獲取系統的ClassLoader實例
- 在dataDir文件夾下創建code_cache/secondary-dexes緩存目錄
- 創建MultiDexExtractor實例保存apk文件目錄和緩存目錄
- 執行MultiDexExtractor.load()加載文件夾下的dex文件
- 調用installSecondaryDexes()安裝加載的dex文件數組
- 如果安裝出現異常則強制重新加載和安裝
- load(Context context, String prefsKeyPrefix, boolean forceReload)
List<? extends File> load(Context context, String prefsKeyPrefix, boolean forceReload) throws IOException {
List files;
// 判斷是否強制重新加載 或 Dex文件是否有改變
if (!forceReload && !isModified(context, this.sourceApk, this.sourceCrc, prefsKeyPrefix)) {
try {
files = this.loadExistingExtractions(context, prefsKeyPrefix);//未強制和未改變的直接從緩存的dex中加載
} catch (IOException var6) {
files = this.performExtractions();
putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(this.sourceApk), this.sourceCrc, files);
}
} else {
files = this.performExtractions(); //強制重新加載
putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(this.sourceApk), this.sourceCrc, files); //緩存信息
}
return files;
}
總結:
-
如果沒有強制重新加載、文件存在且沒有變化則從緩存的dex中加載
-
未加載過的執行加載程序並緩存信息
-
performExtractions()
private List<MultiDexExtractor.ExtractedDex> performExtractions() throws IOException {
String extractedFilePrefix = this.sourceApk.getName() + ".classes”; //設置文件名前綴
this.clearDexDir();
List<MultiDexExtractor.ExtractedDex> files = new ArrayList();
ZipFile apk = new ZipFile(this.sourceApk); //將傳入的APK文件轉換爲Zip壓縮文件
try {
int secondaryNumber = 2; //排除主Dex從第二個dex中開始遍歷加載
for(ZipEntry dexFile = apk.getEntry("classes" + secondaryNumber + ".dex"); dexFile != null; dexFile = apk.getEntry("classes" + secondaryNumber + ".dex")) { //循環獲取apk文件夾下的dex文件
String fileName = extractedFilePrefix + secondaryNumber + ".zip”; //爲每個dex文件生成對應的zip文件名稱
//對每個dex文件都創建一個文件extractedFile並保存到集合中
MultiDexExtractor.ExtractedDex extractedFile = new MultiDexExtractor.ExtractedDex(this.dexDir, fileName);
files.add(extractedFile);
int numAttempts = 0; //統計加載次數,默認加載3次,超過3次還不成功則加載失敗
boolean isExtractionSuccessful = false; //標記是否加載成功
while(numAttempts < 3 && !isExtractionSuccessful) { //循環執行加載
++numAttempts;
extract(apk, dexFile, extractedFile, extractedFilePrefix); //真正執行加載的方法
try {
extractedFile.crc = getZipCrc(extractedFile); //加載成功
isExtractionSuccessful = true;
} catch (IOException var18) {
}
}
++secondaryNumber; //繼續加載下一個dex文件
}
} finally {
}
return files;
}
上述代碼執行邏輯:
-
將傳入的APK文件轉換爲Zip壓縮文件,則每個dex文件都是Zip下的子文件
-
排除主Dex從第二個dex中開始遍歷加載,並根據secondaryNumber獲取Zip下個每個dex文件,然後在code_cache/secondary-dexes目錄下爲每個dex創建zip文件
-
對每個dex文件都創建一個文件extractedFile並保存到集合中
-
執行extract()進行文件加載
-
extract():執行文件的寫入,將apk中除了主dex之外的dex文件逐個讀取寫入到zip文件中
private static void extract(ZipFile apk, ZipEntry dexFile, File extractTo, String extractedFilePrefix) throws IOException, FileNotFoundException {
InputStream in = apk.getInputStream(dexFile); //從dexFile中獲取字節流
ZipOutputStream out = null;
//在緩存文件夾目錄中創建臨時文件夾 tmp-***.zip
File tmp = File.createTempFile("tmp-" + extractedFilePrefix, ".zip", extractTo.getParentFile());
try {
out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(tmp))); //創建ZipOutputStream輸出流
try {
ZipEntry classesDex = new ZipEntry("classes.dex");
classesDex.setTime(dexFile.getTime());
out.putNextEntry(classesDex); //設置Zip文件
byte[] buffer = new byte[16384]; //讀取dexFile中的字節流寫入臨時文件tem中保存在緩存文件夾下
for(int length = in.read(buffer); length != -1; length = in.read(buffer)) {
out.write(buffer, 0, length); //將數據寫入名爲classes.dex的Zip文件
}
out.closeEntry();
} finally {
out.close();
}
} finally {
closeQuietly(in);
tmp.delete();
}
}
- installSecondaryDexes():執行dex文件的記載,在方法中會根據系統的版本進行區分
private static void installSecondaryDexes(ClassLoader loader, File dexDir, List<? extends File> files){
if (!files.isEmpty()) { //文件是否爲null
if (VERSION.SDK_INT >= 19) {
MultiDex.V19.install(loader, files, dexDir); // 根據SDK不同執行不同的install邏輯,從緩存文件中加載
} else if (VERSION.SDK_INT >= 14) {
MultiDex.V14.install(loader, files);
} else {
MultiDex.V4.install(loader, files);
}
}
}
- install(ClassLoader loader, List<? extends File> additionalClassPathEntries, File optimizedDirectory)
static void install(ClassLoader loader, List<? extends File> additionalClassPathEntries, File optimizedDirectory) {
Field pathListField = MultiDex.findField(loader, "pathList”); //反射調用ClassLoader的pathList
Object dexPathList = pathListField.get(loader);
ArrayList<IOException> suppressedExceptions = new ArrayList();
//將取出的zip文件包裝成Element對象並擴招dexElements
MultiDex.expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, new ArrayList(additionalClassPathEntries), optimizedDirectory, suppressedExceptions));
}
private static Object[] makeDexElements(Object dexPathList, ArrayList<File> files, File optimizedDirectory, ArrayList<IOException> suppressedExceptions) {
//調用dexPathList中的makeDexElements方法傳入文件集合,解析zip文件集合中的dex文件保存爲Elements
Method makeDexElements = MultiDex.findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class, ArrayList.class);
//執行方法
return (Object[])((Object[])makeDexElements.invoke(dexPathList, files, optimizedDirectory, suppressedExceptions));
}
上面主要是使用ClassLoader加載文件,關於ClassLoader不熟悉的點擊Android熱修復之路(一)——ClassLoader查看,上述代碼的執行邏輯:
- 反射獲取ClassLoader的pathList
- 調用dexPathList中的makeDexElements方法傳入文件集合,解析zip文件集合中的dex文件保存爲Elements
- 將取出的zip文件包裝成Element對象擴展設置給ClassLoader的pathList
關於MutilDex的加載原理就到此結束了,簡單來說就是將所有的dex都加載在Element數組中,然後擴展只ClassLoader的pathList中,在ClassLoader查找類時就可以找到了;