我的熱修復例子是通過鴻洋大神的博客http://blog.csdn.net/lmj623565791/article/details/49883661;裏面的例子來寫的。
關於熱修復在這裏大概的描述一下:
熱修復又叫做動態加載,熱更新,熱修復等等等一大堆的名字,不過好像都是一個意思:就是通過打補丁的方式來有沒有辦法以補丁的方式動態修復緊急Bug,不再需要重新發布App,不再需要用戶重新下載,覆蓋安裝。安卓App熱補丁動態修復技術介紹具體的可以看一下這篇博客的關於動態修復的介紹。還可以看一下關於apk編譯的過程,我這邊主要是關於具體的實現。
在項目中如果BugClass.突然出現了一個bug,然後我們要發佈一個jar包,把BugClass.jave文件給替換掉,要如何實現呢?
BugClass類:
public class BugClass{
public String bug(){
return "bug class";
}
}
LoadBugClass類:
public class LoadBugClass {
public String getBugString(){
BugClass bugClass = new BugClass();
return bugClass.bug();
}
}
如果在BugClass出現問題了我們要把return返回的字符串改一改要如何實現呢?例如改成這樣:
public class BugClass{
public String bug(){
return "fix the bug class2";
}
}
首先java文件編譯後會形成dex文件,這樣才能在安卓虛擬機上運行。既然要動態修復的話,我們總不可能直接搞一個jar包,然後裏面是class文件吧?!所以我們要把修復的類導出來,然後用dx工具轉化成dex文件的格式。把java導出成jar包的形式我就不多說了,把jar包轉化成dex文件的話,可以在sdk的build-tools文件夾下的隨便一個文件夾點擊進去就可以看到一個dx.bat的文件,那個就是dex的轉換工具。
運行cmd命令行,然後進入到dx.bat存在的目錄下。記得也要把你導出的jar文件放在同一個目錄下:然後輸入命令行:de --dex --output XXX.dex(你要轉化的dex包的名稱) XXX.jar(你導入的jar包),點擊確定後你就可以開心的看到在目錄下會生成個XXX.dex文件,這個就是後面需要用到的要修復的dex文件。
然後把dex文件放在assets文件夾下,這樣要修復的材料準備好了,現在就要着手把這個dex文件加載到安卓虛擬機上。
一般的實現方式是:在服務器上下載dex或者jar包,然後動態加載,我是放在assets文件夾下,差別不是很大,就少了一個具體的從網絡上獲取dex包的過程。下面就是具體的實現方案
MainActivity類
import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.widget.Toast;
public class MainActivity extends Activity {
private static final String TAG = "TAG";
private Handler mHandler = new Handler(){
public void handleMessage(Message msg) {
Toast.makeText(MainActivity.this, msg.obj.toString() + "", Toast.LENGTH_SHORT).show();
};
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
new Thread(new Runnable() {
@Override
public void run() {
Log.d(TAG, "------------start");
//把DexLoaderUtil.SECONDARY_DEX_NAME DexLoaderUtil.THIRD_DEX_NAME在assest中的文件寫入到apk的私有dex文件夾
DexLoaderUtil.copyDex(MainActivity.this, DexLoaderUtil.SECONDARY_DEX_NAME);
String secondDexPath = DexLoaderUtil.getDexPath(MainActivity.this, DexLoaderUtil.SECONDARY_DEX_NAME);
final String optimizedDexOutputPath = DexLoaderUtil.getOptimizedDexPath(MainActivity.this);
DexLoaderUtil.injectAboveEqualApiLevel14(secondDexPath, optimizedDexOutputPath, null, "com.example.cla.LoadBugClass");
String bug = DexLoaderUtil.call(getClassLoader());
Message msg = mHandler.obtainMessage();
msg.obj = bug;
mHandler.sendMessage(msg );
Log.d(TAG, "------------end bug = "+bug);
}
}).start();
}
}
下面是核心類:DexLoadUtil的具體實現:
import android.annotation.SuppressLint;
import android.content.Context;
import android.util.Log;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import dalvik.system.DexClassLoader;
import dalvik.system.PathClassLoader;
/**
* Created by bruce on 11/14/15.
*/
public class DexLoaderUtil {
private static final String TAG = "TAG";
public static final String SECONDARY_DEX_NAME = "fixdex.dex";
public static final String THIRD_DEX_NAME = "fixdex2.dex";
private static final int BUF_SIZE = 8 * 1024;
/**
* 獲取在copyDex函數創建的dex私有文件下的dexName文件
* @param context
* @param dexName
* @return
*/
public static String getDexPath(Context context, String dexName) {
return new File(context.getDir("dex", Context.MODE_PRIVATE), dexName).getAbsolutePath();
}
/**
* 取得可操作的dex地址
* @param context
* @return
*/
public static String getOptimizedDexPath(Context context) {
return context.getDir("outdex", Context.MODE_PRIVATE).getAbsolutePath();
}
/**
* ok,其實就是文件的一個讀寫,將assets目錄名稱爲dexName文件,寫到app的私有目錄中的dex文件。
* @param context
* @param dexName 要修復的文件名稱
*/
public static void copyDex(Context context, String dexName) {
//創建一個私有的文件
File dexInternalStoragePath = new File(context.getDir("dex", Context.MODE_PRIVATE),
dexName);
BufferedInputStream bis = null;
OutputStream dexWriter = null;
try {
bis = new BufferedInputStream(context.getAssets().open(dexName));//獲取Assets文件對應的dexName名稱的文件的輸出流
dexWriter = new BufferedOutputStream(
new FileOutputStream(dexInternalStoragePath));
byte[] buf = new byte[BUF_SIZE];
int len;
while((len = bis.read(buf, 0, BUF_SIZE)) > 0) {
dexWriter.write(buf, 0, len);
}
dexWriter.close();
bis.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* @param context
* @param dexName
*/
public static void loadAndCall(Context context, String dexName) {
final File dexInternalStoragePath = new File(context.getDir("dex", Context.MODE_PRIVATE), dexName);
final File optimizedDexOutputPath = context.getDir("outdex", Context.MODE_PRIVATE);
DexClassLoader cl = new DexClassLoader(dexInternalStoragePath.getAbsolutePath(),
optimizedDexOutputPath.getAbsolutePath(),
null,
context.getClassLoader());
call(cl);
}
/**
* 加載對應類對應的方法
* @param cl
*/
public static String call(ClassLoader cl) {
Class<?> myClasz = null;
try {
myClasz =
cl.loadClass("com.example.cla.LoadBugClass");
Object instance = myClasz.getConstructor().newInstance();
return (String) myClasz.getDeclaredMethod("getBugString").invoke(instance);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
return "";
}
/**
* 熱修復
* @param dexPath 要修復的文件路徑
* @param defaultDexOptPath 優化後的dex文件存放目錄,
* @param nativeLibPath 目標類中使用的C/C++庫的列表,每個目錄用File.pathSeparator間隔開; 可以爲 null
* @param dummyClassName 要修改的類(用於插入到dexElements數組前的類)
* @return
*/
@SuppressLint("NewApi")
public static synchronized Boolean injectAboveEqualApiLevel14(
String dexPath, String defaultDexOptPath, String nativeLibPath, String dummyClassName) {
Log.i(TAG, "--> injectAboveEqualApiLevel14");
//根據context拿到PathClassLoader, 前執行類的裝載器
PathClassLoader pathClassLoader = (PathClassLoader) DexLoaderUtil.class.getClassLoader();
DexClassLoader dexClassLoader = new DexClassLoader(dexPath, defaultDexOptPath, nativeLibPath, pathClassLoader);
try {
dexClassLoader.loadClass(dummyClassName); //利用反射 獲取類
//通過獲取PathClassLoader類加載器PathClassLoader的dexElements
//然後再獲取到要進行熱修復的DexClassLoader的dexElements進行合併
Object dexElements = combineArray(
getDexElements(getPathList(pathClassLoader)),//在調用getDexElements通過pathList取到dexElements對象。
getDexElements(getPathList(dexClassLoader)));
//然後通過getPathList(pathClassLoader),拿到PathClassLoader中的pathList對象,
Object pathList = getPathList(pathClassLoader);
setField(pathList, pathList.getClass(), "dexElements", dexElements);
} catch (Throwable e) {
e.printStackTrace();
return false;
}
Log.i(TAG, "<-- injectAboveEqualApiLevel14 End.");
return true;
}
/**
* 獲取 BaseDexClassLoader中的pathList對象
* @param baseDexClassLoader PathClassLoader對象
* @return
* @throws IllegalArgumentException
* @throws NoSuchFieldException
* @throws IllegalAccessException
* @throws ClassNotFoundException
*/
private static Object getPathList(Object baseDexClassLoader)
throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
return getField(baseDexClassLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
}
/**
* 獲取pathList對象中的DexFile的集合dexElements
* 在BaseDexClassLoader中有個pathList對象,pathList中包含一個DexFile的集合dexElements
* 通過pathList對象可以獲取到DexFile的集合dexElements
* @param paramObject
* @return
* @throws IllegalArgumentException
* @throws NoSuchFieldException
* @throws IllegalAccessException
*/
private static Object getDexElements(Object paramObject)
throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException {
return getField(paramObject, paramObject.getClass(), "dexElements");
}
/**
* 獲取對應類所對應的字段 在這是獲取類 dalvik.system.BaseDexClassLoader的pathList對象 也就是獲取 BaseDexClassLoader中的pathList對象
* @param obj
* @param cl 類
* @param field 對象名稱
* @return
* @throws NoSuchFieldException
* @throws IllegalArgumentException
* @throws IllegalAccessException
*/
private static Object getField(Object obj, Class<?> cl, String field)
throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
Field localField = cl.getDeclaredField(field);
localField.setAccessible(true);
return localField.get(obj);
}
/**
* 把合併成功的 dexElements數組替換掉原本的dexElements數組 完成熱修復
* @param obj pathList對象
* @param cl PathClassLoader類
* @param field 要替換的對象字段
* @param value 要替換的值
* @throws NoSuchFieldException
* @throws IllegalArgumentException
* @throws IllegalAccessException
*/
private static void setField(Object obj, Class<?> cl, String field, Object value)
throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
Field localField = cl.getDeclaredField(field);
localField.setAccessible(true);
localField.set(obj, value);
}
/**
* 通過獲取PathClassLoader類加載器PathClassLoader的dexElements
然後再獲取到要進行熱修復的DexClassLoader的dexElements進行合併合併完成後,將新的數組通過反射的方式設置給pathList.
* @param arrayLhs PathClassLoader的dexElements
* @param arrayRhs DexClassLoader的dexElements
* @return
*/
private static Object combineArray(Object arrayLhs, Object arrayRhs) {
Class<?> componentType = arrayRhs.getClass().getComponentType();
int length = Array.getLength(arrayRhs);
int length2 = Array.getLength(arrayLhs) + length;
Object newInstance = Array.newInstance(componentType, length2);
for (int i = 0; i < length2; i++){
if (i < length){
Array.set(newInstance, i, Array.get(arrayRhs, i));
} else{
Array.set(newInstance, i, Array.get(arrayLhs, i - length));
}
}
return newInstance;
}
}
效果圖: