一、Android插件化叉抡、組件化尔崔、熱修復(fù)的區(qū)別
插件化
插件化是一種將應(yīng)用程序按照模塊或組件進(jìn)行拆分,并以插件的方式動態(tài)加載和運(yùn)行的技術(shù)褥民。其主要原理包括以下幾個步驟:
- 模塊劃分:應(yīng)用程序被分割成多個獨立模塊季春,每個模塊通常作為一個單獨的APK存在。
- 動態(tài)加載:通過自定義類加載器或使用第三方框架(如DexClassLoader)來加載并實例化外部APK中的類
- 資源隔離:由于每個APK可能有自己的資源文件消返,需要通過Hook機(jī)制來實現(xiàn)資源隔離與管理
- 上下文運(yùn)行環(huán)境:在調(diào)用外部APK中代碼時载弄,需要正確處理上下文環(huán)境以確保功能正常運(yùn)行。
總之撵颊,Android插件化利用動態(tài)加載技術(shù)將應(yīng)用程序切割成多個獨立安裝運(yùn)行的組件侦锯,并借助合適框架管理各種資源及上下文環(huán)境等信息。
組件化
組件化是一種將應(yīng)用程序按照功能模塊拆分秦驯,并以組件的方式進(jìn)行開發(fā)尺碰、管理和復(fù)用的技術(shù)。其主要原理包括以下幾個步驟:
- 功能模塊劃分:將應(yīng)用程序按照業(yè)務(wù)邏輯或功能劃分成多個獨立組件译隘。
- 通信機(jī)制:使用合適的通信機(jī)制(如Intent亲桥、事件總線等)來實現(xiàn)不同組件之間的數(shù)據(jù)傳遞與交互。
- 解耦:通過接口定義和依賴注入等方式固耘,使得各個組件之間相互解耦题篷,提高代碼重用性與維護(hù)性。
Android組件化旨在實現(xiàn)模塊化開發(fā)厅目,讓不同團(tuán)隊可以并行開發(fā)不同模塊番枚,并且能夠靈活地替換、新增或刪除某些功能损敷。
熱修復(fù)
熱修復(fù)是一種在應(yīng)用程序運(yùn)行時對已發(fā)布版本進(jìn)行動態(tài)修復(fù)bug或更新功能的技術(shù)葫笼。其主要原理包括以下幾個步驟:
補(bǔ)丁生成與發(fā)布:根據(jù)需要修復(fù)或更新的問題,生成補(bǔ)丁文件并推送到客戶端設(shè)備上拗馒。
補(bǔ)丁加載與應(yīng)用:當(dāng)檢測到有新補(bǔ)丁時路星,通過自定義類加載器等機(jī)制將補(bǔ)丁文件加載到應(yīng)用程序中
動態(tài)修復(fù)/更新:在運(yùn)行時,通過替換诱桂、插入或修改現(xiàn)有代碼來解決問題洋丐,并確保新舊版本之間的兼容性與穩(wěn)定性。
總之挥等,Android熱修復(fù)技術(shù)利用補(bǔ)丁文件實現(xiàn)對已發(fā)布版本進(jìn)行動態(tài)修復(fù)和更新友绝。它可以快速響應(yīng)問題并部署解決方案,而無須重新發(fā)布整個應(yīng)用程序肝劲。
二迁客、Tinker熱修復(fù)的原理
熱修復(fù)的方案有很多種惭墓,其中原理也各不相同扔亥。目前開源的比較有名的有阿里的AndFix趁尼、美團(tuán)的Robust舷夺、qq的QZone以及Tinker等催蝗。
1切威、Tinker的優(yōu)點
- 支持類替換、so替換丙号,資源替換是采用類似Instant-run的方案先朦。
- 補(bǔ)丁包較小,自研diff方案犬缨,下發(fā)的是差量包喳魏,包括的是變更的內(nèi)容。
- 采用全量Dex更新怀薛,不需要額外處理 CLASS_ISPREVERIFIED 問題刺彩。
2、Tinker熱修復(fù)的流程
(1)Tinker將新舊dex做了diff(差分算法)枝恋,得到patch.dex
(2)然后將patch.dex下發(fā)到客戶端创倔,客戶端將patch.dex與舊dex的classes.dex做合并,生成新的classes.dex
(3)在運(yùn)行時通過反射將合成后的全量dex插入到dex elements前面(放在Element數(shù)組的第一個元素)焚碌,完成修復(fù)畦攘。(餓了么的Amigo則是將補(bǔ)丁包中每個dex對應(yīng)的Element取出來,之后組成新的Element數(shù)組十电,在運(yùn)行時通過反射用新的Element數(shù)組替換掉現(xiàn)有的Element數(shù)組)知押。
在ClassLoader的加載過程中,其中一個環(huán)節(jié)就是調(diào)用DexPathList的findClass的方法鹃骂,如下所示台盯,libcore/dalvik/src/main/java/dalvik/system/DexPathList.java
public Class<?> findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {//1
Class<?> clazz = element.findClass(name, definingContext, suppressed);//2
if (clazz != null) {
return clazz;
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
- Element內(nèi)部封裝了DexFile,DexFile用于加載dex文件畏线,因此每個dex文件對應(yīng)一個Element爷恳。多個Element組成了有序的Element數(shù)組dexElements。
-
當(dāng)要查找類時象踊,會在注釋1處遍歷Element數(shù)組dexElements(相當(dāng)于遍歷dex文件數(shù)組)温亲,注釋2處調(diào)用Element的findClass方法,其方法內(nèi)部會調(diào)用DexFile的loadClassBinaryName方法查找類杯矩。如果在Element中(dex文件)找到了該類就返回栈虚,如果沒有找到就接著在下一個Element中進(jìn)行查找。
根據(jù)上面的查找流程史隆,我們將有bug的類Key.class進(jìn)行修改魂务,再將Key.class打包成包含dex的補(bǔ)丁包Patch.jar,通過反射放在Element數(shù)組dexElements的第一個元素,這樣會首先找到Patch.dex中的Key.class去替換之前存在bug的Key.class,排在數(shù)組后面的dex文件中的存在bug的Key.class根據(jù)ClassLoader的雙親委派模式就不會被加載,這就是類的加載方案粘姜,如下圖所示:
類加載方案需要重啟App后讓ClassLoader重新加載新的類鬓照,為什么要重啟呢?這是因為類是無法被卸載的孤紧,因此想要重新加載類就需要重啟App豺裆,因此采用類加載方案的熱修復(fù)框架是不能即時生效的。
3号显、65536限制與LinearAlloc限制
類的加載方案基于Dex分包方案臭猜,什么是Dex分包方案?這個得先從65536限制和LinearAlloc限制說起押蚤。
65536限制
隨著應(yīng)用功能越來越復(fù)雜蔑歌,代碼量不斷增大,引入的庫也越來越多揽碘,可能會在編譯時提示如下異常:
com.android.dex.DexIndexOverflowException:method ID not in [0,0xffff]:65536
這說明應(yīng)用中引用的方法數(shù)超過了最大數(shù)65536個次屠。產(chǎn)生這個問題的原因就是系統(tǒng)的65536限制,65536限制的主要原因是DVM Bytecode的限制雳刺。DVM指令集的方法調(diào)用指令 invoke-kind索引為16bits劫灶,最多能引用65536。
LinearAlloc限制
在安裝應(yīng)用時可能會提示INSTALL_FAILED_DEXOPT,產(chǎn)生的原因就是LinearAlloc限制煞烫,DVM中的LinearAlloc是一個固定的緩存區(qū)浑此,當(dāng)方法數(shù)超出了緩存區(qū)的大小時會報錯。
為了解決65536限制和LinearAlloc限制滞详,從而產(chǎn)生了Dex分包方案凛俱。Dex分包方案主要做的是在打包時將應(yīng)用代碼分成多個Dex,將應(yīng)用啟動時必須用到的類和這些類的直接引用類放到主Dex中料饥,其他代碼放到次Dex中蒲犬。當(dāng)應(yīng)用啟動時先加載主Dex,等到應(yīng)用啟動后再動態(tài)地加載次Dex岸啡,從而緩解了主Dex的65536限制和LinearAlloc限制原叮。
Dex分包方案主要有兩種,分別是Google官方方案巡蘸、Dex自動拆包和動態(tài)加載方案奋隶。
4、PathClassLoader和DexClassLoader
Android系統(tǒng)中又兩個應(yīng)用程序類加載器悦荒,它們分別是PathClassLoader和DexClassLoader唯欣,它們都是繼承于BaseDexClassLoader的,而BaseDexClassLoader繼承ClassLoader搬味。
BaseDexClassLoader類加載:
- 作為ClassLoader的子類境氢,重寫了父類的findClass方法:
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
//在自己的成員變量DexPathList中尋找蟀拷,找不到拋異常
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;
}
- DexPathList的findClass方法:
public Class findClass(String name, List<Throwable> suppressed) {
//循環(huán)遍歷成員變量dexElements,調(diào)用DexFile.loadClassBinaryName加載class
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;
}
PathClassLoader和DexClassLoader的區(qū)別
- 路徑查找方式:
PathClassLoader使用系統(tǒng)默認(rèn)的路徑查找機(jī)制萍聊。它會根據(jù)包名去已安裝的APK文件或系統(tǒng)庫目錄下查找并加載類问芬;而DexClassLoader可以指定自定義的dex文件路徑來進(jìn)行類的查找和加載。這使得DexClassLoader能夠靈活地從外部存儲設(shè)備(如SD卡)或其他位置動態(tài)地加載已打包成dex格式的apk或jar文件寿桨。 - 加載資源文件
PathClassLoader除了加載類此衅,還可以通過getResource()方法來獲取APK中的資源文件;而DexClassLoader無法直接使用getResource()方法來獲取APK中的資源文件牛隅,因為它只負(fù)責(zé)加載類而不處理資源文件炕柔。但是可以通過自定義解析代碼實現(xiàn)對資源的訪問酌泰。 - 類共享與隔離性
所有使用相同PathClassLoader實例創(chuàng)建出來的對象都將共享同一個虛擬內(nèi)存空間媒佣,即處于同一個”命名空間“下。這意味著不同模塊之間可能存在命名沖突等問題陵刹;而每個DexClassLoader實例都會有獨立的虛擬機(jī)內(nèi)存空間默伍,”命名空間“彼此隔離,避免了命名沖突問題衰琐。
總結(jié)起來:
- PathClassLoader是系統(tǒng)默認(rèn)的應(yīng)用程序類加載器也糊,使用系統(tǒng)默認(rèn)的路徑查找機(jī)制,在加載類和資源時比較方便羡宙。
- DexClassLoader可以自定義dex文件路徑進(jìn)行靈活的類加載狸剃,但無法直接訪問APK中的資源文件。
4狗热、核心原理
PackageManagerService拿到apk钞馁,然后從后臺下載修復(fù)的patch.dex包。注意匿刮,這里我們可能有多個dex文件需要更新
public static void loadFixedDex(Context context) {
if (context == null) return;
// Dex文件目錄(私有目錄中僧凰,存在之前已經(jīng)復(fù)制過來的修復(fù)包)
File fileDir = context.getDir(Constants.DEX_DIR, Context.MODE_PRIVATE);
File[] listFiles = fileDir.listFiles();
// 遍歷私有目錄中所有的文件
for (File file : listFiles) {
// 找到修復(fù)包,加入到集合
if (file.getName().endsWith(Constants.DEX_SUFFIX) && !"classes.dex".equals(file.getName())) {
loadedDex.add(file);
}
}
// 模擬類加載器
createDexClassLoader(context, fileDir);
}
這個方法的作用是找到dex文件存在的位置并遍歷dex文件然后保存在一個集合中熟丸。然后調(diào)用createDexClassLoader方法训措。
private static void createDexClassLoader(Context context, File fileDir) {
// 創(chuàng)建臨時的解壓目錄(先解壓到該目錄,再加載java)
String optimizedDir = fileDir.getAbsolutePath() + File.separator + "opt_dex";
// 不存在就創(chuàng)建
File fopt = new File(optimizedDir);
if (!fopt.exists()) {
// 創(chuàng)建多級目錄
fopt.mkdirs();
}
for (File dex : loadedDex) {
// 每遍歷一個要修復(fù)的dex文件光羞,就需要插樁一次
DexClassLoader classLoader = new DexClassLoader(dex.getAbsolutePath(),
optimizedDir, null, context.getClassLoader());
hotfix(classLoader, context);
}
}
private static void hotfix(DexClassLoader classLoader, Context context) {
// 獲取系統(tǒng)PathClassLoader類加載器
PathClassLoader pathLoader = (PathClassLoader) context.getClassLoader();
try {
// 獲取自有的dexElements數(shù)組對象
Object myDexElements = ReflectUtils.getDexElements(ReflectUtils.getPathList(classLoader));
// 獲取系統(tǒng)的dexElements數(shù)組對象
Object systemDexElements = ReflectUtils.getDexElements(ReflectUtils.getPathList(pathLoader));
// 合并成新的dexElements數(shù)組對象
Object dexElements = ArrayUtils.combineArray(myDexElements, systemDexElements);
// 通過反射再去獲取 系統(tǒng)的pathList對象
Object systemPathList = ReflectUtils.getPathList(pathLoader);
// 重新賦值給系統(tǒng)的pathList屬性 --- 修改了pathList中的dexElements數(shù)組對象
ReflectUtils.setField(systemPathList, systemPathList.getClass(), dexElements);
} catch (Exception e) {
e.printStackTrace();
}
}
public class ReflectUtils {
/**
* 通過反射獲取某對象绩鸣,并設(shè)置私有可訪問
*
* @param obj 該屬性所屬類的對象
* @param clazz 該屬性所屬類
* @param field 屬性名
* @return 該屬性對象
*/
private static Object getField(Object obj, Class<?> clazz, String field)
throws NoSuchFieldException, IllegalAccessException, IllegalArgumentException {
Field localField = clazz.getDeclaredField(field);
localField.setAccessible(true);
return localField.get(obj);
}
/**
* 給某屬性賦值,并設(shè)置私有可訪問
*
* @param obj 該屬性所屬類的對象
* @param clazz 該屬性所屬類
* @param value 值
*/
public static void setField(Object obj, Class<?> clazz, Object value)
throws NoSuchFieldException, IllegalAccessException, IllegalArgumentException {
Field localField = clazz.getDeclaredField("dexElements");
localField.setAccessible(true);
localField.set(obj, value);
}
/**
* 通過反射獲取BaseDexClassLoader對象中的PathList對象
*
* @param baseDexClassLoader BaseDexClassLoader對象
* @return PathList對象
*/
public static Object getPathList(Object baseDexClassLoader)
throws NoSuchFieldException, IllegalAccessException, IllegalArgumentException, ClassNotFoundException {
return getField(baseDexClassLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
}
/**
* 通過反射獲取BaseDexClassLoader對象中的PathList對象纱兑,再獲取dexElements對象
*
* @param paramObject PathList對象
* @return dexElements對象
*/
public static Object getDexElements(Object paramObject)
throws NoSuchFieldException, IllegalAccessException, IllegalArgumentException {
return getField(paramObject, paramObject.getClass(), "dexElements");
}
}
參考:http://www.reibang.com/p/e6c4eedd83ab和http://www.reibang.com/p/6e412b0115f1