首页 最新 热门 推荐

  • 首页
  • 最新
  • 热门
  • 推荐

  • 24-11-26 08:01
  • 4566
  • 10905
juejin.cn

大家好,我是瑞英。

本篇会描述一个完整的安卓热修复工具的设计与实现。该热修工具基于instantRun原理,并且参照美团开源的robust框架,能够有效进行代码热修、so热修以及资源热修

热修复:无需通过发版,修复线上客户端问题的一种技术方案,特点是快速止损。

本文描述中:base包就是宿主包【需要被修复的包】,patch包是下发到base包的修复包

为何要热修?

客户端线上出现问题,传统的解决方案就是发一个新的客户端版本,让用户主动触发升级应用,覆盖速度十分有限。问题修复时间越长,损失就会越大。需要一种可以快速修复线上客户端问题的技术-称之为热修复。 热修复能够做到用户无感知,快速修复线上问题

image.png

热修方案概述

原理上看,目前安卓热修主要分三种:基于类加载、基于底层替换、侵入方法插桩的。

主流热修产品

厂商产品修复范围修复时机稳定性接入成本技术方案
腾讯tinker类、资源、so冷启一般高合成差量热修dex并冷启加载
阿里sophix类、资源、so冷启动、即时修复都支持(可选)高高(商用)综合方案(底层替换方案&类加载方案)
美团robust方法修复及时修复高低下文详细介绍

代码修复方案

底层替换方案

直接在native层,将被修复类对应的artMethod进行替换,即可完成方法修复。

每一个java方法在art中都对应着一个ArtMethod,记录了这个java方法的所有信息:所属类、访问权限、代码执行地址等

特性:

  1. 无法实现对原有类方法和字段的增减(只支持方法替换)
  2. 修复了的非静态方法,无法被正常发射调用(因为反射调用的时候会verifyObjectIsClass)
  3. 实效性好,可立即加载生效无需重启应用
  4. 需要针对dalvik虚拟机和art虚拟机做适配,需要考虑指令集的兼容问题
  5. 无法解决匿名内部类增减的情况
  6. 不支持 方法热修

类加载方案

合成修复后全量dex,冷启重新加载类,完成修复

特性:

  1. 需要冷启生效
  2. 高兼容性,几乎可以修复任何代码修复的场景

so修复方案

通过反射将指定热修so路径插入到nativeLibraryDirectories

base构建时保留所有so的md5值,patch包构建时,会进行校验,识别出发生变动的热修so,并将其打入patch中

资源修复方案

资源热修包的构建:

base构建时会保留base包的资源id,以及所有资源md5值,patch构建时,利用base id实现资源id固定,同时将新增资源打入patch中,使用新增资源的方法被自动标注为修复方法

资源热修包的加载: 通过反射调用AssetManager.addAssetPath,添加热修资源路径,在activity loadResources时,触发load热修资源

代码修复方案详解

在base包构建时,对需要被热修的方法进行插桩,保留相关base包构建信息【方法、类、属性以及其混淆信息】,在热修包构建时,依赖注解识别出被热修的方法,并结合base包相关信息,最终构建出热修包。

image.png

实现修复的原理

在base包构建时,对于方法都插入一个条件分支,执行热修代理调用。如果热修代理方法返回结果为true,则当前方法直接返回热修result,即该方法被成功热修【如下图所示】。当然这种侵入base包构建的热修方案,会导致包体积有所增加。

image.png

详解base包插桩指令

根据方法的参数和返回值特性,进行不同proxy方法的插入

  • 根据返回值分类:

    无返回值,则proxy方法直接返回boolean即可,如此被插桩方法中不需要出现proxyResult.isSupport的判断

    有返回值:需要返回ProxyResult

  • 根据参数个数进行分类,使得在插桩时,插桩方法的参数尽可能的少且简单,即插入指令尽可能的少。(目前对5个及以下的参数个数进行分类)

    只有5个以上的参数方法被插桩时,需要采用Object[]数组传递所有的参数。因为构建数组并且初始化数组元素,所需要的指令较多。

    例如:若方法只有一个参数,那么直接传递object对象只需要1条指令,如果通过Object[]传递该对象需要6条指令

    scss
    代码解读
    复制代码
    //有一个参数str:String,存放与局部变量表中 index = 1 //直接传递该object对象 mv.visitMethodInsn(ALOAD, 1) //利用object数组进行传递 mv.visitInsn(1)//数组大小 mv.visitTypeInsn(Opcodes.ANEWARRAY, "java/lang/Object") mv.visitInsn(Opcodes.DUP)// 创建数组object[] mv.visitInsn(Opcodes.ICONST_0)// 下标索引 mv.visitVarInsn(Opcodes.ALOAD, 1) //获取局部变量表中该object对象 mv.visitInsn(Opcodes.AASTORE) //存入数组中
  • 插入的热修代理方法示例

