ClassLoader and 插件化设计

来源:互联网 发布:股票基本面软件 编辑:程序博客网 时间:2024/06/05 15:14

ClassLoader一个经常出现又让很多人望而却步的词,本文将试图以最浅显易懂的方式来讲解 ClassLoader,希望能对不了解该机制的朋友起到一点点作用。

要深入了解ClassLoader,首先就要知道ClassLoader是用来干什么的,顾名思义,它就是用来加载Class文件到JVM,以供程序使用的。我们知道,java程序可以动态加载类定义,而这个动态加载的机制就是通过ClassLoader来实现的,所以可想而知ClassLoader的重要性如何。

看到这里,可能有的朋友会想到一个问题,那就是既然ClassLoader是用来加载类到JVM中的,那么ClassLoader又是如何被加载呢?难道它不是java的类?

没有错,在这里确实有一个ClassLoader不是用java语言所编写的,而是JVM实现的一部分,这个ClassLoader就是bootstrap classloader(启动类加载器),这个ClassLoader在JVM运行的时候加载java核心的API以满足java程序最基本的需求,其中就包括用户定义的ClassLoader,这里所谓的用户定义是指通过java程序实现的ClassLoader,一个是ExtClassLoader,这个ClassLoader是用来加载java的扩展API的,也就是/lib/ext中的类,一个是AppClassLoader,这个ClassLoader是用来加载用户机器上CLASSPATH设置目录中的Class的,通常在没有指定ClassLoader的情况下,程序员自定义的类就由该ClassLoader进行加载。

当运行一个程序的时候,JVM启动,运行bootstrap classloader,该ClassLoader加载java核心API(ExtClassLoader和AppClassLoader也在此时被加载),然后调用ExtClassLoader加载扩展API,最后AppClassLoader加载CLASSPATH目录下定义的Class,这就是一个程序最基本的加载流程。

上面大概讲解了一下ClassLoader的作用以及一个最基本的加载流程,接下来将讲解一下ClassLoader加载的方式,这里就不得不讲一下ClassLoader在这里使用了双亲委托模式进行类加载。

每一个自定义ClassLoader都必须继承ClassLoader这个抽象类,而每个ClassLoader都会有一个parent ClassLoader,我们可以看一下ClassLoader这个抽象类中有一个getParent()方法,这个方法用来返回当前ClassLoader的parent,注意,这个parent不是指的被继承的类,而是在实例化该ClassLoader时指定的一个ClassLoader,如果这个parent为null,那么就默认该ClassLoader的parent是bootstrap classloader,这个parent有什么用呢?

我们可以考虑这样一种情况,假设我们自定义了一个ClientDefClassLoader,我们使用这个自定义的ClassLoader加载java.lang.String,那么这里String是否会被这个ClassLoader加载呢?事实上java.lang.String这个类并不是被这个ClientDefClassLoader加载,而是由bootstrap classloader进行加载,为什么会这样?实际上这就是双亲委托模式的原因,因为在任何一个自定义ClassLoader加载一个类之前,它都会先委托它的父亲ClassLoader进行加载,只有当父亲ClassLoader无法加载成功后,才会由自己加载,在上面这个例子里,因为java.lang.String是属于java核心API的一个类,所以当使用ClientDefClassLoader加载它的时候,该ClassLoader会先委托它的父亲ClassLoader进行加载,上面讲过,当ClassLoader的parent为null时,ClassLoader的parent就是bootstrap classloader,所以在ClassLoader的最顶层就是bootstrap classloader,因此最终委托到bootstrap classloader的时候,bootstrap classloader就会返回String的Class。

