做程序開發,基礎很重要。同樣是擰螺絲人家擰出來的可以經久不壞,你擰出來的遇到點風浪就開始顫抖,可見基本功的重要性。再復雜的技術,也是由一個一個簡單的邏輯構成。先了解核心基礎,才能更好理解前沿高新技術。
- 先看效果{github Demo地址}:(https://github.com/18598925736/HotUpdateDemo)
- Demo使用方法
- Demo源碼概覽
- 熱修復核心技術
- 基礎知識預備
- hook思路
- TIPS
熱更新技術,不是新話題。目前最熱門的熱更新由兩種,一種是騰訊tinker為代表的 需重啟app的熱更新,一種是美團app為代表的instant Run,無需重啟app. 今天先探究 前者的核心原理。
先看效果[github Demo地址] :(https://github.com/18598925736/HotUpdateDemo)
假如說這是我們的app界面,這個界面有個bug,我們直接用一個 TextView來表示
然而,我們的開發人員發現了這個bug,但是產品已經上線。這時候,由于引起bug的 代碼,只有一行,
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceStata) {
super.onCreate(savedINstanceState);
srtContentView(R.layout.activity_main);
TextView textView = findViewById(R.id.tv);
Bug bug = new Bug():
String s = bug.getstr():
textView.setText(s):
}
}


這個時候,機智的程序員用最快的方式修復了這個bug,也只是改了一行代碼:
那么,產品已經在線上,怎么辦?我們通過后臺,向app推送了一個 fix.dex文件, 等這個文件下載完成,app提示用戶,發現新的更新,需要重啟app. 待用戶重啟,代碼修復 即會生效。無需發布新版本!
Demo使用方法
下載Demo代碼之后,會在assets下看到一個fix.dex文件
按照正常的邏輯,我們做bug修復一定是把fix.dex放到服務器上, app去服務器下載它,然后存放在app私有目錄,重啟app之后,fix.dex生效, 當加載到這個類的時候,就會去讀fix.dex中當時打包的已修復bug的類. 但是,我這里為了演示方便,直接放在assets,然后使用 項目中的 AssetsFileUtil類 用io流將它讀寫到 app私有目錄下.
演示方法:
起作用的是誰?就是這個fix.dex文件.

