最近客户需求通过后台下发代码的方式来实现新增功能,权衡了热修复和动态加载最终选择的动态加载jar的方式实现该功能。首先客户端编码,以jar的方式导出,将jar放到服务器供客户端下载并进行动态加载。
DexClassLoader :可以加载文件系统上的jar、dex、apk
PathClassLoader :可以加载/data/app目录下的apk,这也意味着,它只能加载已经安装的apk
URLClassLoader :可以加载Java中的jar,但是由于dalvik不能直接识别jar,所以此方法在Android中无法使用,尽管还有这个类
一、android studio下导出jar
新建一个library工程,不知道怎么创建library工程?本人是这样子创建的:
1、新建一个android工程
2、修改module的build.gradle文件,将 apply plugin: ‘com.android.application’ 改为 apply plugin: ‘com.android.library’,去掉defaultConfig下的applicationId(library工程不需要applicationId)
在library中定义接口:
package fota.adups.myapplication;
/**
* Created by wilson on 2017/5/7.
*/
public interface ILoader {
String sayHi();
}
定义实现类:
package fota.adups.myapplication;
/**
* Created by wilson on 2017/5/7.
*/
public class JarLoader implements ILoader{
public JarLoader() {
}
@Override
public String sayHi() {
return "I am jar loader.xxxxxxx";
}
}
这里要注意了,生成jar时要去掉接口文件,否则宿主会挂掉,android studio下生成jar的详细教程可以点击这里
生成jar后里面的java文件在java中是可以直接用的,但是android虚拟机不能直接识别此.class格式文件,需要进过dx工具处理转化为dex格式文件。在android sdk的build-tools目录(如下图)执行dx --dex --output=test.jar classes.jar
命令
在回头看下转化后的jar里有什么:
正是android可以识别的dex格式
二、在另外一个工程中动态加载jar
动态加载是通过java反射进行的,直接上代码吧
private void loadJar() {
final File optimizedDexOutputPath = new File(getStoragePath(this,false).toString()
+ File.separator + "loader_dex.jar");
Log.d(TAG,"loadJar,"+optimizedDexOutputPath.getAbsolutePath()+",,"+optimizedDexOutputPath.exists());
//jar的包名和主工程的包名一致时可以用下面的代码 特点:方便简单 缺点:具有包名一致的限制
/*BaseDexClassLoader cl = new BaseDexClassLoader(optimizedDexOutputPath.getAbsolutePath(),
this.getFilesDir(),null, this.getClass().getClassLoader());
Class libProviderClazz = null;
try {
// 载入JarLoader类, 并且通过反射构建JarLoader对象, 然后调用sayHi方法
libProviderClazz = cl.loadClass("fota.adups.myapplication.JarLoader");
ILoader loader = (ILoader) libProviderClazz.newInstance();
Toast.makeText(MainActivity.this, loader.sayHi(), Toast.LENGTH_SHORT).show();
} catch (Exception exception) {
// Handle exception gracefully here.
exception.printStackTrace();
}*/
//jar的包名和主工程的可以不一致,通用性强,可以适用于动态加载apk
// 4.1以后不能够将optimizedDirectory设置到sd卡目录, 否则抛出异常.
DexClassLoader classLoader = new DexClassLoader(optimizedDexOutputPath.getAbsolutePath(), getFilesDir().getAbsolutePath(),
null, getClassLoader());
try {
// 通过反射机制调用, 包名为com.example.loaduninstallapkdemo, 类名为UninstallApkActivity
Class mLoadClass = classLoader.loadClass("fota.adups.myapplication.JarLoader");
Constructor constructor = mLoadClass.getConstructor(new Class[] {});
Object testActivity = constructor.newInstance(new Object[] {});
// 获取sayHello方法
Method helloMethod = mLoadClass.getMethod("sayHi", new Class[]{});
helloMethod.setAccessible(true);
Object content = helloMethod.invoke(testActivity, new Object[] {});
Toast.makeText(MainActivity.this, content.toString(), Toast.LENGTH_LONG).show();
} catch (Exception e) {
e.printStackTrace();
}
}
上面的方法中展示了两种方式加载jar,一种是jar和主工程的包名一致时,可以直接在主工程中也定义出jar中的接口,然后从jar中获取接口的具体实现就行了;第二种是jar和主工程的包名不一致,此时利用Java类的祖先Object来获取jar中的接口实现。
值得一提的时,在参考某一篇文档调用BaseDexClassLoader方法时,路径次序填错了,搞得老是报找不到class文件的错误,坑死了。
参考文章:Android动态加载jar/dex
Android动态加载jar、apk的实现
Android插件化探索(四)免安装运行Activity(下)