Quantcast
Channel: CSDN博客移动开发推荐文章
Viewing all articles
Browse latest Browse all 5930

Android中ClassLoader源码解析之真的是你认为的ClassLoader

$
0
0

1.前言

首先,阅读本文章之前,需要了解java中的ClassLoader的基本原理,包括java中的三级ClassLoader机制以及ClassLoader的委托机制,否则下面的内容会不知道在讲什么。虽然Android中的ClassLoader也是遵循其委托机制,但是他没有遵循java的三级ClassLoader机制,而是自己造了一个,修改了java系统的代码,如果将两者混淆的话,在Android中使用ClassLoader的时候,你可能会遇到想不通的问题。(因为作者曾经就踩过坑。。。。。。)

首先,我们看下面一段代码,在Application的onCreate()中添加如下代码:
PathClassLoader classLoader = (PathClassLoader) getApplicationContext().getClassLoader();
Log.d("mytest", "classLoader : " + classLoader + "\n" +
            "parent : " + classLoader.getParent() + "\n" +
            "grandParent : " + classLoader.getParent().getParent() + "\n" +
            "system classloader : " + ClassLoader.getSystemClassLoader() + "\n" +
            "system parent : " + ClassLoader.getSystemClassLoader().getParent());

代码的执行结果,打印内容如下:
 classLoader : dalvik.system.PathClassLoader[dexPath=/data/app/com.gavin.demo2application-1.apk,libraryPath=/data/app-lib/com.gavin.demo2application-1]
 parent : java.lang.BootClassLoader@41099128
 grandParent : null
 system classloader : dalvik.system.PathClassLoader[dexPath=.,libraryPath=null]
 system parent : java.lang.BootClassLoader@41099128

看到上面的打印的内容,我们了解到在Android的项目中使用的ClassLoader是其自定义的PathClassLoader,最重要的一点是打印的dexPath,这个决定了在项目代码中要加载的类的位置(后面详细讲解)。
第二点奇怪的地方就是它的parent是 BootClassLoader,这个又是什么东西,后面详细讲解。
第三点调用ClassLoader.getSystemClassLoader()返回的是PathClassLoader,并且dexPath为. ,我们了解的java中的ClassLoader.getSystemClassLoader()返回的是加载classpath里面的class的ClassLoader,也就是java中的第三级ClassLoader,它是调用sun.misc.Launcher的getClassLoader()方法获取的,详细解析请自行查阅。
第四点它的parent也是BootClassLoader,看来这个必须要分析一下下,毕竟出镜率这么高。

2.Context.getClassLoader()返回的是PathClassLoader

首先,Android中可以使用的CLassLoader有PathClassLoader和DexClassLoader,PathClassLoader只能加载dex文件,我们安装apk之后会在/data/dalvik-cache目录下生产一个名为data@app@com.hujiang.xxx-1.apk@classes.dex的 ODEX 文件,而PathClassLoader要加载apk的时候会到这个文件夹下找对应的dex文件。(ODEX文件就是经过优化的dex文件,详细自行查阅),同时也是我们自己编写的项目中使用的ClassLoader。而DexClassLoader可以加载apk,dex,jar文件,就是被用来实现动态加载机制,加载一个外部的apk文件,实现完全解耦的模块式开发,现在的开源框架有DL(使用代理的方式,其实加载的不是插件中的类)和DroinPlugin(hook掉AMS和PMS实现);现在比较火的热修改也是和其有关系,比如AndFix(它是在运行时将java方法修改成native方法,然后修改调用这个方法的指针,指向修复的方法),nuwa(也就是qq空间实现基于dex分包,修改CLassLoader中的dexElements中的dex顺序实现)以及最新的美团的Robust(基于Android Studio的instance run的原理,为每个类创建代理类)。
关于上面所提的动态加载框架,热修复框架等等都会在后续的文章中进行分析。

上面说了那么多,现在开始分析PathClassLoader和DexCLassLoader的源码实现,他们都是继承BaseDexClassLoader,所有的实现都疯转在了这个类里面,先来看看PathClassLoader和DexClassLoader的源码.
PathClassLoader的代码:
public class PathClassLoader extends BaseDexClassLoader {

37    public PathClassLoader(String dexPath, ClassLoader parent) {
38        super(dexPath, null, null, parent);
39    }
40

63    public PathClassLoader(String dexPath, String libraryPath,
64            ClassLoader parent) {
65        super(dexPath, null, libraryPath, parent);
66    }
67}
68

