編輯:關於Android編程
一、Android的ClassLoader體系
vcrWu/rW0LXEYXBroaPV4rj20rLKx1BhdGhDbGFzc0xvYWRlctf3zqrErMjPtcTA4LzT1NjG97XE1K3S8qOs0vLOqtK7sOOzzNDytrzKx7Cy17DBy6Os1Nq08r+qo6zV4sqxuvJQYXRoQ2xhc3NMb2FkZXK+zcilvNPU2Na4tqi1xGFwayi94tG5s8lkZXijrMi7uvPU2tPFu6+zyW9kZXgpvs2/ydLUwcuhozwvcD4NCjxwPkRleENsYXNzTG9hZGVyv8nS1LzT1NjIzrrOwre+trXEYXBrL2RleC9qYXKjrFBhdGhDbGFzc0xvYWRlcta7xNy809TY0tGwstewtb3Ptc2z1tCjqLy0L2RhdGEvYXBwxL/CvM/Co6m1xGFwa87EvP6hozwvcD4NCjxwPrTTyc/D5s7Sw8fWqrXAo6xEZXhDbGFzc0xvYWRlcrrNUGF0aENsYXNzTG9hZGVyvNPU2NStwO3G5Mq1ysfSu9H5tcSjrL7NysfKudPDs6G+sLK70rvR+aGjPC9wPg0KPHA+PHN0cm9uZz62/qGiRGV4Q2xhc3NMb2FkZXK2r8ysvNPU2LXEyrXP1jwvc3Ryb25nPjwvcD4NCjxwPjxzdHJvbmc+tdrSu7K9o7q0tL2oRGV4Q2xhc3NMb2FkZXK21M/zo6y809TYttTTprXEYXBrL2RleC9qYXLOxLz+oaM8L3N0cm9uZz48L3A+DQo8cHJlIGNsYXNzPQ=="brush:java;">
is = getAssets().open("app.apk");
file = new File(getFilesDir(), "plugin.apk");
fos = new FileOutputStream(file);
byte[] buffer = new byte[1024];
int len;
while ((len = is.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
fos.flush();
String apkPath = file.getAbsolutePath();
dexClassLoader = new DexClassLoader(apkPath, getFilesDir().getAbsolutePath(), null, getClassLoader());
下面來看看DexClassLoader的構造函數
public class DexClassLoader extends BaseDexClassLoader {
// dexPath:是加載apk/dex/jar的路徑
// optimizedDirectory:是dex的輸出路徑(因為加載apk/jar的時候會解壓除dex文件,這個路徑就是保存dex文件的)
// libraryPath:是加載的時候需要用到的lib庫,這個一般不用
// parent:給DexClassLoader指定父加載器
public DexClassLoader(String dexPath, String optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
}
可以看到它調用的是父類的構造函數,所以直接來看BaseDexClassLoader的構造函數。
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
可以看到,它創建了一個DexPathList實例,下面來看看構造函數。
private final Element[] dexElements;
// definingContext對應的就是當前classLoader
// dexPath對應的就是上面傳進來的apk/dex/jar的路徑
// libraryPath就是上面傳進來的加載的時候需要用到的lib庫的目錄,這個一般不用
// optimizedDirectory就是上面傳進來的dex的輸出路徑
public DexPathList(ClassLoader definingContext, String dexPath,
String libraryPath, File optimizedDirectory) {
ArrayList suppressedExceptions = new ArrayList();
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
suppressedExceptions);
}
可以看到它調用的是makeDexElements方法,這個方法就是得到一個裝有dex文件的數組Element[],每個Element對象裡面包含一個DexFile對象成員,它對應的就是dex文件。
static class Element {
private final File file;
private final boolean isDirectory;
private final File zip;
private final DexFile dexFile;
......
}
具體的我們後面再說,下面先看看makeDexElements方法。
// files是一個ArrayList列表,它對應的就是apk/dex/jar文件,因為我們可以指定多個文件。
// optimizedDirectory是前面傳入dex的輸出路徑
// suppressedExceptions為一個異常列表
private static Element[] makeDexElements(ArrayList files, File optimizedDirectory,
ArrayList suppressedExceptions) {
ArrayList elements = new ArrayList();
/*
* Open all files and load the (direct or contained) dex files
* up front.
*/
for (File file : files) {
File zip = null;
DexFile dex = null;
String name = file.getName();
// 如果是一個dex文件
if (name.endsWith(DEX_SUFFIX)) {
// Raw dex file (not inside a zip/jar).
try {
dex = loadDexFile(file, optimizedDirectory);
} catch (IOException ex) {
System.logE("Unable to load dex file: " + file, ex);
}
// 如果是一個apk或者jar或者zip文件
} else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX)
|| name.endsWith(ZIP_SUFFIX)) {
zip = file;
try {
dex = loadDexFile(file, optimizedDirectory);
} catch (IOException suppressed) {
/*
* IOException might get thrown "legitimately" by the DexFile constructor if the
* zip file turns out to be resource-only (that is, no classes.dex file in it).
* Let dex == null and hang on to the exception to add to the tea-leaves for
* when findClass returns null.
*/
suppressedExceptions.add(suppressed);
}
} else if (file.isDirectory()) {
// We support directories for looking up resources.
// This is only useful for running libcore tests.
elements.add(new Element(file, true, null, null));
} else {
System.logW("Unknown file type for: " + file);
}
if ((zip != null) || (dex != null)) {
elements.add(new Element(file, false, zip, dex));
}
}
return elements.toArray(new Element[elements.size()]);
}
前面我們提到過Element,它裡面具體包含哪些元素,現在從上面代碼我們就可以知道了。
static class Element {
private final File file; // 它對應的就是需要加載的apk/dex/jar文件
private final boolean isDirectory; // 第一個參數file是否為一個目錄,一般為false,因為我們傳入的是要加載的文件
private final File zip; // 如果加載的是一個apk或者jar或者zip文件,該對象對應的就是該apk或者jar或者zip文件
private final DexFile dexFile; // 它是得到的dex文件
......
}
上面我們可以看到,它調用的是loadDexFile方法。
// file為需要加載的apk/dex/jar文件
// optimizedDirectorydex的輸出路徑
private static DexFile loadDexFile(File file, File optimizedDirectory)
throws IOException {
if (optimizedDirectory == null) {
return new DexFile(file);
} else {
String optimizedPath = optimizedPathFor(file, optimizedDirectory);
return DexFile.loadDex(file.getPath(), optimizedPath, 0);
}
}
如果我們沒有指定dex輸出目錄的話,就直接創建一個DexFile對象,如果我們指定了dex輸出目錄,我們就需要構造dex輸出路徑。
optimizedPathFor方法用來得到輸出文件dex路徑,就是optimizedDirectory/filename.dex,optimizedDirectory是前面指定的輸出目錄,filename就是加載的文件名,後綴為.dex,最終構造得到一個輸出dex文件路徑.
下面我們重點看看DexFile.loadDex方法。
static public DexFile loadDex(String sourcePathName, String outputPathName,
int flags) throws IOException {
return new DexFile(sourcePathName, outputPathName, flags);
}
下面我們就不往下看了,我們這裡可以進行總結。
1、在DexClassLoader我們指定了加載的apk/dex/jar文件和dex輸出路徑optimizedDirectory,它最終會被解析得到DexFile文件。
2、將DexFile文件對象放在Element對象裡面,它對應的就是Element對象的dexFile成員變量。
3、將這個Element對象放在一個Element[]數組中,然後將這個數組返回給DexPathList的dexElements成員變量。
4、DexPathList是BaseDexClassLoader的一個成員變量。
最終得到一個裝有dex文件的數組Element[],每個Element對象裡面包含一個DexFile對象成員,它對應的就是dex文件。
第二步:調用dexClassLoader的loadClass,得到加載的dex裡面的指定的Class.
clazz = dexClassLoader.loadClass("com.example.apkplugin.PluginTest");
下面我們來分析一下loadClass方法。因為DexClassLoader和BaseDexClassLoader都沒有實現loadClass方法,所以最終調用的是ClassLoader的loadClass方法。
public Class loadClass(String className) throws ClassNotFoundException {
return loadClass(className, false);
}
protected Class loadClass(String className, boolean resolve) throws ClassNotFoundException {
Class clazz = findLoadedClass(className);
if (clazz == null) {
ClassNotFoundException suppressed = null;
try {
clazz = parent.loadClass(className, false);
} catch (ClassNotFoundException e) {
suppressed = e;
}
if (clazz == null) {
try {
clazz = findClass(className);
} catch (ClassNotFoundException e) {
e.addSuppressed(suppressed);
throw e;
}
}
}
return clazz;
}
可以看到它調用的是findClass方法,由於DexClassLoader沒有實現這個方法,所以我們看BaseDexClassLoader的findClass
@Override
protected Class findClass(String name) throws ClassNotFoundException {
List suppressedExceptions = new ArrayList();
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;
}
pathList就是前面創建的DexPathList對象,從上面我們知道,我們加載的dex文件都存放在它的exElements成員變量上面,dexElements就是Element[]數組,所以可以看到BaseDexClassLoader的findClass方法調用的是pathList的findClass方法,我們具體來看看。
可以看到BaseDexClassLoader的findClass方法調用的是DexPathList的findClass方法。
public Class findClass(String name, List suppressed) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
可以看到它就是遍歷dexElements數組,從每個Element對象中拿到DexFile類型的dex文件,然後就是從dex去加載所需要的class文件,直到找到為止。
總結:一個ClassLoader可以包含多個dex文件,每個dex文件是一個Element,多個dex文件排列成一個有序的數組dexElements,當找類的時候,會按順序遍歷dex文件,然後從當前遍歷的dex文件中找類,如果找類則返回,如果找不到從下一個dex文件繼續查找。
三、MultiDex基本原理
當一個app的功能越來越復雜,代碼量越來越多,可以遇到下面兩種情況:
1. 生成的apk在2.3以前的機器無法安裝,提示INSTALL_FAILED_DEXOPT
2. 方法數量過多,編譯時出錯,提示:Conversion to Dalvik format failed:Unable to execute dex: method ID not in [0, 0xffff]: 65536
原因:
1. Android2.3及以前版本用來執行dexopt(用於優化dex文件)的內存只分配了5M
2. 一個dex文件最多只支持65536個方法。
解決方案:
1、使用Multidex,將編譯好的class文件拆分打包成兩個dex,繞過dex方法數量的限制以及安裝時的檢查,在運行時再動態加載第二個dex文件中。
2、使用插件化,將功能模塊分離,減少宿主apk的大小和代碼。
插件化我們這裡先不討論,這裡主要來說說Multidex的原理。
基本原理:
1、除了第一個dex文件(即正常apk包唯一包含的Dex文件),其它dex文件都以資源的方式放在安裝包中。所以我們需要將其他dex文件並在Application的onCreate回調中注入到系統的ClassLoader。並且對於那些在注入之前已經引用到的類(以及它們所在的jar),必須放入第一個Dex文件中。
2、PathClassLoader作為默認的類加載器,在打開應用程序的時候PathClassLoader就去加載指定的apk(解壓成dex,然後在優化成odex),也就是第一個dex文件是PathClassLoader自動加載的。所以,我們需要做的就是將其他的dex文件注入到這個PathClassLoader中去。
3、因為PathClassLoader和DexClassLoader的原理基本一致,從前面的分析來看,我們知道PathClassLoader裡面的dex文件是放在一個Element數組裡面,可以包含多個dex文件,每個dex文件是一個Element,所以我們只需要將其他的dex文件放到這個數組中去就可以了。
實現:
1、通過反射獲取PathClassLoader中的DexPathList中的Element數組(已加載了第一個dex包,由系統加載)
2、通過反射獲取DexClassLoader中的DexPathList中的Element數組(將第二個dex包加載進去)
3、將兩個Element數組合並之後,再將其賦值給PathClassLoader的Element數組
谷歌提供的MultiDex支持庫就是按照這個思路來實現的,我們可以直接來看看源碼。
首先來看看使用:
1、修改Gradle的配置,支持multidex:
android {
compileSdkVersion 21
buildToolsVersion "21.1.0"
defaultConfig {
...
minSdkVersion 14
targetSdkVersion 21
...
// Enabling multidex support.
multiDexEnabled true
}
...
}
dependencies {
compile 'com.android.support:multidex:1.0.0'
}
在manifest文件中,在application標簽下添加MultidexApplication Class的引用,如下所示:
...
使用起來很簡單,下面我們來看看源碼,看是不是按照前面介紹的思路實現的。
首先我們來看看MultiDexApplication類。
public class MultiDexApplication extends Application {
public MultiDexApplication() {
}
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
MultiDex.install(this);
}
}
原來要求使用MultiDexApplication的原因就是它重寫了Application,主要是為了將其他dex文件注入到系統的ClassLoader。
進入MultiDex.install(this)方法。
public static void install(Context context) {
if(IS_VM_MULTIDEX_CAPABLE) {
Log.i("MultiDex", "VM has multidex support, MultiDex support library is disabled.");
// 可以看到,MultiDex不支持SDK版本小於4的系統
} 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 {
try {
// 獲取到應用信息
ApplicationInfo e = getApplicationInfo(context);
if(e == null) {
return;
}
Set var2 = installedApk;
synchronized(installedApk) {
// 得到我們這個應用的apk文件路徑
// 拿到這個apk文件路徑之後,後面就可以從中提取出其他的dex文件
// 並且加載dex放到一個Element數組中
String apkPath = e.sourceDir;
if(installedApk.contains(apkPath)) {
return;
}
// 將這個apk文件路徑放到一個set中
installedApk.add(apkPath);
// 得到classLoader,它就是PathClassLoader
// 後面就可以從這個PathClassLoader中拿到DexPathList中的Element數組
// 這個數組裡面就包括由系統加載第一個dex包
ClassLoader loader;
try {
loader = context.getClassLoader();
} catch (RuntimeException var9) {
Log.w("MultiDex", "Failure while trying to obtain Context class loader. Must be running in test mode. Skip patching.", var9);
return;
}
// 得到apk解壓後得到的dex文件的存放目錄,放到應用的data目錄下
File dexDir = new File(e.dataDir, SECONDARY_FOLDER_NAME);
// 這個方法就是從apk中提取dex文件,放到data目錄下,就不展開了
List files = MultiDexExtractor.load(context, e, dexDir, false);
if(checkValidZipFiles(files)) {
// 這個方法就是將其他的dex文件注入到系統classloader中的具體操作
installSecondaryDexes(loader, dexDir, files);
} else {
files = MultiDexExtractor.load(context, e, dexDir, true);
installSecondaryDexes(loader, dexDir, files);
}
}
} catch (Exception var11) {
Log.e("MultiDex", "Multidex installation failure", var11);
throw new RuntimeException("Multi dex installation failed (" + var11.getMessage() + ").");
}
}
}
下面我們重點看看installSecondaryDexes方法。
// loader對應的就是PathClassLoader
// dexDir是dex的存放目錄
// files對應的就是dex文件
private static void installSecondaryDexes(ClassLoader loader, File dexDir, List files) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException {
if(!files.isEmpty()) {
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版本實現是有差別的,因為它裡面是使用反射實現的,所以會有不同,我們看看MultiDex.V14.install方法。
private static void install(ClassLoader loader, List additionalClassPathEntries, File optimizedDirectory) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
// 這個方法就是使用反射來得到loader的pathList字段
Field pathListField = MultiDex.findField(loader, "pathList");
// 得到loader的pathList字段後,我們就可以得到這個字段的值,也就是DexPathList對象
Object dexPathList = pathListField.get(loader);
// 這個方法就是將其他的dex文件Element數組和第一個dex的Element數組合並
// makeDexElements方法就是用來得到其他dex的Elements數組
MultiDex.expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, new ArrayList(additionalClassPathEntries), optimizedDirectory));
}
下面來看看合並的過程
// instance對應的就是pathList對象
// fieldName 對應的就是字段名,我們要得到的就是pathList對象裡面的dexElements數組
// extraElements對應的就是其他dex對應的Element數組
private static void expandFieldArray(Object instance, String fieldName, Object[] extraElements) throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
// 得到Element數組字段
Field jlrField = findField(instance, fieldName);
// 得到pathList對象裡面的dexElements數組
Object[] original = (Object[])((Object[])jlrField.get(instance));
// 創建一個新的數組用來存放合並之後的結果
Object[] combined = (Object[])((Object[])Array.newInstance(original.getClass().getComponentType(), original.length + extraElements.length));
// 將第一個dex的Elements數組復制到創建的數組中去
System.arraycopy(original, 0, combined, 0, original.length);
// 將其他dex的Elements數組復制到創建的數組中去
System.arraycopy(extraElements, 0, combined, original.length, extraElements.length);
// 將得到的這個合並的新數組的值設置到pathList對象的Element數組字段上
jlrField.set(instance, combined);
}
整體思路跟上面說的基本一致,理解思路,結合上面的注釋基本還是比較清楚的。
四、熱修復的一種實現原理
一個ClassLoader可以包含多個dex文件,每個dex文件是一個Element,多個dex文件排列成一個有序的數組dexElements,當找類的時候,會按順序遍歷dex文件,然後從當前遍歷的dex文件中找類,如果找類則返回,如果找不到從下一個dex文件繼續查找。
理論上,如果在不同的dex中有相同的類存在,那麼會優先選擇排在前面的dex文件的類,如下圖:
所以,如果某些類需要修復,我們可以把有問題的類打包到一個dex(patch.dex)中去,然後把這個dex插入到Elements的最前面,如下圖:
一.JSON的簡介:JSON建構於兩種結構:(1)“名稱/值”對的集合(A collection of name/value pairs)。不同的
前面文章介紹了Activity以及Intent的使用,本文就來介紹Service。如果把Activity比喻為前台程序,那麼Service就是後台程序,Service的整
1.簡介 最近做一個項目,主要通過usb完成pc與Android端的數據傳輸。但是根據api提供的無法監聽usb的插拔,有解釋為不同版本會存在BUG。本
本文承接,Android 開發第五彈:簡易時鐘(鬧鐘) 和 Android 開發第六彈:簡易時鐘(計時器),這一部分是關於秒表的。布局同樣是新建一個類(StopWatch