一、修复的工具
当前主要有两个主流的热修复工具:
1.阿里系:使用了DeXposed(修改了国外的),一年没有维护了,现在又搞了一个andfix,是一种黑客技术。自己去实现了底层的zyqote。从底层C的二进制来入手的。
2.腾讯系:tinker
Java类加载机制来入手的。
这里我们使用tinker。
二、热修复的原理(Java类加载机制)
什么是热修复?
一般的Bug修复,都是等下一个版本解决,然后发布新的apk
热修复:可以直接在客户已经安装的程序当中修复bug。
bug一般会出现在某个类的某个方法地方。如果我们能够动态的将客户手机里面的apk里面的某个类给替换成我们已经修复好的类。
AndroidStudio的Instant run,也是一种热修复或者增量更新的方式。如果只是改了一个类,那么它只会把修改的东西打进新的包里,放到手机上运行。
所以,在用AndroidStudio做热修复的时候记得把Instant run功能关闭。不然会影响热修复实现。
机制:dex分包。mutildex。
如何实现呢?实现的原理?
从Java的类加载机制来入手的:ClassLoader
Android 是如何加载class.dex文件,启动程序。
这里提供了两个类:
1.PathClassLoader :这个类用来加载应用程序的dex
public class PathClassLoader extends BaseDexClassLoader {
}
2.DexClassLoader :这个类可以加载指定的某个dex文件。(限制:必须要在应用程序的目录下面)
public class DexClassLoader extends BaseDexClassLoader {
}
修复方案:
1.搞多个dex
第一个版本:classes.dex。
修复后的补丁包:classes2.dex(包涵了我们修复xxx.class)
这种实现方式也可以用于插件开发。
2.把两个dex合并
将修复的class替换原来出bug的class.
通过BaseDexClassLoader调用findClass(className):
Class<?> findClass(String name)
实际上替换的是修复了的dex文件,这里面集成了修改了的class文件。Element[] dexElements;存储的是dex的集合。
在findClass方法中是通过循环去找dexElements中的类,如果找到了,就会返回这个Class,停止执行寻找。
所以可以采取以下方式:
将修复好的dex插入到dexElements的集合,位置:出现bug的xxx.class所在的dex的前面。
最本质的实现原理:类加载器去加载某个类的时候,是去dexElements里面从头往下查找的。
fixed.dex,classes1.dex,classes2.dex,classes3.dex
三、如何实现
上面已经介绍了其中的原理,接下来在开发中如何具体实现。
步骤
1.先安装一个带有bug的版本apk。
2.修复bug,重新打包成dex文件,放在后台服务器。
3.通过主动方式或者推送将修复的dex文件,放到手机中,进行dex文件的合并。
用AndroidStudio打包multidex(官方待验证)
准备工作
1.配置gradle文件:
1)
dependencies {
compile 'com.android.support:multidex:1.0.1'
}
2)
defaultConfig {
multiDexEnabled true
}
3)
buildTypes {
release {
multiDexKeepFile file('dex.keep')
def myFile = file('dex.keep')
println("isFileExists:"+myFile.exists())
println "dex keep"
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
}
}
2.在Application中的attachBaseContext方法加入MultiDex.install(base);
public class MyApplication extends Application{
@Override
public void onCreate() {
// TODO Auto-generated method stub
super.onCreate();
}
@Override
protected void attachBaseContext(Context base) {
// TODO Auto-generated method stub
MultiDex.install(base);
FixDexUtils.loadFixedDex(base);
super.attachBaseContext(base);
}
}
这样配置之后运行打包出来的apk中有两个dex文件:
代码处理
1.修复的dex文件必须要在应用的目录下面,所以第一步要将修复的文件移动在/data/data/目录下面
代码处理
1.修复的dex文件必须要在应用的目录下面,所以第一步要将修复的文件移动在/data/data/目录下面
public static final String DEX_DIR = "odex";
private void fixBug() {
//目录:/data/data/packageName/odex
File fileDir = getDir(MyConstants.DEX_DIR,Context.MODE_PRIVATE);
//往该目录下面放置我们修复好的dex文件。
String name = "classes2.dex";
String filePath = fileDir.getAbsolutePath()+File.separator+name;
File file= new File(filePath);
if(file.exists()){
file.delete();
}
//搬家:把下载好的在SD卡里面的修复了的classes2.dex搬到应用目录filePath
InputStream is = null;
FileOutputStream os = null;
try {
is = new FileInputStream(Environment.getExternalStorageDirectory().getAbsolutePath()+File.separator+name);
os = new FileOutputStream(filePath);
int len = 0;
byte[] buffer = new byte[1024];
while ((len=is.read(buffer))!=-1){
os.write(buffer,0,len);
}
File f = new File(filePath);
if(f.exists()){
Toast.makeText(this ,"dex 重写成功", Toast.LENGTH_SHORT).show();
}
//热修复
FixDexUtils.loadFixedDex(this);
} catch (Exception e) {
e.printStackTrace();
}
}
2.扫描dex文件目录下的所有dex,并用HashSet存储。
记得每次初始化工具类的时候清空HashSet的数据
private static HashSet<File> loadedDex = new HashSet<File>();
static{
loadedDex.clear();
}
扫描:
public static void loadFixedDex(Context context){
if(context == null){
return ;
}
//遍历所有的修复的dex
File fileDir = context.getDir(MyConstants.DEX_DIR,Context.MODE_PRIVATE);
File[] listFiles = fileDir.listFiles();
for(File file:listFiles){
if(file.getName().startsWith("classes")&&file.getName().endsWith(".dex")){
loadedDex.add(file);//存入集合
}
}
//dex合并之前的dex
doDexInject(context,fileDir,loadedDex);
}
3.获取需要修复的dex文件,并通过反射拿到对应的dex数组,然后一一合并,再通过反射的方式设置给应用的dex数组中。这样就实现了热修复。
private static void doDexInject(final Context appContext, File filesDir,HashSet<File> loadedDex) {
String optimizeDir = filesDir.getAbsolutePath()+File.separator+"opt_dex";
File fopt = new File(optimizeDir);
if(!fopt.exists()){
fopt.mkdirs();
}
//1.加载应用程序的dex
try {
PathClassLoader pathLoader = (PathClassLoader) appContext.getClassLoader(); //原来的dex文件加载路径
for (File dex : loadedDex) {
//2.加载指定的修复的dex文件。
DexClassLoader classLoader = new DexClassLoader(
dex.getAbsolutePath(),//String dexPath,
fopt.getAbsolutePath(),//String optimizedDirectory,
null,//String libraryPath,
pathLoader//ClassLoader parent
);
//3.合并
Object dexObj = getPathList(classLoader);
Object pathObj = getPathList(pathLoader);
Object mDexElementsList = getDexElements(dexObj); //需要修复的dex文件数组
Object pathDexElementsList = getDexElements(pathObj); //原来的dex文件数组
//合并完成
Object dexElements = combineArray(mDexElementsList,pathDexElementsList);
//重写给PathList里面的lement[] dexElements;赋值
Object pathList = getPathList(pathLoader);
setField(pathList,pathList.getClass(),"dexElements",dexElements);
}
} catch (Exception e) {
e.printStackTrace();
}
}
通过反射设置和获取值:
//通过反射获取baseDexClassLoader的pathList
private static Object getPathList(Object baseDexClassLoader) throws Exception {
return getField(baseDexClassLoader,Class.forName("dalvik.system.BaseDexClassLoader"),"pathList");
}
//通过反射获取dexElements
private static Object getDexElements(Object obj) throws Exception {
return getField(obj,obj.getClass(),"dexElements");
}
/**
* 获取某个对象中的属性
* obj:某个对象
* cl:对象的类
* field:属性值
**/
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);
}
/**
* 给某个对象中的添加属性值
* obj:某个对象
* cl:对象的类
* field:属性值
* value:具体值
**/
private static void setField(Object obj,Class<?> cl, String field, Object value) throws Exception {
Field localField = cl.getDeclaredField(field);
localField.setAccessible(true);
localField.set(obj,value);
}
合并数组:
/**
* 两个数组合并
* @param arrayLhs
* @param arrayRhs
* @return
*/
private static Object combineArray(Object arrayLhs, Object arrayRhs) {
Class<?> localClass = arrayLhs.getClass().getComponentType();
int i = Array.getLength(arrayLhs);
int j = i + Array.getLength(arrayRhs);
Object result = Array.newInstance(localClass, j);
for (int k = 0; k < j; ++k) {
if (k < i) {
Array.set(result, k, Array.get(arrayLhs, k));
} else {
Array.set(result, k, Array.get(arrayRhs, k - i));
}
}
return result;
}
// [12345] [9876]
// [9876 12345]
备注:
BaseDexClassLoader类中的
DexPathList pathList;
DexPathList类中的
Element[] dexElements;
源码链接:
http://androidxref.com/4.4.2_r1/xref/libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java#pathList
http://androidxref.com/4.4.2_r1/xref/libcore/dalvik/src/main/java/dalvik/system/DexPathList.java
提供参考源码解析文章阅读:
http://blog.csdn.net/ch15851302205/article/details/44671687
Element[] dexElements;原来的dex文件集合
Element[] dexElements2;合并以后的文件集合
四、测试
通过上面代码的处理,应用就可以实现热修复,那么该怎么生成修复的dex文件呢?
1.找到MyTestClass.class
fixdix_test\app\build\intermediates\bin\TestClass.class
2.配置dx.bat的环境变量
Android\sdk\build-tools\23.0.3\dx.bat
3.命令
dx --dex --output=D:\Users\song\Desktop\dex\classes2.dex D:\Users\ricky\Desktop\dex
命令解释:
–output=D:\Users\song\Desktop\dex\classes2.dex 指定输出路径
D:\Users\song\Desktop\dex 最后指定去打包哪个目录下面的class字节文件(注意要包括全路径的文件夹,也可以有多个class)