美团外卖平台化复用主要是指多端代码复用,正如美团外卖iOS多端复用的推进、支撑与思考文章所述,多端包含有两层意思:其一是相同业务的多入口,指美团外卖业务须要在美团外卖App(下文简称外卖App)和美团App外卖频道(下文简称外卖频道)同时上线;其二是指平台上各个业务线,美团外卖不一样业务线都依赖外卖基础服务,好比登录、定位等。html
多入口及多业务线给美团外卖平台化复用带来了巨大的挑战,此前咱们的一篇博客《美团外卖Android平台化架构演进实践》(下文简称《架构演进实践》)也提到了这个问题,本文将在“代码复用”这一章节的基础上,进一步介绍平台化复用工做面临的挑战以及相应的解决方案。android
美团外卖App和美团App外卖频道业务基本同样,但因为历史缘由,两端代码差别较大,形成一样的子业务需求在一端上线后,另外一端几乎须要从新实现,严重浪费开发资源。在《架构演进实践》一文中,将美团外卖Android客户端平台化架构分为平台层、业务层和宿主层,咱们但愿可以在平台化架构中实现平台层和业务层的多端复用,从而节省子业务需求开发资源,实现多端部署。微信
两端业务虽然基本一致,可是仍旧存在差别,UI、基础服务、需求差别等。这些差别存在于美团外卖平台化架构中的平台层和业务层各个模块中,给平台化复用带来了巨大的挑战。咱们总结了两端代码的差别点,主要包括如下几个方面:网络
前期,咱们尝试经过一些设计方案来绕过上述差别,从而实现两端的代码复用。咱们选择了二级频道页(下文统称金刚页)进行方案尝试,设计以下:架构
其中,KingKongDelegate是Activity生命周期实现的代理类,包含onCreate、onResume等Activity生命周期回调方法。在外卖App和外卖频道两端分别基于各自的基础Activity实现WMKingKongAcitivity和MTKingKongActivity,分别会经过调用KingKongDelegate的方法对Activity的生命周期进行分发。框架
KingKongInjector是两端差别部分的接口集合,包括页面跳转(两端页面差别)、获取页面刷新间隔时间、默认资源等,在外卖App和外卖频道分别有对应的接口实现WMKingKongInjector和MTKingKongInjector。运维
NetworkController则是用Retrofit实现统一的网络请求封装,PageListController是对列表分页加载逻辑以及页面空白、网络加载失败等异常逻辑处理。模块化
在金刚页设计方案中,咱们采用了“代理+继承”的方式,实现了用统一的网络库实现网络请求,定义了统一的基础数据Model,统一了部分基础服务以及基础数据。经过KingKongDelegate屏蔽了两端基础Acitivity的差别,同时,经过KingKongInjector实现了两端差别部分的处理。可是咱们发现这种设计方案存在如下问题:工具
经过代码复用初步尝试总结,咱们总结出平台化复用,须要考虑四件事情:组件化
咱们在实现平台化架构的基础上,通过不断的探索,最终造成适合外卖业务的平台化复用设计:总体分为基础服务层-基础组件层-业务层-宿主层。设计图以下:
分层架构可以实现各层功能的职责分离,同时,咱们要求上层不感知下层的多端差别。在各层中进行组件划分,一样,咱们也要求实现调用组件方不感知组件的多端差别。经过这样的设计,可以使得总体架构更加清晰明朗,复用率提升的同时,不影响架构的复杂度和灵活度。
须要多端复用的业务相对于普通业务而言,最大的挑战在于差别化管理。首先多端的先天条件就决定了多端复用业务会存在差别;其次,多端复用的业务有个性化的需求。在多端复用的差别化管理方案中,咱们总结了如下两种方案:
分支管理经常使用于多个需求在一端上线后,须要在另外一端某一个时间节点跟进的场景,以下图所示:
两端开发1.0版本时,分别要在wm分支(外卖App对应分支)开发feature1和mt分支(外卖频道对应分支)开发feature2。开发2.0版本时,feature1须要在外卖频道上线,feature2须要在外卖App上线,则分别将feature1分支代码合入mt分支,feature2代码合入wm分支。这样经过拉取新需求分支管理的方式,知足了需求的差别化管理。可是这种实现方式存在两个问题:
在Android官网《配置构建变体》章节中介绍了Product Flavor(下文简称Flavor)能够用于实现full版本以及demo版本的差别化管理,经过配置Gradle,能够基于不一样的Flavor生成不一样的apk版本。所以,模块内部的差别化管理是经过Flavor来实现,其原理以下图所示:
其中Common是两端复用的代码,DiffHandler是两端差别部分接口,WMDiffHandler是外卖App对应的Flavor下的DiffHandler实现,MTDiffHandler是外卖频道对应Flavor下的DiffHandler实现。经过两端分别依赖不一样Flavor代码实现模块内差别化管理。
对于需求在两端版本差别化管理,也能够经过配置Flavor来实现,以下图所示:
在1.0版本时,feature1只在外卖App上线,feature2只在外卖频道上线。当2.0版本时,若是feature一、feature2须要同时在两端上线,只须要将对应业务代码移动到共用SourceSet便可实现feature一、feature2代码复用。
综合两种差别代码实现来看,咱们选择使用Flavor方式来实现代码差别化管理。其优点以下:
从Android工程结构来看,使用Flavor只能在module内复用,可是以module为粒度的复用对于差别化管理来讲约束过重。这意味着同个module内不一样模块的差别代码同时存在于对应Flavor目录下,或者说须要将每一个子模块都建立成不一样的module,这样管理代码是很是不便的。《微信Android模块化架构重构实践》一文中提到了一个重要的概念pins工程,pins工程能在module以内再次构建完整的多子工程结构。咱们经过创造性的使用pins工程+Flavor的方案,将差别化的管理单元从module降到了pins工程。而pins工程能够定义到最小的业务单元,例如一个Java文件。总体的设计实现以下:
具体的配置过程,首先须要在Android Studio工程里首先要定义两个Flavor:wm、mt。
productFlavors { wm {} mt {} }
而后使用pins工程结构,把每一个子业务做为一个pins工程,实现以下Gradle配置:
最终的工程目录结构以下:
以名为base的pins工程为例,src/base/main是该工程的两端共用代码,src/base/wm是该工程的外卖App使用的代码,src/base/mt是外卖频道使用的代码。同时,咱们作了代码检查,除了base pins工程能够依赖之外,其余pins不存在直接依赖关系。经过这样实现了module内部更细粒度的工程依赖,同时配合Gradle配置能够实现只编译部分pins工程,使总体代码更加灵活。
经过pins工程+Flavor的差别化管理方式,咱们既实现了需求级别的差别化管理,也实现了模块内的功能差别化管理。同时,pins工程更好的控制了代码粒度以及代码边界,也将差别代码控制在比module更小的粒度。
对于一个App来讲,基础服务的重要性不言而喻,因此在平台化复用中,每每基础服务的差别最大。因为基础服务的使用范围比较广,若是基础服务的差别得不到有效的处理,让上层感知到差别,就会增长架构层与层之间的耦合,上层自己实现业务的难度也会加大。下文里讲解一个咱们在实践过程当中遇到的例子,来阐述咱们的主要解决思路。
在前期探索章节中,咱们提到金刚页因为两端基础Activity差别,以至于要使用代理类来实现Activity生命周期分发。经过采用统一接口以及Flavor方式,咱们能够统一两端基础Activity组件,以下图所示:
分别将两端WMBaseActivity和MTBaseActivity的差别接口统一成DialogController、ToastController以及ActionBarController等通用接口,而后在wm、mt两个Flavor目录下分别定义全限定名彻底相同的BaseActivity,分别继承MTBaseActivity和MTBaseActivity并实现统一接口,接口实现尽可能保持一致。对于上层来讲,若是继承BaseActivity,其可调用的接口彻底一致,从而达到屏蔽两端基础Activity差别的目的。
对于一些通用基础组件,因为使用范围比较广,若是不统一或者差别较大,会形成业务层代码实现差别较大,不利于代码复用。因此咱们采用的策略是外卖App向外卖频道看齐。代码复用前,外卖App主要使用的网络库是Volley,统一切换为外卖频道使用的MTRetrofit;外卖使用的图片库是Fresco,统一切换为外卖频道使用的MTPicasso;其余统一的组件还包括动态加载框架、WebView加载组件、网络监控Cat、线上监控Holmes、日志回捞Logan以及降级限流等。两端代码复用时,修复问题、监控数据能力方面保持统一。
对于登陆、定位等通用基础服务,咱们的原则是能统一尽可能统一,这样能够有效的减小多端复用中来带的多端维护成本,多份变成一份。而对于没法统一的服务,抽象出统一的服务接口,让上层不感知差别,从而减小上层的复用成本。
组件化能够大大的提升一个App的复用率。对于平台化复用的业务而言,也是同样。多个模块之间也是会常用相同的功能,例以下拉刷新、分页加载、埋点、样式等功能。将这些经常使用的功能抽离成组件供上层业务层调用,将能够大大提升复用效果。能够说组件化是平台化复用的必要条件之一。
面对外卖App包含复杂众多的业务功能,一个功能能够被拆分红组件的基本原则是不一样业务库中不一样业务的共用的业务功能或行为功能。而后按照业务实现中相关性的远近,自上而下的依赖性将抽离出来的组件划分为基础通用组件、基础业务组件、UI公共组件。
基础通用组件指那些变化不大,与业务无关的组件,例如页面加载下拉刷新组件(p_refresh),日志记录相关组件(p_log),异常兜底组件(p_exception)。基础业务组件指以业务为基础的组件:评论通用组件(p_ugc),埋点组件(p_judas),搜索通用组件(p_search),红包通用组件(p_coupon)等。UI公共组件指公用View或者UI样式组件,与View 相关的通用组件(p_widget),与UI样式相关的通用组件(p_theme)。
对于抽离出来的基础组件,多端之间的差别怎么处理呢? 例如兜底组件,外卖兜底样式以黄色为主调,而外卖频道中以绿色小团为主调,如图所示:
咱们首先将这个组件划分为一个pins工程,对于多端的差别,在pins工程里面利用Flavor管理多端之间的差别。这样的方案,首先组件是一个独立的模块,其次多端的差别在组件内部被统一处理了,上层业务不用感知组件的实现差别。而因为基础服务层已经将差别化管理了,组件层也不用感知基础服务的差别,减小了组件层的复用成本。
对两端同一个页面来讲,绝大部分的功能模块是可复用的,可是也存在不一致的功能模块。之外卖App和美团外卖频道首页为例,中部流量区等业务基本相同,可是顶部导航栏样式功能和中部流量区布局在两端不同,以下图所示:
针对上述问题,咱们页面复用的实现思路是页面模块化:先将页面功能按照业务类似性以及两端差别拆分红高内聚低耦合的功能单元Block,而后两端页面使用拆分的功能单元Block像搭积木似的搭建页面,单个的单元Block能够采用MVP模式实现。美团点评内部酒旅的Ripper和到店综合Shield页面模块化开发框架也是采用这样的思路。因为咱们要实现两端复用,还要考虑页面之间的差别。对于两端页面差别,咱们统一使用上文中提到的Flavor机制在业务单元内对两端差别化管理,业务单元所在页面不感知业务单元的差别性。对于不一样的差别,单元Block能够在MVP不一样层作差别化管理。
以首页为例,首页Block化复用架构以下图。两端首页头部导航栏UI展现、数据、功能不同,导航栏整个功能就以一个Flavor在两端分别实现;商家列表中部流量区部分虽然总体UI布局不同,可是里面单个功能Block业务逻辑、整个数据同样,继续将中部流量区里面的业务Block化;下方的商家列表项两端同样的功能,用一个公有的Block实现。在各个单元Block已经实现的基础上,两端首页搭建成首页Fragment。
页面模块化后,将两端不一样的差别在各个单元Block以Flavor方式处理,业务单元Block所在页面不用关心各个Block实现差别,不只实现了页面的复用,各个模块功能职责分离,还提升了可维护性。
美团外卖业务须要在外卖平台和美团平台同时部署,所以,在美团外卖平台化架构过程当中就产生了平台化复用的问题。而怎么去实现平台化复用呢?笔者认为须要从不一样粒度去考虑:基础服务、组件、页面。对于基础服务,咱们须要尽量的统一,不能统一的就抽象服务层。组件级别,须要分块分层,将依赖梳理好。页面的复用,最重要的是页面模块化和页面内模块作到职责分离。平台化复用最大的难点在于:差别的管理和屏蔽。本文提出使用pins工程+Flavor的方案,可使得差别代码的管理获得有效的解决。同时利用分层策略,每层都本身处理好本身的差别,使得上层不用关心下层的差别。平台化复用不能单纯的追求复用率,同时要考虑到端的个性化。
到目前为止,咱们实现了绝大部分外卖App和外卖频道代码复用,总体代码复用率达到88.35%,人效提高70%以上。将来,咱们可能会在外卖平台、美团平台、大众点评平台三个平台进行代码复用,其场景将会更加复杂。固然,咱们在作平台化复用的时候,要合理地进行评估,复用带来的“成本节约”和为了复用带来的“成本增长”之间的比率。另外,平台化复用视角不该该局限于业务页面的复用,对于监控、测试、研发工具、运维工具等也能够进行复用,这也是平台化复用理念的核心价值所在。
晓飞,美团点评技术专家。2015年加入美团点评,外卖Android的早期开发者之一。目前是外卖Android App负责人,主要负责版本管理和业务架构。
金光,美团点评高级工程师。2017年加入美团点评,主要负责代码复用及外卖平台化相关工做。
王芳,美团点评高级工程师。2017年加入美团点评,主要负责商家列表页面等相关页面业务。
美团外卖长期招聘Android、iOS、FE 高级/资深工程师和技术专家,Base 北京、上海、成都,欢迎有兴趣的同窗投递简历到wukai05@meituan.com。