原文作者:简书 - 四单老师

项目背景

主站业务经历了长期的迭代维护,业务的增长同时带来每个版本业务量繁重,迭代周期很快。同时团队也在不断的扩张,对应拆分了组内不同的业务线对接不同业务线的需求,最初的Android客户端单一的设计架构已经逐渐不满足快速的业务开发需求。历经组内讨论开始对项目整理进行组件化的迁移,通过组件化的方式满足不同业务线业务开发的稳定性,是迭代开发更灵活,组内协作开发效率得到提升。同时又有新的项目立项需要投入开发,一方面可以通过新项目实践和推进组件化的迁移,另一方面也可以通过组件化拆分后的技术组件复用来更快的搭建和开发新的项目。

组件化的准备

技术准备

1. 主站最初的app项目只有一个模块,业务耦合严重,技术组件很难复用,所以我们采取的第一步是拆分部分基础组件下沉为一个Base库,尽量去解耦业务提取基础技术组件达到多业务模块的复用,也是为了支持新项目和主站项目多个app的技术支持。

2. 考虑组件化后的业务相对隔离,但是客户端组件间需要建立访问,所以需要组件间通信的介入。我们采取的方式是路由、服务和全局通知。

3. 搭建路由库支持,目的是解决业务组件物理隔离后的UI跳转和访问,通过维护路由表的方式寻址到需要访问的业务组件UI。我们采取的是技术实现是通过注解给对应的业务UI比如LoginActivity上用注解申明对应的路由地址,在公共依赖的接口处公开维护这个路由地址常量,暴露给其他业务组件通过方位该地址来跳转到对应的业务组件UI。

@Router(RouterPath.LOGIN_PAGE)
public class LoginActivity extends BaseCompatActivitypublic class RouterPath {/*** 登录*/public static final String LOGIN_PAGE = "/native/youpin-login.html";/*** 搜索key*/public static final String SEARCH_KEY = "/native/youpin-search-key.html";...
}

对应Act绑定上路由地址后,需要对路由的地址进行统一的收集管理。同时也为了支持某些服务动态下发的地址,策略是优先在本地的路由表进行匹配,如果查询到了该地址有对应的Native界面优先跳转到Native的界面,未匹配到则跳转到由webView容器承载的网页。目前我们采取的方式是通过APT自动生成对应路由注解后的activity的收集类。

//自动生成的类,命名规则是RouterGenerator+业务组件模块名称
//RouterGenerator_login.classpublic class RouterGenerator_login implements RouterProvider {public RouterGenerator_login() {}public void loadRouter(Map<String, Route> routerMap, Map<String, Route> pageNameRouterMap) {String keyLoginActivity = "((https|http|yiupin|native)://(w+.)?yiupin.com/native/youpin-login.html)|(" + RouteBuilder.generateUriFromClazz(LoginActivity.class) + ")";routerMap.put(keyLoginActivity, RouteBuilder.build(keyLoginActivity, 0, false, (String[])null, LoginActivity.class));}
}

然后再通过ASM的方式在编译期对所有加载到工程里面的模块组件通过特定的规则进行上面路由辅助类的收集。

//收集路由地址
['scanInterface'        : 'com.kaola.annotation.provider.RouterProvider','scanSuperClasses'     : [],'codeInsertToClassName': 'com.kaola.core.center.router.RouterMap',//未指定codeInsertToMethodName,默认插入到static块中,故此处register必须为static方法'registerMethodName'   : 'register','include'              : ['com/kaola/annotation/provider/result/.*'
]//根据工程依赖的所有组件模块收集所有实现RouterProvider的辅助类。
//然后插入到RouterMap的静态代码块中,默认调用无参构造。
//遍历执行RouterMap中的静态方法register,添加所有路由地址信息到全局路由表sRouterMap中。public class RouterMap {private static Map<String, Route> sRouterMap = new ConcurrentHashMap<>();private static Map<String, Route> sPageRouterMap = new ConcurrentHashMap<>();private static void register(RouterProvider routerProvider) {routerProvider.loadRouter(sRouterMap, sPageRouterMap);}
}

