編輯:關於Android編程
Android開發者應該都遇到了64K最大方法數限制的問題,針對這個問題,google也推出了multidex分包機制,在生成apk的時候,把整個應用拆成n個dex包(classes.dex、classes2.dex、classes3.dex),每個dex不超過64k個方法。使用multidex,在5.0以前的系統,應用安裝時只安裝main dex(包含了應用啟動需要的必要class),在應用啟動之後,需在Application的attachBaseContext中調用MultiDex.install(base)方法,在這時候才加載第二、第三…個dex文件,從而規避了64k問題。
當然,在attachBaseContext方法中直接install啟動second dex會有一些問題,比如install方法是一個同步方法,當在主線程中加載的dex太大的時候,耗時會比較長,可能會觸發ANR。
本文主要分析的是MultiDex.install()到底做了什麼,如何把secondary dexes中的類動態加載進來。
代碼入口很簡單,簡單粗暴,就調用了一個靜態方法MultiDex.install(base);,傳入一個Context對象
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
MultiDex.install(base);
}
下面是主要的代碼
public static void install(Context context) {
Log.i("MultiDex", "install");
if (IS_VM_MULTIDEX_CAPABLE) {
//VM版本大於2.1時,IS_VM_MULTIDEX_CAPABLE為true,這時候MultiDex.install什麼也不用做,直接返回。因為大於2.1的VM會在安裝應用的時候,就把多個dex合並到一塊
} else if (VERSION.SDK_INT < 4) {
//Multi dex最小支持的SDK版本為4
throw new RuntimeException("Multi dex installation failed. SDK " + VERSION.SDK_INT + " is unsupported. Min SDK version is " + 4 + ".");
} else {
try {
ApplicationInfo e = getApplicationInfo(context);
if (e == null) {
return;
}
Set var2 = installedApk;
synchronized (installedApk) {
String apkPath = e.sourceDir;
//檢測應用是否已經執行過install()了,防止重復install
if (installedApk.contains(apkPath)) {
return;
}
installedApk.add(apkPath);
//獲取ClassLoader,後面會用它來加載second dex
DexClassLoader classLoader;
ClassLoader loader;
try {
loader = context.getClassLoader();
} catch (RuntimeException var9) {
return;
}
if (loader == null) {
return;
}
//清空目錄:/data/data//files/secondary-dexes/,其實我沒搞明白這個的作用,因為從後面的代碼來看,這個目錄是沒有使用到的
try {
clearOldDexDir(context);
} catch (Throwable var8) {
}
File dexDir = new File(e.dataDir, "code_cache/secondary-dexes");
//把dex文件緩存到/data/data//code_cache/secondary-dexes/目錄,[後有詳細分析]
List files = MultiDexExtractor.load(context, e, dexDir, false);
if (checkValidZipFiles(files)) {
//進行安裝,[後有詳細分析]
installSecondaryDexes(loader, dexDir, files);
} else {
//文件無效,從apk文件中再次解壓secondary dex文件後進行安裝
files = MultiDexExtractor.load(context, e, dexDir, true);
if (!checkValidZipFiles(files)) {
throw new RuntimeException("Zip files were not valid.");
}
installSecondaryDexes(loader, dexDir, files);
}
}
} catch (Exception var11) {
throw new RuntimeException("Multi dex installation failed (" + var11.getMessage() + ").");
}
}
}
這段代碼的主要邏輯整理如下:
VM版本檢測,如果大於2.1就什麼都不做(系統在安裝應用的時候已經幫我們把dex合並了),如果系統SDK版本小於4就拋出運行時異常 把apk中的secondary dexes解壓到緩存目錄,並把這些緩存讀取出來。應用第二次啟動的時候,會嘗試從緩存目錄中讀取,除非讀取出的文件校驗失敗,否則不再從apk中解壓dexes 根據當前的SDK版本,執行不同的安裝方法先來看看MultiDexExtractor.load(context, e, dexDir, false)
/**
* 解壓apk文件中的classes2.dex、classes3.dex等文件解壓到dexDir目錄中
*
* @param dexDir 解壓目錄
* @param forceReload 是否需要強制從apk文件中解壓,否的話會直接讀取舊文件
* @return 解壓後的文件列表
* @throws IOException
*/
static List 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 {
//把apk中的secondary dex文件解壓到緩存目錄,並把解壓後的文件返回
files = performExtractions(sourceApk, dexDir);
//把解壓信息保存到sharedPreferences中
putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1);
}
return files;
}
首先判斷以下是否需要強制從apk文件中解壓,再進行下CRC校驗,如果不需要從apk重新解壓,就直接從緩存目錄中讀取已解壓的文件返回,否則解壓apk中的classes文件到緩存目錄,再把相應的文件返回。這個方法再往下的分析就不貼出來了,不復雜,大家可以自己去看看。讀取後會把解壓信息保存到sharedPreferences中,裡面會保存時間戳、CRC校驗和dex數量。
得到dex文件列表後,要做的就是把dex文件關聯到應用,這樣應用findclass的時候才能成功。這個主要是通過installSecondaryDexes方法來完成的
/**
* 安裝dex文件
*
* @param loader 類加載器
* @param dexDir 緩存目錄,用以存放opt之後的dex文件
* @param files 需要安裝的dex
* @throws IllegalArgumentException
* @throws IllegalAccessException
* @throws NoSuchFieldException
* @throws InvocationTargetException
* @throws NoSuchMethodException
* @throws IOException
*/
private static void installSecondaryDexes(ClassLoader loader,
File dexDir,
List files) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException {
if (!files.isEmpty()) {
//對不同版本的SDK做不同處理
if (VERSION.SDK_INT >= 19) {
MultiDex.V19.install(loader, files, dexDir);
} else if (VERSION.SDK_INT >= 14) {
MultiDex.V14.install(loader, files, dexDir);
} else {
MultiDex.V4.install(loader, files);
}
}
}
可以看到,對於不同的SDK版本,分別采用了不同的處理方法,我們主要分析SDK>=19的情況,其他情況大同小異,讀者可以自己去分析。
private static final class V19 {
private V19() {
}
/**
* 安裝dex文件
*
* @param loader 類加載器
* @param additionalClassPathEntries 需要安裝的dex
* @param optimizedDirectory 緩存目錄,用以存放opt之後的dex文件
* @throws IllegalArgumentException
* @throws IllegalAccessException
* @throws NoSuchFieldException
* @throws InvocationTargetException
* @throws NoSuchMethodException
*/
private static void install(ClassLoader loader,
List additionalClassPathEntries,
File optimizedDirectory) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
//通過反射獲取ClassLoader對象中的pathList屬性,其實是ClassLoader的父類BaseDexClassLoader中的成員
Field pathListField = MultiDex.findField(loader, "pathList");
//通過屬性獲取該屬性的值,該屬性的類型是DexPathList
Object dexPathList = pathListField.get(loader);
ArrayList suppressedExceptions = new ArrayList();
//通過反射調用dexPathList的makeDexElements返回Element對象數組。方法裡面會讀取每一個輸入文件,生成DexFile對象,並將其封裝進Element對象
Object[] elements = makeDexElements(dexPathList, new ArrayList(additionalClassPathEntries), optimizedDirectory, suppressedExceptions);
//將elements數組跟dexPathList對象的dexElements數組合並,並把合並後的數組作為dexPathList新的值
MultiDex.expandFieldArray(dexPathList, "dexElements", elements);
//處理異常
if (suppressedExceptions.size() > 0) {
Iterator suppressedExceptionsField = suppressedExceptions.iterator();
while (suppressedExceptionsField.hasNext()) {
IOException dexElementsSuppressedExceptions = (IOException) suppressedExceptionsField.next();
Log.w("MultiDex", "Exception in makeDexElement", dexElementsSuppressedExceptions);
}
Field suppressedExceptionsField1 = MultiDex.findField(loader, "dexElementsSuppressedExceptions");
IOException[] dexElementsSuppressedExceptions1 = (IOException[]) ((IOException[]) suppressedExceptionsField1.get(loader));
if (dexElementsSuppressedExceptions1 == null) {
dexElementsSuppressedExceptions1 = (IOException[]) suppressedExceptions.toArray(new IOException[suppressedExceptions
.size()]);
} else {
IOException[] combined = new IOException[suppressedExceptions.size() + dexElementsSuppressedExceptions1.length];
suppressedExceptions.toArray(combined);
System.arraycopy(dexElementsSuppressedExceptions1, 0, combined, suppressedExceptions.size(), dexElementsSuppressedExceptions1.length);
dexElementsSuppressedExceptions1 = combined;
}
suppressedExceptionsField1.set(loader, dexElementsSuppressedExceptions1);
}
}
private static Object[] makeDexElements(Object dexPathList,
ArrayList files,
File optimizedDirectory,
ArrayList suppressedExceptions) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
Method makeDexElements = MultiDex.findMethod(dexPathList, "makeDexElements", new Class[]{ArrayList.class, File.class, ArrayList.class});
return (Object[]) ((Object[]) makeDexElements.invoke(dexPathList, new Object[]{files, optimizedDirectory, suppressedExceptions}));
}
}
在Android中,有兩個ClassLoader,分別是DexPathList和PathClassLoader,它們的父類都是BaseDexClassLoader,DexPathList和PathClassLoader的實現都是在BaseDexClassLoader之中,而BaseDexClassLoader的實現又基本是通過調用DexPathList的方法完成的。DexPathList裡面封裝了加載dex文件為DexFile對象(調用了native方法,有興趣的童鞋可以繼續跟蹤下去)的方法。
上述代碼中的邏輯如下:
當把dex文件加載到pathList的dexElements數組之後,整個multidex.install基本上就完成了。
但可能還有些童鞋還會有些疑問,僅僅只是把Element數組合並到ClassLoader就可以了嗎?還是沒有找到加載類的地方啊?那我們再繼續看看,當用到一個類的時候,會用ClassLoader去加載一個類,加載類會調用類加載器的findClass方法
@Override
protected Class findClass(String name) throws ClassNotFoundException {
List suppressedExceptions = new ArrayList();
//調用pathList的findClass方法
Class c = pathList.findClass(name, suppressedExceptions);
if (c == null) {
ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
for (Throwable t : suppressedExceptions) {
cnfe.addSuppressed(t);
}
throw cnfe;
}
return c;
}
於是繼續跟蹤:
public Class findClass(String name, List suppressed) {
//遍歷dexElements數組
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
//繼續跟蹤會發現調用的是一個native方法
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
到現在就清晰了,當加載一個類的時候,會遍歷dexElements數組,通過native方法從Element元素中加載類名相應的類
到最後,總結整個multidex.install流程,其實很簡單,就做了一件事情,把apk中的secondary dex文件通過ClassLoader轉換成Element數組,並把輸出的數組合與ClassLoader的Element數組合並。
Android控件之使用ListView實現時間軸效果
實現思路:該View是通過ListView實現的,通過實體兩個字段內容content和時間time來展示每個ListItem時間軸是使用上面一條線(20dp)
android基於ListView和CheckBox實現多選和全選記錄的功能
應用開發中經常會有從數據庫中讀取數據顯示,然後選中多條、全部記錄並且刪除的需求。在做定制系統聯系人的時候也遇到這樣的需求,下面寫個簡單的通過ListView和CheckB
Andriod React Native 樣式表中可用樣式屬性
寫了這麼多篇Android React Native的博文,基本上把復雜的東西都搞定了,接下來來看看一些輕松的東西,和布局有關,就是css樣式,那麼一個View可以設置哪
Android微信Tinker熱更新詳細使用
先看一下效果圖Tinker已知問題由於原理與系統限制,Tinker有以下已知問題: Tinker不支持修改AndroidManifest.xml,Tinker不支持新增