Android熱補丁之Robust原理解析(一)

早在16年9月份,美團技術團隊就寫過一篇文章描述 Android 熱補丁框架Robust的簡單實現原理,可是並無開源;而後在17年3月份,美團團隊宣佈正式開源 Robust而且配套了自動打補丁包工具。本系列文章主要解析Robust實現原理,分爲幾個方面javascript

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

本文爲第一篇,主要講解補丁加載過程和基礎包插樁過程,分析版本 0.3.2html

從 InstantRun 提及

不得不說 InstantRun 真是個好東西。目前主流的熱修復框架都有或多或少的參考 InstantRun 的某些技術點,好比 Tinker 的官方文章中明確考慮過 InstantRun 中的 Application 替換,雖然最後沒有采用,可是身爲其兄弟庫的 TinkerPatch 中一鍵接入方案就採用的該技術點。關於該技術點,能夠參考我以前寫的一篇文章 一鍵接入Tinkerjava

咱們知道,InstantRun 對應三種更新機制:android

  • 冷插拔,咱們稱之爲重啓更新機制
  • 溫插拔,咱們稱之爲重啓Activity更新機制
  • 熱插拔,咱們稱之爲熱更新機制

若是你還不熟悉 InstantRun,請參考個人這篇文章從Instant run談Android替換Application和動態加載機制git

而這篇文章的主角 Robust ,其熱修復的關鍵技術點就是採用了 InstantRun 中的熱更新機制,對應於多 ClassLoader 的動態加載方案,即一個 dex 文件對應一個新建 ClassLoader 。github

Robust 原理解析

Robust 的原理能夠簡單描述爲:安全

  1. 打基礎包時插樁,在每一個方法前插入一段類型爲 ChangeQuickRedirect 靜態變量的邏輯
  2. 加載補丁時,從補丁包中讀取要替換的類及具體替換的方法實現,新建 ClassLoader 加載補丁dex。

咱們來分別分析。app

基礎概念

打基礎包時,Robust 爲每一個類新增了一個類型爲 ChangeQuickRedirect 的靜態變量,而且在每一個方法前,增長判斷該變量是否爲空的邏輯,若是不爲空,走打基礎包時插樁的邏輯,不然走正常邏輯。咱們反編譯出基礎包中的代碼以下:框架

//SecondActivity
public static ChangeQuickRedirect u;
protected void onCreate(Bundle bundle) {
        if (u != null) {
            if (PatchProxy.isSupport(new Object[]{bundle}, this, u, false, 78)) {
                PatchProxy.accessDispatchVoid(new Object[]{bundle}, this, u, false, 78);
                return;
            }
        }
        super.onCreate(bundle);
        ...
    }複製代碼

對應於補丁文件,須要有三個文件ide

  • PatchesInfoImpl 用於記錄修改的類,及其對應的 ChangeQuickRedirect 接口的實現,咱們反編譯補丁包得出如下結果,其中的類名是混淆後的。
public class PatchesInfoImpl implements PatchesInfo {
    public List getPatchedClassesInfo() {
        List arrayList = new ArrayList();
        arrayList.add(new PatchedClassInfo("com.meituan.sample.robusttest.l", "com.meituan.robust.patch.SampleClassPatchControl"));
        arrayList.add(new PatchedClassInfo("com.meituan.sample.robusttest.p", "com.meituan.robust.patch.SuperPatchControl"));
        arrayList.add(new PatchedClassInfo("com.meituan.sample.SecondActivity", "com.meituan.robust.patch.SecondActivityPatchControl"));
        EnhancedRobustUtils.isThrowable = false;
        return arrayList;
    }
}複製代碼
  • xxxPatchControlChangeQuickRedirect 接口的具體實現,是一個代理,具體的替換方法是在 xxxPatch 類中
public class SecondActivityPatchControl implements ChangeQuickRedirect {
...

    public boolean isSupport(String methodName, Object[] paramArrayOfObject) {
        return "78:79:90:".contains(methodName.split(":")[3]);
    }