具体实现不再此展开了,此方式的好处就是可以根据需求加载需要的业务组件并且实现自动注册和收集路由到路由表。如果觉得独立开发路由库的成本较高,也可以采取业界主流的一些路由库比如ARouter等,基本类似。

4. 关于组件间服务通信的方式,目前采取的是暴露对应的服务接口供各个业务组件方调用。每个业务组件都会申明需要对外暴露提供的方法,并在自己的业务组件模块内实现这些具体被调用的方法。对外接口库根据模块划分,可以申明和维护通信间的一些数据类型,比如公开的数据model和对应需要访问的一些路由地址等。为了便于服务的动态收集,这些服务接口可以统一的继承某个规则接口,然后采取上述路由的方式,对所有实现了该规则接口的服务接口统一的收集管理。

facade/pay/model/PayModel.classIPayService.class//IService.class,统一对继承IService的服务接口的具体实现类进行收集interface PayService : IService {fun startH5PaySercive(context: Context)}pay_module/PayServiceImpl.classclass PayServiceImpl : PayService {override fun startH5PaySercive(context: Context) {//...}}

剩下一些特点场景的业务,比如:登录成功后需要全局通知刷新多个UI某个业务状态的时候,目前采取EventBus的方式进行订阅通知。

5. 在组件base库一定下沉和组件间通信方式的确立,开始对组件的具体的拆分粒度进行划分。大致划分为业务组件和技术组件两部分。

组件化的拆分流程

拆分前的考虑

考虑新的项目投入的人力资源有限,并且需要快速的开发上线,同时业务也有重合的场景。所以当时采取的开发策略是将主站未组件化的代码完全拷贝一份到新项目,并在此的基础上进行改造。改造的原则必须遵循2个应用共建同一套BaseLib,但是由于主站的BaseLib里面会耦合一些自身的业务组件,同时避免对BaseLib的修改影响到主站的业务开发而增加不必要的工作量。当时采取的策略是通过增加一层业务基础组件库来做新项目组件化拆分的缓冲层BaseCompatLib。

拆分过程

拆分过程中有很多业务组件共用的情况,结合当时的开发周期可以适当的去解耦部分业务组件重新划分到对应拆分后的业务模块中。如果时间有限,可以先挪到BaseCompatLib这个缓冲成暂时共用待后续再拆,从而避免对2个项目共用的Base库频繁修改带来的负担。

初期的业务模块独立编译的配置方式,仅供参考:

//gradle.properties中申明编译配置是否是独立编译
# Module Build
isModuleInjectBuild=true//moduleLibrary的build.gradle中申明编译方式
if (isModuleInjectBuild.toBoolean()) {apply from: '../build_module.gradle'
} else {apply from: '../build_app.gradle'
}//新建一个appbuild文件,用来支业务组件以app方式编译时所需的配置
//示例:
java/appbuild/BuildInfo.class //独立配置HomeServiceImpl.class //改写应用启动跳转的UIApp.class //独立编译时的application,用于初始化配置android {//配置源码路径sourceSets {main {jniLibs.srcDirs = ['src/main/jnilibs']//如果是整体编译,可以移除独立编译所需的额外代码if (isModuleInjectBuild.toBoolean()) {java {exclude 'appbuild/**'}}}}
}

遇到的问题

拆分后的独立模块由于一些基础服务的初始化仍停留在app壳工程,一些sdk或者初始化服务没有统一的管理。优先级混乱并且耦合大量的业务逻辑,导致业务模块拆分后无法独立运行,缺失对应组件所需服务的初始化步骤。开始改造初始化的业务,原理同自动收集一致。

