请选择 进入手机版 | 继续访问电脑版
仪陇社区论坛网 门户 电脑数码 查看内容

Android进阶:关于插件化和热修复梳理

2019-11-16 18:14| 发布者: 双周1城| 查看: 353| 评论: 0

摘要: 概述插件化和热修复,从技术实现的角度来说,原理想通。他们都是从系统加载器的角度出发,无论是采用hook方式,亦或是代理方式或者是其他底层实现,都是通过“欺骗”Android系统的方式来让宿主正常的加载和运行插件 ...

概述

插件化和热修复,从技术实现的角度来说,原理想通。他们都是从系统加载器的角度出发,无论是采用hook方式,亦或是代理方式或者是其他底层实现,都是通过“欺骗”Android 系统的方式来让宿主正常的加载和运行插件(补丁)中的内容;

插件化,更多是想把需要实现的模块或功能当做一个独立的提取出来,减少宿主的规模,当需要使用到相应的功能时再去加载相应的模块。

热修复,则往往是从修复bug的角度出发,强调的是在不需要二次安装应用的前提下修复已知的bug。

宿主: 就是当前运行的APP
插件: 相对于插件化技术来说,就是要加载运行的apk类文件
补丁: 相对于热修复技术来说,就是要加载运行的.patch,.dex,*.apk等一系列包含dex修复内容的文件。

Android进阶:关于插件化和热修复梳理

Android插件化和热修复的关系

了解插件化和热修复之前需要知道下面一些东西。

ClassLoader类加载器。每个java程序都是由class类组成的,只有把这些class类加载到JVM中,程序才能够运行。那么,用来加载这些类的就是ClassLoader类加载器。

关于ClassLoader和ClassLoader的双亲委托模式,可查看之前文章。

简述双亲委托模型

1. 当前ClassLoader首先从自己已经加载的类中查询是否此类已经加载,如果已经加载则直接返回原来已经加载的类。

每个类加载器都有自己的加载缓存,当一个类被加载了以后就会放入缓存,等下次加载的时候就可以直接返回了。

2. 当前classLoader的缓存中没有找到被加载的类的时候,委托父类加载器去加载,父类加载器采用同样的策略,首先查看自己的缓存,然后委托父类的父类去加载,一直到bootstrp ClassLoader.

3. 当所有的父类加载器都没有加载的时候,再由当前的类加载器加载,并将其放入它自己的缓存中,以便下次有加载请求的时候直接返回。

Android中有哪几种ClassLoader?

ClassLoader直接实现的子类有BaseDexClassLoader和SecureClassLoader。

BaseDexClassLoader的子类有如下几个:
PathClassLoader、DexClassLoader、InMemoryDexClassLoader

SecureClassLoader有一个子类:

URLClassLoader

各自作用:

InMemoryDexClassLoader:加载缓存中的dex文件或文件集。

PathClassLoader:只能加载已经安装到Android系统中的apk文件(/data/app目录),是Android默认使用的类加载器。

DexClassLoader:可以加载任意目录下的dex/jar/apk/zip文件。

URLClassLoader:从指向JAR文件和目录的URL的搜索路径加载类和资源。

热修复基本原理

常规的JVM类似,在Android中类的加载也是通过ClassLoader来完成。PathClassLoader和DexClassLoader都是继承自BaseDexClassLoader,我们可以看一下BaseDexClassLoader的构造函数。

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

这个构造函数只做了一件事,就是通过传递进来的相关参数,初始化了一个DexPathList对象。DexPathList的构造函数,就是将参数中传递进来的程序文件(就是补丁文件)封装成Element对象,并将这些对象添加到一个Element的数组集合dexElements中去

对于开发者来说,我们关注的重点应该是如何去找到需要加载的类

