Android 热修复一

news2024/9/9 5:30:41

一、什么是热修复?

在我们应用上线后出现bug需要及时修复时,不用再发新的安装包,只需要发布补丁包,在客户无感知下修复掉bug。

实现效果:

Demo源码:

https://gitee.com/sziitjim/hotfix

 二、怎么进行热修复?

1.开发端:生成补丁包,开发者把Bug修复后,将修复后的代码打包成.jar或者.dex文件,上传到服务端。

2.服务端:补丁包管理,制作好的补丁包放到服务器进行管理;

3.用户端:执行热修复,APP启动后,通过api查询如果服务端有补丁包,则下载并执行补丁包,进行bug修复,补丁包要在调用bug代码前执行才能修复达到bug效果;

三、制作补丁包流程:

1、把Bug修复掉后,先生成类的class文件。

1.1首先我在代码里面制造了一个bug,程序运行后抛出异常,

1.2修复bug,成类的class文件;

 2、执行命令:dx --dex --output=patch.jar com/hotfix/Utils.class  ,生成补丁包.jar或者.dex文件,我这里生成.jar文件;

命令目录地址不要输错:

注意:如果提示错误:'dx' 不是内部或外部命令,也不是可运行的程序 或批处理文件。 

要配置dex环境变量:

 如果提示错误:-Djava.ext.dirs=D:\Android\Sdk\build-tools\30.0.2\lib is not supported. Use -classpath instead.
Error: Could not create the Java Virtual Machine.
Error: A fatal exception has occurred. Program will exit.

则修改:D:\Android\Sdk\build-tools\30.0.2目录下的dx.bat 最后一行

把   call "%java_exe%" %javaOpts% -Djava.ext.dirs="%frameworkdir%" -jar "%jarpath%" %params%
改成 call "%java_exe%" %javaOpts% --class-path="%frameworkdir%" -jar "%jarpath%" %params%

四、使用补丁包:

1.做测试我这边没有使用服务器管理,直接将生成的补丁包复制到手机SDcard目录下,由APP端读取SDcard目录下补丁包patch.jar使用;

2.使用类加载器 ClassLoader获取补丁包的Utils类,代替APP原来的Utils,到达修复bug的效果,实现流程:

1、获取当前程序的PathClassLoader对象;
2、反射获得PathClassLoader父类BaseDexClassLoader的pathList对象;
3、反射获取pathList的dexElements对象 (oldElement)
4、把补丁包patch.jar转化为Element数组:patchElement(反射执行makePathElements)
5、合并patchElement+oldElement = newElement (Array.newInstance)
6、反射把oldElement赋值成newElement 

Android6.0及以下系统代码实现:

// 1、获取程序的PathClassLoader对象
ClassLoader classLoader = application.getClassLoader();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
	try {
		ClassLoaderInjector.inject(application, classLoader, patchFile);
	} catch (Throwable throwable) {
	}
	return;
}
try {
	// 2、反射获得PathClassLoader父类BaseDexClassLoader的pathList对象
	Field pathListField = ShareReflectUtil.findField(classLoader, "pathList");
	Object pathList = pathListField.get(classLoader);
	// 3、反射获取pathList的dexElements对象 (oldElement)
	Field dexElementsField = ShareReflectUtil.findField(pathList, "dexElements");
	Object[] oldElements = (Object[]) dexElementsField.get(pathList);
	// 4、把补丁包变成Element数组:patchElements(反射执行makePathElements)
	Object[] patchElements = null;
	ArrayList<IOException> ioExceptions = new ArrayList<>();
	if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
		Method makePathElementsMethod = ShareReflectUtil.findMethod(pathList, "makePathElements",
				List.class, File.class, List.class);
		patchElements = (Object[]) makePathElementsMethod.invoke(pathList, patchFile,
				application.getCacheDir(), ioExceptions);
	} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
		Method makePathElements = ShareReflectUtil.findMethod(pathList, "makeDexElements",
				ArrayList.class, File.class, ArrayList.class);
		patchElements = (Object[])
				makePathElements.invoke(pathList, patchFile, application.getCacheDir(), ioExceptions);
	}
	// 5、合并patchElement+oldElement = newElement (Array.newInstance)
	Object[] newElements = (Object[]) Array.newInstance(oldElements.getClass().getComponentType(),
			oldElements.length + patchElements.length);
	Log.i("FixUtil", "installPatch patchElements " + patchElements.length + ",oldElements " + oldElements.length);
	// copy patchElements
	System.arraycopy(patchElements, 0, newElements, 0, patchElements.length);
	// copy oldElements
	System.arraycopy(oldElements, 0, newElements, patchElements.length, oldElements.length);
	// 6、反射把oldElement赋值成newElement
	dexElementsField.set(pathList, newElements);
} catch (Exception e) {
	Log.i("FixUtil", "err:" + e.getMessage());
}

