热修复/热更新

news2024/11/24 16:25:22

热修复/热更新

  • 一.Android热修复
  • 二.热修复框架
  • 三.类加载器
    • 0.BootClassLoader
    • 1.PathClassLoader
    • 2.DexClassLoader
  • 四.实现思路
  • 五.代码
    • 1.FixManager
    • 2.App
    • 3.更加标准的代码
  • 五.制作补丁包
    • 1.写段有bug的工具类,并写个点击按钮调用
    • 2.运行项目到模拟器上
    • 3.修复ToastUtils工具类,本地测试没问题,build项目,并将ToastUtils.class文件拷贝出来
    • 4.创建dex/com/bawei/myfix文件夹,里面只有修复好的ToastUtils.class文件
    • 5.生成dex补丁文件
    • 6.将补丁文件放在对应的SD卡目录下进行修复,注意读写SD卡权限

一.Android热修复

热修复,就是对线上版本的静默更新。当APP发布上线之后,如果出现了严重的bug,通常需要重新发版来修复,但是重新走发布流程可能时间比较长,重新安装APP用户体验也不友好,所以出现了热修复,热修复就是通过发布一个插件,使APP运行的时候加载插件里面的代码,从而解决缺陷,并且对用户来说是无感的(有时候可能需要重启一下APP)。

热修复的实现方案,一种是类加载方案,即dex插桩,这种思路在插件化中也会用到;还有一种是底层替换方案,即修改替换ArtMethod。采用类加载方案的主要是以腾讯系为主,包括微信的Tinker、qq空间的QZone、美团的Robust、饿了么的Amigo;采用底层替换方案的主要是阿里系的AndFix等。

热修复包括3部分:开发端、服务端和用户端。在开发端,通过Gradle插件生成补丁包,并上传到云端,客户端通过判断是否需要下载新的补丁包,并执行热修复。
在这里插入图片描述

(1)无需重新发版。
(2)快速修复线上bug,修复成功率高,降低损失。
(3)用户无感知修复,无需下载最新的应用,代价小。

二.热修复框架

在这里插入图片描述
我们主要使用tinker

三.类加载器

0.BootClassLoader

系统类加载器,当系统启动的时候加载常用类

1.PathClassLoader

加载应用中的类,只能加载已经安装到 Android 系统中的 APK 文件。因此不符合插件化的需求,不作考虑。

2.DexClassLoader

支持加载外部的 APK、Jar 或者 dex 文件,正好符合文件化的需求,所有的插件化方案都是使用 DexClassloader 来加载插件 APK 中的 .class文件的

在这里插入图片描述
类加载器的时序图
在这里插入图片描述

四.实现思路

在这里插入图片描述
在这里插入图片描述

五.代码

1.FixManager

public class FixManager {
    private Context mContext;
    private FixManager(Context context){
        mContext = context;
    }
    private static FixManager manager;
    public static FixManager getInstance(Context context){
        if(manager == null){
            synchronized (FixManager.class){
                if(manager == null){
                    manager = new FixManager(context);
                }
            }
        }
        return manager;
    }


