一:热修复相关
热修复概念: 以补丁的方式动态修复紧急Bug,不再需要重新发布App,不再需要用户重新下载。
PathClassloader和DexClassLoader:
(1)PathClassloader作为其系统类和应用类的加载器,只能去加载已经安装到Android系统中的apk文件。
(2)DexClassLoader可以用来从.jar和.apk类型的文件内部加载classes.dex文件。可以用来执行非安装的程序代码。
(3)Android使用PathClassLoader作为其类加载器,DexClassLoader可以从.jar和.apk类型的文件内部加载classes.dex文件。
热修复原理:
PathClassLoader和DexClassLoader都继承自BaseDexClassLoader
在BaseDexClassLoader中有如下源码:
#BaseDexClassLoader
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class clazz = pathList.findClass(name);
if (clazz == null) {
throw new ClassNotFoundException(name);
}
return clazz;
}
#DexPathList
public Class findClass(String name) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext);
if (clazz != null) {
return clazz;
}
}
}
return null;
}
#DexFile
public Class loadClassBinaryName(String name, ClassLoader loader) {
return defineClass(name, loader, mCookie);
}
private native static Class defineClass(String name, ClassLoader loader, int cookie);1 n j
n
1 BaseDexClassLoader中有个pathList对象,pathList中包含一个DexFile的集合dexElements,而对于类加载呢,就是遍历这个集合,通过DexFile去寻找。
2 一个ClassLoader可以包含多个dex文件,每个dex文件是一个Element,多个dex文件排列成一个有序的数组dexElements,当找类的时候,会按顺序遍历dex文件,然后从当前遍历的dex文件中找类,如果找类则返回,如果找不到从下一个dex文件继续查找。
3 理论上,如果在不同的dex中有相同的类存在,那么会优先选择排在前面的dex文件的类,如下图:
4 把有问题的类打包到一个dex(patch.dex)中去,然后把这个dex插入到Elements的最前面,如下图:
二:阻止相关类打上CLASS_ISPREVERIFIED标志
dex校验: 如果两个相关联的类在不同的dex中就会报错,例如ClassA 引用了ClassB,但是发现这这两个类所在的dex不在一起,其中:
- ClassA 在classes.dex中
- ClassB 在patch.dex中
结果发生了错误。
dex校验的前提: 如果引用者这个类被打上了CLASS_ISPREVERIFIED标志,那么就会进行dex的校验。
相关类打上CLASS_ISPREVERIFIED标志的发生场景:
在虚拟机启动的时候,当verify选项被打开的时候,如果static方法、private方法、构造函数等,其中的直接引用(第一层关系)到的类都在同一个dex文件中,那么这个类就会被打上CLASS_ISPREVERIFIED
下图是class A 打上CLASS_ISPREVERIFIED标志
下图是class A 没有打上CLASS_ISPREVERIFIED标志
其中AntilazyLoad类会被打包成单独的hack.dex,这样当安装apk的时候,classes.dex内的类都会引用一个在不相同dex中的AntilazyLoad类,这样就防止了类被打上CLASS_ISPREVERIFIED的标志了,只要没被打上这个标志的类都可以进行打补丁操作。
在class文件中插入代码来阻止相关类打上CLASS_ISPREVERIFIED标志:在dx工具执行之前,将LoadBugClass.class文件呢,进行修改,再其构造中添加System.out.println(dodola.hackdex.AntilazyLoad.class),然后继续打包的流程。
原始代码
package dodola.hackdex;
public class AntilazyLoad
{
}
package dodola.hotfix;
public class BugClass
{
public String bug()
{
return "bug class";
}
}
package dodola.hotfix;
public class LoadBugClass
{
public String getBugString()
{
BugClass bugClass = new BugClass();
return bugClass.bug();
}
}
三:插入jar
插入代码
System.out.println(dodola.hackdex.AntilazyLoad.class)
在构造函数中插入操作代码(javassist)
package test;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
public class InjectHack
{
public static void main(String[] args)
{
try
{
String path = "/Users/zhy/develop_work/eclipse_android/imooc/JavassistTest/";
ClassPool classes = ClassPool.getDefault();
classes.appendClassPath(path + "bin");//项目的bin目录即可
CtClass c = classes.get("dodola.hotfix.LoadBugClass");
CtConstructor ctConstructor = c.getConstructors()[0];
ctConstructor
.insertAfter("System.out.println(dodola.hackdex.AntilazyLoad.class);");
c.writeFile(path + "/output");
} catch (Exception e)
{
e.printStackTrace();
}
}
}
把AntilazyLoad.class打包成jar包,然后写入App的私有目录,最后把该jar对应的dexElements文件插入到数组的最前面。
public class HotfixApplication extends Application
{
@Override
public void onCreate()
{
super.onCreate();
//创建jar对应的文件
File dexPath = new File(getDir("dex", Context.MODE_PRIVATE), "hackdex_dex.jar");
//将asset文件中的jar写到App的私有目录下面。
Utils.prepareDex(this.getApplicationContext(), dexPath, "hackdex_dex.jar");
// 把jar对应的dexElements插入到dex数组最前面。
HotFix.patch(this, dexPath.getAbsolutePath(), "dodola.hackdex.AntilazyLoad");
try
{
this.getClassLoader().loadClass("dodola.hackdex.AntilazyLoad");
} catch (ClassNotFoundException e)
{
e.printStackTrace();
}
}
}
创建jar对应的文件
public class Utils {
private static final int BUF_SIZE = 2048;
public static boolean prepareDex(Context context, File dexInternalStoragePath, String dex_file) {
BufferedInputStream bis = null;
OutputStream dexWriter = null;
bis = new BufferedInputStream(context.getAssets().open(dex_file));
dexWriter = new BufferedOutputStream(new FileOutputStream(dexInternalStoragePath));
byte[] buf = new byte[BUF_SIZE];
int len;
while ((len = bis.read(buf, 0, BUF_SIZE)) > 0) {
dexWriter.write(buf, 0, len);
}
dexWriter.close();
bis.close();
return true;
}
找相应的ClassLoader进行操作
public final class HotFix
{
public static void patch(Context context, String patchDexFile, String patchClassName)
{
if (patchDexFile != null && new File(patchDexFile).exists())
{
try
{
if (hasLexClassLoader())
{
injectInAliyunOs(context, patchDexFile, patchClassName);
} else if (hasDexClassLoader())
{
injectAboveEqualApiLevel14(context, patchDexFile, patchClassName);
} else
{
injectBelowApiLevel14(context, patchDexFile, patchClassName);
}
} catch (Throwable th)
{
}
}
}
}
Combine(合并)App的DexElements和AntilazyLoad.class的DexElements
private static boolean hasDexClassLoader()
{
try
{
Class.forName("dalvik.system.BaseDexClassLoader");
return true;
} catch (ClassNotFoundException e)
{
return false;
}
}
private static void injectAboveEqualApiLevel14(Context context, String str, String str2)
throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException
{
PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
Object a = combineArray(getDexElements(getPathList(pathClassLoader)),
getDexElements(getPathList(
new DexClassLoader(str, context.getDir("dex", 0).getAbsolutePath(), str, context.getClassLoader()))));
Object a2 = getPathList(pathClassLoader);
setField(a2, a2.getClass(), "dexElements", a);
pathClassLoader.loadClass(str2);
}
将Patch.jar补丁插入到APP中,过程和插入AntilazyLoad.class一样
public class HotfixApplication extends Application
{
@Override
public void onCreate()
{
super.onCreate();
File dexPath = new File(getDir("dex", Context.MODE_PRIVATE), "hackdex_dex.jar");
Utils.prepareDex(this.getApplicationContext(), dexPath, "hack_dex.jar");
HotFix.patch(this, dexPath.getAbsolutePath(), "dodola.hackdex.AntilazyLoad");
try
{
this.getClassLoader().loadClass("dodola.hackdex.AntilazyLoad");
} catch (ClassNotFoundException e)
{
e.printStackTrace();
}
dexPath = new File(getDir("dex", Context.MODE_PRIVATE), "path_dex.jar");
Utils.prepareDex(this.getApplicationContext(), dexPath, "path_dex.jar");
HotFix.patch(this, dexPath.getAbsolutePath(), "dodola.hotfix.BugClass");
}
}
四:总结
(1)因为我们的Patch是以独立的jar包,插入到APP的DexElements中,
所以如果APP中中的类引用了Patch中的类,就会在校验时报错。因为当进行dex校验时,如果两个相关联的类在不同的dex中就会报错。(LoadBugClass引用BugClass)。
(2) 为了防止上述错误就要阻止Dex校验,阻止Dex校验的方法是阻止相关类打上CLASS_ISPREVERIFIED标志。
(3) 阻止相关类打上CLASS_ISPREVERIFIED标志的做法是:在相关引用的类(LoadBugClass.class)的构造方法中,引用另外一个jar中类AntilazyLoad.class
(4)因为AntilazyLoad.class在另一个jar中,所以需要把该jar对应的dex插入到App中。并且在Application中的onCreate()方法中将该类加载进来。
(5)把Patch.jar对应的dexElement加载进App中。因为Patch.jar放在dex数组的第一个位置,所以首先被加载。即:如果在不同的dex中有相同的类存在,那么会优先选择排在前面的dex文件的类。