DexClassLoader源码如下:
public class DexClassLoader extends BaseDexClassLoader {

55    public DexClassLoader(String dexPath, String optimizedDirectory,
56            String libraryPath, ClassLoader parent) {
57        super(dexPath, new File(optimizedDirectory), libraryPath, parent);
58    }
59}
可以看出他们仅仅是重写了构造函数,所以所有的实现都是在BaseDexClassLoader里面。
参数:dexPath:要加载的apk或者jar文件的路径,optimizedDirectory:从apk中解析出dex文件存储的路径,libraryPath:apk文件中类要使用的c/c++代码,parent:父装载器,也就是真正loadclass的装载器。

下面重点来了,分析BaseDexClassLoader的源码,首先构造函数如下:
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }
super()走的是ClassLoader的设置parent的ClassLoader的方式,重点是下面的PathList,他存储的是dex的集合,因为apk是可以dex分包,它里面含有一个DexElement的集合,每一个Element就对应一个dex文件。

DexPathList的构造函数的核心代码如下:
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                           suppressedExceptions);
makeDexElements()就是解析dex文件成对应的DexElement,代码如下:
private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory,
                                            ArrayList<IOException> suppressedExceptions) {
        ArrayList<Element> elements = new ArrayList<Element>();
209        /*
210         * Open all files and load the (direct or contained) dex files
211         * up front.
212         */
213        for (File file : files) {//遍历所有的dex文件
214            File zip = null;
215            DexFile dex = null;//这是核心的类,处理将dex文件转化成对应的DexFile对象
216            String name = file.getName();
217
218            if (name.endsWith(DEX_SUFFIX)) {//.dex结尾(针对PathClassLoader处理)
219                // Raw dex file (not inside a zip/jar).
220                try {
221                    dex = loadDexFile(file, optimizedDirectory);//核心方法,内部是调用的DexFile的loadDex()方法
222                } catch (IOException ex) {
223                    System.logE("Unable to load dex file: " + file, ex);
224                }
225            } else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX)
226                    || name.endsWith(ZIP_SUFFIX)) {//.dex .jar .apk 结尾 (针对DexClassLoader处理)
227                zip = file;
228
229                try {
230                    dex = loadDexFile(file, optimizedDirectory);
231                } catch (IOException suppressed) {
232                    /*
233                     * IOException might get thrown "legitimately" by the DexFile constructor if the
234                     * zip file turns out to be resource-only (that is, no classes.dex file in it).
235                     * Let dex == null and hang on to the exception to add to the tea-leaves for
236                     * when findClass returns null.
237                     */
238                    suppressedExceptions.add(suppressed);
239                }
240            } else if (file.isDirectory()) {
241                // We support directories for looking up resources.
242                // This is only useful for running libcore tests.
243                elements.add(new Element(file, true, null, null));//创建Element对象
244            } else {
245                System.logW("Unknown file type for: " + file);
246            }
247
248            if ((zip != null) || (dex != null)) {
249                elements.add(new Element(file, false, zip, dex));
250            }
251        }
252
253        return elements.toArray(new Element[elements.size()]);
254    }
上面的代码是遍历所有的dex文件,然后调用的DexFile的loadDex()方法,内部就是创建一个DexFile对象,这个构造函数中会调用openDexFile()解析dex文件,这是一个native()方法,使用c代码实现的,这里就不分析了,如果想要学习,推荐一篇博客:DexClassLoader源码解析
关于DexFile,AndFix就是直接使用DexFile直接加载的dex文件,而没有使用DexClassLoader,有兴趣可以查看AndFix的源码。

3.Context的getClassLoader()为什么返回是PathClassLoader,探索其中奥秘

