09.源码阅读(从Android源码角度入手自己实现热修复)

简介: 首先我们要知道Activity是如何启动的,在文章https://www.jianshu.com/p/bd5208574430中我们已经看了Activity启动的源码,https://www.

首先我们要知道Activity是如何启动的,在文章https://www.jianshu.com/p/bd5208574430中我们已经看了Activity启动的源码,https://www.jianshu.com/p/40f436881390 ClassLoader加载类的源码,这里再简单回顾一下

img_48e5a2da4c2a2007770206859ff325bc.png
Activity加载流程.png

看了Activity的启动流程和ClassLoader加载类的方式之后,我们知道,当一个Activity要启动的时候,最终其实是通过从BaseDexClassLoader中的一个DexPathList类中的dexElements数组中取出来,反射得到对象返回的

那么当开发中出现了问题,比如一个Activity中发生了bug,我们要紧急修复这个问题,同时又不需要应用重启,该怎么做呢,热修复的解决方案有几个可选的阿里 AndFix(不再维护了,并且不支持8.0及以上,不建议使用),腾讯Tinker(需要重启才能生效)。不过今天我们不使用第三方的实现,只从源码方面找答案

关键点就在于dexElements这个数组,经过我们的分析,我们已经知道,所有的类的dexFile文件在应用启动时就已经被保存的这个数组中了,包括发生了错误的文件类,当类被加载的时候,会从前往后遍历这个数组,找到对应的class然后反射得到对象,那么我们的解决办法是,能否将正确的文件类插入到这个数组中,当这个类启动的时候,先加载到我们修复的正确的dexFile文件,从而达到修复的目的,要使用的技术就是反射

实现原理借鉴了腾讯的Tinker,不过Tinker核心思想是利用DexDiff算法对比差异生成Patch补丁包,将生成的差异dex文件插入dexElements,而我们做的却少了生成差分包的过程,是将整个dex文件插入替换,Tinker原理图如下:

img_19509cc91da8f5cf00205a82688e0ef7.png
20170630144819943.png

我们当前要实现的思路就是现上有一个发生bug的app有待修复,我们在线下生成一个修复后的apk,将其后缀改为.zip,然后解压打开,得到里边的classes.dex文件,客户端下载这个修复的dex到本地,然后将这个dex插入本地apk的dex数组中实现修复bug的功能。

构造方法执行,初始化dex文件的存储位置

public FixDexManager(Context context) {
        this.mContext = context;
        //获取系统能够访问的dex目录
        this.mDexDir = context.getDir("odex",Context.MODE_PRIVATE);
    }

遍历所有dex文件,存入集合中,这样做也是仿照了AndFix的处理方式,目录下可能存在多个dex文件,所以我们需要将他们都放入集合中,然后开始修复

public void loadFixDex() throws Exception{
        File[] dexFiles = mDexDir.listFiles();
        List<File> fixDexFiles = new ArrayList<>();
        for (File dexFile : dexFiles) {
            if (dexFile.getName().endsWith(".dex")){
                fixDexFiles.add(dexFile);
            }
        }
        fixDexFiles(fixDexFiles);
    }

进入fixDexFiles方法
加载到程序已经运行的dexElements数组,这个数组包括存在问题的类,然后通过BaseDexClassLoader加载得到我们修复后的dex文件中的dexElements数组,最终将这个正确的dexElements数组插入到程序已经运行的dexElements中,从而当程序要启动一个类的时候,会从数组中获取到正确的类,达到修复bug的目的

private void fixDexFiles(List<File> fixDexFiles) throws Exception{
        //1.先获取已经运行的dexElement
        ClassLoader applicationClassLoader = mContext.getClassLoader();
        //Element数组对象
        Object applicationDexElements = getDexElementByClassLoader(applicationClassLoader);

        File optimizedDirectory = new File(mDexDir,"odex");
        if (!optimizedDirectory.exists()){
            optimizedDirectory.mkdirs();
        }

        //修复
        for (File fixDexFile : fixDexFiles) {
            //参数:
            // String dexPath,  dex路径
            // File optimizedDirectory,
            // String librarySearchPath,  so文件位置
            // ClassLoader parent   父classloader
            ClassLoader fixDexClassLoader = new BaseDexClassLoader(
                    fixDexFile.getAbsolutePath(),//dex路径,必须要在应用目录下的odex文件中
                    optimizedDirectory,
                    null,
                    applicationClassLoader
            );

            Object fixDexElements = getDexElementByClassLoader(fixDexClassLoader);

            //3.将下载的dex插入到已经运行的dexElement的最前边,合并
            //applicationClassLoader 数组合并fixDexElements数组
            applicationDexElements = combineArray(fixDexElements, applicationDexElements);

            //把合并的数组注入到原来的类中 applicationClassLoader
            injectDexElements(applicationClassLoader, applicationDexElements);
        }
    }

通过反射再次获取到dexElements这个数组的Field,和它所在DexPathList的对象,注入即可

    //把dexElements注入到classLoader中
    private void injectDexElements(ClassLoader classLoader, Object dexElements) throws Exception{
        //先获取pathList
        Field pathListField = BaseDexClassLoader.class.getDeclaredField("pathList");
        pathListField.setAccessible(true);
        Object pathList = pathListField.get(classLoader);

        //获取pathList中的dexElements
        Field dexElementsField = pathList.getClass().getDeclaredField("dexElements");
        dexElementsField.setAccessible(true);

        //利用反射进行注入
        dexElementsField.set(pathList,dexElements);
    }

