做者:kaedeahtml
项目:android-dynamical-loadingjava
Java程序中,JVM虚拟机是经过类加载器ClassLoader加载.jar文件里面的类的。Android也相似,不过Android用的是Dalvik/ART虚拟机,不是JVM,也不能直接加载.jar文件,而是加载dex文件。android
先要经过Android SDK提供的DX工具把.jar文件优化成.dex文件,而后Android的虚拟机才能加载。注意,有的Android应用能直接加载.jar文件,那是由于这个.jar文件已经通过优化,只不事后缀名没改(其实已是.dex文件)。git
若是对ClassLoader的工做机制有兴趣,具体过程请参考 Android 动态加载基础 ClassLoader工做机制,这里再也不赘述。github
首先咱们能够经过JDK的编译命令javac把Java代码编译成.class文件,再使用jar命令把.class文件封装成.jar文件,这与编译普通Java程序的时候彻底同样。编程
以后再用Android SDK的DX工具把.jar文件优化成.dex文件(在“android-sdk\build-tools\具体版本\”路径下)segmentfault
dx --dex --output=target.dex origin.jar // target.dex就是咱们要的了缓存
此外,咱们能够现把代码编译成APK文件,再把APK里面的.dex文件解压出来,或者直接把APK文件当成.dex使用(只是APK里面的静态资源文件咱们暂时还用不到)。至此咱们发现,不管加载.jar,仍是.apk,其实都和加载.dex是等价的,Android能加载.jar和.apk,是由于它们都包含有.dex,直接加载.apk文件时,ClassLoader也会自动把.apk里的.dex解压出来。服务器
与JVM不一样,Android的虚拟机不能用ClassCload直接加载.dex,而是要用DexClassLoader或者PathClassLoader,他们都是ClassLoader的子类,这二者的区别是框架
DexClassLoader:能够加载jar/apk/dex,能够从SD卡中加载未安装的apk;
PathClassLoader:要传入系统中apk的存放Path,因此只能加载已经安装的apk文件;
使用前,先看看DexClassLoader的构造方法
public DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) { super((String)null, (File)null, (String)null, (ClassLoader)null); throw new RuntimeException("Stub!"); }
注意,咱们以前提到的,DexClassLoader并不能直接加载外部存储的.dex文件,而是要先拷贝到内部存储里。这里的dexPath就是.dex的外部存储路径,而optimizedDirectory则是内部路径,libraryPath用null便可,parent则是要传入当前应用的ClassLoader,这与ClassLoader的“双亲代理模式”有关。
实例使用DexClassLoader的代码
File optimizedDexOutputPath = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "test_dexloader.jar");// 外部路径 File dexOutputDir = this.getDir("dex", 0);// 没法直接从外部路径加载.dex文件,须要指定APP内部路径做为缓存目录(.dex文件会被解压到此目录) DexClassLoader dexClassLoader = new DexClassLoader(optimizedDexOutputPath.getAbsolutePath(),dexOutputDir.getAbsolutePath(), null, getClassLoader());
到这里,咱们已经成功把.dex文件给加载进来了,接下来就是如何调用.dex里面的代码,主要有两种方式。
使用DexClassLoader加载进来的类,咱们本地并无这些类的源码,因此没法直接调用,不过能够经过反射的方法调用,简单粗暴。
DexClassLoader dexClassLoader = new DexClassLoader(optimizedDexOutputPath.getAbsolutePath(), dexOutputDir.getAbsolutePath(), null, getClassLoader()); Class libProviderClazz = null; try { libProviderClazz = dexClassLoader.loadClass("me.kaede.dexclassloader.MyLoader"); // 遍历类里全部方法 Method[] methods = libProviderClazz.getDeclaredMethods(); for (int i = 0; i < methods.length; i++) { Log.e(TAG, methods[i].toString()); } Method start = libProviderClazz.getDeclaredMethod("func");// 获取方法 start.setAccessible(true);// 把方法设为public,让外部能够调用 String string = (String) start.invoke(libProviderClazz.newInstance());// 调用方法并获取返回值 Toast.makeText(this, string, Toast.LENGTH_LONG).show(); } catch (Exception exception) { // Handle exception gracefully here. exception.printStackTrace(); }
毕竟.dex文件也是咱们本身维护的,因此能够把方法抽象成公共接口,把这些接口也复制到主项目里面去,就能够经过这些接口调用动态加载获得的实例的方法了。
pulic interface IFunc{ public String func(); } // 调用 IFunc ifunc = (IFunc)libProviderClazz; String string = ifunc.func(); Toast.makeText(this, string, Toast.LENGTH_LONG).show();
到这里,咱们已经成功从外部路径动态加载一个.dex文件,并执行里面的代码逻辑了。经过从服务器下载最新的.dex文件并替换本地的旧文件,就能初步实现“APP的动态升级了”。
虽然已经能动态更改代码逻辑了,可是UI界面要怎么更改啊?Android开发中大部分的状况下,UI界面都是经过XML布局实现的,放在res目录下,但是.dex库里面并无这些静态资源啊,因此没法改变XML布局。(这里即便直接动态加载APK文件,可是经过DexClassLoader只能加载新的APK其中的.dex文件,并没有法加载其中的res资源文件,因此若是在动态加载的.dex中直接使用新的APK的res资源的话会抛出异常。)
你们都知道,全部的XML布局在运行的时候都要经过LayoutInflator渲染成View的实例,这个实例与咱们使用纯Java代码建立的View实例几乎是等价的,并且后者可能效率还更高,全部的XML布局实现的UI界面都有等价的纯代码的建立方案。由此伸展开来,res目录下全部XML资源都有等价的纯代码的实现方式,好比XML动画、XML Drawable等。
因此,若是想要动态更改应用的UI界面的话,能够经过用纯代码建立布局的形式来解决。此外,还能够模仿LayoutInflator的工做方式,本身写一套布局解析器来解析XML文件,这样就能在彻底不依赖res资源的状况下建立UI界面了,固然这样的工做量很多,并且,彻底避开res资源的话,全部的分辨率、国际化等自适应问题都要本身在应用层写代码维护了,显然脱离res资源框架不是一个很明智的作法,可是这种作法确实可行,在咱们以前的实际生产中的项目中也稳定使用着,这里出于责任问题就不方便公开细节了。
(说实在,这种方案很是繁琐,很差维护,一方面,这是产品一句“技术可行就作呗”而产生的解决方案;另外一方面,可是动态加载技术还很不成熟,也没有什么实际投入到生产的项目,因此采起了很是保守的开发方式)。
Activity须要在Manifest里注册,而后一标准的Intent启动才会具备生命周期,很明显,若是想要动态加载的.dex里的Activity没有注册的话,是没法启动的。
有一种简单粗暴的作法就是能够把.dex里全部须要用到的Activity都事先注册到原项目里,不过这样一来若是.dex里的Activity有变化,原项目就必须跟着升级。
另一种方案是使用Fragment,Fragment自带生命周期,不须要在Manifest里注册,因此能够在.dex里使用Fragment来代替Activity,代价就是Fragment之间的切换会繁琐许多。
当初咱们开始设计动态加载方案的时候,尚未ART模式。随着Kitkat的发布以及ART模式的出现,咱们开始担忧“用DexClassLoader加载.dex文件”的方案会不会在ART模式上面存在兼容性问题。
其实,ART模式相比原来的Dalvik,会在安装APK的时候,使用Android系统自带的dex2oat工具把APK里面的.dex文件转化成OAT文件,OAT文件是一种Android私有ELF文件格式,它不只包含有从DEX文件翻译而来的本地机器指令,还包含有原来的DEX文件内容。这使得咱们无需从新编译原有的APK就可让它正常地在ART里面运行,也就是咱们不须要改变原来的APK编程接口。ART模式的系统里,一样存在DexClassLoader类,包名路径也没变,只不过它的具体实现与原来的有所不一样,可是接口是一致的。
package dalvik.system; import dalvik.system.BaseDexClassLoader; import java.io.File; public class DexClassLoader extends BaseDexClassLoader { public DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) { super((String)null, (File)null, (String)null, (ClassLoader)null); throw new RuntimeException("Stub!"); } }
也就是说,ART模式在加载.dex文件的方法上,对Dalvik作了向下兼容,因此使用DexClassLoader加载进来的.dex文件一样也会被转化成OAT文件再被执行,“以DexClassLoader为核心的动态加载方案”在ART模式上能够稳定运行。
关于ART模式以及OAT文件的详细分析,请参考官方的ART and Dalvik,以及老罗的Android ART运行时无缝替换Dalvik虚拟机的过程分析。
以上大体就是“Android动态性加载初级阶段”的解决方案,虽然如今已经能投入到具体的生产中去,可是还有一些问题没法忽略。
没法使用res目录下的资源,特别是使用XML布局,以及没法经过res资源到达自适应
没法动态加载新的Activity等组件,由于这些组件须要在Manifest中注册,动态加载没法更改当前APK的Manifest
以上问题能够经过反射调用Framework层代码以及代理Activity的方式解决,能够把这种的动态加载框架成为“代理模式”。
http://44289533.iteye.com/blog/1954453
http://blog.csdn.net/bboyfeiyu/article/details/11710497
http://www.cnblogs.com/over140/archive/2011/11/23/2259367.html