Android溫故而知新 - ClassLoader

安卓插件化越來(lái)越流行,其中用到的技術(shù)不外乎加載外部的資源和加載外部的代碼,關(guān)于加載外部資源我之前寫過(guò)一篇文章《安卓皮膚包機(jī)制的原理》,感興趣的同學(xué)可以去看一下。

加載外部代碼的作用在于熱更新。程序主體定義接口,具體實(shí)現(xiàn)放在外部。只需要替換外部代碼,就能修復(fù)bug甚至是更新功能。相比傳統(tǒng)的ota手段更加省流量,用戶體驗(yàn)也更加的好,畢竟有很多的用戶是不喜歡更新的。

這篇文章我想復(fù)習(xí)一下ClassLoader的相關(guān)知識(shí),它是加載外部代碼的核心原理。

雖然android自己實(shí)現(xiàn)了一個(gè)特殊的虛擬機(jī),它的類加載機(jī)制和普通的java程序有點(diǎn)區(qū)別。但是我還是想從普通的java程序講起,一方面多知道點(diǎn)東西總是好的,另一方面它們的基本原理是一樣的,對(duì)我們理解安卓的類加載機(jī)制也有很大的幫助。

普通java程序的類加載機(jī)制

我們都知道java代碼需要先編譯成class文件才能被jvm加載運(yùn)行。那jvm又是如何加載class文件的呢?

其實(shí)class文件是通過(guò)ClassLoader加載到j(luò)vm的。java自帶了三個(gè)ClassLoader,分別是:

  • BootstrapClassLoader 用于加載核心類庫(kù)
  • ExtClassLoader 用于加載拓展庫(kù)
  • AppClassLoader 用于加載當(dāng)前應(yīng)用的類

然后需要說(shuō)明的是java類不是一次性全部加載的,而是只有在用到的時(shí)候才會(huì)去加載。

因?yàn)槿考虞d的話會(huì)加載一些沒(méi)有用到的類,造成資源的浪費(fèi)。所以當(dāng)程序需要用到某個(gè)類時(shí),才會(huì)通過(guò)ClassLoader在系統(tǒng)的特定路徑搜索這個(gè)類的class文件并將它加載到j(luò)vm去執(zhí)行。

ExtClassLoader和AppClassLoader都是URLClassLoader的子類,他們內(nèi)部保存了URL列表用于指定搜索路徑。我們可以通過(guò)URLClassLoader.getURLs()方法獲取到這個(gè)URL列表。

BootstrapClassLoader雖然不是URLClassLoader的子類,但我們也可以從sun.misc.Launcher.getBootstrapClassPath().getURLs()方法獲取到BootstrapClassLoader的搜索路徑。

下面的代碼打印了各個(gè)ClassLoader的搜索路徑:

import java.net.URL;
import java.net.URLClassLoader;

public class ClassLoaderURLs {
    public static void main(String[] args) {
        System.out.println("BootstrapClassLoader urls :");
        URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
        for (URL url : urls) {
            System.out.println(url);
        }

        URLClassLoader extClassLoader = (URLClassLoader) ClassLoader.getSystemClassLoader().getParent();
        System.out.println("\n" + extClassLoader + " urls :");
        urls = extClassLoader.getURLs();
        for (URL url : urls) {
            System.out.println(url);
        }

        URLClassLoader appClassLoader = (URLClassLoader) ClassLoader.getSystemClassLoader();
        System.out.println("\n" + appClassLoader + " urls :");
        urls = appClassLoader.getURLs();
        for (URL url : urls) {
            System.out.println(url);
        }
    }
}

打印如下:

BootstrapClassLoader urls :
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_40.jdk/Contents/Home/jre/lib/resources.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_40.jdk/Contents/Home/jre/lib/rt.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_40.jdk/Contents/Home/jre/lib/sunrsasign.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_40.jdk/Contents/Home/jre/lib/jsse.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_40.jdk/Contents/Home/jre/lib/jce.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_40.jdk/Contents/Home/jre/lib/charsets.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_40.jdk/Contents/Home/jre/lib/jfr.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_40.jdk/Contents/Home/jre/classes

