Android热修--base包插桩详解

404 阅读4分钟

大家好,我是瑞英。

本篇会使用具体实例详细分析一下,基于美团开源热修框架中,对base包的插桩方案所进行的优化。目的:减小包体。

名词解析:

  • base包:宿主包,可能拉取热修并加载完成,最终使得问题被修复的线上包。
  • patch包:热修包,基于base包打出的能够解决线上问题的包。

原始美团base包插桩方案解析

  1. 为类插入一个重定向属性,changeQuickRedirect

    
    public interface ChangeQuickRedirect {
        Object accessDispatch(String methodName, Object[] paramArrayOfObject);
    
        boolean isSupport(String methodName, Object[] paramArrayOfObject);
    }
    
  2. 为方法体中插入PatchProxy.proxy()方法调用

    /**
    * 
    * @param paramsArray[] 参数数组
    * @param current 当前类对象this
    * @param changeQuickRedirect 重定向对象
    * @param isStatic 当前方法是否为静态方法
    * @param methodNumber 方法编号
    * @param paramsClassTypes 参数类型数组
    * @param returnType 返回类型
    * @return
    */
    
    public class PatchProxy {
       public static PatchProxyResult proxy(Object[] paramsArray, Object current, ChangeQuickRedirect changeQuickRedirect, boolean isStatic, int methodNumber, Class[] paramsClassTypes, Class returnType) {
       PatchProxyResult patchProxyResult = new PatchProxyResult();
       if (PatchProxy.isSupport(paramsArray, current, changeQuickRedirect, isStatic, methodNumber, paramsClassTypes, returnType)) {
           patchProxyResult.isSupported = true;
           patchProxyResult.result = PatchProxy.accessDispatch(paramsArray, current, changeQuickRedirect, isStatic, methodNumber, paramsClassTypes, returnType);
       }
       return patchProxyResult;
       }
    }
    

上述两个步骤在一个Test1示例上,字节码插桩后,结果如下:

public final class Test1 {
    public static ChangeQuickRedirect changeQuickRedirect; // 热修重定向属性

    private Test1() {
    }

    public final boolean test(boolean r8, String string) {
        // PatchProxy.proxy调用
        PatchProxyResult proxy = PatchProxy.proxy(new Object[]{new Byte(r8 ? (byte) 1 : (byte) 0), string}, this, changeQuickRedirect, false, 237, Boolean.TYPE);
        if (proxy.isSupported) {
            return ((Boolean) proxy.result).booleanValue();
        }
        return r8;
    }
}

如果对上述Test1类进行热修

  • 首先会打出一个Test1PatchControl用于将原方法调用转发到热修方法中
public class Test1PatchControl implements ChangeQuickRedirect {

    /**
        methodName格式:【className + ":" + methodName + ":" + isStatic + ":" + methodNumber】
        根据方法名,将当前调用分发到对应的修复方法
    */
    public Object accessDispatch(String methodName, Object[] paramArrayOfObject) {
        // 调用匹配的修复方法
        Tes1Patch.test()
        return null;
    }

    public boolean isSupport(String methodName, Object[] paramArrayOfObject) {
      // 根据方法编号,判定当前方法是否被热修
    }
}
  • 其次会打出一个Test1Patch类,包含了被修复的test()方法

  • 最后会打出PatchesInfo类,记录被热修的类信息

    public class PatchesInfoImpl implements PatchesInfo {
        public List getPatchedClassesInfo() {
            ArrayList var1 = new ArrayList();
            PatchedClassInfo var2 = new PatchedClassInfo("com.test.Test1", "com.test.Test1PatchControl");
        }
    }
    

加载热修包时,会通过反射对被修复类的静态属性changeQuickRedirect注入值,实现的效果是: Test1.changeQuickRedirect = new Test1PatchControl()

优化后的base插桩

对上述的插桩方案进行仔细分析,进行优化

  1. 不再对每个base包中的类插入一个静态changeQuickRedirect属性,同时proxy方法中也无需传递这个参数

加载热修包时,被修复类的信息可以直接存在一个静态map中,proxy()方法中,直接读取这个map的信息来判断当前类有无对应修复类

image.png

  1. 精简proxy方法传递的参数

根据被插桩方法的参数和返回值特性,调用不同的proxy方法.

目的:使得base插入的指令尽可能的少

  • 根据返回值类型划分

    • 无返回值,则proxy方法直接返回true/false,表示当前方法是否被热修,如此被插桩方法中不需要出现result.isSupport的判断语句,再决定当前方法是否被热修
    fun proxyWithoutRes(...省略): Boolean {
        return proxy(...).isSupported
    }
    
    • 有返回值,则proxy方法需要返回result
    fun proxy(...省略): Result {
        return result
    }
    
  • 根据被插桩方法的参数个数划分

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

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

//有一个参数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) //存入数组中
  • 去掉原有传递的参数:isStatic

    直接根据传入的当前类对象current:Object,如果为空,就可确定为被插桩方法是静态方法

  • 去掉原有传递的参数:被插桩方法的参数类型数组和返回值类型Class[] paramsClassTypes, Class returnType

    热修插桩生成Test1PatchControl.accessDispatch()方法中,会对每个参数进行类型转换(编译时就能够拿到参数的各个类型,不需要特意传递),returnType同理

  • ChangeQuickRedirect接口中定义接口方法参数变动[为了适配PatchProxy.proxy方法参数的变动]

image.png

最终PatchProxy.proxy方法被精简为下图所示:

image.png

上述Test1使用优化后的base插桩方案生成的字节码,如下:[明显指令数减少]

public final class Test1 {

    private Test1() {
    }

    public final boolean test(boolean r8, String string) {
        PatchProxyResult proxyPara2 = PatchProxy.proxyPara2(new Boolean(r8), string, this, Test1.class, 138);
        if (proxy2Para.isSupported) {
            return ((Boolean) proxy2Para.result).booleanValue();
        }
        return r8;
    }
}