假设我们现在要去查找一个名为name的class,那么DexClassLoader将通过以下步骤实现:

  • 在DexClassLoader的findClass 方法中通过一个DexPathList对象findClass()方法来获取class
  • 在DexPathList的findClass 方法中,对之前构造好dexElements数组集合进行遍历,一旦找到类名与name相同的类时,就直接返回这个class,找不到则返回null。
  • 总的来说,通过DexClassLoader查找一个类,最终就是就是在一个数组中查找特定值的操作。

    综合以上所有的观点,我们很容易想到一种非常简单粗暴的热修复方案。

  • 在Application的onCreate()方法中获取应用本身的BaseDexClassLoader,然后通过反射得到对应的dexElements
  • 创建一个新的DexClassLoader实例,然后加载sdCard上的补丁包,然后通过同样的方法得到对应的dexElements
  • 将两个dexElements合并,然后再利用反射将合并后的dexElements赋值给应用本身的BaseDexClassLoader
  • 但是,Android虚拟机和常规的JVM 不同,加载的并不是.class而是dex(准确的来说是经过优化的odex),在这样一个过程中,势必会有一些新的问题值得我们去关注。这个问题就是的CLASS_ISPREVERIFIED。

  • 在apk安装的时候系统会将dex文件优化成odex文件,在优化的过程中会涉及一个预校验的过程
  • 校验方式:假设A该类在它的static方法,private方法,构造函数,override方法中直接引用到B类。如果A类和B类在同一个dex中,那么A类就会被打上CLASS_ISPREVERIFIED标记
  • 如果在运行时被打上CLASS_ISPREVERIFIED的类引用了其他dex的类,就会报错
  • 而普通分包方案则不会出现这个错误,因为引用和被引用的两个类一开始就不在同一个dex中,所以校验的时候并不会被打上CLASS_ISPREVERIFIED
  • 补充一下第二条:A类如果还引用了一个C类,而C类在其他dex中,那么A类并不会被打上标记。换句话说,只要在static方法,构造方法,private方法,override方法中直接引用了其他dex中的类,那么这个类就不会被打上CLASS_ISPREVERIFIED标记。
  • 要在已经编译完成后的类中植入对其他类的引用,就需要操作字节码,惯用的方案是插桩。常见的工具有javaassist,asm等。
  • QQ 空间超级补丁方案

    根据上面的第六条,我们只要让所有类都引用其他dex中的某个类就可以了。

    QQ空间补丁方案的关键就在于字节码的注入而不是dex的注入。它使用javaassist 插桩的方式解决了CLASS_ISPREVERIFIED的难题。

    Android进阶:关于插件化和热修复梳理

    1. 在所有类的构造函数中插入这行代码 System.out.println(AntilazyLoad.class);
      这样当安装apk的时候,classes.dex内的类都会引用一个在不相同dex中的AntilazyLoad类,这样就防止了类被打上CLASS_ISPREVERIFIED的标志了,只要没被打上这个标志的类都可以进行打补丁操作。
    2. hack.dex在应用启动的时候就要先加载出来,不然AntilazyLoad类会被标记为不存在,即使后面再加载hack.dex,AntilazyLoad类还是会提示不存在。该类只要一次找不到,那么就会永远被标上找不到的标记了。
    3. 我们一般在Application中执行dex的注入操作,所以在Application的构造中不能加上System.out.println(AntilazyLoad.class);这行代码,因为此时hack.dex还没有加载进来,AntilazyLoad并不存在。
    4. 之所以选择构造函数是因为他不增加方法数,一个类即使没有显式的构造函数,也会有一个隐式的默认构造函数。

    插入代码的难点

    1. 首先在源码中手动插入不太可行,hack.dex此时并没有加载进来,AntilazyLoad.class并不存在,编译不通过。
    2. 所以我们需要在源码编译成字节码之后,在字节码中进行插入操作。对字节码进行操作的框架有很多,但是比较常用的则是ASM和javaassist。
    3. 但AndroidStudio是使用Gradle构建项目,编译-打包都是自动化的。需要对Gradle非常熟悉。

    Tinker

    Android进阶:关于插件化和热修复梳理

    QQ空间超级补丁,“超级补丁”很多情况下意味着补丁文件很大,而将这样一个大文件夹加载在内存中构建一个Element对象,插入到数组最前端是需要耗费时间的,无疑会印象应用启动的速度。因此Tinker 提出了另外一种思路。

    Tinker的思路是这样的,通过修复好的class.dex 和原有的class.dex比较差生差量包补丁文件patch.dex,在手机上这个patch.dex又会和原有的class.dex 合并生成新的文件fix_class.dex,用这个新的fix_class.dex 整体替换原有的dexPathList的中的内容,可以说是从根本上把bug给干掉了。

    HotFix

    以上提到的两种方式,虽然策略有所不同,但总的来说都是从上层ClassLoader的角度出发,如果想要新的补丁文件再次生效,无论你是插桩还是提前合并,都需要重新启动应用来加载新的DexPathList。

    AndFix 提供了一种运行时在Native修改Filed指针的方式,实现方法的替换,达到即时生效无需重启,对应用无性能消耗的目的。

    由于他是Native层操作,因此如果我们在Java层中新增字段,或者是修改类的方法,他是无能为力的。

    Android进阶:关于插件化和热修复梳理

    Sophix(收费)

    阿里推出业界首个非侵入式热修复方案Sophix。它提供了一套客户端服务端一体的热更新方案,做到了图形界面一键打包、加密传输、签名校验和服务端控制发布与灰度功能,让你用最少的时间实现最强大可靠的全方位热更新。

    其他及总结

    各个大厂还有各自的实现,比如饿了吗的Amigo,美团的Robust,实现及优缺点各有差异,但总的来说就是两大类

    • ClassLoader 加载方案
    • Native层替换方案

    综上所述,其实对于热修复很难有一种十分完美的解决方案。在Android开发中,四大组件使用前需要在AndroidManifest中提前声明,而如果需要使用热修复的方式,无论是提前占坑亦或是动态修改,都会带来很强的侵入性(因此,Sophix是不支持四大组件修复的,这也是其非侵入性设计理念无法避免的事情了,不知道以后会不会有新的办法)。热修复方案,目前最多的问题还是兼容性。一句话,没有最好的,只有合适的。


    路过

    雷人

    握手

    鲜花

    鸡蛋