手動實現(xiàn)最簡單的Android熱修復(最新最全詳細小白教程)

版權聲明:轉載請附上原地址。 https://blog.csdn.net/hq942845204/article/details/81044158

前言

最近了解到了熱修復相關的東西,于是很好奇原理,便一番搜索資料,同時為了加深對熱修復的理解,便自己照著網上的例子去實現(xiàn)一個熱修復,因為基礎相對比較差,而且網上很多例子都是過時的,而且很多細節(jié)不注意到的話,就是一個坑,而且還五花八門的,于是我覺得將自己的這個實現(xiàn)熱修復的例子記錄下來事很有必要的,主要是參考并綜合了網上很多熱修復的例子,自己實現(xiàn)并完成整個從0到1的過程,好了,我們開始吧!

需要知道的基本概念和原理

首先是熱修復的基本概念,我不太喜歡那種專業(yè)術語的描述方式,因為那樣很容易讓人覺得晦澀難懂,而且我覺得唯一的效果就是營造出一種初學者覺得高大上的裝X效果而已,所以我就說下我的理解:假定一個場景,你的APP上線了,現(xiàn)在發(fā)現(xiàn)了一個小Bug,這個Bug很簡單,可能是一行代碼的事,但是由于你才上線,要是再重新打包你的APP再上線,這個過程就很麻煩了,于是我們期望有一種方式,不需要用戶來重新安裝新的APP即可運行我們修復了Bug的APP,這種方式就叫熱修復。

是不是覺得很神奇。我也是,在沒接觸之前,我也覺得很神奇,但是明白之后,其實真的沒什么,很簡單。

再說下熱修復的基本原理,這里很多網上的講解非常的專業(yè),我看了之后也理解了好久,但是自己再梳理一下,其實沒有那么難理解,我還是以通俗的方式來說:

假設有一個數(shù)組,這個數(shù)組,里面有很多個dex文件(不了解的只需要知道里面就是存放了類的二進制數(shù)據(jù),用來給安卓虛擬機加載),然后安卓虛擬機在加載類文件的時候,會有個順序,我們暫且不用管是什么順序,或者是怎么加載的,我們只需要知道,它會有順序,我們假定它從數(shù)組下標為0的地方開始循環(huán)找,一旦找到了對應的文件,那么后面即便仍然還有該類的dex文件,也不去加載了,相當與前面加載的會覆蓋后面的,就是這樣一個原理。

那么實際中,可以怎么實現(xiàn)呢,我們可以將相應的dex文件放在服務器上,然后在用戶不知道的情況下(可以在歡迎界面掃描服務器山的文件,如果有則下載進行熱修復,否則不管),將這個dex文件從服務器上下載下來,并移動到指定位置即可。

我們也不需知道具體移動到哪里了,只知道移動的地方需要滿足的條件是:在對應的類的dex文件加載順序之前。這樣就可以實現(xiàn)覆蓋效果,讓新的類文件比舊的類文件先加載,舊的就不會生效,達到我們想要的效果。

動手實現(xiàn)

在動手實現(xiàn)前,需要知道的事:

上面我們說了一種方案是從服務器上下載對應的dex文件,這里因為只是模擬一下效果,便采用手動復制對應的dex文件到指定目錄下,來達到同樣的效果。

開始吧:

首先我們新建工程,隨便寫一個Bug,比如我這里除數(shù)為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方法時肯定會提示異常導致退出,現(xiàn)在我們以熱修復的方式來修復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方法,觸發(fā)Bug,另一個按鈕點擊修復Bug,需要注意的是,千萬不要忘記了權限的申請,因為整個過程涉及到文件的讀取和寫入,而6.0以上需要動態(tài)獲取權限,所以要在清單文件中加入下列兩行代碼。

<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,看名字就知道這是兩個類加載器,用來加載類的,想知道具體實現(xiàn)的,下面就是源碼

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文件中加入代碼,不要加入到某個節(jié)點下。最終代碼如下

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包存放的地方在配置文件中配置了,比如我這里就在這個目錄下,如圖

我這里的,名字叫l(wèi)og,所以最終得到的是一個名為log.jar的文件,現(xiàn)在我們用這個jar來得到dex文件,需要用到的工具是dx,這個工具在哪里呢,就是SDK目錄下的build-tools,然后隨便選擇一個版本進去就可以看到名為dx.bat的文件,這個就是我們需要使用的。

我們將log.jar文件復制到這個目錄下,按住shift右擊鼠標在該目錄下打開命令行,輸入命令

dx --dex --output=D:/test log.jar

其中D:/test為保存生產的dex文件的目錄,同時注意空格?;剀嚾魶]有錯誤說明生產成功,我們來到指定的D:/test目錄,發(fā)現(xiàn)我們的最終目標正靜靜的躺在里面等著我們,嘿嘿!

好了,我們現(xiàn)在將這個乖巧的classes.dex文件復制到我們的手機目錄下,這里為了演示效果,我就采用的模擬器,如下,我這里將它重命名為classes2.jar,不重命名也沒關系,名字無所謂,復制的目錄為

這個過程在實際當中就是用戶下載服務器上的對應文件,然后用代碼將其放到指定目錄下,只不過這里我們是手動模擬的這個操作。

然后我們就可以運行我們的程序了,但是運行程序之前還有兩件事:

一:沒猜錯的話,你現(xiàn)在的代碼是修復Bug后的代碼,所以我們要將代碼改會錯誤的版本,也就是下面這個

這樣我們才能有Bug來修復嘛,不然我們Bug都沒有,修復啥呢,對不

二:打開AndroidStudio的設置,取消掉instant run這里的勾勾

這樣做是干嘛,取消掉這個勾勾之后,AndroidStudio在給我們安裝新應用時,就不會只安裝修改的部分,而是全部代碼都重新編譯并安裝。

好了,我們準備工作做完了。接下來運行看效果吧。

首先我們點擊修復按鈕進行模擬熱修復,看到修復成功的字樣,說明修復成功,然后我們再點擊HELLO按鈕,這里按照預期會導致除數(shù)為0的異常,但是你會驚訝的發(fā)現(xiàn),程序沒有崩掉,而是Toast提示 結果10。說明程序已經被熱修復,因為我們生成的dex文件中,將除數(shù)b改為了1,而這個正確的版本被安卓虛擬機預先加載了,所以不會執(zhí)行我們程序中錯誤版本的代碼。

結語

至此,一個完整的最簡單的小白熱修復程序已經完成?。∮信d趣的可以深入研究哦??!

有問題歡迎留言,我會及時回復的。

---------------------

作者:黃慶慶

來源:CSDN

原文:https://blog.csdn.net/hq942845204/article/details/81044158

版權聲明:本文為博主原創(chuàng)文章,轉載請附上博文鏈接!

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容