热修复 笔记 第二部分 实战篇
Posted xzj_2013
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了热修复 笔记 第二部分 实战篇相关的知识,希望对你有一定的参考价值。
热修复 实战
上一篇我们总结了下 热修复的实现思路:
- 获取当前程序的PathClassLoader
- 反射获取DexPathClassLoader的pathList属性
- 反射获取pathList中的属性dexElements
- 把自己的补丁包 patch.dex 转化为 Elements[]数组 pathElements
- 将pathElements 插入到 dexElements最前面,得到新的 newElements
- 将newElements反射替换原来的dexElements
那么现在我们就按照这个思路写代码去实现热修复的功能:
第一步:实现修复的代码
根据思路
- 我们首先需要获取的是ClassLoader;
ClassLoader classLoader = ctx.getClassLoader();
Log.d("Eric","classloader is "+classLoader);
就这么简单的一行代码 就获取到了加载类的ClassLoader,
再看看输出
2019-09-28 22:09:23.177 19705-19705/recycle.shaw.com.testapplication D/Eric: classloader is dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/recycle.shaw.com.testapplication-2/base.apk", zip file "/data/app/recycle.shaw.com.testapplication-2/split_lib_slice_7_apk.apk"],nativeLibraryDirectories=[/data/app/recycle.shaw.com.testapplication-2/lib/x86, /system/lib, /vendor/lib]]]
我们发现它就是一个PathClassLoader;
- 反射获取DexPathClassLoader的pathList属性
从思路上看我们会多次调用反射去获取私有属性和获取方法
那我们将反射的这些调用封装下
public class SharedReflectUtils
private static final String TAG = "SharedReflectUtils";
/**
*
* @param ins 对象
* @param name 属性名
* @return Field
* @throws
*/
public static Field getField(Object ins,String name) throws NoSuchFieldException
//遍历所有的父类(因为getDeclaredField只能获取自己的属性和方法,getField只能获取自己以及父类的所以公开的方法,所以需要变量)
for (Class<?> cls = ins.getClass();cls!=null;cls = cls.getSuperclass())
Log.d("Eric","getClass is "+cls.getName());
try
Field field = cls.getDeclaredField(name);
if (field!=null)
Log.d("Eric","field is "+field.getName());
field.setAccessible(true);
return field;
catch (NoSuchFieldException e)
e.printStackTrace();
throw new NoSuchFieldException("not find "+ name+" Field ");
/**
*
* @param ins 对象
* @param name 方法名
* @param paramsType 参数类型
* @return Method
* @throws
*/
public static Method getMethod(Object ins,String name,Class<?>... paramsType) throws NoSuchMethodException
//必须传入参数,因为方法可能会被重载
for (Class<?> cls = ins.getClass();cls!=null;cls = cls.getSuperclass())
try
Method method = cls.getDeclaredMethod(name,paramsType);
if (method!=null)
method.setAccessible(true);
return method;
catch (NoSuchMethodException e)
e.printStackTrace();
throw new NoSuchMethodException("not found "+name + " Method");
看了源码 我们知道pathList是PathClassLoader的父类的一个私有属性
那么通过反射:
Field pathListField = SharedReflectUtils.getField(classLoader,"pathList");
Object pathListObj = pathListField.get(classLoader);
这样我们就能得到pathList的实例;
- 反射获取pathList中的属性dexElements
从源码中我们知道dexElements是pathList的一个属性,我们可以通过类似pathList的获取方式,去获取到dexElements的实例:
Field dexElementsField = SharedReflectUtils.getField(pathListObj,"dexElements");
Object[] olddexElementsObj = (Object[]) dexElementsField.get(pathListObj);
- 把自己的补丁包 patch.dex 转化为 Elements[]数组 pathElements
那这个怎么将dex转化为 Elements[]呢?
这个我们需要跟踪源码中dexElements的初始化,在PathList的构造方法中我们就可以知道是通过PathList内的一个makePathElements的方法实现的
那同理我们也可以通过反射调用这个方法实现将我们的Dex转化为 Elements[]数组
Method makeDexElementsMethod = SharedReflectUtils.getMethod(o,"makePathElements", List.class, File.class,List.class);
Object[] newDexElements = (Object[]) makeDexElementsMethod.invoke(o,dexList,cachePath,exceptionList);
因为dexElements本质上是一个数组,所以我们可以在获取的时候转换为Object[];
但是我们在android其他版本上运行后就会抛出一个异常NoSuchMethodException
这是为什么呢?
因为不同的版本,实现这个转换的方法不一样,这就需要我们去适配
所以具体的实现我们根据不同版本的源码应该这样实现:
public static Object[] getPatchElement(Context ctx,Object o,String path,ClassLoader classLoader) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException
ArrayList dexList = new ArrayList();
dexList.add(new File(path));
File cachePath = ctx.getCacheDir();
ArrayList<IOException> exceptionList = new ArrayList<IOException>();
if(Build.VERSION.SDK_INT >=Build.VERSION_CODES.N)
//Android 7.0+
Method makeDexElementsMethod = SharedReflectUtils.getMethod(o,"makeDexElements", List.class, File.class,List.class,ClassLoader.class);
return (Object[]) makeDexElementsMethod.invoke(o,dexList,cachePath,exceptionList,classLoader);
else if (Build.VERSION.SDK_INT >=Build.VERSION_CODES.M)
Method makeDexElementsMethod = SharedReflectUtils.getMethod(o,"makePathElements", List.class, File.class,List.class);
return (Object[]) makeDexElementsMethod.invoke(o,dexList,cachePath,exceptionList);
else if(Build.VERSION.SDK_INT <Build.VERSION_CODES.M && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
//android 4.4+ -6.0之间(不包括右边界)
Method makeDexElementsMethod = SharedReflectUtils.getMethod(o,"makeDexElements", ArrayList.class, File.class,ArrayList.class);
return (Object[]) makeDexElementsMethod.invoke(o,dexList,cachePath,exceptionList);
else if(Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT )
Log.d("Eric",“暂不提供4.4以下的版本支持”);
return new Object[];
-
将pathElements 插入到 dexElements最前面,得到新的 newElements
这一步我们需要把3和4得到的两个Object进行合并,得到一个新的dexElements;首先我们new一个新的Element数组,
然后通过System.arraycopy先加入patch.dex转换的新newDexElements 加入,这样能确保我们修复的dex在这个数组的最前面
在System.arraycopy原有的DexElements;
Object tt = Array.newInstance(olddexElementsObj.getClass().getComponentType(),olddexElementsObj.length+newdexElementsObj.length);
System.arraycopy(newdexElementsObj,0,tt,0,newdexElementsObj.length);
System.arraycopy(olddexElementsObj,0,tt,newdexElementsObj.length,olddexElementsObj.length);
这样我们就实现了Elements的插入;
- 将newElements反射替换原来的dexElements
通过反射调用属性的set方法,我们就实现了替换的功能
dexElementsField.set(pathListObj,tt);
这样我们就完成了整个修复的功能,那么怎么验证是否是正确的呢?
整体的代码如下:
public static void install(Context ctx,String dexPath)
ClassLoader classLoader = ctx.getClassLoader();
Log.d("Eric","classloader is "+classLoader);
try
Field pathListField = SharedReflectUtils.getField(classLoader,"pathList");
Object pathListObj = pathListField.get(classLoader);
Field dexElementsField = SharedReflectUtils.getField(pathListObj,"dexElements");
Object[] olddexElementsObj = (Object[]) dexElementsField.get(pathListObj);
Object[] newdexElementsObj = getPatchElement(ctx,pathListObj,dexPath,classLoader);
Object tt = Array.newInstance(olddexElementsObj.getClass().getComponentType(),olddexElementsObj.length+newdexElementsObj.length);
System.arraycopy(newdexElementsObj,0,tt,0,newdexElementsObj.length);
System.arraycopy(olddexElementsObj,0,tt,newdexElementsObj.length,olddexElementsObj.length);
dexElementsField.set(pathListObj,tt);
catch (NoSuchFieldException e)
e.printStackTrace();
catch (IllegalAccessException e)
e.printStackTrace();
catch (NoSuchMethodException e)
e.printStackTrace();
catch (InvocationTargetException e)
e.printStackTrace();
我们只需要在Application中调用修复即可
public class MvApplcation extends Application
public static ClassLoader appLoad;
@Override
public void onCreate()
super.onCreate();
@Override
protected void attachBaseContext(Context base)
super.attachBaseContext(base);
FixUtil.install(this,"mnt/sdcard/patch.jar");
为什么是在Application中,因为我们无法预知具体哪一个类出现了bug,我们只能在程序最开始就进行替换,而程序一运行最先执行的就是Application的attachBaseContext方法
这也是为什么Application是不能进行热修复的原因
于是我们进入下一步验证
第二步:验证修复功能是否正确
- 既然要修复问题,那么首先我们需要制造一个Bug
public class Utils
public static int log()
Log.d("Eric","错误的代码");
//人为制造一个有bug的代码
return 1/0;
然后我们在MainActivity中调用这个类的log()方法
public class MainActivity extends AppCompatActivity
@Override
protected void onCreate(Bundle savedInstanceState)
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Utils.log();
很显然程序会崩溃,且日志会输出如下异常
Process: recycle.shaw.com.testapplication, PID: 16008
java.lang.RuntimeException: Unable to start activity ComponentInforecycle.shaw.com.testapplication/recycle.shaw.com.testapplication.MainActivity: java.lang.ArithmeticException: divide by zero
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2665)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2726)
at android.app.ActivityThread.-wrap12(ActivityThread.java)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1477)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:154)
at android.app.ActivityThread.main(ActivityThread.java:6119)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:886)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:776)
Caused by: java.lang.ArithmeticException: divide by zero
at recycle.shaw.com.testapplication.Utils.log(Utils.java:10)
at recycle.shaw.com.testapplication.MainActivity.onCreate(MainActivity.java:16)
at android.app.Activity.performCreate(Activity.java:6679)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1118)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2618)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2726)
at android.app.ActivityThread.-wrap12(ActivityThread.java)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1477)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:154)
at android.app.ActivityThread.main(ActivityThread.java:6119)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:886)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:776)
既然异常有了那么我们就需要进入下一阶段
- 输出补丁包
修改有bug的类
public class Utils
public static int log()
// Log.d("Eric","错误的代码");
Log.d("Eric","FIX BUG");
return 1/1;
修改完成后,我们需要对这个Module进行编译:
编译后的class会在如下目录下 当然不同的AS版本编译生成的class目录会不同,但一定是在app\\build\\intermediates目录,具体名字就需要你们自己去找了,我的As编译后的结果如图
点击Utils.class我们也可以看到我们编译出来的修改后的代码
怎么输出补丁包呢?
android提供的一个工具dx.bat用于实现打包,As的打包也是通过这个工具实现的,不同的是As是通过代码实现自动化打包,而现在我们使用是手动调用
这个工具是在我们的sdk中\\build-tools\\29.0.0下任意一个SDK版本下都有这个dx.bat工具
使用如下命令就会生成一个文件patch.dex:
注意:打包的时候,最好打包的class目录是从工程的根目录开始,也就是说这个目录结构要包括根目录项我的工程是recycle.shaw.com.testapplication,那么我打包的时候class文件的路径就是从recycle这个目录开始一直到需要打包的class文件,而且我也推荐这种方式。至于可不可以从recycle的上级一起打包,我没有测试过,但是一定要包含从根目录recycle到Utils.class中间的所有目录结果,不然打包的文件是有问题的;
至于多class打包,我们可以通过脚本 一个一个打入,也可以将所有的class放入一个jar内后在将该jar打入
既然修复的补丁包已经生成,那么我们进入下一阶段
- 将我们的补丁包push到上面代码中指定的目录mnt/sdcard/patch.jar
注意:这个目录和名字可以随便定,但是要匹配,同时还要注意目录的权限设置问题 - 至此整个修复就完成了,我们只需要重新启动应用,去看看程序是否正常运行即可验证我们的修复是否成功
显然到这里,是验证我们的修复是OK的;
但是这真的代表所以的设备都是OK的么?
其实不然,我们测试后很容易发现:
在Android4.4上会抛出一个异常
java.lang.IllegalAccessError:Class ref in pre-verified class resolved to unexpected implementation的错误
这个是什么原因呢?
留待我们下一篇解决;
以上是关于热修复 笔记 第二部分 实战篇的主要内容,如果未能解决你的问题,请参考以下文章