    public Object accessDispatch(String methodName, Object[] paramArrayOfObject) {
        try {
            SecondActivityPatch secondActivityPatch;
            ...
            Object obj = methodName.split(":")[3];
            if ("78".equals(obj)) {
                secondActivityPatch.onCreate((Bundle) paramArrayOfObject[0]);
            }
            if ("79".equals(obj)) {
                return secondActivityPatch.getTextInfo((String) paramArrayOfObject[0]);
            }
            if ("90".equals(obj)) {
                secondActivityPatch.RobustPubliclambda$onCreate$0((View) paramArrayOfObject[0]);
            }
            return null;
        } catch (Throwable th) {
            th.printStackTrace();
        }
    }
}複製代碼

最終調用 accessDispatch 方法,該方法會根據傳遞過來的方法簽名,調用xxxPatch的修改過的方法。

  • xxxPatch 具體的替換實現類,代碼就不貼了。

其過程能夠簡單描述爲,下發補丁包後,新建 DexClassLoader 加載補丁 dex 文件,反射獲得 PatchesInfoImpl class,並建立其對象,調用 getPatchedClassesInfo() 方法獲得哪些修改的類(好比 SecondActivity),而後再經過反射循環拿到每一個修改類在當前環境中的的class,將其中類型爲 ChangeQuickRedirect 的靜態變量反射修改成 xxxPatchControl.java 這個class new 出來的對象。

用官方的一種圖很好的表達了替換原理。

Robust

補丁加載過程分析

demo中的補丁加載就一句

new PatchExecutor(getApplicationContext(), new PatchManipulateImp(),  new Callback()).start();複製代碼

PatchExecutor 是個 Thread

public class PatchExecutor extends Thread {
    @Override
    public void run() {
        ...
        applyPatchList(patches);
        ...
    }

    /** * 應用補丁列表 */
    protected void applyPatchList(List<Patch> patches) {
        ...
        for (Patch p : patches) {
            ...
            currentPatchResult = patch(context, p);
            ...
            }
    }

    protected boolean patch(Context context, Patch patch) {
        ...
        DexClassLoader classLoader = new DexClassLoader(patch.getTempPath(), context.getCacheDir().getAbsolutePath(),
                null, PatchExecutor.class.getClassLoader());
        patch.delete(patch.getTempPath());
        ...
        try {
            patchsInfoClass = classLoader.loadClass(patch.getPatchesInfoImplClassFullName());
            patchesInfo = (PatchesInfo) patchsInfoClass.newInstance();
            } catch (Throwable t) {
             ...
        }
        ...
        for (PatchedClassInfo patchedClassInfo : patchedClasses) {
            ...
            try {
                oldClass = classLoader.loadClass(patchedClassName.trim());
                Field[] fields = oldClass.getDeclaredFields();
                for (Field field : fields) {
                    if (TextUtils.equals(field.getType().getCanonicalName(), ChangeQuickRedirect.class.getCanonicalName()) && TextUtils.equals(field.getDeclaringClass().getCanonicalName(), oldClass.getCanonicalName())) {
                        changeQuickRedirectField = field;
                        break;
                    }
                }
                ...
                try {
                    patchClass = classLoader.loadClass(patchClassName);
                    Object patchObject = patchClass.newInstance();
                    changeQuickRedirectField.setAccessible(true);
                    changeQuickRedirectField.set(null, patchObject);
                    } catch (Throwable t) {
                    ...
                }
            } catch (Throwable t) {
                 ...
            }
        }
        return true;
    }
}複製代碼

開啓一個子線程,經過指定的路徑去讀patch文件的jar包,patch文件能夠爲多個,每一個patch文件對應一個 DexClassLoader 去加載,每一個patch文件中存在PatchInfoImp,經過遍歷其中的類信息進而反射修改其中 ChangeQuickRedirect 對象的值。

基礎包插樁過程分析

相似 InstantRun , Robust 也是使用 Transform API 修改字節碼文件,該 API 容許第三方插件在 .class 文件打包爲 dex 文件以前操做編譯好的 .class 字節碼文件。

Robust 中的 Gradle-Plugin 就是操做字節碼的名爲 robust 的 gradle 插件項目。咱們來簡單看下實現。