    public void loadFixClass() throws NoSuchFieldException, IllegalAccessException {
        //1.反射机制获得补丁包的dexElements:DexClassLoader
        //1.0 准备补丁包的路径
        String patchPath = mContext.getExternalFilesDir(null).getAbsolutePath()+"/output.dex";//补丁包的SD卡路径,
        String cachePatchPath = mContext.getDir("patch",Context.MODE_PRIVATE).getAbsolutePath();//补丁包的缓存路径
        //1.1 将补丁到的dex文件加载到虚拟机内存中
        DexClassLoader dexClassLoader = new DexClassLoader(patchPath,cachePatchPath,null,mContext.getClassLoader());
        Class<?> superclass = dexClassLoader.getClass().getSuperclass();//获得BaseDexClassLoader class对象
        Field pathListField = superclass.getDeclaredField("pathList");//获得BaseDexClassLoader类成员属性
        pathListField.setAccessible(true);//属性的私有的需要暴力访问
        Object pathListObject = pathListField.get(dexClassLoader);//获得dexClassLoader对象的pathList属性值
        Field dexElementsField = pathListObject.getClass().getDeclaredField("dexElements");//获得PathList类的成员属性
        dexElementsField.setAccessible(true);//属性的私有的需要暴力访问
        Object dexElementsObject = dexElementsField.get(pathListObject);//获得pathListObject对象dexElements属性值
        //2.反射机制获得宿主app的 dexElements
        ClassLoader pathClassLoader = mContext.getClassLoader();//获得PathClassLoader加载器对象
        Object myPathListField = pathListField.get(pathClassLoader);//获得PathClassLoader对象的pathList属性值
        Object myDexElementsObject = dexElementsField.get(myPathListField);//获得myPathListField对象dexElements属性值
        //3.将2个数组合并newDexElements:补丁包在前面,宿主在后面
        int fixLength = Array.getLength(dexElementsObject);//补丁包的长度
        int myLength = Array.getLength(myDexElementsObject);//宿主数组长度
        int newDexElementsLength = fixLength + myLength;//新数组的长度
        //新的数组 参数一:数组中元素的类型 参数二:长度
        Object newDexElements = Array.newInstance(dexElementsObject.getClass().getComponentType(), newDexElementsLength);
        for(int i = 0;i<newDexElementsLength;i++){
            if(i<fixLength){//放补丁包
                Object o = Array.get(dexElementsObject, i);
                Array.set(newDexElements,i,o);
            }else{
                Object o = Array.get(myDexElementsObject, i - fixLength);
                Array.set(newDexElements,i,o);
            }
        }
        //4.反射机制将newDexElements新数组给宿主app放回去
        dexElementsField.set(myPathListField,newDexElements);
    }
}

2.App

public class App extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        try {
            FixManager.getInstance(this).loadFixClass();
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

3.更加标准的代码

package com.bawei.myfix;

import android.content.Context;

import java.io.File;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.util.HashSet;

import dalvik.system.DexClassLoader;
import dalvik.system.PathClassLoader;

/**
 * @Author : yaotianxue
 * @Time : On 2023/6/8 18:26
 * @Description : FixDexUtils
 */
public class FixDexUtils {
    private static final String DEX_SUFFIX = ".dex";

    private static final String APK_SUFFIX = ".apk";

    private static final String JAR_SUFFIX = ".jar";

    private static final String ZIP_SUFFIX = ".zip";

    private static final HashSet<File> loadedDex = new HashSet<File>();

    //加载补丁,使用默认目录:data/data/包

    public static void loadFixedDex(Context context) {

        loadFixedDex(context, null);

    }

    //加载补丁包

    public static void loadFixedDex(Context context, File patchFilesDir) {

        if (context == null) {

            return;

        }

        // 遍历所有的修复dex

        File fileDir = patchFilesDir != null ? patchFilesDir : new File(context.getExternalCacheDir().getAbsolutePath()); // data/data/包名/cache(这个可以任意位置)

        File[] listFiles = fileDir.listFiles();

        for (File file : listFiles) {

            if (file.getName().startsWith("classes") &&(file.getName().endsWith(DEX_SUFFIX) || file.getName().endsWith(APK_SUFFIX) || file.getName().endsWith(JAR_SUFFIX) || file.getName().endsWith(ZIP_SUFFIX))) {

                loadedDex.add(file);// 存入集合

            }

        }

        // dex合并之前的dex

        doDexInject(context);

    }