sun.misc.Launcher$ExtClassLoader@74a14482 urls :
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_40.jdk/Contents/Home/jre/lib/ext/cldrdata.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_40.jdk/Contents/Home/jre/lib/ext/dnsns.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_40.jdk/Contents/Home/jre/lib/ext/jfxrt.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_40.jdk/Contents/Home/jre/lib/ext/localedata.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_40.jdk/Contents/Home/jre/lib/ext/nashorn.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_40.jdk/Contents/Home/jre/lib/ext/sunec.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_40.jdk/Contents/Home/jre/lib/ext/sunjce_provider.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_40.jdk/Contents/Home/jre/lib/ext/sunpkcs11.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_40.jdk/Contents/Home/jre/lib/ext/zipfs.jar
file:/System/Library/Java/Extensions/AppleScriptEngine.jar
file:/System/Library/Java/Extensions/dns_sd.jar
file:/System/Library/Java/Extensions/j3daudio.jar
file:/System/Library/Java/Extensions/j3dcore.jar
file:/System/Library/Java/Extensions/j3dutils.jar
file:/System/Library/Java/Extensions/jai_codec.jar
file:/System/Library/Java/Extensions/jai_core.jar
file:/System/Library/Java/Extensions/libAppleScriptEngine.jnilib
file:/System/Library/Java/Extensions/libJ3D.jnilib
file:/System/Library/Java/Extensions/libJ3DAudio.jnilib
file:/System/Library/Java/Extensions/libJ3DUtils.jnilib
file:/System/Library/Java/Extensions/libmlib_jai.jnilib
file:/System/Library/Java/Extensions/mlibwrapper_jai.jar
file:/System/Library/Java/Extensions/MRJToolkit.jar
file:/System/Library/Java/Extensions/vecmath.jar
file:/usr/lib/java/libjdns_sd.jnilib

sun.misc.Launcher$AppClassLoader@28d93b30 urls :
file:/Users/linjw/workspace/class_loader_demo/

我們可以看到這些url有指向jar包的,也有指向一個(gè)目錄的(還有指向.jnilib文件的,這個(gè)我們可以不用管)。

ClassLoader從指定的路徑下搜索class文件。而jar包其實(shí)是一個(gè)壓縮包,將class文件打包在一起,所以ClassLoader也可以從jar包中搜索需要用到的class。

Java類的加載流程

ClassLoader的創(chuàng)建

我們先從ClassLoader的創(chuàng)建開(kāi)始說(shuō)起。我們可以直接看sun.misc.Launcher的源碼,它在構(gòu)造函數(shù)中創(chuàng)建了ExtClassLoader和AppClassLoader:

public Launcher() {
    // Create the extension class loader
    ClassLoader extcl;
    try {
        extcl = ExtClassLoader.getExtClassLoader();
    } catch (IOException e) {
        throw new InternalError(
            "Could not create extension class loader", e);
    }

    // Now create the class loader to use to launch the application
    try {
        loader = AppClassLoader.getAppClassLoader(extcl);
    } catch (IOException e) {
        throw new InternalError(
            "Could not create application class loader", e);
    }

    // Also set the context class loader for the primordial thread.
    Thread.currentThread().setContextClassLoader(loader);

    ...
}

ExtClassLoader.getExtClassLoader()是一個(gè)工廠方法:

public static ExtClassLoader getExtClassLoader() throws IOException
{
    final File[] dirs = getExtDirs();

    try {
        // Prior implementations of this doPrivileged() block supplied
        // aa synthesized ACC via a call to the private method
        // ExtClassLoader.getContext().

        return AccessController.doPrivileged(
            new PrivilegedExceptionAction<ExtClassLoader>() {
                public ExtClassLoader run() throws IOException {
                    int len = dirs.length;
                    for (int i = 0; i < len; i++) {
                        MetaIndex.registerDirectory(dirs[i]);
                    }
                    return new ExtClassLoader(dirs);
                }
            });
    } catch (java.security.PrivilegedActionException e) {
        throw (IOException) e.getException();
    }
}

AppClassLoader.getAppClassLoader(final ClassLoader extcl)也是一個(gè)工廠方法,它需要傳入一個(gè)ClassLoader作為AppClassLoader的父ClassLoader。而我們將ExtClassLoader傳了進(jìn)去,也就是說(shuō)ExtClassLoader是AppClassLoader的父ClassLoader:

