這是我參與更文挑戰的第1天,活動詳情查看: 更文挑戰java
首先,咱們須要持有如下幾個問題:android
上面一共有7個問題,若是是新同窗的話,後面兩條可能不會很瞭解,建議自行補課學習。因而最基本的5個問題,咱們必須明白,這是咱們每一個開發者學習一個新知識的基本須要作到的。git
其實簡單來講,熱修復就是一種動態加載技術,好比你線上某個產品此時出現了bug:shell
傳統流程:debug->測試->發佈新版 ->用戶安裝(各平臺審覈時間不一,並且用戶須要手動下載或者更新)數組
集成熱修復狀況下:dubug->測試->推送補丁->自動下載補丁修復 (用戶不知狀況,自動下載補丁並修復)緩存
對比下來,咱們不難發現,傳統流程存在這幾大弊端:服務器
上面三個緣由中,咱們主要來談一下 Instant Run:markdown
Android Studio2.0時,新增了一個 Instant Run的功能,而各大廠的熱修復方案,在代碼,資源等方面的實現都是很大程度上參考了Instant Run的代碼。因此能夠說 Instant Run 是推動Android 熱修復的主因。cookie
那 Instant Run內部是如何作到這一點呢?app
- 構建一個新的 AssetManager(資源管理框架),並經過反射調用這個 addAssetPath,把這個完整的新資源加入到 AssetManager中,這樣就獲得了一個含有全部新資源的 AssetManager.
- 找到全部以前引用到原有AssetManager的地方,經過反射,把引用出替換爲新的AssetManager.
參考自 <深刻探索Android熱修復技術原理>
咱們都知道熱修復都至關於動態加載,那麼動態加載到底動態在哪裏了呢。
說到這個就躲不過一個關鍵點 ClassLoader(類加載器) ,因此咱們先從Java開始。
咱們都知道Java的類加載器有四種,分別爲:
類加載過程以下:
過程: 加載-鏈接(驗證-準備-解析)-初始化
加載
將類的信息(字節碼)從文件中獲取並載入到JVM的內存中
鏈接
驗證:檢查讀入的結構是否符合JVM規範
準備:分配一個結構來存儲類的信息
解析:將類的常量池中的全部引用改變成直接引用
初始化
執行靜態初始化程序,把靜態變量初始化成指定的值
其中用到的三個主要機制:
- 雙親委託機制
- 全盤負責機制
- 緩存機制
其實後面的兩個機制都是主要從雙親委託機制延續而來。詳細的Java類加載請參考個人另外一篇博客
在說明了Java 的ClassLoader以後,咱們接下來開始Android的ClassLoader,不一樣於Java的是,Java中的ClassLoader能夠加載 jar 文件和 Class文件,而Android中加載的是Dex文件,這就須要從新設計相關的ClassLoader類。因此Android 的ClassLoader 咱們會說的詳細一點
在這裏,順便提一下,這裏貼的代碼版本是Android 9.0,在8.0之後,PathClassLoader和DexClassLoader並無什麼區別,由於惟一的一個區別參數 optimizedDirectory已經被廢棄。
首先是 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
}
//找到根加載器依然爲null,只能本身加載了
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
return c;
}
複製代碼
這裏有個問題,JVM雙親委託機制能夠被打破嗎?先保留疑問。
咱們主要去看他的 findClass方法
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
複製代碼
這個方法是一個null實現,也就是須要咱們開發者本身去作。
從上面基礎咱們知道,在Android中,是有 PathClassLoader和 DexClassLoader,而它們又都繼承與 BaseDexClassLoader,而這個BaseDexClassLoader又繼承與 ClassLoader,並將findClass方法交給子類本身實現,因此咱們從它的兩個子類 PathClassLoader和 DexClassLoader入手,看看它們是怎麼處理的。
這裏礙於Android Studio沒法查看相關具體實現源碼,因此咱們從源碼網站上查詢:
public class PathClassLoader extends BaseDexClassLoader {
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
// dexPath: 須要加載的文件列表,文件能夠是包含了 classes.dex 的 JAR/APK/ZIP,也能夠直接使用 classes.dex 文件,多個文件用 「:」 分割
// librarySearchPath: 存放須要加載的 native 庫的目錄
// parent: 父 ClassLoader
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
}
複製代碼
由註釋看能夠發現PathClassLoader被用來加載本地文件系統上的文件或目錄,由於它調用的 BaseDexClassLoader的第二個參數爲null,即未傳入優化後的Dex文件。
注意:Android 8.0以後,BaseClassLoader第二個參數爲(optimizedDirectory)爲null,因此DexClassLoader與PathClassLoader並沒有區別
public class DexClassLoader extends BaseDexClassLoader {
// dexPath: 須要加載的文件列表,文件能夠是包含了 classes.dex 的 JAR/APK/ZIP,也能夠直接使用 classes.dex 文件,多個文件用 「:」 分割
// optimizedDirectory: 存放優化後的 dex,能夠爲空
// librarySearchPath: 存放須要加載的 native 庫的目錄
// parent: 父 ClassLoader
public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
}
複製代碼
DexClassLoader用來加載jar、apk,其實還包括zip文件或者直接加載dex文件,它能夠被用來執行未安裝的代碼或者未被應用加載過的代碼,也就是咱們修復過的代碼。
注意:Android 8.0以後,BaseClassLoader第二個參數爲(optimizedDirectory)爲null,因此DexClassLoader與PathClassLoader並沒有區別
從上面咱們能夠看到,它們都繼承於BaseDexClassLoader,而且它們真正的實現行爲都是調用的父類方法,因此咱們來看一下BaseDexClassLoader.
public class BaseDexClassLoader extends ClassLoader {
private static volatile Reporter reporter = null;
//核心關注點
private final DexPathList pathList;
BaseDexClassLoader 構造函數有四個參數,含義以下:
// dexPath: 須要加載的文件列表,文件能夠是包含了 classes.dex 的 JAR/APK/ZIP,也能夠直接使用 classes.dex 文件,多個文件用 「:」 分割
// optimizedDirectory: 存放優化後的 dex,能夠爲空
// librarySearchPath: 存放須要加載的 native 庫的目錄
// parent: 父 ClassLoader
public BaseDexClassLoader(String dexPath, File optimizedDirectory, String librarySearchPath, ClassLoader parent) {
//classloader,dex路徑,目錄列表,內部文件夾
this(dexPath, optimizedDirectory, librarySearchPath, parent, false);
}
public BaseDexClassLoader(String dexPath, File optimizedDirectory, String librarySearchPath, ClassLoader parent, boolean isTrusted) {
super(parent);
this.pathList = new DexPathList(this, dexPath, librarySearchPath, null, isTrusted);
if (reporter != null) {
reportClassLoaderChain();
}
}
...
public BaseDexClassLoader(ByteBuffer[] dexFiles, ClassLoader parent) {
// TODO We should support giving this a library search path maybe.
super(parent);
this.pathList = new DexPathList(this, dexFiles);
}
//核心方法
@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;
}
...
}
複製代碼
從上面咱們能夠發現,BaseDexClassLoader其實也不是主要處理的類,因此咱們繼續去查找 DexPathList.
final class DexPathList {
//文件後綴
private static final String DEX_SUFFIX = ".dex";
private static final String zipSeparator = "!/";
** class definition context */ private final ClassLoader definingContext;
//內部類 Element
private Element[] dexElements;
public DexPathList(ClassLoader definingContext, String dexPath, String librarySearchPath, File optimizedDirectory) {
this(definingContext, dexPath, librarySearchPath, optimizedDirectory, false);
}
DexPathList(ClassLoader definingContext, String dexPath,
String librarySearchPath, File optimizedDirectory, boolean isTrusted) {
if (definingContext == null) {
throw new NullPointerException("definingContext == null");
}
if (dexPath == null) {
throw new NullPointerException("dexPath == null");
}
if (optimizedDirectory != null) {
if (!optimizedDirectory.exists()) {
throw new IllegalArgumentException(
"optimizedDirectory doesn't exist: "
+ optimizedDirectory);
}
if (!(optimizedDirectory.canRead()
&& optimizedDirectory.canWrite())) {
throw new IllegalArgumentException(
"optimizedDirectory not readable/writable: "
+ optimizedDirectory);
}
}
this.definingContext = definingContext;
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
// save dexPath for BaseDexClassLoader
//咱們關注這個 makeDexElements 方法
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
suppressedExceptions, definingContext, isTrusted);
this.nativeLibraryDirectories = splitPaths(librarySearchPath, false);
this.systemNativeLibraryDirectories =
splitPaths(System.getProperty("java.library.path"), true);
List<File> allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories);
allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories);
this.nativeLibraryPathElements = makePathElements(allNativeLibraryDirectories);
if (suppressedExceptions.size() > 0) {
this.dexElementsSuppressedExceptions =
suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
} else {
dexElementsSuppressedExceptions = null;
}
}
static class Element {
//dex文件爲null時表示 jar/dex.jar文件
private final File path;
//android虛擬機文件在Android中的一個具體實現
private final DexFile dexFile;
private ClassPathURLStreamHandler urlHandler;
private boolean initialized;
/** * Element encapsulates a dex file. This may be a plain dex file (in which case dexZipPath * should be null), or a jar (in which case dexZipPath should denote the zip file). */
public Element(DexFile dexFile, File dexZipPath) {
this.dexFile = dexFile;
this.path = dexZipPath;
}
public Element(DexFile dexFile) {
this.dexFile = dexFile;
this.path = null;
}
public Element(File path) {
this.path = path;
this.dexFile = null;
}
public Class<?> findClass(String name, ClassLoader definingContext,
List<Throwable> suppressed) {
//核心點,DexFile
return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed)
: null;
}
/** * Constructor for a bit of backwards compatibility. Some apps use reflection into * internal APIs. Warn, and emulate old behavior if we can. See b/33399341. * * @deprecated The Element class has been split. Use new Element constructors for * classes and resources, and NativeLibraryElement for the library * search path. */
@Deprecated
public Element(File dir, boolean isDirectory, File zip, DexFile dexFile) {
System.err.println("Warning: Using deprecated Element constructor. Do not use internal"
+ " APIs, this constructor will be removed in the future.");
if (dir != null && (zip != null || dexFile != null)) {
throw new IllegalArgumentException("Using dir and zip|dexFile no longer"
+ " supported.");
}
if (isDirectory && (zip != null || dexFile != null)) {
throw new IllegalArgumentException("Unsupported argument combination.");
}
if (dir != null) {
this.path = dir;
this.dexFile = null;
} else {
this.path = zip;
this.dexFile = dexFile;
}
}
...
}
...
//主要做用就是將 咱們指定路徑中全部文件轉化爲DexFile,同時存到Eelement數組中
//爲何要這樣作?目的就是爲了讓findClass去實現
private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
List<IOException> suppressedExceptions, ClassLoader loader, boolean isTrusted) {
Element[] elements = new Element[files.size()];
int elementsPos = 0;
//遍歷全部文件
for (File file : files) {
if (file.isDirectory()) {
//若是存在文件夾,查找文件夾內部查詢
elements[elementsPos++] = new Element(file);
//若是是文件
} else if (file.isFile()) {
String name = file.getName();
DexFile dex = null;
//判斷是不是dex文件
if (name.endsWith(DEX_SUFFIX)) {
// Raw dex file (not inside a zip/jar).
try {
//建立一個DexFile
dex = loadDexFile(file, optimizedDirectory, loader, elements);
if (dex != null) {
elements[elementsPos++] = new Element(dex, null);
}
} catch (IOException suppressed) {
System.logE("Unable to load dex file: " + file, suppressed);
suppressedExceptions.add(suppressed);
}
} else {
try {
dex = loadDexFile(file, optimizedDirectory, loader, elements);
} catch (IOException suppressed) {
/* * IOException might get thrown "legitimately" by the DexFile constructor if * the zip file turns out to be resource-only (that is, no classes.dex file * in it). * Let dex == null and hang on to the exception to add to the tea-leaves for * when findClass returns null. */
suppressedExceptions.add(suppressed);
}
if (dex == null) {
elements[elementsPos++] = new Element(file);
} else {
elements[elementsPos++] = new Element(dex, file);
}
}
if (dex != null && isTrusted) {
dex.setTrusted();
}
} else {
System.logW("ClassLoader referenced unknown path: " + file);
}
}
if (elementsPos != elements.length) {
elements = Arrays.copyOf(elements, elementsPos);
}
return elements;
}
---
private static DexFile loadDexFile(File file, File optimizedDirectory, ClassLoader loader, Element[] elements)throws IOException {
//判斷可複製文件夾是否爲null
if (optimizedDirectory == null) {
return new DexFile(file, loader, elements);
} else {
//若是不爲null,則進行解壓後再建立
String optimizedPath = optimizedPathFor(file, optimizedDirectory);
return DexFile.loadDex(file.getPath(), optimizedPath, 0, loader, elements);
}
}
-----
public Class<?> findClass(String name, List<Throwable> suppressed) {
//遍歷初始化好的DexFile數組,並由Element調用 findClass方法去生成
for (Element element : dexElements) {
//
Class<?> clazz = element.findClass(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
複製代碼
上面的代碼有點複雜,我摘取了其中一部分咱們須要關注的點,便於咱們進行分析:
在BaseDexClassLoader中,咱們發現最終加載類的是由 DexPathList 來進行的,因此咱們進入了 DexPathList 這個類中,咱們能夠發現 在初始化的時候,有一個關鍵方法須要咱們注意 makeDexElements。而這個方法的主要做用就是將 咱們指定路徑中全部文件轉化爲 DexFile ,同時存到 Eelement 數組中。
而最開始調用的 DexPathList中的findClass() 反而是由Element 調用的 findClass方法,而Emement的findClass方法中實際上又是 DexFile 調用的 loadClassBinaryName 方法,因此帶着這個疑問,咱們進入 DexFile這個類一查究竟。
public final class DexFile {
*
If close is called, mCookie becomes null but the internal cookie is preserved if the close
failed so that we can free resources in the finalizer.
/
@ReachabilitySensitive
private Object mCookie;
private Object mInternalCookie;
private final String mFileName;
...
DxFile(String fileName, ClassLoader loader, DexPathList.Element[] elements) throws IOException {
mCookie = openDexFile(fileName, null, 0, loader, elements);
mInternalCookie = mCookie;
mFileName = fileName;
//System.out.println("DEX FILE cookie is " + mCookie + " fileName=" + fileName);
}
//關注點在這裏
public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
return defineClass(name, loader, mCookie, this, suppressed);
}
//
private static Class defineClass(String name, ClassLoader loader, Object cookie, DexFile dexFile, List<Throwable> suppressed) {
Class result = null;
try {
//這裏調用了一個 JNI層方法
result = defineClassNative(name, loader, cookie, dexFile);
} catch (NoClassDefFoundError e) {
if (suppressed != null) {
suppressed.add(e);
}
} catch (ClassNotFoundException e) {
if (suppressed != null) {
suppressed.add(e);
}
}
return result;
}
private static native Class defineClassNative(String name, ClassLoader loader, Object cookie, DexFile dexFile) throws ClassNotFoundException, NoClassDefFoundError;
複製代碼
咱們從 loadClassBinaryName 方法中發現,調用了 defineClass 方法,最終又調用了 defineClassNative 方法,而 defineClassNative 方法是一個JNI層的方法,因此咱們沒法得知具體如何。可是咱們思考一下,從開始的 BaseDexClassLoader一直到如今的 DexFile,咱們一直從入口找到了最底下,不難猜想,這個 defineClassNative 方法內部就是 C/C++幫助咱們以字節碼或者別的生成咱們須要的 dex文件,這也是最難的地方所在。
最後咱們再用一張圖來總結一下Android 中類加載的過程。
在瞭解完上面的知識以後,咱們來總結一下,Android中熱修復的原理?
Android中既然已經有了DexClassLoader和 PathClassLoader,那麼我在加載過程當中直接替換我本身的Dex文件不就能夠了,也就是先加載我本身的Dex文件不就好了,這樣不就實現了熱修復。
抱着這個問題,如何選用一個最合適的框架,是咱們Android開發者必需要考慮的,下面咱們就分析一下各方案的差異。
目前市場上的熱修復框架不少,從阿里熱修復網站找了一個圖來對比一下:
平臺 | Sophix | AndFix | Tinker | Qzone | Robust |
---|---|---|---|---|---|
即時生效 | yes | yes | no | no | yes |
性能損耗 | 較小 | 較小 | 較大 | 較大 | 較小 |
侵入式打包 | 無侵入式打包 | 無侵入式打包 | 依賴侵入式打包 | 依賴侵入式打包 | 依賴侵入式打包 |
Rom體積 | 較小 | 較小 | 較大 | 較小 | 較小 |
接入複雜度 | 傻瓜式接入 | 比較簡單 | 複雜 | 比較簡單 | 複雜 |
補丁包大小 | 較小 | 較小 | 較小 | 較大 | 通常 |
全平臺支持 | yes | yes | yes | yes | yes |
類替換 | yes | yes | yes | yes | no |
so替換 | yes | no | yes | no | no |
資源替換 | yes | no | yes | yes | no |
簡單劃分就是3大巨頭,阿里,騰訊,美團。並非誰支持的功能多就用誰,在接入方面咱們須要綜合考慮。
詳細的技術對比請參考 Android熱修復技術選型——三大流派解析
以我我的的體驗來講吧:目前體驗了Tinker和 Sophix
Tinker
Tinker的集成有點麻煩,我我的以爲挺簡單,並且補丁管理系統 TinkerPatch是收費的(有免費額度),補丁下發慢,大概須要5分鐘的等待時間。
Tinker有一個免費版後臺,Bugly,補丁管理是免費的,熱修復用的Tinker,集成很那啥。。。em,建議多讀官網教程看視頻,由於有補丁上傳監測,下發一個補丁須要5-10分鐘等待生效,撤回補丁須要10分鐘左右生效,並且一次可能不會生效,後臺觀察日誌須要屢次才能夠實現補丁撤回。(測試設備:小米5s Plus,Android 8.0)
最後總結:
優勢:免費,簡單
缺點:集成麻煩,出現問題沒法第一時間獲得解決方案,畢竟免費的理解一下
性能方法:須要冷啓動以後纔會生效
Sophix
官網教程詳細,徹底傻瓜式,響應快,出現問題,解決效率高,畢竟花了錢的。
性能方面:冷啓動+即時響應(有條件),
有點:功能最多,支持版本最多,解決問題快
缺點:付費
別的框架沒有體驗,也就不妄自評價了。關於以上方案的實現原理,你們能夠點擊Android熱修復技術選型——三大流派解析,或者百度搜索。簡單瞭解並不困難。
有了熱修復,咱們就能夠隨心所欲了嗎?
開始講騷話:
並非,熱修復受限於各類機型設備,並且也有失敗的可能性,因此咱們開發者,對於補丁包一樣也要抱有敬畏之心。
對於熱修復一樣也因爲嚴格的過程,可是咱們平常開發至少要保證如下幾點:
debug-> 打補丁包->開發設備測試->灰度下發(條件下發)->全量下發
下面針對我開發中遇到的問題,給出解決方案。
多渠道打包使用 美團 的一鍵打包方案。補丁包的話,其實並不會影響,由於補丁包通常改動的代碼相同,但前提是須要保證咱們每一個渠道基準包沒問題。若是改動代碼有區別,那就須要針對這個渠道單獨打補了。
Android開發通常集成了 Jenkins 或者別的自動化打包工具,咱們通常基準包都在 app/build/bakApk目錄下,因此咱們能夠經過編寫 shell 命令,在jenkins中打包時,將生成的基準包移動到一個特定的文件夾便可。tinker,Sophix都是支持服務器後臺的,因此咱們也能夠經過自動化構建工具上傳補丁包,若是相應的熱修復框架不支持服務器管理的話,那麼能夠將補丁包上傳的指定的文件夾,而後咱們app打開時,訪問咱們的服務器接口下拉最新的補丁包,而後在service中合成。不過 **Tinker(bugly) **, Sophix 都是支持後臺管理,因此具體使用那種方案咱們自行選擇。
關於熱修復的到這裏就基本寫完了,散散落落竟然寫了這麼多,其實難的不是熱修復,而是Android中類加載的過程及一些基礎相關知識,理解了這些,咱們才能真正明白那些優秀的框架究竟是怎樣去修復的。
若是本文有幫到你的地方,不勝榮幸。若是有什麼地方有錯誤或者疑問,也歡迎你們提出。