    private static void doDexInject(Context appContext) {

        String optimizeDir = appContext.getFilesDir().getAbsolutePath();// data/data/包名/files (这个必须是自己程序下的目录)

        File fopt = new File(optimizeDir);

        if (!fopt.exists()) {

            fopt.mkdirs();

        }

        try {

            // 1.加载应用程序的dex

            PathClassLoader pathLoader = (PathClassLoader) appContext.getClassLoader();

            for (File dex : FixDexUtils.loadedDex) {

                // 2.加载指定的修复的dex文件

                DexClassLoader dexLoader = new DexClassLoader(dex.getAbsolutePath(), fopt.getAbsolutePath(), null, pathLoader);

                // 3.合并

                Object dexPathList = getPathList(dexLoader);

                Object pathPathList = getPathList(pathLoader);

                Object leftDexElements = getDexElements(dexPathList);

                Object rightDexElements = getDexElements(pathPathList);

                // 合并完成

                Object dexElements = combineArray(leftDexElements, rightDexElements);

                // 重写给PathList里面的Element[] dexElements;赋值

                Object pathList = getPathList(pathLoader);// 一定要重新获取,不要用pathPathList,会报错

                setField(pathList, pathList.getClass(), dexElements);

            }

        } catch (Exception e) {

            e.printStackTrace();

        }

    }

    //反射给对象中的属性重新赋值

    private static void setField(Object obj, Class<?> cl, Object value) throws NoSuchFieldException, IllegalAccessException {

        Field declaredField = cl.getDeclaredField( "dexElements");

        declaredField.setAccessible(true);

        declaredField.set(obj, value);

    }

    // 反射得到对象中的属性值

    private static Object getField(Object obj, Class<?> cl, String field) throws NoSuchFieldException, IllegalAccessException {

        Field localField = cl.getDeclaredField(field);

        localField.setAccessible(true);

        return localField.get(obj);

    }

    //反射得到类加载器中的pathList对象