public static ClassLoader getAppClassLoader(final ClassLoader extcl)
    throws IOException
{
    final String s = System.getProperty("java.class.path");
    final File[] path = (s == null) ? new File[0] : getClassPath(s);

    // Note: on bugid 4256530
    // Prior implementations of this doPrivileged() block supplied
    // a rather restrictive ACC via a call to the private method
    // AppClassLoader.getContext(). This proved overly restrictive
    // when loading  classes. Specifically it prevent
    // accessClassInPackage.sun.* grants from being honored.
    //
    return AccessController.doPrivileged(
        new PrivilegedAction<AppClassLoader>() {
            public AppClassLoader run() {
            URL[] urls =
                (s == null) ? new URL[0] : pathToURLs(path);
            return new AppClassLoader(urls, extcl);
        }
    });
}

每一個(gè)ClassLoader都有一個(gè)父ClassLoader,我們可以通過(guò)ClassLoader.getParent()方法獲取。同時(shí)我們也能使用Class.getClassLoader()獲取加載這個(gè)類的ClassLoader。所以讓我們來(lái)看看下面的代碼:

public class GetClassLoader {
    public static void main(String[] args) {
        ClassLoader loader = GetClassLoader.class.getClassLoader();
        do {
            System.out.println(loader);
        } while ((loader = loader.getParent()) != null);
    }
}

查看打印我們可以知道, GetClassLoader是AppClassLoader加載的,而AppClassLoader的父ClassLoader是ExtClassLoader:

sun.misc.Launcher$AppClassLoader@28d93b30
sun.misc.Launcher$ExtClassLoader@74a14482

但是如果我們查看String的ClassLoader又會(huì)發(fā)現(xiàn)它是null的:

public class GetClassLoader {
    public static void main(String[] args) {
        ClassLoader loader = String.class.getClassLoader();
        System.out.println("loader : " + loader);
    }
}
loader : null

那是不是說(shuō)String不是由ClassLoader加載的?當(dāng)然不是!其實(shí)String是BootstrapClassLoader加載的。BootstrapClassLoader負(fù)責(zé)加載java的核心類。

但是為什么String.class.getClassLoader()拿到的是null呢?

原因是BootstrapClassLoader實(shí)際上不是一個(gè)java類,它是由C/C++編寫的,它本身是虛擬機(jī)的一部分。所以在java中當(dāng)然沒(méi)有辦法獲取到它的引用。

雙親委托

相信大家如果知道ClassLoader的話應(yīng)該有聽(tīng)說(shuō)過(guò)雙親委托,那下面我們就來(lái)講一下雙親委托究竟是怎么一回事。

