android架构傲视苍穹《Android》VIP专题

谈谈我的理解-组件化/模块化

2017-06-03  本文已影响2124人  前行的乌龟

今天来回味下组件化和模块化,这2种说法时一回事,当然还是有区别的,下面再详细说,其实很简单,只是设计范围的不同,也都是约定俗成的东西。为了方便我下面都说组件化了

到现在组件化真的不是什么新鲜东西了,大公司都用的滚瓜烂熟,龙飞凤舞了,也就是现在部分中型项目和小项目在组件化的路上努力。所以同志们,组件化没玩过的,不熟悉的赶紧搞起来,说一点,你不会组件化,发布影子工程那么对你来说就是个噩梦。从本质上来讲任何技术进步都是在现实需求的逼迫下抓耳挠腮,耗尽无数头发才想出来的。哈哈,这里说个笑话罢了。所以呢组件化这个东西出来这么久了,页发展了这么久了,用的人越来越多,那肯定是对我们显示开发大有裨益的,下伙伴们不会,不熟悉抓紧啦,要不面试问你你怎么回答呢!

下面来正式说说组件化

组件化这个东西其实并不复杂,他就是种思路,本质上是一种 app 架构思路,说穿了很简单的,难在组件化改造的时候,真正写起代码会出现不少棘手的问题,当然这些坑基本前人都趟完了,这里我主要时记录下,要是你看到熟悉的部分,请不要骂我啊,毕竟都是前辈们的东西啊。

这里补充一下,组件化时一种 app 架构,他的发展也是沿着正常的技术发展脉络来的,也是为了以追求高复用,高可维护性的目的的代码封装,区别是组件化是对整个 app 的再次封装。

废话了这么多,那么什么是组件化呢,各位看官想不要着急,在详细说组件化之前,我们要搞懂2个概念,就是上面说的组件和模块。

首先组件和模块都不是官方规定的,都是这些技术发展下来大家约定俗成的概念,其实很简单,一说就明白

以上我谈了下我自己对于模块化,组件化的理解,是目前开发中对于模块和组件的理解。在模块化和组件化的发展中概念也是有些调整变化的,大家只要看现在时什么样子就好了,深入学习的话有兴趣可以看看组件化,模块化的发展历程。

我认为 Android 模块化探索与实践 对于模块化,组件化概念的解释时最优秀的。

组件化和模块化在现在看是一回事了,如果把一个项目看成是袋中的组合的话,那么模块就是体积最大的哪些袋子,组件就是体积小的袋子,大的袋子是最直接可被外接观测和接触的袋子,大的袋子也是用小的袋子组成的,一个不太恰当的比喻吧,模块和组件就是这样的关系,是我们对业务和功能拆分,封装的理解。

好了正式开始介绍了组件化啦

组件化在工程表现上就是我们把 app 按照其业务的不同,划分为不同的 module模块,把各种功能封装成一个个 library,module 之间时严格禁止横向依赖的,要不怎么单独使用呢,我不能为了用一个 module,把相关的module 都带上吧,要是这么 module 还有依赖的module 呢,这样谈复用性就是扯淡了。

主 app 就是我们常说的壳工程依赖这些 module,library 由需求的 module 依赖,但是要考虑library 版本的问题,随着业务和功能的扩展,library 的数量也是巨大的,微信在组件化拆分时据说拆分出80多个 module,可见 library 也是少不了的。

module 和 library 多数时候我们时提供arr 和 jar 来给壳工程引用的,arr 和 jar 在编译时是不会再编译的,只会检查版本,保留一个最新的版本,既提高了 app 的编译速度,页提供一种资源冲突解决方式。

下面我方一些图来描述一下组件化,大伙仔细看看,图比文字可生动多了

modularization.png modules.png 20170118062508842.png 20170118064804889.png 20161202165647074.png

项目如何组件化:

20170522211601227.png 3688153-dba93d79b7426568.png

组件化核心:router##

我们在抽象 module 时,module 之间是没有相互依赖的,是严格解耦的,为了达到我们复用的目的。module 之间不能相互依赖,就没法调用别的 module 的代码了,那么面对业务之间的页面相互调起,相互通信这些常见的需求我们该怎么办,没错就是大伙在上面的图里面看见的东西 router。