    private static Object getPathList(Object baseDexClassLoader) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {

        return getField(baseDexClassLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");

    }

    //反射得到pathList中的dexElements

    private static Object getDexElements(Object pathList) throws NoSuchFieldException, IllegalAccessException {

        return getField(pathList, pathList.getClass(), "dexElements");

    }

    //数组合并

    private static Object combineArray(Object left, Object right) {

        Class<?> componentType = left.getClass().getComponentType();

        int i = Array.getLength(left);// 得到左数组长度(补丁数组)

        int j = Array.getLength(right);// 得到原dex数组长度

        int k = i + j;// 得到总数组长度(补丁数组+原dex数组)

        Object result = Array.newInstance( componentType, k);// 创建一个类型为componentType,长度为k的新数组

        System.arraycopy(left, 0, result, 0, i);

        System.arraycopy(right, 0, result, i, j);

        return result;

    }

}

五.制作补丁包

1.写段有bug的工具类,并写个点击按钮调用

public class ToastUtils {
    public static void toast(){
        int a = 1;
        int b = 0;
        int c = a/b;
    }
}

2.运行项目到模拟器上

3.修复ToastUtils工具类,本地测试没问题,build项目,并将ToastUtils.class文件拷贝出来

在这里插入图片描述

4.创建dex/com/bawei/myfix文件夹,里面只有修复好的ToastUtils.class文件

5.生成dex补丁文件

(1)Android SDK提供了dx.bat工具将class文件转成dex文件,目录如下:
在这里插入图片描述
(2)将第4步骤创建的dex文件夹放在sdk目录下,如上图所示
(3)cmd到SDK的路径,如上图所示
(4)执行命令:将dex文件夹里面的内容打成output.dex
.\dx --dex --output = .\output.dex .\dex

6.将补丁文件放在对应的SD卡目录下进行修复,注意读写SD卡权限

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

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

相关文章

node_modules插件代码修改,如何快速修改防止npm install安装覆盖代码

一、背景 有些组件不满足业务时咱们只能修改插件的源码&#xff1a; 直接在项目的node_modules下找到插件的源码直接修改&#xff1b; 优点&#xff1a;简单直接、快速见效&#xff1b;缺点&#xff1a;不能持久化&#xff0c;一旦重新npm install安装就失效&#xff1b;不方…

(五)ArcGIS空间数据的转换与处理——数据处理

ArcGIS空间数据的转换与处理——数据处理 目录 ArcGIS空间数据的转换与处理——数据处理 1.数据裁剪1.1矢量数据裁剪1.1.1图解1.1.2用法1.1.3操作步骤 1.2栅格数据裁剪1.2.1矩形裁剪1.2.2利用已有数据裁剪 2.数据拼接2.1矢量数据的拼接2.1.1图解2.1.2用法2.1.3 操作步骤 2.2栅格…

区块链学习三——比特币的数据结构

区块链学习三——比特币的数据结构 文章内容来源于北京大学肖臻老师《区块链技术与应用》公开课 文章目录 区块链学习三——比特币的数据结构 一、哈希指针&#xff08;hash pointers&#xff09;二、区块链三、Merkle tree1.Merkle tree的作用&#xff1a;Merkle Proof2.Proo…

2023最新版本Activiti7系列-任务分配

任务分配 1.固定分配 在指派用户任务的审批人时。我们是直接指派的固定账号。但是为了保证流程设计审批的灵活性。我们需要各种不同的分配方式&#xff0c;所以这节我们就详细的来介绍先在Activiti7中我们可以使用的相关的分配方式. 固定分配就是我们前面介绍的&#xff0c;在绘…

vr沉浸式仿真实训展厅中控系统提高课堂纪律

为解决实训教学过程中“看不到、进不去、成本高、危险大”的问题&#xff0c;VR智能中控系统为职业教育及高等教育老师提供一个数字化、沉浸式、集中管控的实训教学工具。 VR智能中控系统通过对VR教学课堂的实时监控、数据的收集和分析&#xff0c;为气象学院的教学提供更多帮助…

STM32 HAL库 使用 USB HID 配置

STM32 HAL库 HID 配置 STM32 CubeMax 配置修改USB描述符修改HID设备PID VID修改HID报告间隔修改USB报告长度发送和接收发送接收 STM32 CubeMax 配置 我这里使用的是HS接口但使用的是内部FS核。 修改USB描述符 在 usbd_custom_hid_if.c 的 CUSTOM_HID_ReportDesc_HS 或 CUSTO…

python基础----08-----json、pyecharts模块介绍以及折线图、地图、柱状图的绘制

一 python变量和json数据的相互转化 json就是 一种在各个编程语言中流通的数据格式&#xff0c;负责不同编程语言中的数据传递和交互、类似于: 国际通用语言 -英语。 import json if __name__ __main__:# 1. 将python变量转成json(列表->json)# 准备列表&#xff0c;列表内…

css background-position属性

定义 background-position 属性用于设置或获取元素背景图像相对于原点的初始位置。 background-position 基本语法 background-position&#xff1a;背景图片水平位置参数 背景图片垂直位置参数 &#xff1b; background-position 通常使用水平方向以及垂直方向的组合来定义背…

locust的安装和运行的demo

最近开始学习locust&#xff0c;从最初的安装到运行一个简单的demo。 lcoust官网上有介绍安装和使用&#xff1a;https://docs.locust.io/en/stable/installation.html locust 需要的python环境&#xff0c;首先要安装python。 因为本人已经安装了python以及python…

Spring Boot集成WebSocket Demo,简单明了

如果是初次搭建Spring BootWebSocket项目&#xff0c;不需要太复杂&#xff0c;只需要快速上手&#xff0c;那么你搜到的大部分文章可能都不适合你&#xff0c;我的这篇文章以最精简的方式搭建一个可以运行并通信的Spring BootWebSocket的Demo项目&#xff0c;有了根基之后再进…

记录部署ChatGLM大语言模型过程

1.什么是 ChatGLM&#xff1a; ChatGLM-6B 是一个开源的、支持中英双语的对话语言模型&#xff0c;基于 General Language Model 架构&#xff0c;具有 62 亿参数。结合模型量化技术&#xff0c;用户可以在消费级的显卡上进行本地部署&#xff08;INT4 量化级别下最低只需 6GB…

融合创新,着眼全局:泛工具行业增长如何顺势而为?

提到工具&#xff0c;你想到的是什么&#xff1f;办公场景中的扫描、传输、会议等工具&#xff0c;还是生活中帮你记录点滴、培养习惯的监督类APP&#xff0c;亦或者是消费支付买买买、旅游出行预订&#xff0c;甚至回家后的智能家居……工具类应用已经渗入我们工作生活的方方面…

数据结构与算法12:图、广度优先、深度优先

目录 【图】 【图的存储方法】 方法1&#xff1a;邻接矩阵 方法2&#xff1a;邻接链表 【图的算法】 广度优先搜索&#xff08;BFS&#xff09; 深度优先搜索&#xff08;DFS&#xff09; 【图】 在 数据结构与算法09&#xff1a;二叉树 这篇文章中讲述了“树”这种数…

RPC(远程过程调用)与消息队列介绍

文章目录 前言 一、过程调用分类 1.本地调用(Local Procedure Call,简称LPC) 2.远程调用(Remote Procedure Call&#xff0c;简称RPC) 二、API/SDK的区别是什么? 开发过程中,我们经常需要调用别人写的功能 三、WebClient 四、消息服务 目录 前言 一、过程调用分类 1.本…

excel转xmind

有如下excel&#xff0c;我们想把它转为xmind&#xff1b; 一、主流程 先说一下主要的流程&#xff1a; 需要把excel数据复制出来&#xff0c;放到文本编辑器&#xff08;如notepad&#xff09;中&#xff0c;比较乱哈&#xff0c;如下&#xff1a; 然后需要调整成如下格式…

JavaSE-04【方法】

JavaSE-04【方法】 第一章 方法 1.1 方法定义的格式详解 1、方法&#xff1a;就是若干语句的功能集合2、生活案例&#xff1a; 爆米花机&#xff1a; 原料&#xff1a;(玉米、糖) 产物&#xff1a;爆米花 3、方法中的两个重要名词 参数&#xff1a;即原料&#xff0c;就是进…

api接口汇总的平台

大麦网是一个在线购票平台&#xff0c;为音乐会、演唱会、话剧、体育比赛等各类娱乐活动提供门票销售服务。通过大麦网&#xff0c;用户可以轻松购买心仪的演出门票&#xff0c;并享受到良好的购票体验。 为了让更多用户了解到大麦网的商品详情&#xff0c;并能够方便地获取相…

网络协议 — IPv6 互联网协议第 6 版

目录 文章目录 目录IPv6IPv6 数据包格式固定报头扩展头部 IPv6 地址格式IPv6 网络的基本组成元素IPv6 的地址分类和寻址模式单播地址全球唯一地址&#xff08;Global Unique Address&#xff09;唯一本地地址&#xff08;Unique Local Unicast Address&#xff09;链路本地地址…

新手学习eclipse使用

目录 1 工具安装2 安装插件3 创建项目4 启动项目总结 对于新手程序员来说&#xff0c;选择一款趁手的工具还是有必要的。目前IDE比较好使用的是两块IDEA和eclipse&#xff0c;IDEA收费而且每年的费用不低&#xff0c;eclipse免费比较适合使用。 1 工具安装 下载地址&#xff1…

oracle-缩小表空间

刚准备收拾东西准备下班&#xff0c;突然接一个帮忙的事情&#xff0c;11g rac环境数据磁盘组使用率100%了&#xff0c;业务无法使用了&#xff0c;重新开电脑速战速决。 直接登录环境中&#xff0c;计划立刻释放一点点空间出来让业务恢复使用&#xff0c;业务恢复了再考虑增加…