热修复

时间:2022-03-18 19:07:10

个人博客

http://www.milovetingting.cn

热修复

前言

最近在熟悉Android热修复方面的知识,纸上得来终觉浅,因此写了一个基于dex分包方案的简单Demo。

热修复是什么

在热修复技术出现前,对于已经发布的应用,如果遇到BUG,需要再次发布版本,用户需要更新应用版本,才可以解决问题。这种方式,存在新版本覆盖所需要的时间较长、需要全量更新的问题。而基于热修复技术,可以打包出修复的补丁包,推送给客户端或者客户端拉取,可以减少修复BUG所需时间、减少更新包大小。

热修复

热修复分类

热修复

基于Dex分包的热修复方案原理

在Android中,类加载器的结构如下:

热修复

加载Dex的流程

PathClassLoader与DexClassLoader都可以加载Dex,但最终都是通过他们的父类BaseDexClassLoader的findClass方法加载的

public class PathClassLoader extends BaseDexClassLoader {
    
    public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null, null, parent);
    }

    public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
        super(dexPath, null, librarySearchPath, parent);
    }
}

public class DexClassLoader extends BaseDexClassLoader {
    
    public DexClassLoader(String dexPath, String optimizedDirectory,
            String librarySearchPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
    }
}

BaseDexClassLoader中的findClass方法

//BaseDexClassLoader中的代码
private final DexPathList pathList;

 @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        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;
    }

可以看到,BaseDexClassLoader中的findClass方法又是通过DexPathList的findClass方法来具体实现的

//DexPathList中的代码

private Element[] dexElements;

public Class findClass(String name, List<Throwable> suppressed) {
        for (Element element : dexElements) {
            DexFile dex = element.dexFile;

            if (dex != null) {
                Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
                if (clazz != null) {
                    return clazz;
                }
            }
        }
        if (dexElementsSuppressedExceptions != null) {
            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
        }
        return null;
    }

通过遍历dexElements中的元素来查找class,如果找到就不再往后查找。

 public DexPathList(ClassLoader definingContext, String dexPath,
            String librarySearchPath, File optimizedDirectory) {
        //...
        this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
         //...
    }

dexElements是在构造方法中赋值的。

基于上面的分析,如果在dexElements数组的开始位置插入补丁dex,那么系统则会应用补丁包中的class,从而达到替换原来的class的效果。

由于dex在应用启动加载过后,不会再次重复加载。因此,这种方案只有在冷启动后,再次加载dex才会生效。

实现方案

在Application中,加载补丁dex,通过反射,将补丁dex插入到BaseDexClassLoader的属性:pathList中的dexElements数据开始位置。

实现代码:

public class App extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        try {
            PatchUtil.loadPatch(getApplicationContext(), "/sdcard/patch.dex");
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

public class PatchUtil {