class RobustTransform extends Transform implements Plugin<Project> {
    ...
    @Override
    void apply(Project target) {
            //解析項目下robust.xml配置文件
            robust = new XmlSlurper().parse(new File("${project.projectDir}/${Constants.ROBUST_XML}"))
            ...
            project.android.registerTransform(this)
            project.afterEvaluate(new RobustApkHashAction())
        }

    @Override
    void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException {
    ...
    ClassPool classPool = new ClassPool()
    project.android.bootClasspath.each {
        logger.debug "android.bootClasspath " + (String) it.absolutePath
        classPool.appendClassPath((String) it.absolutePath)
    }
    ...
    def box = ConvertUtils.toCtClasses(inputs, classPool)
    insertRobustCode(box, jarFile)
    writeMap2File(methodMap, Constants.METHOD_MAP_OUT_PATH)
    ...
    }
}複製代碼

首先讀取 robust.xml 配置文件並初始化,可配置選項包括:

  • 一些開關選項
  • 須要熱補丁的包名或者類名,這些包名下的全部類都被會插入代碼
  • 不須要熱補的包名或者類名,能夠在須要熱補的包中剔除指定的類或者包

而後經過 Transform API 調用 transform() 方法,掃描全部類加入到 classPool 中,調用 insertRobustCode() 方法。

def insertRobustCode(List<CtClass> box, File jarFile) {
        ZipOutputStream outStream=new JarOutputStream(new FileOutputStream(jarFile));
        new ForkJoinPool().submit {
            box.each { ctClass ->
                if (isNeedInsertClass(ctClass.getName())) {
                   //將class設置爲public ctClass.setModifiers(AccessFlag.setPublic(ctClass.getModifiers()))
                    boolean addIncrementalChange = false;
                    ctClass.declaredBehaviors.findAll {
                    //規避接口和無方法類
                        if (ctClass.isInterface() || ctClass.declaredMethods.length < 1) {
                            return false;
                        }
                        if (!addIncrementalChange) {
                        //插入 public static ChangeQuickRedirect changeQuickRedirect;
                            addIncrementalChange = true;
                            ClassPool classPool = it.declaringClass.classPool
                            CtClass type = classPool.getOrNull(Constants.INTERFACE_NAME);
                            CtField ctField = new CtField(type, Constants.INSERT_FIELD_NAME, ctClass);
                            ctField.setModifiers(AccessFlag.PUBLIC | AccessFlag.STATIC)
                            ctClass.addField(ctField)
                            logger.debug "ctClass: " + ctClass.getName();
                        }

                        if (it.getMethodInfo().isStaticInitializer()) {
                            return false
                        }

                        // synthetic 方法暫時不aop 好比AsyncTask 會生成一些同名 synthetic方法,對synthetic 以及private的方法也插入的代碼,主要是針對lambda表達式
                        if ((it.getModifiers() & AccessFlag.SYNTHETIC) != 0 && !AccessFlag.isPrivate(it.getModifiers())) {
                            return false
                        }
                        //不支持構造方法
                        if (it.getMethodInfo().isConstructor()) {
                            return false
                        }
                        //規避抽象方法
                        if ((it.getModifiers() & AccessFlag.ABSTRACT) != 0) {
                            return false
                        }
                        //規避NATIVE方法
                        if ((it.getModifiers() & AccessFlag.NATIVE) != 0) {
                            return false
                        }
                        //規避接口
                        if ((it.getModifiers() & AccessFlag.INTERFACE) != 0) {
                            return false
                        }

                        if (it.getMethodInfo().isMethod()) {
                            if (AccessFlag.isPackage(it.modifiers)) {
                                it.setModifiers(AccessFlag.setPublic(it.modifiers))
                            }
                            //判斷是否有方法調用,返回是否插莊
                            boolean flag = modifyMethodCodeFilter(it)
                            if (!flag) {
                                return false
                            }
                        }
                        //方法過濾
                        if (isExceptMethodLevel && exceptMethodList != null) {
                            for (String exceptMethod : exceptMethodList) {
                                if (it.name.matches(exceptMethod)) {
                                    return false
                                }
                            }
                        }

                        if (isHotfixMethodLevel && hotfixMethodList != null) {
                            for (String name : hotfixMethodList) {
                                if (it.name.matches(name)) {
                                    return true
                                }
                            }
                        }
                        return !isHotfixMethodLevel
                    }.each { ctBehavior ->
                        // methodMap must be put here
                        methodMap.put(ctBehavior.longName, insertMethodCount.incrementAndGet());
                        try {
                        if (ctBehavior.getMethodInfo().isMethod()) {
                                boolean isStatic = ctBehavior.getModifiers() & AccessFlag.STATIC;
                                CtClass returnType = ctBehavior.getReturnType0();
                                String returnTypeString = returnType.getName();
                                def body = "if (${Constants.INSERT_FIELD_NAME} != null) {"
                                body += "Object argThis = null;"
                                if (!isStatic) {
                                    body += "argThis = \$0;"
                                }

                                body += " if (com.meituan.robust.PatchProxy.isSupport(\$args, argThis, ${Constants.INSERT_FIELD_NAME}, $isStatic, " + methodMap.get(ctBehavior.longName) + ")) {"
                                body += getReturnStatement(returnTypeString, isStatic, methodMap.get(ctBehavior.longName));
                                body += " }"
                                body += "}"
                                ctBehavior.insertBefore(body);
                            }
                        } catch (Throwable t ) {
                            logger.error "ctClass: " + ctClass.getName() + " error: " + t.toString();
                        }
                    }
                    }
                zipFile(ctClass.toBytecode(),outStream,ctClass.name.replaceAll("\\.","/")+".class");
            }
        }.get()
        outStream.close();
        logger.debug "robust insertMethodCount: " + insertMethodCount.get()
    }複製代碼