我们来看一下ClassLoader中的一段源代码:
Java代码 复制代码 收藏代码
  1. protected synchronized Class loadClass(String name,boolean resolve)
  2. throws ClassNotFoundException
  3. {
  4. // 首先检查该name指定的class是否有被加载
  5. Class c = findLoadedClass(name);
  6. if (c == null) {
  7. try {
  8. if (parent != null) {
  9. //如果parent不为null,则调用parent的loadClass进行加载
  10. = parent.loadClass(name, false);
  11. } else {
  12. //parent为null,则调用BootstrapClassLoader进行加载
  13. c = findBootstrapClass0(name);
  14. }
  15. } catch (ClassNotFoundException e) {
  16. //如果仍然无法加载成功,则调用自身的findClass进行加载
  17. c = findClass(name);
  18. }
  19. }
  20. if (resolve) {
  21. resolveClass(c);
  22. }
  23. return c;
  24. }
    protected synchronized Class loadClass(String name, boolean resolve)throws ClassNotFoundException    {// 首先检查该name指定的class是否有被加载Class c = findLoadedClass(name);if (c == null) {    try {if (parent != null) {    //如果parent不为null,则调用parent的loadClass进行加载c = parent.loadClass(name, false);} else {//parent为null,则调用BootstrapClassLoader进行加载    c = findBootstrapClass0(name);}    } catch (ClassNotFoundException e) {        //如果仍然无法加载成功,则调用自身的findClass进行加载        c = findClass(name);    }}if (resolve) {    resolveClass(c);}return c;    }


从上面一段代码中,我们可以看出一个类加载的大概过程与之前我所举的例子是一样的,而我们要实现一个自定义类的时候,只需要实现findClass方法即可。

为什么要使用这种双亲委托模式呢?

第一个原因就是因为这样可以避免重复加载,当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次。

第二个原因就是考虑到安全因素,我们试想一下,如果不使用这种委托模式,那我们就可以随时使用自定义的String来动态替代java核心api中定义类型,这样会存在非常大的安全隐患,而双亲委托的方式,就可以避免这种情况,因为String已经在启动时被加载,所以用户自定义类是无法加载一个自定义的ClassLoader。

上面对ClassLoader的加载机制进行了大概的介绍,接下来不得不在此讲解一下另外一个和ClassLoader相关的类,那就是Class类,每个被ClassLoader加载的class文件,最终都会以Class类的实例被程序员引用,我们可以把Class类当作是普通类的一个模板,JVM根据这个模板生成对应的实例,最终被程序员所使用。

我们看到在Class类中有个静态方法forName,这个方法和ClassLoader中的loadClass方法的目的一样,都是用来加载class的,但是两者在作用上却有所区别。
Class<?> loadClass(String name)
Class<?> loadClass(String name, boolean resolve)
我们看到上面两个方法声明,第二个方法的第二个参数是用于设置加载类的时候是否连接该类,true就连接,否则就不连接。

说到连接,不得不在此做一下解释,在JVM加载类的时候,需要经过三个步骤,装载、连接、初始化。装载就是找到相应的class文件,读入JVM,初始化就不用说了,最主要就说说连接。

连接分三步,第一步是验证class是否符合规格,第二步是准备,就是为类变量分配内存同时设置默认初始值,第三步就是解释,而这步就是可选的,根据上面loadClass方法的第二个参数来判定是否需要解释,所谓的解释根据《深入JVM》这本书的定义就是根据类中的符号引用查找相应的实体,再把符号引用替换成一个直接引用的过程。有点深奥吧,呵呵,在此就不多做解释了,想具体了解就翻翻《深入JVM吧》,呵呵,再这样一步步解释下去,那就不知道什么时候才能解释得完了。

我们再来看看那个两个参数的loadClass方法,在JAVA API 文档中,该方法的定义是protected,那也就是说该方法是被保护的,而用户真正应该使用的方法是一个参数的那个,一个参数的loadclass方法实际上就是调用了两个参数的方法,而第二个参数默认为false,因此在这里可以看出通过loadClass加载类实际上就是加载的时候并不对该类进行解释,因此也不会初始化该类。而Class类的forName方法则是相反,使用forName加载的时候就会将Class进行解释和初始化,forName也有另外一个版本的方法,可以设置是否初始化以及设置ClassLoader,在此就不多讲了。

不知道上面对这两种加载方式的解释是否足够清楚,就在此举个例子吧,例如JDBC DRIVER的加载,我们在加载JDBC驱动的时候都是使用的forName而非是ClassLoader的loadClass方法呢?我们知道,JDBC驱动是通过DriverManager,必须在DriverManager中注册,如果驱动类没有被初始化,则不能注册到DriverManager中,因此必须使用forName而不能用loadClass。

通过ClassLoader我们可以自定义类加载器,定制自己所需要的加载方式,例如从网络加载,从其他格式的文件加载等等都可以,其实ClassLoader还有很多地方没有讲到,例如ClassLoader内部的一些实现等等,本来希望能够讲得简单易懂一点,可是结果自己看回头好像感觉并不怎么样,郁闷,看来自己的文笔还是差太多了,希望能够给一些有需要的朋友一点帮助吧。

Android中的类装载器DexClassLoader

分类: android 90人阅读 评论(0)收藏 举报

目录(?)[+]

  1. publicDexClassLoaderStringdexPathStringoptimizedDirectoryStringlibraryPathClassLoaderparent
    1. Parameters
[html] view plaincopyprint?
类装载器DexClassLoader的介绍
在java中,有个概念叫做“类加载器”(ClassLoader),它的作用就是动态的装载Class文件。标准的java sdk中有一个
ClassLoader类,借助这个类可以装载想要的Class文件,每个ClassLoader对象在初始化时必须制定Class文件的路径。
可能有人会问,在写程序的时候不是有import关键字可以引用制定的类吗?为何还要使用这个类加载器呢?
原因其实是这样的,使用import关键字引用的类必须符合以下两个条件
  • 类文件必须在本地,当程序运行时需要次类时,这时类装载器会自动装载该类,程序员不需要关注此过程。
  • 编译的时候必须有这个类文件,否则编译不通过。
如果想让程序在运行的时候动态调用怎么办呢?用import显示是不符合上面的两种要求的。此时ClassLoader就派上用场了。
关于java的Classloader的装载机制请参考此链接http://www.iteye.com/topic/83978,最好到网上自行搜索一下。
http://www.artima.com/insidejvm/ed2/《Inside the Java Virtural Machine》
对于android应用程序,本质上使用的是java开发,使用标准的java编译器编译出Class文件,和普通的java开发不同的
地方是把class文件再重新打包成dex类型的文件,这种重新打包会对Class文件内部的各种函数表、变量表等进行优化,
最终产生了dex文件。dex文件是一种经过android打包工具优化后的Class文件,因此加载这样特殊的Class文件就需要特殊的类装载器,
所以android中提供了DexClassLoader类。
类装载器DexClassLoader类结构
继承关系:

A class loader that loads classes from .jar and .apk files containing a classes.dex entry. This can be used to execute code not installed as part of an application.

This class loader requires an application-private, writable directory to cache optimized classes. UseContext.getDir(String, int) to create such a directory:

   File dexOutputDir = context.getDir("dex", 0); 

Do not cache optimized classes on external storage. External storage does not provide access controls necessary to protect your application from code injection attacks.

翻译:这个类加载器用来从.jar和.apk类型的文件内部加载classes.dex文件。通过这种方式可以用来执行非安装的程序代码,作为程序的一部分进行运行。
这个装载类需要一个程序私有的,可写的文件目录去存放优化后的classes文件。通过Contexct.getDir(String, int)来创建

这个目录:

File dexOutputDir = context.getDir("dex",0);

不要把优化优化后的classes文件存放到外部存储设备上,防代码注入攻击。


这个类只有一个构造函数:

publicDexClassLoader (String dexPath,String optimizedDirectory, String libraryPath, ClassLoader parent)

Added in API level 3

Creates a DexClassLoader that finds interpreted and native code. Interpreted classes are found in a set of DEX files contained in Jar or APK files.

创建一个DexClassLoader用来找出指定的类和本地代码(c/c++代码)。用来解释执行在DEX文件中的class文件。

路径的分隔符使用通过System的属性path.separator 获得:.

String separeater = System.getProperty("path.separtor");

Parameters
dexPath需要装载的APK或者Jar文件的路径。包含多个路径用File.pathSeparator间隔开,在Android上默认是":"optimizedDirectory优化后的dex文件存放目录,不能为nulllibraryPath目标类中使用的C/C++库的列表,每个目录用File.pathSeparator间隔开; 可以为nullparent该类装载器的父装载器,一般用当前执行类的装载器
类装载器DexClassLoader的具体使用

这个类的使用过程基本是这样:
  • 通过PacageMangager获得指定的apk的安装的目录,dex的解压缩目录,c/c++库的目录
  • 创建一个 DexClassLoader实例
  • 加载指定的类返回一个Class
  • 然后使用反射调用这个Class
下面是代码部分,代码参考自《Android内核剖析》(作者柯元旦,这本书不错,推荐阅读):
[html] view plaincopy
  1. @SuppressLint("NewApi") private void useDexClassLoader(){
  2. //创建一个意图,用来找到指定的apk
  3. Intentintent =new Intent("com.suchangli.android.plugin", null);
  4. //获得包管理器
  5. PackageManagerpm =getPackageManager();
  6. List<ResolveInfo>resolveinfoes = pm.queryIntentActivities(intent, 0);
  7. //获得指定的activity的信息
  8. ActivityInfoactInfo =resolveinfoes.get(0).activityInfo;
  9. //获得包名
  10. StringpacageName =actInfo.packageName;
  11. //获得apk的目录或者jar的目录
  12. StringapkPath =actInfo.applicationInfo.sourceDir;
  13. //dex解压后的目录,注意,这个用宿主程序的目录,android中只允许程序读取写自己
  14. //目录下的文件
  15. StringdexOutputDir =getApplicationInfo().dataDir;
  16. //native代码的目录
  17. StringlibPath =actInfo.applicationInfo.nativeLibraryDir;
  18. //创建类加载器,把dex加载到虚拟机中
  19. DexClassLoadercalssLoader =new DexClassLoader(apkPath, dexOutputDir, libPath,
  20. this.getClass().getClassLoader());
  21. //利用反射调用插件包内的类的方法
  22. try {
  23. Class<?>clazz = calssLoader.loadClass(pacageName+".Plugin1");
  24. Objectobj =clazz.newInstance();
  25. Class[]param =new Class[2];
  26. param[0] = Integer.TYPE;
  27. param[1] = Integer.TYPE;
  28. Methodmethod =clazz.getMethod("function1", param);
  29. Integerret = (Integer)method.invoke(obj, 1,12);
  30. Log.i("Host", "return result is " + ret);
  31. } catch (ClassNotFoundException e) {
  32. e.printStackTrace();
  33. } catch (InstantiationException e) {
  34. e.printStackTrace();
  35. } catch (IllegalAccessException e) {
  36. e.printStackTrace();
  37. } catch (NoSuchMethodException e) {
  38. e.printStackTrace();
  39. } catch (IllegalArgumentException e) {
  40. e.printStackTrace();
  41. } catch (InvocationTargetException e) {
  42. e.printStackTrace();
  43. }
  44. }
Plugin1.apk中的一个类:
[html] view plaincopy
  1. package com.suchangli.plugin1;
  2. public class Plugin1 {
  3. public int function1(int a, int b){
  4. return a+b;
  5. }
  6. }


Log输出的结果:
注意要在Plugin1中的清单文件的一个activity中添加一个如下的action,这个action必须是宿主和插件约定好的。
源代码:
通过以上代码就可以访问另一个apk中的代码了,一个比较麻烦的地方就是必须用反射才能调用方法,
有没不用反射也可以调用方法方式呢?当然有,通过接口。

请参见下一篇博客:基于类装载器设计“插件”框架

android基于类装载器DexClassloader设计“插件框架”

分类: android 36人阅读 评论(0)收藏 举报
[java] view plaincopyprint?
插件相关介绍
首先插件只是一个逻辑概念,而不是什么技术标准,主要包含如下几个意思:
  • 插件不能独立运行,必须运行一个宿主程序中,宿主程序去调用插件(ps:微信的游戏算不算插件?感觉算是一种)
  • 插件一般情况下可以独立安装,android中就可以设计一个apk
  • 宿主程序中可以管理插件,比如添加,删除,禁用等。
  • 宿主程序应该保证插件向下兼容,新的宿主程序应该兼容老的插件
新浪微博的主题就是通过插件实现的切换。
由于ClassLoader具有动态装载程序的特点,因此可以使用此技术来一种插件框架。下面就是实现的细节。
使用DexClassLoader实现插件框架
大家了解高焕堂老师的EIT造型吗?如果不了解的话可以去网上搜一下,这种造型可以用来设计一个插件,一个框架,甚至一个平台。
高老师总结的确实精炼,容易理解,很多设计模式都是在这个EIT的之上或者变型生做的设计。
那么今天咱们用到EIT造型了吗?当然用到了,咱们直接上代码吧。
上一篇博客中是通过反射调用插件中的类的方法的,今天咱们要把接口作为联系主程序和插件程序的桥梁。
要实现住程序通过接口调用插件的程序,那么主程序和插件程序必须有相同的接口文件,也就是两个程序里都有接口的java类文件。
首先,在主程序里定义一个接口文件,然后原样的copy到插件程序中去,接口如下:
package com.suchangli.plugin;
public interface CommonInterface {
int function1(int a, int b);
}
包结构如下所示:
宿主程序:
插件程序
主程序的代码修改成使用接口
[java] view plaincopy
  1. @Override
  2. protectedvoid onCreate(Bundle savedInstanceState) {
  3. super.onCreate(savedInstanceState);
  4. setContentView(R.layout.activity_main);
  5. useDexClassLoader2();
  6. }
  7. @SuppressLint("NewApi")privatevoid useDexClassLoader2(){
  8. //创建一个意图,用来找到指定的apk
  9. Intent intent =new Intent("com.suchangli.android.plugin",null);
  10. //获得包管理器
  11. PackageManager pm = getPackageManager();
  12. List<ResolveInfo> resolveinfoes = pm.queryIntentActivities(intent,0);
  13. //获得指定的activity的信息
  14. ActivityInfo actInfo = resolveinfoes.get(0).activityInfo;
  15. //获得包名
  16. String pacageName = actInfo.packageName;
  17. //获得apk的目录或者jar的目录
  18. String apkPath = actInfo.applicationInfo.sourceDir;
  19. //dex解压后的目录,注意,这个用宿主程序的目录,android中只允许程序读取写自己
  20. //目录下的文件
  21. String dexOutputDir = getApplicationInfo().dataDir;
  22. //native代码的目录
  23. String libPath = actInfo.applicationInfo.nativeLibraryDir;
  24. //创建类加载器,把dex加载到虚拟机中
  25. DexClassLoader calssLoader =new DexClassLoader(apkPath, dexOutputDir, libPath,
  26. this.getClass().getClassLoader());
  27. //利用反射调用插件包内的类的方法
  28. try {
  29. Class<?> clazz = calssLoader.loadClass(pacageName+".Plugin1");
  30. CommonInterface obj = (CommonInterface)clazz.newInstance();
  31. int ret = obj.function1(1,13);
  32. Log.i("Host","return result is " + ret);
  33. }catch (ClassNotFoundException e) {
  34. e.printStackTrace();
  35. }catch (InstantiationException e) {
  36. e.printStackTrace();
  37. }catch (IllegalAccessException e) {
  38. e.printStackTrace();
  39. }catch (IllegalArgumentException e) {
  40. e.printStackTrace();
  41. }
  42. }
也就这几句代码不同:
插件程序的类现接口:
[java] view plaincopy
  1. package com.suchangli.plugin1;
  2. import com.suchangli.plugin.CommonInterface;
  3. publicclass Plugin1 implements CommonInterface{
  4. publicint function1(int a, int b){
  5. return a+b;
  6. }
  7. }
直接安装两个程序,调用的时候会报这种错误:
copy过去报错,并且这种方式也不太现实,因为提供给插件开发者的时候肯定是以jar包的形式进行提供,而不是以原文件的形式提供,
更何况现在还报错。究其原因是什么呢?
其实是这样的,这个java文件被当做程序的一部分(本来就是一部分)(jar包是以外部jar的方式添加进去的,外部jar包会作为程序的一部分被最终的程序文件中,也会报同样的错误),从而使得在主程序和插件程序中存在包名相同但验证码不同的类文件。
导出jar包,这个大家应该都会,不会的到网上搜一下。
把jar包放进插件的libs文件加下
引用jar包
使用红色框的“Add Libary”,而不是蓝色框的“Add External JARs”.
如果还是不行就通过这种方式:
再重新安装一次插件,运行一次主程序,结果如下:
主程序如何搜索到插件的,是在插件程序的menifest.xml文件中,定义了一个activity,定义了一个action:
这样主程序就可以使用PacageManager类的queryIntentActivites()方法查询相关的插件程序列表了。
程序主题插件的实现
在主程序中添加如下一个方法:
[java] view plaincopy
  1. privatevoid useDexClassloader3(){
  2. //创建一个意图,用来找到指定的apk
  3. Intent intent =new Intent("com.suchangli.android.plugin",null);
  4. //获得包管理器
  5. PackageManager pm = getPackageManager();
  6. List<ResolveInfo> resolveinfoes = pm.queryIntentActivities(intent,0);
  7. //获得指定的activity的信息
  8. ActivityInfo actInfo = resolveinfoes.get(0).activityInfo;
  9. //获得包名
  10. String pacageName = actInfo.packageName;
  11. try {
  12. Resources res = pm.getResourcesForApplication(pacageName);
  13. int id = 0;
  14. id = res.getIdentifier("ic_launcher","drawable", pacageName);
  15. Log.i("","resId is " + id);
  16. }catch (NameNotFoundException e) {
  17. e.printStackTrace();
  18. }
  19. }
从上面的代码可以看出,我们能获得插件程序的资源文件的id,那么资源文件就很容易获得了,使用插件的形式进行主题替换就会很容易实现了。
期待大家能在DexClassLoader的基础上开发出一个开源的插件框架,或者通用的程序主题替换程序架构。
如果想一块写这种开源框架的可以和我联系,我能帮忙的尽量帮忙。谢谢大家了。
[java] view plaincopyprint?
原创粉丝点击