美文网首页Android进阶之路Android开发经验谈Android技术知识
Android 模块化、组件化、插件化及热修复,大集结篇!

Android 模块化、组件化、插件化及热修复,大集结篇!

作者: cff70524f5cf | 来源:发表于2019-10-08 20:43 被阅读0次

    前言

    谈到热修复相信大家应该比较熟悉,因为它是目前比较重要的技术,平常面试中也是被问的比较多。插件化和热修复同出一门,俩者都属于动态更新,而模块化和组件化是基础。相信看完本篇的内容,对于这些模糊的概念应该会有一个比较清晰的了解。

    一、模块化
    二、组件化
    三、插件化
    四、热修复
    五、总结

    一、模块化

    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,按需下载,动态更新。

    实现原理

    1.通过dexclassloader加载。
    2.代理模式添加生命周期。
    3.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需要被重启,但是仍然不需要重新安装 。

    五、总结

    模块化、组件化、插件化通讯方式不同之处

    1.模块化相互引入,抽取了公共的common模块,其他模块自然要引入这个module。
    2.组件化主流是隐式和路由。隐式使解耦和灵活大大降低,因此路由是主流。
    3.插件化本身是不同进程,因此是binder机制进程间通讯。

    好了,文章到这里就结束了,如果你觉得文章写得不错就给个赞呗?如果你觉得那里值得改进的,请给我留言。一定会认真查询,修正不足。谢谢。

    希望读到这的您能转发分享和关注一下我,以后还会更新技术干货,谢谢您的支持!

    转发+点赞+关注,第一时间获取最新知识点

    相信自己,没有做不到的,只有想不到的。

    Android架构师之路很漫长,一起共勉吧!

    以下墙裂推荐阅读!!!

    相关文章

      网友评论

        本文标题:Android 模块化、组件化、插件化及热修复,大集结篇!

        本文链接:https://www.haomeiwen.com/subject/sgxipctx.html