手写热更新

手写热更新

热更新 / 热修复

不安装新版本的软件,直接从⽹络下载新功能模块来对软件进⾏局部更新

热更新和插件化的区别

区别有两点

  1. 插件化的内容在原 App 中没有,⽽热更新是原 App 中的内容做了改动

  2. 插件化在代码中有固定的⼊⼝,⽽热更新则可能改变任何⼀个位置的代码

热更新的原理

  • ClassLoader 的 dex ⽂件替换
  • 直接修改字节码

前置知识:loadClass() 的类加载过程

  • 宏观上:是⼀个带缓存的、从上到下的加载过程(即⽹上所说的「双亲委托机制」)

  • 对于具体的⼀个 ClassLo00000000ader:

    • 先从⾃⼰的缓存中取
    • ⾃⼰没有缓存,就找⽗ ClassLoader 要(parent.loadClass())
    • ⽗ View 也没有,就⾃⼰加载(fifindClass())
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    //参数resolve没用到,是java中的loadClass有两个参数,此处与它一致
    protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException{
    //First,check if the class has already been loaded
    Class<?> c = findLoadedClass(name);
    if(c == null){
    try{
    if(parent != null){
    c = parent.loadClass(name, false);
    } else {
    c = findBootstrapClassOrNull(name);
    }
    } catch (ClassNotFoundException e){
    //ClassNotFoundException thrown if class not found
    //from the non-null parent class loader
    }

    if(c == null){
    //If still not found, then invoke findClass in order
    //to find the class.
    c = findClass(name);
    }
    }
    return c;
    }
  • BaseDexClassLoader 或者它的⼦类(DexClassLoaderPathClassLoader 等)的 fifindClass()

    • 通过它的 pathList.fifindClass()
    • 它的 pathList.loadClass() 通过 DexPathListdexElementsfifindClass()
    • 所以热更新的关键在于,把补丁 dex ⽂件加载放进⼀个 Element,并且插⼊到dexElements 这个数组的前⾯(插⼊到后⾯的话会被忽略掉)【先加载过的class,之后再遇到相同class就不会再加载】
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    // src/main/assets/apk/27_hot_update-debug.apk
    File apk = new File(getCacheDir() + "/27_hot_update-debug.apk");
    if(!apk.exists()){
    try{
    InputStream is =getAssets().open("apk/27_hot_update-debug.apk");
    int size = is.available();
    byte[] buffer = new byte[size];
    is.read(buffer);
    is.close();

    FileOutputStream fos = new FileOutputStream(apk);
    fos.write(buffer);
    fos.close();
    } catch (Exception e){
    throw new RuntimeException(e);
    }
    }
    try{
    ClassLoader classLoader = getClassLoader();
    Class loaderClass = BaseDexClassLoader.class;
    Field pathListField = loaderClass.getDeclaredField("pathList");
    pathListField.setAccessible(true);
    Object pathListObject = pathListField.get(classLoader);
    Class pathListClass = pathListObject.getClass();
    Field dexElementsField = pathListClass.getDeclaredField("dexElements");
    dexElementsField.setAccessible(true);

    //classLoader.pathList.dexElements = ???
    PathClassLoader newClassLoader = new PathClassLoader(apk.getPath(), null);
    Object newPathListObject = pathListField.get(newClassLoader);
    Object newDexElementsObject = dexElementsField.get(newPathListObject);
    dexElementsField.set(pathListObject, newDexElementsObject);
    } catch (NoSuchFieldException e){
    e.printStackTrace();
    } catch (IllegalAccessException e){
    e.printStackTrace();
    }

⼿写热更新

  • 因为⽆法在更新之前就指定要更新谁;所以不能定义新的 ClassLoader,⽽只能选择对 ClassLoader 进⾏修改,让它能够加载补丁⾥⾯的类

  • 因为补丁的类在原先的 App 中已经存在,所以应该把补丁的 Element 对象插⼊到 dexElements 的前⾯才⾏,插⼊到后⾯会被忽略掉。

  • 具体的做法:反射

    1. ⾃⼰⽤补丁创建⼀个 PathClassLoader

    2. 把补丁 PathClassLoader ⾥⾯的 elements 替换到旧的⾥⾯去

    3. 注意:

      1. 尽早加载热更新(通⽤⼿段是把加载过程放在 Application.attachBaseContext()
      2. 热更新下载完成后在需要时先杀死程序才能让补丁⽣效
    4. 优化:热更新没必要把所有内容都打过来,只要把改变的类拿过来就⾏了

      • ⽤ d8 把指定的 class 打包进 dex
    5. 完整化:从⽹上加载

    6. 再优化:把打包过程写⼀个 task