Android-Tinker热修复原理
前言
最近工作挺忙的,所以文章也停了一段时间,前段时间也一直在抽空继续学习ndk开发,学习ffmpeg编译出 Android平台静态库后的使用。
这篇Tinker热修复,很早前准备的,但是一直没弄完,今天正好回顾了下,整理了出来,希望记录下。
简介
正常的开发流程与热修复开发流程的对比,如图:
热修复的优势:
1、无需重新发布新版本,省时省力;2、 用户无感知修复,也无需下载最新应用,代价小;3、 修复成功率高,把损失降到最低;
目前市面上热修复的框架对比:
我们进来就来看看这个Tinker热修复;这类来看看上层的类修复,不去看NDK层的修复了。
问: 为什么Tinker热修复需要冷启动才能生效?
答: 因为ClassLoader类加载器里面的loadclass方法里面调用的findLoaderClass方法是查找已经加载的类;
源码浅析(类加载器)
DexClassLoader
.java文件编译成class文件,然后在我们Android里面通过虚拟机通过dex文件来加载;
DexClassLoader继承自BaseDexClassLoader;
apk是通过这个DexClassLoader加载起来进行加载的;
package dalvik.system;
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
}
构造方法中创建出DexPathList这个对象,这个对象看名字就知道是存放Dex文件路径的集合;
public class BaseDexClassLoader extends ClassLoader {
private final DexPathList pathList;
......
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, librarySearchPath, null);
if (reporter != null) {
reporter.report(this.pathList.getDexPaths());
}
}
......
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
//通过DexPathList对象调用findClass方法获取类对象
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;
}
......
}
dexElements的数组存放了需要加载的dex文件,通过makeDexElement方法去初始化这个数组;
makeDexElement方法中遍历传入的dex文件,判断是文件并且文件名以.dex结尾,然后通过loadDexFile方法,去加载dex文件,然后加入到这个dexElements数组中;
final class DexPathList {
private static final String DEX_SUFFIX = ".dex";
//这个数组存放需要加载的dex文件
private Element[] dexElements;
......
//构造方法
public DexPathList(ClassLoader definingContext, String dexPath,
String librarySearchPath, File optimizedDirectory) {
......
//调用makeDexElements方法创建出这个数组
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
suppressedExceptions, definingContext);
.......
}
private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
List<IOException> suppressedExceptions, ClassLoader loader) {
Element[] elements = new Element[files.size()];
int elementsPos = 0;
/*
* Open all files and load the (direct or contained) dex files up front.
*/
for (File file : files) {
if (file.isDirectory()) { //如果是文件夹
elements[elementsPos++] = new Element(file);
} else if (file.isFile()) { //如果是文件
String name = file.getName();
//文件名以dex结尾
if (name.endsWith(DEX_SUFFIX)) {
try {
//调用loadDexFile方法来加载dex文件
//optimizedDirectory临时解压目录
DexFile dex = loadDexFile(file, optimizedDirectory, loader, elements);
if (dex != null) {
elements[elementsPos++] = new Element(dex, null);
}
} catch (IOException suppressed) {
System.logE("Unable to load dex file: " + file, suppressed);
suppressedExceptions.add(suppressed);
}
} else {
DexFile dex = null;
try {
dex = loadDexFile(file, optimizedDirectory, loader, elements);
} catch (IOException suppressed) {
suppressedExceptions.add(suppressed);
}
if (dex == null) {
elements[elementsPos++] = new Element(file);
} else {
elements[elementsPos++] = new Element(dex, file);
}
}
} else {
System.logW("ClassLoader referenced unknown path: " + file);
}
}
if (elementsPos != elements.length) {
//copy合并两个数组,得到新的dex文件的数组
elements = Arrays.copyOf(elements, elementsPos);
}
//将创建并添加上加载好的dex文件的数组返回出去
return elements;
}
......
private static DexFile loadDexFile(File file, File optimizedDirectory, ClassLoader loader,
Element[] elements)
throws IOException {
if (optimizedDirectory == null) {
return new DexFile(file, loader, elements);
} else {
String optimizedPath = optimizedPathFor(file, optimizedDirectory);
return DexFile.loadDex(file.getPath(), optimizedPath, 0, loader, elements);
}
}
......
}
小结
通过上面的源码:
我们能了解到DexPathList类中,有一个dexElements的数组存放了需要加载的dex文件,通过makeDexElement方法去初始化这个数组,makeDexElement方法中遍历传入的dex文件,判断是文件并且文件名以.dex结尾,然后通过loadDexFile方法,去加载dex文件,然后加入到这个dexElements数组中;
App安装成功后,系统会复制一份apk文件到私有目录(data/app/包名~xxx/base.apk)
思路
大体的思路
将修复好的dex文件下载到本机,并插桩式插入到dexElements数组中的前面,这个时候就会先加载我们修复好的dex文件;系统加载完修复好的dex中的class,后面出现相同的class就会跳过,跳过的
思路细节
1、创建BaseDexClassLoader子类DexClassLoader,也就是创建我们自己的类加载器;
2、加载修复好的dex文件(服务器网络下载),并复制文件到私有目录;
3、将自己的dex和系统的dexElements数组进行合并(要将修复的dex的索引放置到0);
4、反射技术,将合并出来的dexElements数组赋值给系统的pathList;
Tinker的dex修复
SystemClassLoaderAdder 类
这个类里面做了不同Android系统的适配,因为适配的系统版本里面需要反射的点不一样;
package com.tencent.tinker.loader;
...
...
public class SystemClassLoaderAdder {
public static void installDexes(Application application, ClassLoader loader, File dexOptDir, List<File> files,
boolean isProtectedApp, boolean useDLC) throws Throwable {
ShareTinkerLog.i(TAG, "installDexes dexOptDir: " + dexOptDir.getAbsolutePath() + ", dex size:" + files.size());
if (!files.isEmpty()) {
files = createSortedAdditionalPathEntries(files);
//获取传入进来的类加载器
ClassLoader classLoader = loader;
if (Build.VERSION.SDK_INT >= 24 && !isProtectedApp) {
classLoader = NewClassLoaderInjector.inject(application, loader, dexOptDir, useDLC, files);
} else {
injectDexesInternal(classLoader, files, dexOptDir);
}
//install done
sPatchDexCount = files.size();
ShareTinkerLog.i(TAG, "after loaded classloader: " + classLoader + ", dex size:" + sPatchDexCount);
if (!checkDexInstall(classLoader)) {
//reset patch dex
SystemClassLoaderAdder.uninstallPatchDex(classLoader);
throw new TinkerRuntimeException(ShareConstants.CHECK_DEX_INSTALL_FAIL);
}
}
}
// 适配了不同版本
static void injectDexesInternal(ClassLoader cl, List<File> dexFiles, File optimizeDir) throws Throwable {
if (Build.VERSION.SDK_INT >= 23) {
V23.install(cl, dexFiles, optimizeDir);
} else if (Build.VERSION.SDK_INT >= 19) {
V19.install(cl, dexFiles, optimizeDir);
} else if (Build.VERSION.SDK_INT >= 14) {
V14.install(cl, dexFiles, optimizeDir);
} else {
V4.install(cl, dexFiles, optimizeDir);
}
}
......
......
}
V23\V19.install方法
install的做法就是,先获取BaseDexClassLoader的dexPathList对象,然后通过dexPathList的makeDexElements函数将我们要安装的dex转化成Element[]对象,最后将其和dexPathList的dexElements对象进行合并,就是新的Element[]对象,因为我们添加的dex都被放在dexElements数组的最前面,所以当通过findClass来查找这个类时,就是使用的我们最新的dex里面的类。
看看他的注释:
dalvik.system.BaseDexClassLoader. We modify its:我们修改它(BaseDexClassLoader);
dalvik.system.DexPathList pathList field to append additional DEX: DexPathList pathList字段,用于追加额外的DEX文件条目。
package com.tencent.tinker.loader;
...
...
public class SystemClassLoaderAdder {
......
......
/**
* Installer for platform versions 23.
*/
private static final class V23 {
private 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.
*/
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) {
ShareTinkerLog.w(TAG, "Exception in makePathElement", e);
throw e;
}
}
}
......
......
/**
* Installer for platform versions 19.
*/
private static final class V19 {
private 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.
*/
Field pathListField = ShareReflectUtil.findField(loader, "pathList");
Object dexPathList = pathListField.get(loader);
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
ShareReflectUtil.expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,
new ArrayList<File>(additionalClassPathEntries), optimizedDirectory,
suppressedExceptions));
if (suppressedExceptions.size() > 0) {
for (IOException e : suppressedExceptions) {
ShareTinkerLog.w(TAG, "Exception in makeDexElement", e);
throw e;
}
}
}
}
代码实现
工具类-ReflectUtils
package com.lk.tinker_demo.utils;
import java.lang.reflect.Field;
public class ReflectUtils {
/**
* 通过反射获取某对象,并设置私有可访问
*
* @param obj 该属性所属类的对象
* @param clazz 该属性所属类
* @param field 属性名
* @return 该属性对象
*/
private static Object getField(Object obj, Class<?> clazz, String field)
throws NoSuchFieldException, IllegalAccessException, IllegalArgumentException {
Field localField = clazz.getDeclaredField(field);
localField.setAccessible(true);
return localField.get(obj);
}
/**
* 给某属性赋值,并设置私有可访问
*
* @param obj 该属性所属类的对象
* @param clazz 该属性所属类
* @param value 值
*/
public static void setField(Object obj, Class<?> clazz, Object value)
throws NoSuchFieldException, IllegalAccessException, IllegalArgumentException {
Field localField = clazz.getDeclaredField("dexElements");
localField.setAccessible(true);
localField.set(obj, value);
}
/**
* 通过反射获取BaseDexClassLoader对象中的PathList对象
*
* @param baseDexClassLoader BaseDexClassLoader对象
* @return PathList对象
*/
public static Object getPathList(Object baseDexClassLoader)
throws NoSuchFieldException, IllegalAccessException, IllegalArgumentException, ClassNotFoundException {
return getField(baseDexClassLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
}
/**
* 通过反射获取BaseDexClassLoader对象中的PathList对象,再获取dexElements对象
*
* @param paramObject PathList对象
* @return dexElements对象
*/
public static Object getDexElements(Object paramObject)
throws NoSuchFieldException, IllegalAccessException, IllegalArgumentException {
return getField(paramObject, paramObject.getClass(), "dexElements");
}
}
工具类-ArrayUtils
package com.lk.tinker_demo.utils;
import java.lang.reflect.Array;
public class ArrayUtils {
/**
* 合并数组
*
* @param arrayLhs 前数组(插队数组)
* @param arrayRhs 后数组(已有数组)
* @return 处理后的新数组
*/
public static Object combineArray(Object arrayLhs, Object arrayRhs) {
// 获得一个数组的Class对象,通过Array.newInstance()可以反射生成数组对象
Class<?> localClass = arrayLhs.getClass().getComponentType();
// 前数组长度
int i = Array.getLength(arrayLhs);
// 新数组总长度 = 前数组长度 + 后数组长度
int j = i + Array.getLength(arrayRhs);
// 生成数组对象
Object result = Array.newInstance(localClass, j);
for (int k = 0; k < j; ++k) {
//先把自己的放入数组
if (k < i) {
// 从0开始遍历,如果前数组有值,添加到新数组的第一个位置
Array.set(result, k, Array.get(arrayLhs, k));
} else {
// 添加完前数组,再添加后数组,合并完成
Array.set(result, k, Array.get(arrayRhs, k - i));
}
}
return result;
}
}
工具类-FixDexUtils
package com.lk.tinker_demo.utils;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class FileUtils {
/**
* 复制文件
*
* @param sourceFile 源文件
* @param targetFile 目标文件
* @throws IOException IO异常
*/
public static void copyFile(File sourceFile, File targetFile)
throws IOException {
// 新建文件输入流并对它进行缓冲
FileInputStream input = new FileInputStream(sourceFile);
BufferedInputStream inBuff = new BufferedInputStream(input);
// 新建文件输出流并对它进行缓冲
FileOutputStream output = new FileOutputStream(targetFile);
BufferedOutputStream outBuff = new BufferedOutputStream(output);
// 缓冲数组
byte[] b = new byte[1024 * 5];
int len;
while ((len = inBuff.read(b)) != -1) {
outBuff.write(b, 0, len);
}
// 刷新此缓冲的输出流
outBuff.flush();
// 关闭流
inBuff.close();
outBuff.close();
output.close();
input.close();
}
}
核心步骤类-FixDexUtils
package com.lk.tinker_demo.utils;
import android.content.Context;
import java.io.File;
import java.util.HashSet;
import dalvik.system.DexClassLoader;
import dalvik.system.PathClassLoader;
public class FixDexUtils {
//存放需要修复的dex集合
private static HashSet<File> loadedDex = new HashSet<>();
static {
//修复前先清空
loadedDex.clear();
}
public static void loadFixedDex(Context context) {
if (context == null)
return;
//dex文件目录
File fileDir = context.getDir("odex", Context.MODE_PRIVATE);
File[] files = fileDir.listFiles();
for (File file : files) {
if (file.getName().endsWith(".dex") && !"classes.dex".equals(file.getName())) {
//找到要修复的dex文件
loadedDex.add(file);
}
}
//创建类加载器
createDexClassLoader(context, fileDir);
}
/**
* 创建类加载器
*
* @param context
* @param fileDir
*/
private static void createDexClassLoader(Context context, File fileDir) {
String optimizedDirectory = fileDir.getAbsolutePath() + File.separator + "opt_dex";
File fOpt = new File(optimizedDirectory);
if (!fOpt.exists()) {
fOpt.mkdirs();
}
DexClassLoader classLoader;
for (File dex : loadedDex) {
//初始化类加载器
classLoader = new DexClassLoader(dex.getAbsolutePath(), optimizedDirectory, null,
context.getClassLoader());
//热修复
hotFix(classLoader, context);
}
}
private static void hotFix(DexClassLoader myClassLoader, Context context) {
//系统的类加载器
PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
try {
//重要的来了
// 获取自己的DexElements数组对象
Object myDexElements = ReflectUtils.getDexElements(
ReflectUtils.getPathList(myClassLoader));
// 获取系统的DexElements数组对象
Object sysDexElements = ReflectUtils.getDexElements(
ReflectUtils.getPathList(pathClassLoader));
// 合并
Object dexElements = ArrayUtils.combineArray(myDexElements, sysDexElements);
// 获取系统的 pathList
Object sysPathList = ReflectUtils.getPathList(pathClassLoader);
// 重新赋值给系统的 pathList
ReflectUtils.setField(sysPathList, sysPathList.getClass(), dexElements);
} catch (Exception e) {
e.printStackTrace();
}
}
}
调用方法
private void fixBug() {
//1 从服务器下载dex文件 比如v1.1修复包文件(classes2.dex)
File sourceFile = new File(Environment.getExternalStorageDirectory(), "classes2.dex");
// 目标路径:私有目录
//getDir("odex", Context.MODE_PRIVATE) data/user/0/包名/app_odex
File targetFile = new File(getDir("odex",
Context.MODE_PRIVATE).getAbsolutePath() + File.separator + "classes2.dex");
if (targetFile.exists()) {
targetFile.delete();
}
try {
// 复制dex到私有目录
FileUtils.copyFile(sourceFile, targetFile);
Toast.makeText(this, "复制到私有目录完成", Toast.LENGTH_SHORT).show();
//调用加载dex修复
FixDexUtils.loadFixedDex(this);
} catch (IOException e) {
e.printStackTrace();
}
}
dex文件的生成,可以找到对应的class文件(app/build/intermediates/javac/debug/classes/包名/修复的class文件),记得要先Make Project 生成最新的class文件,然后然后通过SDK里面带的dx.bat工具 打出dex文件;
具体步骤可以看看:Android 下 class文件 转 dex(转载)
辛苦各位童鞋观看到最后,如果博客中有不对的地方望指出,大神勿喷,谢谢~~ 一起学习和进步