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(转载)

辛苦各位童鞋观看到最后,如果博客中有不对的地方望指出,大神勿喷,谢谢~~ 一起学习和进步

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值