Android熱修復技術——QQ空間補丁方案解析(1)

傳統的app開發模式下,線上出現bug,必須通過發佈新版本,用戶手動更新後才能修復線上bug。隨着app的業務越來越複雜,代碼量爆發式增長,出現bug的機率也隨之上升。如果單純靠發版修復線上bug,其較長的新版覆蓋期無疑會對業務造成巨大的傷害,更不要說大型app開發通常涉及多個團隊協作,發版排期必須多方協調。
那麼是否存在一種方案可以在不發版的前提下修復線上bug?有!而且不只一種,業界各家大廠都針對這一問題拿出了自家的解決方案,較爲著名的有騰訊的Tinker和阿里的Andfix以及QQ空間補丁。網上對上述方案有很多介紹性文章,不過大多不全面,中間略過很多細節。筆者在學習的過程中也遇到很多麻煩。所以筆者將通過接下來幾篇博客對上述兩種方案進行介紹,力求不放過每一個細節。首先來看下QQ空間補丁方案。

1. Dex分包機制

大家都知道,我們開發的代碼在被編譯成class文件後會被打包成一個dex文件。但是dex文件有一個限制,由於方法id是一個short類型,所以導致了一個dex文件最多隻能存放65536個方法。隨着現今App的開發日益複雜,導致方法數早已超過了這個上限。爲了解決這個問題,Google提出了multidex方案,即一個apk文件可以包含多個dex文件。
不過值得注意的是,除了第一個dex文件以外,其他的dex文件都是以資源的形式被加載的,換句話說就是在Application.onCreate()方法中被注入到系統的ClassLoader中的。這也就爲熱修復提供了一種可能:將修復後的代碼達成補丁包,然後發送到客戶端,客戶端在啓動的時候到指定路徑下加載對應dex文件即可。
根據Android虛擬機的類加載機制,同一個類只會被加載一次,所以要讓修復後的類替換原有的類就必須讓補丁包的類被優先加載。接下來看下Android虛擬機的類加載機制。

2. 類加載機制

Android的類加載機制和jvm加載機制類似,都是通過ClassLoader來完成,只是具體的類不同而已:
1
Android系統通過PathClassLoader來加載系統類和主dex中的類。而DexClassLoader則用於加載其他dex文件中的類。上述兩個類都是繼承自BaseDexClassLoader,具體的加載方法是findClass:

public class BaseDexClassLoader extends ClassLoader {
    private final DexPathList pathList;

    /**
     * Constructs an instance.
     *
     * @param dexPath the list of jar/apk files containing classes and
     * resources, delimited by {@code File.pathSeparator}, which
     * defaults to {@code ":"} on Android
     * @param optimizedDirectory directory where optimized dex files
     * should be written; may be {@code null}
     * @param libraryPath the list of directories containing native
     * libraries, delimited by {@code File.pathSeparator}; may be
     * {@code null}
     * @param parent the parent class loader
     */
    public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }

    @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;
    }
}

從代碼中可以看到加載類的工作轉移到了pathList中,pathList是一個DexPathList類型,從變量名和類型名就可以看出這是一個維護Dex的容器:

/*package*/ final class DexPathList {
    private static final String DEX_SUFFIX = ".dex";
    private static final String JAR_SUFFIX = ".jar";
    private static final String ZIP_SUFFIX = ".zip";
    private static final String APK_SUFFIX = ".apk";

    /** class definition context */
    private final ClassLoader definingContext;

    /**
     * List of dex/resource (class path) elements.
     * Should be called pathElements, but the Facebook app uses reflection
     * to modify 'dexElements' (http://b/7726934).
     */
    private final Element[] dexElements;

    /**
     * Finds the named class in one of the dex files pointed at by
     * this instance. This will find the one in the earliest listed
     * path element. If the class is found but has not yet been
     * defined, then this method will define it in the defining
     * context that this instance was constructed with.
     *
     * @param name of class to find
     * @param suppressed exceptions encountered whilst finding the class
     * @return the named class or {@code null} if the class is not
     * found in any of the dex files
     */
    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;
    }
}

DexPathListfindClass也很簡單,dexElements是維護dex文件的數組,每一個item對應一個dex文件。DexPathList遍歷dexElements,從每一個dex文件中查找目標類,在找到後即返回並停止遍歷。所以要想達到熱修復的目的就必須讓補丁dex在dexElements中的位置先於原有dex:
23
這就是QQ空間補丁方案的基本思路,接下來的博文筆者將以一個實際的例子詳述QQ空間補丁熱修復的過程

發佈了81 篇原創文章 · 獲贊 19 · 訪問量 9萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章