傳統的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來完成,只是具體的類不同而已:
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;
}
}
DexPathList
的findClass
也很簡單,dexElements
是維護dex文件的數組,每一個item對應一個dex文件。DexPathList
遍歷dexElements
,從每一個dex文件中查找目標類,在找到後即返回並停止遍歷。所以要想達到熱修復的目的就必須讓補丁dex在dexElements
中的位置先於原有dex:
這就是QQ空間補丁方案的基本思路,接下來的博文筆者將以一個實際的例子詳述QQ空間補丁熱修復的過程