router 是我们统一制定的模块间通讯协议,router 中我们主要是处理一下几个问题:

router.png

router 这东西有现成的,你也可以自己封装。使用的思路都是把 router 作为一个组件,所有的业务 module 都依赖这个 router 组件,当然壳app 也是,然后我们把需要的模块间页面跳转,数据传递,初始化都注册到 router 中,这里面就体现到我们定义的统一,通用的模块通讯协议的重要性了,router 维护多个集合保存这里关系,然后我们通过router 就可以实现模块间的通讯了。

router 的封装还是挺麻烦的,要写好了不容易,现在用的比较多的有:

上面我介绍了几个 router 路由,基本上不论时自己写还是用现成的,router 基本上都是上面这几个的样子了,当然了现在好的 router 还是要使用 APT注解技术来动态去 router 注册模块方法,自己写代码去注册的话使用很使用,有些问题不好处理,比如 router 的静态实例要是被回收了,你再 new 一个出来,那么模块注册的方法怎么办,写起来太麻烦,还不如 APT 注解来的方便,扩展性也好。这里有个ToyBricks_Android项目模块化解决方案 可以解决 APT不能扫描 arr 包的问题。

最后说一下,module 间的通讯其实可以分成3种:

页面调起现在的 router 都可以很好的完成这个任务。

某些事件的通知,比如我切换城市了,通知某些页面去显示或是刷新数据,这个根据业务来说影响的范围会很广的,会影响多个业务的,因为 module 的复用性,我们在 module 中是不能确定会具体影响哪些业务module 的,那么这种场景使用 eventbus/广播比较合适了。

直接调用默写模块的业务方法,这属性业务模块间在业务上的强耦合了,这个碰到产品这么设计你也没办法,一般碰到这样的场景也是会保证相关的业务module 都是会加载的,所以呢在定义 router 灵活一些,可以做到调用指定module 的某些方法


router 我不想说太多,也说不好,这部分大伙看我最后的链接吧,或是看看上面4个路由也可以,不论如何实现,router 是为了给 module 模块搭建一个通讯的中间平台,目的就是这样。仔细的大家多看吧,这里我也是看别人的。

我再逼逼一下,module 和 library 我们尽量不要提供源代码的方式提供依赖,这不符合我们复用的目的,到时候你发布几个影子功能或是别的 app,那么你使用源代码依赖方式的 module 和 library 你怎么提供维护,所以尽量使用 arr 和 jar 的方式。我们可以把一些定位相近library 打包成一个 module 也是不错的。

我们一定要熟悉gradle的使用,在组件化中我们会大量的使用 gradle 提供各种资源加载的配置和环境配置

组件化最核心的目的就是代码的高可复用和高可维护和高可扩展性能,其他的优点都是属于连带性质的,我们要先把握住核心点学习,其他的都不是主要,有时间再看


组件化碰到的问题

1. 子模块单独编译测试

在做组件化开发时,我们测试 module 库都是把 module 单独达成 apk 文件,在发布module时 提供 library 供外界依赖,这都是通过配置 module 的 gradle 的编译模式实现的

首先在子模块build.gradle中定义常量,来标示模块目前是否处于开发模式

def isDebug = true

在子模块的build.gradle中进行模式配置。debug模式下编译成独立app,release模式下编译成library。

if (isDebug.toBoolean()) {
    apply plugin: 'com.android.application'
} else {
    apply plugin: 'com.android.library'
}

两种模式下模块AndroidManifest.xml文件是有差别的。作为独立运行的app,有自己的Application,要加Launcher的入口intent,作为library不需要。这个问题很好解决,写两个不同的AndroidManifest.xml即可,并在gradle中进行配置。

在 gradle 脚本中配置

android {
    sourceSets {
        main {
            if(isDebug.toBoolean()) {
                manifest.srcFile 'src/debug/AndroidManifest.xml'
            } else {
                manifest.srcFile 'src/release/AndroidManifest.xml'
            }
        }
    }
}

2. sdk和第三方库的版本一致性

不同module依赖sdk版本不一致,会因兼容性问题导致编译问题。
不同module引用了同一个第三方库的不同版本,并且这个库没有做到向前兼容,就有可能出现方法找不到、参数不对应等问题。
所以有必要统一整个project的依赖版本。

