前言:
又有一陣子沒有更新博客了,最近本人在完成一個開源項目。馬上又到年尾了,所以時間比較趕。等完成之後會給大家分享一下,希望小夥伴們能多多支持(star)啊!好了,回到今天的主題,熱修復這個技術想必大家都聽過,或者也試過。這確實是一個非常牛逼的技術。但其實已經不算是新東西了。這些年來國內各大互聯網巨頭也都推出了自己的熱修復方案,比如我要說的微信的tinker,還有像阿里的andfix,等等。之所以會出現這麼一個東西,估計大家也都能猜個七八。無非就是傳統的APP上線流程帶來的各種弊端。小公司的小項目也許可能影響不大。但像大廠的那些DAU海量的項目那就影響非常巨大了。大家也都看到了,我今天分享的是class文件的修復,其實熱修復能做的遠不止這些啊,它還能對資源文件,so庫等等進行修復。限於篇幅和複雜度呢,接下來我將不會涉及到這些。大家如果有興趣可以去看看Tinker的官網,好了,下面就讓我帶大家來看一看熱修復的簡單實現吧!
一,過去與現在:
下面通過兩張圖我們來看看傳統APP開發與熱修復開發的不同之處:
很明顯在第五步開始出現了不同,那我來總結一下熱修復的優勢有哪些:
- 無需重新發布新版本省時省力
- 用戶無感修復,無需下載新版本,體驗很好
- 修復效率高,降低損失
二 ,熱修復原理:
關於原理我也直接畫了張圖,帶大家看看
總結起來就是利用反射,把修復好的dexclassloader的pathlist的dexElements,和系統原本的dexElements合併,注意合併時把修復好的放前面,最後把這個合併的賦值給系統的pathList即可。說完這些我估計有很多小夥伴會很懵,首先要給大家說的是,這種class文件修復的做法原理和tinker差不多,這個小夥伴也可以去看看tinker的源碼。可能有些小夥伴對這些API或者流程很不理解啊,首先大家要知道,一個apk文件它其實就是打包了dex文件和資源文件,小夥伴可以將apk文件的後綴名改爲zip,再去解壓一下就知道了。那麼dex文件呢就是我們Android工程內所有class文件的合集。因爲APP最終運行的時候就是靠Android系統去執行這些dex文件,整個過程就是從我們寫下Java文件開始(源碼期),經由JVM編譯成class文件(編譯期),然後通過dex工具轉換成dex文件,最後經過Dalvik虛擬機去執行。所以我們的要做到class修復,重點就是要改變這些有bug的dex文件。而這些dex文件就存在於dexclassloader的pathlist的dexElements數組中,之後重點就是各種反射的用法了。關於反射小夥伴們如果不熟悉可以自己去查查資料回顧一下。那麼,以上所有這些流程,下面我將通過代碼實例展示給大家看看。
三,編碼實現:
下面我直接上代碼,註釋已經很清楚了:
//這裏我直接將已修復的dex文件打包好複製到APP私有路徑中,實際運用中我們先是從服務器拉取
public void goHotFix(View view) {
//獲取到apk的私有存儲路徑
File filesDir = getDir("dex", Context.MODE_PRIVATE);
//獲取到沒有bug的dex文件的名字
String name = "Test.dex";
//創建該dex文件的file
String path = new File(filesDir, name).getAbsolutePath();
//根據這個路徑去創建一個新的file對象
File file = new File(path);
//如果這個文件存在就刪除掉
if(file.exists()){
file.delete();
}
//創建io流
InputStream is = null;
FileOutputStream os = null;
try {
is = new FileInputStream(new File(Environment.getExternalStorageDirectory(),name));
os = new FileOutputStream(path);
int len = 0;
byte[] bytes = new byte[1024];
while ((len = is.read(bytes))!=-1){
os.write(bytes,0,len);
}
File f = new File(path);
if(f.exists()){
Toast.makeText(this, "文件複製成功", Toast.LENGTH_SHORT).show();
}
FixManager.loadDex(this);
}catch (Exception e){
e.printStackTrace();
}finally {
try {
is.close();
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* Author by YX, Date on 2019/12/21.
* 修復Dex文件的工具類
*/
public class FixManager {
//創建一個用來存儲加載到的dex文件的集合
private static HashSet<File> loadedDexSet = new HashSet<>();
static {
//保證集合操作前爲空
loadedDexSet.clear();
}
/**
*這裏就是代替dex文件的操作
* 根據上下文來加載dex文件,並放入集合中
* @param context
*/
public static void loadDex(Context context){
if(context == null){
return;
}
//獲取當前應用所在私有路徑,也就是dex文件的目錄
File odexDir = context.getDir("dex", Context.MODE_PRIVATE);
//通過該目錄獲得目錄下所有文件的數組
File[] files = odexDir.listFiles();
for (File file : files) {
if(file.getName().startsWith("classes")||file.getName().endsWith(".dex")){
loadedDexSet.add(file);
}
}
//創建一個目錄,用來裝載解壓的文件
String optimizeDir = odexDir.getAbsolutePath() + File.separator + "opt_dex";
File fopt = new File(optimizeDir);
//如果這個目錄不存在就創建
if(!fopt.exists()){
fopt.mkdirs();
}
//遍歷這個dex集合
for (File file : loadedDexSet) {
//獲取當前dex類加載器
DexClassLoader dexClassLoader = new DexClassLoader(file.getAbsolutePath(), fopt.getAbsolutePath(), null, context.getClassLoader());
//實現一個類加載器的對象
PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
try {
//通過反射拿到系統類加載器
Class<?> baseDexClassLoaderClass = Class.forName("dalvik.system.BaseDexClassLoader");
Field systemPathList = baseDexClassLoaderClass.getDeclaredField("pathList");
systemPathList.setAccessible(true);
Object splObj = systemPathList.get(pathClassLoader);
Class<?> pathListClass = splObj.getClass();
Field dexElements = pathListClass.getDeclaredField("dexElements");
dexElements.setAccessible(true);
Object dexElementsObj = dexElements.get(splObj);
//創建自己的類加載器
Class<?> myBaseDexClassLoaderClass = Class.forName("dalvik.system.BaseDexClassLoader");
Field myPathList = myBaseDexClassLoaderClass.getDeclaredField("pathList");
myPathList.setAccessible(true);
Object mysplObj = myPathList.get(dexClassLoader);
Class<?> myPathListClass = mysplObj.getClass();
Field myDexElements = myPathListClass.getDeclaredField("dexElements");
myDexElements.setAccessible(true);
Object mydexElementsObj = myDexElements.get(mysplObj);
//進行dex文件的融合
Class<?> componentType = dexElementsObj.getClass().getComponentType();
//分別得到兩個dexElements的長度
int systemDEL = Array.getLength(dexElementsObj);
int myDEL = Array.getLength(mydexElementsObj);
//創建一個能放入它們的數組
int newL = systemDEL + myDEL;
Object newDEL = Array.newInstance(componentType, newL);
for (int i = 0; i < newL; i++) {
if(i < myDEL){
Array.set(newDEL,i,Array.get(mydexElementsObj,i));
}else {
Array.set(newDEL,i,Array.get(dexElementsObj,i-myDEL));
}
}
//將融合後的數組賦值給系統
Field systemDexElements = pathListClass.getDeclaredField("dexElements");
systemDexElements.setAccessible(true);
systemDexElements.set(splObj,newDEL);
} catch (Exception e) {
e.printStackTrace();
}finally {
Toast.makeText(context, "修復成功", Toast.LENGTH_SHORT).show();
}
}
}
}
public class MyApp extends Application{
@Override
public void onCreate() {
super.onCreate();
}
@Override
protected void attachBaseContext(Context base) {
//注意這裏要到gradle裏面先開啓分包
MultiDex.install(base);
FixManager.loadDex(base);
super.attachBaseContext(base);
}
}
上面代碼就是總體實現,之後我們將編譯好的apk解壓,要注意的是,由於做了分包處理,我們要把size更大的那個dex文件提取出來使用,爲什麼要分包,給你一個數字65536。可能有些小夥伴會覺得這樣做,豈不是apk的體積會變得很龐大。其實呢這只是一種簡單通俗的做法。但也確實會存在上述問題。我們也可以單獨把修復的文件打成dex文件,這樣就更好一點。當然我還是要說這只是我自己的做法,小夥伴們可以自己去研究tinker的源碼看看。如果不知道如何單獨把出錯經修復的Java文件打包成dex文件,下面我也分幾步告訴大家 :
- 首先rebuild一下工程,在app\build\intermediates\classes\debug下拿到想要代替的class文件
- 在AndroidSDK的build-tools下隨便選一個版本進去打開cmd命令,
- 輸入dx --dex --no-strict --output空格 生成dex文件的路徑 空格 class文件的路徑 最後回車即可
如果是按我的做法就是把這個dex文件取名爲Test放到sdcard根目錄下即可,隨後就能完成修復工作了。工程源碼我已開源,想要的小夥伴看這裏 https://github.com/OMGyan/XHotFix
以上!!!