你值得知道的Android 热修复,以及热修复原理

一般产品上线周期比较长,而且如果不是强制更新,无法做到100%的用户都更新,如果上线之后,产品出现bug,那么怎么办?一般都是再发一个版本,或者等到下一个版本再解决。如果再发一个版本,显然是不靠谱的,用户安装也有厌倦的时候,说不定直接把app给卸载了,而且给用户的体验也不好。如果等到下一个版本再修复,那么用户每次使用都出现这个bug,显然是不能接受的,这都造成用户量流失,给我们用户造成不良的影响。

那么有没有一种办法是不需要发布版本,不需要用户安装,而且又能够及时觉得这个问题的呢?当然是有的,那就是使用热修复技术。

热修复技术一般来说有两种途径来解决:

1、阿里系的 AndFix,原理是从底层C的二进制来入手的。

2、腾讯系的 tinker,原理是Java类加载机制来入手的。 当然热修复技术不单单这两种,各大公司都开源自己的热修复技术,如滴滴的 VirtualAPK,美团的Robust等等。

既然有那么多的热修复项目开源,读者可以下载下来学习,看看实现的原理,本文主要是讲解从Java类加载器机制来讲解热修复。

首先我们理解DexClassLoader和PathClassLoader,他们都是用来加载应用程序的dex文件,但DexClassLoader是指可以加载指定的某个dex文件,而且具有限制性,那就是必须要在应用程序的目录下面的dex文件。

dexPath:包含classes和resources的jar/apk文件的路径; libraryPath:包含本地的目录列表,可以为null; parent:父类加载器。

dexPath:包含classes和resources的jar/apk文件的路径; optimizedDirectory:写入优化的dex文件的目录,一定不能是为空; libraryPath:包含本地的目录列表,可以为null; parent:父类加载器。

DexClassLoader和PathClassLoader类中都只有构造方法,而且构造方法都super到父类了。我们看下BaseDexClassLoader的类加载器的构造方法。

BaseDexClassLoader类中包括构造方法,findClass(找到类)、findResourece,findLibrary等等方法,这里主要看下构造方法。

public BaseDexClassLoader(String dexPath, File optimizedDirectory,
  String libraryPath, ClassLoader parent) {
            super(parent);
            this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);    
}
复制代码

主要这里new了一个DexPathList对象,并且把参数传过去了,DexPathList到底是干什么的呢?我们去探个究竟。

类的简介中我们知道,DexPathList其实就是与类加载器(ClassLoader)相关联的一对条目列表。而这些列表包含了dex、jar、zip和apk文件,而提高了使用列表来查找类和资源的方法。这里需要说一下的是平时我们看源码一定要主要看作者的注释,从注释里我们看知道类或者方法的功能。

从DexPathList源码中我们注意到一个属性就是dexElements

 /**
    * List of dex/resource (class path) elements.
    * Should be called pathElements, but the Facebook app uses reflection
    * to modify 'dexElements' (http://b/7726934).
    */
private final Element[] dexElements;
复制代码

这个就是存放dex/resource的集合,我们注意到该类的findClass方法:

findClass的时候就是遍历dexElements,取出DexFile,找到我们需要的类并返回。也就是说我们可以通过dexElements找到我们出bug的类,因为dexElements就是存放Element元素,而Element中包含DexFile,DexFile中可以找到bug类,找到bug类,然后我们替换掉bug类所在的dex文件就可以了。而且再dexElements介绍中,已经说明了可以通过反射获取得到dexElements。

那么我们思考一下:既然DexClassLoader和PathClassLoader都可以加载dex文件,那么我们能不能使用多个dex文件?也就是第一个版本使用的是classes.dex,修复后的补丁包classes2.dex,在补丁包中包涵了我们修复class文件。然后将这两个文件合并,将修复的class替换原来出bug的class,接下来通过反射拿到dexElements,将修复好的dex插入到dexElements的集合,插入的位置就是出现bug的class所在的dex的前面。这样子做最本质的实现原理就是:类加载器去加载某个类的时候,是去dexElements里面从头往下查找的。

需要多个dex的需要multidex支持。 1、再app的build.gradle中添加

