功能实现:将代码有bug的类通过热修复技术动态替换的效果
demo下载地址:里面有所以代码以及patch包运行即可
http://download.csdn.net/detail/h291850336/9911383
基本介绍:
android的Dalvik/ART虚拟机虽然与标准Java的JVM虚拟机不一样,ClassLoader具体的加载细节不一样,但是工作机制是类似的,也就是说在Android中同样可以采用类似的动态加载插件的功能
参考博客
http://blog.csdn.net/xyang81/article/details/7292380
https://juejin.im/post/5976af6151882510ca6d0559
https://github.com/dodola/HotFix
https://mp.weixin.qq.com/s?__biz=MzI1MTA1MzM2Nw==&mid=400118620&idx=1&sn=b4fdd5055731290eef12ad0d17f39d4a&scene=1&srcid=1106Imu9ZgwybID13e7y2nEi#wechat_redirect
java加载机制
Java默认提供的三个ClassLoader
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
System.out.println("-----------appclassloader-----------------------");
// classloaertest获取类加载器
Class classtest = ClassLoaderTest.class;
ClassLoader mainLoader = classtest.getClassLoader();
System.out.println("main class loader : " + mainLoader.toString()); //sun.misc.Launcher$AppClassLoader@63947c6b
//打印 appclassloader 的加载器
URL[] murls = ((URLClassLoader)mainLoader).getURLs();
for (URL url : murls){
System.out.println(url);
}
System.out.println("---------ExtClassLoader-------------------------");
//打印classtest 的parent字段
ClassLoader parentClass = mainLoader.getParent();
System.out.println("parent class : " + parentClass.toString()); //sun.misc.Launcher$ExtClassLoader@3cd1a2f1
//打印 appclassloader 的加载器
URL[] parenturls = ((URLClassLoader)parentClass).getURLs();
for (URL url : parenturls){
System.out.println(url);
}
System.out.println("-------------extclassloader---------------------");
//extclassloader 的parent字段
ClassLoader extclassloaderParent = parentClass.getParent();
System.out.println("parent class : " + (extclassloaderParent == null ? "null" : extclassloaderParent.toString()));
System.out.println("-------------bootstrapclassloader--------------------");
//打印 bootstrapclassloader
try {
Class lancherClass = Class.forName("sun.misc.Launcher");
Method methodGetCLassPath = lancherClass.getDeclaredMethod("getBootstrapClassPath", null);
if(methodGetCLassPath != null){
methodGetCLassPath.setAccessible(true);
Object mObj = methodGetCLassPath.invoke(null, null);
if(mObj != null) {
Method methodUrls = mObj.getClass().getDeclaredMethod("getURLs", null);
if(methodUrls != null){
methodUrls.setAccessible(true);
URL[] murlBoot = (URL[])methodUrls.invoke(mObj, null);
for (URL url : murlBoot){
System.out.println(url);
}
}
}
}else {
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
分别输出
-----------appclassloader-----------------------
main class loader : sun.misc.Launcher$AppClassLoader@63947c6b
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/charsets.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/deploy.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/ext/cldrdata.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/ext/dnsns.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/ext/jfxrt.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/ext/localedata.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/ext/nashorn.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/ext/sunec.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/ext/sunjce_provider.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/ext/sunpkcs11.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/ext/zipfs.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/javaws.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/jce.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/jfr.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/jfxswt.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/jsse.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/management-agent.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/plugin.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/resources.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/rt.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/lib/ant-javafx.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/lib/dt.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/lib/javafx-mx.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/lib/jconsole.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/lib/packager.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/lib/sa-jdi.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/lib/tools.jar
file:/Users/sunmeng/Desktop/springmvc/gs-rest-service/aaa/out/production/aaa/
file:/Applications/IntelliJ%20IDEA%2015.app/Contents/lib/idea_rt.jar
---------ExtClassLoader-------------------------
parent class : sun.misc.Launcher$ExtClassLoader@3cd1a2f1
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/ext/cldrdata.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/ext/dnsns.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/ext/jfxrt.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/ext/localedata.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/ext/nashorn.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/ext/sunec.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/ext/sunjce_provider.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/ext/sunpkcs11.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/ext/zipfs.jar
file:/System/Library/Java/Extensions/AppleScriptEngine.jar
file:/System/Library/Java/Extensions/dns_sd.jar
file:/System/Library/Java/Extensions/j3daudio.jar
file:/System/Library/Java/Extensions/j3dcore.jar
file:/System/Library/Java/Extensions/j3dutils.jar
file:/System/Library/Java/Extensions/jai_codec.jar
file:/System/Library/Java/Extensions/jai_core.jar
file:/System/Library/Java/Extensions/libAppleScriptEngine.jnilib
file:/System/Library/Java/Extensions/libJ3D.jnilib
file:/System/Library/Java/Extensions/libJ3DAudio.jnilib
file:/System/Library/Java/Extensions/libJ3DUtils.jnilib
file:/System/Library/Java/Extensions/libmlib_jai.jnilib
file:/System/Library/Java/Extensions/mlibwrapper_jai.jar
file:/System/Library/Java/Extensions/MRJToolkit.jar
file:/System/Library/Java/Extensions/vecmath.jar
file:/usr/lib/java/libjdns_sd.jnilib
-------------extclassloader---------------------
parent class : null
-------------bootstrapclassloader--------------------
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/resources.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/rt.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/sunrsasign.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/jsse.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/jce.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/charsets.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/jfr.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/classes
除了Java默认提供的三个ClassLoader之外,用户还可以根据需要定义自已的ClassLoader,而这些自定义的ClassLoader都必须继承自java.lang.ClassLoader类,也包括Java提供的另外二个ClassLoader(Extension ClassLoader和App ClassLoader)在内,但是Bootstrap ClassLoader不继承自ClassLoader,因为它不是一个普通的Java类,底层由C++编写,已嵌入到了JVM内核当中,当JVM启动后,Bootstrap ClassLoader也随着启动,负责加载完核心类库后,并构造Extension ClassLoader和App ClassLoader类加载器。
功能实现
android类加载器
//加载apk中的文件
System.out.println("xxxx main class loader(PathClassLoader) : " + classLoader.toString()); // dalvik.system.PathClassLoader
/* pathclassloader
DexPathList[[zip file "/data/app/com.mas.sunmeng.hotfixtest-2/base.apk",
zip file "/data/app/com.mas.sunmeng.hotfixtest-2/split_lib_dependencies_apk.apk",
zip file "/data/app/com.mas.sunmeng.hotfixtest-2/split_lib_slice_0_apk.apk",
zip file "/data/app/com.mas.sunmeng.hotfixtest-2/split_lib_slice_1_apk.apk",
zip file "/data/app/com.mas.sunmeng.hotfixtest-2/split_lib_slice_2_apk.apk",
zip file "/data/app/com.mas.sunmeng.hotfixtest-2/split_lib_slice_3_apk.apk",
zip file "/data/app/com.mas.sunmeng.hotfixtest-2/split_lib_slice_4_apk.apk",
zip file "/data/app/com.mas.sunmeng.hotfixtest-2/split_lib_slice_5_apk.apk",
zip file "/data/app/com.mas.sunmeng.hotfixtest-2/split_lib_slice_6_apk.apk",
zip file "/data/app/com.mas.sunmeng.hotfixtest-2/split_lib_slice_7_apk.apk",
zip file "/data/app/com.mas.sunmeng.hotfixtest-2/split_lib_slice_8_apk.apk",
zip file "/data/app/com.mas.sunmeng.hotfixtest-2/split_lib_slice_9_apk.apk"],
nativeLibraryDirectories=[/data/app/com.mas.sunmeng.hotfixtest-2/lib/x86, /vendor/lib, /system/lib]]]
*/
ClassLoader parentClassLoader = classLoader.getParent();
if(parentClassLoader != null ){
System.out.println("xxxx main class parent loader (BootClassLoader) : " + parentClassLoader.toString()); // java.lang.BootClassLoader
}
// parentClassLoader.getParent() 为空
if(parentClassLoader.getParent() != null ){
System.out.println("xxxx BootClassLoader parent class loader : " + parentClassLoader.getParent().toString()); // null
}
可以看见有2个Classloader实例,一个是BootClassLoader(系统启动的时候创建的),另一个是PathClassLoader(应用启动时创建的,用于加载“/data/app/xxx.apk”里面的类)。 由此也可以看出,一个运行的Android应用至少有2个ClassLoader。
动态加载的实现思路
DexClassLoader可以加载.jar和.apk类型的文件内部的classes.dex文件,用来执行非安装的程序,那么我们可以将有bug的类做成jar文件,然后将类动态插入到类加载器中,通过dexclassloader读取jar文件获取对应的dexpathlist中的dexelements,然后将补丁的dexelements插入到有bug的dexpathlist的dexelements之前,让类加载器先加载没有bug的类即可。
例如:
/**
* Created by sunmeng on 17/7/26.
*/
public class BugClass {
public String haveBug(){
return "xxxx there hava a bug !";
}
}
我们认为此类是有bug的类,需要讲返回的字符串做修改,
然后将修改后的代码打包成jar文件
jar cvf pathbug.jar com/mas/sunmeng/hotfixtest/BugClass.class //必须是全路径
然后将pathbug.jar做成补丁包path_dex.jar,只有通过dex工具打包而成的文件才能被Android虚拟机(dexopt)执行
./dx --dex --output=path_dex.jar pathbug.jar
若在进行class转jar包是不是全路径会报异常
Caused by: com.android.dx.cf.iface.ParseException: class name (com/mas/sunmeng/hotfixtest/BugClass) does not match path (BugClass.class)
打成补丁包后下面进行使用
private String LoadBugClassName = "com.mas.sunmeng.hotfixtest.BugClass";
/**
* jar cvf pathbug.jar com/mas/sunmeng/hotfixtest/BugClass.class //必须是全路径
* 否则执行./dx --dex --output=path_dex.jar pathbug.jar会报错
* 错误提示Caused by: com.android.dx.cf.iface.ParseException: class name (com/mas/sunmeng/hotfixtest/BugClass) does not match path (BugClass.class)
*/
private static final int BUF_SIZE = 2048;
@Override
public void onCreate() {
super.onCreate();
File dexpath = new File(getDir("dex", Context.MODE_PRIVATE), "path_dex.jar");
//将path包复制到私有目录下
BufferedInputStream bis = null;
OutputStream dexWriter = null;
try{
bis = new BufferedInputStream(getAssets().open("path_dex.jar"));
dexWriter = new BufferedOutputStream(new FileOutputStream(dexpath));
byte[] buf = new byte[BUF_SIZE];
int len;
while ((len = bis.read(buf, 0, BUF_SIZE)) > 0){
dexWriter.write(buf, 0, len);
}
}catch (Exception e){
}finally {
if(bis != null){
try {
bis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(dexWriter != null){
try {
dexWriter.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
//文件复制成功后
if(dexpath.getAbsolutePath() != null && new File(dexpath.getAbsolutePath()).exists()) {
//判断是否有loader,根据不同的系统中ClassLoader的类型来做相应的处理
//LexClassLoader应该是阿里自己的ClassLoader
// 我们分 API 14以上和以下进行分析
try{
if(hasLexClassLoader()){
PathClassLoader obj = (PathClassLoader) getClassLoader();
String replaceAll = new File(dexpath.getAbsolutePath()).getName().replaceAll("\\.[a-zA-Z0-9]+", ".lex");
Class cls = Class.forName("dalvik.system.LexClassLoader");
Object newInstance =
cls.getConstructor(new Class[] {String.class, String.class, String.class, ClassLoader.class}).newInstance(
new Object[] {getDir("dex", 0).getAbsolutePath() + File.separator + replaceAll,
getDir("dex", 0).getAbsolutePath(), dexpath.getAbsolutePath(), obj});
cls.getMethod("loadClass", new Class[] {String.class}).invoke(newInstance, new Object[] {LoadBugClassName});
setField(obj, PathClassLoader.class, "mPaths",
appendArray(getField(obj, PathClassLoader.class, "mPaths"), getField(newInstance, cls, "mRawDexPath")));
setField(obj, PathClassLoader.class, "mFiles",
combineArray(getField(obj, PathClassLoader.class, "mFiles"), getField(newInstance, cls, "mFiles")));
setField(obj, PathClassLoader.class, "mZips",
combineArray(getField(obj, PathClassLoader.class, "mZips"), getField(newInstance, cls, "mZips")));
setField(obj, PathClassLoader.class, "mLexs",
combineArray(getField(obj, PathClassLoader.class, "mLexs"), getField(newInstance, cls, "mDexs")));
}else if(hasDexClassLoader()){
//系统版本在14以上的
PathClassLoader pathClassLoader = (PathClassLoader) getClassLoader();
// 根据 DexPathList类及属性名dexElements获取到我们补丁包对应的有序数组dexElements上面已经得到了两个有序数组dexElements,
// 一个存放的的是没有打补丁之前的dex有序数组dexElements,另外一个是我们的补丁包对应的dex有序数组dexElements
// a 就是我们合并后的有序dex数组dexElements
Object a = combineArray(getDexElements(getPathList(pathClassLoader)),
getDexElements(getPathList(
new DexClassLoader(dexpath.getAbsolutePath(), getDir("dex", 0).getAbsolutePath(), dexpath.getAbsolutePath(), getClassLoader()))));
Object a2 = getPathList(pathClassLoader);
// 重新设置DexPathList 的有序数组对象dexElements值
setField(a2, a2.getClass(), "dexElements", a);
pathClassLoader.loadClass(LoadBugClassName);
}else{
injectBelowApiLevel14(this, dexpath.getAbsolutePath(), LoadBugClassName);
}
}catch (Exception e){
}
}
}
@TargetApi(14)
private static void injectBelowApiLevel14(Context context, String dexpath, String bugclass)
throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
PathClassLoader obj = (PathClassLoader) context.getClassLoader();
DexClassLoader dexClassLoader =
new DexClassLoader(dexpath, context.getDir("dex", 0).getAbsolutePath(), dexpath, context.getClassLoader());
dexClassLoader.loadClass(bugclass);
setField(obj, PathClassLoader.class, "mPaths",
appendArray(getField(obj, PathClassLoader.class, "mPaths"), getField(dexClassLoader, DexClassLoader.class,
"mRawDexPath")
));
setField(obj, PathClassLoader.class, "mFiles",
combineArray(getField(obj, PathClassLoader.class, "mFiles"), getField(dexClassLoader, DexClassLoader.class,
"mFiles")
));
setField(obj, PathClassLoader.class, "mZips",
combineArray(getField(obj, PathClassLoader.class, "mZips"), getField(dexClassLoader, DexClassLoader.class,
"mZips")));
setField(obj, PathClassLoader.class, "mDexs",
combineArray(getField(obj, PathClassLoader.class, "mDexs"), getField(dexClassLoader, DexClassLoader.class,
"mDexs")));
obj.loadClass(bugclass);
}
private static Object getPathList(Object obj) throws ClassNotFoundException, NoSuchFieldException,
IllegalAccessException {
return getField(obj, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
}
private static Object getDexElements(Object obj) throws NoSuchFieldException, IllegalAccessException {
return getField(obj, obj.getClass(), "dexElements");
}
private static Object appendArray(Object obj, Object obj2) {
Class componentType = obj.getClass().getComponentType();
int length = Array.getLength(obj);
Object newInstance = Array.newInstance(componentType, length + 1);
Array.set(newInstance, 0, obj2);
for (int i = 1; i < length + 1; i++) {
Array.set(newInstance, i, Array.get(obj, i - 1));
}
return newInstance;
}
private static Object getField(Object obj, Class cls, String str)
throws NoSuchFieldException, IllegalAccessException {
Field declaredField = cls.getDeclaredField(str);
declaredField.setAccessible(true);
return declaredField.get(obj);
}
private static void setField(Object obj, Class cls, String str, Object obj2)
throws NoSuchFieldException, IllegalAccessException {
Field declaredField = cls.getDeclaredField(str);
declaredField.setAccessible(true);
declaredField.set(obj, obj2);
}
private static Object combineArray(Object obj, Object obj2) {
Class componentType = obj2.getClass().getComponentType();
int length = Array.getLength(obj2);
int length2 = Array.getLength(obj) + length;
Object newInstance = Array.newInstance(componentType, length2);
for (int i = 0; i < length2; i++) {
if (i < length) {
Array.set(newInstance, i, Array.get(obj2, i));
} else {
Array.set(newInstance, i, Array.get(obj, i - length));
}
}
return newInstance;
}
private static boolean hasLexClassLoader() {
try {
Class.forName("dalvik.system.LexClassLoader");
return true;
} catch (ClassNotFoundException e) {
return false;
}
}
private static boolean hasDexClassLoader() {
try {
Class.forName("dalvik.system.BaseDexClassLoader");
return true;
} catch (ClassNotFoundException e) {
return false;
}
}