前言
最近公司打算自己在Tinker的基础上做二次开发,因为TinkerPatch是要收费的,而且价格不菲,说白了就是想白嫖!
顺便研究了下的Tineker类修复的原理,本文只涉及类的热修复,至于资源以及so库的热修复有时间再研究吧!
正文
在进入主题之前我们先来了解下Android的 ClassLoder,android 的 ClassLoader 主要又以下几种:PathClassLoader、DexClassLoader等,我们在Application中通过getClassLoader()得知ClassLode是 PathClassLoader ,Tinker就是通过处理PathClassLoader来实现类的热修复。先来看看 PathClassLoader 源码,PathClassLoader是继承BaseDexClassLoader重要的操作都在里面,ClassLoader 重要的方法就是findClass,如下:
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
Class c = pathList.findClass(name, suppressedExceptions);
if (c == null) {
ClassNotFoundException cnfe = new ClassNotFoundException(
"Didn't find class \"" + name + "\" on path: " + pathList);
for (Throwable t : suppressedExceptions) {
cnfe.addSuppressed(t);
}
throw cnfe;
}
return c;
}
可以看到,最终的逻辑在DexPathList的findClass()里面,如下:
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;
}
通过遍历dexElements执行 Element 的 findClass() ,只要找到就返回。Element可以理解为一个dex文件的表示,Tinker就是通过发射修改 dexElements 这个 Element 数组,将补丁里面的dex对应的 Element 插入到 dexElements 前面来实现类的热修复;比如 patch.dex 里面有 A.class ,原来的旧的apk里面也有 A.class ,由于 patch.dex 对应的 Element 在前面,每次调用 A.class 的方法时都是走的 patch.dex 里面的逻辑。以下是 Tinker 里扒下来的关键代码,做了部分删减,这个是 android6.0 及其以上的设备:
public class SystemClassLoaderAdder {
private static final String TAG = "SystemClassLoaderAdder";
public static void install(ClassLoader loader, List<File> additionalClassPathEntries,
File optimizedDirectory)
throws IllegalArgumentException, IllegalAccessException,
NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException {
/* The patched class loader is expected to be a descendant of
* dalvik.system.BaseDexClassLoader. We modify its
* dalvik.system.DexPathList pathList field to append additional DEX
* file entries.
*/
Log.d(TAG, "ClassLoader =" + loader.getClass().getCanonicalName());
Field pathListField = ShareReflectUtil.findField(loader, "pathList");
Object dexPathList = pathListField.get(loader);
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
ShareReflectUtil.expandFieldArray(dexPathList, "dexElements", makePathElements(dexPathList,
new ArrayList<File>(additionalClassPathEntries), optimizedDirectory,
suppressedExceptions));
if (suppressedExceptions.size() > 0) {
for (IOException e : suppressedExceptions) {
Log.w(TAG, "Exception in makePathElement", e);
throw e;
}
}
}
/**
* A wrapper around
* {@code private static final dalvik.system.DexPathList#makePathElements}.
*/
private static Object[] makePathElements(
Object dexPathList, ArrayList<File> files, File optimizedDirectory,
ArrayList<IOException> suppressedExceptions)
throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
Method makePathElements = null;
try {
makePathElements = ShareReflectUtil.findMethod(dexPathList, "makePathElements", List.class, File.class,
List.class);
} catch (NoSuchMethodException e) {
Log.e(TAG, "NoSuchMethodException: makePathElements(List,File,List) failure");
try {
makePathElements = ShareReflectUtil.findMethod(dexPathList, "makePathElements", ArrayList.class, File.class, ArrayList.class);
} catch (NoSuchMethodException e1) {
Log.e(TAG, "NoSuchMethodException: makeDexElements(ArrayList,File,ArrayList) failure");
}
}
return (Object[]) makePathElements.invoke(dexPathList, files, optimizedDirectory, suppressedExceptions);
}
在 HotFixApplication 的 attachBaseContext() 中增加如下代码:
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
List<File> fileList = new ArrayList<>(1);
File patchFile = new File(getCacheDir().getAbsolutePath() + "/classes.dex");
if (patchFile.exists()) {
fileList.add(patchFile);
}
patchFile = new File(getCacheDir().getAbsolutePath() + "/classes2.dex");
if (patchFile.exists()) {
fileList.add(patchFile);
}
if (fileList.size() > 0) {
try {
SystemClassLoaderAdder.install(getClassLoader(), fileList, new File(getDataDir().getAbsolutePath() + "/odex"));
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
在classes.dex、classes2.dex是修改之后的apk解压之后出来,这里为了图方便,直接把补丁的 dex 放在 cache 目录下,杀掉进程,重启就会生效。