w4lle's Notes

人生如逆旅,我亦是行人。

w4lle's avatar w4lle

Android热补丁之Robust(二)自动化补丁原理解析

在 Android 热补丁框架 Robust 中,几个重要的流程包括:

  • 补丁加载过程
  • 基础包插桩过程
  • 补丁包生成过程

在上一篇文章Android热补丁之Robust原理解析(一)中,我们分析了前两个,补丁加载过程和基础包插桩过程,分析的版本为 0.3.2
该篇文章为该系列的第二篇文章,主要分析补丁自动化生成的过程,分析的版本为0.4.82

时间跨度有点大…

系列文章:

整体流程

首先在 Gradle 插件中注册了一个名为 AutoPatchTranform 的 Tranform

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class AutoPatchTransform extends Transform implements Plugin<Project> {
@Override
void apply(Project target) {
initConfig();
project.android.registerTransform(this)
}
def initConfig() {
NameManger.init();
InlineClassFactory.init();
ReadMapping.init();
Config.init();
...
ReadXML.readXMl(project.projectDir.path);
Config.methodMap = JavaUtils.getMapFromZippedFile(project.projectDir.path + Constants.METHOD_MAP_PATH)
}

该类为插件的入口,实现了 GradlePlugin 并继承自 Transform,在入口处初始化配置并注册 Transform。配置主要是读取 Robust xml 配置、混淆优化后的 mapping 文件、插庄过程中生成的 methodsMap.robust 文件、初始化内联工厂类等等。
然后最主要的是transform方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Override
void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException {
project.android.bootClasspath.each {
Config.classPool.appendClassPath((String) it.absolutePath)
}
def box = ReflectUtils.toCtClasses(inputs, Config.classPool)
...
autoPatch(box)
...
}
def autoPatch(List<CtClass> box) {
...
ReadAnnotation.readAnnotation(box, logger);
if(Config.supportProGuard) {
ReadMapping.getInstance().initMappingInfo();
}
generatPatch(box,patchPath);
zipPatchClassesFile()
executeCommand(jar2DexCommand)
executeCommand(dex2SmaliCommand)
SmaliTool.getInstance().dealObscureInSmali();
executeCommand(smali2DexCommand)
//package patch.dex to patch.jar
packagePatchDex2Jar()
deleteTmpFiles()
}

transform 方法中,使用 javassist API 把所有需要处理的类加载到待扫描队列中,然后调用autoPatch方法自动生成补丁。
autoPatch方法中,主要做了这么几件事情:

  1. 读取被 @Add、@Modify、RobustModify.modify() 标注的方法或类并记录
  2. 解析 mapping 文件并记录每个类和类中方法混淆前后对应的信息,其中方法存储的信息有:返回值,方法名,参数列表,混淆后的名字;字段存储的信息有:字段名,混淆后的名字
  3. 根据得到的信息,generatPatch 方法实际生成补丁
  4. 将生成的补丁class打包成jar包
  5. jar -> dex
  6. dex -> smali
  7. 处理 smali,主要是处理 super 方法和处理混淆关系
  8. smali -> dex
  9. dex -> jar

1、2 比较好懂就不逐步分析了,主要看3;后面的5、6、7、8、9 都是为了 7 中处理 smali,所以只要搞懂smali处理就好了。下面我们分步来看。

生成补丁

主要逻辑在 generatPatch 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def generatPatch(List<CtClass> box,String patchPath){
...
InlineClassFactory.dealInLineClass(patchPath, Config.newlyAddedClassNameList)
initSuperMethodInClass(Config.modifiedClassNameList);
//auto generate all class
for (String fullClassName : Config.modifiedClassNameList) {
CtClass ctClass = Config.classPool.get(fullClassName)
CtClass patchClass = PatchesFactory.createPatch(patchPath, ctClass, false, NameManger.getInstance().getPatchName(ctClass.name), Config.patchMethodSignatureSet)
patchClass.writeFile(patchPath)
patchClass.defrost();
createControlClass(patchPath, ctClass)
}
createPatchesInfoClass(patchPath);
}
...
}

分为两个部分

逐步翻译

