功能实现:将代码有bug的类通过热修复技术动态替换的效果
demo下载地址:里面有所以代码以及patch包运行即可
http://download.youkuaiyun.com/detail/h291850336/9911383
基本介绍:
android的Dalvik/ART虚拟机虽然与标准Java的JVM虚拟机不一样,ClassLoader具体的加载细节不一样,但是工作机制是类似的,也就是说在Android中同样可以采用类似的动态加载插件的功能
参考博客
http://blog.youkuaiyun.com/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; } }