热修复 笔记 第二部分 实战篇

Posted xzj_2013

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了热修复 笔记 第二部分 实战篇相关的知识,希望对你有一定的参考价值。

热修复 实战

上一篇我们总结了下 热修复的实现思路:

  • 获取当前程序的PathClassLoader
  • 反射获取DexPathClassLoader的pathList属性
  • 反射获取pathList中的属性dexElements
  • 把自己的补丁包 patch.dex 转化为 Elements[]数组 pathElements
  • 将pathElements 插入到 dexElements最前面,得到新的 newElements
  • 将newElements反射替换原来的dexElements

那么现在我们就按照这个思路写代码去实现热修复的功能:

第一步:实现修复的代码
根据思路

  1. 我们首先需要获取的是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;

  1. 反射获取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的实例;

  1. 反射获取pathList中的属性dexElements
    从源码中我们知道dexElements是pathList的一个属性,我们可以通过类似pathList的获取方式,去获取到dexElements的实例:
 Field dexElementsField =  SharedReflectUtils.getField(pathListObj,"dexElements");
 Object[] olddexElementsObj = (Object[]) dexElementsField.get(pathListObj);
  1. 把自己的补丁包 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[];
    
  1. 将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的插入;

  1. 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的错误
    这个是什么原因呢?
    留待我们下一篇解决;

以上是关于热修复 笔记 第二部分 实战篇的主要内容,如果未能解决你的问题,请参考以下文章

热修复 笔记 第三部分 优化篇

简明 JavaScript 函数式编程——入门篇

群集架构篇

[redis读书笔记] 第二部分 集群

[redis读书笔记] 第二部分 单机数据库

福利丨教学视频分享:机器学习实战-KNN-第二部分