在最外层build.gradle中定义的常量能被整个project的build.gradle文件引用,统一的版本定义可以放在这里。

ext {
    android_compileSdkVersion = 25
    android_buildToolsVersion = '25.0.2'
    android_minSdkVersion = 21
    android_targetSdkVersion = 25

    lib_appcompat = 'com.android.support:appcompat-v7:25.1.1'
    lib_picasso = 'com.squareup.picasso:picasso:2.5.2'
    lib_gson = 'com.google.code.gson:gson:2.6.1'
}

我没试过 arr资源的 module 是否还可以使用这种方式


3. 资源id冲突

android 中 module的资源文件最后都是会合并到主项目中的,资源文件的 id 最终和 moudle 是的 id 时不一样的,所以这就会出现资源重名的问题,解决这个问题,我们的做法就是module 资源加一个统一的前缀

andorid{
    ...

    buildTypes{
        ...
    }

    resourcePrefix "moudle_prefix"

}

但是注意 res 文件夹下的文件可以用 gradle 脚本加前缀,但是图片资源不行,图片资源我们还是需要在命名时自己添加前缀

4. application初始化的问题

子模块作为application时,有一些初始化的工作需要在Application.onCreate时进行。而作为library时,调不到这个onCreate。所以自己写一个静态方法,供主工程的Application调用。

public class ApplicationA extends Application {

@Override public void onCreate() {
  super.onCreate();
  //给底层library设置context
  AppContext.init(getApplicationContext());
}
  /**
   * 作为library时需要初始化的内容
   */
  public static void onCreateAsLibrary() {
    //给FunctionBus传入接口的实例
    FunctionBus.setFunction(new FunctionA() {
      @Override public String getData(String key) {
        return "xixi";
      }
    });
  }
}
主工程的Application onCreate时记得初始化子模块。

public class MainApplication extends Application {

  @Override public void onCreate() {
    super.onCreate();
    AppContext.init(getApplicationContext());
    ApplicationA.onCreateAsLibrary();
    ApplicationB.onCreateAsLibrary();
  }
}

除了提供方法在壳工程里面调用,还可以结合使用了 APT 技术的 router 来做,使用注解,就不用我们自己去调用了,彻底解耦

5. library依赖问题

先说一个问题,在组件化工程模型图中,多媒体组件和Common组件都依赖了日志组件,而A业务组件有同时依赖了多媒体组件和Common组件,这时候就会有人问,你这样搞岂不是日志组件要被重复依赖了,而且Common组件也被每一个业务组件依赖了,这样不出问题吗?

其实大家完全没有必要担心这个问题,如果真有重复依赖的问题,在你编译打包的时候就会报错,如果你还是不相信的话可以反编译下最后打包出来的APP,看看里面的代码你就知道了。组件只是我们在代码开发阶段中为了方便叫的一个术语,在组件被打包进APP的时候是没有这个概念的,这些组件最后都会被打包成arr包,然后被app壳工程所依赖,在构建APP的过程中Gradle会自动将重复的arr包排除,APP中也就不会存在相同的代码了;

但是虽然组件是不会重复了,但是我们还是要考虑另一个情况,我们在build.gradle中compile的第三方库,例如AndroidSupport库经常会被一些开源的控件所依赖,而我们自己一定也会compile AndroidSupport库 ,这就会造成第三方包和我们自己的包存在重复加载,解决办法就是找出那个多出来的库,并将多出来的库给排除掉,而且Gradle也是支持这样做的,分别有两种方式:根据组件名排除或者根据包名排除,下面以排除support-v4库为例:

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile("com.jude:easyrecyclerview:$rootProject.easyRecyclerVersion") {
        exlude module: 'support-v4'//根据组件名排除
        exlude group: 'android.support.v4'//根据包名排除
    }
}