首先调用InlineClassFactory.dealInLineClass(patchPath, Config.newlyAddedClassNameList)识别被优化过的方法,这里的优化是泛指,包括被优化、内联、新增过的类和方法,具体的逻辑为扫描修改后的所有类和类中的方法,如果这些类和方法不在 mapping 文件中存在,那么可以定义为被优化过,其中包括@Add新增的类或方法。
然后调用initSuperMethodInClass方法识别修改后的所有类和类中的方法中,分析是否如包含 super 方法,如果有那么缓存下来。
然后调用 PatchesFactory.createPatch 反射翻译修改的类和方法,具体的实现在

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
private CtClass createPatchClass(CtClass modifiedClass, boolean isInline, String patchName, Set patchMethodSignureSet, String patchPath) throws CannotCompileException, IOException, NotFoundException {
//清洗需要处理的方法,略..
CtClass temPatchClass = cloneClass(modifiedClass, patchName, methodNoNeedPatchList);
JavaUtils.addPatchConstruct(temPatchClass, modifiedClass);
CtMethod reaLParameterMethod = CtMethod.make(JavaUtils.getRealParamtersBody(), temPatchClass);
temPatchClass.addMethod(reaLParameterMethod);
dealWithSuperMethod(temPatchClass, modifiedClass, patchPath);
for (CtMethod method : temPatchClass.getDeclaredMethods()) {
// shit !!too many situations need take into consideration
// methods has methodid and in patchMethodSignatureSet
if (!Config.addedSuperMethodList.contains(method) && reaLParameterMethod != method && !method.getName().startsWith(Constants.ROBUST_PUBLIC_SUFFIX)) {
method.instrument(
new ExprEditor() {
public void edit(FieldAccess f) throws CannotCompileException {
...
if (f.isReader()) { f.replace(ReflectUtils.getFieldString(f.getField(), memberMappingInfo, temPatchClass.getName(), modifiedClass.getName()));
} else if (f.isWriter()) {
f.replace(ReflectUtils.setFieldString(f.getField(), memberMappingInfo, temPatchClass.getName(), modifiedClass.getName()));
}
...
}
@Override
void edit(NewExpr e) throws CannotCompileException {
//inner class in the patched class ,not all inner class
...
if (!ReflectUtils.isStatic(Config.classPool.get(e.getClassName()).getModifiers()) && JavaUtils.isInnerClassInModifiedClass(e.getClassName(), modifiedClass)) {
e.replace(ReflectUtils.getNewInnerClassString(e.getSignature(), temPatchClass.getName(), ReflectUtils.isStatic(Config.classPool.get(e.getClassName()).getModifiers()), getClassValue(e.getClassName())));
return;
}
...
@Override
void edit(Cast c) throws CannotCompileException {
MethodInfo thisMethod = ReflectUtils.readField(c, "thisMethod");
CtClass thisClass = ReflectUtils.readField(c, "thisClass");
def isStatic = ReflectUtils.isStatic(thisMethod.getAccessFlags());
if (!isStatic) {
//inner class in the patched class ,not all inner class
if (Config.newlyAddedClassNameList.contains(thisClass.getName()) || Config.noNeedReflectClassSet.contains(thisClass.getName())) {
return;
}
// static函数是没有this指令的,直接会报错。
c.replace(ReflectUtils.getCastString(c, temPatchClass))
}
}
@Override
void edit(MethodCall m) throws CannotCompileException {
...
if (!repalceInlineMethod(m, method, false)) {
Map memberMappingInfo = getClassMappingInfo(m.getMethod().getDeclaringClass().getName());
if (invokeSuperMethodList.contains(m.getMethod())) {
int index = invokeSuperMethodList.indexOf(m.getMethod());
CtMethod superMethod = invokeSuperMethodList.get(index);
if (superMethod.getLongName() != null && superMethod.getLongName() == m.getMethod().getLongName()) {
String firstVariable = "";
if (ReflectUtils.isStatic(method.getModifiers())) {
//修复static 方法中含有super的问题,比如Aspectj处理后的方法
MethodInfo methodInfo = method.getMethodInfo();
LocalVariableAttribute table = methodInfo.getCodeAttribute().getAttribute(LocalVariableAttribute.tag);
int numberOfLocalVariables = table.tableLength();
if (numberOfLocalVariables > 0) {
int frameWithNameAtConstantPool = table.nameIndex(0);
firstVariable = methodInfo.getConstPool().getUtf8Info(frameWithNameAtConstantPool)
}
}
m.replace(ReflectUtils.invokeSuperString(m, firstVariable));
return;
}
}
m.replace(ReflectUtils.getMethodCallString(m, memberMappingInfo, temPatchClass, ReflectUtils.isStatic(method.getModifiers()), isInline));
}
...
}
});
}
}
//remove static code block,pay attention to the class created by cloneClassWithoutFields which construct's
CtClass patchClass = cloneClassWithoutFields(temPatchClass, patchName, null);
patchClass = JavaUtils.addPatchConstruct(patchClass, modifiedClass);
return patchClass;
}

这段代码其实是这个插件的核心部分,总体来说就是将修改后的代码全部翻译成反射调用生成 xxxPatch 类。
我们先只关注method.instrument()这个方法,这个Javassist的API,作用是遍历方法中的代码逻辑,包括:

  • FieldAccess,字段访问操作。分为字段读和写两种,分别调用ReflectUtils.getFieldString方法,将代码逻辑使用Javassist翻译成反射调用,然后替换。
  • NewExpr,new 对象操作。也分为两种
    • 非静态内部类,调用ReflectUtils.getNewInnerClassString翻译成反射,然后替换
    • 外部类,调用ReflectUtils.getCreateClassString翻译成反射,然后替换
  • Cast,强转操作。调用ReflectUtils.getCastString翻译成反射,然后替换
  • MethodCall,方法调用操作。情况比较复杂,以下几种情形
    • lamda表达式,调用ReflectUtils.getNewInnerClassString生成内部类的方法并翻译成反射,然后替换
    • 修改的方法是内联方法,调用ReflectUtils.getInLineMemberString方法生成占位内联类xxInLinePatch,并在改类中把修改的方法翻译成反射,然后替换调用,这方法中又有一些其他情况判断,感兴趣的读者可以自行阅读
    • 如果是super方法,这个情况后面单独拎出来说
    • 正常方法,调用ReflectUtils.getMethodCallString方法翻译成反射,然后替换
  • 生成补丁类并增加构造方法

请注意,以上所有方法和需要处理的方法都需要特别注意方法签名!

控制补丁行为

最后调用 createControlClass(patchPath, ctClass)createPatchesInfoClass(patchPath);生成 PatchesInfoImpl、xxxPatchControl 写入补丁信息和控制补丁行为。

其中,PatchesInfoImpl中包含所有补丁类的一一对应关系,比如 MainActivity -> MainActivityPatch,不清楚的可以参考该系列的上一篇文章。生成的xxxPatchControl类用于生成xxPatch类,并判断补丁中的方法是否和methods.robust中的方法id匹配,如果匹配才会去调用补丁中方法。

至此整体流程基本梳理完成。后面会针对具体的复杂情况加以解析。

this 如何处理

首先,在补丁类中 xxPatch 中,this指代的是xxPatch类的对象,而我们是想要的对象是被补丁的类的对象。

PatchesFactory.createPatchClass()方法中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
CtMethod reaLParameterMethod = CtMethod.make(JavaUtils.getRealParamtersBody(), temPatchClass);
temPatchClass.addMethod(reaLParameterMethod);
public static String getRealParamtersBody() {
StringBuilder realParameterBuilder = new StringBuilder();
realParameterBuilder.append("public Object[] " + Constants.GET_REAL_PARAMETER + " (Object[] args){");
realParameterBuilder.append("if (args == null || args.length < 1) {");
realParameterBuilder.append(" return args;");
realParameterBuilder.append("}");
realParameterBuilder.append(" Object[] realParameter = new Object[args.length];");
realParameterBuilder.append("for (int i = 0; i < args.length; i++) {");
realParameterBuilder.append("if (args[i] instanceof Object[]) {");
realParameterBuilder.append("realParameter[i] =" + Constants.GET_REAL_PARAMETER + "((Object[]) args[i]);");
realParameterBuilder.append("} else {");
realParameterBuilder.append("if (args[i] ==this) {");
realParameterBuilder.append(" realParameter[i] =this." + ORIGINCLASS + ";");
realParameterBuilder.append("} else {");
realParameterBuilder.append(" realParameter[i] = args[i];");
realParameterBuilder.append(" }");
realParameterBuilder.append(" }");
realParameterBuilder.append(" }");
realParameterBuilder.append(" return realParameter;");
realParameterBuilder.append(" }");
return realParameterBuilder.toString();
}

这段的作用是,在每个xxPatch补丁类中都插入一个getRealParameter()方法,反编译出来最终的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public Object[] getRealParameter(Object[] objArr) {
if (objArr == null || objArr.length < 1) {
return objArr;
}
Object[] objArr2 = new Object[objArr.length];
for (int i = 0; i < objArr.length; i++) {
if (objArr[i] instanceof Object[]) {
objArr2[i] = getRealParameter((Object[]) objArr[i]);
} else if (objArr[i] == this) {
objArr2[i] = this.originClass;
} else {
objArr2[i] = objArr[i];
}
}
return objArr2;
}

它的作用是,在每个xxPatch中使用的this,都转换成xx被补丁类的对象。其中的originClass就是补丁类的对象。

super 如何处理

this类似,xxPatch中调用 super 方法同样需要转为调用被补丁类中相关方法的super调用。

还是在PatchesFactory.createPatchClass()方法中有dealWithSuperMethod(temPatchClass, modifiedClass, patchPath);调用

1
2
3
4
5
6
7
8
9
private void dealWithSuperMethod(CtClass patchClass, CtClass modifiedClass, String patchPath) throws NotFoundException, CannotCompileException, IOException {
...
methodBuilder.append("public static " + invokeSuperMethodList.get(index).getReturnType().getName() + " " + ReflectUtils.getStaticSuperMethodName(invokeSuperMethodList.get(index).getName()) + "(" + patchClass.getName() + " patchInstance," + modifiedClass.getName() + " modifiedInstance," + JavaUtils.getParameterSignure(invokeSuperMethodList.get(index)) + "){");
...
CtClass assistClass = PatchesAssistFactory.createAssistClass(modifiedClass, patchClass.getName(), invokeSuperMethodList.get(index));
...
methodBuilder.append(NameManger.getInstance().getAssistClassName(patchClass.getName()) + "." + ReflectUtils.getStaticSuperMethodName(invokeSuperMethodList.get(index).getName()) + "(patchInstance,modifiedInstance");
...
}

保留主要代码,根据方法签名生成了一个新的方法,以staticRobust+methodName命名,方法中调用以RobustAssist结尾的类中的同名方法,并调用 PatchesAssistFactory.createAssistClass 方法生成该类,这个类的父类是被补丁类的父类。

1
2
3
4
5
6
7
8
9
10
PatchesAssistFactory.createAssistClass:
static createAssistClass(CtClass modifiedClass, String patchClassName, CtMethod removeMethod) {
...
staticMethodBuidler.append("public static " + removeMethod.returnType.name + " " + ReflectUtils.getStaticSuperMethodName(removeMethod.getName())
+ "(" + patchClassName + " patchInstance," + modifiedClass.getName() + " modifiedInstance){");
staticMethodBuidler.append(" return patchInstance." + removeMethod.getName() + "(" + JavaUtils.getParameterValue(removeMethod.getParameterTypes().length) + ");");
staticMethodBuidler.append("}");
...
}

然后在遍历MethodCall过程中,处理方法调用

1
2
3
4
5
6
7
m.replace(ReflectUtils.invokeSuperString(m, firstVariable));
...
def static String invokeSuperString(MethodCall m, String originClass) {
...
stringBuilder.append(getStaticSuperMethodName(m.methodName) + "(this," + Constants.ORIGINCLASS + ",\$\$);");
...
}

传递的参数,patch、originClass(被补丁类对象)、方法实际参数列表。

反编译出的结果实际是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
MainFragmentActivity:
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
...
}
MainFragmentActivityPatch:
public static void staticRobustonCreate(MainFragmentActivityPatch mainFragmentActivityPatch, MainFragmentActivity mainFragmentActivity, Bundle bundle) {
MainFragmentActivityPatchRobustAssist.staticRobustonCreate(mainFragmentActivityPatch, mainFragmentActivity, bundle);
}
MainFragmentActivityPatchRobustAssist:
public class MainFragmentActivityPatchRobustAssist extends WrapperAppCompatFragmentActivity {
public static void staticRobustonCreate(MainFragmentActivityPatch mainFragmentActivityPatch, MainFragmentActivity mainFragmentActivity, Bundle bundle) {
mainFragmentActivityPatch.onCreate(bundler);
}
}