Android6.0以上系统代码实现:

public class ClassLoaderInjector {
    public static void inject(Application app, ClassLoader oldClassLoader, List<File> patchs) throws Throwable {
        Log.i("ClassLoaderInjector", "inject");
        //创建我们自己的加载器
        ClassLoader newClassLoader
                = createNewClassLoader(app, oldClassLoader, patchs);
        doInject(app, newClassLoader);
        Log.i("ClassLoaderInjector", "inject end");
    }

    private static ClassLoader createNewClassLoader(Context context, ClassLoader oldClassLoader, List<File> patchs) throws Throwable {

        /**
         * 1、先把补丁包的dex拼起来
         */
        // 获得原始的dexPath用于构造classloader
        StringBuilder dexPathBuilder = new StringBuilder();
        String packageName = context.getPackageName();
        boolean isFirstItem = true;
        for (File patch : patchs) {
            //添加:分隔符  /xx/a.dex:/xx/b.dex
            if (isFirstItem) {
                isFirstItem = false;
            } else {
                dexPathBuilder.append(File.pathSeparator);
            }
            dexPathBuilder.append(patch.getAbsolutePath());
        }

        /**
         * 2、把apk中的dex拼起来
         */
        //得到原本的pathList
        Field pathListField = ShareReflectUtil.findField(oldClassLoader, "pathList");
        Object oldPathList = pathListField.get(oldClassLoader);

        //dexElements
        Field dexElementsField = ShareReflectUtil.findField(oldPathList, "dexElements");
        Object[] oldDexElements = (Object[]) dexElementsField.get(oldPathList);

        //从Element上得到 dexFile
        Field dexFileField = ShareReflectUtil.findField(oldDexElements[0], "dexFile");
        for (Object oldDexElement : oldDexElements) {
            String dexPath = null;
            DexFile dexFile = (DexFile) dexFileField.get(oldDexElement);
            if (dexFile != null) {
                dexPath = dexFile.getName();
            }
            if (dexPath == null || dexPath.isEmpty()) {
                continue;
            }
            if (!dexPath.contains("/" + packageName)) {
                continue;
            }
            if (isFirstItem) {
                isFirstItem = false;
            } else {
                dexPathBuilder.append(File.pathSeparator);
            }
            dexPathBuilder.append(dexPath);
        }
        String combinedDexPath = dexPathBuilder.toString();

        /**
         * 3、获取apk中的so加载路径
         */
        //  app的native库(so) 文件目录 用于构造classloader
        Field nativeLibraryDirectoriesField = ShareReflectUtil.findField(oldPathList, "nativeLibraryDirectories");
        List<File> oldNativeLibraryDirectories = (List<File>) nativeLibraryDirectoriesField.get(oldPathList);
        StringBuilder libraryPathBuilder = new StringBuilder();
        isFirstItem = true;
        for (File libDir : oldNativeLibraryDirectories) {
            if (libDir == null) {
                continue;
            }
            if (isFirstItem) {
                isFirstItem = false;
            } else {
                libraryPathBuilder.append(File.pathSeparator);
            }
            libraryPathBuilder.append(libDir.getAbsolutePath());
        }

        String combinedLibraryPath = libraryPathBuilder.toString();

        //创建自己的类加载器
        ClassLoader result = new EnjoyClassLoader(combinedDexPath, combinedLibraryPath, ClassLoader.getSystemClassLoader());
        return result;
    }


    private static void doInject(Application app, ClassLoader classLoader) throws Throwable {
        Thread.currentThread().setContextClassLoader(classLoader);

        Context baseContext = (Context) ShareReflectUtil.findField(app, "mBase").get(app);
        if (Build.VERSION.SDK_INT >= 26) {
            ShareReflectUtil.findField(baseContext, "mClassLoader").set(baseContext, classLoader);
        }

        Object basePackageInfo = ShareReflectUtil.findField(baseContext, "mPackageInfo").get(baseContext);
        ShareReflectUtil.findField(basePackageInfo, "mClassLoader").set(basePackageInfo, classLoader);

        if (Build.VERSION.SDK_INT < 27) {
            Resources res = app.getResources();
            try {
                ShareReflectUtil.findField(res, "mClassLoader").set(res, classLoader);

                final Object drawableInflater = ShareReflectUtil.findField(res, "mDrawableInflater").get(res);
                if (drawableInflater != null) {
                    ShareReflectUtil.findField(drawableInflater, "mClassLoader").set(drawableInflater, classLoader);
                }
            } catch (Throwable ignored) {
                // Ignored.
            }
        }
    }
}

