京东iOS客户端组件管理实践

AudreaWilhi 2年前
   <p>编者按:从去年开始,关于iOS组件化的讨论和分享非常多,也形成了几种比较成熟的方案。组件多了,它们的依赖关系、版本等的管理成为问题,但这方面的分享很少。京东iOS不但实施了组件化,还专门开发了一套组件管理系统。希望京东的实践可以给大家一些参考思路。</p>    <h2>前言</h2>    <p>先大概交代下背景:京东的iOS客户端从2011年2月发布至今已历经6年+的时间,研发团队也从最终的几个人变成了N多人,业务的复杂度早已不可想象。</p>    <p>我个人认为一个超过了10人的团队做组件化是合适的,也有必要。当然少于10个人也应该去思考一下应用框架该如何演变,组织的这件事。</p>    <h2>目标</h2>    <p>对于每家应用还得结合实际业务来考虑,毕竟技术最终也是为了业务而服务。京东iOS组件化的目的从业务层面来讲主要是为了解决:多业务的并行集成,多部门的业务输出。</p>    <p>从技术层面来讲我们做组件化的目标是为了可以做到减缓代码腐化过程,加快编译时间,模块分权管理,代码规范,bug减少,独立开发、调试、自动化编译,集成等等工作(好像很厉害的样子)。更近一步的讲,我们需要一套自动化的系统来帮助我们完成所有的组件管理工作,让开发人员能更专注于代码层面,无需关心应用配置,渠道,以及如何集成等问题。</p>    <h2>组件管理演进之路</h2>    <p>任何事物都有一个演进的过程,就像罗马不是一天建成的一样。iOS这些年各种技术,花样层出不穷,好多公司,好多大牛在iOS组件化方面分享出了好多宝贵的经验,也让我们少走了许多弯路。关于iOS组件化的做法每家公司都大同小异,真正需要我们去深挖的应该是怎么把组件有序的管理起来,这也是我们想和大家探讨的内容,在讨论组件管理内容之前我们先简单说明下组件化实现的大概思路。</p>    <ul>     <li>代码解耦</li>     <li>Cocoapods管理</li>    </ul>    <h2>代码解耦</h2>    <p>首先入手的工作就是代码的解耦,这里其实没有太多的技术含量,只要胆大心细,有计划,一个工程总能被拆成一个个独立的模块。我们把每一个独立的模块就称之为组件,相互之间不能通过硬编码引用的方式进行调用。通信通过自定义的协议进行。</p>    <h3>组件原则</h3>    <ul>     <li>组件被定义为两种类型的组件:基础组件,业务组件。</li>     <li>基础组件可以被业务组件依赖,基础组件不可依赖业务组件。</li>     <li>业务组件不可依赖业务组件。</li>    </ul>    <h3>自定义协议</h3>    <p>组件之间通信遵循一套自定义的协议通信,实现的方式网上有些开源的项目,我们综合各家实现考虑,最后定出的一个方案:组件间的通信应该是轻量级的,调用完就走,不留痕迹,不需要维护通信数据。</p>    <p>大致实现如下:</p>    <p>(点击放大图像)</p>    <p><img src="https://simg.open-open.com/show/879baa5e600e3ffe061315e83595b075.jpg"></p>    <p>我们实现一个JDRouter的组件,用来实现组件与组件之间的通信。只暴露一个头文件,两个方法:输入,输出(宏规范)。</p>    <p>输入(A调用B)</p>    <pre>  <code class="language-objectivec">router://JDBClass/getString?name=Steven</code></pre>    <p>输出(B被A调用)</p>    <pre>  <code class="language-objectivec">+(id)getDataWithString:(NSString *)name {      NSString *str = [NSString stringWithFormat:@"HI, %@", name];      return str;  }</code></pre>    <p>输入说明</p>    <p>通过JDRouter调用,类似于有这样一个方法,完成a到b的通信</p>    <pre>  <code class="language-objectivec">id g = [JDRouter openURL:@"router://JDBClass/getString?name=steven" arg:nil error:nil completion:nil];</code></pre>    <p>输出统一</p>    <p>输入的方法统一了,输出也得统一,没有规矩不成方圆,但又不能通过说教的方式要求大家去提供输出方法。如果有一个统一的办法可以不需要协议注册,协议管理的机制,直接写一个类方法可以让JDRouter通过URI里的内容可以映射过去就好办了,我们使用宏替换,在JDRouter里提供一个输出的规范,类似这样:</p>    <pre>  <code class="language-objectivec">#define JDROUTER_EXTERN_METHOD(m,i,p,c) + (id) routerHandle_##m##_##i:(NSDictionary*)arg callback:(Completion)callback</code></pre>    <p>输出规范统一,将上面的类方法变为宏替换输出:</p>    <pre>  <code class="language-objectivec">JDROUTER_EXTERN_METHOD(JDBClass, eat, getString, callback) {        NSString *str = [NSString stringWithFormat:@"HI, %@", name];      return str;    }</code></pre>    <h3>AppDelegate入口解耦</h3>    <p>页面容器,第三方SDK初始化工作需要在启动时完成,我们通过hook AppDelegate,将入口所做的工作交给一个组件完成,在该组件中注册其它需要在启动时调用AppDelegate方法的组件,让每个组件都可以拥有一类似didFinishLaunchingWithOptions方法。</p>    <ul>     <li> <p>AppDelegateModule组件实现hook AppDelegate</p> </li>     <li> <p>入口组件</p> </li>    </ul>    <pre>  <code class="language-objectivec">+ (void) load {  NSArray *modules = @[@"MainModule"];  NSString *url = @"router://AppDelegateModule/setDidFinishLaunchingModules";  [JDRouter openURL:url arg:modules error:nil completion:nil];    //  NSString *urlrun = @"router://AppDelegateModule/run";  [JDRouter openURL:urlrun arg:nil error:nil completion:nil];  }</code></pre>    <p>在MainModule中实现AppDelegate的方法</p>    <pre>  <code class="language-objectivec">static UIWindow *gWindow = nil;  static UIViewController *gTempViewController = nil;  + (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions  {      gWindow = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];      [[[UIApplication sharedApplication] delegate] setWindow:gWindow];        gTempViewController = [[UIViewController alloc] init];      gTempViewController.view.backgroundColor = [UIColor redColor];      gWindow.rootViewController = gTempViewController;      [gWindow makeKeyAndVisible];      return YES;  }</code></pre>    <p>把实例方法改为类方法</p>    <p>将需要调用appdelegate方法的组件,在MainModule中注册,这里的modules是个数据,可解决调用delegate方法的顺序问题。</p>    <p>NSArray *modules = @[@"MainModule", @"需要调用的组件"];</p>    <h2>Cocoapods管理</h2>    <p>代码解耦很简单,只要遵循几个原则即可,最根本的问题就是业务与业务之间不能有耦合,不然组件化这件事就没有意义。我们通过Cocoapods把每一个组件都拆成独立的pod库。代码库管理选择gitlab(开源,提供API,可二次开发),后续需要对每一个组件进行权限管理,比如有一些涉及到安全的组件只有安全组的开发人员具体源码权限,其他人只能拿到二进制,再比如组件需要具有master的权限才可以进行发布,集成等工作。</p>    <p>关于Cocoapods,ruby,gitlab环境大家网上搜索一下。</p>    <p>准备工作</p>    <ul>     <li>通过Cocoapods搭建私有库,创建相应的模版。</li>     <li>不推荐Cocoapods编译二进制文件,自己写脚本编起来更灵活。</li>     <li>自定义gem,完成podspec源码二进制切换。</li>     <li>二进制文件(组件编译为静态包)存储到内部云。</li>     <li>使用工具/脚本管理podfile。</li>     <li>系统管理pod库。</li>     <li>工程结构</li>     <li>通过Cocoapods搭的自定义库,自定义模版。</li>    </ul>    <p>每个同学拿到的组件都是一个相同结构的工程,所需要做的工作就是在相应的Pods/Developemnt Pods/组件/Classes下编码,组件输出类通过模版创建,可在相应的类里使用JDROUTER_EXTERN_METHOD提供接口。</p>    <h2>iBiu组件管理系统</h2>    <p>以上是行业中对iOS组件化的一个大体思路。如果我们完全手动做这些工作的话,成本会很大,Cocoapods配置说明查询,组件版本依赖,统一集成等等。为了解决这些人为干预所引发的各种问题,我们研发了组件管理系统 <strong>iBiu</strong> 。</p>    <p>主App解耦工作和系统设计是同时进行的,所以最初系统只是为京东iOS主App所设计,在独立出一些组件后,我们就在思考一件事,让系统可管理京东其它的App。</p>    <h3>去应用化</h3>    <p>如果公司的所有应用全部都组件化,并且组件间统一协议通信,那么应用最终的输出方式应该就像工厂加工一样,加工过程就是组件组合过程,出厂时贴上标签。听起来很理想,事实上是可以做到的,从iBiu系统上线到目前一个月时间我们接入管理着三个应用包括主App,超过100个组件。</p>    <h3>组件配置表</h3>    <p>组件如何组合被抽象出一个组件配置表,记录了不同应用的组件配置,对应到具体的组件责任人,版本,对接产品,测试以及开发,通过工具一键完成组件的发布与集成等工作。</p>    <p>(点击放大图像)</p>    <p><img src="https://simg.open-open.com/show/a34d0c5a6ec567452bbd2ed1d7a9a324.jpg"></p>    <p>上图解释起来就是,假如有一个虚拟的App-A,在它下边有包含了一系列的组件,我们可以通过“组件配置表”(配置表里记录了组件的版本,是否为二进制,依赖等)对组件进行组合,最终输出我们想要的App。</p>    <p>再将上边的设计升级一下:</p>    <p>(点击放大图像)</p>    <p><img src="https://simg.open-open.com/show/8eebcf8e09fa3f049c89f7510c9f7d41.jpg"></p>    <p>让应用可以包含应用(这里所说的应用是一个虚拟的应用,或者叫做Collection更合适)。Collection可以任意包含另外的Collection,同时可以拿到Collection下的组件,如上图,App-A这个Collection最终是有另外三个Collection下的组件所组合而成。</p>    <h2>系统设计</h2>    <p>主要由三大块内容构成:</p>    <ul>     <li>脚本(提供开发环境,如果pod管理,git管理,文件管理等)</li>     <li>iBiu工具(可视化)</li>     <li>iBiu Server(后台管理,API)</li>    </ul>    <p>(点击放大图像)</p>    <p><img src="https://simg.open-open.com/show/5f104caedcb4a78e8513a4ca080815a5.jpg"></p>    <p>我们希望化繁为简,最终开发同学只需要安装一个可视化的工具就可以做到:组件注册,组合,发布,集成等工作,但基础工作还得一步步的来。</p>    <p>第一步:</p>    <p>通过pod命令创建相应的组件对应库</p>    <pre>  <code class="language-objectivec">pod lib create $lib_name --template-url=${TEMPLATE_URL}</code></pre>    <p>第二步:</p>    <p>将创建组件的脚本封装,我们希望把脚本放到iBiu Server这台机器上,让这个过程成为:开发者注册->审核->自动生成组件->代码提交Gitlab->通知开发者。</p>    <p>问题一个接一个的出现了:</p>    <p>坑:由于公司网络原因,不同网段同学无法访问一台工作网络环境中的机器。</p>    <p>坑:脚本创建组件这个过程依赖Xcode环境。</p>    <p>也就是说我们无法用一个Mac机器充当服务器,但又必须要Xcode环境。第一想法就是把iBiu Server部到线上环境,找一台Mac机器把脚本放上去,让iBiu Server访问这台Mac,但线上环境是完全返向不回来工作网络的。</p>    <p>坑:换线下环境,部署iBiu Server,还是访问不了工作网络中的这台Mac。</p>    <p>最后只能在iBiu Server这台机器上部了套Jenkins,生成组件的脚本部在Mac这台节点上,问题解决。(这块一直是个不太理想的做法,为了解决问题也只能这么做了)</p>    <p>第三步:</p>    <p>统一用户体系,ERP账号与iBiu、Gitlab打通。</p>    <p>第四步:</p>    <p>修改Podfile</p>    <p>如果是手动引入组件的话,每一个组件对应的Podfile可能会是下图这样,该组件所有的开发者,对该组件引入修改都有可能造成冲突。</p>    <p>(点击放大图像)</p>    <p><img src="https://simg.open-open.com/show/a4d7b7c9ef897141b578da0e945bf21c.png"></p>    <p>如果可以通过Podfile读取到“组件配置表”,而配置表又可以同步的话,能解决的问题就不只是冲突的问题了。</p>    <p>我们创建了一个biu gem,用来去处理配置表解析的工作,Podfile最终变成下图的样子,开发者无需手动修改它。</p>    <p>(点击放大图像)</p>    <p><img src="https://simg.open-open.com/show/c6cc4ea2f97d617b47ccd839872b3d2a.png"></p>    <p>所有工作都通过mekeup_pods这个方法完成。</p>    <p>第五步:</p>    <p>我们需要把每个组件在发布时都对应二进制文件输出,有关xcodebuild打包二进制的脚本就不在这里描述了。</p>    <p>将二进制统一输出为一个xxxx.framework,也方便查看</p>    <p>(点击放大图像)</p>    <p><img src="https://simg.open-open.com/show/a07a1a54a7645b542d5b266217e0e197.png"></p>    <p>脚本会把每次编译的负责人信息写到xxxx-umbrella.h文件中,方便问题跟踪。</p>    <p>(点击放大图像)</p>    <p><img src="https://simg.open-open.com/show/0fc08bcd70acb000dc36fff2f9dcc2bf.png"></p>    <p>第六步:</p>    <p>将所有shell脚本通过Packages打包,提供给所有开发者可以直接使用。</p>    <p>第七步:</p>    <p>iBiu可视化工具iBiu Server端开发,工具主体功能的开发时间并没有花了太多的时间,主要时间还是花在了梳理整个管理流程上。</p>    <p>第八步:</p>    <p>合并iBiu脚本,可视化工具,Packages打包。</p>    <p>(点击放大图像)</p>    <p><img src="https://simg.open-open.com/show/ff770858e2746cfb801330d21c88f11c.png"></p>    <h2>组件管理实践</h2>    <h3>系统及可视化工具</h3>    <p>通过工具申请组件</p>    <p>(点击放大图像)</p>    <p><img src="https://simg.open-open.com/show/27dfe553328fcbbdfbc9ccd00e3f63de.png"></p>    <p>开发者选择对应的App,输入相应的组件名等信息,将注册信息提交给iBiu Server,系统管理员会收到注册邮件,在后台完成审核。同时会系统邮件发给相应的开发者。</p>    <p>审核过程通过前面提到的,系统会自动完成创建pod库,git库,开发者权限分配等操作。</p>    <p>组件申请人即是该组件的负责人,拥用开发者权限分配,组件发布,集成,协议管理等权限。</p>    <p>后台管理开发者权限分配:</p>    <p>(点击放大图像)</p>    <p><img src="https://simg.open-open.com/show/2e8504601fcaab63734bca901a408ba2.png"></p>    <p>开发调试组合安装</p>    <p>开发者收到组件审核邮件后,根据git地址拉取代码,通过iBiu可视化工具打开组件工程,勾选要组合的组件进行安装。</p>    <p>所勾选的组件可以根据当前用户的权限分源码与二进制两种方式,是否依赖。</p>    <p>如果某用户所组合的某个组件需要调试,但没有源码权限,可找该组件的负责人要求开通权限。</p>    <p>(点击放大图像)</p>    <p><img src="https://simg.open-open.com/show/2e3285ebc4935bd935e5d16434dc1b9d.png"></p>    <p>生成组件配置表</p>    <p>(点击放大图像)</p>    <p><img src="https://simg.open-open.com/show/f5291ccbad159c83f8ecab9d53eae13d.png"></p>    <p>根据所勾选的组件,生成一张组件配置表,这时所组合安装生成的App即是一个根据配置表生成的工程。</p>    <p>坑:配置表是脱离组件工程独立存在的一个JSON描述文件,因为每个开发者对每一个组件所拥有的权限不一样,如果把配置表随工程代码一并提交,很可能造成表冲突,安装不了组件。解决办法就是使拥用master权限的用户可以将表内容同步至服务端,组件参与者下载同步表。然后在几个关键点验证表中组件对应权限,没有源码权限的提示用户自动切为二进制方式。</p>    <h3>组件安装过程</h3>    <p>(点击放大图像)</p>    <p><img src="https://simg.open-open.com/show/65c33c98c4b274c7616c456bf9ec0ef9.png"></p>    <p>配置版本组合安装</p>    <p>拥有组件master权限的用户可以将相应组件发布,集成到某个应用下的某个版本。</p>    <p>我们的应用迭代周期一般为一个月左右,对应到不同的项目,不同的产品,研发及测试。通过iBiu系统可以做到并行版以本开发,测试,集成。</p>    <p>组件的发布,集成不受项目的时间限制,随时可以发布。</p>    <p>App整体集成,组件的集成需要按项目流程进行,组件需要在App回归测试时前两天集成。点击iBiu可视化工具右上角集成按扭,选择组件版本,集成到某个应用的某个版本中。</p>    <p>每个应用的每个版本,在组件集成阶段生成相应的组件配置表,超过这个时间,应用版本锁定,组件将无法再集成到该应用版本中。</p>    <p>例如应用“京东”当前要发布AppStore的版本为6.1.3,发布时间7月19,倒推时间,灰度1周,集成测试为1周,那么所有该版本的组件必须在两周前两天完成集成。过期不候,只能按组件上一个稳定版本集成,如果有特殊情况需特殊对待。</p>    <p>JDRouter协议管理</p>    <p>每个业务组件都应该提供JDRouter接口,通过后台管理这些接口,方便所有开发者查询。</p>    <p>(点击放大图像)</p>    <p><img src="https://simg.open-open.com/show/f7e7b33c19fef4b510f263c818081f40.png"></p>    <p>持续集成改造</p>    <p>应用以前是可以持续集成的,iBiu系统组件管理后需要对持续集成系统做一些改造。在相应的节点机器上安装iBiu安装包,运行后自动安装环境。</p>    <p>CI通过iBiu Server所提供的API,脚本检测创建XXXXAppModule(空组件),获取某个应用的某个版本的组件配置表安装组件,完成打包工作。</p>    <h2>统一配置</h2>    <p>组件化带来的一个问题,每个组件独立存在,工程结构类似于这样:</p>    <p>(点击放大图像)</p>    <p><img src="https://simg.open-open.com/show/1b7b344fdaaf99e1a31b1f3d655fcdff.png"></p>    <p>Example for XXXX是当前组件在开发过程中所用来测试,验证的demo代码。</p>    <p>Pods为最终组件输出实际工程区域。</p>    <p>编译组合的组件,输出最终的App,如果在某个版本需要更新icon或启动图怎么办?总不可能让每个组件在Example中修改这些资源或数据。</p>    <p>有没有可以统一这些配置的办法,而又没必要让开发者人为的替换管理?</p>    <p>答案是肯定的,我们在iBiu可视化工具中加入了应用版本的配置拉取脚本功能。简单点讲就是安装组合时执行远程脚本。在iBiu后台针对应用版本配置相应的脚本,可以修改工程任意配置。</p>    <p>(点击放大图像)</p>    <p><img src="https://simg.open-open.com/show/63d02da8b6d40dda1c56a614f714c532.png"></p>    <p>这是一段检测替换icon的脚本,通过安装组件时可执行远程脚本,能做的事情不仅限于替换个图片这么简单,由于可执行远程脚本权限太大,该功能使用还是需要谨慎。</p>    <h2>最后</h2>    <p>该文章主要描述京东iOS组件管理的解决思路,由于内容过多,无法展开细讲每个环节,读者朋友如果对某些细节感兴趣可以留言,我们有针对性的另开专题。</p>    <p> </p>    <p>来自:http://www.infoq.com/cn/articles/jd-ios-component-management</p>    <p> </p>