参数是按照实际的方法参数传进去的,最后调用了xxPatch.superMethod方法。但是这样也并没有实现super方法的转义啊,再往下看。

在处理smali过程中,有这么一段:

1
2
3
4
5
6
7
private String invokeSuperMethodInSmali(final String line, String fullClassName) {
...
result = line.replace(Constants.SMALI_INVOKE_VIRTUAL_COMMAND, Constants.SMALI_INVOKE_SUPER_COMMAND);
...
result = result.replace("p0", "p1");
...
}

在处理之前,smali是长这样的:invoke-invoke-virtual {p0, p2}, Lcom/meituan/robust/patch/SecondActivityPatch;->onCreate(Landroid/os/Bundle;)V,
处理之后是这样的:invoke-super {p1, p2}, Landroid/support/v7/app/AppCompatActivity;->onCreate(Landroid/os/Bundle;)V,意思就是本来是调用正常方法,现在转为调用super方法,并且把参数换了一下,把p0(补丁类对象)换成了p1(被补丁类对象),这样就完成了super的处理。反编译后最终结果:

1
2
3
4
5
public class MainFragmentActivityPatchRobustAssist extends WrapperAppCompatFragmentActivity {
public static void staticRobustonCreate(MainFragmentActivityPatch mainFragmentActivityPatch, MainFragmentActivity mainFragmentActivity, Bundle bundle) {
super.onCreate(bundle);
}
}

