插件化和热修复,从技术实现的角度来说,原理想通。他们都是从系统加载器的角度出发,无论是采用hook方式,亦或是代理方式或者是其他底层实现,都是通过“欺骗”Android 系统的方式来让宿主正常的加载和运行插件(补丁)中的内容;
插件化,更多是想把需要实现的模块或功能当做一个独立的提取出来,减少宿主的规模,当需要使用到相应的功能时再去加载相应的模块。
热修复,则往往是从修复bug的角度出发,强调的是在不需要二次安装应用的前提下修复已知的bug。
宿主: 就是当前运行的APP插件: 相对于插件化技术来说,就是要加载运行的apk类文件补丁: 相对于热修复技术来说,就是要加载运行的.patch,.dex,*.apk等一系列包含dex修复内容的文件。
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查找一个类,最终就是就是在一个数组中查找特定值的操作。
但是,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等。根据上面的第六条,我们只要让所有类都引用其他dex中的某个类就可以了。
QQ空间补丁方案的关键就在于字节码的注入而不是dex的注入。它使用javaassist 插桩的方式解决了CLASS_ISPREVERIFIED的难题。
插入代码的难点
QQ空间超级补丁,“超级补丁”很多情况下意味着补丁文件很大,而将这样一个大文件夹加载在内存中构建一个Element对象,插入到数组最前端是需要耗费时间的,无疑会印象应用启动的速度。因此Tinker 提出了另外一种思路。
Tinker的思路是这样的,通过修复好的class.dex 和原有的class.dex比较差生差量包补丁文件patch.dex,在手机上这个patch.dex又会和原有的class.dex 合并生成新的文件fix_class.dex,用这个新的fix_class.dex 整体替换原有的dexPathList的中的内容,可以说是从根本上把bug给干掉了。
以上提到的两种方式,虽然策略有所不同,但总的来说都是从上层ClassLoader的角度出发,如果想要新的补丁文件再次生效,无论你是插桩还是提前合并,都需要重新启动应用来加载新的DexPathList。
AndFix 提供了一种运行时在Native修改Filed指针的方式,实现方法的替换,达到即时生效无需重启,对应用无性能消耗的目的。
由于他是Native层操作,因此如果我们在Java层中新增字段,或者是修改类的方法,他是无能为力的。
阿里推出业界首个非侵入式热修复方案Sophix。它提供了一套客户端服务端一体的热更新方案,做到了图形界面一键打包、加密传输、签名校验和服务端控制发布与灰度功能,让你用最少的时间实现最强大可靠的全方位热更新。
各个大厂还有各自的实现,比如饿了吗的Amigo,美团的Robust,实现及优缺点各有差异,但总的来说就是两大类
综上所述,其实对于热修复很难有一种十分完美的解决方案。在Android开发中,四大组件使用前需要在AndroidManifest中提前声明,而如果需要使用热修复的方式,无论是提前占坑亦或是动态修改,都会带来很强的侵入性(因此,Sophix是不支持四大组件修复的,这也是其非侵入性设计理念无法避免的事情了,不知道以后会不会有新的办法)。热修复方案,目前最多的问题还是兼容性。一句话,没有最好的,只有合适的。