Android热修

大家好,我是瑞英。

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

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

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

为何要热修?

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

热修方案概述

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

主流热修产品

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

代码修复方案

底层替换方案

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

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

特性:

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

类加载方案

合成修复后全量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包相关信息,最终构建出热修包。

实现修复的原理

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

详解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) //存入数组中
  • 5个以上参数的方法的热修代理方法(区分为有返回值和无返回值)

  • 5个及以下参数的方法的热修代理方法

java 复制代码
    //如下方法是被插桩到base包中的方法
    @JvmStatic
    fun proxyVoid0Para(obj: Any?, cls: Class<*>, methodNumber: Int): Boolean {
        return proxy(arrayOf(), obj, cls, methodNumber).isSupported
    }

    @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 proxy0Para(obj: Any?, cls: Class<*>, methodNumber: Int): PatchProxyResult {
        return proxy(arrayOf(), obj, cls, methodNumber)
    }
   

    @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<Object, Object> 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, Function1<? super 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中

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

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

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

总结

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

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

相关推荐
__water13 天前
『网络游戏』服务器向客户端分发消息【21】
服务器·客户端
网络研究院16 天前
Node.js 中的 WebSocket 底层实现
服务器·websocket·网络协议·node.js·客户端·底层·机制
大雪山掘金17 天前
开屏广告框架的设计
ios·客户端
jyl_sh1 个月前
CMake教程:第一步:一个基本的起点
开发语言·c++·客户端·cmake
jyl_sh1 个月前
Windows Ribbon框架
windows·ribbon·c#·客户端
jyl_sh1 个月前
Ribbon布局和尺寸调整
ribbon·c#·wpf·客户端
cyt涛2 个月前
Redis访问工具
数据库·redis·spring cache·注解·客户端·lettuce·redistemplae
Crazy Struggle2 个月前
.NET 8 + WPF 企业级工作流系统
wpf·客户端·工作流·.net 8.0
邂逅岁月2 个月前
【网络编程通关之路】 Udp 基础回显服务器(Java实现)及你不知道知识原理详解 ! ! !
服务器·前端·网络·后端·udp·java-ee·客户端