interface IInitializer {fun loadInQueue(queue: PriorityQueue<InitialTask>) //收集需要的服务进队列fun init(processName: String) //对应初始化服务的实现
}class InitialManager {companion object {private val mInitializerQueue = PriorityQueue<InitialTask>() //服务队列private var mCurProcessName: String = "" //当前启动的进程//应用初始化时的调用的入口函数@JvmStaticfun initial(curProcessName: String) {mCurProcessName = curProcessNameinitialInProcess()}@JvmStaticfun initialInProcess() {loop@ while (mInitializerQueue.isNotEmpty()) {  //搜索接入了多少三方sdk功能,总任务队列val initialTask = mInitializerQueue.poll() //按优先级取//根据是否拥有权限去加载普通任务//特殊不需要检查权限的任务,包括:Config和Permission初始化本身的任务。//目前这些优先级必须高于普通任务,否则会被提前打断,等到权限获取后才会执行。when {PermissionUtils.isNecessaryPermissionGranted() || initialTask.isNoNeedPermissionCheck() -> {executeTask(initialTask)}else -> {//一旦被权限检查打断不能执行,取出的任务重新放回队列。跳出任务队列,等待权限获取后的再次执行。mInitializerQueue.add(initialTask)break@loop}}}}/*** 执行任务,匹配对应进程,对应进程启动对应需要初始化的任务,沿用主站的逻辑*/private fun executeTask(initialTask: InitialTask) {initialTask.processName.forEach {//当前进程和服务需要初始化的进程相匹配或者是全进程需要就加载if (it == mCurProcessName || it == InitialTask.INITIAL_ALL_PROCESS) {Log.d("InitialManager", "initial - process:$mCurProcessName & initialTask:${initialTask.initialName}")initialTask.initializer.init(mCurProcessName)return@forEach}}}@JvmStaticfun register(initializer: IInitializer) {initializer.loadInQueue(mInitializerQueue)}}//示例服务
class QiyuSdkInitial : IInitializer {override fun loadInQueue(queue: PriorityQueue<InitialTask>) {//主进程需要val initialTask = InitialTask(processName = mutableListOf(ProcessConst.MAIN_PROCESS, ProcessConst.NIM_PROCESS),initialName = this::class.java.simpleName,initializer = this)queue.add(initialTask)}override fun init(processName: String) {try {QiyuSdk.initUnicorn(AppDelegate.sApplication)} catch (e: Throwable) {e.printStackTrace()}}
}

彻底组件化

组件库的独立发布和维护

原有拆分的本地组件彻底分离出去,采取独立发布和维护的方式迭代更新。

  1. 新建git仓库和本地组件项目,然后以module的方式将原有项目中的业务module导入到本地新建的项目中。推送该项目到git的独立仓库。(目前未采取git subModule的 方式管理,但大致差不多)
  2. 新建的本地项目中再新建一个对应的接口工程用于对外暴露模块中的业务访问。
project: component-login/app            //壳工程/login          //登录模块module/login-facade   //登录模块接口module

  1. 添加打包aar发布到maven仓库的脚本用来独立发布login和login-facade模块。
  2. 遵循对应的发布规范,不同项目的app壳工程根据自身的业务需求进行对应的组件依赖,版本开发阶段可采取snapshot的进行依赖。不同的业务组件也可以通过依赖其他不同的业务组件接口达到访问的目的。(如需实际运行,不光需要再接入接口库还需要依赖对应的组件工程)
  3. 目前友品采取的是jenkins的打包发布方式,仅供参考。

本地开发调试模式

在组件开发过程中,单纯的依靠远程方式依赖,对开发阶段的频繁修改不友好。所以我们采取依赖覆盖的方式,让原有的依赖在编译过程中替换掉远程的版本改用本地的版本进行引用。

// 自定义const.gradle环境声明
def version = '1.5.11'
ext.sdk = [YpBase        : { "com.kaola:ypbase:${version}" }
]//app build.gradle
dependencies {api gradle.sdk.YpBase(this)
}//setting.gradle
gradle.ext {sdk = sdk
}
//本地依赖时需要修改为本地的路径
def YpBase_PATH = "localpath/base"
def YpBase_as_aar = []
def YpBase_as_sources = [['YpBase', ":base", ['type': 'project', 'path': "${YpBase_PATH}/base"]],
]
def overrideList = YpBase_as_aar
// *核心* 打开注释使用源码引入YpBase
overrideList = YpBase_as_sourcesdef overrideLibrary(Map define, String whichLibrary, String name, Map prjType) {def overrideType = prjType.get("type")if (overrideType == 'module') {include(name)define.put(whichLibrary, {it.project(name)})} else if (overrideType == 'project') {include(name)project(name).projectDir = new File(prjType.get('path'))define.put(whichLibrary, {it.project(name)})} else if (overrideType == 'aar') {define.put(whichLibrary, { prjType.get('path') })} else {; // ignore}
}for (int i = 0; i < overrideList.size(); i++) {def override = overrideList[i]println 'override: ' + override[0]overrideLibrary(sdk, override[0], override[1], override[2])
}

通过以上的方式让Base的依赖从远程替换为本地module的形式。开发阶段就可以通过AS的refactor进行代码的优化和重构,对本地Base修改后到Base的git分支进行对应的提交或MR合回主分支然后走规范的发布打包流程。

组件版本依赖管理

组件项目中会有对Base或者接口库的引用,对于Base我们可以选择compileOnly的方式,也可以选择直接依赖的方式。在集成到项目中后依赖会遵循gradle的依赖传递原则。特别注意:

  1. 避免环形依赖的产生。比如:facade -> base, base -> facade。遇到这种情况需要拆分所需依赖到另外一层。
  2. 在远程依赖替换为本地依赖做开发修改时可能会遇到远程依赖和本地依赖的冲突。比如:app -> login -> com.xxx:base; app -> home -> /localpath/base。 此时可以采取下面的方式进行依赖优先选择本地的方式排除掉其他组件中的远程依赖。
//setting.gradle
def base_exist = false
for (int i = 0; i < overrideList.size(); i++) {def override = overrideList[i]println 'override: ' + override[0]if (override[0] == 'YpBase') {base_exist = true}overrideLibrary(sdk, override[0], override[1], override[2])
}gradle.ext {kulabase_exist = base_exist
}//app.gradle
if (gradle.kulabase_exist) {println 'kulabase_exist exist, exclude all aar dependences'android {configurations {all*.exclude group: 'com.xxx', module: 'base'}}
}

后续

到此为止基本上组件化就可以持续稳定的开发和维护了,组件化后也给团队的开发效率带来一定的提升,代码也可以在一定可控的范围内稳定的维护。并且在各自维护的组件中,大家也可以根据各自需求选择合适自己业务的开发框架比如:mvp、LiveData、Rx等或者尝试使用新语言Kotlin去编写。解决业务耦合带来的负担同时也使各个组件达到了较高的可复用性,灵活的支持不同的应用项目,达到可插拔的方式集成开发。后续项目也会做一些优化,针对版本依赖的管理和简化组件编译和发布集成的流程来提高协作开发的效率。

ASM自动收集参考:https://github.com/luckybilly/AutoRegister

android app.build文件_网易友品 Android 客户端组件化演进相关推荐

  1. java调用七鱼返回文件_网易七鱼 Android 高性能日志写入方案

    本文来自网易云社区 作者:网易七鱼 Android 开发团队 前言 网易七鱼作为一款企业级智能客服系统,对于系统稳定性要求很高,不过难保用户在使用中不会出现问题,而 Android SDK 安装在用户 ...

  2. android查看程序缓存文件,Android App的文件缓存目录

    Android app的文件缓存目录可以是app内置私有的目录,当然也可以选择外置sdcard目录 内置私有目录 /data/data/[packagename]/files 文件缓存目录,一般存小的 ...

  3. 如何实现接口统一入口_网易考拉Android App如何实现统一弹框

    摘要 在快速开发的背景下,经历了n个版本后的考拉Android App中已经存在了各种各样看似相同却各有差别的弹框样式.其中包括系统弹框和自定义弹框,并且在线上时常会出现IllegalArgument ...

  4. java路由总线_网易考拉Android客户端路由总线设计

    1.前言 $ e7 |  ~% L) i7 @7 B& t3 T5 h* P/ e2 s 当前,Android路由框架已经有很多了,如雨后春笋般出现,大概是因为去年提出了Android组件化的 ...

  5. 非静默授权没有弹出弹框_网易考拉Android统一弹框

    作者:钱成杰 链接:https://blog.csdn.net/jessicaiu/article/details/82739334 背景 在快速开发的背景下,经历了n个版本后的考拉Android A ...

  6. Android Ftp 下载文件:服务端搭建,客户端编写使用详情

    一  FTP与HTTP HTTP是超文本传输协议:面向网页的. FTP是File Transfer Protocol,文件传输协议:面向文件的. 1.FTP (1)FTP比HTTP复杂 FTP和HTT ...

  7. android app功能 配置,配置安装时分发  |  Android 开发者  |  Android Developers

    借助功能模块,您可以从应用的基本模块中分离某些功能和资源,并将其包含在 app bundle 中.然后,您可以自定义分发选项,以便控制搭载 Android 5.0(API 级别 21)或更高版本的设备 ...

  8. android强制全屏_如何强制任何Android应用进入全屏浸入模式(无生根)

    android强制全屏 Google released an Android version of Chrome in 2012, and have never bothered to give it ...

  9. 快速android app开发,快速學會開發 Android App

    快速學會開發Android App 作者:吳玉舒 / 臺灣大學計算機及資訊網路中心程式設計組幹事 智慧型手機發展迅速,其中以Android和iOS為主要的平台,根據 2012年6月Nielsen 的調 ...

最新文章

  1. OC学习篇之---对象的拷贝
  2. 敏捷团队迭代交付能力计算模型
  3. Markovdecisionprocesses_Discretestochasticdynamicprogramming下载
  4. boost::range::for_each相关的测试程序
  5. html页面怎么让高度充满屏幕,怎么让一个页面高度占整个屏幕的_html/css_WEB-ITnose...
  6. floor mod sqlserver_ORACLE和SQLServer-SQL语句的区别
  7. 如何使用Jemeter对HTTP接口压测
  8. [Ext JS 4] 实战之 Picker 和 Picker Field
  9. 《计算机网络基础与应用》笔记
  10. Word中的参考文献引用
  11. 区块链开发之验证ETH地址的有效性
  12. 发掘 iGoogle
  13. python源文件的扩展名是什么_python源文件后缀是什么
  14. 谈谈NVivo12的版本细节
  15. 程序设计:从圆的半径求得圆的面积
  16. 运行shell脚本时怎么知道jdk路径_linux查看java jdk安装路径和设置环境变量 – HouYing – 博客频道 – CSDN.NET...
  17. STC89C52RC最小系统程序下载方法
  18. 【软件测试】按照开发阶段划分:单元测试、集成测试、系统测试
  19. 中山联禾科技推出欧姆龙PLC联网模块
  20. 全闪SDS之数据库加速解决方案

热门文章

  1. Windows10安装TensorFlow GPU版本
  2. go 求两个时间相差的天数
  3. 思考:固态硬盘的普及,是否影响到了存储引擎的设计?
  4. 【Vue】脚手架 Vue CLI 的使用
  5. 【Java多线程】并发时的线程安全:快乐影院示例
  6. PAT1056 组合数的和 (15 分)
  7. JDK14中的java tools简介
  8. 《深入理解Java虚拟机》读后总结(一)JVM内存模型
  9. Java中的Atomic包使用指南
  10. PooledDirectByteBuf源码分析