如需轉載請評論或簡信,并注明出處,未經允許不得轉載
github地址:https://github.com/Geekholt/HotFix
目錄
前言
bug一般是一個或多個class
出現了問題,在一個理想的狀態下,我們只需將修復好的這些個class
更新到用戶手機上的app中就可以修復這些bug了。要怎么才能動態更新這些class
呢?其實,不管是哪種熱修復方案,肯定是如下幾個步驟:
- 下發補丁(內含修復好的class)到用戶手機,即讓app從服務器上下載(網絡傳輸)
- app通過"某種方式",使補丁(
apk
、dex
、jar
等文件)中的class
被app調用(本地更新)
這里的"某種方式",對本篇而言,就是使用Android的類加載器,通過類加載器加載這些修復好的class
,覆蓋對應有問題的class,理論上就能修復bug了。
Java類加載機制(雙親委派模型)
在加載一個字節碼文件時,會詢問當前的classLoader
是否已經加載過此字節碼文件。如果加載過,則直接返回,不再重復加載。如果沒有加載過,則會詢問它的Parent是否已經加載過此字節碼文件,同樣的,如果已經加載過,就直接返回parent加載過的字節碼文件,而如果整個繼承線路上的classLoader
都沒有加載過,才由child類加載器(即,當前的子classLoader
)執行類的加載工作。
特點
如果一個類被classLoader
繼承線路上的任意一個加載過,那么在以后整個系統的生命周期中,這個類都不會再被加載,大大提高了類的加載效率。
作用
- 類加載的共享功能
一些Framework
層級的類一旦被頂層classLoader
加載過,會緩存到內存中,以后在任何地方用到,都不會去重新加載。
- 類加載的隔離功能
不同繼承線路上的classLoader
加載的類,肯定不是同一個類,這樣可以避免某些開發者自己去寫一些代碼冒充核心類庫,來訪問核心類庫中可見的成員變量。如java.lang.String
在應用程序啟動前就已經被系統加載好了,如果在一個應用中能夠簡單的用自定義的String類把系統中的String類替換掉的話,會有嚴重的安全問題。
驗證多個類是同一個類的成立條件:
- 相同的
className
- 相同的
packageName
- 被相同的
classLoader
加載
驗證雙親委派模型
找到ClassLoader
這個類中的loadClass()
方法,它調用的是另一個2個參數的重載loadClass()
方法。
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
找到最終這個真正的loadClass()
方法,下面便是該方法的源碼:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// First, check if the class has already been loaded
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;
}
可以看到,如前面所說,加載一個類時,會有如下3步:
- 檢查當前的
classLoader
是否已經加載琮這個class
,有則直接返回,沒有則進行第2步。 - 調用父
classLoader
的loadClass()
方法,檢查父classLoader
是否有加載過這個class
,有則直接返回,沒有就繼續檢查上上個父classLoader
,直到頂層classLoader
。 - 如果所有的父
classLoader
都沒有加載過這個class
,則最終由當前classLoader
調用findClass()
方法,去dex文件中找出并加載這個class
。
Android中的ClassLoader
Android跟java有很大的淵源,基于jvm的java應用是通過ClassLoader
來加載應用中的class的,Android對jvm優化過,使用的是dalvik虛擬機,且class文件會被打包進一個dex文件中,底層虛擬機有所不同,那么它們的類加載器當然也是會有所區別。
Android中最主要的類加載器有如下4個
-
BootClassLoader
:加載Android Framework層中的class字節碼文件(類似java的Bootstrap ClassLoader) -
PathClassLoader
:加載已經安裝到系統中的Apk的class
字節碼文件(類似java的App ClassLoader
) -
DexClassLoader
:加載制定目錄的class字節碼文件(類似java中的Custom ClassLoader
) -
BaseDexClassLoader
:PathClassLoader
和DexClassLoader
的父類
一個app一定會用到BootClassLoader、PathClassLoader這2個類加載器,可通過如下代碼進行驗證:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ClassLoader classLoader = getClassLoader();
if (classLoader != null) {
Log.e(TAG, "classLoader = " + classLoader);
while (classLoader.getParent() != null) {
classLoader = classLoader.getParent();
Log.e(TAG, "classLoader = " + classLoader);
}
}
}
上面代碼中可以通過上下文拿到當前類的類加載器(PathClassLoader
),然后通過getParent()得到父類加載器(BootClassLoader
),這是由于Android中的類加載器和java類加載器一樣使用的是雙親委派模型。
PathClassLoader與DexClassLoader的區別
一般的源碼在Android Studio中可以查到,但 PathClassLoader 和 DexClassLoader 的源碼是屬于系統級源碼,所以無法在Android Studio中直接查看。可以到androidxref.com這個網站上直接查看,下面會列出之后要分析的幾個類的源碼地址。
以下是Android 5.0中的部分源碼:
使用場景
先來介紹一下這兩種Classloader在使用場景上的區別
-
PathClassLoader
:只能加載已經安裝到Android系統中的apk文件(/data/app目錄),是Android默認使用的類加載器。 -
DexClassLoader
:可以加載任意目錄下的dex/jar/apk/zip文件,比PathClassLoader
更靈活,是實現熱修復的重點。
代碼差異
下面來看一下PathClassLoader
與DexClassLoader
的源碼的差別,都非常簡單
// PathClassLoader
public class PathClassLoader extends BaseDexClassLoader {
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
}
// DexClassLoader
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
}
}
通過比對,可以得出2個結論:
-
PathClassLoader
與DexClassLoader
都繼承于BaseDexClassLoader
。 -
PathClassLoader
與DexClassLoader
在構造函數中都調用了父類的構造函數,但DexClassLoader
多傳了一個optimizedDirectory
。
BaseDexClassLoader
通過觀察PathClassLoader
與DexClassLoader
的源碼我們就可以確定,真正有意義的處理邏輯肯定在BaseDexClassLoader
中,所以下面著重分析BaseDexClassLoader
源碼。
構造函數
先來看看BaseDexClassLoader
的構造函數都做了什么:
public class BaseDexClassLoader extends ClassLoader {
private final DexPathList pathList;
...
public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent){
super(parent);
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
...
}
-
dexPath
:要加載的程序文件(一般是dex文件,也可以是jar/apk/zip文件)所在目錄。 -
optimizedDirectory
:dex文件的輸出目錄(因為在加載jar/apk/zip等壓縮格式的程序文件時會解壓出其中的dex文件,該目錄就是專門用于存放這些被解壓出來的dex文件的)。 -
libraryPath
:加載程序文件時需要用到的庫路徑。 -
parent
:父加載器
tip:從一個完整App的角度來說,程序文件指定的就是apk包中的classes.dex
文件;但從熱修復的角度來看,程序文件指的是補丁。
因為PathClassLoader只會加載已安裝包中的dex文件,而DexClassLoader不僅僅可以加載dex文件,還可以加載jar、apk、zip文件中的dex。jar、apk、zip其實就是一些壓縮格式,要拿到壓縮包里面的dex文件就需要解壓,所以,DexClassLoader在調用父類構造函數時會指定一個解壓的目錄。
BaseDexClassLoader.findClass()
類加載器肯定會提供有一個方法來供外界找到它所加載到的class,該方法就是findClass()
,不過在PathClassLoader
和DexClassLoader
源碼中都沒有重寫父類的findClass()
方法,但它們的父類BaseDexClassLoader就有重寫findClass()
,所以來看看BaseDexClassLoader
的findClass()
方法都做了哪些操作,代碼如下:
private final DexPathList pathList;
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
// 實質是通過pathList的對象findClass()方法來獲取class
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;
}
可以看到,BaseDexClassLoader
的findClass()
方法實際上是通過DexPathList
的findClass()
方法來獲取class的,而這個DexPathList
對象恰好在之前的BaseDexClassLoader
構造函數中就已經被創建好了。所以,下面就來看看DexPathList
類中都做了什么。
Element集合—DexPathList
構造函數
private final Element[] dexElements;
public DexPathList(ClassLoader definingContext, String dexPath,
String libraryPath, File optimizedDirectory) {
...
this.definingContext = definingContext;
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,suppressedExceptions);
...
}
這個構造函數中,保存了當前的類加載器definingContext
,并調用了makeDexElements()
得到Element
集合。
通過對splitDexPath(dexPath)的源碼追溯,發現該方法的作用其實就是將dexPath目錄下的所有程序文件轉變成一個File集合。而且還發現,dexPath是一個用冒號(":")作為分隔符把多個程序文件目錄拼接起來的字符串(如:/data/dexdir1:/data/dexdir2:...)。
那接下來無疑是分析makeDexElements()
方法了,因為這部分代碼比較長,我就貼出關鍵代碼,并以注釋的方式進行分析:
private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory, ArrayList<IOException> suppressedExceptions) {
// 1.創建Element集合
ArrayList<Element> elements = new ArrayList<Element>();
// 2.遍歷所有dex文件(也可能是jar、apk或zip文件)
for (File file : files) {
ZipFile zip = null;
DexFile dex = null;
String name = file.getName();
...
// 如果是dex文件
if (name.endsWith(DEX_SUFFIX)) {
dex = loadDexFile(file, optimizedDirectory);
// 如果是apk、jar、zip文件(這部分在不同的Android版本中,處理方式有細微差別)
} else {
zip = file;
dex = loadDexFile(file, optimizedDirectory);
}
...
// 3.將dex文件或壓縮文件包裝成Element對象,并添加到Element集合中
if ((zip != null) || (dex != null)) {
elements.add(new Element(file, false, zip, dex));
}
}
// 4.將Element集合轉成Element數組返回
return elements.toArray(new Element[elements.size()]);
}
在這個方法中,看到了一些眉目,總體來說,DexPathList
的構造函數是將一個個的程序文件(可能是dex、apk、jar、zip)封裝成一個個Element
對象,最后添加到Element集合中。
其實,Android的類加載器(不管是PathClassLoader,還是DexClassLoader),它們最后只認dex文件,而loadDexFile()是加載dex文件的核心方法,可以從jar、apk、zip中提取出dex,但這里先不分析了,因為第1個目標已經完成,等到后面再來分析吧。
findClass()
再來看DexPathList
的findClass()
方法:
public Class findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
// 遍歷出一個dex文件
DexFile dex = element.dexFile;
if (dex != null) {
// 在dex文件中查找類名與name相同的類
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
結合DexPathList
的構造函數,其實DexPathList
的findClass()
方法很簡單,就只是對Element
數組進行遍歷,一旦找到類名與name相同的類時,就直接返回這個class
,找不到則返回null。
為什么是調用DexFile的loadClassBinaryName()方法來加載class?這是因為一個Element對象對應一個dex文件,而一個dex文件則包含多個class。也就是說Element數組中存放的是一個個的dex文件,而不是class文件。這可以從Element這個類的源碼和dex文件的內部結構看出。
總結
經過對PathClassLoader
、DexClassLoader
、BaseDexClassLoader
、DexPathList
的分析,我們知道,安卓的類加載器在加載一個類時會先從自身DexPathList對象中的Element數組中獲取(Element[] dexElements
)到對應的類,之后再加載。采用的是數組遍歷的方式,不過注意,遍歷出來的是一個個的dex文件。在for循環中,首先遍歷出來的是dex文件,然后再是從dex文件中獲取class,所以,我們只要讓修復好的class打包成一個dex文件,放于Element
數組的第一個元素,這樣就能保證獲取到的class是最新修復好的class了(當然,有bug的class也是存在的,不過是放在了Element
數組的最后一個元素中,所以沒有機會被拿到而已。