使用方法:
在Application启动的时候初始化并调用修复方法,这种实现方式有很大的问题,因为是将修复后的dex插入原来的dex数组,以保证加载类的时候可以加载到正确的类,那么这种情况下,如果当前bug页面已经加载出来,这时候再通过这种方式注入dex是不会起作用的,必须在启动bug类之前将dex注入,否则如果不重启,不会有效果。所以这种方式实现的热修复必须要重启生效。

    FixDexManager manager = new FixDexManager(this);
        //加载所有修复的dex包,第一次的时候会从服务器上下载到修复的dex包并放在我们制定的目录下,
        //第二次进入的时候,直接读取保存的文件进行修复
        try {
            manager.loadFixDex();

            File file = new File(Environment.getExternalStorageDirectory().getAbsolutePath(),"fix.apatch");
            if (file.exists()){
                try {
                    manager.fixDex(file.getAbsolutePath());
                    Toast.makeText(getApplicationContext(),"修复bug成功",Toast.LENGTH_SHORT).show();
                } catch (Exception e) {
                    Toast.makeText(getApplicationContext(),"修复bug失败",Toast.LENGTH_SHORT).show();
                    e.printStackTrace();
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

总结一下:
通过这种方式实现热修复,需要重启app,它的缺点也很明显,由于是将整个dex文件注入到原来dex数组中,会使app内存占用增大一倍左右,我这边亲测,原本占空间10.8M的app,注入新的dex后,内存占用变成了19.8M,可以考虑分包的方案解决这个问题,减少体积。

相关文章
|
4月前
|
Ubuntu 开发工具 Android开发
Repo下载AOSP源码:基于ubuntu22.04 环境配置,android-12.0.0_r32
本文介绍了在基于Ubuntu 22.04的环境下配置Python 3.9、安装repo工具、下载和同步AOSP源码包以及处理repo同步错误的详细步骤。
271 0
Repo下载AOSP源码:基于ubuntu22.04 环境配置,android-12.0.0_r32
|
4月前
|
开发工具 git 索引
repo sync 更新源码 android-12.0.0_r34, fatal: 不能重置索引文件至版本 ‘v2.27^0‘。
本文描述了在更新AOSP 12源码时遇到的repo同步错误,并提供了通过手动git pull更新repo工具来解决这一问题的方法。
162 1
|
4月前
|
开发工具 uml git
AOSP源码下载方法,解决repo sync错误:android-13.0.0_r82
本文分享了下载AOSP源码的方法,包括如何使用repo工具和处理常见的repo sync错误,以及配置Python环境以确保顺利同步特定版本的AOSP代码。
580 0
AOSP源码下载方法,解决repo sync错误:android-13.0.0_r82
|
4月前
|
Java Android开发 芯片
使用Android Studio导入Android源码:基于全志H713 AOSP,方便解决编译、编码问题
本文介绍了如何将基于全志H713芯片的AOSP Android源码导入Android Studio以解决编译和编码问题,通过操作步骤的详细说明,展示了在Android Studio中利用代码提示和补全功能快速定位并修复编译错误的方法。
188 0
使用Android Studio导入Android源码:基于全志H713 AOSP,方便解决编译、编码问题
|
4月前
|
Android开发
我的Android 进阶修炼(1): AOSP源码根目录结构
本文介绍了AOSP源码的根目录结构,提供了基于MTK9269 Android 9.0源码的目录说明,帮助读者了解AOSP源码的组织方式和各目录的功能。
228 0
我的Android 进阶修炼(1): AOSP源码根目录结构
|
4月前
|
开发工具 Android开发 git
全志H713 Android 11 :给AOSP源码,新增一个Product
本文介绍了在全志H713 Android 11平台上新增名为myboard的产品的步骤,包括创建新的device目录、编辑配置文件、新增内核配置、记录差异列表以及编译kernel和Android系统的详细过程。
202 0
|
4月前
|
Ubuntu 开发工具 Android开发
Repo下载、编译AOSP源码:基于Ubuntu 21.04,android-12.1.0_r27
文章记录了作者在Ubuntu 21.04服务器上配置环境、下载并编译基于Android 12.1.0_r27版本的AOSP源码的过程,包括解决编译过程中遇到的问题和错误处理方法。
237 0
|
29天前
|
开发框架 前端开发 Android开发
安卓与iOS开发中的跨平台策略
在移动应用开发的战场上,安卓和iOS两大阵营各据一方。随着技术的演进,跨平台开发框架成为开发者的新宠,旨在实现一次编码、多平台部署的梦想。本文将探讨跨平台开发的优势与挑战,并分享实用的开发技巧,帮助开发者在安卓和iOS的世界中游刃有余。
|
16天前
|
搜索推荐 前端开发 API
探索安卓开发中的自定义视图:打造个性化用户界面
在安卓应用开发的广阔天地中,自定义视图是一块神奇的画布,让开发者能够突破标准控件的限制,绘制出独一无二的用户界面。本文将带你走进自定义视图的世界,从基础概念到实战技巧,逐步揭示如何在安卓平台上创建和运用自定义视图来提升用户体验。无论你是初学者还是有一定经验的开发者,这篇文章都将为你打开新的视野,让你的应用在众多同质化产品中脱颖而出。
40 19
|
16天前
|
JSON Java API
探索安卓开发:打造你的首个天气应用
在这篇技术指南中,我们将一起潜入安卓开发的海洋,学习如何从零开始构建一个简单的天气应用。通过这个实践项目,你将掌握安卓开发的核心概念、界面设计、网络编程以及数据解析等技能。无论你是初学者还是有一定基础的开发者,这篇文章都将为你提供一个清晰的路线图和实用的代码示例,帮助你在安卓开发的道路上迈出坚实的一步。让我们一起开始这段旅程,打造属于你自己的第一个安卓应用吧!
41 14
下一篇
DataWorks