android熱修復方案

熱補丁方案有很多,其中比較出名的有騰訊Tinker、阿里的AndFix、美團的Robust以及QZone的超級補丁方案。他們的優劣如下:

一、Tinker 熱修復

Tinker通過 Dexdiff 算法將原apk和修復後的apk中的dex文件進行對比,生成差分包,運行時將差分包中的dex和原包中的dex進行合併,從而加載差分包中修復好的類。因爲是運行時加載的dex文件,所以修復完成後不能即時生效,需要重啓app。

二、Qzone熱修復
QQ空間的熱修復原理和tinker有異曲同工之處,它基於dex分包方案,把bug類修復完成之後,單獨生成一個dex文件,運行期間加載dex補丁,運行的是修復後的類。在Android中所有我們運行期間需要的類都是由ClassLoader(類加載器)進行加載,因此讓ClassLoader加載全新的類替換掉出現Bug的類即可完成熱修復。所以也需要重啓才能生效。

三、AndFix熱修復

在native動態替換java層的方法,通過native層hook java層的代碼。執行方法時,會直接將修復後的方法再native層進行替換,達到修復的效果,這種方式修復後直接會生效,不需要重啓。

四、Robust美團熱修復方案

方法運行時會在方法內插入一段代碼,如果有修復內容,會將執行的代碼重定向到其他方法中。

參考了Instans Run的原理。這種方案也是不需要重啓的

五、我們基於QQ空間的熱修復方案進行研究

 1. ART與Dalvik

 什麼是Dalvik:

​ Dalvik是Google公司自己設計用於Android平臺的Java虛擬機。支持已轉換爲.dex(Dalvik Executable)格式的Java應用程序的運行,.dex格式是專爲Dalvik應用設計的一種壓縮格式,適合內存和處理器速度有限的系統。

什麼是ART:

Android Runtime, Android 4.4 中引入的一個開發者選項,也是 Android 5.0 及更高版本的默認模式。在應用安裝的時候Ahead-Of-Time(AOT)預編譯字節碼到機器語言,這一機制叫Ahead-Of-Time(AOT)預編譯。應用程序安裝會變慢,但是執行將更有效率,啓動更快。

在Dalvik下,應用運行需要解釋執行,常用熱點代碼通過即時編譯器(JIT)將字節碼轉換爲機器碼,運行效率低。而在ART 環境中,應用在安裝時,字節碼預編譯(AOT)成機器碼,安裝慢了,但運行效率會提高。
ART佔用空間比Dalvik大(字節碼變爲機器碼), “空間換時間"。
預編譯也可以明顯改善電池續航,因爲應用程序每次運行時不用重複編譯了,從而減少了 CPU 的使用頻率,降低了能耗。
Dexopt與DexAot

這兩個操作是Art架構安裝時的操作, ART會執行AOT,但針對Dalvik 開發的應用也能在 ART 環境中運作。

dexopt:對dex文件進行驗證和優化,優化後的格式爲odex(Optimized dex) 文件
dexAot:在安裝時對 dex 文件執行dexopt優化之後,再將odex進行 AOT 提前編譯操作,編譯爲OAT可執行文件(機器碼)

 2. ClassLoader

 Java 類加載器

BootClassLoader , 用於加載Android Framework層class文件。

PathClassLoader ,用於Android應用程序類加載器。可以加載指定的dex,以及jar、zip、apk中的classes.dex

DexClassLoader,加載指定的,以及jar、zip、apk 中的classes.dex。

我們可以在activity中打印來進行驗證:

	@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        /*
         * 測試classLoader的一些使用情況
         */
        // 我們外部的類都是用的PathClassLoader
        ClassLoader classLoader1 = this.getClassLoader();
        LogUtils.i("loader1 === " + classLoader1);

        // 父加載器就是BootClassLoader,所以這個類先從framework中去查找,找不到就從我們本地中查找
        LogUtils.i("loader1 parent === " + classLoader1.getParent());

        // framework層的類加載都是用的BootClassLoader
        ClassLoader classLoader2 = Activity.class.getClassLoader();
        LogUtils.i("loader  === " + classLoader2);
    }

打印結果:
  

 

3. 源碼跟蹤

在虛擬機中,加載一個類時,使用的時ClassLoader中的loadClass方法進行加載的,看一下源碼:

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
            // First, check if the class has already been loaded
            // 一個類加載後會加入到緩存中,以後加載時從緩存中讀取就可以了
            // 如果找不到就從父親classLoader中查找
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    c = findClass(name);
                }
            }
            return c;
    }

代碼中可以看到,虛擬機加載一個類時,會從父ClassLoader中查找這個類,父ClassLoade找不到會遞歸到父親的父親,如果祖輩都找不到時,纔會使用當前的ClassLoader進行查找。這就是傳說中的雙親委託機制。爲什麼這樣做呢?

