手動實現最簡單的Android熱修復(最新最全詳細小白教程)
前言
最近了解到了熱修復相關的東西,於是很好奇原理,便一番搜尋資料,同時為了加深對熱修復的理解,便自己照著網上的例子去實現一個熱修復,因為基礎相對比較差,而且網上很多例子都是過時的,而且很多細節不注意到的話,就是一個坑,而且還五花八門的,於是我覺得將自己的這個實現熱修復的例子記錄下來事很有必要的,主要是參考並綜合了網上很多熱修復的例子,自己實現並完成整個從0到1的過程,好了,我們開始吧!
需要知道的基本概念和原理
首先是熱修復的基本概念,我不太喜歡那種專業術語的描述方式,因為那樣很容易讓人覺得晦澀難懂,而且我覺得唯一的效果就是營造出一種初學者覺得高大上的裝X效果而已,所以我就說下我的理解:假定一個場景,你的APP上線了,現在發現了一個小Bug,這個Bug很簡單,可能是一行程式碼的事,但是由於你才上線,要是再重新打包你的APP再上線,這個過程就很麻煩了,於是我們期望有一種方式,不需要使用者來重新安裝新的APP即可執行我們修復了Bug的APP,這種方式就叫熱修復。
是不是覺得很神奇。我也是,在沒接觸之前,我也覺得很神奇,但是明白之後,其實真的沒什麼,很簡單。
再說下熱修復的基本原理,這裡很多網上的講解非常的專業,我看了之後也理解了好久,但是自己再梳理一下,其實沒有那麼難理解,我還是以通俗的方式來說:
假設有一個陣列,這個陣列,裡面有很多個dex檔案(不瞭解的只需要知道里面就是存放了類的二進位制資料,用來給安卓虛擬機器載入),然後安卓虛擬機器在載入類檔案的時候,會有個順序,我們暫且不用管是什麼順序,或者是怎麼載入的,我們只需要知道,它會有順序,我們假定它從陣列下標為0的地方開始迴圈找,一旦找到了對應的檔案,那麼後面即便仍然還有該類的dex檔案,也不去載入了,相當與前面載入的會覆蓋後面的,就是這樣一個原理。
那麼實際中,可以怎麼實現呢,我們可以將相應的dex檔案放在伺服器上,然後在使用者不知道的情況下(可以在歡迎介面掃描伺服器山的檔案,如果有則下載進行熱修復,否則不管),將這個dex檔案從伺服器上下載下來,並移動到指定位置即可。
我們也不需知道具體移動到哪裡了,只知道移動的地方需要滿足的條件是:在對應的類的dex檔案載入順序之前。這樣就可以實現覆蓋效果,讓新的類檔案比舊的類檔案先載入,舊的就不會生效,達到我們想要的效果。
動手實現
在動手實現前,需要知道的事:
上面我們說了一種方案是從伺服器上下載對應的dex檔案,這裡因為只是模擬一下效果,便採用手動複製對應的dex檔案到指定目錄下,來達到同樣的效果。
開始吧:
首先我們新建工程,隨便寫一個Bug,比如我這裡除數為0的Bug
public class TestCaculate {
public int a = 10;
public int b = 0;
public void caculate(Context context) {
Toast.makeText(context, "結果" + a / b, Toast.LENGTH_SHORT).show();
}
}
當我們呼叫caculate方法時肯定會提示異常導致退出,現在我們以熱修復的方式來修復Bug。
首先我們需要生成的檔案就是我們修復好Bug的程式的dex檔案,看清楚了,是修復好Bug的,代表什麼意思呢,也就是在進行下一步之前,TestCaculate程式碼是這樣的
public class TestCaculate {
public int a = 10;
public int b = 1;//已經修復
public void caculate(Context context) {
Toast.makeText(context, "結果" + a / b, Toast.LENGTH_SHORT).show();
}
}
然後我們在佈局檔案中新增二個按鈕,一個按鈕點選呼叫caculate方法,觸發Bug,另一個按鈕點選修復Bug,需要注意的是,千萬不要忘記了許可權的申請,因為整個過程涉及到檔案的讀取和寫入,而6.0以上需要動態獲取許可權,所以要在清單檔案中加入下列兩行程式碼。
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
MainActivity程式碼如下
public class MainActivity extends AppCompatActivity {
private Button btn, btn_fix;
public static final int REQUEST_CODE = 1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btn = findViewById(R.id.btn);
btn_fix = findViewById(R.id.btn_fix);
btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
TestCaculate testCaculate = new TestCaculate();
testCaculate.caculate(MainActivity.this);
}
});
btn_fix.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
fix();
}
});
ActivityCompat.requestPermissions(MainActivity.this,
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUEST_CODE);
}
private void fix() {
try {
String dexPath = Environment.getExternalStorageDirectory() + "/classes2.dex";
HotFix.patch(this, dexPath, "com.aiiage.testhotfix.TestCaculate");
Toast.makeText(this, "修復成功", Toast.LENGTH_SHORT).show();
} catch (Exception e) {
Toast.makeText(this, "修復失敗" + e.getMessage(), Toast.LENGTH_SHORT).show();
e.printStackTrace();
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
switch (requestCode) {
case REQUEST_CODE: {
if (grantResults.length > 0) {
// permission was granted
} else {
// permission denied
}
return;
}
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}
然後就是我們熱修復的工具類,怎麼使用,在MainActivity中已經有使用的程式碼了,工具類中用到了反射的知識,但是不是本文的重點,有需要的小夥伴自行學習相關知識,這個工具類中,最終要的兩個類就是DexClassLoader和PathClassLoader,看名字就知道這是兩個類載入器,用來載入類的,想知道具體實現的,下面就是原始碼
public final class HotFix {
/**
* 修復指定的類
*
* @param context 上下文物件
* @param patchDexFile dex檔案
* @param patchClassName 被修復類名
*/
public static void patch(Context context, String patchDexFile, String patchClassName) {
if (patchDexFile != null && new File(patchDexFile).exists()) {
try {
if (hasLexClassLoader()) {
injectInAliyunOs(context, patchDexFile, patchClassName);
} else if (hasDexClassLoader()) {
injectAboveEqualApiLevel14(context, patchDexFile, patchClassName);
} else {
injectBelowApiLevel14(context, patchDexFile, patchClassName);
}
} catch (Throwable th) {
}
}
}
private static boolean hasLexClassLoader() {
try {
Class.forName("dalvik.system.LexClassLoader");
return true;
} catch (ClassNotFoundException e) {
return false;
}
}
private static boolean hasDexClassLoader() {
try {
Class.forName("dalvik.system.BaseDexClassLoader");
return true;
} catch (ClassNotFoundException e) {
return false;
}
}
private static void injectInAliyunOs(Context context, String patchDexFile, String patchClassName)
throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException,
InstantiationException, NoSuchFieldException {
PathClassLoader obj = (PathClassLoader) context.getClassLoader();
String replaceAll = new File(patchDexFile).getName().replaceAll("\\.[a-zA-Z0-9]+", ".lex");
Class cls = Class.forName("dalvik.system.LexClassLoader");
Object newInstance =
cls.getConstructor(new Class[]{String.class, String.class, String.class, ClassLoader.class}).newInstance(
new Object[]{context.getDir("dex", 0).getAbsolutePath() + File.separator + replaceAll,
context.getDir("dex", 0).getAbsolutePath(), patchDexFile, obj});
cls.getMethod("loadClass", new Class[]{String.class}).invoke(newInstance, new Object[]{patchClassName});
setField(obj, PathClassLoader.class, "mPaths",
appendArray(getField(obj, PathClassLoader.class, "mPaths"), getField(newInstance, cls, "mRawDexPath")));
setField(obj, PathClassLoader.class, "mFiles",
combineArray(getField(obj, PathClassLoader.class, "mFiles"), getField(newInstance, cls, "mFiles")));
setField(obj, PathClassLoader.class, "mZips",
combineArray(getField(obj, PathClassLoader.class, "mZips"), getField(newInstance, cls, "mZips")));
setField(obj, PathClassLoader.class, "mLexs",
combineArray(getField(obj, PathClassLoader.class, "mLexs"), getField(newInstance, cls, "mDexs")));
}
@TargetApi(14)
private static void injectBelowApiLevel14(Context context, String str, String str2)
throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
PathClassLoader obj = (PathClassLoader) context.getClassLoader();
DexClassLoader dexClassLoader =
new DexClassLoader(str, context.getDir("dex", 0).getAbsolutePath(), str, context.getClassLoader());
dexClassLoader.loadClass(str2);
setField(obj, PathClassLoader.class, "mPaths",
appendArray(getField(obj, PathClassLoader.class, "mPaths"), getField(dexClassLoader, DexClassLoader.class,
"mRawDexPath")
));
setField(obj, PathClassLoader.class, "mFiles",
combineArray(getField(obj, PathClassLoader.class, "mFiles"), getField(dexClassLoader, DexClassLoader.class,
"mFiles")
));
setField(obj, PathClassLoader.class, "mZips",
combineArray(getField(obj, PathClassLoader.class, "mZips"), getField(dexClassLoader, DexClassLoader.class,
"mZips")));
setField(obj, PathClassLoader.class, "mDexs",
combineArray(getField(obj, PathClassLoader.class, "mDexs"), getField(dexClassLoader, DexClassLoader.class,
"mDexs")));
obj.loadClass(str2);
}
private static void injectAboveEqualApiLevel14(Context context, String str, String str2)
throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
Object a = combineArray(getDexElements(getPathList(pathClassLoader)),
getDexElements(getPathList(
new DexClassLoader(str, context.getDir("dex", 0).getAbsolutePath(), str, context.getClassLoader()))));
Object a2 = getPathList(pathClassLoader);
//新的dexElements物件重新設定回去
setField(a2, a2.getClass(), "dexElements", a);
pathClassLoader.loadClass(str2);
}
/**
* 通過反射先獲取到pathList物件
*
* @param obj
* @return
* @throws ClassNotFoundException
* @throws NoSuchFieldException
* @throws IllegalAccessException
*/
private static Object getPathList(Object obj) throws ClassNotFoundException, NoSuchFieldException,
IllegalAccessException {
return getField(obj, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
}
/**
* 從上面獲取到的PathList物件中,進一步反射獲得dexElements物件
*
* @param obj
* @return
* @throws NoSuchFieldException
* @throws IllegalAccessException
*/
private static Object getDexElements(Object obj) throws NoSuchFieldException, IllegalAccessException {
return getField(obj, obj.getClass(), "dexElements");
}
private static Object getField(Object obj, Class cls, String str)
throws NoSuchFieldException, IllegalAccessException {
Field declaredField = cls.getDeclaredField(str);
declaredField.setAccessible(true);//設定為可訪問
return declaredField.get(obj);
}
private static void setField(Object obj, Class cls, String str, Object obj2)
throws NoSuchFieldException, IllegalAccessException {
Field declaredField = cls.getDeclaredField(str);
declaredField.setAccessible(true);//設定為可訪問
declaredField.set(obj, obj2);
}
//合拼dexElements
private static Object combineArray(Object obj, Object obj2) {
Class componentType = obj2.getClass().getComponentType();
int length = Array.getLength(obj2);
int length2 = Array.getLength(obj) + length;
Object newInstance = Array.newInstance(componentType, length2);
for (int i = 0; i < length2; i++) {
if (i < length) {
Array.set(newInstance, i, Array.get(obj2, i));
} else {
Array.set(newInstance, i, Array.get(obj, i - length));
}
}
return newInstance;
}
private static Object appendArray(Object obj, Object obj2) {
Class componentType = obj.getClass().getComponentType();
int length = Array.getLength(obj);
Object newInstance = Array.newInstance(componentType, length + 1);
Array.set(newInstance, 0, obj2);
for (int i = 1; i < length + 1; i++) {
Array.set(newInstance, i, Array.get(obj, i - 1));
}
return newInstance;
}
}
佈局檔案就兩個按鈕,就不貼了,佔空間,好了,程式碼準備完畢,接著下一步吧。
接下來,我們生成專案對應的dex檔案,網上資料有點少,,而且有的還是錯的,各種莫名其妙的操作,哎說多了都是淚,但是也還是有正確的,我這裡採用了一種相對簡單的方式,首先在app的module下的build.gradle檔案中加入程式碼,不要加入到某個節點下。最終程式碼如下
apply plugin: 'com.android.application'
android {
compileSdkVersion 28
defaultConfig {
applicationId "com.aiiage.testhotfix"
minSdkVersion 26
targetSdkVersion 28
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.android.support:appcompat-v7:28.0.0-alpha3'
implementation 'com.android.support.constraint:constraint-layout:1.1.2'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}
//加入的程式碼從這裡開始
task clearJar(type: Delete) {
delete('libs/log.jar')
}
task makeJar(type: org.gradle.api.tasks.bundling.Jar) {
//指定生成的jar名稱
baseName 'log'
//從哪裡打包class檔案
from('build/intermediates/classes/debug/com/aiiage/testhotfix/')
//打包到jar後的目錄結構
into('com/aiiage/testhotfix/')
//去掉不需要打包的目錄和檔案
exclude('text/', 'BuildConfig.class', 'R.class', 'BuildConfig.class')
exclude {
it.name.startsWith('R$');
}
}
makeJar.dependsOn(clearJar, build)
加入的程式碼代表什麼意思註釋已經很清楚了,這個過程最終會生成一個jar包,然後開啟AndroidStudio底下的命令列,如圖
在命令列中,我們輸入gradlew makeJar 注意,不要輸錯了,等待約2分鐘左右,看到如下的字樣,代表生成jar成功
生成的jar包存放的地方在配置檔案中配置了,比如我這裡就在這個目錄下,如圖
我這裡的,名字叫log,所以最終得到的是一個名為log.jar的檔案,現在我們用這個jar來得到dex檔案,需要用到的工具是dx,這個工具在哪裡呢,就是SDK目錄下的build-tools,然後隨便選擇一個版本進去就可以看到名為dx.bat的檔案,這個就是我們需要使用的。
我們將log.jar檔案複製到這個目錄下,按住shift右擊滑鼠在該目錄下開啟命令列,輸入命令
dx --dex --output=D:/test log.jar
其中D:/test為儲存生產的dex檔案的目錄,同時注意空格。回車若沒有錯誤說明生產成功,我們來到指定的D:/test目錄,發現我們的最終目標正靜靜的躺在裡面等著我們,嘿嘿!
好了,我們現在將這個乖巧的classes.dex檔案複製到我們的手機目錄下,這裡為了演示效果,我就採用的模擬器,如下,我這裡將它重新命名為classes2.jar,不重新命名也沒關係,名字無所謂,複製的目錄為
這個過程在實際當中就是使用者下載伺服器上的對應檔案,然後用程式碼將其放到指定目錄下,只不過這裡我們是手動模擬的這個操作。
然後我們就可以執行我們的程式了,但是執行程式之前還有兩件事:
一:沒猜錯的話,你現在的程式碼是修復Bug後的程式碼,所以我們要將程式碼改會錯誤的版本,也就是下面這個
這樣我們才能有Bug來修復嘛,不然我們Bug都沒有,修復啥呢,對不
二:開啟AndroidStudio的設定,取消掉instant run這裡的勾勾
這樣做是幹嘛,取消掉這個勾勾之後,AndroidStudio在給我們安裝新應用時,就不會只安裝修改的部分,而是全部程式碼都重新編譯並安裝。
好了,我們準備工作做完了。接下來執行看效果吧。
首先我們點選修復按鈕進行模擬熱修復,看到修復成功的字樣,說明修復成功,然後我們再點選HELLO按鈕,這裡按照預期會導致除數為0的異常,但是你會驚訝的發現,程式沒有崩掉,而是Toast提示 結果10。說明程式已經被熱修復,因為我們生成的dex檔案中,將除數b改為了1,而這個正確的版本被安卓虛擬機器預先載入了,所以不會執行我們程式中錯誤版本的程式碼。
結語
至此,一個完整的最簡單的小白熱修復程式已經完成!!有興趣的可以深入研究哦!!
有問題歡迎留言,我會及時回覆的。
原始碼下載
相關文章
- Android 熱修復其實很簡單Android
- Tinker 熱修復框架 簡單上手教程框架
- Android熱修復之Tinker整合最新詳解Android
- Android熱修復簡單總結Android
- 史上最全最強SpringMVC詳細示例實戰教程SpringMVC
- 熱修復(一)原理與實現詳解
- 最新最全的史上最簡單的IDEA破解教程(破解到2100年)Idea
- 簡單易懂的tinker熱修復原理分析
- Android 熱修復 - Tinker 實現及踩過的坑Android
- 最全最強SpringMVC詳細示例實戰SpringMVC
- Android 熱修復Android
- Flutter Android 端熱修復(熱更新)實踐FlutterAndroid
- 你值得知道的Android 熱修復,以及熱修復原理Android
- Android 中外掛化學習—教你實現熱補丁動態修復Android
- Android熱修復原理Android
- 阿里熱修復AndFix的使用教程阿里
- Android熱修復原理(一)熱修復框架對比和程式碼修復Android框架
- 【Android 熱修復】美團Robust熱修復框架原理解析Android框架
- Kotlin系列教程——史上最全面、最詳細的學習教程,持續更新中....Kotlin
- 動手實現一個簡單的promisePromise
- Android 熱修復總結Android
- 史上最全、最詳細的 kafka 學習筆記!Kafka筆記
- 史上最全、最詳細的Docker學習資料Docker
- Java設計模式之單例模式,這是最全最詳細的了Java設計模式單例
- Android tinker熱修復——實戰接入專案Android
- Centos7安裝mysql5.7.27 史上最全最簡單的教程CentOSMySql
- 熱修復——深入淺出原理與實現
- 自建服務端實現Tinker熱修復服務端
- 在 Java 中運用動態掛載實現 Bug 的熱修復Java
- flutter在android端啟動流程和熱修復FlutterAndroid
- Android 熱補丁動態修復框架小結Android框架
- ChatGPT 怎麼註冊最新詳細教程 新手小白一看就會ChatGPT
- 自己動手實現一個簡單的 IOC
- robust 熱修復實踐
- Nginx 實現動態封禁IP,詳細教程來了Nginx
- 小白零基礎建站詳細教程
- 最簡單jboss教程!
- 使用rails實現最簡單的CRUDAI