Android 热修复一

一、什么是热修复?

在我们应用上线后出现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 

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

一文了解Docker之网络模型

目录 1.Docker网络 1.1 Docker网络模型概述 1.2 Docker网络驱动程序 1.2.1 host模式 1.2.2 bridge模式 1.2.3 container模式 1.2.4 none模式 1.3 Docker网络命令示例 1.3.1 创建一个自定义网络 1.3.2 列出所有网络 1.3.3 连接容器到网络 1.3.4 断开容器与网络的连接…

如何与ChatGPT愉快地聊天

原文链接&#xff1a;https://mp.weixin.qq.com/s/ui-O4CnT_W51_zqW4krtcQ 人工智能的发展已经走到了一个新的阶段&#xff0c;在这个阶段&#xff0c;人工智能可以像人一样与我们进行深度的文本交互。其中&#xff0c;OpenAI的ChatGPT是一个具有代表性的模型。然而&#xff0…

【ARM Coresight 系列文章 3.1 - ARM Coresight DP 对 AP 的访问 1】

文章目录 1.1 DP 中相关寄存器的介绍1.1.1 DPACC and APACC 寄存器1.1.2 DP SELECT 寄存器1.1.3 AP CSW寄存器1.1.4 AP TAR 寄存器1.1.5 AP DRW寄存器1.1.6 AP Banked Data registers 1.1 DP 中相关寄存器的介绍 如果DAP接入的是JTAG接口&#xff0c;那么将会通过APACC寄存器来…

[VUE学习]权限管理系统前端vue实现8-右上角用户头像显示实现

