Android中的dex、apk、ClassLoader詳解

dalvik加載、運行過程

我們編寫java代碼都是.java格式的,但是jvm並不能識別.java文件,它只能加載、執行.class文件,所以我們要通過javac命令將.java文件編譯成.class文件,然後通過java命令運行.class文件。其實,如果用C或者Python編寫的程序正確轉換成.class文件後,java虛擬機也是可以識別運行的。

dalvik與jvm差不多,區別就是dalvik只能加載、運行.dex文件(至於如何識別、運行,後面會講到)。我們的Android程序也是用java編寫的,生成的也是.java文件,所以需要把.java文件轉換成.dex文件,dalvik才能執行。IDE編譯、打包的過程,就是將.java文件轉換成.dex文件的過程,我們可以簡單看一下編譯過程,加深理解。

下面是官方介紹的打包流程圖:

這裏寫圖片描述

總結一下,主要就是這幾步:

1、根據res目錄下的資源文件、AndroidManifest.xml生成R.java文件;
2、處理aidl,生成對應的java文件,如果沒有aidl,則跳過;
3、將前兩步生成的java文件和src目錄下源碼一起編譯成class文件;
4、通過class文件生成成dex文件;
5、將資源文件和dex文件一起打包,生成初始apk;
6、對初始apk簽名 ;

由此可見,項目編譯後,主要結晶就是dex文件。apk的安裝過程,就是把apk解壓成第4步中的dex文件和原始資源文件(比如圖片),運行過程就是dalvik加載、運行dex文件的過程。這裏有兩個過程,一個是加載,一個是運行,它們又是怎樣運作的呢?

dex文件的加載是通過DexClassLoader、PathClassLoader等類來完成的,下面將會從源碼角度對這個過程詳細分析,這也是熱修復技術、插件化技術的核心。

dex的運行就涉及到比較底層的東西了,本文只做一定介紹,瞭解一下dex文件的大概。

Dex文件

通過命令“javac HelloWorld.java”可以生成HelloWorld.class文件。
再通過命令“dx –dex –output=HelloWorld.dex HelloWorld.class”就會生成HelloWorld.dex文件了。

我們通過十六進制文本編輯器打開HelloWorld.dex文件,如下圖:

這裏寫圖片描述

注意看下面的“Name、Value”,這就是dex文件的標準格式。就像通信協議一樣,dalvik虛擬機讀到什麼內容,就按照預定好的協議執行,這就是dalvik運行dex文件的過程。

ClassLoader

先把我們需要分析的類列出來,捋一捋繼承關係、類的主要作用。

ClassLoader
所有XXXClassLoader的基類,負責加載apk/dex/jar文件;

BootClassLoader
繼承自ClassLoader,負責加載Android系統類庫,我們不會用到;

BaseDexClassLoader
繼承自ClassLoader,看名字就知道是對類加載的抽象;

PathClassLoader
繼承自BaseDexClassLoader,負責加載宿主apk/dex/jar文件;

DexClassLoader
繼承自BaseDexClassLoader,這個比較靈活,每個參數都可以自定義,我們一般用這個來加載自定義的apk/dex/jar文件;

DexPathList
這個類有兩個作用:
① 把dex文件解壓到宿主程序的私有目錄中,因爲jvm只能運行宿主程序的dex文件;
② 通過apk/dex/jar文件生成Element[]數組,方便ClassLoader使用;

由於BootClassLoader是加載系統類庫的,我們就不分析了。我們主要分析PathClassLoader加載apk中的dex文件,分析完這個過程,自定義一個ClassLoader來加載自定義dex文件就不成問題了。

1、PathClassLoader
先看構造方法:

public class PathClassLoader extends BaseDexClassLoader {
    public PathClassLoader(String dexPath, ClassLoader parent) {
        // 調用父類的構造方法
        super(dexPath, null, null, parent);
    }

    public PathClassLoader(String dexPath, String libraryPath,
            ClassLoader parent) {
        // 調用父類的構造方法
        super(dexPath, null, libraryPath, parent);
    }
}

// 參數dexPath:待加載的apk/dex/jar文件路徑;
// 參數optimizedDirectory:dex的輸出路徑,將apk/dex/jar解壓出dex文件,複製到指定路徑,用於dalvik運行
// 參數librarySearchPath:加載時候需要用到的lib庫,這個一般不用,可以傳入Null
// 參數parent:指定父加載器
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
                String libraryPath, ClassLoader parent) {
    super(parent);
    this.originalPath = dexPath;
    this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}

這裏有兩個“疑點”,一是PathClassLoader是無法指定optimizedDirectory參數的,也就是說,無法保證解壓出來的dex文件在宿主程序中,dalvik就無法運行。另一個就是new一個對象還必須傳一個父類對象作爲參數,爲什麼呢?下面分析loadClass()方法時再說明。

我們再看一下加載的方法,加載方法在基類ClassLoader中:

