前言
热修复出现这么久,一直没有机会在项目中接触到,现在趁还不是很忙的时候来一探究竟。众所周知,热修复的就是在不发版的情况还能替换运行的代码,实现不发版还能解决一些小bug的场景,发布一次的代价是巨大的,如此一来好处真是多了去,但是在详细了解背后,隐患还是有的,不说别的,至少Google Play上线app就不允许,不过带着对技术的好奇还是来一探究竟。如果真有需要应用的场景,当下腾讯的Tinker和阿里的Sophix都是比较成熟的框架,根据开发文档集成入项目即可,前者免费后者收费。
大纲
1.实现原理
2.简易实现
3.混淆问题解决和gradle脚本
4.类加载校验问题解决
5.Android N混编问题解决
6.总结
1.实现原理
具体实现原理不同框架各不相同,详情可参照其他博客详细解说,在此仅用简单一句话概括
1.Tinker,需重启应用,使用dexdif差分出补丁dex和基准apk差异,生成出查分包在用户端生成新dex,替换旧dex加载实现;应用最广泛
2.AndFix,不需重启应用,底层修改相关结构体成员指向修改后成员;已停止维护
3.Robust,不需重启应用,所有类插入静态变量,所有方法插入控制逻辑,当不需要替换时静态变量为null走正常逻辑,若出现问题使静态变量不为null,从而走控制逻辑;增加代码量但最有效
4.QZone,需重启应用,生成改动后的dex,在用户端加载初期通过反射操作classLoader,在其dexElements头部插入补丁dex,达到替换目标类
本文主要讲述仿照Qzone方案如何实现
2.简易实现
首先要理解classLoader加载机制,先检验自己缓存是否加载,然后双亲委托机制查看自己父类是否加载,若都没有则自己加载。
//Android9.0 ClassLoader.java
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
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.
c = findClass(name);
}
}
return c;
}
然后是遍历从dexElement中加载到dex也就是要加载类,dexElements是个数组,因此我们只需将我们的补丁dex插入到该数组头部即可完成替换。
//Android 9.0 DexPathList.java
public Class<?> findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
Class<?> clazz = element.findClass(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
这里值得注意的是,要反射操作系统类一定要做好版本适配,目前看来5.0以下、5.0 - 7.0、7.0以上这个三个区间相关函数都稍许不同,应该根据不同api获取正确函数;在此仅根据9.0适配。
屡一下需要反射操作的类和成员以及方法
//Android 9.0
//BaseDexClassLoader.java
private final DexPathList pathList;
//DexPathList.java
//最终需要加入补丁的数组
private Element[] dexElements;
//补丁file生成Element[]的函数
private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
List<IOException> suppressedExceptions, ClassLoader loader, boolean isTrusted) {...}
明确操作步骤了就可以写我们的代码了
public static void init(Application application, String dexPath) {
//获取PathClassLoader
ClassLoader classLoader = application.getClassLoader();
try {
//获取PathClassLoader父类BaseDexClassLoader的pathList
Object pathList = ReflectionUtils.getClassLoaderPathList(classLoader.getClass(), classLoader);
//获取DexPathList的dexElements
Field dexElementsF = ReflectionUtils.getFiledObj(pathList.getClass(), "dexElements", pathList);
Object[] dexElements = (Object[]) dexElementsF.get(pathList);
//获取DexPathList的makeDexElements方法
Method makeDexElementsM = ReflectionUtils.getMethod(pathList.getClass(), "makeDexElements", List.class, File.class, List.class, ClassLoader.class);
//makeDexElements第一个参数,dexFile的list
List<File> files = new ArrayList<>();
files.add(new File(dexPath));
//makeDexElements第二个参数,输出路径
File dexOutDir = application.getCacheDir();
//makeDexElements第三个参数为IOException,传空list即可;第四个为当前classLoader实例
//创建出我们自己的Element[]
Object[] patchDexElements = (Object[]) makeDexElementsM.invoke(dexElements, files, dexOutDir, new ArrayList<>(), classLoader);
//创建一个新的Element[]
Object[] newDexElements = (Object[]) Array.newInstance(dexElements.getClass().getComponentType(), dexElements.length + patchDexElements.length);
//将自己的Element[]和旧Element[]合并为新Element[]
//顺序不能错,[0]:patchDexElements,[1]:dexElements
System.arraycopy(patchDexElements, 0, newDexElements, 0, patchDexElements.length);
System.arraycopy(dexElements, 0, newDexElements, patchDexElements.length, dexElements.length);
//使用新Element[]替换旧的Element[]
dexElementsF.set(pathList, newDexElements);
} catch (IllegalAccessException | NoSuchFieldException | NoSuchMethodException | InvocationTargetException | NullPointerException e) {
e.printStackTrace();
}
}
/**
* 从当前class获取filed,若找不到就从父类找,主要适配classLoader
*
* @param className
* @return
*/
public static Object getClassLoaderPathList(@NonNull Class<?> className, ClassLoader classLoader) throws IllegalAccessException {
Log.e("className1:", className.getSimpleName());
Object object = null;
try {
Field field = className.getDeclaredField(NAME_PATH_LIST);
field.setAccessible(true);
object = field.get(classLoader);
} catch (NoSuchFieldException e) {
//pathList是BaseDexClassLoader的属性,classLoader是PathClassLoader,因此则需从其父类BaseDexClassLoader寻找
if (className.getSuperclass() != null) {
object = getClassLoaderPathList(className.getSuperclass(), classLoader);
} else {
e.printStackTrace();
}
}
return object;
}
public static Field getFiledObj(@NonNull Class<?> className, String filedName, Object obj) throws NoSuchFieldException {
Log.e("className2:", className.getSimpleName());
Field field = null;
field = className.getDeclaredField(filedName);
field.setAccessible(true);
return field;
}
public static Method getMethod(@NonNull Class<?> className, String methodName, Class<?>... parameterTypes) throws NoSuchMethodException {
Log.e("className3:", className.getSimpleName());
Method method = null;
method = className.getDeclaredMethod(methodName, parameterTypes);
method.setAccessible(true);
return method;
}
最后手动配置下,MainActivity是bug代码
public class MainActivity extends AppCompatActivity {
private TextView tv_main;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tv_main = findViewById(R.id.tv_main);
throw new NullPointerException("bug");
//dx --dex --output=patch.dex dex
// tv_main.setText("Hello Hot fix 2!");
}
}
App里配置热修复逻辑
public class App extends Application {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
File patchFile = new File(getCacheDir(), "patch.dex");
Log.e("patch dir:", patchFile.getAbsolutePath() + ", " + patchFile.exists());
HotFixUtils.init(this, patchFile.getAbsolutePath());
}
}
运行就会crash抛出异常
修改后的MainActivity
public class MainActivity extends AppCompatActivity {
private TextView tv_main;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tv_main = findViewById(R.id.tv_main);
// throw new NullPointerException("bug");
//dx --dex --output=patch.dex dex
tv_main.setText("Hello Hot fix 2!");
}
}
rebuild一下去app\build\intermediates\javac\debug\compileDebugJavaWithJavac\classes\包名\下找到MainActivity.class,这里需注意不同gradle版本路径有所不同,只要找到编译后class文件即可。我这里使用的5.1.1。
找到后在当前目录下建立一个文件目录,将.class放入,如dex/com/zidian/hellohotfix/MainActivity.class
使用sdk\build-tools\版本号\dx工具将.class文件生成.dex文件,在包含上述dex文件夹路径下运行terminal指令(需配置build-tools环境变量,没有配置则需在dx工具所在路径运行,路径名需自行修改)
这样就在dex文件夹同级目录下生成了patch.dex
将这个文件push到我们App里设置的路径中,我的cache路径是/data/user/0/com.zidian.hellohotfix/cache/patch.dex,所以设备中应该是这样
7这样就设置完毕了,再来运行下我们app,发现就不闪退了,到此完成热修复的简易实现~
7.未完待续
之后的步骤需要涉及到gradle相关较深知识,还有ASM字节码插桩等;本想一笔带过以热修复实现为主,后面一想如果只是单纯照搬实现,不了解其中原理对学习起不到积极作用,所以实现暂且到此为止,我先去补习其他知识去~