热修复的简单实现
热修复没你想的那么难,只要了解了热修复的原理,你也可以轻轻松松自己实现个简单的热修复demo。我们在实现热修复功能前,先要了解几个类的关系和作用,classLoader、BaseDexClassLoader、DexclassLoader、PathClassLoader、DexPahtList、Elements。我画了一张流程图阐述了它们的关系。
DexclassLoader和PathClassLoader都继承BaseDexClassLoader,而DexclassLoader是可以加载指定路径下的dex、jar、apk文件的,而PathClassLoader只能加载安装的apk里边的dex文件。BaseDexClassLoader里有个成员变量叫DexPathLIst,dex文件的加载都在这个类里,它有个方法叫findclass,看它的源码:(搜索源码可地址:http://androidxref.com/7.1.2_r36/),它会遍历Element数组去找到相应的class文件。
/**
402 * Finds the named class in one of the dex files pointed at by
403 * this instance. This will find the one in the earliest listed
404 * path element. If the class is found but has not yet been
405 * defined, then this method will define it in the defining
406 * context that this instance was constructed with.
407 *
408 * @param name of class to find
409 * @param suppressed exceptions encountered whilst finding the class
410 * @return the named class or {@code null} if the class is not
411 * found in any of the dex files
412 */
413 public Class findClass(String name, List<Throwable> suppressed) {
414 for (Element element : dexElements) {
415 DexFile dex = element.dexFile;
416
417 if (dex != null) {
418 Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
419 if (clazz != null) {
420 return clazz;
421 }
422 }
423 }
424 if (dexElementsSuppressedExceptions != null) {
425 suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
426 }
427 return null;
428 }
所以我们知道了两个很重要的类:private final DexPathList pathList; private Element[] dexElements; 贴出来方便我们反射用。
那么这个逻辑就很清晰了:PathClassLoader加载apk的内部的dex,它是通过DexPathList的findclass方法去遍历Element数组去加载dex。那么我们可以通过反射去给Element重新赋值得到一个包含外部和内部的新的Element数组,而外部Elements数组我们可以通过DexclassLoader得到。ok我们上代码:
首先我们创建一个DexclassLoader和PahtClassLoader,关于fixed.jar的生成可参考我的另一篇文章:跳转
public DexClassLoader getDexClassLoader(){
File path = new File(Environment.getExternalStorageDirectory().getAbsolutePath()+File.separator + "fixed.jar");
File dexOutputDir = this.getDir(".dex", 0);
DexClassLoader dexClassLoader = new DexClassLoader(path.getAbsolutePath(),dexOutputDir.getAbsolutePath(),null,getPathClassLoader());
return dexClassLoader;
}
public PathClassLoader getPathClassLoader(){
return (PathClassLoader) this.getClassLoader();
}
之后通过反射拿到它俩的成员变量pathList和pathList的成员变量dexElements
public void getPathAndDexElements() throws Exception{
DexClassLoader dexClassLoader = getDexClassLoader();
Object objDexCLassLoaderPathList = reflectPathList(dexClassLoader);
Object objDexCLassLoaderPathListElements = reflectDexElements(objDexCLassLoaderPathList);
if(objDexCLassLoaderPathListElements!=null){
//Toast.makeText(this,"得到了DexCLassLoaderPathListElements",Toast.LENGTH_LONG).show();
}
PathClassLoader pathClassLoader = getPathClassLoader();
Object objPathClassLoaderPathList = reflectPathList(pathClassLoader);
Object objPathClassLoaderPathListElements = reflectDexElements(objPathClassLoaderPathList);
if(objPathClassLoaderPathListElements!=null){
//Toast.makeText(this,"得到了objPathClassLoaderPathListElements",Toast.LENGTH_LONG).show();
}
Object objCombine = combineElements(objDexCLassLoaderPathListElements,objPathClassLoaderPathListElements);
fixComplete(objCombine);
}
public Object reflectPathList(Object obj) throws Exception{
return getField(obj,Class.forName("dalvik.system.BaseDexClassLoader"),"pathList");
}
public Object reflectDexElements(Object obj)throws Exception{
return getField(obj,obj.getClass(),"dexElements");
}
public Object getField(Object obj,Class<?> c ,String field)throws Exception{
Field f = c.getDeclaredField(field);
f.setAccessible(true);
return f.get(obj);
}
这样我们就得到了DexClassLoader和PathClassLoader的pathlist对象和pathlist中的dexElements对象,接下来我们需要对这两个Element进行合并得到一个新的Element并重新赋给PathClassLoader的Elements对象。
public Object combineElements(Object dexElementsObj,Object pathElementsObj) throws Exception{
Class<?> cla = pathElementsObj.getClass().getComponentType();
int dexElementsLength = Array.getLength(dexElementsObj);
int pathElementsLength = Array.getLength(pathElementsObj);
//Toast.makeText(this,"........."+dexElementsLength+"....."+pathElementsLength,Toast.LENGTH_LONG).show();
int newLength = dexElementsLength+pathElementsLength;
Object elements = Array.newInstance(cla,newLength);
for (int i=0;i<newLength;i++){
if (i < dexElementsLength) {
Array.set(elements, i, Array.get(dexElementsObj, i));
} else {
Array.set(elements, i, Array.get(pathElementsObj, i - dexElementsLength));
}
}
//Toast.makeText(this,"合并成功,新数组长度="+Array.getLength(elements),Toast.LENGTH_LONG).show();
return elements;
}
public void fixComplete(Object newElements)throws Exception{
PathClassLoader pathClassLoader = getPathClassLoader();
Object objPathClassLoaderPathList = reflectPathList(pathClassLoader);
Class cla = objPathClassLoaderPathList.getClass();
Field field = cla.getDeclaredField("dexElements");
field.setAccessible(true);
field.set(objPathClassLoaderPathList,newElements);
Toast.makeText(this,"修复完成",Toast.LENGTH_SHORT).show();
}
这样我们就完成了。完整代码如下:
Activity相关:
import android.os.Environment;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;
import com.example.lenovo.testwidget.bean.HotFixBean;
import com.example.lenovo.testwidget.bean.HotFixBean2;
import java.io.File;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import dalvik.system.DexClassLoader;
import dalvik.system.PathClassLoader;
public class HotFixActivity extends AppCompatActivity implements View.OnClickListener {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_hot_fix);
findViewById(R.id.bt_damaged).setOnClickListener(this);
findViewById(R.id.bt_damaged2).setOnClickListener(this);
findViewById(R.id.bt_fixgood).setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()){
case R.id.bt_damaged:
new HotFixBean(this);
break;
case R.id.bt_damaged2:
new HotFixBean2(this);
break;
case R.id.bt_fixgood:
try {
getPathAndDexElements();
} catch (Exception e) {
e.printStackTrace();
}
break;
}
}
public Object reflectPathList(Object obj) throws Exception{
return getField(obj,Class.forName("dalvik.system.BaseDexClassLoader"),"pathList");
}
public Object reflectDexElements(Object obj)throws Exception{
return getField(obj,obj.getClass(),"dexElements");
}
public Object getField(Object obj,Class<?> c ,String field)throws Exception{
Field f = c.getDeclaredField(field);
f.setAccessible(true);
return f.get(obj);
}
public DexClassLoader getDexClassLoader(){
File path = new File(Environment.getExternalStorageDirectory().getAbsolutePath()+File.separator + "fixed.jar");
File dexOutputDir = this.getDir(".dex", 0);
DexClassLoader dexClassLoader = new DexClassLoader(path.getAbsolutePath(),dexOutputDir.getAbsolutePath(),null,getPathClassLoader());
return dexClassLoader;
}
public PathClassLoader getPathClassLoader(){
return (PathClassLoader) this.getClassLoader();
}
public void getPathAndDexElements() throws Exception{
DexClassLoader dexClassLoader = getDexClassLoader();
Object objDexCLassLoaderPathList = reflectPathList(dexClassLoader);
Object objDexCLassLoaderPathListElements = reflectDexElements(objDexCLassLoaderPathList);
if(objDexCLassLoaderPathListElements!=null){
//Toast.makeText(this,"得到了DexCLassLoaderPathListElements",Toast.LENGTH_LONG).show();
}
PathClassLoader pathClassLoader = getPathClassLoader();
Object objPathClassLoaderPathList = reflectPathList(pathClassLoader);
Object objPathClassLoaderPathListElements = reflectDexElements(objPathClassLoaderPathList);
if(objPathClassLoaderPathListElements!=null){
//Toast.makeText(this,"得到了objPathClassLoaderPathListElements",Toast.LENGTH_LONG).show();
}
Object objCombine = combineElements(objDexCLassLoaderPathListElements,objPathClassLoaderPathListElements);
fixComplete(objCombine);
}
public Object combineElements(Object dexElementsObj,Object pathElementsObj) throws Exception{
Class<?> cla = pathElementsObj.getClass().getComponentType();
int dexElementsLength = Array.getLength(dexElementsObj);
int pathElementsLength = Array.getLength(pathElementsObj);
//Toast.makeText(this,"........."+dexElementsLength+"....."+pathElementsLength,Toast.LENGTH_LONG).show();
int newLength = dexElementsLength+pathElementsLength;
Object elements = Array.newInstance(cla,newLength);
for (int i=0;i<newLength;i++){
if (i < dexElementsLength) {
Array.set(elements, i, Array.get(dexElementsObj, i));
} else {
Array.set(elements, i, Array.get(pathElementsObj, i - dexElementsLength));
}
}
//Toast.makeText(this,"合并成功,新数组长度="+Array.getLength(elements),Toast.LENGTH_LONG).show();
return elements;
}
public void fixComplete(Object newElements)throws Exception{
PathClassLoader pathClassLoader = getPathClassLoader();
Object objPathClassLoaderPathList = reflectPathList(pathClassLoader);
Class cla = objPathClassLoaderPathList.getClass();
Field field = cla.getDeclaredField("dexElements");
field.setAccessible(true);
field.set(objPathClassLoaderPathList,newElements);
Toast.makeText(this,"修复完成",Toast.LENGTH_SHORT).show();
}
}
javabean相关,我直接修复了两个类
import android.content.Context;
import android.widget.Toast;
/**
* Created by lenovo on 2019/5/5.
*/
public class HotFixBean {
public HotFixBean(Context context){
int i = 1000;
int j = 0;
int k = i/j;
Toast.makeText(context,"HotFixBean结果为:"+k,Toast.LENGTH_SHORT).show();
}
}
import android.content.Context;
import android.widget.Toast;
/**
* Created by lenovo on 2019/5/5.
*/
public class HotFixBean2 {
public HotFixBean2(Context context){
int i = 10;
int j = 0;
int k = i/j;
Toast.makeText(context,"HotFixBean2结果为:"+k,Toast.LENGTH_SHORT).show();
}
}
xml代码:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:orientation="vertical"
tools:context="com.example.lenovo.testwidget.HotFixActivity">
<Button
android:textSize="20sp"
android:text="损坏的1"
android:id="@+id/bt_damaged"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<Button
android:textSize="20sp"
android:text="损坏的2"
android:id="@+id/bt_damaged2"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<Button
android:textSize="20sp"
android:layout_marginTop="100dp"
android:text="修复它"
android:id="@+id/bt_fixgood"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
别忘了权限,6.0要动态申请,我省事是在手机上手动开启的。
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
还有就是,要先点击修复才生效,因为类一旦被加载就更改不了了,正常情况是要把代码放在Application下的。
不要直接把项目run在手机上,要使用apk安装到手机上才能生效。