// 通過類名加載
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
    // 在已加載的類中查找
    Class<?> clazz = findLoadedClass(className);

    if (clazz == null) {
        ClassNotFoundException suppressed = null;
        try {
            // 如果沒有,就讓parent去加載
            clazz = parent.loadClass(className, false);
        } catch (ClassNotFoundException e) {
            suppressed = e;
        }

        if (clazz == null) {
            try {
                // 如果parent也沒有加載到,就自己加載
                clazz = findClass(className);
            } catch (ClassNotFoundException e) {
                e.addSuppressed(suppressed);
                throw e;
            }
        }
    }

    return clazz;
}

邏輯是這樣的:

1、先去已加載的列表中查找,如果有(已經加載過),就直接返回;如果沒有,就讓parent去加載;
2、父類仍舊是調用基類ClassLoader的loadClass()方法(如果parent沒有重寫該方法,一般parent都會傳系統自帶的類,甚至是基類,所以基本不存在被重寫的情況),等於是把第一步重複一次;
3、一直找到最頂層的parent,如果頂層parent在已加載列表中還是沒有找到,就會調用findClass()進行加載,並返回;
4、通過parent一層一層地返回,如果最終還是沒有(所有parent都沒有加載到),就自己進行加載;

這樣設計的邏輯就是防止多次加載,一個類只永遠只會被加載一次。

另外要注意的是,只有“PackageName + ClassName + 加載它的ClassLoader”這三個元素一致,才認爲它是同一個類。所以,指定系統的parent能最大限度地保證類的一致性。

上面的邏輯中可以看到,如果沒有加載過,就會調用findClass()方法進行加載,BaseDexClassLoader重載了這個方法:

@Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        Class clazz = pathList.findClass(name);
        if (clazz == null) {
            throw new ClassNotFoundException(name);
        }
        return clazz;
}

最終調用了DexPathList的findClass()方法,那我們分析一下DexPathList的主要邏輯:

// 構造方法,BaseDexClassLoader的構造方法中會new出DexPathList實例
public DexPathList(ClassLoader definingContext, String dexPath,
            String libraryPath, File optimizedDirectory) {
    ……
    // 通過dexPath路徑下的apk/jar/dex文件解壓到optimizedDirectory目錄中,並生成Elements[]數組
    this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory);
}

private static Element[] makeDexElements(ArrayList<File> files,
            File optimizedDirectory) {
    ArrayList<Element> elements = new ArrayList<Element>();
    // 遍歷該路徑下的文件
    for (File file : files) {
        ZipFile zip = null;
        DexFile dex = null;
        String name = file.getName();
        if (name.endsWith(DEX_SUFFIX)) {
            // 如果是dex文件,就加載該文件
            dex = loadDexFile(file, optimizedDirectory);
        } else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX)
                    || name.endsWith(ZIP_SUFFIX)) {
            // 如果是壓縮文件,就生成ZipFile
            zip = new ZipFile(file);
        } 
        ……
        if ((zip != null) || (dex != null)) {
            // 通過上面生成的DexFile或ZipFile,生成Element對象,添加到List中
            elements.add(new Element(file, zip, dex));
        }
    }

    // List轉換成數組返回
    return elements.toArray(new Element[elements.size()]);
}

// 通過File生成DexFile
private static DexFile loadDexFile(File file, File optimizedDirectory)
            throws IOException {
    if (optimizedDirectory == null) {
        // 如果輸出路徑爲空,就會使用默認路徑(當前File所在路徑)
        return new DexFile(file);
    } else {
        // 生成解壓路徑
        String optimizedPath = optimizedPathFor(file, optimizedDirectory);
        return DexFile.loadDex(file.getPath(), optimizedPath, 0);
    }
}

/**
 * Converts a dex/jar file path and an output directory to an
 * output file path for an associated optimized dex file.
 */
private static String optimizedPathFor(File path,
            File optimizedDirectory) {
    String fileName = path.getName();
    if (!fileName.endsWith(DEX_SUFFIX)) {
        int lastDot = fileName.lastIndexOf(".");
        if (lastDot < 0) {
            fileName += DEX_SUFFIX;
        } else {
            StringBuilder sb = new StringBuilder(lastDot + 4);
            sb.append(fileName, 0, lastDot);
            sb.append(DEX_SUFFIX);
            fileName = sb.toString();
        }
    }
    File result = new File(optimizedDirectory, fileName);
    return result.getPath();
}

// 根據name查找dex,將其轉換成Class,返回給ClassLoader
public Class findClass(String name) {
    // 遍歷Element[]數組
    for (Element element : dexElements) {
        DexFile dex = element.dexFile;
        if (dex != null) {
            // 通過name嘗試加載Class
            Class clazz = dex.loadClassBinaryName(name, definingContext);
            if (clazz != null) {
                // 如果加載成功,就返回
                return clazz;
            }
        }
    }
    return null;
}

分析到這裏ClassLoader的加載機制就完結了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章