Android類加載器ClassLoader

Java類加載器(ClassLoader)

Java中的ClassLoader是加載class文件,而Android中的虛擬機(jī)無(wú)論是dvm還是art都只能識(shí)別dex文件。

1 Android中的ClassLoader

因此Java中的ClassLoader在Android中不適用。Android中的java.lang.ClassLoader這個(gè)類也不同于Java中的java.lang.ClassLoader。
Android中的ClassLoader類型也可分為系統(tǒng)ClassLoader和自定義ClassLoader。
其中系統(tǒng)ClassLoader包括3種分別是:

  • BootClassLoader,Android系統(tǒng)啟動(dòng)時(shí)會(huì)使用BootClassLoader來(lái)預(yù)加載常用類,與Java中的Bootstrap ClassLoader不同的是,它并不是由C/C++代碼實(shí)現(xiàn),而是由Java實(shí)現(xiàn)的。BootClassLoader是ClassLoader的一個(gè)內(nèi)部類。
  • PathClassLoader,全名是dalvik/system.PathClassLoader,可以加載已經(jīng)安裝的Apk,也就是/data/app/package 下的apk文件,也可以加載/vendor/lib, /system/lib下的nativeLibrary。
  • DexClassLoader,全名是dalvik/system.DexClassLoader,可以加載一個(gè)未安裝的apk文件

PathClassLoader和DexClasLoader都是繼承自 dalviksystem.BaseDexClassLoader,它們的類加載邏輯全部寫在BaseDexClassLoader中。

Android中的ClassLoader中的繼承體系,其中SecureClassLoader和UrlClassLoader是在Java中的類加載器,在Android中是沒(méi)法辦使用的。

在MainActivity中打印當(dāng)前的ClassLoader

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ClassLoader classLoader = getClassLoader();
        while (classLoader != null) {
            System.out.println("classLoader: " + classLoader);
            classLoader = classLoader.getParent();
        }
    }
}

結(jié)果如下:

dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.sososeen09.classloadtest-1/base.apk"],nativeLibraryDirectories=[/vendor/lib, /system/lib]]]
java.lang.BootClassLoader@aced87d

從打印的結(jié)果也可以證實(shí):App系統(tǒng)類加載器是PathClassLoader,而BootClassLoader是其parent類加載器。


2083810-64e93a71d99c1459.png

2 ClassLoader源碼分析

在Android中我們主要關(guān)心的是PathClassLoader和DexClassLoader。

2.1 PathClassLoader

PathClassLoader用來(lái)操作本地文件系統(tǒng)中的文件和目錄的集合。并不會(huì)加載來(lái)源于網(wǎng)絡(luò)中的類。Android采用這個(gè)類加載器一般是用于加載系統(tǒng)類和它自己的應(yīng)用類。這個(gè)應(yīng)用類放置在data/data/包名下。

package dalvik.system;

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);
    }
}

2.2 DexClassLoader

DexClassLoader可以加載一個(gè)未安裝的APK,也可以加載其它包含dex文件的JAR/ZIP類型的文件
DexClassLoader需要一個(gè)對(duì)應(yīng)用私有且可讀寫的文件夾來(lái)緩存優(yōu)化后的class文件。而且一定要注意不要把優(yōu)化后的文件存放到外部存儲(chǔ)上,避免使自己的應(yīng)用遭受代碼注入攻擊。

package dalvik.system;
import java.io.File;
public class DexClassLoader extends BaseDexClassLoader {
    public DexClassLoader(String dexPath, String optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), libraryPath, parent);
    }
}

PathClassLoader和DexClassLoader除了構(gòu)造方法傳參不同,其它的邏輯都是一樣的。要注意的是DexClassLoader構(gòu)造方法第2個(gè)參數(shù)指的是dex優(yōu)化緩存路徑,這個(gè)值是不能為空的。而PathClassLoader對(duì)應(yīng)的dex優(yōu)化緩存路徑為null是因?yàn)锳ndroid系統(tǒng)自己決定了緩存路徑。

2.3 BaseDexClassLoader

BaseDexClassLoader的構(gòu)造方法有四個(gè)參數(shù):

  • dexPath,指的是在Androdi包含類和資源的jar/apk類型的文件集合,指的是包含dex文件。多個(gè)文件用“:”分隔開(kāi),用代碼就是File.pathSeparator。
  • optimizedDirectory,指的是odex優(yōu)化文件存放的路徑,可以為null,那么就采用默認(rèn)的系統(tǒng)路徑。
  • libraryPath,指的是native庫(kù)文件存放目錄,也是以“:”分隔。
  • parent,parent類加載器

