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

猜你喜欢

转载自blog.csdn.net/dreams_deng/article/details/106215171