Android的ClassLoader、DexLoader和插件化
2016-02-01 17:15
501 查看
》Android 动态升级。
Android 插件化 —— 指将一个程序划分为不同的部分,比如一般 App 的皮肤样式就可以看成一个插件;
Android 组件化 —— 这个概念实际跟上面相差不那么明显,组件和插件较大的区别就是:组件是指通用及复用性较高的构件,比如图片缓存就可以看成一个组件被多个 App 共用;
Android 动态加载 —— 这个实际是更高层次的概念,也有叫法是热加载或 Android 动态部署,指容器(App)在运⾏状态下动态加载某个模块,从而新增功能或改变某⼀部分行为.
》JVM 及 Dalvik 对类唯一的识别是 ClassLoader id + PackageName + ClassName。
Android 也有自己的 ClassLoader,分为 dalvik.system.DexClassLoader
和 dalvik.system.PathClassLoader,区别在于 PathClassLoader 不能直接从 zip 包中得到 dex,因此只支持直接操作 dex 文件或者已经安装过的 apk(因为安装过的 apk 在 cache 中存在缓存的 dex 文件)。而 DexClassLoader 可以加载外部的 apk、jar 或 dex文件,并且会在指定的 outpath 路径存放其 dex 文件。
关于动态加载apk,理论上可以用到的有DexClassLoader、PathClassLoader和URLClassLoader。
DexClassLoader :可以加载文件系统上的jar、dex、apk
PathClassLoader :可以加载/data/app目录下的apk,这也意味着,它只能加载已经安装的apk
URLClassLoader :可以加载java中的jar,但是由于dalvik不能直接识别jar,所以此方法在android中无法使用,尽管还有这个类
在Android系统中,一个App的所有代码都在一个Dex文件里面。Dex是一个类似Jar的存储了多有Java编译字节码的归档文件。
对于虚拟机来说,其实所有的代码都是在运行时被加载进来的。而不同于C语言还存在着静态链接。虚拟机在所有Java代码执行之前被启动,然后开始把字节码加载到环境中执行,我们可以理解成所有的代码都是动态加载到虚拟机里的。
在目前的软硬件环境下,Native App与Web App在用户体验上有着明显的优势,但在实际项目中有些会因为业务的频繁变更而频繁的升级客户端,造成较差的用户体验,而这也恰恰是Web App的优势。
PathClassLoader不能主动从zip包中释放出dex,因此只支持直接操作dex格式文件,或者已经安装的apk(因为已经安装的apk在cache中存在缓存的dex文件)。而DexClassLoader可以支持.apk、.jar和.dex文件,并且会在指定的outpath路径释放出dex文件。
》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》
关于插件,已经在各大平台上出现过很多,eclipse插件、chrome插件、3dmax插件,所有这些插件大概都为了在一个主程序中实现比较通用的功能,把业务相关或者让可以让用户自定义扩展的功能不附加在主程序中,主程序可在运行时安装和卸载。
在android如何实现插件也已经被广泛传播,实现的原理都是实现一套插件接口,把插件实现编成apk或者dex,然后在运行时使用DexClassLoader动态加载进来,这里分享一下DexClassLoader加载原理和分析在实现插件时不同操作造成错误的原因。
先来回顾一下如何在Android平台下做插件吧,首先定义一个插件接口IPlugin(其实不使用接口也可以,在加载类的时候直接使用反射调用相关类,但写代码来比较蛋疼):
Java
Java
Java
写好这个接口后,导出这个IPlugin生成jar包,这个相当于SDK了,然后新建一个工程并,这个工程以引用方式(即eclipse中externallibrary)引用这个包后,实现这个接口:
Java
编译这个工程并生成apk或者导出实现类生成dex,这时就做好了我们的插件实体,最后在我们的主工程里把插件接口的jar(即插件SDK)放在lib目录下在apk编译时打包进来,同时用下面的代码在需要的时候加载进来调用:
Java
好,这样我们就实现了一个简单的插件,现在来问两个问题:
1.为什么插件SDK要放在lib目录?放在lib目录和非lib目录以external方式引用的区别是什么?
2.为什么插件SDK只能导出接口,在插件工程里要以external方式引用又不是放在lib目录了?
在回答这两个问题之前,我们来做下实验:
1.主工程不把插件sdk放在lib目录下,而是以external方式引用,插件SDK和插件工程引用的方式不变。这时在运行时会产生如下错误:
java.lang.ClassNotFoundException: PluginImpl
at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:61)
at java.lang.ClassLoader.loadClass(ClassLoader.java:501)
at java.lang.ClassLoader.loadClass(ClassLoader.java:461)
at org.cmdmac.host.MainActivity.onCreate(MainActivity.java:23)
at android.app.Activity.performCreate(Activity.java:5084)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1079)
at
……
2.在插件工程里把SDK放到lib目录下,主工程引用方式不变,会出现下面的错误
java.lang.IllegalAccessError: Class ref in pre-verified class resolved to unexpected implementation
dalvik.system.DexFile.defineClass(Native Method)
dalvik.system.DexFile.loadClassBinaryName(DexFile.java:211)
dalvik.system.DexPathList.findClass(DexPathList.java:315)
dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.j
3.在插件工程把SDK放到lib目录下,加载的classloader改为:
ClassLoaderclassLoader=ClassLoader.getSystemClassLoader();
会出现下面的错误
java.lang.ClassCastException: org.cmdmac.plugin.PluginImpcannot be cast to org.cmdmac.pluginsdk.AbsPlugin
com.example.org.cmdmac.host.test.MainActivity.onCreate(MainActivity.java:30)
android.app.Activity.performCreate(Activity.java:5084)
android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1079)
com.lbe.security.service.core.client.internal.InstrumentationDelegate.callActivityOnCreate(InstrumentationDelegate.java:76)
android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2044)
这些错误是怎么来的?解析答案得从JAVA类加载原理出发:
Java的类加载器一般为URLClassLoader,在Android里是不能用的,取而代之的是DexClassLoader和PathClassLoader。
Java中的类加载器大致可以分成两类,一类是系统提供的,另外一类则是由Java应用开发人员编写的。系统提供的类加载器主要有下面三个:
引导类加载器(bootstrapclassloader):它用来加载Java的核心库,是用原生代码来实现的,并不继承自java.lang.ClassLoader。
扩展类加载器(extensionsclassloader):它用来加载Java的扩展库。Java虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载Java类。
系统类加载器(systemclassloader):它根据Java应用的类路径(CLASSPATH)来加载Java类。一般来说,Java应用的类都是由它来完成加载的。可以通过ClassLoader.getSystemClassLoader()来获取它。
类加载器在尝试自己去查找某个类的字节代码并定义它时,会先代理给其父类加载器,由父类加载器先去尝试加载这个类,依次类推。在介绍代理模式背后的动机之前,首先需要说明一下Java虚拟机是如何判定两个Java类是相同的。Java虚拟机不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。只有两者都相同的情况,才认为两个类是相同的。即便是同样的字节代码,被不同的类加载器加载之后所得到的类,也是不同的。比如一个Java类com.example.Sample,编译之后生成了字节代码文件Sample.class。两个不同的类加载器ClassLoaderA和ClassLoaderB分别读取了这个Sample.class文件,并定义出两个java.lang.Class类的实例来表示这个类。这两个实例是不相同的。对于Java虚拟机来说,它们是不同的类。试图对这两个类的对象进行相互赋值,会抛出运行时异常ClassCastException。
由java类加载器原理可以得到如下答案:
关于第一个错误:
Android默认的类加载器是PathClassLoader那么:
ClassLoaderclassLoader=context.getClassLoader();
这个得到的结果就是PathClassLoader,它加载了一个apk或者dex里的所有类,当以exteral方式引用时,由于生成的主工程的apk是没有把接口类打包进来的,这时使用PathClassLoader去加载时也是没有加载到Impl的,由于PathClassLoader是父加载器,它找不到就会使用类加载器本身(即DexClassLoader)去查找,他去查找时发现需要引用AbsPlugin和IPlugin,这时再去找了一圈,也是没有找到,因此出现ClasNotFound错误。
关于第二个错误:
第二个错误是由于主工程和插件都包含和插件的接口,这时使用PathClassLoader在主工程查找时找到AbsPlugin和IPlguin,用DexClassLoader加载Impl时因为也会加载AbsPlugin和IPlugin,但这时使用DexClassLoader在plugin.apk也找到了,因此出现两个相同类的但是由不同的类加载器加载的,就出现了这个错误,这个错误类型出错的代码可以查看Resolve.cpp的dvmResolveClass函数。
关于第三个错误:
这个错误是在类型转换的时候出现,原因也是两个不同的基类,但原因不同,是因为使用SystemClassLoader加载时只能在plugin.apk里找到,但在进行类型转换时查找AbsPlugin和IPlugin是在主工程中查找的,这时的情况下,主工程的AbsPlugin和Impl继承的AbsPlugin是在不同的类加载器加载的,不能进行类型转换了。
Android 插件化 —— 指将一个程序划分为不同的部分,比如一般 App 的皮肤样式就可以看成一个插件;
Android 组件化 —— 这个概念实际跟上面相差不那么明显,组件和插件较大的区别就是:组件是指通用及复用性较高的构件,比如图片缓存就可以看成一个组件被多个 App 共用;
Android 动态加载 —— 这个实际是更高层次的概念,也有叫法是热加载或 Android 动态部署,指容器(App)在运⾏状态下动态加载某个模块,从而新增功能或改变某⼀部分行为.
》JVM 及 Dalvik 对类唯一的识别是 ClassLoader id + PackageName + ClassName。
Android 也有自己的 ClassLoader,分为 dalvik.system.DexClassLoader
和 dalvik.system.PathClassLoader,区别在于 PathClassLoader 不能直接从 zip 包中得到 dex,因此只支持直接操作 dex 文件或者已经安装过的 apk(因为安装过的 apk 在 cache 中存在缓存的 dex 文件)。而 DexClassLoader 可以加载外部的 apk、jar 或 dex文件,并且会在指定的 outpath 路径存放其 dex 文件。
关于动态加载apk,理论上可以用到的有DexClassLoader、PathClassLoader和URLClassLoader。
DexClassLoader :可以加载文件系统上的jar、dex、apk
PathClassLoader :可以加载/data/app目录下的apk,这也意味着,它只能加载已经安装的apk
URLClassLoader :可以加载java中的jar,但是由于dalvik不能直接识别jar,所以此方法在android中无法使用,尽管还有这个类
在Android系统中,一个App的所有代码都在一个Dex文件里面。Dex是一个类似Jar的存储了多有Java编译字节码的归档文件。
对于虚拟机来说,其实所有的代码都是在运行时被加载进来的。而不同于C语言还存在着静态链接。虚拟机在所有Java代码执行之前被启动,然后开始把字节码加载到环境中执行,我们可以理解成所有的代码都是动态加载到虚拟机里的。
在目前的软硬件环境下,Native App与Web App在用户体验上有着明显的优势,但在实际项目中有些会因为业务的频繁变更而频繁的升级客户端,造成较差的用户体验,而这也恰恰是Web App的优势。
PathClassLoader不能主动从zip包中释放出dex,因此只支持直接操作dex格式文件,或者已经安装的apk(因为已经安装的apk在cache中存在缓存的dex文件)。而DexClassLoader可以支持.apk、.jar和.dex文件,并且会在指定的outpath路径释放出dex文件。
》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》
关于插件,已经在各大平台上出现过很多,eclipse插件、chrome插件、3dmax插件,所有这些插件大概都为了在一个主程序中实现比较通用的功能,把业务相关或者让可以让用户自定义扩展的功能不附加在主程序中,主程序可在运行时安装和卸载。
在android如何实现插件也已经被广泛传播,实现的原理都是实现一套插件接口,把插件实现编成apk或者dex,然后在运行时使用DexClassLoader动态加载进来,这里分享一下DexClassLoader加载原理和分析在实现插件时不同操作造成错误的原因。
插件Sample
先来回顾一下如何在Android平台下做插件吧,首先定义一个插件接口IPlugin(其实不使用接口也可以,在加载类的时候直接使用反射调用相关类,但写代码来比较蛋疼):1 2 3 4 5 | public interface IPlugin { public String getName(); public String getVersion(); public void show(); } |
1 2 3 4 5 | public interface IPlugin { public String getName(); public String getVersion(); public void show(); } |
1 2 3 4 5 | public abstract class AbsPlugin { public abstract String getName(); public abstract String getVersion(); public abstract void show(); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 | public class PluginImp extends AbsPlugin { public String getName() { return "PluginImp" ; } public String getVersion() { return "1.0" ; } public void show() { android.util.Log.( "PluginImp" , "ha ha I'm pluginimp" ); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | try { ClassLoader classLoader= context.getClassLoader() ; DexClassLoader localDexClass Loader = new DexClassLoader( "/sdcard/plugin.apk" , null ,classLoader) ; //load class Class localClass = localDexClassLoader.loadClass( "org.cmdmac.plugin.PluginImpl" ); //construct instance Constructor localConstructor = localClass.getConstructor( new Class[] {}); Object instance = localConstructor.newInstance( new Object[] {}); //call method IPlugin plugin = (IPlugin)instance; plugin.show (); } catch (Excpetion e) { //To do something } |
原理剖析
好,这样我们就实现了一个简单的插件,现在来问两个问题:1.为什么插件SDK要放在lib目录?放在lib目录和非lib目录以external方式引用的区别是什么?
2.为什么插件SDK只能导出接口,在插件工程里要以external方式引用又不是放在lib目录了?
在回答这两个问题之前,我们来做下实验:
1.主工程不把插件sdk放在lib目录下,而是以external方式引用,插件SDK和插件工程引用的方式不变。这时在运行时会产生如下错误:
java.lang.ClassNotFoundException: PluginImpl
at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:61)
at java.lang.ClassLoader.loadClass(ClassLoader.java:501)
at java.lang.ClassLoader.loadClass(ClassLoader.java:461)
at org.cmdmac.host.MainActivity.onCreate(MainActivity.java:23)
at android.app.Activity.performCreate(Activity.java:5084)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1079)
at
……
2.在插件工程里把SDK放到lib目录下,主工程引用方式不变,会出现下面的错误
java.lang.IllegalAccessError: Class ref in pre-verified class resolved to unexpected implementation
dalvik.system.DexFile.defineClass(Native Method)
dalvik.system.DexFile.loadClassBinaryName(DexFile.java:211)
dalvik.system.DexPathList.findClass(DexPathList.java:315)
dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.j
3.在插件工程把SDK放到lib目录下,加载的classloader改为:
ClassLoaderclassLoader=ClassLoader.getSystemClassLoader();
会出现下面的错误
java.lang.ClassCastException: org.cmdmac.plugin.PluginImpcannot be cast to org.cmdmac.pluginsdk.AbsPlugin
com.example.org.cmdmac.host.test.MainActivity.onCreate(MainActivity.java:30)
android.app.Activity.performCreate(Activity.java:5084)
android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1079)
com.lbe.security.service.core.client.internal.InstrumentationDelegate.callActivityOnCreate(InstrumentationDelegate.java:76)
android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2044)
这些错误是怎么来的?解析答案得从JAVA类加载原理出发:
Java的类加载器一般为URLClassLoader,在Android里是不能用的,取而代之的是DexClassLoader和PathClassLoader。
Java中的类加载器大致可以分成两类,一类是系统提供的,另外一类则是由Java应用开发人员编写的。系统提供的类加载器主要有下面三个:
引导类加载器(bootstrapclassloader):它用来加载Java的核心库,是用原生代码来实现的,并不继承自java.lang.ClassLoader。
扩展类加载器(extensionsclassloader):它用来加载Java的扩展库。Java虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载Java类。
系统类加载器(systemclassloader):它根据Java应用的类路径(CLASSPATH)来加载Java类。一般来说,Java应用的类都是由它来完成加载的。可以通过ClassLoader.getSystemClassLoader()来获取它。
类加载器在尝试自己去查找某个类的字节代码并定义它时,会先代理给其父类加载器,由父类加载器先去尝试加载这个类,依次类推。在介绍代理模式背后的动机之前,首先需要说明一下Java虚拟机是如何判定两个Java类是相同的。Java虚拟机不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。只有两者都相同的情况,才认为两个类是相同的。即便是同样的字节代码,被不同的类加载器加载之后所得到的类,也是不同的。比如一个Java类com.example.Sample,编译之后生成了字节代码文件Sample.class。两个不同的类加载器ClassLoaderA和ClassLoaderB分别读取了这个Sample.class文件,并定义出两个java.lang.Class类的实例来表示这个类。这两个实例是不相同的。对于Java虚拟机来说,它们是不同的类。试图对这两个类的对象进行相互赋值,会抛出运行时异常ClassCastException。
由java类加载器原理可以得到如下答案:
关于第一个错误:
Android默认的类加载器是PathClassLoader那么:
ClassLoaderclassLoader=context.getClassLoader();
这个得到的结果就是PathClassLoader,它加载了一个apk或者dex里的所有类,当以exteral方式引用时,由于生成的主工程的apk是没有把接口类打包进来的,这时使用PathClassLoader去加载时也是没有加载到Impl的,由于PathClassLoader是父加载器,它找不到就会使用类加载器本身(即DexClassLoader)去查找,他去查找时发现需要引用AbsPlugin和IPlugin,这时再去找了一圈,也是没有找到,因此出现ClasNotFound错误。
关于第二个错误:
第二个错误是由于主工程和插件都包含和插件的接口,这时使用PathClassLoader在主工程查找时找到AbsPlugin和IPlguin,用DexClassLoader加载Impl时因为也会加载AbsPlugin和IPlugin,但这时使用DexClassLoader在plugin.apk也找到了,因此出现两个相同类的但是由不同的类加载器加载的,就出现了这个错误,这个错误类型出错的代码可以查看Resolve.cpp的dvmResolveClass函数。
关于第三个错误:
这个错误是在类型转换的时候出现,原因也是两个不同的基类,但原因不同,是因为使用SystemClassLoader加载时只能在plugin.apk里找到,但在进行类型转换时查找AbsPlugin和IPlugin是在主工程中查找的,这时的情况下,主工程的AbsPlugin和Impl继承的AbsPlugin是在不同的类加载器加载的,不能进行类型转换了。
相关文章推荐
- Android简单的修剪图片 上传图片
- Android按返回键退出程序但不销毁,程序后台运行,同QQ退出处理方式
- 【Android基础学习】Android权限
- 获取Android控件的宽和高
- Android Toast 解析以及减少“无意义的”toast
- 【OpenSource】【Android】Android 开源项目
- 【OpenSource】【Android】Android 开源项目
- 【OpenSource】【Android】Android 开源项目
- 【OpenSource】【Android】Android 开源项目
- 【OpenSource】【Android】Android 开源项目
- 【OpenSource】【Android】Android 开源项目
- 【OpenSource】【Android】Android 开源项目
- 【OpenSource】【Android】Android 开源项目
- Android Dialog 列表的创建
- android 窗口的使用
- Android Fragment是什么
- AS不能发布release版本的解决方案
- 自学android 坑2
- Android 模拟屏幕点击和物理按键方式
- android学习中知识点集合(未完)