单项目结构:
将程序的所有功能以及依赖库都集中在一个项目下进行管理,不同业务或非业务通过包名区分,使得项目结构清晰,比如常见的登录、反馈、上报功能等。
-
优点:结构简单,适合小团队、需要快速迭代且复杂度不高的产品
适合:产品处于探索期、未成形、或是需要快速迭代和验证功能、总体功能未稳定
缺点:扩展性相对较差,模块间耦合性较高,不利于大型项目的开发
适合小项目和需要快速迭代开发的项目。
插件化:
把 App 拆分成一个宿主和多个插件,插件可以在运行期动态加载
出现背景:
- 基于开发考虑,项目过于庞大时通过插件化解耦
- 基于运行考虑,通过插件化进行动态的功能运营
- 基于质量考虑,线上热修复(HotFix)
特点:重、黑科技;对团队和技术要求高。每个组负责单独插件开发,适合航母级应用。
组件化:
将一个 App 按照功能或者业务拆分为多个模块,每个模块作为单独的组件(module),可以独立开发和调试,最终在发布时,再将这些组件合并成完整的 Apk。
- 组件化有优点,当然也有代价,真正有必要才去做,没必要完全不用做这种事情。
为什么需要组件化?
高内聚代码的强制解耦
-
各组件相互独立,便于开发和调试
功能点比较少的项目,通过单项目工程就足以应付开发场景,当功能比较多的时候,比如集成一个直播、社交功能..功能点独立,完全可以由其它开发团队来开发、或根据模块由不同开发人员进行开发 (节省时间,提高开发效率)
-
便于项目的集成和更新
比如这个组件并不在这个版本上使用,但是要先开发出来,等到下一个版本需要上的时候这个组件能够非常快集成进项目中去。
-
易于复用和扩展
UI库样式一致、意见反馈这些模块,随时可以扔到另外的项目中去用。
-
有损服务,动态加载
Android 生态复杂,手机性能差异巨大,有损服务出现在插件化概念中,可以通过判断用户手机性能,如果性能高就加载所有组件,如果比较差就先加载核心的,其他一些组件可能需要用户去开启或者去下载的 。(插件化范畴了)
...
组件化主流方案
-
冯森林 MDCC 2016 中国移动开发者大会
多module,debug时候是apk,发布时作为library
-
大众点评 (AAR 独立 repo)
组件放到单独的 git 仓库中,打包生成 aar ,供宿主或其他组件调用
-
淘宝 Atlas 动态组件化(Dynamic Bundle)框架
类似 OSGI ,其实是一种插件化方式
组件化模型
-
单工程模型:
业务组件和基础库在一个项目里面,只是通过分包名进行区分,组件之前耦合性严重。各组件调用强引用。
单工程模型 -
组件化模型
- 最上层是业务组件,比如 新闻、直播、社交、意见反馈组件等等,会用到下层的图片库网络库(基础库)
- 各组件通过通信模块(ARouter)间接进行通信。
- App外壳作为宿主,这个外壳加载包装其他组件,类似手机操作系统,安装不同App。
- 最低层是UI库和基础库(UI库比较少人做,一般只是基础库CommonLib)
组件化具体实践
1. 组件依赖方式
-
AAR 依赖,
子module全部打包生成aar,由一个壳工程去组装构建 App,各业务模块独立拆分Git仓库,提高项目隔离性。
优点:隔离性好
缺点:上层依赖底层,发布顺序必须等到依赖的底层 AAR 开发完毕才能发布,有一定依赖性 -
Compile project 依赖
Module 在 debug 模式下作为Application,在release模式下作为library,各组件在调试时可以独立运行,在 App 发布时作为lib嵌入主项目。(简单、常用)
2. 组件独立编译
开发模式下,子模块可以单独调试,生成独立的 App,而在主程序发布时,则作为 library嵌入主程序,在子模块的
build.gradle
配置中,可根据常量判断是否处于开发模式
-
根目录配置文件中配置各个模块组件是否处于 App 还是 Module。
-
项目根目录
gradle.properties
配置:# 是否需要单独编译 true表示不需要,false表示需要 #isApp_Home=false #isApp_Chat=false #isApp_Video=false #isApp_Me=false
-
在各个子模块中配置(例如Home Module):
if (isApp_Home.toBoolean()) { apply plugin: 'com.android.application' } else { apply plugin: 'com.android.library' }
-
-
applicationId,只有在 application 的情况下才需要声明。
defaultConfig { if (isApp_Home.toBoolean()) { //单独运行时候需要 applicationId applicationId "tsou.cn.module_me" } }
-
sourceSets:在debug模式下生成自己的清单文件(需要入口函数或测试类作为一个独立app来运行的时候)
sourceSets { main { if (isDebug.toBoolean()) { manifest.srcFile 'src/main/debug/AndroidManifest.xml' } else { manifest.srcFile 'src/main/release/AndroidManifest.xml' java { exclude 'debug/**' } } } }
-
在app主模块中:
if (isNeedHomeModule.toBoolean()) { compile project (':module_home') } if (isNeedChatModule.toBoolean()) { compile project (':module_chat') } if (isNeedRecomModule.toBoolean()) { compile project (':module_recom') } if (isNeedMeModule.toBoolean()) { compile project (':module_me') }
3. 组件 SDK 版本一致性
根目录定义ext统一版本,子module引用。避免版本冲突
-
project 的 build.gradle 中定义常量
ext { minSdkVersion = 16 supportVersion='27.1.1' }
-
组件的
build.gradle
引用常量defaultConfig { minSdkVersion rootProject.ext.minSdkVersion } dependencies { api("com.android.support:appcompat-v7:${supportVersion}") ... }
4. 资源合并冲突(*)
合并冲突: 是指多个Manifest文件中含有同一属性但值不同时,默认合并规则解决不了从而导致的冲突。
当冲突发生时,高优先级的Manifest属性值会覆盖低优先级属性值。这个优先级规则由高到低依次是:buildType下的Manifest设置 > productFlavor下的Manifest设置 > 主工程src/main > dependency&library
不同 module 资源id合并:
- 如果两个模块中定义了相同资源id,将使用应用中的资源id
- 如果多个 AAR 库之间发生冲突,将使用依赖项列表首先列出(位于dependencies块顶部)的库中的资源
- 为避免资源id冲突,请使用在模块中具有唯一性前缀或其他一致性的命名方案
- 3.1:Gradle 设置资源前缀:
resourcePrefix "user_"
- 3.2:修改 aapt(入侵了源码和app编译过程,存在风险)
- 3.1:Gradle 设置资源前缀:
-
模块私有资源:
私有资源描述可参考如果不想让其他 module 访问我当前 module 的资源,可以申明私有属性
1)资源库中所有资源默认处于公开状态
2)要将所有资源隐私设为私有,至少将一个特定的属性定义为公开
3)在res/value目录下,创建public.xml文件,定义公开资源<resources> <public name="mylib_main_layout" type="layout"/> <public name="mylib_public_string" type="string"/> </resources> 除了以上定义的共有资源以外的都是私有资源
-
AndroidManifest.xml 合并冲突
Android Studio工程通常包含多个AndroidManifest文件,最终构建成APK时,会合并成一个AndroidManifest文件。
合并冲突: 是指多个Manifest文件中含有同一属性但值不同时,默认合并规则解决不了从而导致的冲突。
当冲突发生时,高优先级的Manifest属性值会覆盖低优先级属性值。这个优先级规则由高到低依次是:
buildType下的Manifest设置 > productFlavor下的Manifest设置 > 主工程src/main > dependency&library清单文件合并规则.png合并规则标记:
1. 节点标记:
tools:node="replace"
:在高优先级 Manifest 中添加,完全替换低优先级
2. 属性标记:
tools:remove="attr"
3. 标记选择器:
selector
5. 组件间解耦(页面跳转解耦+组件间通信解耦):
Splash(壳工程)--> 首页(module)-->意见反馈(module)
以上三个需要交互的页面处于三个不同的Module,组件不可能完全拆分的干净,势必会有交互和通信;
-
Android 原生支持 URL Scheme/AIDL(Service)
protocol://host:port/path?params <data android:scheme="protocol" android:host="host" android:port="8080" android:path="/path" /> Intent intent = new Intent(Intent.ACTION_VIEW,Uri.parse("url"));
Scheme 跳转解耦每个页面都需要在Manifest配置,扩展性差,跳转过程无法控制,参数传递困难 。 适合H5跳转到原生应用,如果原生跳转使用这个的话,规则太多,清单文件都需要去定义,非常不灵活。
AIDL(Service) 解耦,各组件都需要维护AIDL文件,相对比较复杂。
-
基于注解的路由框架(Arouter)
特点:
- 直接URL路由&参数解析赋值
- 支持多模块项目
- 支持InstantRun
- 允许自定义拦截器(AOP)
- 提供IOC容器(控制反转)
- 隐射关系自动注册
- 灵活的降级策略
路由编译注解:
给每个Activity/Fragment都加上一个注解标记(路径),最后编译的时候把这些路径都生成在自己组件 apt 目录(build)下,真正运行的时候,扫描所有组件的路径,得到所有的映射关系,path(key)对应Activity(value),path 对应 Activity,跳转的时候就可以找到路径进行跳转。其实是个Map<String,RouteMeta>,好处是,所有的映射关系不是通过反射完成,而是在编译期间生成。拦截编译注解:
组件化跳转时候不知道其他组件申明了什么Activity,直接跳转有可能找不到对应Activity,所以这里应该有一种降级的策略,如果没有找到对应Activity,可以提示用户而不是直接Crash。或者是验证登录。-
参数注入注解:
@Autowired String name;
ARouter特点:
1)APT编译期生成关系映射表
2)运行期通过路径查找对应页面进行跳转
3)页面跳转完全解耦,不需要访问你那个类
4)缺点是 path、params非常量,可能会写错(需要自己管理)
ARouter不同组件接口调用:
提供IProvider:
面向接口编程:通过接口进行通信。
通信双方必须共同依赖该接口(缺点)。
如果我没有这个接口根本就调不到。
可以写到CommonLib中,但是不是好的方式。
组件间解耦思考?
- 组件间提供的页面以及接口路径,是以字符串还是常量的方式?如果是常量形式,常量放在哪?
- 组件间数据传递,非普通类型如何传递?Bean 应该放在哪个模块?
需要传递的Bean位置:依赖包或CopyTask拷贝到CommomLib(公共的地方其实也就是)
解决方案:
创建组件的时候,同时创建该组件的依赖包。
组件通过Service依赖包提供自身可以被访问的页面路径常量,接口常量,以及需要传递参数的所有的Bean。 Module 之间依赖对方的 依赖包,通过依赖包中的公共部分提供通信基础。
缺点是组件多的时候 Module 会变 Double,解决方案是通过 Gradle Copy Task 拷贝到CommonLib 目录下,具体:
依赖包不用单独建立Module,直接写在自己Module中,按照一定规则的路径存放。然后通过Gradle的脚本拷贝到 CommonLib目录下。(其实最后还是放到了公共的CommonLib目录下)
6. 组件初始化
组件何时何地初始化:每个module实现一个入口类,用于统一调度(类似Application)。
7. 组件化可能遇到的坑
-
R文件:
App 项目生成的 R 文件是 static final(常量)的, ADT14后 library 项目中生成的并不是final类型。
影响:- switch...case...
- ButterKnife 注解
其依赖于常量,所以当R文件不是常量时会失效。(R2或放弃使用..)
-
库发布
默认情况下,library只发布release版本,当app时debug版本(未混淆),而 library 是已经混淆过的,而会导致编译问题。
具体可参考一、在library module中的build.gradle中设置如下:
android{ publishNonDefault true //不让发布默认release版本 }
二、在主 module 的build.gradle设置如下:
dependencies { releaseCompile project(path: ':library', configuration: 'release') debugCompile project(path: ':library', configuration: 'debug') //flavor1Compile project(path:‘:lib_name’,configration:'release') //flavor2Compile project(path:‘:lib_name’,configration:'debug') }
这样可以让app和library的debug和release保持一致
-
重复依赖:provided project 、exclude module
-
Project 重复依赖
if(isApp_News.toBoolean()){ compile project(':CommonLib') }else{ provided project(': CommonLib')//去除依赖 }
-
Duplicate entry(多个入口)
//exclude 命令 compile('com.jakewharton:butterknife:8.5.1'){ exclude module:'support-compat' }
-
- 其他:
DataBinding、Dagger、Retrolambda等第三方、多渠道打包、混淆和加固、Application Context
相关认识:
组件化的实施对开发人员和团队管理者提出了更高水平的要求.相对传统方式,在项目的管理和组织上难度加大,要求开发人员对业务有更深层次上的理解.
组件化首要做的事情就是划分组件.如何划分并没有一个确切的标准,建议早期实施组件化的时候,可以以一种”较粗”的粒度来进行,这样的好处在于后期随着对业务的理解和熟悉进行再次细分,而不会有太大的成本
这样的技术其实对于纯开发而言难度是不大的,真正的难度在于如何剥离现有的业务线。粒度大拆分比较容易,但是不利于今后的维护。粒度小需要对业务有很深的理解,但是能很好的解耦并且提高灵活度,所以具体的情况需要在具体的实际开发中进行分析。
组件化开发不是银弹,并不能完全解决当前业务复杂的情况,在进行项目实施和改进之前,一定要多加考量.
对当前项目实施组件化我的一些理解
组件化优点:
- 提高团队开发效率(并行开发)
- 项目结构清晰(多个业务 Module)
- 降低代码耦合性(各个业务组件相互隔离)
- 复用已有组件
一些组件化参考链接:
Android-组件化如何处理多个ModuleApplication共存问题?
使用阿里ARouter路由实现组件化(模块化)开发流程+源码
Android组件化框架设计与实践
我所理解的Android组件化之通信机制
Demo