Android热修复原理-类加载机制与反射

1,热修复方案:

 (1) 阿里的AndFix 补丁方案,通过natvie层hook住带有bug的方法从而替换Java层的代码 。四五年没有维护了,应该是被弃用了,因为是通过C++代码来完成类的替换。实际就是反射拿到旧的类class,然后再把补丁包里面的class进行一个赋值,例如 old.class  = new.class. 是可以立即生效的。

(2) 美团的Robust 补丁方案 ,就是在编译打包的阶段对每个函数都加入修复的代码。有点类似代理,将方法执行的代码重定向到修复了的方法中去。(运用了字节码插桩技术)  是可以立即生效的。

(3) 腾讯的Tinker, 通过对比指定的base APK版本 和修复好了之后的 APK包中dex文件 , 补丁包就是两者的差分文件,patch.dex  , 再把patch.dex 和用户手机里的旧的classes.dex文件合成得到修复之后的dex文件,然后重启加载合成新的dex文件 (有点像增量更新)。

(4) 美团的Qzone超级补丁方案,基于dex分包方案,把BUG方法修复以后,放到一个单独的dex补丁文件,让程序运行期间加载dex补丁,执行修复后的方法。

2:类加载机制

  不管是哪一种热修复方案,都用到了类加载机制与反射。首先类加载机制的一个核心就是,双亲委托机制(双亲有歧义实际上我感觉应该叫父亲委托机制),意思是某个类加载器在加载类时,首先将加载任务委托给父 - 类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务或者没有父类加载器时,才自己去加载。我们还是用Android SDK 的源码来看一看,看一下ClassLoader.java里面的loadClass方法。

    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
            // First, check if the class has already been loaded
            // 首先判断这个类是否被加载过
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                try {
                    if (parent != null) {
                        // 这个parent实际上就是ClassLoader的一个成员变量,一般是构造子类时来给它赋值
                        c = parent.loadClass(name, false);
                    } else {
                       //如果子类没有给它赋值,直接调用BootClassLoader来找这个类
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    //如果父类加载器也找不到,通过自己来找
                    c = findClass(name);
                }
            }
            return c;
    }

我们主要看看这个findClass, 热修复我们一般拿到的类加载器是PathClassLoader。

 findClass这个函数在PathClassLoader 找不到,说明在它的父类里面BaseDexClassLoader里面。那么我再来看一看BaseDexClassLoader这个类里面的findClass方法。

可以看到这个成员变量pathList是一个对象,对应的类是DexPathList, findClass实际上是调用DexPathList里面的findClass,我们再继续看下源码。

 

DexPathList这个类的findClass方法,是通过遍历dexElements这个数组,调用数组里面的元素element的findClass方法。那么这个dexElements又是个什么东西呢?

就是DexPathList的一个成员变量 ,那么是在那里赋值的呢?继续找一下源码。

可以看到是通过maxDexElements这个函数来赋值的,首先注意第一个参数dexPath ,这个实际上就是传入的我们APK里面编译打包之后的Classes.dex 文件的路径,所以可以知道makeDexElements这个函数的功能就是把DexFile转化成Element数组的。(注意: 有的是版本是通过makePathElements这个函数来吧DexFile文件转化成数组的,不同的版本可能参数会有变化,我这个是Andoid9.0的源码)

可以看到这里面有一个for循环,遍历那个Dex File ,当我们APP的方法超过了65535个方法之后,我们一般都会进行分包,在代码里面build.gradle配置文件里面配置

android {
    defaultConfig {
        minSdkVersion 21 
        targetSdkVersion 26
        multiDexEnabled true   //启用分包方案
    }
}

然后在编译打包阶段,我们就能看到有classes.dex , classes2.dex 以及更多。 

3,热修复的流程。

通过上面一层一层的代码分析,我们可以知道,首先获得应用的PathClassLoader,这个活动很简单,我们只需要调用

context.getClassLoader(); 

拿到PathClassLoader之后,我们知道编译后的class文件是被打包成Dex文件的,而Dex文件又通过makeDexElements函数转化成Element数组,这个数组dexElements又是DexPathList的一个成员变量 , 而DexPathlist 又位于BaseDexClassLoader里面,也是它的一个成员变量,变量名是pathList ,而且还是一个私有的。所以我们热修复,就是通过反射拿到这个pathList , 然后把我们修复好的补丁包例如叫做patch.dex ,通过makeDexElement函数转化成数组 ,并且与旧的dexElements数组合并 ,最主要的是修复的数组应该放到前面,这样就会先加载到修复好的类。同时,我们应该把程序关掉重新加载,原因在于如下:

假如我们在线更新补丁包,但是之前有bug的类已经被加载过了,那么就只会走findLoadedClass,此时就不会走findClass这个方法, 因此需要重启一下应用。咱以前打王者荣耀,赛季更新完之后经常会关掉程序,打开才会生效,就是这个原因。

4,反射的过程

稍微用代码写写思路(伪代码)

	private void ReflectFix(Context context) throws NoSuchFieldException, IllegalAccessException {
		ClassLoader classLoader = context.getClassLoader(); //拿到当前应用的ClassLoader ,这种方式拿到的是PathClassLoader,
		Class<?> clazz =  classLoader.getClass().getSuperclass(); // 因为得到的是PathClassLoader,我们要拿到的是BaseDexClassLoader,这个类是它的父类,因此通过getSuperClass来获得
		Field dexElements = clazz.getDeclaredField("pathList"); //拿到dexElements这个数组,变量名是pathList,因为是私有的,通过getDeclaredField获得
		dexElements.setAccessible(true); // 禁用安全检查,因为它不是public而是私有的,直接调用会报IllegalAccessException这个异常 ,之后就可以使用get方法拿到对象了。
		Object dexPathList = dexElements.get(classLoader);
		Object[] objects = makePathElements(dexPathList, optimizedDirectory,suppressedExceptions);
		// 这个objects就是我们的Elements数组了,然后用补丁包里面的dex文件和app原本有的classes.dex文件 ,合并生成一个新的数组,赋值即可。
	}

后面的流程没有细写,就是通过System.arrayCopy将数组扩容并且进行赋值。

以上。。。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值