我們知道ClassLoader.loadClass()的方法可以加載一個(gè)類,所以研究一個(gè)類的加載流程,最好的方法當(dāng)然還是去看源碼啦:

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // 首先,從緩存中查詢?cè)擃愂遣皇潜患虞d過(guò),如果加載過(guò)就可以直接返回
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    //判斷它的父ClassLoader是否為空,如果不為空就調(diào)用父ClassLoader的loadClass方法去加載該類
                    c = parent.loadClass(name, false);
                } else {
                    //如果它的父ClassLoader為空,則調(diào)用BootstrapClassLoader去加載該類,所以此時(shí)從邏輯上來(lái)講BootstrapClassLoader是父ClassLoader
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                
            }

            if (c == null) {
                long t1 = System.nanoTime();
                
                //如果父ClassLoader不能加載該類才由自己去加載,這個(gè)方法從本ClassLoader的搜索路徑中查找該類
                c = findClass(name);
                
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

從代碼中我們可以看到,加載一個(gè)類的時(shí)候,ClassLoader先會(huì)讓父類去加載,如果父類加載失敗,才會(huì)由它自己去加載,這就是我們說(shuō)的雙親委托。

為什么類加載需要設(shè)計(jì)成雙親委托的方式呢?原因就在于雙親委托可以防止類被重復(fù)加載。如果父ClassLoader已經(jīng)加載過(guò)一個(gè)類了,子ClassLoader就不會(huì)再次加載,可以防止同一個(gè)類被兩個(gè)ClassLoader重復(fù)加載的問(wèn)題。

這里還需要說(shuō)的是,當(dāng)我們自定義一個(gè)ClassLoader的時(shí)候,最好將AppClassLoader設(shè)為父ClassLoader。這樣的話可以保證我們自定義的ClassLoader找加載類失敗的時(shí)候還能從父ClassLoader中加載這個(gè)類。

雙親委托模式的流程如下圖所示:

1.png

自定義ClassLoader

有時(shí)候我們可以繼承ClassLoader實(shí)現(xiàn)自己的類加載器。自定義ClassLoader有兩種方式:

  1. 重寫loadClass方法
  2. 重寫findClass方法

他們有什么區(qū)別呢,還記得上一級(jí)ClassLoader.loadClass()的源碼嗎?loadClass方法內(nèi)會(huì)先調(diào)用父ClassLoader的loadClass方法,如果父ClassLoader沒(méi)有加載過(guò)該類才會(huì)調(diào)用本ClassLoader的findClass方法去加載類。

所以如果想要打破雙親委托機(jī)制的話就可以loadClass(),而如果還想繼續(xù)沿用雙親委托機(jī)制的話就只需要重寫findClass就好了。

我們寫個(gè)小例子:

public class MyClassLoader extends ClassLoader {
    public String mClassDir;

    public MyClassLoader(String classDir) {
        this.mClassDir = classDir;
    }


    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        File file = new File(mClassDir, getClassFileName(name));
        if (file.exists()) {
            try {
                FileInputStream is = new FileInputStream(file);

                ByteArrayOutputStream buf = new ByteArrayOutputStream();
                int len;
                while ((len = is.read()) != -1) {
                    buf.write(len);
                }

                byte[] data = buf.toByteArray();
                is.close();
                buf.close();

                return defineClass(name, data, 0, data.length);
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        return super.findClass(name);
    }

    private String getClassFileName(String fullName) {
        int index = fullName.lastIndexOf(".");
        if (index == -1) {
            return fullName + ".class";
        } else {
            return fullName.substring(index + 1) + ".class";
        }
    }
}

因?yàn)槲覀儾恍枰蚱齐p親委托機(jī)制所以只需要重寫findClass方法就可以了。我們自定義的ClassLoader會(huì)從指定的路徑中搜索class文件,將它讀入內(nèi)存,然后通過(guò)調(diào)用ClassLoader.defineClass()方法去加載這個(gè)類。

我們?cè)?Users/linjw/workspace/class_loader_demo目錄下創(chuàng)建了一個(gè)Test.java:

package linjw.demo.classloader;
public class Test {
    public String getData() {
        return "Hello World";
    }
}

然后通過(guò)javac命令編譯出Test.class文件,同樣放在/Users/linjw/workspace/class_loader_demo目錄下。

然后用我們的MyClassLoader去加載它:

MyClassLoader loader = new MyClassLoader("/Users/linjw/workspace/class_loader_demo");
Class clazz = loader.loadClass("linjw.demo.classloader.Test");
if (clazz != null) {
    Object obj = clazz.newInstance();
    Method method = clazz.getDeclaredMethod("getData");
    String result = (String) method.invoke(obj);
    System.out.println(result);
    System.out.println("ClassLoader : " + clazz.getClassLoader());
} else {
    System.out.println("can't load class");
}

可以看到下面的打印,說(shuō)明我們已經(jīng)成功用MyClassLoader加載了Test這個(gè)類:

Hello World
ClassLoader : linjw.demo.classloader.MyClassLoader@66cd51c3

這里還有一個(gè)小的知識(shí)點(diǎn),如果一個(gè)類是由某個(gè)ClassLoader加載的,那么它import的類也會(huì)由這個(gè)ClassLoader去加載。這里我們可以做一個(gè)實(shí)驗(yàn):

package linjw.demo.classloader;

import linjw.demo.classloader.Test;

public class Test2 {
    public String getData(){
        return "Test ClassLoader : " + Test.class.getClassLoader();
    }
}

我們寫一個(gè)Test2類,它會(huì)import Test并返回Test的ClassLoader。讓我們寫個(gè)demo看看這個(gè)Test的ClassLoader:

MyClassLoader loader = new MyClassLoader("/Users/linjw/workspace/class_loader_demo");
Class clazz = loader.loadClass("linjw.demo.classloader.Test2");
if (clazz != null) {
    Object obj = clazz.newInstance();
    Method method = clazz.getDeclaredMethod("getData");
    String result = (String) method.invoke(obj);
    System.out.println(result);
} else {
    System.out.println("can't load class");
}