内联怎么处理

内联是个广义的概念,包括了混淆过程中的优化(修改方法签名、删除方法等)、内联。在上面的分析中处理zi方法基本也提到了,缺啥补啥:就是把内联掉的方法再补回来。
对于内联的方法,不能用@Modify注解标注,只能使用RobustModify.modify()标注,因为在基础包中方法都没了,打了l补丁方法也没用。

主要逻辑在遍历MethodCall -> repalceInlineMethod() -> ReflectUtils.getInLineMemberString()

1
2
3
4
...
stringBuilder.append(" instance=new " + NameManger.getInstance().getInlinePatchName(method.declaringClass.name) + "(\$0);")
stringBuilder.append("\$_=(\$r)instance." + getInLineMethodName(method) + "(" + parameterBuilder.toString() + ");")
...

作用就是把内联掉的方法调用替换为InLine类中的新增方法。

结果就是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Parent {
private String first=null;
//privateMethod被内联了
// private void privateMethod(String fir){
// System.out.println(fir);
//}
public void setFirst(String fir){
first=fir;
Parent children=new Children();
//children.privateMethod("Robust");
//内联替换的逻辑
ParentInline inline= new ParentInline(children);
inline.privateMethod("Robust");
}
}
public class ParentInline{
private Parent children ;
public ParentInline(Parent p){
children=p;
}
//混淆为c
public void privateMethod(String fir){
System.out.println(fir);
}
}

总结

Robust 的核心其实就是自动化生成补丁这块,至于插庄、补丁加载这些都是很好实现的,因为没有很多的特殊情况需要处理。
这篇文章主要分析了自动化补丁插件的主要工作流程,和一些特殊情况的处理,文章有限,当然还有很多特殊情况没有分析,这里只是提供一些分析源码的思路,碰到特殊情况可以按照这个思路排查解决问题。

就像代码中有一行注释,我觉得特别能概括自动化生成补丁的团队的心里路程,在此也再次感谢美团团队对开源做出的贡献。

1
// shit !!too many situations need take into consideration

总体来说,Robust 坑是有的,但是它也是高可用性、高兼容性的热修复框架,尤其是在Android 系统开放性越来越收紧的趋势下,Robust 作为不 hook 系统 API 的热修复框架优势更加突出。虽然其中可能有一些坑,只要我们对原理熟悉掌握,才有信心能搞定这些问题。

下一篇文章,主要就讲下Robust与其他框架搭配使用出现的一些坑。

参考

本文链接: http://w4lle.com/2018/05/28/robust-1/

版权声明:本文为 w4lle 原创文章,可以随意转载,但必须在明确位置注明出处!
本文链接: http://w4lle.com/2018/05/28/robust-1/