1.现在有个问题 我们再没有token情况下通过url可以直接访问页面 这不可以 所以我们需要添加路由守卫 拦截 2.permission.js的代码 import router from "/router/index" import store from "/store"router.beforeEach((to,from,next)>{const whiteList…

React类组件

1. React组件 将页面按照界面功能进行拆分&#xff0c;每一块界面都拥有自己的独立逻辑&#xff0c;这样可以提高项目代码的可维护性。其中React组件分为两种&#xff0c;一种是类式组件&#xff0c;一种是函数式组件。这里我们将的是比较常用的类式组件&#xff0c;但是在后续…

括号生成(力扣)递归 JAVA

目录 题目描述&#xff1a;纯递归解法&#xff1a;递归 回溯&#xff1a; 题目描述&#xff1a; 数字 n 代表生成括号的对数&#xff0c;请你设计一个函数&#xff0c;用于能够生成所有可能的并且 有效的 括号组合。 示例 1&#xff1a; 输入&#xff1a;n 3 输出&#xff1a…

子集 (力扣)数学推理 JAVA

给你一个整数数组 nums &#xff0c;数组中的元素 互不相同 。返回该数组所有可能的子集&#xff08;幂集&#xff09;。 解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。 示例 1&#xff1a; 输入&#xff1a;nums [1,2,3] 输出&#xff1a;[[],[1],[2],[1,2],[3],[…

Unity/Shader 零碎知识点

坐标系 Unity使用的是左手坐标系&#xff1b;观察空间&#xff0c;通俗来讲就是以摄像机为原点的坐标系&#xff0c;摄像机的前向是z轴的负方向&#xff0c;与模型和世界空间中的定义相反&#xff0c;z轴的坐标减少意味着场景深度的增加 点积 abba|a||b|cos<a,b> 结果为常…

邮票面值-2022年全国青少年信息素养大赛Python国赛第5题

[导读]&#xff1a;超平老师计划推出《全国青少年信息素养大赛Python编程真题解析》50讲&#xff0c;这是超平老师解读Python编程挑战赛真题系列的第7讲。 全国青少年信息素养大赛&#xff08;原全国青少年电子信息智能创新大赛&#xff09;是“世界机器人大会青少年机器人设计…

Spring Boot原理分析 | SpringApplication、Yaml、Properties

&#x1f497;wei_shuo的个人主页 &#x1f4ab;wei_shuo的学习社区 &#x1f310;Hello World &#xff01; Spring Boot Spring开源框架&#xff0c;轻量级的Java开发框架&#xff0c;解决企业级应用开发的复杂性而创建&#xff0c;简化开发 基于POJO的轻量级和最小侵入型编程…

Kafka入门,漏消费和重复消费, 消费者事务,数据积压(二十四)

漏消费和重复消费 重复消费&#xff1a;已经消费了数据&#xff0c;但是offset没提交。 漏消费&#xff1a;先提交offset后消费&#xff0c;有可能会造成数据得漏消费 消费者事务 如果向完成consumer端得进准一次性消费&#xff0c;那么需要Kafka消费端将消费过程和提交offs…

【Accumulate】Gitee解决每次推送输入账户密码问题

【前言】 每次建立私人仓库后&#xff0c;一推送就得输入账户密码&#xff0c;真的巨烦人啊。 【解决】 step1&#xff1a; 绑定私匙&#xff1a; 配置Git_犟小孩的博客-CSDN博客 step2&#xff1a; 每次绑定远程仓库的时候&#xff0c;使用SSH绑定 如果已经绑定过了&…

YoloV2

时间线 Motivation Yolo-v1是在检测精度尚可的前提下达到了实时检测&#xff0c;同年的SSD检测速度略慢但检测精度远高于Yolo-v1&#xff0c;因此&#xff0c;Yolo-v2则是着眼于检测得更快更准&#xff0c;同时它利用WordTree创造性地将Imag…

回归预测 | MATLAB实现WOA-DBN鲸鱼算法优化深度置信网络的多输入回归预测

回归预测 | MATLAB实现WOA-DBN鲸鱼算法优化深度置信网络的多输入回归预测 目录 回归预测 | MATLAB实现WOA-DBN鲸鱼算法优化深度置信网络的多输入回归预测效果一览基本介绍模型描述程序设计参考资料 效果一览 基本介绍 基于鲸鱼算法优化深度置信网络(WOA-DBN)的数据回归预测&…

vim的使用方法及相关按键

目录 一、安装vim 二、vim的使用 1.打开vim 2.vim的四种模式使用 &#xff08;1&#xff09;命令模式&#xff08;快捷键的使用&#xff09; &#xff08;2&#xff09;编辑模式 &#xff08;3&#xff09;末行模式 &#xff08;4&#xff09;可视化模式 一、安装vim …

虹科方案 | Redis Enterprise:适用于任何企业的矢量数据库解决方案

用户希望他们遇到的每个应用程序和网站都具有搜索功能。然而&#xff0c;超过80%的业务数据是非结构化的&#xff0c;以文本、图像、音频、视频或其他格式存储。因此&#xff0c;我们需要一种跨非结构化数据的搜索方式。 什么是矢量数据库&#xff08;vector database&#xff…

基于深度学习的高精度老虎检测识别系统(PyTorch+Pyside6+YOLOv5模型)

摘要&#xff1a;基于深度学习的高精度老虎检测识别系统可用于日常生活中或野外来检测与定位老虎目标&#xff0c;利用深度学习算法可实现图片、视频、摄像头等方式的老虎目标检测识别&#xff0c;另外支持结果可视化与图片或视频检测结果的导出。本系统采用YOLOv5目标检测模型…

uniapp学习之【uniapp的返回事件 onBackPress 在微信小程序中不生效的问题】

uniapp 的返回事件 onBackPress 在微信小程序中不生效的问题 场景&#xff1a;页面中点击左上角的返回按钮,监听返回操作,页面返回前执行了一些操作, uniapp 页面生命周期中有 onBackPress ,因此将操作写在了 onBackPress () 页面生命周期钩子当中, H5 测试一切正常,但是微信开…

集合面试题--LinkedList数组

目录 单向链表 介绍 时间复杂度分析 双向链表 时间复杂度分析 总结 ArrayList和LinkedList的区别是什么&#xff1f; 单向链表 介绍 时间复杂度分析 双向链表 时间复杂度分析 总结 ArrayList和LinkedList的区别是什么&#xff1f;

某网站JS加密、OB混淆与CSS反爬实战分析

1. 写在前面 最近一段时间接触了一些小说网站的业务。发现很多的小说网站&#xff0c;甚至一些小站它们的安全防护措施做的都很到位&#xff01;例如上次说到的的五秒盾也是存在于一个小说小站。今天要讲的这个网站它集JS加密、ob混淆、CSS反爬于一体 目标站点&#xff1a; aH…