通過(guò)打印可以知道Test也是由MyClassLoader加載的:

linjw.demo.classloader.MyClassLoader@66cd51c3

Context ClassLoader

Context ClassLoader并不是一個(gè)實(shí)際的類,它只是Thread的一個(gè)成員變量:

public class Thread implements Runnable {
    private ClassLoader contextClassLoader;

    private void init2(Thread parent) {
        this.contextClassLoader = parent.getContextClassLoader();
        ...
    }

    public ClassLoader getContextClassLoader() {
        return contextClassLoader;
    }

    public void setContextClassLoader(ClassLoader cl) {
        contextClassLoader = cl;
    }
    
    ...
}

每個(gè)Thread都有一個(gè)相關(guān)聯(lián)的ClassLoader,子線程默認(rèn)使用父線程的ClassLoader。

而線程的默認(rèn)ClassLoader是AppClassLoader:

public Launcher() {
    ...
    
    try {
        loader = AppClassLoader.getAppClassLoader(extcl);
    } catch (IOException e) {
        throw new InternalError(
            "Could not create application class loader", e);
    }
    
    //設(shè)置AppClassLoader為當(dāng)前線程的Context ClassLoader
    Thread.currentThread().setContextClassLoader(loader);

    ...
}

Context ClassLoader的存在是為了解決使用雙親委托機(jī)制下父ClassLoader無(wú)法找到子ClassLoader的問(wèn)題。假如有下面的委托鏈:

ClassLoaderA -> AppClassLoader -> ExtClassLoader -> BootstrapClassLoader

那么委派鏈左邊的ClassLoader就可以很自然的使用右邊的ClassLoader所加載的類。

但如果是右邊的ClassLoader想要反過(guò)來(lái)使用左邊的ClassLoader所加載的類就無(wú)能為力了。

這個(gè)時(shí)候如果使用Context ClassLoader就能從線程中獲得左邊的ClassLoader了。

那什么時(shí)候會(huì)出現(xiàn)右邊的ClassLoader想要反過(guò)來(lái)使用左邊的ClassLoader所加載的類的情況呢?

我們上一節(jié)剛剛說(shuō)過(guò):“如果一個(gè)類是由某個(gè)ClassLoader加載的,那么它import的類也會(huì)由這個(gè)ClassLoader去加載”。

舉個(gè)例子,Java 提供了很多服務(wù)提供者接口(Service Provider Interface,SPI),允許第三方為這些接口提供實(shí)現(xiàn)。如JAXP(XML處理的Java API)的SPI__接口__定義包含在 javax.xml.parsers包中,它是由BootstrapClassLoader加載的。

但是它的實(shí)現(xiàn)代碼很可能是作為Java應(yīng)用所依賴的jar包被包含進(jìn)來(lái),如實(shí)現(xiàn)了JAXP SPI的Apache Xerces所包含的jar包,它由AppClassLoader加載。

我們用javax.xml.parsers.DocumentBuilderFactory類中的newInstance()方法用來(lái)生成一個(gè)新的DocumentBuilderFactory的實(shí)例, DocumentBuilderFactory是一個(gè)抽象類,它定是java核心庫(kù)的一部分,由BootstrapClassLoader去加載。因此,DocumentBuilderFactory里面import的類都由BootstrapClassLoader去加載。

但是DocumentBuilderFactory的實(shí)現(xiàn)類卻是在org.apache.xerces.jaxp.DocumentBuilderFactoryImpl中定義的, BootstrapClassLoader無(wú)法加載它。這個(gè)時(shí)候就需要在DocumentBuilderFactory. newInstance()的代碼中使用Context ClassLoader,找到AppClassLoader去加載DocumentBuilderFactoryImpl這個(gè)實(shí)現(xiàn)類。

安卓中的ClassLoader

安卓的的類也是通過(guò)ClassLoader加載的,但是并不是java中的BootstrapClassLoader、 ExtClassLoader或者AppClassLoader。寫個(gè)小demo看看安卓中加載類的是哪些ClassLoader:

Log.d("DxClassLoader", "BootClassLoader :" + String.class.getClassLoader());

ClassLoader loader = MainActivity.class.getClassLoader();
do {
    Log.d("DxClassLoader", "loader :" + loader);
} while ((loader = loader.getParent()) != null);