defaultConfig {
        multiDexEnabled true
}

buildTypes {
        release {
            multiDexKeepFile file('dex.keep')
            println "dex keep"
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
        }
}

dependencies {
    compile 'com.android.support:multidex:1.0.1'
}
复制代码

2、在application的attachBaseContext添加MultiDex.install(base);

据上面的原理,下面是实现的热修复的代码

FixDexUtils:

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);
	}

    //使用合并的dexElements替换出错的dexElements
	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);
	}


//	fileDir = {File@15225} "/data/data/com.dex.main/app_odex"
//			0 = {File@15240} "/data/data/com.dex.main/app_odex/opt_dex"
//			1 = {File@15241} "/data/data/com.dex.main/app_odex/classes2.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();
		}
		try {
            //1.使用PathClassLoader加载应用程序的dex(系统的加载器)
			PathClassLoader pathLoader = (PathClassLoader) appContext.getClassLoader();

			for (File dex : loadedDex) {
				//2.通过DexClassLoader加载指定的修复的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");
	}

	/**
	 * 两个数组合并
     * 有bug的dex还是保留,只是把没bug的插入到有bug的前面即可
	 * @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);//新建一个数组,总长度为两个数组的总和,即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;//返回总的数组
	}

}
复制代码

MyApplication:

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);

	}
}
复制代码

MyConstants:

public class MyConstants {
	public static final String DEX_DIR = "odex";
}
复制代码

MyTestClass:

public class MyTestClass {
	public  void testFix(Context context){
		int i = 10;
		int a = 0;
		Toast.makeText(context, "shit:"+i/a, Toast.LENGTH_SHORT).show();
	}
}
复制代码

MainActivity:

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);
		//往该目录下面放置我们修复好的dex文件。
		String name = "classes2.dex";
		String filePath = fileDir.getAbsolutePath()+File.separator+name;
		File file= new File(filePath);
		if (fileDir.exists()) {
			if (file.exists()) {
				file.delete();
			}
		}
		//搬家:把下载好的在SD卡里面的修复了的classes2.dex搬到应用目录filePath
		InputStream is = null;
		FileOutputStream os = null;
		try {
			String dpath = Environment.getExternalStorageDirectory().getAbsolutePath()+File.separator;
			is = new FileInputStream(dpath+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();
		}


	}
}
复制代码

activity_main:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin">
	<Button 
	    android:layout_width="fill_parent"
	    android:layout_height="wrap_content"
	    android:text="test"
	    android:onClick="test"
	    android:layout_centerInParent="true"
	    android:id="@+id/btn_test"
	    />
	<Button 
	    android:layout_width="fill_parent"
	    android:layout_height="wrap_content"
	    android:text="fix"
	    android:onClick="fix"
	    android:layout_below="@id/btn_test"
	    />
</RelativeLayout>
复制代码
编译dex文件

1、修复好bug,然后rebuild一下,获取java文件对应的所有class文件(即找到修复好的java文件所对应的calss文件):app\build\intermediates\classes\debug\包路径\类名字,然后右键show in Explorer。把文件复制出来,复制的时候连同整个包名路径(即带包名的整个文件夹)都复制出来。(也可以找到class文件之后选择压缩,注意压缩时选择绝对路径,再解压可保留原有的包目录)

2、cmd到sdk的build-tools\sdk版本 目录下。

3、使用命令dx --dex --output=D:\Users\dex\classes2.dex D:\Users\dex 命令解释: --output=D:\Users\dex\classes2.dex 指定输出路径和文件名 D:\Users\dex` 最后指定去打包哪个目录下面的class字节文件(注意:要包括全路径的文件夹,也可以有多个class)

可以看到我这个dex文件夹下有包含全包名路径的文件夹,该文件夹里面有class文件。

当我们点击第一次进来点击test的时候,会闪退,第二次进来,我们点击fix时候,已经把bug修复好了,这时候就不会闪退了。

参考文章: 《Android4.4.2 DexClassLoader源码分析》

BaseDexClassLoader源码连接:BaseDexClassLoader

DexPathList源码连接:DexPathList

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值