我们都知道getApplicationContext()的返回的实现类是ContextImpl,下面来看看ContextImpl的getClassLoader()的代码实现:
public ClassLoader getClassLoader() {
        return mPackageInfo != null ?
                mPackageInfo.getClassLoader() : ClassLoader.getSystemClassLoader();
}
我们看到如果mPackageInfo不为null,就调用它的getClassLoader()方法,否则调用ClassLoader.getSystemClassLoader(),这里我们看到了ClassLoader的getSystemClassLoader()方法,但是这里还不是重点,重点是mPackageInfo这个对象,这是什么呢,它是一个LoadedAPK对象。它又是什么呢?
官方文档说明如下:
Local state maintained about a currently loaded .apk.
LoaderAPK对象是apk在内存中的表示。通过这个LoaderApk对象可以拿到apk中代码和资源,甚至里面Activity和Service等信息。
那么它又是哪里创建的,又是什么时候创建的呢,如果你了解Activity的启动过程,你就明白了(如果想了解,推荐老罗的文章),这里就不赘述了。在ActivityThread里面有一个mPackages的map类型的成员变量,根据键值(packageName)存储对应的LoadedApk对象。启动Activity的时候要调用LoadedApk的getClassLoader(),来加载对应的Activity class文件,然后通过反射创建这个activity的实例;那么获取这个对象,会先去mPackages中去查找有没有缓存,如果没有就创建一个新的LoadedAPK对象。
下面代码截取自ActivityThread中启动Activity的过程中创建LoadedApk的代码:
public final LoadedApk getPackageInfoNoCheck(ApplicationInfo ai,
        CompatibilityInfo compatInfo) {
    return getPackageInfo(ai, compatInfo, null, false, true, false);
}
private LoadedApk getPackageInfo(ApplicationInfo aInfo, CompatibilityInfo compatInfo,
        ClassLoader baseLoader, boolean securityViolation, boolean includeCode,
        boolean registerPackage) {
        // 获取userid信息
    final boolean differentUser = (UserHandle.myUserId() != UserHandle.getUserId(aInfo.uid));
    synchronized (mResourcesManager) {
    // 尝试获取缓存信息
        WeakReference<LoadedApk> ref;
        if (differentUser) {
            // Caching not supported across users
            ref = null;
        } else if (includeCode) {
            ref = mPackages.get(aInfo.packageName);
        } else {
            ref = mResourcePackages.get(aInfo.packageName);
        }

        LoadedApk packageInfo = ref != null ? ref.get() : null;
        if (packageInfo == null || (packageInfo.mResources != null
                && !packageInfo.mResources.getAssets().isUpToDate())) {
                // 缓存没有命中,直接new
            packageInfo =
                new LoadedApk(this, aInfo, compatInfo, baseLoader,
                        securityViolation, includeCode &&
                        (aInfo.flags&ApplicationInfo.FLAG_HAS_CODE) != 0, registerPackage);

        // 省略。。更新缓存
        return packageInfo;
    }
}
下面看上面使用到的LoadedApk的构造函数,其实LoadedApk还有一个构造函数,在ContextImpl创建自己的实例的同时创建其LoadedApk的成员变量的时候使用了。
108    public LoadedApk(ActivityThread activityThread, ApplicationInfo aInfo,
109            CompatibilityInfo compatInfo,
110            ActivityThread mainThread, ClassLoader baseLoader,
111            boolean securityViolation, boolean includeCode) {
<span style="white-space:pre">	</span>   .......(省略)
126
127        if (mAppDir == null) {
128            if (ActivityThread.mSystemContext == null) {//这个context很重要,一个ActivityThread只有这一个,是静态全局的
129                ActivityThread.mSystemContext =
130                    ContextImpl.createSystemContext(mainThread);
131                ActivityThread.mSystemContext.getResources().updateConfiguration(
132                         mainThread.getConfiguration(),
133                         mainThread.getDisplayMetricsLocked(compatInfo, false),
134                         compatInfo);
135                //Slog.i(TAG, "Created system resources "
136                //        + mSystemContext.getResources() + ": "
137                //        + mSystemContext.getResources().getConfiguration());
138            }
139            mClassLoader = ActivityThread.mSystemContext.getClassLoader();//这个ClassLoader就是最后返回的那个ClassLoader
140            mResources = ActivityThread.mSystemContext.getResources();
141        }
142    }
143
看到这里,我们只要最终这个CLassLoader的来源是从Context那里后去的,也就是ActivityThread的mSystemContext里面的ClassLoader,我们来看一下这个mSystemContext的创建过程,代码如下:
1458    static ContextImpl createSystemContext(ActivityThread mainThread) {
1459        ContextImpl context = new ContextImpl();
1460        context.init(Resources.getSystem(), mainThread);//这个init操作也没有创建他里面LoadedApk成员变量
1461        return context;
1462    }
所以最终调用的代码,就是最开始的ContextImpl的getClassLoader()方法,并且mPackageInfo(LoadedAPK对象)为null,所以最终调用的是ClassLoader.getSystemClassLoader(),所以最终结论就是系统ClassLoader是通过ClassLoader.getSystemClassLoader()创建。

4.揭开ClassLoader.getSystemClassLoader()在Android中的神秘面纱


其代码如下:
public static ClassLoader getSystemClassLoader() {
     return SystemClassLoader.loader;
}
static private class SystemClassLoader {
        public static ClassLoader loader = ClassLoader.createSystemClassLoader();
}
    private static ClassLoader createSystemClassLoader() {
        String classPath = System.getProperty("java.class.path", ".");
        return new PathClassLoader(classPath, BootClassLoader.getInstance());
    }
对于最有一个核心方法createSystemClassLoader(),官方说明如下:
Create the system class loader. Note this is NOT the bootstrap class
loader (which is managed by the VM). We use a null value for the parent
to indicate that the bootstrap loader is our parent.
创建系统的ClassLoader。注释:这不是bootstrap ClassLoader(被虚拟机管理的ClassLoader)。我们使用null作为我们系统ClassLoader的parent来表明bootstrap就是我们的系统ClassLoader的parent。这里也就是充分说明了Android系统不是使用的java原生的bootstrap来加载,而是使用自己的创建的套机制。取代bootstrap的是用null(bootstrap不是一个ClassLoader对象,所以他的子级ClassLoader调用getParent()返回的是null),而取代java中第二级的ClassLoader使用Android中创建的最基层的BootClassLoader(也就是上面的PathClassLoader的parent)。
这个BootClassLoader是单例的,所以全局只有一个,我们也可以得出,系统所有执行装载类的操作,都会执行到这个对象。代码如下:
public BootClassLoader() {
     super(null, true);//他的parent为null(模拟它的parent是bootstrap)
}

还有我们看到上面PathClassLoader构造函数中传递的第一个参数classPath,这个就是我们之前打印出来的dexPath。为什么系统的ClassLoader.getSystemClassLoader()返回的PathClassLoader 的dexPath指向的是当前apk的安装路径;但是我们自己创建的返回的PathClassLoader的dexPath却是.  我们发现是由上面的System.getProperty("java.class.path",".");决定的,看到get操作,相对的也就会有set操作,那么这个值是什么时候设置的呢。这我们就要追溯到Android framwork中System中的initSystemProperty()方法中,调用VMRuntime.getRuntime().classPath()的值,这个值应该就是当前apk的安装路径。
代码如下: 路径:/libcore/luni/src/main/java/java/lang/System.java(4.0.4)
private static void initSystemProperties() {
        VMRuntime runtime = VMRuntime.getRuntime();
        Properties p = new Properties();

        String projectUrl = "http://www.android.com/";
        String projectName = "The Android Project";

        p.put("java.boot.class.path", runtime.bootClassPath());
        p.put("java.class.path", runtime.classPath());
}
但是,为什么我们自己调用,获取这个值就是. 我的猜想就是 系统创建完对应的ClassLoader之后,就将这个值修改成了.(当然这仅仅是我个人的猜想,希望了解的大神能帮我解答一下,因为我实在是找不到原因,困惑好久了)。


5.总结

1.Android系统最顶级的ClassLoader是BootClassLoader(替代java中第二级的ext ClassLoader),而用来加载系统的类是使用的以这儿BootClassLoader为parent的PathClassLoader(替代java中第三级加载classpath的ClassLoader)。

2.Android系统的PathClassLoader的dexPath(要加载类的路径),指向当前apk安装的路径,然后使用DexFile来解析对应的dex文件,装载里面的class

3.DexClassLoader,主要用于加载外部插件,也就是可以直接加载一个apk文件,现在的插件化动态加载机制,热修复等都要使用到它的特性,当然直接使用里面的DexFile直接加载dex文件也是可以(AndFix就是这样做的)。

4.自己遇到的坑:我之前在学习动态化的时候,看完原理之后,自己创建一个DexClassLoader ,它的parent我设置的是系统PathClassLoader(也就是context.getClassLoader()),但是load外部apk中的类的时候,报错:当前apk的安装路径下(也就是这个apk文件对应生成的ODEX文件)找不到对应的类,也就是ClassNotFoundException,但是自己不明白为什么我用DexClassLoader加载的外部apk,但是它却到系统中去寻找。现在明白了因为我传递的是PathClassLoader的dexPath是指向系统的apk的路径的,自然会到那里去找。但是如果传递的是ClassLoader.getSystemClassLoader(),因为dexPath是. ,是Directory,就不会创建DexFile,也就不会走DexFile的loadDexFile()方法,而是直接调用它的parent也就是BootClassLoader的findClass()来装载当前类。

文章终于写完了,鄙人愚钝,水平有限,文章难免有错,欢迎指出。










作者:u010014658 发表于2016/9/19 16:54:52 原文链接
阅读:191 评论:0 查看评论

Viewing all articles
Browse latest Browse all 5930

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>