注意:Android不同系统版本中ClassLoader涉及的源码都可能存在差异,目前demo只适配到了Android9,也就是说安装在Android9.0以上的手机上,该热修复未必能生效,每个版本的适配可以参考:腾讯热修复框架tinker的NewClassLoaderInjector.java类

代码地址:tinker-android/tinker-android-loader/src/main/java/com/tencent/tinker/loader/NewClassLoaderInjector.java · Gitee 极速下载/tinker - Gitee.com

Demo源码:

https://gitee.com/sziitjim/hotfix 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/734045.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

极速上手k8s,Kubernetes 从入门到摸鱼系列-理论篇

1. 引言&#x1f44b; 大家好&#xff0c;我是比特桃&#xff01;随着微服务架构越来越流行&#xff0c;大规模的微服务容器编排成了一件具有挑战的事情。在这次容器化云原生的发展中&#xff0c;Docker 成了容器化的赢家&#xff0c;而 Kubernetes 则成为了容器编排的赢家。k…

「已解决」 模块““umi“” ““@umijs/max“” 没有导出的成员“useRequest” “request” 问题的所有方法汇总

背景 使用 Umi 搭建项目时候有的时候会出现这种错误&#xff0c;模块““umi”” ““umijs/max”” 没有导出的成员“useRequest” “request”。 解决 tsconfig.json "paths": {"/*": ["src/*"],"/*": ["./src/.umi/*"…

用JShaman本地部署版,加密2.7MB的Webpack生成的JS文件

JShaman是知名的JS代码保护平台。在线使用&#xff0c;一键混淆加密&#xff0c;无需注册、无需登录。可免费用&#xff0c;也有商业服务&#xff1b;有在线使用的SAAS平台网站&#xff0c;也有本地部署版。很方便、很强大&#xff0c;很专业。 今天&#xff0c;测试使用JSham…

unordered_map模拟实现|STL源码剖析系列|开散列

博主很久没有更新过STL源码剖析这个系列的文章了&#xff0c;主要是因为大部分STL常用的容器&#xff0c;博主都已经发过文章了&#xff0c;今天博主带着大家把哈希表也模拟实现一下。 前言 那么这里博主先安利一下一些干货满满的专栏啦&#xff01; 手撕数据结构https://blo…

点云最小外包矩形计算

1、原理介绍 一簇点云的最小外包矩形&#xff08;Minimum Bounding Rectangle&#xff0c;MBR&#xff09;&#xff0c;是指用一个矩形将该簇点云框起来&#xff0c;所有点云数据在矩形框内。如下图所示为一个矩形框刚好将点云数据全部包围。 下面给出一种基于最大重叠度的最小…

[工业互联-19]:如何在QT中增加SOEM主站

目录 第1章 基本步骤 第2章 详细步骤 2.1.QT安装 2.2.VS安装 2.3.Win10 Debuggers 2.4.QT配置 2.5. SOEM移植 &#xff08;&#xff11;&#xff09;lib库生成 &#xff08;2&#xff09;文件移植: 文件整理 第1章 基本步骤 要在QT中添加SOEM主站功能&#xff0c;您需…

用OpenCV创建一张灰度黑色图像并设置某一列为白色

这段代码首先创建了一个400行600列的单通道灰度图像。然后,它遍历图像中的每个像素。如果像素位于列索引为30的列中,则将该像素的值设置为255。在灰度图像中,0表示黑色,255表示白色。因此,这段代码将图像的第30列设置为白色。 在 OpenCV 中,cv::Mat 构造函数的调用 cv::…

【算法 -- LeetCode】(13)罗马数字转整数

1、题目 罗马数字包含以下七种字符&#xff1a; I&#xff0c; V&#xff0c; X&#xff0c; L&#xff0c;C&#xff0c;D 和 M。 字符 数值 I 1 V 5 X 10 L 50 C 100 D 500 M …

哈希表和字符串专题1—205. 同构字符串 1002. 查找共用字符 925. 长按键入 844.比较含退格的字符串 C++实现