java
代码解读
复制代码
@JvmStatic fun proxyVoid4Para( param1: Any?, param2: Any?, param3: Any?, param4: Any?, obj: Any?, cls: Class<*>, methodNumber: Int ): Boolean { return proxy(arrayOf(param1, param2, param3, param4), obj, cls, methodNumber).isSupported } @JvmStatic fun proxy4Para(param1: Any?, param2: Any?, param3: Any?, param4: Any?, obj: Any?, cls: Class<*>, methodNumber: Int): PatchProxyResult { return proxy(arrayOf(param1, param2, param3, param4), obj, cls, methodNumber) }
  • proxy方法传递的参数详解

    • 当前方法的参数
    • 当前类(用于查找当前类是否有热修对象)
    • 当前类对象(如果是静态方法则传null,用于对当前类非静态属性的访问)
    • 方法编号(用于匹配热修方法)

详解patch包插桩

每一个被修复的类(PatchTestAct)必然会插桩生成两个类:

  • Patch类(PatchTestActPatch),这个类中有修复方法
  • 一个控制类(实现ChangeQuickRedirect接口,PatchTestActPatchControl),分发执行Patch类中的修复方法

从上述PatchProxy.proxy方法中可以看出。所有被热修的类,会被存在一个重定向map中。执行proxy方法时,若表中有该被插桩类,则对应执行该插桩类的热修对象(ChangeQUickRedirect实现类对象),执行该对象的

accessDispatch方法。每个方法在base构建时都会有一个编号。热修对象通过传入的方法编号,确定最终执行的热修方法。

java
代码解读
复制代码
public interface ChangeQuickRedirect { /** * 将方法的执行分发到对应的修复方法 * @param methodName 被插桩的方法编号 * @param paramArrayOfObject 参数值列表 * @param obj 被插桩类对象 * @return */ Object accessDispatch(String methodNumber, Object[] paramArrayOfObject, Object obj); /** * 判断方法是否能被分发到对应的修复方法 */ boolean isSupport(String methodNumber); /** * 判断方法是否能被分发到对应的修复方法 */ boolean isSupport(String methodNumber); }

如上述例子中,要热修该PatchTestAct2.test方法,对该方法加上@Modify注解后,进行热修patch构建后生成的PatchControl类和Patch类分别是:

java
代码解读
复制代码
public class PatchTestActPatchControl implements ChangeQuickRedirect { public static final String MATCH_ALL_PARAMETER = "(\\w*\\.)*\\w*"; private static final Map keyToValueRelation = new WeakHashMap(); public PatchTestActPatchControl() { } public Object accessDispatch(String methodNumber, Object[] paramArrayOfObject, Object var3) { try { PatchTestActPatch var4 = null; if (var3 != null) { if (keyToValueRelation.get(var3) == null) { var4 = new PatchTestActPatch(var3); keyToValueRelation.put(var3, (Object)null); } else { var4 = (PatchTestActPatch)keyToValueRelation.get(var3); } } else { var4 = new PatchTestActPatch((Object)null); } if ("119".equals(methodNumber)){var4.invokeAddMethod((Context)paramArrayOfObject[0]); } if ("120".equals(methodNumber)) { var4.test((String)paramArrayOfObject[0], (Function1)paramArrayOfObject[1]); } } catch (Throwable var7) { var7.printStackTrace(); } return null; } public boolean isSupport(String methodName) { return ":119::120:".contains(":" + methodName + ":"); } private static Object fixObj(Object booleanObj) { if (booleanObj instanceof Byte) { byte byteValue = (Byte)booleanObj; boolean booleanValue = byteValue != 0; return new Boolean(booleanValue); } else { return booleanObj; } } // 看起来好像没有用到这个方法 public Object getRealParameter(Object var1) { return var1 instanceof PatchTestAct ? new PatchTestActPatch(var1) : var1; } }
java
代码解读
复制代码
public class PatchTestActPatch { PatchTestAct originClass; /** * 传入原始对象 */ public PatchTestActPatch(Object var1) { this.originClass = (PatchTestAct)var1; } /** * 将所访问的变量做一个转换,如果访问的是当前类this,则需要转换为this.originClass对象 */ public Object[] getRealParameter(Object[] var1) { if (var1 != null && var1.length >= 1) { Object[] var2 = (Object[])Array.newInstance(var1.getClass().getComponentType(), var1.length); for(int var3 = 0; var3 < var1.length; ++var3) { if (var1[var3] instanceof Object[]) { var2[var3] = this.getRealParameter((Object[])var1[var3]); } else if (var1[var3] == this) { var2[var3] = this.originClass; } else { var2[var3] = var1[var3]; } } return var2; } else { return var1; } } /** * 被修复的方法 */ public final void test(String str, Function1super String, Unit> a) { String var3 = "str"; Object[] var5 = this.getRealParameter(new Object[]{str, var3}); Class[] var6 = new Class[]{Object.class, String.class}; EnhancedRobustUtils.invokeReflectStaticMethod("checkNotNullParameter", Intrinsics.class, var5, var6); String var7 = "a"; Object[] var9 = this.getRealParameter(new Object[]{a, var7}); Class[] var10 = new Class[]{Object.class, String.class}; EnhancedRobustUtils.invokeReflectStaticMethod("checkNotNullParameter", Intrinsics.class, var9, var10); Object[] var12 = this.getRealParameter(new Object[]{str}); Class[] var13 = new Class[]{Object.class}; Object var14; if (a == this && 0 == 0) { var14 = ((PatchTestActPatch)a).originClass; } else { var14 = a; } Object var10000 = (Object)EnhancedRobustUtils.invokeReflectMethod("invoke", var14, var12, var13, Function1.class); } }

每一个新增方法(在base包中不存在的方法):

对这个新增方法所在类打一个InlinePatch.class类,该类中定义这个新增方法

热修代码的处理过程

从字节码到patch.dex中

image.png

代码修复中解决的关键问题

本方案支持,方法修复、新增方法、新增类、新增属性、新增override方法。主要解决了以下问题:

  • 修复方法中对其他类属性、方法的调用
  • 修复代码中,存在调用base包中被删除的方法的指令
  • 修复代码中存在匿名内部类的生成和使用、when表达式与enum联用
  • 修复方法中存在调用父类方法的指令
  • 修复代码中存在invokeDynamic指令(单接口lambda表达式/函数式接口、高阶函数等)
  • 新增方法是override方法,并且使用其多态属性
  • 修复构造方法、新增构造方法
  • 修复方法有@JvmStatic注解,@JvmOverloads注解,这些注解方法被java 和kotlin调用不同而编译出不同的字节码
  • r8内联、外联、类合并等系列优化操作,使得编译结果与原始字节码有很大的差异

总结

本文所描述的代码修复方案,相对于美团原始方案做了较大优化,base插桩对插入指令做了精简,且不再对每个类插入属性用于判断当前类是否被热修,而是将被修复类的信息存在一个静态map中。patch插桩完全重新处理,大大拓展了可修复的范围,提高了热修工具可用性。后续也扩展支持了,通过字节码对比自动识别需要修复的代码,无需开发者手动标注。

除上文所述之外,热修也有一些其他方面值得讨论,热修sop、热修包的构建速度提升,以及热修包的下发和加载等。

参考: github.com/Meituan-Dia…

注:本文转载自juejin.cn的瑞英的文章"https://juejin.cn/post/7426988056635015206"。版权归原作者所有,此博客不拥有其著作权,亦不承担相应法律责任。如有侵权,请联系我们删除。
复制链接
复制链接
相关推荐
发表评论
登录后才能发表评论和回复 注册

/ 登录

评论记录:

未查询到任何数据!
回复评论:

分类栏目

后端 (14832) 前端 (14280) 移动开发 (3760) 编程语言 (3851) Java (3904) Python (3298) 人工智能 (10119) AIGC (2810) 大数据 (3499) 数据库 (3945) 数据结构与算法 (3757) 音视频 (2669) 云原生 (3145) 云平台 (2965) 前沿技术 (2993) 开源 (2160) 小程序 (2860) 运维 (2533) 服务器 (2698) 操作系统 (2325) 硬件开发 (2492) 嵌入式 (2955) 微软技术 (2769) 软件工程 (2056) 测试 (2865) 网络空间安全 (2948) 网络与通信 (2797) 用户体验设计 (2592) 学习和成长 (2593) 搜索 (2744) 开发工具 (7108) 游戏 (2829) HarmonyOS (2935) 区块链 (2782) 数学 (3112) 3C硬件 (2759) 资讯 (2909) Android (4709) iOS (1850) 代码人生 (3043) 阅读 (2841)

热门文章

140
Android
关于我们 隐私政策 免责声明 联系我们
Copyright © 2020-2025 蚁人论坛 (iYenn.com) All Rights Reserved.
Scroll to Top