Android项目组件化思考

随着App版本不断迭代更新,新老业务代码难免会越来越复杂;如果还是单一工程的话,然后,以下问题势必就不可避免了。

  • 影响开发效率(个人开发的话工程师需要熟悉的代码越来越多;多人协作的话必然影响效率)
  • 代码耦合严重,编译时长增加(动一发都需要重新打包测试,等待时间过长)
  • 单元测试,难度加大(工程师将需要掌握更多的业务逻辑代码)
  • 涉及到有些代码特殊性,并不需要所有开发都去了解它,那如何控制代码权限呢
    有什么方案,可以解决这些问题呢?
    项目组件化这个时候就需要登场了,将功能按业务线拆分成一个个小项目一来开发,大家觉得怎么样?

组件化一般组成成分

前面一张图,已经很形象解释了组件化和插件化的区别:
前者需要在编译的时候将各模块(手、身体、头等)组合起来,同样这些模块也是可以单独运行的,但是编译之后这就是一个整体了。
而后者则是由一个个独立的可运行的程序组合而成,并且在程序编译之后也可以动态的加载这些模块(小的机器人),组合成一个功能随时可变化的程序。

组件化主要成分介绍如下:

  • App壳(运行入口)
    • 配置启动apk的入口
    • 组合所有Library
  • 拆分的业务线模块(Library)
    • 每个Library需要提供方法,实现各Library通信
    • 每个Library可配置为单独运行,调试
    • 每个Library之间相互不冲突,可动态组合各Library成实现对应需要的功能
  • 公用代码Libaray
    • 比如资源
    • 或者一些共有实体类

思考组件化需要实现那些功能

对于这个问题,由于涉及比较多,先从理论上讲解,不涉及过多代码,后面可依据项目具体代入,了解这些原理是如何实现,基本就了解如何进行组件化开发了。

  • 如何让每个Model既是Library又可单独运行
  • 每个Library可以相互通信 (不同的组件Library之间交互、UI跳转等)
  • 防止每个Library中的资源名冲突
  • 动态加载卸载组件Library等功能
  • 如何让每个model可保持独立运行测试(方便调试改bug)
  • 尽可能让每个Library不需要每次重新编译,减少编译时间
  • 对于每个Library公用的代码封装为一个都需要依赖的Library (或者两个:建议将所有资源图片等都放在一个Library上)

如何让每个Model既是Library又可单独运行

怎么区分,模块是可运行程序还是Library呢?
要解决这个问题,就需要了解 build.gradle 文件中 apply plugin: 的配置。
要动态支持既可执行又可设为Library,那我根据参数配置成动态的吧

  • 即:module/build.gradle文件中添加:
    1
    2
    3
    4
    5
    if (isDebug.toBoolean()) {
    apply plugin: 'com.android.application'
    } else {
    apply plugin: 'com.android.library'
    }
    isDebug为gradle.properties文件中的一个配置参数
    1
    2
    # 组件单独调试开关,true 可以,false 不可以,需要点击 "Sync Project"
    isDebug=false
  • 注意事项,isDebug可能读取有问题,如有问题,可如下方式获取
    1
    2
    3
    4
    5
    6
    Properties gradleProperties = new Properties()
    gradleProperties.load(project.rootProject.file('gradle.properties').newDataInputStream())
    def propertyisDebug = gradleProperties.getProperty('isDebug', "true")
    ext {
    isDebug = propertyisDebug
    }

关于通信的方案有哪些

特别提醒

  • 只要model间可以传递数据,亦可以封装方法实现组件跳转等,主要需要做到的就是不依赖工程的方式实现该功能(类似于反射的方式调用其它组件代码)。

有何方案防止各model中的资源名冲突

可利用resourcePrefix属性,配置当前model前缀必须以xx开头,否则就会报错

  • module/build.gradle文件中添加:
    1
    2
    3
    //设置了resourcePrefix值后,所有的资源名必须以指定的字符串做前缀,否则会报错。
    //但是resourcePrefix这个值只能限定xml里面的资源,并不能限定图片资源,所有图片资源仍然需要手动去修改资源名。
    resourcePrefix "module_"

如何动态加载卸载组件model等功能

简单说,如何动态的配置依赖,比如debug配置debug的组件,relese配置relese的组件

遇到的问题和解决方案

  • android library中默认是不支持debug模式,根据不同的编译环境依赖对应的包的 解决方案 如下:

    • 在android library的gradle中加入以下配置

      1
      publishNonDefault true
    • 在项目工程的依赖中添加如下配置

      1
      2
      debugCompile project(path: ':library', configuration: 'debug')
      releaseCompile project(path: ':library', configuration: 'release')

