🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
[TOC] ## 原架构模型 单一工程模型:逻辑分层模型应该也是最常见的项目结构模型,口袋助理现有的框架,就是基于逻辑分层模型的拓展。图中可以看出,app下的目录要将近80+个,其中: * 有属于业务层的目录(cloud、crm\_order等) * 有属于中间件层的目录(acl、advert等) * 也有基础组基层的目录(utils、ui、uin等) ![](https://img.kancloud.cn/42/07/4207b9ade37a7c038ca7a9c6f8edb516_692x267.png) ![](https://img.kancloud.cn/e2/bf/e2bfa9405571e6af242969ca4bf060ce_891x304.png) ## MVC,MVP,MVVM [Android App的设计架构:MVC,MVP,MVVM与架构经验谈](https://www.tianmaying.com/tutorial/AndroidMVC), ## 组件化 模块化 插件化 组件化和模块化是很类似的一对概念,原则上说在一些场景上进行同义替换是没有问题的,两者都是对代码结构的“由大到小”的调整,两者的目的都是为了重用和解耦. * 模块化 * 避免重复造轮子,节省开发维护成本; * 降低项目复杂性,提升开发效率; * 多个团队公用同一个模块,在一定层度上确保了技术方案的统一性 * 组件化 * 业务模块间解耦 * 单个业务模块单独编译打包,加快编译速度 * 多团队间并行开发、测试 如果一定要进行区分组件化和模块化的话 模块化 * 更类似于一种功能的切分,比如很多公司都常见的,网络请求模块、登录注册模块单独拿出来,交给一个团队开发,而在用的时候只需要接入对应模块的功能就可以了 * 在代码结构上没有过多的要求,不同模块的源代码可以直接放在放在主项目下 * 在编译上,模块化下的模块必须依赖于主项目,在主项目中被调用和集成后,才能进行测试和运行 组件化 * 更倾向于业务的切分,比如打开淘宝或者大众点评来看看,里面的美食、电影、酒店、外卖就是一个一个的业务 * 在代码结构上,组件化有更高的要求,一般来讲都需要单独的独立目录,并且组件之间保持低耦合 * 在编译上,Debug期间,组件可以不依赖于主项目,而单独进行测试和运行,甚至可以独立打包发布;Release版本下,则可以灵活的作为aar组合式的实现主项目功能; * 插件化则不同,插件化是在开发时就将整个app拆分成很多apk模块,这些模块包括一个宿主和多个插件,每个模块都是一个apk(组件化的每个模块是个lib),最终打包的时候将宿主apk和插件apk分开或者联合打包 * 可以这么理解:插件化是在[运行时],而组件化是在[编译时]。换句话说,插件化是基于多 APK 的,而组件化本质上还是只有一个 APK。 ## 主App多Lib开发模型 借助组件化这一思想,我们在”单工程”模型的基础上 * 将业务层中的各业务抽取出来,封装成相应的业务组件; * 将基础库中各部分抽取出来,封装成基础组件 * 主工程是一个可运行的app,作为各组件的入口(主工程也被称之为壳程序) * 这些组件或以jar的形式呈现,或以aar的形式呈现.主工程通过依赖的方式使用组件所提供的功能 ![](https://img.kancloud.cn/1b/3e/1b3e1fd8b55b14497616e82e201749fa_578x386.png) ## 模块化实施之路 ### 要求 1. 必须与其他开发并行 2. 工作量要可控,可执行分步执行 3. 控制风险(不改变原意) ### 步骤 第一环节:从app module向下独立出基础组件 第二环节:将整个原来的app module作为中间层,向上独立出业务 module. 尽量不要并行,会导致混乱 第三环节:将app和中间层作为SDK,内部更新,不干扰上层业务module层 ## 问题一:拆分模块的粒度 ## 问题二:各个app中的Application 冲突和聚合 ### 困境 每一个组件都会需要【调用原始Application内部函数调用】 每一个组件都会需要【感知Application生命周期】 ### 解决内部函数调 方法下沉 ### 解决生命周期 其实解决思路很简单, 无非就是在开发时让每个组件可以独立管理自己的生命周期, 在运行时又可以让每个组件的生命周期与宿主的生命周期进行合并 (在不修改或增加宿主代码的情况下完成) ### 目前方案 类似观察者模式,譬如注册点击事件 2. 在基础层中定义有生命周期方法 (attachBaseContext(), onCreate() ...) 的接口 AppLifecyclesInterface 3. 在基础层中提供一个用于管理组件生命周期的管理类 ConfigModule 4. 每个组件都手动将自己的生命周期实现类AppLifecycles注册进这个管理类 5. 在集成调试时, 宿主在自己的 Application对应生命周期方法中通过管理类去遍历调用注册的所有生命周期实现类即可(通过反射AndroidManifest.meta对应路径方式) 6. 在调试时,每个业务module 都使用基类的 BaseApplication ### 其余方案 * 使用 AnnotationProcessor,解析注解在编译期间生成源代码自动注册所有组件的生命周期实现类, 然后宿主再在对应的生命周期方法中去调用//无法动态修改 * 在App中添加配置文件app\_libraries.properties,配置文件中添加对应的组件的Application,App初始化时读取配置文件中的内容通过反射调用组件中的Application的生命周期方法。 ## 问题三:Gradle文件的统一配置依赖管理 由于有诸多的module和子工程,如果各个子工程随意引入第三方工具包或不同版本的三方包,势必会导致软件项目的混乱 因此对各个module的Gradle文件进行统一配置管理也是十分有必要的 1. 详细可参看 [Gradle依赖的统一管理](https://mp.weixin.qq.com/s?__biz=MzA4NTQwNDcyMA==&mid=402733201&idx=1&sn=052e12818fe937e28ef08331535a179e&scene=23&srcid=0313t2uhyzGx2iNSAllnKimj#rd) 2. 更进一步的,由于很多组件都需要buid.gradle,里面基本很多东西都是一样的,可以在主工程新建一个文件夹并创建一个模板.gradle, 在其他module中直接引入,譬如[AndroidModuleDemo](https://github.com/hongxialiu/AndroidModuleDemo) ~~~ apply from: rootProject.file('script/library_work.gradle') ~~~ ## 问题四:被依赖module中BuildConfig.DEBUG的值总为false 在Android的实际开发中,一般会有这样的需求,debug和release版本不同,接口地址不同,同时控制日志是否打印等,系统为我们提供了一个很方便的类BuildConfig可以自动判断是否是debug模式 有了BuildConfig.DEBUG之后,你在代码中可以直接写入 ~~~ if (BuildConfig.DEBUG) { Log.d(TAG, "output something"); } ~~~ 但是在Android Studio中,被依赖module里BuildConfig.DEBUG的值总为false,因为Library projects目前只会生成release的包. 例如module A依赖module B和module C,在Eclipse里运行时B和C里BuildConfig.DEBUG的值会是true(导出签名apk后会自动变成false); 然而在android Studio里B和C里的BuildConfig.DEBUG值总是false,A里的正常。这样就导致if(BuildConfig.DEBUG){Log.d(...)}日志无法正常显示 具体解决方法参见 [(2.2.8.9) 解决被赖module中BuldConfig.DEBUG的值总为false问题](http://blog.csdn.net/fei20121106/article/details/73912634) ## 问题五:资源重名 因为我们拆分出了很多业务组件和功能组件,并在最后一起打包处理,在合并过程中就有可能会出现资源名冲突问题,例如A组件和B组件都有一张叫做“ic\_back”的图标,这时候在集成模式下打包APP就会编译出错 解决方式,总的来说可以分为两种方式:前缀限制和统一管理 我们可以混杂使用 * 全部图标、颜色、动画、raw、values、menu素材全部放入 Basic Component Layer 基础组件层的 CommonRes中 * 涉及公共的layout素材放入Basic Component Layer 基础组件层的 CommonRes中 * Middleware Layer 中间件层:各个中间件module的layout xml使用 前缀限制 * Business Module Layer 业务 Module 层:各个业务module的layout xml使用 前缀限制 * 前缀限制 * 因为分了多个 module,在合并工程的时候总会出现资源引用冲突,比如两个 module 定义了同一个资源名。 这个问题也不是新问题了,做 SDK 基本都会遇到,可以通过设置 resourcePrefix 来避免。设置了这个值后,你所有的资源名必须以指定的字符串做前缀,否则会报错。 ~~~ andorid{ defaultConfig { resourcePrefix "moudle_prefix" } } ~~~ 但是 resourcePrefix 这个值只能限定 xml 里面的资源,并不能限定图片资源,所有图片资源仍然需要你手动去修改资源名 另外一种方式是,将应用使用到的所有 res资源放到一个单独module中进行统一管理,尤其是图片和xml资源 ## 问题六:解决重复依赖 ### 使用api 默认情况下,如果是 aar 依赖,gradle 会自动帮我们找出新版本的库而抛弃旧版本的重复依赖。 但是如果你使用的是 project 依赖,gradle 并不会去去重,最后打包就会出现代码中有重复的类了。 通过将子App中的 compile 改为 provided,可实现只在最终的项目中 compile 对应的代码; ### exclude 排除重复三方包 ## 问题七:混淆方案 组件化项目的Java代码混淆方案采用在集成模式下集中在app壳工程中混淆,各个业务组件不配置混淆文件。集成开发模式下在app壳工程中build.gradle文件的release构建类型中开启混淆属性,其他buildTypes配置方案跟普通项目保持一致,Java混淆配置文件也放置在app壳工程中,各个业务组件的混淆配置规则都应该在app壳工程中的混淆配置文件中添加和修改。 ## 问题八:R.java文件的常量问题 ![](https://img.kancloud.cn/30/3a/303a6775cd69fc040533a759f1cd9238_415x282.png) 尤其注意一点,在app中的R.string.xx这样标量是一个 static final静态常量,而 lib中的 R.string.xx 则是static静态量,这是由于android的打包机制决定的,目前无法改变 因此在Debug模式下开发的时候,一定记得不能把 R中的变量 作为常量使用,譬如 switch case ## 问题九:业务组件组件化 ### 数据库组件化 ### 搜索组件化 ### 建联推送组件化 ## 问题十: 跨组件通信 因为各个业务模块之间是各自独立的, 并不会存在相互依赖的关系, 所以一个业务模块是访问不了其他业务模块的代码的, 如果想从 A Module的 A 页面跳转到 B Module的 B 页面, 或者 在A Module 中调用 B Module的某个函数,光靠模块自身是不能实现的, 所以这时必须依靠外界的其他媒介提供这个跨组件通信的服务; 跨组件通信主要有以下两种场景: ### 页面跳转与函数调用 统一对外暴露的接口,与使用什么路由框架无关,都可以做的可替换 需要值得注意的是:[Android架构建设之组件化、模块化建设](https://blog.csdn.net/xinanheishao/article/details/79980286)提出了一种基于ContentProvider 的交互方式,值得思考 ### class引用 这个主要靠类下沉 虽然可以使用 反射 或者 序列化,但这两者都不利于后续维护 ## 推荐阅读 [微信Android模块化架构重构实践](https://mp.weixin.qq.com/s?__biz=MzAwNDY1ODY2OQ==&mid=2649286672&idx=1&sn=4d9db00c496fcafd1d3e01d69af083f9&chksm=8334cc92b4434584e8bdb117274f41145fb49ba467ec0cd9ba5e3551a8abf92f1996bd6b147a&mpshare=1&scene=1&srcid=06309KcVegxww8kRannKXmkM&key=9965dca0b72a0a7428febd95a3bc61657924797129ae35d34f67f2cfc5c5ac09bec624714cd4662b978742d3424726f08b3ea1b9cb858cccf97dbb56bd5bfdd07a81917eedc452194d3c6b438d76dfac&ascene=0&uin=Mjg5NTY2MjM0MA==&devicetype=iMac%20MacBookPro11,4%20OSX%20OSX%2010.12.5%20build(16F73)&version=12020810&nettype=WIFI&fontScale=100&pass_ticket=X8yiKyEXbEsX7ouYBsjW0ddHl5Zc0CXaGzDaapndysc89C7Z257hmzlRaR3CQk)