library重复依赖的问题算是都解决了,但是我们在开发项目的时候会依赖很多开源库,而这些库每个组件都需要用到,要是每个组件都去依赖一遍也是很麻烦的,尤其是给这些库升级的时候,为了方便我们统一管理第三方库,我们将给给整个工程提供统一的依赖第三方库的入口,前面介绍的Common库的作用之一就是统一依赖开源库,因为其他业务组件都依赖了Common库,所以这些业务组件也就间接依赖了Common所依赖的开源库。

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    //Android Support
    compile "com.android.support:appcompat-v7:$rootProject.supportLibraryVersion"
    compile "com.android.support:design:$rootProject.supportLibraryVersion"
    compile "com.android.support:percent:$rootProject.supportLibraryVersion"
    //网络请求相关
    compile "com.squareup.retrofit2:retrofit:$rootProject.retrofitVersion"
    compile "com.squareup.retrofit2:retrofit-mock:$rootProject.retrofitVersion"
    compile "com.github.franmontiel:PersistentCookieJar:$rootProject.cookieVersion"
    //稳定的
    compile "com.github.bumptech.glide:glide:$rootProject.glideVersion"
    compile "com.orhanobut:logger:$rootProject.loggerVersion"
    compile "org.greenrobot:eventbus:$rootProject.eventbusVersion"
    compile "com.google.code.gson:gson:$rootProject.gsonVersion"
    compile "com.github.chrisbanes:PhotoView:$rootProject.photoViewVersion"

    compile "com.jude:easyrecyclerview:$rootProject.easyRecyclerVersion"
    compile "com.github.GrenderG:Toasty:$rootProject.toastyVersion"

    //router
    compile "com.github.mzule.activityrouter:activityrouter:$rootProject.routerVersion"
}

6. module不同业务环境使用不同代码

我们做项目至少会有测试和线上2套环境吧,组件化让我们开始重视 gradle,通过 gradle 配置我们可以减少很多代码的书写的,切换环境我们也是可以用 gradle 实现的,在不通过的环境下注册不同的代码文件,看下面这张图

1281144-3d9c01d6ac6d30fe.png

我们有 debug 和 release2个环境,里面放的是不同环境执行的代码,main 里面是跟环境切换无关的代码部分,我我们这样设置 gradle 就可以了

android {
    // ...
    sourceSets {
        debug {
            java.srcDirs = ['src/main/java', 'src/debug/java']
        }
        release {
            java.srcDirs = ['src/main/java', 'src/release/java']
        }
    }
}

此外在发布该 library 时,需要指定一些设置,如下:

android {
    // ...
    defaultConfig {
        // ...
        defaultPublishConfig 'release'
        publishNonDefault true
    }
}

说明:

业务组件 module 依赖不同的基础组件生产的 library,如下:

dependencies {
    // ...
    debugCompile project(path: ':baselibrary', configuration: "debug")
    releaseCompile project(path: ':baselibrary', configuration: "release")
}

在使用通过这样的配置脚本解决了多个 APK 包依赖同一组件生产的不同的 library,最终得到我们需要的开发/测试/生产 APK 包。


一个好的组件化文档是必须的

组件化是 android 开发步入新时代的未来,是代码膨胀,支持快速开发的必然,一个好的组件化文档在现今来看也是必须的了

下面贴个图

20160311130348_213.jpg 20160311130349_276.jpg

组件化的坑

组件化是好,但是坑也是不少,不好填,尤其是 databinding,dagger,bufferkinft,这是源于 studio 编译的问题。

studio 中 module 虽然时在代码上独立于壳工程的,但是在编译时最后还是要合并到壳工程中的,要不怎么达成一个 APK 文件,要是多个 APK 文件把不成了插件化了嘛,插件化坑更多啊。合并 module 到壳工程就会产生一个根本问题,module 的 R 文件数值改变了。module 的 R文件数据不是固定的,只有壳工程的 R 文件才是常量值,时不变的,module 的 R 文件数值在把 modul 的资源合并到壳工程后才会确定下来,那么这就对依靠编译时注解的技术造成了难题,你指定的 R 路径最后找不到,并且据说这里面还涉及注解的 final ,不了解,看到有人这么说,所以大家在开发组件化时对于带注解技术的框架要多注意,有坑要多看才能爬过去

组件化文章:


优秀的组件化方案


优秀的router 路由器设计


gradle学习文章

学习 gradwle 的入门系列,翻译自官方文档,适合入口看,学习各种概念

gradle 各种常用使用

飞雪无情的 gradle 教程

吴小龙的gradle 的教程

上一篇下一篇

猜你喜欢

热点阅读