前言
谈到热修复相信大家应该比较熟悉,因为它是目前比较重要的技术,平常面试中也是被问的比较多。插件化和热修复同出一门,俩者都属于动态更新,而模块化和组件化是基础。相信看完本篇的内容,对于这些模糊的概念应该会有一个比较清晰的了解。
原文链接:https://blog.csdn.net/csdn_aiyang/article/details/103735995?
一、模块化
1、概念
模块(Module),Android Studio提出的概念,根据不同关注点将原项目中共享的部分或业务抽取出来形成独立module,这就类似我们最集成的第三方库的SDK。
2、思想
实际开发中,我们通常会抽取第三方库、整个项目的初始化的代码、自定义的Utils工具类、自定义View 、图片、xml这些(value目录下的各种xml文件)等到一个共有的Common模块中,其他模块在配置Gradle依赖后,就能够调用这些API。
特别注意的是style.xml文件,对于全局共用的style,我们应该把它也放在common模块中。例如我们的项目theme主题,本来是放在main组件的style里面,我们可以把它移到common中,这样其他组件调试时,作为一个单独的项目,也能和主项目有一样的主题。
总之,你认为需要共享的资源,都应该放在common组件中。
3、使用
每一个Module都可以在自身的 build.gradle 中进行设置两种格式:application和library。
apply plugin: 'com.android.application'
//或
apply plugin: 'com.android.library'
引用时,就像添加依赖GitHub库一样。
二、组件化
1、概念
组件化是基于模块化的,可以在打包时是设置为library,开始调试运行是设置成application。目的是解耦与加快开发。组件化适用于多人合作开发的场景,隔离不需要关注的模块,大家各自分工、各守其职。简而言之,就是把一个项目分开成多个项目
(1)好处
- 业务模块分开,解耦的同时也降低了项目的复杂度。
- 开发调试时不需要对整个项目进行编译。
- 多人合作时可以只关注自己的业务模块,把某一业务当成单一项目来开发。
- 可以灵活的对业务模块进行组装和拆分。
(2)规则
- 只有上层的组件才能依赖下层组件,不能反向依赖,否则可能会出现循环依赖的情况;
- 同一层之间的组件不能相互依赖,这也是为了组件之间的彻底解耦;
2、使用
1、在整个项目 gradle.properties 文件中,添加代码
#是否处于debug状态
isDebug = flase
2、在其他Module的 build.gradle 文件中,添加代码
if (isDebug.toBoolean()) {
apply plugin: 'com.android.application'
} else {
apply plugin: 'com.android.library'
}
3、在宿主Module的 build.gradle 文件中,添加代码
dependencies {
compile fileTree(include: ['*.jar'], dir: 'libs')
//...
if(!isDebug.toBoolean()){//不是debug,就添加依赖其他模块
compile project(':home')
compile project(':personal')
compile project(':video')
}
if(isDebug.toBoolean()){
compile project(':common')
}
}
3、版本管理
每个Module的build.gradle文件中很多地方需要些写版本号,例如 targetSdkVersion、appcompat-v7、第三方库等。修改时都要同时修改多份build.gradle文件。如果把版本号可以统一管理起来,就会省时省力,又避免不同的组件使用的版本不一样,导致合并在一起时引起冲突。
整个项目根目录下的 build.gradle 文件中,添加代码
ext {
compileSdkVersion = 25
buildToolsVersion = "25.0.2"
minSdkVersion = 14
targetSdkVersion = 25
versionCode = 1
versionName = "1.0"
}
每个Mudule的 build.Gradle 文件中,改写代码
android {
compileSdkVersion rootProject.ext.compileSdkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode rootProject.ext.versionCode
versionName rootProject.ext.versionName
}
//...
}
4、模块间跳转
我们知道,通常在Gradle中依赖的库是可以直接引用的,即通过startActivity跳转。根据组件化的规则,宿主可以依赖下层组件,而组件之间不可以依赖。因此,当常规业务模块之间遇到业务需求,进行互相跳转时该怎么处理?
这里简单介绍两种方式,即路由和反射。路由的方式以用阿里的ARouter/美团的WMRouter,但是我觉得人少、项目小的公司必要用到这么强大的工具,直接反射就好。
放在common组件中的EventUtile工具类
public class EventUtil{
/**
* 页面跳转
* className 全路径类名
*/
public static void open(Context context,String className){
try {
Class clazz = Class.forName(className);
Intent intent = new Intent(context,clazz);
context.startActivity(intent);
} catch (ClassNotFoundException e) {
Log.e("zhuang","未集成,无法跳转");
}
}
/**
* 页面跳转,可以传参,参数放在intent中,所以需要传入一个intent
*/
public static void open(Context context,String className,Intentintent){
try {
Class clazz = Class.forName(className);
intent.setClass(context,clazz);
context.startActivity(intent);
} catch (ClassNotFoundException e) {
Log.e("zhuang","未集成,无法跳转");
}
}
}
5、资源命名问题
首先,多组件集成时,特别容易出现资源命名重复的问题。可以让各个组件中使用统一前缀,比如home组件中的资源,以home_开通、video组件中以video_开头。当然,如果是嫌麻烦,我们可以在build.gradle文件中,加入如下代码:
resourcePrefix"home_"
但是这个功能其实很弱。比较xml文件报错,依然可以运行,图片文件不已home_为前缀,也不会报错。
三、插件化
也是属于模块化的一种体现。将完整的项目按业务划分不同的插件,分治法,越小的模块越容易维护。单位是apk,一个完整的项目。插件化比热修复简单,插件化只是增加新的功能或资源文件。灵活性在于加载apk,按需下载,动态更新。
实现原理
- 通过dexclassloader加载。
- 代理模式添加生命周期。
- hook思想跳过清单验证。
Android 使用Java的反射机制总结
Android 动态代理与Hook机制详解
总结
- 宿主和插件分开编译
- 动态更新插件
- 按需下载插件
- 缓解65535方法数限制
四、热修复
1、概述
热修复与插件化都利用classloader实现加载新功能。热修复比插件化复杂,插件化只是增加新的功能或资源文件,所以不涉及抢先加载旧类的使命。热修复为了修复bug,要将新的同名类替旧的同名bug类,要抢在加载bug类之前加载新的类。
2、流派
热修复作为当下热门的技术,在业界内比较著名的有阿里巴巴的AndFix、Dexposed,腾讯QQ空间的超级补丁和微信的Tinker,以及大众点评nuwa和美团Robust。阿里百川推出的HotFix热修复服务就基于AndFix技术,定位于线上紧急BUG的即时修复。虽然Tinker支持修复的功能强大兼容性很好,但是不能即时生效、集成负责、补丁包大。
3、原理
(1)native修复方案
AndFix
提供了一种运行时在Native修改Filed指针的方式,实现方法的替换,达到即时生效无需重启,对应用无性能消耗的目的。实现过程三步骤:
- setup():对于Dalvik的即时编译机制(JIT),在运行时装载libdvm.so动态链接库,从而获取native层内部函数:dvmThreadSelf( ):查询当前的线程;dvmDecodeIndirectRef( ):根据当前线程获得ClassObject对象。
- setFieldFlag():把 private、protected的方法和字段都改为public,这样才可被动态库看见并识别,因为动态库会忽略非public属性的字段和方法。
- replaceMethod():该步骤是方法替换的核心。拿到新旧方法的指针,将指针指向新的替换方法来实现方法替换。
(2)Dex 分包方案
概述
DEX分包是为了解决65536方法限制,系统在应用打包APK阶段,会将有调用关系的类打包在同一个Dex文件中,并且同一个dex中的类会被打上
CLASS_ISPREVERIFIED
的标志。因为加载后的类不能卸载,必须通过重启后虚拟机进行加载才能实现修复,所以此方案不支持即时生效。
QQ空间超级补丁
是把BUG方法修复以后放到一个patch.dex,拿到当前应用BaseDexClassloader后,通过反射获取到DexPathList属性对象pathList、再反射调用pathList的dexElements方法把patch.dex转化为Element[],两个Element[]进行合并,最后把patch.dex插入到dexElements数组的最前面,让虚拟机去加载修复完后的方法,就可以达到修复目的。
问题
而然,问题就是两个有调用关系的类不再同一个Dex文件中,那么就会抛“unexpected DEX problem”异常报错。解决办法,就是单独放一个AnitLazyLoad类在另外DEX中,在每一个类的构造方法中引用其他DEX中的唯一AnitLazyLoad类,避免类被打上CLASS_ISPREVERIFIED标志。
不足
此方案通过增加dex来修复,但是修复的类到了一定数量,就需要花不少的时间加载。对手淘这种航母级应用来说,启动耗时增加2s以上是不能够接受的事。在ART模式下,如果类修改了结构,就会出现内存错乱的问题。为了解决这个问题,就必须把所有相关的调用类、父类子类等等全部加载到patch.dex中,导致补丁包异常的大,进一步增加应用启动加载的时候,耗时更加严重。
微信Tinker
微信Tinker采用的是DEX差量包,整体替换DEX的方案。主要的原理是与QQ空间超级补丁技术基本相同,但不将patch.dex增加到elements数组中。差量的方式拿到patch.dex,开启新进程的服务TinkerPatchService,将patch.dex与应用中的classes.dex合并,得到一个新的fix_classess.dex。通过反射操作得到PathClassLoader的DexPatchList,再反射调用patchlist的makeDexElements()方法,把fix_classess.dex直接替换到Element[]数组中去,达到修复的目的。从而提高了兼容性和稳定性。
(3)Instand Run 方案
Instant Run,是android studio2.0新增的一个运行机制,用来减少对当前应用的构建和部署的时间。
构建项目的流程:
构建修改的部分 → 部署修改的dex或资源 → 热部署,温部署,冷部署。
热拔插:方法实现的修改,或者变量值修改,不需要重启应用,不需要重建当前activity。
温拔插:代码修改涉及到了资源文件,activity需要被重启。
冷拔插:修改了继承规则、修改了方法签名,app需要被重启,但是仍然不需要重新安装 。
五、总结
模块化、组件化、插件化通讯方式不同之处
- 模块化相互引入,抽取了公共的common模块,其他模块自然要引入这个module。
- 组件化主流是隐式和路由。隐式使解耦和灵活大大降低,因此路由是主流。
- 插件化本身是不同进程,因此是binder机制进程间通讯。