前面幾篇博文已經介紹了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目錄下執行命令:
dx --dex --output patch.jar fixbug.jar
注:–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());
清除所有補丁文件的接口:
removeAllPatch();
到此接口已介紹完。
中途遇到的坑:
執行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 = 2048;
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 = 0; i < patchFiles.length; i++) {
if (patchFiles[i].getName().lastIndexOf(".jar") == patchFiles[i]
.getName().length() - 4) {// 僅處理.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 = 0; i < patchFiles.length; i++) {
if (patchFiles[i].getName().lastIndexOf(".jar") == patchFiles[i]
.getName().length() - 4) {
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, 0, BUF_SIZE)) > 0) {
digests.update(buf, 0, len);
dexWriter.write(buf, 0, len);
}
dexWriter.close();
bis.close();
BigInteger bi = new BigInteger(1, digests.digest());
String result = bi.toString(16);
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 = 0; 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或者不足,可以即時告知我,我會即時修改。