1、避免重複加載,當父加載器已經加載了該類的時候,就沒有必要子ClassLoader再加載一次。

2、安全性考慮,防止核心API庫被隨意篡改。

顯而易見,這樣不管是父加載器還是自己,都會走到findClass()方法:

 @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        Class c = pathList.findClass(name, suppressedExceptions);
        if (c == null) {
            ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
            for (Throwable t : suppressedExceptions) {
                cnfe.addSuppressed(t);
            }
            throw cnfe;
        }
        return c;
    }

 ClassLoader中findCliss方法只拋出了一個異常?那肯定是它的子類重寫並實現它了~, 在android中,ClassLoader都是繼承了BaseDexClassLoader(可以看PathClassLoader和DexClassLoader, 往上有些人說PathClassLoader可以加載內部類,DexClassLoader纔可以加載外部存儲卡的文件,其實這兩者都可以加載, 沒有任何區別),以上代碼就是在BaseDexClassLoader中實現了類的查找。裏面是通過pathList來進行查找的。繼續看pathList(DexPathList.java)中的實現: 

public Class findClass(String name, List<Throwable> suppressed) {
        for (Element element : dexElements) {
            DexFile dex = element.dexFile;

            if (dex != null) {
                Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
                if (clazz != null) {
                    return clazz;
                }
            }
        }
        if (dexElementsSuppressedExceptions != null) {
            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
        }
        return null;
    }

 可以看到,dexElements是一個數組, 這裏遍歷了dexElements,DexFile對象可以看作是dex文件, 如果找到了類直接返回了,這裏驗證了我們上面所說的。QQ空間熱修復就是將修復包patch.dex加入到dexElements開始的位置,當虛擬機加載類時,會先從patch.dex中查找,找到了直接返回,找不到還使用原來的,這樣就達到了熱修復的效果。

dexElements是一個Element類型的數組,源碼中這個Element是私有的,如何創建新的Element並加入到dexElements中呢? 先來看看源碼中的dexElements是怎麼創建的:

// save dexPath for BaseDexClassLoader
this.dexElements = makePathElements(splitDexPath(dexPath), optimizedDirectory,
                                            suppressedExceptions);

 在pathList的構造方法中可以看到,通過makePathElements可以創建一個element數組,所以我們通過反射來調用makePathElements方法創建一個新的數組,再獲取到原數組,將兩個數組合併到dexElements就可以了。

拿SDK23舉例:

首先通過classloader找到pathList對象,

再執行pathList中的makePathElements方法創建補丁包的Element數組

反射拿到原來的dexElements數組

將兩個數組進行合併,放到一個新的數組中

再反射修改dexElements,將新數組覆蓋調原來的數組,完成熱修復。

代碼如下:

public static void install(ClassLoader classLoader,
                                   File patch) {


            List<File> patchs = new ArrayList<>();
            patchs.add(patch);


            // 查找pathList字段
            Field pathListField = ReflectUtils.findField(classLoader, "pathList");

            // 1. 獲取pathList對象
            try {
                Object pathList = pathListField.get(classLoader);

                if (pathList == null) {
                    throw new RuntimeException("pathList對象爲空");
                }

                Method method = ReflectUtils.findMethod(pathList, "makePathElements", List.class, File.class, List.class);


                ArrayList<IOException> suppressedExceptions = new ArrayList<>();

                // 2. 補丁包的elements數組
                Object[] patchElements = (Object[]) method.invoke(null, patchs, null, suppressedExceptions);

                Field dexElementsField = ReflectUtils.findField(pathList, "dexElements");
                // 3. 原來的dex數組
                Object[] oldElements = (Object[]) dexElementsField.get(pathList);

                // 進行合併
                // 4. 首先利用反射創建一個盛放兩個數組的新數組
                Object[] newElements = (Object[]) Array.newInstance(oldElements.getClass().getComponentType(),
                        oldElements.length + patchElements.length);

                // 5. 將兩個數組放到新數組中,補丁包的要放在前面
                System.arraycopy(patchElements, 0, newElements, 0, patchElements.length);
                System.arraycopy(oldElements, 0, newElements, patchElements.length, oldElements.length);

                // 6. 將原來的dexElement數組用新數組替換掉
                dexElementsField.set(pathList, newElements);

            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
                LogUtils.i("error == " + e.getTargetException().getMessage());
            }


        }

sdk23 , 19 , 14, 4 這些版本創建dexElement數組的方式不一樣,或許是方法名不同,或許是參數不同,需要對這幾個版本單獨做適配,這裏只列舉了sdk23的反射方法,其他版本原理相同。同時,這一部分內容可參考Tinker熱修復方案來進行適配:tinker方案 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章