热修复分为两种:
- 阿里系:从底层C的二进制来入手的。
- 腾讯系:从Java类加载机制来入手的。
什么是热修复?
一般的bug修复,都是等下一个版本解决,然后发布新的apk。
热修复:可以直接在客户已经安装的app当中修复bug。
本文采用Java类加载机制来实现热修复
实现原理:Android的类加载器在加载一个类时会先从自身的DexPathList对象种的Element数组种获取(Element[]dexElements)对应的类。在循环中,首先遍历出来的是dex文件,然后再从dex文件中获取class。因此,我们只要让修复好的class打包成一个dex文件,放在Element数组的第一个元素,这样就能保证获取到的class是最新修复好的class了(当然,由bug的class也是存在的,不过是放在Elements数组后面,没机会被拿到)
实现步骤:
1.修复好有问题额Java文件
2.将修复好的java文件编译成class文件:可以使用Android studio的rebuild project功能将文件进行编译,然后从build目录下找到对应的class文件
3.使用dex指令打包成dex文件
dx --dex --output=c:\Users\dell\Desktop\dex\classes2.dex c:\Users\dell\Desktop\dex
命令解释:
--output=c:\Users\dell\Desktop\dex\classes2.dex 指定输出路径
c:\Users\dell\Desktop\dex 最后指定去打包哪个目录下面的class字节文件(注意要包括全路径的文件夹否则会报错,也可以有多个class)
4.将打包成的dex文件放在对应的目录下(位置一定要正确)
准备工作:
1.首先我们是要用到一个工具,这个工具就是multidex,这个工具的作用就是每个类生成单独的.class字节码文件
使用multidex就需要先配置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中使用
package com.dex.main;
import com.dn.fixutils.FixDexUtils;
import android.app.Application;
import android.content.Context;
import android.support.multidex.MultiDex;
import dalvik.system.DexClassLoader;
import dalvik.system.PathClassLoader;
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);
}
}
记得在AndroidManifest里面的application标签中引入Application
<application
···
android:name=".MyApplition"
··· >
···
</application>
代码实现:
MainActivity:
package com.dex.main;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import com.dn.fixutils.FixDexUtils;
import com.dn.fixutils.MyConstants;
import com.dn.test.MyTestClass;
import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.os.Environment;
import android.util.Log;
import android.view.View;
import android.widget.Toast;
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void test(View v){
MyTestClass myTestClass = new MyTestClass();
myTestClass.testFix(this);
}
public void fix(View v){
fixBug();
}
private void fixBug() {//注意文件存放的路径
//目录:/data/data/packageName/odex
File fileDir = getDir(MyConstants.DEX_DIR,Context.MODE_PRIVATE);
System.out.println(fileDir.getName());
//往该目录下面放置我们修复好的dex文件。
String name = "classes2.dex";
String filePath = fileDir.getAbsolutePath()+File.separator+name;
//Toast.makeText(this ,filePath, Toast.LENGTH_SHORT).show();
File file= new File(filePath);
if(file.exists()){
file.delete();
}
//搬家:把下载好的在SD卡里面的修复了的classes2.dex搬到应用目录filePath
InputStream is = null;
FileOutputStream os = null;
String filename=Environment.getExternalStorageDirectory().getAbsolutePath()+File.separator+name;
Toast.makeText(this ,filename, Toast.LENGTH_SHORT).show();
try {
is = new FileInputStream(filename);
// System.out.println("==========="+filename);
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);
Toast.makeText(this ,f.exists()+"", Toast.LENGTH_SHORT).show();
if(f.exists()){
Toast.makeText(this ,"dex 重写成功", Toast.LENGTH_SHORT).show();
}
//热修复
FixDexUtils.loadFixedDex(this);
} catch (Exception e) {
e.printStackTrace();
}
}
}
MyTestClass:
package com.dn.test;
import android.content.Context;
import android.widget.Toast;
public class MyTestClass {
public void testFix(Context context){
int i = 10;
int a = 0;
Toast.makeText(context, "shit:"+i/a, Toast.LENGTH_SHORT).show();
}
}
FixDexUtils(热修复工具类,若不想深究,直接用该类即可):
package com.dn.fixutils;
import java.io.File;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashSet;
import android.content.Context;
import android.os.Build;
import android.util.Log;
import dalvik.system.DexClassLoader;
import dalvik.system.PathClassLoader;
public class FixDexUtils {
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);
}
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);
}
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();
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);
Object pathDexElementsList = getDexElements(pathObj);
//合并完成
Object dexElements = combineArray(mDexElementsList,pathDexElementsList);
//重写给PathList里面的lement[] dexElements;赋值
Object pathList = getPathList(pathLoader);
setField(pathList,pathList.getClass(),"dexElements",dexElements);
}
} catch (Exception e) {
e.printStackTrace();
}
}
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);
}
private static Object getPathList(Object baseDexClassLoader) throws Exception {
return getField(baseDexClassLoader,Class.forName("dalvik.system.BaseDexClassLoader"),"pathList");
}
private static Object getDexElements(Object obj) throws Exception {
return getField(obj,obj.getClass(),"dexElements");
}
/**
* 两个数组合并
* @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;
}
}
MyConstants:
package com.dn.fixutils;
public class MyConstants {
public static final String DEX_DIR = "odex";
}