打印如下:

09-27 23:11:03.432 21151-21151/? D/DxClassLoader: BootClassLoader :java.lang.BootClassLoader@ad96016
09-27 23:11:03.432 21151-21151/? D/DxClassLoader: loader :dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/linjw.demo.classloader-2/base.apk"],nativeLibraryDirectories=[/data/app/linjw.demo.classloader-2/lib/arm64, /vendor/lib64, /system/lib64]]]
09-27 23:11:03.433 21151-21151/? D/DxClassLoader: loader :java.lang.BootClassLoader@ad96016

我們可以看到安卓中用的了PathClassLoader和BootClassLoader兩個(gè)ClassLoader,其中BootClassLoader是PathClassLoader的parent。

而和在java程序不同的是String是由BootClassLoader加載的。安卓的BootClassLoader其實(shí)就相當(dāng)于java的BootstrapClassLoader,只不過(guò)它是由java實(shí)現(xiàn)的而不是由c/c++實(shí)現(xiàn)的。

PathClassLoader

我們?cè)谏弦还?jié)中將PathClassLoader打印出來(lái)的時(shí)候可以看到一個(gè)apk路徑:

[zip file "/data/app/linjw.demo.classloader-2/base.apk"]

apk其實(shí)是一個(gè)也是一個(gè)zip壓縮包,我們可以將一個(gè)apk文件后綴改成.zip然后就可以直接解壓了。PathClassLoader的作用其實(shí)就是在這個(gè)zip包中加載dex文件,我們通過(guò)它甚至可以加載其他應(yīng)用的代碼,但它只能加載已安裝的應(yīng)用。

例如我們可以新建一個(gè)ext工程,它的包名為linjw.demo.classloader.ext,然后在里面創(chuàng)建Test類:

package linjw.demo.classloader.ext;

public class Test {
    public String getData() {
        return "Hello World";
    }
}

然后編譯出apk來(lái),并且安裝。之后就能從這個(gè)apk中加載出Test類了:

String path = null;
PackageManager pm = getPackageManager();
try {
    path = pm.getApplicationInfo("linjw.demo.classloader.ext", 0).sourceDir;
} catch (PackageManager.NameNotFoundException e) {
    e.printStackTrace();
}

PathClassLoader loader = new PathClassLoader(path, ClassLoader.getSystemClassLoader());

try {
    Class clazz = loader.loadClass("linjw.demo.classloader.ext.Test");

    if (clazz != null) {
        Object obj = clazz.newInstance();
        Method method = clazz.getDeclaredMethod("getData");
        String result = (String) method.invoke(obj);
        Log.d("DxClassLoader", result);
    } else {
        Log.d("DxClassLoader", "can't load class");
    }
} catch (ClassNotFoundException e) {
    e.printStackTrace();
} catch (NoSuchMethodException e) {
    e.printStackTrace();
} catch (InstantiationException e) {
    e.printStackTrace();
} catch (IllegalAccessException e) {
    e.printStackTrace();
} catch (InvocationTargetException e) {
    e.printStackTrace();
}

可以得到打印:

09-27 23:39:16.571 24077-24077/? D/DxClassLoader: Hello World

DexClassLoader

PathClassLoader只能加載已經(jīng)安裝的應(yīng)用里面的類,但是DexClassLoader卻能加載未安裝的應(yīng)用里面的類。例如我們將apk放到存儲(chǔ)卡目錄下而不去安裝它:

String dir = Environment.getExternalStorageDirectory().getAbsolutePath();
File apk = new File(dir, "Ext.apk");
File dexOutputDir = this.getDir("dex", 0);
DexClassLoader loader = new DexClassLoader(
        apk.getAbsolutePath(),
        dexOutputDir.getAbsolutePath(),
        null, getClassLoader());

try {
    Class clazz = loader.loadClass("linjw.demo.classloader.ext.Test");

    if (clazz != null) {
        Object obj = clazz.newInstance();
        Method method = clazz.getDeclaredMethod("getData");
        String result = (String) method.invoke(obj);
        Log.d("DxClassLoader", result);
    } else {
        Log.d("DxClassLoader", "can't load class");
    }
} catch (ClassNotFoundException e) {
    e.printStackTrace();
} catch (NoSuchMethodException e) {
    e.printStackTrace();
} catch (InstantiationException e) {
    e.printStackTrace();
} catch (IllegalAccessException e) {
    e.printStackTrace();
} catch (InvocationTargetException e) {
    e.printStackTrace();
}

