• 热修复


    个人博客

    http://www.milovetingting.cn

    热修复

    前言

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

    热修复是什么

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

    热修复

    热修复分类

    热修复2

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

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

    热修复3

    加载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,在项目的对应模块的uildintermediatesjavacdebugclasses目录下,将生成的对应class复制出来,放在其它位置,如D:HotFix,复制出来的class文件要放在对应的包结构下,如:

    热修复4

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

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

    热修复5

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

    输入以下命令生成dex:--dex --output=D:HotFixpatch.dex D:HotFix

    这里为简化操作,只是简单将文件推到/sdcard/下,对应具体的业务,可以通过网络下载回来。这里由于用到了sdcard,6.0以上的设备,需要申请存储的运行时权限。

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

    应用补丁前

    热修复6

    应用补丁后

    热修复7

    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中的类,即字节码插桩。这块内容在下篇文章会展现。

    源码地址:https://github.com/milovetingting/Samples/tree/master/HotFix

  • 相关阅读:
    一些 Ubuntu 使用的小技巧
    体验 Web 自动化测试工具 Selenium
    CentOS 7 上安装 Nginx
    Windows查看端口占用情况
    Windows远程登录提醒:由于没有远程桌面授权服务器可以提供许可证,远程会话连接已断开。请跟服务器管理员联系。
    Vue动态的改变css样式
    centos7 U盘安装卡在 starting dracut initqueue hook Reached target Basic System
    用tsc编译ts文件的时候报错,tsc : 无法加载文件,因为在此系统上禁止运行脚本;
    Linux修改SSH默认的端口号
    Centos编译安装新版本Git
  • 原文地址:https://www.cnblogs.com/milovetingting/p/12339915.html
Copyright © 2020-2023  润新知