文章目录 205. 同构字符串1002. 查找共用字符925. 长按键入844.比较含退格的字符串栈模拟双指针 205. 同构字符串 class Solution { public:bool isIsomorphic(string s, string t) {unordered_map<char, char> map1;unordered_map<char, char> map2;for(int i0, j…

AI绘画:StableDiffusion炼丹Lora攻略-实战萌宠图片生成

Lora攻略-实战萌宠图片生成 写在前面的话一&#xff1a;准备二、Lora作用1.AI模特2.炼衣服Lora3.改变画风/画面背景Lora模型究竟是什么&#xff1f; 三、如何炼制自己的Lora模型&#xff1f;四、炼丹前的准备&#xff08;**下载整合包**&#xff09;五、选择合适的大模型六、高…

管理类联考——逻辑——记忆篇——数字编码——公式

&#x1f3e0;个人主页&#xff1a;fo安方的博客✨ &#x1f482;个人简历&#xff1a;大家好&#xff0c;我是fo安方&#xff0c;考取过HCIE Cloud Computing、CCIE Security、CISP、RHCE、CCNP RS、PEST 3等证书。&#x1f433; &#x1f495;兴趣爱好&#xff1a;b站天天刷&…

MySQL练习题(2)

创建如下员工标表 插入数据 1-- 按员工编号升序排列不在10号部门工作的员工信息 2-- 查询姓名第二个字母不是A且薪水大于1000元的员工信息&#xff0c;按薪水降序排列 4-- 求每个部门的平均薪水 5-- 求每个部门的最高薪水 6-- 求每个部门…

Coverity 2021.9 for win Coverity 2022.6 for linux

Coverity是一款快速、准确且高度可扩展的静态分析 (SAST) 解决方案&#xff0c;可帮助开发和安全团队在软件开发生命周期 (SDLC) 的早期解决安全和质量缺陷&#xff0c;跟踪和管理整个应用组合的风险&#xff0c;并确保符合安全和编码标准。Coverity 是一款精确的综合静态分析与…

JVM04-优化JVM内存分配以及内存持续上升问题和CPU过高问题排查

1-JVM内存分配 1.1-JVM内存分配性能问题 JVM内存分配不合理最直接的表现就是频繁的GC&#xff0c;这会导致上下文切换等性能问题&#xff0c;从而降低系统的吞吐量、增加系统的响应时间。因此&#xff0c;如果你在线上环境或性能测试时&#xff0c;发现频繁的GC&#xff0c;且…

密码学入门——分组密码模式

文章目录 参考书一、简介二、ECB模式三、CBC模式四、CFB模式五、OFB模式六、CTR模式 参考书 图解密码技术&#xff0c;第三版 一、简介 分组密码工作模式指的是将明文分成固定长度的块&#xff0c;并对每个块应用相同的加密算法进行加密的过程。这些块的长度通常是64位或128…

leetcode 654. 最大二叉树

2023.7.9 又是一道递归构造二叉树的题&#xff0c;和昨天做的那道题从中序与后序遍历序列构造二叉树类似&#xff0c;5分钟AC了。 大致思路就是通过找到数组中的最大值&#xff0c;并将其作为根节点&#xff0c;然后递归地构建左子树和右子树&#xff0c;最终返回整个最大二叉树…

PMP项目管理-敏捷

敏捷知识体系&#xff1a; 传统项目特点 1> 一开始就对详细的需求进行很高的投入 2> 价值只有到项目结束的时候才能体现, 风险较高 3> 一开始就要编写很多的文档 4> 客户参与度不高, 澄清完需求之后基本不参与 5> 需要花大量的时间来汇报当前的项目状态 6> 无…

Freertos-mini智能音箱项目---IO扩展芯片PCA9557

项目上用到的ESP32S3芯片引脚太少&#xff0c;选择了PCA9557扩展IO&#xff0c;通过一路i2c可以扩展出8个IO。这款芯片没有中断输入&#xff0c;所以更适合做扩展输出引脚用&#xff0c;内部寄存器也比较少&#xff0c;只有4个&#xff0c;使用起来很容易。 输入寄存器 输出寄存…

线程本地变量交换框架-TransmitterableThreadLocal(阿里开源)

上文 &#xff1a;秒级达百万高并发框架-Disruptor TransmitterableThreadLocal介绍 TransmitterableThreadLocal简称TTL 是阿里巴巴开源的一个框架。TransmittableThreadLocal是对Java中的ThreadLocal进行了增强和扩展。它旨在解决在线程池或异步任务调用链中&#xff0c;Thre…

MAC电脑查看SHA256方式

背景 现在很多网站下载大文件时&#xff0c;以往通过查看文件大小来确定是否下载正确&#xff0c;但是很多情况下&#xff0c;文件下载后大小差不多&#xff0c;但是很多时候却时候出现无法安装的问题&#xff0c;有可能还是下载的文件出现错误&#xff0c;导致文件无法正常使…