同樣可以得到打印:

09-27 23:54:29.206 25472-25472/? D/DxClassLoader: Hello World

我們可以看到, DexClassLoader的構(gòu)造函數(shù)的參數(shù)比PathClassLoader的要多出一個(gè)optimizedDirectory:

public class DexClassLoader extends BaseDexClassLoader {
    public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
        super((String)null, (File)null, (String)null, (ClassLoader)null);
        throw new RuntimeException("Stub!");
    }
}
public class PathClassLoader extends BaseDexClassLoader {
    public PathClassLoader(String dexPath, ClassLoader parent) {
        super((String)null, (File)null, (String)null, (ClassLoader)null);
        throw new RuntimeException("Stub!");
    }

    public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
        super((String)null, (File)null, (String)null, (ClassLoader)null);
        throw new RuntimeException("Stub!");
    }
}

那這個(gè)optimizedDirectory到底有什么作用呢?其實(shí)optimizedDirectory是用來(lái)存放從apk中解壓出來(lái)的dex文件的。

DexClassLoader和PathClassLoader其實(shí)歸根結(jié)底都是通過(guò)DexFile這個(gè)類去加載的dex文件,并不是直接讀取的apk。因?yàn)槿绻看味夹枰鈮翰拍芗虞d代碼的話效率實(shí)在太低了。

DexClassLoader可以主動(dòng)解壓apk,所以可以加載未安裝的應(yīng)用中的代碼。但PathClassLoader不會(huì)主動(dòng)解壓apk,它是讀取的已經(jīng)安裝的apk在cache中存在緩存的dex文件,所以它只能加載已安裝應(yīng)用中的代碼。

生成dex文件

DexClassLoader和PathClassLoader最后都是加載的dex文件。所以我們可以直接將dex文件的路徑傳給他們?nèi)ゼ虞d。但dex文件又是個(gè)什么東西呢?

普通的java程序中,JVM虛擬機(jī)可以通過(guò)ClassLoader去加載jar到的加載類的目的。但是android使用的Dalvik/ART虛擬機(jī)不能直接加載jar包,需要把.jar文件優(yōu)化成.dex文件才能加載。所以實(shí)際上dex文件是優(yōu)化過(guò)的jar包。

我們可以用Android SDK提供的DX工具把.jar文件優(yōu)化成.dex文件。我們用之前的Test.java做例子,具體步驟如下:

1.使用javac命令編譯Test.java得到Test.class文件(我這邊的java環(huán)境是1.8的,如果不指定用1.7的話生成dex也會(huì)失敗,報(bào)com.android.dx.cf.iface.ParseException: bad class file magic (cafebabe) or version (0034.0000))

javac -source 1.7 -target 1.7 Test.java

2.將創(chuàng)建目錄子目錄linjw/demo/classloader/ext并將Test.class移動(dòng)到子目錄中(因?yàn)門est的package是linjw.demo.classloader.ext,所以要根據(jù)它生成同樣的目錄,要不然生成dex會(huì)失敗)

mkdir -p linjw/demo/classloader/ext
mv Test.class linjw/demo/classloader/ext

3.使用jar命令將linjw目錄打包成jar包

jar -cf Test.jar linjw

4.用dx工具將jar包優(yōu)化成dex包

/Users/linjw/androidsdk/android-sdk-macosx/build-tools/19.1.0/dx --dex --output=Test.dex Test.jar

動(dòng)態(tài)加載dex文件

然后我們就能將它放到存儲(chǔ)卡中用DexClassLoader或者PathClassLoader去加載了。

使用反射的反射加載


String dir = Environment.getExternalStorageDirectory().getAbsolutePath();
File dex = new File(dir, "Test.dex");
File dexOutputDir = this.getDir("dex", 0);

//使用PathClassLoader加載dex
//PathClassLoader loader = new PathClassLoader(dex.getAbsolutePath(), getClassLoader());

//使用DexClassLoader加載dex
DexClassLoader loader = new DexClassLoader(
        dex.getAbsolutePath(),
        dexOutputDir.getAbsolutePath(),
        null,
        getClassLoader());