如何让每个model可保持独立运行

  • 需要运行肯定需要指定AndroidManifest.xml可运行Activity
  • 如何实现可执行与不能执行的Application功能呢?
    • 非Library模式下可只是添加自定义Application,再非测试模式下参考下条剔除不需要代码
    • Library模式下
      • 通过壳model统一管理每个model的假Application
      • 在需要的方法中遍历代理每个假Application调用相应方法
  • 对于Library模式下,怎么剔除上面这些不需要的代码呢?
    • 可在main目录下新建debug目录,存放debug模式下的AndroidManifest.xml
    • 可在java目录下新建debug包,存放debug模式下需要的java代码
    • 然后实现过滤,同也支持其他目录等配置
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      sourceSets {
      main {
      if (isModule.toBoolean()) {
      res.srcDirs = ['src/main/res']
      manifest.srcFile 'src/main/module/AndroidManifest.xml'
      } else {
      manifest.srcFile 'src/main/AndroidManifest.xml'
      res.srcDirs = ['src/main/res', 'src/main/res-debug']
      //集成开发模式下排除debug文件夹中的所有Java文件
      java {
      exclude 'debug/**'
      }
      }
      }
      }

尽可能让每个model不需要每次重新编译,减少编译时间

  • 对于某些Library,改动不频繁,不需要每次都编译,可以尝试作版本管理

    版本只对已发布到maven仓库的library才有效。
    那难道我们要把组件都发布成maven吗?是的。
    那难道我们要把组件都发布到远处仓库吗?不是的。
    具体怎么做呢?

  • 我们可以把组件发布到本地或远程仓库,甚至于Project文件夹目录的本地仓库。

工程根目录的build.gradle中添加如下代码即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
buildscript {
...
}

allprojects {
apply plugin: 'maven'

repositories {
jcenter()
maven {
//本地maven仓库,路径是./projectName/.repo-tmp
url 'file://' + project.rootProject.rootDir + File.separator + '.repo-tmp'
}
}

configurations.all {
// check for updates every build
resolutionStrategy.cacheChangingModulesFor 1, 'seconds'
}
}

subprojects { subp ->
//发布后的group名称
project.group = 'goluck.top'
subp.afterEvaluate {
//只能发布设置了版本号的Module
if (!(subp.version + '').equals('unspecified')) {

if (subp.extensions.findByName('android') != null
&& !getPlugins().hasPlugin("com.android.application")) {
android.libraryVariants.all { variant ->
if (variant.name.equals('release')) {
def generateandroidSourcesJar = task("generate${variant.name.capitalize()}SourcesJar", type: Jar) {
classifier = 'sources'
from android.sourceSets.main.java.sourceFiles
}
artifacts {
archives generateandroidSourcesJar
}
}
}
} else if (subp.extensions.findByName('android') == null) {
//java工程
//生成source 文件
task('sourcesJar', type: Jar) {
from sourceSets.main.java.srcDirs
classifier = 'sources'
}
artifacts {
archives sourcesJar
}
}
uploadArchives {
repositories.mavenDeployer {
repository(url: 'file://' + project.rootProject.rootDir + File.separator + '.repo-tmp')
pom.groupId = project.group
pom.artifactId = project.name
pom.version = project.version
}
}
}
}
}
  • 然后在需要发布的Module的build.gradle中添加版本号:
    1
    2
    3
    4
    version = '1.0'
    android{
    ...
    }
  • 打开Gradle projects
    • 点开配置好的model 在Tasks下面会出现 upload/uploadArchives。
    • 点击执行uploadArchives即可将当前model发布到Project下的 .repo-tmp 文件夹中
  • 然后可修改原来的引用方式:

    1
    2
    compile project(':module') //旧
    compile 'goluck.top:module:1.0'//新 goluck.top为自己配置的group名称
  • 当前这种发布到本地仓库的优点

    • 可以进行组件的版本管理,
    • 节约整个工程运行时的build时间。
    • 方便重用
    • 缺点
      • 每次Module修改的时候不能实时生效需要重新发布到仓库刷新后才能生效

组件化开发的建议

  • 在我们每一个模块文档之后,我们应该使用arr的形式引入依赖,上传我们的maven库,再去compile下来,同时注释掉setting.gradle的配置,这样子有助于编译加快。
  • 删除各个module的test代码,也就是src目录下的test,因为那些都是包含一些task的,在同步或者是编译的时候会被执行,减慢了编译速度。
  • 四大组件应该在各自module里面声明。
  • 在Base中提供一个BaseApplication

其他注意事项

  • 每个module统一版本号,例如:
    • 根目录下 build.gradle 添加
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      def androidSupportVersion = '25.3.1'
      ext {
      //编译的 SDK 版本,如API20
      compileSdkVersion = 25
      //构建工具的版本,其中包括了打包工具aapt、dx等,如API20对应的build-tool的版本就是20.0.0
      buildToolsVersion = "26.0.0"
      //兼容的最低 SDK 版本
      minSdkVersion = 14
      targetSdkVersion = 22
      appcompatV7 = "com.android.support:appcompat-v7:$androidSupportVersion"
      constraintLayout = 'com.android.support.constraint:constraint-layout:1.0.2'
      }
    • module/build.gradle 配置统一版本号
      1
      2
      3
      4
      5
      android {
      compileSdkVersion rootProject.ext.compileSdkVersion
      buildToolsVersion rootProject.ext.buildToolsVersion
      //……
      }

其他疑问

  • 如果model1依赖了common,model1也依赖了common,会不会导致重复依赖呢?答案:不会,实际上在 release 构建 APP 的过程中 Gradle 会自动将重复的 aar 包排除。

相关推荐阅读