Android熱補丁之Robust(二)自動化補丁原理解析

w4lle發表於2018-06-11

在 Android 熱補丁框架 Robust 中,幾個重要的流程包括:

  • 補丁載入過程
  • 基礎包插樁過程
  • 補丁包生成過程

在上一篇文章Android熱補丁之Robust原理解析(一)中,我們分析了前兩個,補丁載入過程和基礎包插樁過程,分析的版本為 0.3.2。 該篇文章為該系列的第二篇文章,主要分析補丁自動化生成的過程,分析的版本為0.4.82

時間跨度有點大...

系列文章:

整體流程

首先在 Gradle 外掛中註冊了一個名為 AutoPatchTranform 的 Tranform

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方法

@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 方法

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 反射翻譯修改的類和方法,具體的實現在

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()方法中

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()方法,反編譯出來最終的結果:

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);呼叫

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 方法生成該類,這個類的父類是被補丁類的父類。

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過程中,處理方法呼叫

m.replace(ReflectUtils.invokeSuperString(m, firstVariable));
...
def static String invokeSuperString(MethodCall m, String originClass) {
    ...
    stringBuilder.append(getStaticSuperMethodName(m.methodName) + "(this," + Constants.ORIGINCLASS + ",\$\$);");
    ...
}
複製程式碼

傳遞的引數,patch、originClass(被補丁類物件)、方法實際引數列表。

反編譯出的結果實際是這樣的:

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過程中,有這麼一段:

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的處理。反編譯後最終結果:

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()

...
stringBuilder.append(" instance=new " + NameManger.getInstance().getInlinePatchName(method.declaringClass.name) + "(\$0);")
stringBuilder.append("\$_=(\$r)instance." + getInLineMethodName(method) + "(" + parameterBuilder.toString() + ");")
...
複製程式碼

作用就是把內聯掉的方法呼叫替換為InLine類中的新增方法。

結果就是這樣的:

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 的核心其實就是自動化生成補丁這塊,至於插莊、補丁載入這些都是很好實現的,因為沒有很多的特殊情況需要處理。 這篇文章主要分析了自動化補丁外掛的主要工作流程,和一些特殊情況的處理,文章有限,當然還有很多特殊情況沒有分析,這裡只是提供一些分析原始碼的思路,碰到特殊情況可以按照這個思路排查解決問題。

就像程式碼中有一行註釋,我覺得特別能概括自動化生成補丁的團隊的心裡路程,在此也再次感謝美團團隊對開源做出的貢獻。

//  shit !!too many situations need take into  consideration
複製程式碼

總體來說,Robust 坑是有的,但是它也是高可用性、高相容性的熱修復框架,尤其是在Android 系統開放性越來越收緊的趨勢下,Robust 作為不 hook 系統 API 的熱修復框架優勢更加突出。雖然其中可能有一些坑,只要我們對原理熟悉掌握,才有信心能搞定這些問題。

下一篇文章,主要就講下Robust與其他框架搭配使用出現的一些坑。

參考

相關文章