前面幾篇博文已經介紹了2種熱修複架構的使用及源碼分析,AndFix相容性比較好,而Dexposed Art處于Beta版。
AndFix和Dexposed都是阿裡的開源項目。
Alibaba-AndFix Bug熱修複架構的使用
Alibaba-AndFix Bug熱修複架構原理及源碼解析
Alibaba-Dexposed架構線上熱更新檔修複的使用
Alibaba-Dexposed Bug架構原理及源碼解析
今天主要介紹的架構是根據騰訊的部落格使用ClassLoader寫的熱修複架構。
騰訊部落格:【新技能get】讓App像Web一樣釋出新版本
首先,看需要修複的類部分:
package cn.coolspan.open.fixbug;
import java.io.File;
import android.app.Activity;
import android.os.Bundle;
import android.os.Environment;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Toast;
/**
* MainActivity 2015-12-22 下午10:30:57
*
* @author 喬曉松 [email protected]
*/
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.button1).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
MyApplication myApplication = (MyApplication) getApplication();
File patch = new File(
Environment.getExternalStorageDirectory(), "patch.jar");
Log.e("file:", "" + patch.exists());
myApplication.fixBugManage.addPatch(patch.getAbsolutePath());
}
});
findViewById(R.id.button2).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
//修複此位置的bug
Toast.makeText(MainActivity.this, "...bug...",
Toast.LENGTH_SHORT).show();
}
});
}
}
以上是手機上安裝的apk存在bug的位置。
接下,是修複完成Bug後的類:
......
......
findViewById(R.id.button2).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
Toast.makeText(MainActivity.this, "fix...bug...",
Toast.LENGTH_SHORT).show();
}
});
......
......
修複好了,build工程(工具也會自動build),把java編譯成class檔案。
由于我使用debug模式調試安裝的apk,是以我build完成後找的也是debug下的class檔案,如下:

紅色框标注的部分就是MainActivity build完成後生成class檔案,如果你想問我怎麼是三個class檔案,原因是:onCreate中有2個注冊的點選時間監聽器,每個監聽器都生成了一個新的class檔案。
把相關修複bug的類的class檔案複制到一個檔案下(fixbugdex),目前我也儲存了class所在的包。如下:
以下,拷貝方式不可取:
如果在工具中看class檔案,效果如下:
這裡看到的僅有一個MainActivity.class,切記,這是工具為了友善你檢視class檔案,顯示上做了處理。不能從此位置單獨一個個class檔案拷貝,例如從此位置單獨拷貝出MainActivity.class,這個class就不是完成的類了,檔案内容如下:
是以此方式不可取,當然可以直接拷貝整個包。
然後cmd到剛才的fixbugdex檔案
然後執行指令 jar cvf fixbug.jar cn/*
這條指令就是把cn下的所有檔案打包到fixbug.jar檔案中
執行完成後:
檢視fixbug.jar内容:
接下,需要把jar檔案轉換成dex檔案:
工具:dx
下載下傳工具并解壓到AndroidSdk–>platform-tools目錄
同時,也可以把fixbug.jar檔案拷貝到AndroidSdk–>platform-tools目錄,然後你也可以使用絕對路徑.
cmd到AndroidSdk–>platform-tools目錄下執行指令:
注:–output 後面可以接絕對路徑。
執行完成後的結果:
檢視一下patch.jar檔案的内容:
打開應用執行Toast按鈕:
我為了測試友善,把patch.jar檔案放到了sdcard根目錄中,當然也可以選擇網上下載下傳的方式,其實都是一樣的。
啟動後效果如下:
然後,點選加載更新檔檔案,并推出應用重新進入,并點選Toast按鈕:
到此,bug已經被修複完成。
Api接口介紹:
首先,把FixBugManage.java檔案引入到你的項目中
首先,在自定義Application中初始化:
init(versionCode);
當versionCode與之前的versionCode不同,會自動清除掉之前addPatch所有的更新檔檔案
當versionCode與之前的versionCode相同,會自動加載之前addPatch所有的更新檔檔案
package cn.coolspan.open.fixbug;
import android.app.Application;
public class MyApplication extends Application {
public FixBugManage fixBugManage;
@Override
public void onCreate() {
super.onCreate();
this.fixBugManage = new FixBugManage(this);
this.fixBugManage.init("1.0");
}
}
添加新更新檔檔案接口:
addPatch(patchPath);
MyApplication myApplication = (MyApplication) getApplication();
File patch = new File(
Environment.getExternalStorageDirectory(), "patch.jar");
myApplication.fixBugManage.addPatch(patch.getAbsolutePath());
清除所有更新檔檔案的接口:
到此接口已介紹完。
中途遇到的坑:
執行
jar cvf fixbug.jar cn/*
異常:bad class file magic (cafebabe) or version (0033.0000)
解決方法:在build.gradle中jdk的版本修改為1.6
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_6
targetCompatibility JavaVersion.VERSION_1_6
}
FixBugManage源碼分析:
package cn.coolspan.open.fixbug;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.math.BigInteger;
import java.security.MessageDigest;
import dalvik.system.DexClassLoader;
import dalvik.system.PathClassLoader;
import android.content.Context;
import android.content.SharedPreferences;
/**
* FixBugManage 2015-12-22 下午9:59:28
*
* @author 喬曉松 [email protected]
*/
public class FixBugManage {
private Context context;
private static final int BUF_SIZE = ;
private File patchs;
private File patchsOptFile;
public FixBugManage(Context context) {
this.context = context;
this.patchs = new File(this.context.getFilesDir(), "patchs");// 存放更新檔檔案
this.patchsOptFile = new File(this.context.getFilesDir(), "patchsopt");// 存放預處理更新檔檔案壓縮處理後的dex檔案
}
/**
* 初始化版本号
*
* @param versionCode
*/
public void init(String versionCode) {
SharedPreferences sharedPreferences = this.context
.getSharedPreferences("fixbug", Context.MODE_PRIVATE);
String oldVersionCode = sharedPreferences
.getString("versionCode", null);
if (oldVersionCode == null
|| !oldVersionCode.equalsIgnoreCase(versionCode)) {
this.initPatchsDir();// 初始化更新檔檔案目錄
this.clearPaths();// 清楚所有的更新檔檔案
sharedPreferences.edit().clear().putString("versionCode", versionCode)
.commit();// 存儲版本号
} else {
this.loadPatchs();// 加載已經添加的更新檔檔案(.jar)
}
}
/**
* 讀取更新檔檔案夾并加載
*/
private void loadPatchs() {
if (patchs.exists() && patchs.isDirectory()) {// 判斷檔案是否存在并判斷是否是檔案夾
File patchFiles[] = patchs.listFiles();// 擷取檔案夾下的所有的檔案
for (int i = ; i < patchFiles.length; i++) {
if (patchFiles[i].getName().lastIndexOf(".jar") == patchFiles[i]
.getName().length() - ) {// 僅處理.jar檔案
this.loadPatch(patchFiles[i].getAbsolutePath());// 加載jar檔案
}
}
} else {
this.initPatchsDir();
}
}
/**
* 加載單個更新檔檔案
*
* @param patchPath
*/
private void loadPatch(String patchPath) {
try {
injectDexAtFirst(patchPath, patchsOptFile.getAbsolutePath());// 讀取jar檔案中dex内容
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* patch所在檔案目錄
*
* @param patchPath
*/
public void addPatch(String patchPath) {
File inFile = new File(patchPath);
File outFile = new File(patchs, inFile.getName());
this.copyFile(outFile, inFile);
this.loadPatch(patchPath);
}
/**
* 移除所有的patch檔案
*/
public void removeAllPatch() {
this.clearPaths();
}
/**
* 清除所有的更新檔檔案
*/
private void clearPaths() {
if (patchs.exists() && patchs.isDirectory()) {
File patchFiles[] = patchs.listFiles();
for (int i = ; i < patchFiles.length; i++) {
if (patchFiles[i].getName().lastIndexOf(".jar") == patchFiles[i]
.getName().length() - ) {
patchFiles[i].delete();
}
}
}
}
/**
* 初始化存放更新檔的檔案目錄
*/
private void initPatchsDir() {
if (!this.patchs.exists()) {
this.patchs.mkdirs();
}
if (!this.patchsOptFile.exists()) {
this.patchsOptFile.mkdirs();
}
}
/**
* 複制檔案從inFile到outFile
*
* @param outFile
* @param inFile
* @return
*/
private boolean copyFile(File outFile, File inFile) {
BufferedInputStream bis = null;
OutputStream dexWriter = null;
try {
MessageDigest digests = MessageDigest.getInstance("MD5");
bis = new BufferedInputStream(new FileInputStream(inFile));
dexWriter = new BufferedOutputStream(new FileOutputStream(outFile));
byte[] buf = new byte[BUF_SIZE];
int len;
while ((len = bis.read(buf, , BUF_SIZE)) > ) {
digests.update(buf, , len);
dexWriter.write(buf, , len);
}
dexWriter.close();
bis.close();
BigInteger bi = new BigInteger(, digests.digest());
String result = bi.toString();
File toFile = new File(outFile.getParentFile(), result + ".jar");
outFile.renameTo(toFile);
return true;
} catch (Exception e) {
if (dexWriter != null) {
try {
dexWriter.close();
} catch (IOException ioe) {
ioe.printStackTrace();
}
}
if (bis != null) {
try {
bis.close();
} catch (IOException ioe) {
ioe.printStackTrace();
}
}
return false;
}
}
public static void injectDexAtFirst(String dexPath, String defaultDexOptPath)
throws NoSuchFieldException, IllegalAccessException,
ClassNotFoundException {
DexClassLoader dexClassLoader = new DexClassLoader(dexPath,
defaultDexOptPath, dexPath, getPathClassLoader());// 把dexPath檔案更新檔處理後放入到defaultDexOptPath目錄中
Object baseDexElements = getDexElements(getPathList(getPathClassLoader()));// 擷取當面應用Dex的内容
Object newDexElements = getDexElements(getPathList(dexClassLoader));// 擷取更新檔檔案Dex的内容
Object allDexElements = combineArray(newDexElements, baseDexElements);// 把目前apk的dex和更新檔檔案的dex進行合并
Object pathList = getPathList(getPathClassLoader());// 擷取目前的patchList對象
setField(pathList, pathList.getClass(), "dexElements", allDexElements);// 利用反射設定對象的值
}
private static PathClassLoader getPathClassLoader() {
PathClassLoader pathClassLoader = (PathClassLoader) FixBugManage.class
.getClassLoader();// 擷取類加載器
return pathClassLoader;
}
private static Object getDexElements(Object paramObject)
throws IllegalArgumentException, NoSuchFieldException,
IllegalAccessException {
return getField(paramObject, paramObject.getClass(), "dexElements");// 利用反射擷取到dexElements屬性
}
private static Object getPathList(Object baseDexClassLoader)
throws IllegalArgumentException, NoSuchFieldException,
IllegalAccessException, ClassNotFoundException {
return getField(baseDexClassLoader,
Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");// 利用反射擷取到pathList屬性
}
/**
* 此方法是合并2個數組,把更新檔dex中的内容放到數組最前,達到修複bug的目的
*
* @param firstArray
* @param secondArray
* @return
*/
private static Object combineArray(Object firstArray, Object secondArray) {
Class<?> localClass = firstArray.getClass().getComponentType();
int firstArrayLength = Array.getLength(firstArray);
int allLength = firstArrayLength + Array.getLength(secondArray);
Object result = Array.newInstance(localClass, allLength);
for (int k = ; k < allLength; ++k) {
if (k < firstArrayLength) {
Array.set(result, k, Array.get(firstArray, k));
} else {
Array.set(result, k,
Array.get(secondArray, k - firstArrayLength));
}
}
return result;
}
public 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);// 擷取值
}
public 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);// 設定值
}
}
如有bug或者不足,可以即時告知我,我會即時修改。