Tinker热修复原理实现
作者:互联网
热修复:
方案1: 在已加载类直接替换原有方法, 在原有类的基础上进行修改,无法实现对原有类的进行方法和 字段增减
AndFix 会出现部分机型 上热修复失效, 不稳定
类加载方案2:
APP重新启动,让ClassLoader加载新类
1. App 类加载器 ClassLoader下 子类 BaseDexClassLoader
加载 dexElements(classex dex,classex2 dex.....)
Elements数组:
fixed.dex classes.dex class2.dex
修复包 主包 bug 包
修复类 Caluator.class 被加载以后, 后面有 bug的 Caluator.class 不会加载了
private Element[] dexElements 里面就是一个一个.dex
android 源码阅读, BaseDexClassLoader源码:
30public class BaseDexClassLoader extends ClassLoader {
31
32 /**
33 * Hook for customizing how dex files loads are reported.
34 *
35 * This enables the framework to monitor the use of dex files. The
36 * goal is to simplify the mechanism for optimizing foreign dex files and
37 * enable further optimizations of secondary dex files.
38 *
39 * The reporting happens only when new instances of BaseDexClassLoader
40 * are constructed and will be active only after this field is set with
41 * {@link BaseDexClassLoader#setReporter}.
42 */
43 /* @NonNull */ private static volatile Reporter reporter = null;
44
45 private final DexPathList pathList;
46
47 /**
48 * Constructs an instance.
49 * Note that all the *.jar and *.apk files from {@code dexPath} might be
50 * first extracted in-memory before the code is loaded. This can be avoided
51 * by passing raw dex files (*.dex) in the {@code dexPath}.
52 *
53 * @param dexPath the list of jar/apk files containing classes and
54 * resources, delimited by {@code File.pathSeparator}, which
55 * defaults to {@code ":"} on Android.
56 * @param optimizedDirectory this parameter is deprecated and has no effect
57 * @param librarySearchPath the list of directories containing native
58 * libraries, delimited by {@code File.pathSeparator}; may be
59 * {@code null}
60 * @param parent the parent class loader
61 */
62 public BaseDexClassLoader(String dexPath, File optimizedDirectory,
63 String librarySearchPath, ClassLoader parent) {
64 super(parent);
65 this.pathList = new DexPathList(this, dexPath, librarySearchPath, null);
66
67 if (reporter != null) {
68 reportClassLoaderChain();
69 }
70 }
71 }
DexPathList:
final class DexPathList {
51 private static final String DEX_SUFFIX = ".dex";
52 private static final String zipSeparator = "!/";
53
54 /** class definition context */
55 private final ClassLoader definingContext;
56
57 /**
58 * List of dex/resource (class path) elements.
59 * Should be called pathElements, but the Facebook app uses reflection
60 * to modify 'dexElements' (http://b/7726934).
61 */
62 private Element[] dexElements;
}
makeDexElements()方法:
final class DexPathList {
private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
309 List<IOException> suppressedExceptions, ClassLoader loader) {
310 Element[] elements = new Element[files.size()];
311 int elementsPos = 0;
312 /*
313 * Open all files and load the (direct or contained) dex files up front.
314 */
315 for (File file : files) {
316 if (file.isDirectory()) {
317 // We support directories for looking up resources. Looking up resources in
318 // directories is useful for running libcore tests.
319 elements[elementsPos++] = new Element(file);
320 } else if (file.isFile()) {
321 String name = file.getName();
322
323 if (name.endsWith(DEX_SUFFIX)) {
324 // Raw dex file (not inside a zip/jar).
325 try {
326 DexFile dex = loadDexFile(file, optimizedDirectory, loader, elements);
327 if (dex != null) {
328 elements[elementsPos++] = new Element(dex, null);
329 }
330 } catch (IOException suppressed) {
331 System.logE("Unable to load dex file: " + file, suppressed);
332 suppressedExceptions.add(suppressed);
333 }
334 } else {
335 DexFile dex = null;
336 try {
337 dex = loadDexFile(file, optimizedDirectory, loader, elements);
338 } catch (IOException suppressed) {
339 /*
340 * IOException might get thrown "legitimately" by the DexFile constructor if
341 * the zip file turns out to be resource-only (that is, no classes.dex file
342 * in it).
343 * Let dex == null and hang on to the exception to add to the tea-leaves for
344 * when findClass returns null.
345 */
346 suppressedExceptions.add(suppressed);
347 }
348
349 if (dex == null) {
350 elements[elementsPos++] = new Element(file);
351 } else {
352 elements[elementsPos++] = new Element(dex, file);
353 }
354 }
355 } else {
356 System.logW("ClassLoader referenced unknown path: " + file);
357 }
358 }
359 if (elementsPos != elements.length) {
360 elements = Arrays.copyOf(elements, elementsPos);
361 }
362 return elements;
363 }
}
2. 创建BaseDexClassLoader 子类 DexClassLoader
加载已经修复好的 class2.dex (网路下载)
把自己的 dex 和系统的 dexElement 进行合并, 修复好的 dex的索引为 0
放射技术,复制给系统的 pathList
2. 环境模拟:
Android学习笔记----解决“com.android.dex.DexIndexOverflowException: method ID not in [0, 0xffff]: 65536”问题
问题: 应用方法数超过最大数 65536, 因为 DVM Bytecode限制, DVM 指令集的方法调用 指令 invoke-kind 索引为 16 bits
分包机制:
AndroidManifest.xml
apply plugin: 'com.android.application'
android {
compileSdkVersion 28
defaultConfig {
applicationId "ndkdemo.denganzhi.com.myapplication"
minSdkVersion 15
targetSdkVersion 28
versionCode 1
versionName "1.1"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
// 1. 开启分包
multiDexEnabled true
// 2. 设置分包配置文件, classes.dex 主包中放哪些类
multiDexKeepFile file('multidex.keep')
}
// 3. 配置分包参数
dexOptions {
javaMaxHeapSize "4g"
preDexLibraries = false
additionalParameters = [ // 配置multidex参数
'--multi-dex', // 多dex分包
'--set-max-idx-number=50000', // 每个包内方法数上限
'--main-dex-list=' + '/multidex.keep', // 打包到主classes.dex的文件列表
'--minimal-main-dex'
]
}
}
dependencies {
// 4. 配置分包依赖
implementation 'com.android.support:multidex:1.0.3'
}
3. MyApplication.java 配置
public class MyApplicaton extends Application {
@Override
public void onCreate() {
super.onCreate();
}
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
MultiDex.install(this);
// FixDexUtils.loadFixedDex(this);
}
}
在 app 目录下创建 multidex.keep
ndkdemo/denganzhi/com/myapplication/BaseActivity.class
ndkdemo/denganzhi/com/myapplication/MyApplicaton.class
ndkdemo/denganzhi/com/myapplication/MainActivity.class
编译正确代码 把 classes2.dex 拷贝出来, 上传到android sd卡下 adb push C:\Users\denganzhi\Desktop\classes2.dex /sdcard/
奔溃代码: Calulator.java
package ndkdemo.denganzhi.com.myapplication.utils;
import android.content.Context;
import android.widget.Toast;
public class Calculator {
public void calculate(Context context){
int a = 666;
int b = 0;
Toast.makeText(context, "calculate >>> " + a / b, Toast.LENGTH_SHORT).show();
}
}
public void crashClick(View view){
Calculator calculator=new Calculator();
calculator.calculate(this);
}
3. 热修复代码实现:
public void fix(View view){
// /storage/emulated/0
//1 从服务器下载dex文件 比如v1.1修复包文件(classes2.dex)
File sourceFile = new File(Environment.getExternalStorageDirectory(), "classes2.dex");
// 2. 把 classes2.dex 拷贝到 data/user/0/包名/app_odex 目录下
// 目标路径:私有目录
//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();
}
// 3. 使用 DexClassLoader 加载 自己的 class.dex
// 插入到 dexElements 的头部 ,最开始加载
try {
// 复制dex到私有目录
FileUtils.copyFile(sourceFile, targetFile);
Toast.makeText(this, "复制到私有目录 完成", Toast.LENGTH_SHORT).show();
FixDexUtils.loadFixedDex(this);
} catch (IOException e) {
e.printStackTrace();
}
}
FileUtils.java
package ndkdemo.denganzhi.com.myapplication.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.java
package ndkdemo.denganzhi.com.myapplication.utils;
import android.content.Context;
import android.util.Log;
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) {
//初始化类加载器
// dex.getAbsolutePath() classes2.dex路径
// dex:/data/user/0/ndkdemo.denganzhi.com.myapplication/app_odex/classes2.dex\
// optimizedDirectory:/data/user/0/ndkdemo.denganzhi.com.myapplication/app_odex/opt_dex
Log.e("denganzhi","dex:"+ dex.getAbsolutePath()+
" optimizedDirectory:"+optimizedDirectory);
/*
* DexClassLoader当然也是一种ClassLoader,但本身属于顾名思义是用来加载Dex文件的,是安卓系统独有的一种类加载器
* dex.getAbsolutePath(): 包含dex文件的jar包或apk文件路径
* optimizedDirectory: 要求一个应用私有可写的目录去缓存编译的class文件
* 释放目录,可以理解为缓存目录,必须为应用私有目录,不能为空
*/
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数组对象
// 获取 dalvik.system.BaseDexClassLoader 中 dexElements 属性
Object myDexElements = ReflectUtils.getDexElements(
ReflectUtils.getPathList(myClassLoader));
// 获取系统的DexElements数组对象
// 通过反射获取BaseDexClassLoader对象中的PathList对象,再获取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();
}
}
}
ArrayUtils.java 代码实现:
package ndkdemo.denganzhi.com.myapplication.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;
}
}
ReflectUtils.java代码实现:
package ndkdemo.denganzhi.com.myapplication.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");
}
}
MyApplicaton.java 代码:
public class MyApplicaton extends Application {
@Override
public void onCreate() {
super.onCreate();
}
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
MultiDex.install(this);
// App 每次启动都要 加载class2.dex 文件
FixDexUtils.loadFixedDex(this);
}
}
源码下载: https://download.csdn.net/download/dreams_deng/12439069
标签:dex,file,修复,public,Tinker,File,new,原理,class 来源: https://blog.csdn.net/dreams_deng/article/details/106215171