可以看到,在BaseDexClassLoader類中初始化了DexPathList這個(gè)類的對(duì)象。這個(gè)類的作用是存放指明包含dex文件、native庫(kù)和優(yōu)化目錄。

# dalvik.system.BaseDexClassLoader
    public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }

dalvik.system.DexPathList封裝了dex路徑,是一個(gè)final類,而且訪問(wèn)權(quán)限是包權(quán)限,也就是說(shuō)外界不可繼承,也不可訪問(wèn)這個(gè)類。

2.4 DexPathList

BaseDexClassLoader在其構(gòu)造方法中初始化了DexPathList對(duì)象,我們來(lái)看一下DexPathList的源碼,我們需要重點(diǎn)關(guān)注一下它的成員變量dexElements,它是一個(gè)Element[]數(shù)組,是包含dex的文件集合。

Element是DexPathList的一個(gè)靜態(tài)內(nèi)部類。DexPathList的構(gòu)造方法有4個(gè)參數(shù)。從其構(gòu)造方法中也可以看到傳遞過(guò)來(lái)的classLoader對(duì)象和dexPath不能為null,否則就拋出空指針異常。

# dalvik.system.DexPathList

private final Element[] dexElements;

public DexPathList(ClassLoader definingContext, String dexPath,
        String libraryPath, File optimizedDirectory) {
    if (definingContext == null) {
        throw new NullPointerException("definingContext == null");
    }

    if (dexPath == null) {
        throw new NullPointerException("dexPath == null");
    }

    if (optimizedDirectory != null) {
        if (!optimizedDirectory.exists())  {
            throw new IllegalArgumentException(
                    "optimizedDirectory doesn't exist: "
                    + optimizedDirectory);
        }
        // 如果文件不是可讀可寫的也會(huì)拋出異常
        if (!(optimizedDirectory.canRead()
                        && optimizedDirectory.canWrite())) {
            throw new IllegalArgumentException(
                    "optimizedDirectory not readable/writable: "
                    + optimizedDirectory);
        }
    }

    this.definingContext = definingContext;
    ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
    // 通過(guò)makeDexElements方法來(lái)獲取Element數(shù)組
    // splitDexPath(dexPath)方法是用來(lái)把我們之前按照“:”分隔的路徑轉(zhuǎn)為File集合。
    this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                       suppressedExceptions);
    if (suppressedExceptions.size() > 0) {
        this.dexElementsSuppressedExceptions =
            suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
    } else {
        dexElementsSuppressedExceptions = null;
    }
    this.nativeLibraryDirectories = splitLibraryPath(libraryPath);
}

makeDexElements方法的作用是獲取一個(gè)包含dex文件的元素集合。

