前面几篇博文已经介绍了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
打开应用执行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或者不足,可以即时告知我,我会即时修改。