try {
    Class clazz = loader.loadClass("linjw.demo.classloader.ext.Test");

    if (clazz != null) {
        Object obj = clazz.newInstance();
        Method method = clazz.getDeclaredMethod("getData");
        String result = (String) method.invoke(obj);
        Log.d("DxClassLoader", result);
    } else {
        Log.d("DxClassLoader", "can't load class");
    }
} catch (ClassNotFoundException e) {
    e.printStackTrace();
} catch (NoSuchMethodException e) {
    e.printStackTrace();
} catch (InstantiationException e) {
    e.printStackTrace();
} catch (IllegalAccessException e) {
    e.printStackTrace();
} catch (InvocationTargetException e) {
    e.printStackTrace();
}

使用接口的方式加載

或者我們也可以使用接口的方式:

1.添加ITest接口:

package linjw.demo.classloader.ext;

public interface ITest {
    String getData();
}

2.Test類實(shí)現(xiàn)ITest接口:

package linjw.demo.classloader.ext;

public class Test implements ITest {
    @Override
    public String getData() {
        return "Hello World";
    }
}

3.將它們一起打包到Test.dex

javac -source 1.7 -target 1.7 Test.java ITest.java

mkdir -p linjw/demo/classloader/ext

mv Test.class linjw/demo/classloader/ext

mv Test.class linjw/demo/classloader

jar -cf Test.jar linjw

/Users/linjw/androidsdk/android-sdk-macosx/build-tools/19.1.0/dx --dex --output=Test.dex Test.jar

4.在安卓項(xiàng)目中導(dǎo)入ITest接口并調(diào)整代碼:

String dir = Environment.getExternalStorageDirectory().getAbsolutePath();
File dex = new File(dir, "Test.dex");
File dexOutputDir = this.getDir("dex", 0);

//使用PathClassLoader加載dex
//PathClassLoader loader = new PathClassLoader(dex.getAbsolutePath(), getClassLoader());

//使用DexClassLoader加載dex
DexClassLoader loader = new DexClassLoader(
    dex.getAbsolutePath(),
    dexOutputDir.getAbsolutePath(),
    null,
    getClassLoader());

try {
Class clazz = loader.loadClass("linjw.demo.classloader.ext.Test");

if (clazz != null) {
    //注意這里,使用的是ITest
    ITest obj = (ITest) clazz.newInstance();
    String result = obj.getData();
    Log.d("DxClassLoader", result);
} else {
    Log.d("DxClassLoader", "can't load class");
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}

其實(shí)我比較推薦使用在程序主體中定義接口,加載外部實(shí)現(xiàn)代碼的這種方法。一方面它比反射的效率高,另一方面也比較容易閱讀。

最后編輯于
?著作權(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)容

  • 針對(duì)app線上修復(fù)技術(shù),目前有好幾種解決方案,開(kāi)源界往往一個(gè)方案會(huì)有好幾種實(shí)現(xiàn)。重復(fù)的實(shí)現(xiàn)會(huì)有造輪子之嫌,但分析解...
    石先閱讀 5,130評(píng)論 2 34
  • 本文僅為學(xué)習(xí)筆記;不是原創(chuàng)文章; 動(dòng)態(tài)加載的關(guān)鍵問(wèn)題ClassLoader機(jī)制ClassLoader概念:Java...
    shuixingge閱讀 2,472評(píng)論 0 6
  • 作者簡(jiǎn)介 原創(chuàng)微信公眾號(hào)郭霖 WeChat ID: guolin_blog 本篇是fank909的第四篇投稿,詳細(xì)...
    木木00閱讀 1,622評(píng)論 1 14
  • 0、前言 讀完本文,你將了解到: 一、為什么說(shuō)Jabalpur語(yǔ)言是跨平臺(tái)的 二、Java虛擬機(jī)啟動(dòng)、加載類過(guò)程分...
    vivi_wong閱讀 1,274評(píng)論 0 10
  • 晚上睡覺(jué)前,浩明把粉色小椅子擺在臥室門口。 浩明特意熬夜到很晚,他想看看這招有沒(méi)有用。但是不知怎的,慢慢的他覺(jué)得越...
    小杜打醋閱讀 258評(píng)論 0 0