    /**
     * 加载patch
     *
     * @param context
     * @param patch
     * @throws NoSuchFieldException
     * @throws IllegalAccessException
     */
    public static void loadPatch(Context context, String patch) throws NoSuchFieldException,
            IllegalAccessException {

        //如果patch不存在,直接返回
        File patchFile = new File(patch);
        if (!patchFile.exists()) {
            return;
        }

        //获取系统的PathClassLoader
        PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();

        //获取BaseDexClassLoader中DexPathList类型的属性:pathList
        Field pathListField = pathClassLoader.getClass().getSuperclass().getDeclaredField(
                "pathList");
        pathListField.setAccessible(true);
        Object pathListObject = pathListField.get(pathClassLoader);

        //获取DexPathList中Element[]类型的dexElements
        Field dexElementsField = pathListObject.getClass().getDeclaredField("dexElements");
        dexElementsField.setAccessible(true);
        Object dexElementsObject = dexElementsField.get(pathListObject);

        //设置optimizedDirectory
        File odex = context.getDir("odex", Context.MODE_PRIVATE);
        //创建自定义的DexClassLoader
        DexClassLoader dexClassLoader = new DexClassLoader(patch, odex.getAbsolutePath(), null,
                context.getClassLoader());
        //获取BaseDexClassLoader中DexPathList类型的属性:pathList
        Field patchPathListField = dexClassLoader.getClass().getSuperclass().getDeclaredField(
                "pathList");
        patchPathListField.setAccessible(true);
        Object patchPathListObject = patchPathListField.get(dexClassLoader);

        //获取DexPathList中Element[]类型的dexElements
        Field patchDexElementsField = patchPathListObject.getClass().getDeclaredField(
                "dexElements");
        patchDexElementsField.setAccessible(true);
        Object patchDexElementsObject = patchDexElementsField.get(patchPathListObject);

        //合并数组
        Class<?> elementClazz = dexElementsObject.getClass().getComponentType();
        int dexElementsSize = Array.getLength(dexElementsObject);
        int patchDexElementsSize = Array.getLength(patchDexElementsObject);
        int newDexElementsSize = dexElementsSize   patchDexElementsSize;
        Object newDexElements = Array.newInstance(elementClazz, newDexElementsSize);
        for (int i = 0; i < newDexElementsSize; i  ) {
            if (i < patchDexElementsSize) {
                Array.set(newDexElements, i, Array.get(patchDexElementsObject, i));
            } else {
                Array.set(newDexElements, i, Array.get(dexElementsObject,
                        i - patchDexElementsSize));
            }
        }

        //替换原来的dexElements
        dexElementsField.set(pathListObject, newDexElements);
    }

}

模拟发布应用中出现的BUG

public class Foo {

    /**
     * 显示Toast
     *
     * @param context
     * @param text
     */
    public static void showToastShort(Context context, String text) {
        Toast.makeText(context, text, Toast.LENGTH_SHORT).show();
    }

}


public class MainActivity extends AppCompatActivity {

     private Foo foo;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        foo = new Foo();
        foo.showToastShort(getApplicationContext(), "出现BUG啦~~~");
    }
}

生成修复补丁

public class MainActivity extends AppCompatActivity {

     private Foo foo;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        foo = new Foo();
        foo.showToastShort(getApplicationContext(), "BUG修复啦~~~");
    }
}

在Android Studio中,先Build-Clean Project,然后Build-Rebuild Project,在项目的对应模块的buildintermediatesjavacdebugclasses目录下,将生成的对应class复制出来,放在其它位置,如D:HotFix,复制出来的class文件要放在对应的包结构下,如:

热修复

使用SDK中自带的dx工具生成dex文件

打开CMD窗口,定位到SDK中的build-tools文件夹中对应的版本,如28.0.0

热修复

也可以将这个路径加入到系统的环境变量中,就可以在任何位置调用dx命令

输入以下命令生成dex:--dex --output=D:HotFixpatch.dex D:HotFix
这里为简化操作,只是简单将文件推到/sdcard/下,对应具体的业务,可以通过网络下载回来。这里由于用到了sdcard,6.0以上的设备,需要申请存储的运行时权限。

结束应用的进程,再次打开应用,就会加载补丁dex,运行修复后的代码。

应用补丁前

热修复

应用补丁后

热修复

CLASS_ISPREVERIFIED问题

这个问题只在Dalvik虚拟机之下出现(Android 4.4以下默认使用dalvik,5.0以后默认使用art虚拟机)。出现的原因:

apk在安装时,Dalvik虚拟机如果发现一个类A引用了其它类B,如果这个类B和类A位于同一个dex里,那么类A就会打上CLASS_ISPREVERIFIED标记。因此,如果类A引用了一个有BUG的类C,修复时用multidex热修复方案加载一个patch.dex,由于这个类已经被打上标记,而重启应用后,再次加载dex时,这个类C又位于另一个dex中,程序就会报错。

目前网上用的比较多的解决方案是,在类的构造函数中动态引入一个位于其它dex中的类,即字节码插桩。这块内容在下篇文章会展现。