# dalvik.system.DexPathList
private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory,
                                         ArrayList<IOException> suppressedExceptions) {
    ArrayList<Element> elements = new ArrayList<Element>();

    // 遍歷打開(kāi)所有的文件并且加載直接或者間接包含dex的文件。
    for (File file : files) {
        File zip = null;
        DexFile dex = null;
        String name = file.getName();

        if (file.isDirectory()) {
            // We support directories for looking up resources.
            // This is only useful for running libcore tests.
            // 可以發(fā)現(xiàn)它是支持傳遞目錄的,但是說(shuō)只測(cè)試libCore的時(shí)候有用
            elements.add(new Element(file, true, null, null));
        } else if (file.isFile()){
            // 如果文件名后綴是.dex,說(shuō)明是原始dex文件
            if (name.endsWith(DEX_SUFFIX)) {
                // Raw dex file (not inside a zip/jar).
                try {
                    //調(diào)用loadDexFile()方法,加載dex文件,獲得DexFile對(duì)象
                    dex = loadDexFile(file, optimizedDirectory);
                } catch (IOException ex) {
                    System.logE("Unable to load dex file: " + file, ex);
                }
            } else {
                // dex文件包含在其它文件中
                zip = file;

                try {
                    // 同樣調(diào)用loadDexFile()方法
                    dex = loadDexFile(file, optimizedDirectory);
                } catch (IOException suppressed) {
                    // 和加載純dex文件不同的是,會(huì)把異常添加到異常集合中
                    /*
                     * 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 {
            System.logW("ClassLoader referenced unknown path: " + file);
        }

        // 如果zip或者dex二者一直不為null,就把元素添加進(jìn)來(lái)
        // 注意,現(xiàn)在添加進(jìn)來(lái)的zip存在不為null也不包含dex文件的可能。
        if ((zip != null) || (dex != null)) {
            elements.add(new Element(file, false, zip, dex));
        }
    }

    return elements.toArray(new Element[elements.size()]);
}

通過(guò)上面的代碼也可以看到,加載一個(gè)dex文件調(diào)用的是loadDexFile()方法。

# dalvik.system.DexPathList
private static DexFile loadDexFile(File file, File optimizedDirectory)
        throws IOException {
    // 如果緩存存放目錄為null就直接創(chuàng)建一個(gè)DexFile對(duì)象返回
    if (optimizedDirectory == null) {
        return new DexFile(file);
    } else {
        // 根據(jù)緩存存放目錄和文件名得到一個(gè)優(yōu)化后的緩存文件路徑
        String optimizedPath = optimizedPathFor(file, optimizedDirectory);
        // 調(diào)用DexFile的loadDex()方法來(lái)獲取DexFile對(duì)象。
        return DexFile.loadDex(file.getPath(), optimizedPath, 0);
    }
}

DexFile的loadDex()方法如下,內(nèi)部也做了一些調(diào)用。拋開(kāi)這些細(xì)節(jié)來(lái)講,它的作用就是加載DexFile文件,而且會(huì)把優(yōu)化后的dex文件緩存到對(duì)應(yīng)目錄

# dalvik.system.DexFile 
static public DexFile loadDex(String sourcePathName, String outputPathName,
    int flags) throws IOException {

    /*
     * TODO: we may want to cache previously-opened DexFile objects.
     * The cache would be synchronized with close().  This would help
     * us avoid mapping the same DEX more than once when an app
     * decided to open it multiple times.  In practice this may not
     * be a real issue.
     */
    //loadDex方法內(nèi)部就是調(diào)用了DexFile的一個(gè)構(gòu)造方法
    return new DexFile(sourcePathName, outputPathName, flags);
}

private DexFile(String sourceName, String outputName, int flags) throws IOException {
    if (outputName != null) {
        try {
            String parent = new File(outputName).getParent();
            if (Libcore.os.getuid() != Libcore.os.stat(parent).st_uid) {
                throw new IllegalArgumentException("Optimized data directory " + parent
                        + " is not owned by the current user. Shared storage cannot protect"
                        + " your application from code injection attacks.");
            }
        } catch (ErrnoException ignored) {
            // assume we'll fail with a more contextual error later
        }
    }

    mCookie = openDexFile(sourceName, outputName, flags);
    mFileName = sourceName;
    guard.open("close");
    //System.out.println("DEX FILE cookie is " + mCookie + " sourceName=" + sourceName + " outputName=" + outputName);
}

private static long openDexFile(String sourceName, String outputName, int flags) throws IOException {
    // Use absolute paths to enable the use of relative paths when testing on host.
    return openDexFileNative(new File(sourceName).getAbsolutePath(),
                             (outputName == null) ? null : new File(outputName).getAbsolutePath(),
                             flags);
}

private static native long openDexFileNative(String sourceName, String outputName, int flags);

分析到這,我們可以小結(jié)一下:在BaseDexClassLoader對(duì)象構(gòu)造方法內(nèi),創(chuàng)建了PathDexList對(duì)象。而在PathDexList構(gòu)造方法內(nèi)部,通過(guò)調(diào)用一系列方法,把直接包含或者間接包含dex的文件解壓縮并緩存優(yōu)化后的dex文件,通過(guò)PathDexList的成員變量 Element[] dexElements來(lái)指向這個(gè)文件。

2.5 ClassLoader的loadeClass()

Java類加載器,類加載是按需加載,也就是說(shuō)當(dāng)明確需要使用class文件的時(shí)候才會(huì)加載。
在Android中ClassLoader的loadeClass()方法。
與在Java中的loadClass()方法主要流程是類似的,不過(guò)因?yàn)锳ndroid中BootClassLoader是用Java代碼寫的,所以可以直接當(dāng)作系統(tǒng)類加載器的parent類加載器。

在Android中如果parent類加載器找不到類,最終還是會(huì)調(diào)用ClassLoader對(duì)象自己的findClass()方法

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;
}

我們可以去看一下BaseDexClassLoader類的findClass()方法。

# dalvik.system.BaseDexClassLoader
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
    List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
    // 調(diào)用DexPathList對(duì)象的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;
}

可以看到,實(shí)際上BaseDexClassLoader調(diào)用的是其成員變量DexPathList pathList的findClass()方法。

# dalvik.system.DexPathList
public Class findClass(String name, List<Throwable> suppressed) {
    // 遍歷Element
    for (Element element : dexElements) {
        // 獲取DexFile,然后調(diào)用DexFile對(duì)象的loadClassBinaryName()方法來(lái)加載Class文件。
        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;
}

從上面的代碼中我們也可以看到,實(shí)際上DexPathList最終還是遍歷其自身的Element[]數(shù)組,獲取DexFile對(duì)象來(lái)加載Class文件。我們之前講DexPathList構(gòu)造方法內(nèi)是調(diào)用其makeDexElements()方法來(lái)創(chuàng)建Element[]數(shù)組的,而且也提到了如果zip文件或者dex文件二者之一不為null,就把元素添加進(jìn)來(lái),而添加進(jìn)來(lái)的zip存在不為null也不包含dex文件的可能。從上面的代碼中也可以看到,獲取Class的時(shí)候跟這個(gè)zip文件沒(méi)什么關(guān)系,調(diào)用的是dex文件對(duì)應(yīng)的DexFile的方法來(lái)獲取Class。

數(shù)組的遍歷是有序的,假設(shè)有兩個(gè)dex文件存放了二進(jìn)制名稱相同的Class,類加載器肯定就會(huì)加載在放在數(shù)組前面的dex文件中的Class。現(xiàn)在很多熱修復(fù)技術(shù)就是把修復(fù)的dex文件放在DexPathList中Element[]數(shù)組的前面,這樣就實(shí)現(xiàn)了修復(fù)后的Class搶先加載了,達(dá)到了修改bug的目的

2.6 DexFile的defineClass()

Android加載一個(gè)Class是調(diào)用DexFile的defineClass()方法。而不是調(diào)用ClassLoader的defineClass()方法。這一點(diǎn)與Java不同,畢竟Android虛擬機(jī)加載的dex文件,而不是class文件。

# dalvik.system.DexFile
public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
    return defineClass(name, loader, mCookie, suppressed);
}

private static Class defineClass(String name, ClassLoader loader, long cookie,
                                 List<Throwable> suppressed) {
    Class result = null;
    try {
        result = defineClassNative(name, loader, cookie);
    } catch (NoClassDefFoundError e) {
        if (suppressed != null) {
            suppressed.add(e);
        }
    } catch (ClassNotFoundException e) {
        if (suppressed != null) {
            suppressed.add(e);
        }
    }
    return result;
}

在Android中ClassLoader的defineClass()方法已經(jīng)不能用了。可以看到它的方法體里直接拋出異常了,而且在BaseDexClassLoader中也沒(méi)有重寫這個(gè)方法,畢竟BaseDexClassLoader加載類的邏輯已經(jīng)變了。

# java.lang.ClassLoader
protected final Class<?> defineClass(String className, byte[] classRep, int offset, int length,
        ProtectionDomain protectionDomain) throws java.lang.ClassFormatError {
    throw new UnsupportedOperationException("can't load this type of class file");
}

Android中加載一個(gè)類是遍歷PathDexList的Element[]數(shù)組,這個(gè)Element包含了DexFile,調(diào)用DexFile的方法來(lái)獲取Class文件,如果獲取到了Class,就跳出循環(huán)。否則就在下一個(gè)Element中尋找Class。

4使用DexClassLoader加載類

寫一個(gè)Test類

class Test {
    public Test() {
    }

    public void print() {
        System.out.println("this is Test Class");
    }
}

通過(guò)javac命令生成class文件,然后通過(guò)Android中的dex工具生成dex文件。
為了方便測(cè)試,我把生成的dex文件放在了assets文件中。


2083810-9a448c7910a74af2.png

測(cè)試的時(shí)候,先把a(bǔ)ssets中的classes.dex文件復(fù)制到本地一個(gè)目錄。
主要代碼如下:

// 1. 先把a(bǔ)ssets中的classes.dex文件復(fù)制到一個(gè)本地目錄中
File originDex = null;
try {
    InputStream open = getAssets().open("classes.dex");
    File dexOutputDir = getCacheDir();
    originDex = new File(dexOutputDir, "classes.dex");
    FileOutputStream fileOutputStream = new FileOutputStream(originDex);
    byte[] bytes = new byte[1024];
    int len = 0;
    while ((len = open.read(bytes)) != -1) {
        fileOutputStream.write(bytes, 0, len);
    }
    fileOutputStream.close();
    open.close();
} catch (IOException e) {
    e.printStackTrace();
}

// 2. 創(chuàng)建DexClassLoader加載dex文件中的類
if (originDex != null) {
    File dexOptimizeDir = getDir("dex", Context.MODE_PRIVATE);
    String dexOutputPath = dexOptimizeDir.getAbsolutePath();
    DexClassLoader dexClassLoader = new DexClassLoader(originDex.getAbsolutePath(), dexOutputPath, null, getClassLoader());

    try {
        Class<?> clazz = dexClassLoader.loadClass("com.sososeen09.Test");
        System.out.println("loaded class: " + clazz);
        System.out.println("class loader: " + clazz.getClassLoader());
        System.out.println("class loader parent: " + clazz.getClassLoader().getParent());
        Constructor constructor = clazz.getConstructor();
        constructor.setAccessible(true);
        Object o = constructor.newInstance();
        Method print = clazz.getDeclaredMethod("print");
        print.setAccessible(true);
        print.invoke(o);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

打印結(jié)果:

I/System.out: loaded class: class com.sososeen09.Test
I/System.out: class loader: dalvik.system.DexClassLoader[DexPathList[[dex file "/data/data/com.sososeen09.android_basic_knowledge/cache/classes.dex"],nativeLibraryDirectories=[/vendor/lib, /system/lib]]]
I/System.out: class loader parent: dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.sososeen09.android_basic_knowledge-2/base.apk", zip file "/data/app/com.sososeen09.android_basic_knowledge-2/split_lib_dependencies_apk.apk", zip file "/data/app/com.sososeen09.android_basic_knowledge-2/split_lib_slice_0_apk.apk", zip file "/data/app/com.sososeen09.android_basic_knowledge-2/split_lib_slice_1_apk.apk", zip file "/data/app/com.sososeen09.android_basic_knowledge-2/split_lib_slice_2_apk.apk", zip file "/data/app/com.sososeen09.android_basic_knowledge-2/split_lib_slice_3_apk.apk", zip file "/data/app/com.sososeen09.android_basic_knowledge-2/split_lib_slice_4_apk.apk", zip file "/data/app/com.sososeen09.android_basic_knowledge-2/split_lib_slice_5_apk.apk", zip file "/data/app/com.sososeen09.android_basic_knowledge-2/split_lib_slice_6_apk.apk", zip file "/data/app/com.sososeen09.android_basic_knowledge-2/split_lib_slice_7_apk.apk", zip file "/data/app/com.sososeen09.android_basic_knowledge-2/split_lib_slice_8_apk.apk", zip file "/data/app/com.sososeen09.android_basic_knowledge-2/split_lib_slice_9_apk.apk"],nativeLibraryDirectories=[/vendor/lib, /system/lib]]]
I/System.out: this is Test Class

可以看到我們通過(guò)DexClassLoader對(duì)象正確的加載到了我們自己的dex文件中的類。

4. 總結(jié)

Android中的類加載器是BootClassLoader、PathClassLoader、DexClassLoader,其中BootClassLoader是虛擬機(jī)加載系統(tǒng)類需要用到的,PathClassLoader是App加載自身dex文件中的類用到的,DexClassLoader可以加載直接或間接包含dex文件的文件,如APK等。

PathClassLoader和DexClassLoader都繼承自BaseDexClassLoader,它的一個(gè)DexPathList類型的成員變量pathList很重要。DexPathList中有一個(gè)Element類型的數(shù)組dexElements,這個(gè)數(shù)組中存放了包含dex文件(對(duì)應(yīng)的是DexFile)的元素。BaseDexClassLoader加載一個(gè)類,最后調(diào)用的是DexFile的方法進(jìn)行加載的。

無(wú)論是熱修復(fù)還是插件化技術(shù)中都利用了類加載機(jī)制,所以深入理解Android中的類加載機(jī)制對(duì)于理解這些技術(shù)的原理很有幫助。

參考

Android類加載器ClassLoader
類加載機(jī)制系列2——深入理解Android中的類加載器

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容