如上圖所示: 核心類其實就只有一個: ClassLoaderHookHelper ,它 就是 讓 fix.dex這個補丁發揮作用的 " 幕后大佬". 這個核心類:有3個方法,分別是在不同的系統版本上,來對源碼程序邏輯進行 hook,提高hook的兼容性. 
下面是完整 ClassLoaderHookHelper代碼 以及 使用它的 MyApp完整代碼 :
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Array;
import java.lang.reflect.Field
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
public class ClassLoaderHookHelper {
//23和19的差別,就是 makeXXXElements 方法名和參數要求不同
//后者是 makeDexElements(ArrayList<File> files, File optimizedDirectory,ArrayList<IOException> suppressedExceptions)
//前者是 makePathElements(List<File> files, File optimizedDirectory,List<IOException> suppressedExceptions)
public static void hookV23(ClassLoader classLoader,File outDexFilePath,File optimizedDirectory)throws IllegalAccessException, InvocationTargetException {
Field pathList =ReflectionUtil.getField(classLoader,"pathList");//1、獲DexPathList pathList 屬性
object dexpathListobj =pathList.get(classLoader);//2、獲DexPathList pathList對象
Field dexElementsField =ReflectionUtil.getField(dexPathListObj, "dexElements");//3、獲得DexPathList的dexElements屬性
Object[] oldElements =(Object[]) dexElementsField.get(dexPathListObj);//4、獲得pathList對象中 dexElements 的屬性值
...
}
}
其實 熱修復的核心技術,就一句話,
HookClassLoader,但是要深入了解它,需要相當多的基礎知識,下面列舉出必須要知道的一些東西。基礎知識預備
1.Dex文件是什么?
我們寫安卓,目前還是用 java比較多,就算是用 kotlin,它最終也是要轉換成 java來運行。 java文件,被編譯成 class之后,多個 class文件,會被打包成
classes.dex,被放到apk中,安卓設備拿到apk,去安裝解析( 預編譯balabala...),當我們運行 app時, app的程序邏輯全都是在classes.dex中。所以,dex文件是什么?一句話,dex文件是 android app的源代碼的最終打包
androidStudio 打包 apk的時候會生成 Dex,其實它使用的是 SDK的 dx命令,我們可以用 dx命令自己去打包想要打包的 class. 命令格式為:dx --dex --output=output.dex xxxx.class 將上面的output 和 xxxx換成你想要的文件名即可。
注:dx.bat在 安卓 SDK的目錄下:比如我d的`C:\XXXXX\AndroidStudioAbout\sdk1\build-tools\28.0.3\dx.bat
ClassLoader是什么?ClassLoader來自 jdk,翻譯為 :類加載器,用于將 class文件中的類,加載到內存中,生成 class對象。只有存在了 Class對象,我們才可以創建我們想要的對象。 android SDK繼承了JDK的 classLoader,創造出了新的 ClassLoader子類。下圖表示了 android9.0-28 所有的ClassLoader直接或者間接子類. 
比較多的是 BaseDexClassLoader, DexClassLoader , PathClassLoader, 其他這些,應該是谷歌大佬 創造出來新的 類加載器子類吧,還沒研究過。
注: 關于 DexClassLoader和 PathClassLoader ,網上資料有個誤區,應該不少人都認為, PathClassLoader 用于加載 app內部的 dex文件, DexClassLoader用于加載外部的 dex文件,但是其實只要看一眼 這兩個類的關系,就會發現,它們都是繼承自 BaseDexClassLoader,他們的構造函數內部都會去執行父類的構造函數。他們只有一個差別,那就是 PathClssLoader不用傳 optimizedDirectory這個參數,但是 DexClassLoader必須傳。這個參數的作用是,傳入一個 dex優化之后的存放目錄。而事實上,雖然 PathClassLoader不要求傳這個 optimizedDirectory,但是它實際上是給了一個默認值。emmmm............所以不要再認為 PathClassLoader不能加載外部的 dex了,它只是沒有讓你傳 optimizedDirectory而已。
另外: BootClassLoader用于加載 AndroidFramework層class文件( SDK中沒有這個BootClassLoader,也是很奇怪) PathClassLoader 是用于Android應用程序類的加載器,可以加載指定的 dex,以及 jar、 zip、 apk中的 classes.dex。 DexClassLoader 可以加載指定的 dex,以及 jar、 zip、 apk中的 classes.dex。
ClassLoader的雙親委托機制是什么?android里面 ClassLoader的作用,是將 dex文件中的類,加載到內存中,生成 Class對象,供我們使用 (舉個例子:我寫了一個 A類,app運行起來之后,當我需要new 一個 A, ClassLoader首先會幫我查找 A的 Class對象是否存在,如果存在,就直接給我 Class對象,讓我拿去 new A,如果不存在,就會出創建這個 A的 Class對象。) 這個查找的過程,就遵循 雙親委托機制。一句話解釋 雙親委托機制:某個 類加載器在加載某個 類的時候,首先會將 這件事委托給 parent類加載器,依次遞歸,如果 parent類加載器可以完成加載,就會直接返回 Class對象。如果 parent找不到或者沒有父了,就會 自己加載。
下圖是 安卓源碼 ClassLoader.java:
紅字注解,很容易讀懂 ClassLoader去 load一個 class的過程.
OK,現在可以來解讀我是如何去hook ClassLoader的了. 解讀之前,先弄清楚,我為何 要 hookClassLoader,為什么 hook了它之后,我的 fix.dex就能發揮作用?先解決這個疑問,既然是 hook,那么自然要讀懂源碼,因為 hook就是在理解源碼思維的前提下,更改源碼邏輯。 一張圖解決你的疑問: 
按照上面圖,去追蹤源碼,會發現, ClassLoader最終會從 DexFile對象中去獲得一個 Class對象。并且在 DexPathList類中 findClass的時候,存在一個 Element數組的遍歷。這就意味著,如果存在多個 dex文件,多個 dex文件中都存在同樣一個 class,那么它會從第一個開始找,如果找到了,就會立即返回。如果沒找到,就往下一個dex去找。
也就是說,如果我們可以在 這個數組中插入我們自己的修復bug的 fix.dex,那我們就可以讓我們 已經修復bug的補丁類發揮作用,讓類加載器優先讀取我們的 補丁類.
OK,理解了源碼的邏輯,那我們可以動手了。來解析SDK 23的 hookClassLoader過程吧!
確定思路,我們要改變app啟動之后,自帶的ClassLoader對象(具體實現類是PathClassLoader )中 DexPathList 中 Element[] element 的實際值。
那么,步驟:
1.取得
PathClassLoader的pathList的屬性
2.取得PathClassLoader的pathList的屬性真實值(得到一個DexPathList對象)
3.獲得DexPathList中的dexElements屬性
4.獲得DexPathList對象中dexElements屬性的真實值(它是一個Element數組) 做完這4個步驟,我們得到下面的代碼
5.用外部傳入的Dex文件路徑,構建一個我們自己的Element數組
6.將從外部傳入的ClassLoader中得到的Element數組和 我們自己的Element數組合并起來, 注意,我們自己的數組元素要放前面!
7.將剛才合并的新Element數組,設置到 外部傳入的ClassLoader里面去。
OK,收官!
上面的內容,讀起來可能會有一些疑問,我預估到了一些,將答案寫在下面
1. 當我們需要反射獲得一個類的某個方法或者成員變量時,我們只想拿
getDeclareXX,因為我們只想拿本類中的成員,但是僅僅getDeclareXX不能跨越繼承關系 拿到 父類中的非私有成員,所以我寫了ReflectionUtil.java,支持跨越繼承關系 拿到父類的非私有成員。
2. 這種熱修復,是不是下載的包會很大,和原先的apk差不多大?答案是,NO,我們只需要將我們修復bug之后的補丁dex下載到設備,讓app重啟,去讀取這個dex即可。補丁包很小,甚至只有1K.
3. 這種修復方式必須重啟么? 是的,必須重啟,當然,存在不需要重啟就可以修復bug的方法,那種方法叫做instant run方案,本文不涉及。而,當前這種方案叫做:MultipleDex即,多dex方案。
*4.** 為什么要對SDK23 ,19,14 寫不同的hook代碼?因為SDK版本的變遷,導致 一些類的關系,變量名,方法名,方法參數(個數和類型)都會發生變化,所以,要針對各個變遷的版本進行兼容。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。