該方法作了如下幾件事:

  • 將class設置爲public
  • 規避 接口
  • 規避 無方法類
  • 規避 構造方法
  • 規避 抽象方法
  • 規避 native方法
  • 規避 synthetic方法
  • 過濾配置文件中不須要修復的類
  • 經過 javassist 在類中插入 public static ChangeQuickRedirect changeQuickRedirect;
  • 經過 javassist 在方法中插入邏輯代碼段
  • 經過 zipFile() 方法寫回class文件

最後調用 writeMap2File() 將插樁的方法信息寫入 robust/methodsMap.robust 文件中,此文件和混淆的mapping文件須要備份。

總結

到這裏本篇文章結束,主要講了下基礎原理、補丁加載流程和插樁過程。咱們也能夠簡單的對 Robust 作下總結。

優勢:

  • 因爲使用多ClassLoader方案(補丁中無新增Activity,因此不算激進類型的動態加載,無需hook system),兼容性和穩定性更好,不存在preverify的問題
  • 因爲採用 InstantRun 的熱更新機制,因此能夠即時生效,不須要重啓
  • 支持Android2.3-7.X版本
  • 對性能影響較小,不須要合成patch
  • 支持方法級別的修復,支持靜態方法
  • 支持新增方法和類
  • 支持ProGuard的混淆、內聯、編譯器優化後引發的問題(橋方法、lambda、內部類等)等操做

固然,有優勢就會有缺點:

  • 暫時不支持新增字段,但能夠經過新增類解決
  • 暫時不支持修復構造方法,已經在內測
  • 暫時不支持資源和 so 修復,不過這個問題不大,由於獨立於 dex 補丁,已經有很成熟的方案了,就看怎麼打到補丁包中以及 diff 方案。
  • 對於返回值是 this 的方法支持不太好
  • 沒有安全校驗,須要開發者在加載補丁以前本身作驗證
  • 可能會出現深度方法內聯致使的不可預知的錯誤(概率很小能夠忽略)

總的來講,Robust是可用的、高穩定性的、成功率很高(官方說99.9%)的、無侵入的一款優秀的熱修復框架。

參考

本文地址 w4lle.github.io/2017/03/31/…

相關文章
相關標籤/搜索