Android动态加载入门 简单加载模式

基本信息

初步了解Android动态加载

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

如何获取可以加载的.dex文件

首先咱们能够经过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解压出来。服务器

加载并调用.dex里面的方法

与JVM不一样,Android的虚拟机不能用ClassCload直接加载.dex,而是要用DexClassLoader或者PathClassLoader,他们都是ClassLoader的子类,这二者的区别是框架

  1. DexClassLoader:能够加载jar/apk/dex,能够从SD卡中加载未安装的apk;

  2. 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的动态升级了”。

如何动态更改XML布局

虽然已经能动态更改代码逻辑了,可是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资源框架不是一个很明智的作法,可是这种作法确实可行,在咱们以前的实际生产中的项目中也稳定使用着,这里出于责任问题就不方便公开细节了。

(说实在,这种方案很是繁琐,很差维护,一方面,这是产品一句“技术可行就作呗”而产生的解决方案;另外一方面,可是动态加载技术还很不成熟,也没有什么实际投入到生产的项目,因此采起了很是保守的开发方式)。

使用Fragment代替Activity

Activity须要在Manifest里注册,而后一标准的Intent启动才会具备生命周期,很明显,若是想要动态加载的.dex里的Activity没有注册的话,是没法启动的。

有一种简单粗暴的作法就是能够把.dex里全部须要用到的Activity都事先注册到原项目里,不过这样一来若是.dex里的Activity有变化,原项目就必须跟着升级。

另一种方案是使用Fragment,Fragment自带生命周期,不须要在Manifest里注册,因此能够在.dex里使用Fragment来代替Activity,代价就是Fragment之间的切换会繁琐许多。

ART模式的兼容性问题

当初咱们开始设计动态加载方案的时候,尚未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动态性加载初级阶段”的解决方案,虽然如今已经能投入到具体的生产中去,可是还有一些问题没法忽略。

  1. 没法使用res目录下的资源,特别是使用XML布局,以及没法经过res资源到达自适应

  2. 没法动态加载新的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

相关文章
相关标签/搜索