您的位置:首页 > 移动开发 > Android开发

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加载原理和分析在实现插件时不同操作造成错误的原因。


插件Sample

先来回顾一下如何在Android平台下做插件吧,首先定义一个插件接口IPlugin(其实不使用接口也可以,在加载类的时候直接使用反射调用相关类,但写代码来比较蛋疼):

Java

1

2

3

4

5
public
interface
IPlugin
{


public
String
getName();


public
String
getVersion();


public
void
show();


}

Java

1

2

3

4

5
public
interface
IPlugin
{


public
String
getName();


public
String
getVersion();


public
void
show();


}

Java

1

2

3

4

5
public
abstract
class
AbsPlugin
{


public
abstract
String
getName();


public
abstract
String
getVersion();


public
abstract
void
show();


}

写好这个接口后,导出这个IPlugin生成jar包,这个相当于SDK了,然后新建一个工程并,这个工程以引用方式(即eclipse中externallibrary)引用这个包后,实现这个接口:

Java

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


}


}

编译这个工程并生成apk或者导出实现类生成dex,这时就做好了我们的插件实体,最后在我们的主工程里把插件接口的jar(即插件SDK)放在lib目录下在apk编译时打包进来,同时用下面的代码在需要的时候加载进来调用:

Java

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"
,
dexoutputpath,
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是在不同的类加载器加载的,不能进行类型转换了。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: