目录
一.动态加载技术介绍
在程序运行的时候,加载一些程序自身不存在的可执行文件并运行这些文件里的代码逻辑。动态加载具有一下几个特点:
- 应用在运行的时候加载一些本地不存在的可执行文件实现一些特定的功能;
- 这些可执行文件是可以替换的;
- 更换静态资源不属于动态加载,如换启动图,换主题或者用服务器开关控制广告的隐藏实现等;
- Android中动态加载的核心思想是动态调用外部的dex文件。
二.动态加载技术的类型
在Android项目中,动态加载技术按照加载的可执行文件可以分为两种。
1.动态加载so库
Android的NDK中其实就是使用了动态加载.so库并通过JNI调用其封装的方法。后者一般是由C/C++编译而成,运行在Native层效率比在虚拟机执行的java代码高很多,所以Android经常加载动态.so库来完成一些对性能要求比较高的工作,如T9搜索,Bitmap的解码,图片的高斯模糊等。
2.动态加载dex/jar/apk文件
Android项目中,所有的java代码都会被编译成dex文件,Android应用运行时,就是通过执行dex文件里的业务逻辑来工作的。使用动态加载技术可以在Android应用运行是加载外部的dex文件,通过网络下载新的dex文件并替换原有的dex文件就可以达到不安装新的APK文件就升级应用的目的。
三.类加载器的概念
类加载器(Class Loader)是Java中一个很重要的概念,类加载器负责加载java类的字节码到Java虚拟机中。在Android中的类加载器有DexClassLoader和PathClassLoader。它们的区别是:
- DexClassLoader:可以加载jar/apk/dex,可以从SD卡中加载未安装的apk;
- PathClassLoader:只能加载已经安装的apk文件。
四.动态加载的原理
无论哪种动态加载其基本原理就是在程序运行时加载一些外部的可执行文件,然后调用这些文件的某个方法执行业务逻辑。对于这些外部的可执行文件,在Android应用调用前都要先把它们拷贝到data/packagename/内部存储文件路径,使用类加载器加载相应的文件通过反射获取内部资源,以供宿主APP的使用,动态加载的大致过程就是:
- 把可执行文件(.so/dex/jar/apk)拷贝到应用APP内部存储;
- 加载可执行文件;
- 调用具体的方法执行业务;
五.动态加载的实现方式
1.简单动态加载模式
通过JDK的编译命令javac把jiava代码编译成.class文件,再使用jar命令把.class文件封装成.jar文件。最后使用Android SDK的DX工具把.jar文件优化成.dex文件。
通过DexClassLoader加载后使用反射或者接口方式调用里面的方法。
private void getOutData(){
File optimizadDexOutputPath = new File(Environment.getExternalStorageDirectory().getAbsolutePath()+File.separator+"test_dexloafer.jar");
// 无法直接从外部路径加载.dex文件,需要指定APP内部路径作为缓存目录(.dex文件会被解压到此目录)
File dexOutputDir = this.getDir("dex",0);
//构造类加载器
DexClassLoader dexClassLoader = new DexClassLoader(optimizadDexOutputPath.getAbsolutePath(),dexOutputDir.getAbsolutePath(),null,ClassLoader.getSystemClassLoader());
}
2.使用反射的方式
使用DexClassLoader加载进来的类无法直接调用,可以通过反射的方式调用。
private void getOutData(){
File optimizadDexOutputPath = new File(Environment.getExternalStorageDirectory().getAbsolutePath()+File.separator+"test_dexloafer.jar");
File dexOutputDir = this.getDir("dex",0);
DexClassLoader dexClassLoader = new DexClassLoader(optimizadDexOutputPath.getAbsolutePath(),dexOutputDir.getAbsolutePath(),null,ClassLoader.getSystemClassLoader());
Class libProviderClazz = null;
try {
libProviderClazz = dexClassLoader.loadClass("包名.类名");
//遍历所有的方法
Method[] methods = libProviderClazz.getDeclaredMethods();
for (int i = 0;i<methods.length;i++){
Log.e("test",methods[i].toString());
}
//通过方法名获取func方法
Method func= libProviderClazz.getDeclaredMethod("func");
//外部可以调用
func.setAccessible(true);
//调用该方法获得值
String string = (String) func.invoke(libProviderClazz.newInstance());
Toast.makeText(this, string, Toast.LENGTH_SHORT).show();
}catch (Exception e){
e.printStackTrace();
}
}
3.使用接口的方式
把.dex文件的方法抽象成公共接口,把这些接口复制到主项目中,通过接口的调用实现动态加载
pulic interface IFunc{
public String func();
}
// 调用
IFunc ifunc = (IFunc)libProviderClazz;
String string = ifunc.func();
Toast.makeText(this, string, Toast.LENGTH_LONG).show();
4.使用代理Activity模式
使用插件apk里的Activity需要解决两个问题:
1.如何使插件apk里面的Activity具有生命周期
主项目apk注册一个空壳的ProxyActivity,通过在ProxyActivity的生命周期里面同步调用插件中的Activity的生命周期方法,从而实现执行插件apk的业务逻辑。
2.如何使插件apk里的Activity具有上下文环境
插件里需要用到的新资源都是通过java代码的方式创建(xml布局,动画,点九图)。可以通过AsserManager实例创建Resource实例
public int getAppNameResId(String apkPath){
PackageManager packageManager = this.getPackageManager();
PackageInfo info = packageManager.getPackageArchiveInfo(apkPath,0);
return info.applicationInfo.labelRes;
}
private Resources getExtResource(Context context,String apkPath){
int mDexPath = getAppNameResId(apkPath);
Resources res = context.getResources();
try {
Class<?> assetClass = Class.forName("android.content.res.AssetManager");
Object assetManager = assetClass.getConstructor(null).newInstance(null);
assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath",String.class);
addAssetPath.invoke(assetManager,mDexPath);
Class<?> resClass = Class.forName("android.content.res.Resources");
res = (Resources) resClass.getConstructor(assetClass,res.getDisplayMetrics().getClass(),res.getConfiguration().getClass())
.newInstance(assetManager,res.getDisplayMetrics(),res.getConfiguration());
}catch (Exception e){
e.printStackTrace();
}
return res;
}
六.如何创建插件工程
插件工程作为一个android library .gradle文件中的 apply plugin:'com.android.library',插件工程就是提供一个类,生成一个jar包供主工程加载调用。
step1.双击gradle中assemble任务
step2.在./myplugin/build/intermediates/aar_main_jar/debug/classes.jar路径下找到jar文件
step3.打开dx工具执行以下命令对origin.jar做优化
dx --dex --output=target.jar origin.jar
step4.将buildTypes的debug中的minifyEnabled设置false
debug{
minifyEnabled false
}
step5.使用adb push XXX.jar /sdcard/ 将dex/jar放在sdcard。
七.动态加载的优点
- 规避APK覆盖安装的升级过程,提高用户体验;
- 动态修复应用的一些紧急bug;
- 当应用体积庞大的时候可以把一些模块通过动态加载以插件的形式分割出来,减少项目的体积提高编译的速度,也能让主项目和插件项目并行开发;
- 插件模块可以用懒加载的方式在需要的时候才初始化,提高应用的启动速度;
- 从项目管理上,分割插件模块做到了项目级别的代码分离,降低了模块之间的耦合度;
- 在Android应用上推广其他应用的时候,可以使用动态加载即使让用户优先体验新应用的功能而不用下载并安装全新的apk
- 减少主项目dex的方法数
八.动态加载的缺点
- 随着动态加载框架复杂程度的加深,项目的构建过程变得复杂;
- 由于插件项目是独立开发的,当主项目运行插件时,代码逻辑容易出现bug;
- 有些框架使用反射调用了部分Android系统Framework层的代码,存在兼容性的风险;
- 采用动态加载插件在使用系统资源使可能会遇到兼容性问题。
参考