傳統(tǒng)的app開(kāi)發(fā)模式下,線上出現(xiàn)bug忘伞,必須通過(guò)發(fā)布新版本,用戶手動(dòng)更新后才能修復(fù)線上bug沙兰。隨著app的業(yè)務(wù)越來(lái)越復(fù)雜氓奈,代碼量爆發(fā)式增長(zhǎng),出現(xiàn)bug的機(jī)率也隨之上升鼎天。如果單純靠發(fā)版修復(fù)線上bug舀奶,其較長(zhǎng)的新版覆蓋期無(wú)疑會(huì)對(duì)業(yè)務(wù)造成巨大的傷害,更不要說(shuō)大型app開(kāi)發(fā)通常涉及多個(gè)團(tuán)隊(duì)協(xié)作斋射,發(fā)版排期必須多方協(xié)調(diào)育勺。
那么是否存在一種方案可以在不發(fā)版的前提下修復(fù)線上bug?有罗岖!而且不只一種涧至,業(yè)界各家大廠都針對(duì)這一問(wèn)題拿出了自家的解決方案,較為著名的有騰訊的Tinker和阿里的Andfix以及QQ空間補(bǔ)丁桑包。網(wǎng)上對(duì)上述方案有很多介紹性文章南蓬,不過(guò)大多不全面,中間略過(guò)很多細(xì)節(jié)哑了。筆者在學(xué)習(xí)的過(guò)程中也遇到很多麻煩赘方。所以筆者將通過(guò)接下來(lái)幾篇博客對(duì)上述兩種方案進(jìn)行介紹,力求不放過(guò)每一個(gè)細(xì)節(jié)弱左。首先來(lái)看下QQ空間補(bǔ)丁方案窄陡。
1. Dex分包機(jī)制
大家都知道,我們開(kāi)發(fā)的代碼在被編譯成class文件后會(huì)被打包成一個(gè)dex文件拆火。但是dex文件有一個(gè)限制跳夭,由于方法id是一個(gè)short類(lèi)型,所以導(dǎo)致了一個(gè)dex文件最多只能存放65536個(gè)方法榜掌。隨著現(xiàn)今App的開(kāi)發(fā)日益復(fù)雜优妙,導(dǎo)致方法數(shù)早已超過(guò)了這個(gè)上限。為了解決這個(gè)問(wèn)題憎账,Google提出了multidex方案套硼,即一個(gè)apk文件可以包含多個(gè)dex文件。
不過(guò)值得注意的是胞皱,除了第一個(gè)dex文件以外邪意,其他的dex文件都是以資源的形式被加載的九妈,換句話說(shuō)就是在Application.onCreate()
方法中被注入到系統(tǒng)的ClassLoader
中的。這也就為熱修復(fù)提供了一種可能:將修復(fù)后的代碼達(dá)成補(bǔ)丁包雾鬼,然后發(fā)送到客戶端萌朱,客戶端在啟動(dòng)的時(shí)候到指定路徑下加載對(duì)應(yīng)dex文件即可。
根據(jù)Android虛擬機(jī)的類(lèi)加載機(jī)制策菜,同一個(gè)類(lèi)只會(huì)被加載一次晶疼,所以要讓修復(fù)后的類(lèi)替換原有的類(lèi)就必須讓補(bǔ)丁包的類(lèi)被優(yōu)先加載。接下來(lái)看下Android虛擬機(jī)的類(lèi)加載機(jī)制又憨。
2. 類(lèi)加載機(jī)制
Android的類(lèi)加載機(jī)制和jvm加載機(jī)制類(lèi)似翠霍,都是通過(guò)ClassLoader來(lái)完成,只是具體的類(lèi)不同而已:
Android系統(tǒng)通過(guò)
PathClassLoader
來(lái)加載系統(tǒng)類(lèi)和主dex中的類(lèi)蠢莺。而DexClassLoader
則用于加載其他dex文件中的類(lèi)寒匙。上述兩個(gè)類(lèi)都是繼承自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;
}
}
從代碼中可以看到加載類(lèi)的工作轉(zhuǎn)移到了pathList
中躏将,pathList
是一個(gè)DexPathList
類(lèi)型锄弱,從變量名和類(lèi)型名就可以看出這是一個(gè)維護(hù)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
也很簡(jiǎn)單,dexElements
是維護(hù)dex文件的數(shù)組祸憋,每一個(gè)item對(duì)應(yīng)一個(gè)dex文件会宪。DexPathList
遍歷dexElements
,從每一個(gè)dex文件中查找目標(biāo)類(lèi)夺衍,在找到后即返回并停止遍歷狈谊。所以要想達(dá)到熱修復(fù)的目的就必須讓補(bǔ)丁dex在dexElements
中的位置先于原有dex:
這就是QQ空間補(bǔ)丁方案的基本思路,接下來(lái)的博文筆者將以一個(gè)實(shí)際的例子詳述QQ空間補(bǔ)丁熱修復(fù)的過(guò)程