手机淘宝的客户端架构探索之路

时间:2021-06-15 20:00:32
主讲人:冯森林(无锋/ Oasis Feng)

产品挑战

淘宝手机客户端承载并整合多样化的业务生态。

手机淘宝的客户端架构探索之路

淘宝手机客户端生态是非常多样的,有IM形态的旺信,购物形态的天猫,工具形态的充值,教育形态的淘宝大学等等。在这样的架构中要支持5个以上的BU,十多个部门开发的代码。能够安全、稳定的运行,并且能够保证基本的用户体验,这对底层的架构来说,是个非常严峻的挑战。淘宝内部把客户端的底层架构称之为“航母”,因为要在上面起降不同作战性质的战斗机群。因此,“航母”的甲板就显得尤其重要,它需要有高度灵活的架构来支撑。

研发挑战

手机淘宝的客户端架构探索之路

除了来自产品方面的复杂性挑战外,来自研发方面的挑战可能是大家想象不到的更大的挑战。
这幅图是在去年下半年淘宝客户端「All -In」的时候的真实写照,含义如下:

  • 大量业务同时涌入。
  • 火车模型的悬崖效应。火车模型是指一种运作良好的,能够支持客户端按时发布的运作形态,但在发布过程中遇到了一些不可抗拒的因素造成一些不能进入火车的东西一定要进入火车来进行发布。比如赶上某个时间点上的运营需求,它在那个时间上卡的很死,没有选择余地,这时就不得不把火车发布时间推迟。而火车发布一旦推迟,就会形成一个恶性循环,可能导致下下个版本的产品需求会因为这次发布的推迟而担心它的时间节点往后推迟的更多,它就会争抢的进入下一个版本。这个恶性循环就是造成这个火车越来越庞大,周期会变得越来越长,就会变成一种不堪重负的现状。
  • 10余个团队的代码整合。
  • 在Android 2.X方法数的天花板。

在这些量变上需要呼唤一个架构的质变!

发展历程

手机淘宝的客户端架构探索之路
2010年,第一个版本的手机淘宝客户端发布,当时Android版本和iOS版本的路线是不同的,当时的Android版本是披着App外衣的Mobile Web的封装,大量的使用WebView。除了首页、登录等核心的体验环节,其它都是用WebView封装的。那个时候的iOS版本相对来说更加纯粹些,它是围绕购物主链路的基本功能的实现。因为它会专注在Native开发上,因此它在功能覆盖面上相对来说会少些。在支付环节我们使用WebView来封装支付宝支付的支付流程。

2012年,随着业务的快速扩张,代码库的压力就变的很大。就需要对代码做某种层次的拆分和组装,在业务层面上还是在单工程中进行开发,但是会使用分支来进行开发,使得不同阶段的特性可控的进入代码库。在底层开始进行代码功能的抽离,把中间件都抽离成独立的工程来开发,最后来组装到一个工程内。

2013年,在这样的开发模式上,已经无法支撑业务上的巨大挑战了。业务代码规模上,如果不做任何拆分的话,方法数的瓶颈可能会碰到多次。这时Atlas插件框架就应运而生了,在这个框架内实现了业务以独立独立的功能去开发,最后以一个插件的形式集成到客户端内。在iOS上,采用了一个类似的事物,但是是在工程阶段解决的,是一个多工程化的插件开发,最后交付产物使用.framework集成到一个主工程来。

不同的探索-Hybrid

WindVane (2012-2014)
除了这个Native 的开发线索外,还有Hybrid上的尝试,指的是HTML和Native的混合开发。从2012年开始,自上而下的在各个层面做了很多优化,使得Web App的体验尽可能接近Native App。例如:

  1. 衔接Android/iOS Native的导航交互以及动画的一些改善。
  2. URL总线(对接Native UI总线)
  3. JS Bridge: JS <-> Native(在上面做了性能以及安全性增强)
  4. 缓存 & 预缓存,使得Web App在手势加载和后续加载中,保持足够快的加载相应速度。(兼容Cache-Control + AppCache)
  5. 数据采集、本地、网络性能监控
  6. 安全加固、审核、隔离,人及识别
  7. 增强的网络层(SPDY的实现、DNS旁路解析等)

2014 手机淘宝自诞生以来,最大规模的底层重构

在现在的架构上,定义了5个关键字:归一、轻量、透明、延展、敏捷。

反思

手机淘宝的客户端架构探索之路
上面这幅图展示的是传统的服务端架构和客户端App架构的对比,传统的服务端架构中最底下是一个OS,一般是Linux。最上面服务端的业务,而中间有非常多的层次可以在架构上,按照我们的意愿搭建中间的各个层次的衔接环节,使得架构具有足够的灵活性和扩展性。但是到了客户端就会面对一个完全不同的现状,客户端的OS(Android或iOS)本质上并不是一个很瘦的像Linux这样的OS,而是在OS上有一个很重的App Framework,开发一个普通的客户端应用所要用到的绝大多数接口都在Framework里的,而上面的业务也是一个非常复杂多样化的业务,最后会发现“架构”是在中间的一个非常尴尬的夹心层,因为会遇到很多在服务端架构中不需要面临的挑战。

  1. 体积的制约。体积对用户来说是一个非常敏感的概念,如果我们要在架构上做很多事情的话,通常意味着架构占据的代码量会比较大。在服务端架构中我们可以容忍我们在架构层面去做几十兆的代码。但是对于客户端架构,即使你的架构只有一两兆,对于一个客户端可能都占据了10%,20%的容量。
  2. 性能的挑战。从性能上来看,对于服务端架构我们通常关注的是吞吐率,我们不会去关注启动速度。一个服务端的启动哪怕是花了一两分钟,只要它运作起来吞吐率足够高,支持的并发能力足够好,响应速度足够快,我们就认为这是一个良好的架构。但客户端不同,客户端的进程对用户而言,往往是一个栈态的,手机里面使用完一个应用,退出之后可能过不了多久就会被回收掉,当用户下次再打开的时候,它会再次启动进程,需要重新完成一次初始化的流程。如果在这个上面做了很多事情的话,会导致程序启动的速度会很慢,在很多用户看来,这就是一种不可接受的用户体验。

综合这两方面的因素,我们对于客户端的架构需要审慎的去对待,这里有许多需要权衡的条件。

归一

一切皆组件:告别插件,拥抱组件(bundle)

刚才提到的Android的Atlas开发框架,以及iOS的插件化的业务开发框架,本质上都是在既有的客户端开发模型上面的扩展。这里面主工程仍然包含着核心业务,比如交易的核心链路,搜索一个商品,在店铺里面查找一个商品,这些都在主工程里面。但是从新的架构开始,把一切都化作组件,把所有的可以涉及业务的代码全部都放到了组件里面去完成,这样我们得到了一个完全归一化的架构,无论他是一个边缘业务或是一个核心业务都是等同对待的。下面是一个简化的底层架构图。

手机淘宝的客户端架构探索之路

在这里对组件的定义是可被部署的单元,这里面可以有涉及UI的部分,服务的部分(指不需要关注界面,纯粹提供功能的部分)服务区别于Library是在它需要在系统维护唯一的一个单例,同时它有一部分能力是传统Library不具备的,具有调用者的感知能力,知道调用会话是来在哪一个调用方。中间是一个总线层有三个总线:UI, Service, Message. 下面是一个整个新的架构中多出来的一个部分Runtime,称之为“容器”。里面有三个职能核心的是中间的Bootstrap,负责整个应用的引导流程。左边Bundle Management组件的管理,右边Lifecycle Management声明周期的管理。这张图右边部分是对应到工程上的结构,所有的Bundle都对应着这样的应用或服务的工程,每个Bundle都是独立的工程来进行开发,总线是以Library形式让工程调用。下面的Runtime Project这个容器工程只依赖于总线。而不与上面的任何工程发生关系,所以上面的Bundle工程和容器互相是看不见的。Library作为一种公共以来最终会被打包到应用中。

轻量

  1. 启动流程
    • 容器的初始化
    • 类加载(首次启动还需dexopt)
    • 核心中间件的初始化(有些初始化是要尽早进行的,比如DNS的预缓存,它越早进行对后面的性能优化越明显。还有一些早期初始化保证后面核心功能可用性,比如故障上报的一些机制,如果不尽早初始化,可能在启动阶段的crash就无法上报了。并且这个初始化是可异步的)
    • 启动入口的Bundle(这个是可配置的Bundle,一旦被配置就表示用户点击图标后首先会看到的界面)
  2. 组件管理:只负责添加、删除、替换(支持批量事务化)
    • 后面提到的动态部署的能力是通过上层的Bundle内部提供的业务功能与这个组件管理的这三个接口配合来完成的,比如说要完成组件的在线升级,和服务端打交道的机制,所有的文件存储管理的部分都是在这个上层的Bundle类完成的,底层的组件只管基本的三个操作。

透明

我们要构建的是一个透明容器,由于技术上的一些先天约束,我们只能在Android平台上构建一个完全的透明。

这里有一个概念是Bundle即App,容器亦OS。我们这样讲是因为我们希望任何一个组件的开发都像开发传统的一个App一样简单,但开发一个Bundle的时候,我们不希望它感觉自己是在一个容器中运行,而是就感觉自己像一个App一样在OS中运行。

生命周期管理

手机淘宝的客户端架构探索之路
为了保证这样一个透明性,我们在声明周期管理里面做了一些Bundle开发与App开发无差异化的处理像上面这张图中,左侧是一个传统的App运行在Android平台上面,右侧就是一个Bundle运行在一个Runtime容器上面,他们是一个完全对应的关系。这个对应关系体现在这样几个方面:

  1. 首先是程序的初始化,Application,我们都知道系统在启动Android四大组件中的任何一个组件都会完成的一个初始化过程。我们要确保在进入任何一个Bundle类的组件(Android四大组件)时会触发Bundle的Application,这个Application和一个App的Application没有任何差别。
  2. MY_PACKAGE_UPDATED是应用程序升级后会触发的一个事件。在Bundle替换完成后,会触发MY_PACKAGE_UPDATED这个就相当于应用被操作系统手机后产生的操作。
  3. BOOT_COMPLETED是指操作系统初始化时,会触发拥有这个权限的这个应用所完成的操作,它会完整的对应到容器里面。容器的Application阶段会触发Bundle的BOOT_COMPLETED,使得Runtime容器更像一个操作系统。

二级容器

透明性除了体现在生命周期管理上,还体现在二级容器上。因为我们认为Bundle即App,容器亦OS,因此在任何一个Bundle类可以把这种透明性进行传递。比如前面提到的Web系统WindVane,它可能以前就是一个简单的控件,一个WebView,但现在就是一个Bundle的二级容器。如下面这张图所示,WindVane是一个Runtime容器,然后它上面又有一个Web的Bundle。但是它们作为一个整体是以一个Bundle的形式运行在Native的Runtime上面的。还有一些Micro App,就是对于一些场景特定化的应用进行服务端特别编排的应用模型,它有一个Runtime,它上面的Micro App运行在它的Runtime上,整体同样是以一个Bundle的形式运行在Native的Runtime上面的。综上,就是透明性中二级容器的概念。

手机淘宝的客户端架构探索之路

总线

除了生命周期管理和二级容器,透明性还表现在总线的设计上。这里会提到三大总线:UI总线、服务总线、消息总线。

UI总线

UI总线:以跨平台统一的URL作为寻址方式(Web、Android、iOS)

  1. 比如一个淘宝商品页的URL:item.taobao.com. 在Android上会映射到DetailActivity;iOS上会映射到TBTradeDetailViewController. 通过这样一个机制,我们就可以在很多环节配置一套URL,然后在多个平台上面实现完全统一的业务逻辑和控制逻辑。
  2. 自动降级机制。没有Bundle 承载的URL,将自动以Web 容器加载。这样就可以实现,当一个新的业务到达手机当中的时候,当手机客户端还没有实现这个功能,就会自动以Web的形式把它运作出来。当想要保证轻量化的时候,可能会裁剪掉一些Bundle,这时没有了这些Bundle,这些功能将会自动以Web的方式替换掉,使得在整个流程里面它是完整的。体验可能会有一些差别,但是功能是不受影响的。
  3. 在Android里面,运用了去中心化的设计。Android本身又通过一个URL去映射一个Activity的机制存在的,在这个机制上加入了一个URL Hook的机制,它的出现主要是用来解决一些时候需要去处理跨整个应用的处理,比如一些广告流程可能需要去拦截特殊的URL里面的参数,它其实不知道它会在具体的哪一个界面里面触发。URL的Hook机制就可以保证URL里面只要产生了URL参数,就能够被自动Hook掉。在iOS里面由于没有全局去中心化的分发机制存在,因此没有办法把它Hook掉,所以设计了一个中心的分发流程,也同时支持Hook机制。

服务总线

服务总线本质上就是我们希望在客户端运用SOA的开发思想,它是基于接口的服务调用。在Android上面我们可以利用原生AIDL(Android Interface Definition Language)去实现这个服务总线。通过它就可以自动在Android上面自动生成一些服务的接口,但是由于Android本身设计上面的一些考量,这个服务接口是一个纯异步化的接口,使得在一些调用流程中不易使用。这个服务需要使用RPC到Android的系统进程里面完成服务的初始化,会使得服务的启动变慢。因此在容器里面实现了一个轻量化的服务启动,并没有走Android的Service Binder机制,但是兼容了Service Binder的流程的。是用接口兼容的方式实现了轻量化的服务启动流程,可以规避RPC带来的开销。

由于这个机制兼容Android的AIDL,因此可以轻松实现跨进程、跨App的服务部署。在多个应用间使用统一的服务,然后相互调用。比如可以在淘宝的很多App间实现服务的相互调用。

消息总线

在消息总线方面,仍然是基于OS原生消息机制实现消息总线,Android里面使用Broadcast,iOS使用NSNotification。考虑到性能的需要,在Android里面会使用进程内轻量的消息通道,进程内使用Broadcast会带来一些额外开销。

延展

大家可能会好奇,通常我们服务端会考虑从它的延展性,客户端为什么也要考虑它的延展性。其实我们的客户端同样也需要考虑它的延展性,做过大型App的同学可能会这种体会,当代码量扩展到一定程度后,会遇到很多方面的瓶颈,比如开发变慢、构建变慢,因此在各个方面都要利用延展性来考虑架构的设计。

标准化

首先是 标准化,这可能与阿里特定的组织结构有关。因为我们是无线事业部,我们支持阿里的所有团队的无线化的进程,提供很多寄出设施。在这个过程中,我们会和非常多的团队打交道,会集成非常多BU的代码,这些代码如果在非常特定的框架中的话。增加的就是团队之间的沟通开销,比如会增加其他团队学习这个东西的成本。所以这个过程中应尽量使用标准化的方式来解决这个问题。比如:

  1. Bundle交付产物AAR(Android在比较新的构建流程里面会使用的标准的中间件的中间产物,这个格式和传统的library加上Android里面一些特有的资源没有差别的),这用这种方式,在其他团队为我们开发Bundle的时候,在构建开发工具的时候不用做很多特殊的处理。在这个基础上的构建产物AWB(一个内部叫法),格式是相当于APK中剔除了共享依赖。为什么构建产物要剔除共享的依赖,是因为在之前Atlas的框架中没有使用这个设计,会使得一些中间件,大家都在使用的,它会打在每个插件的包里面,最后再合成一个大的APK的时候,这里面就会有非常多的冗余中间件。
  2. Bundle间可以跨进程、甚至跨App对接。

灵活性

除了标准化外,还需要一个灵活性。

  1. 除了需要服务手机淘宝这样一个客户端,还需要按照业务方的需求对这些Bundle*的组装,比如把生活相关的Bundle抽取出来就可以包装成淘宝生活,把扫码相关的抽取出来,就可以重新包装成马上淘。这是Bundle的可复用程度最大化的表现,这样,其他团队也更愿意以Bundle的形式去开发,交付给不同的业务线使用。
  2. Bundle对容器是没有依赖的,在之前的图中我们看到Bundle只会依赖中间的总线层,对容器是没有依赖的。如果把这些Bundle交给一些对团队,他可以不适用你的容器,但是可以完全无障碍的使用你交付的这些Bundle,因为这里面是一些完全标准化的东西,Activity还是那些Activity,Service还是那些Service,它并不需要在容器上运行,完全可以在一个没有容器的常规App中去调用。

适应性

最后在适应性方面,从微型App到巨型App,按需采用的容器能力。

  1. 容器有完全独立的三大职能:启动加载、生命周期管理、组件管理。
    后两者可以完全*剥离的两个职能。比如一个还处于很婴儿期的应用,这个时期的应用我们对它只有一个要求,只要它能尽快交付,尽快发布进的版本,开发足够敏捷就够,其他能力对我来说都是浮云。对于这样的应用完全不需要使用容器,就按照传统的App开发流程也挺好的,为什么还需要使用容器呢?但是我们可以提供给它一个完全难以抗拒的诱惑力就在于交付能力。我们可以实现动态部署,你要的交付能力不就是,你早上你诞生了一个想法,上午把它设计出来,下午把它开发好,晚上就能够把它发布到用户手里。这个是大家在做传统Web开发里面都梦寐以求的东西。所以,即使是一个初创阶段的应用,也是非常渴望拥有这样的能力,只要能够使用容器,并不需要把App进行拆分,然后放到一个Bundle里面,动态部署可以把你的Bundle完整的更新替换,有了随时到达用户手中的能力。
  2. 当你的应用,开始可能没有容器,但某一天,需要迁移到容重的时候,正因为我在对透明性上做了工作,我们并不需要对代码做实质上的改变,因为它对容器没有任何调用上的依赖。适应性在Android上面能够更好的体现,因为三大总线都是基于OS封装,所以他的互操作性能够在最大限度上得到保证。
  3. 从巨型App时代的臃肿回归田园App时代的轻盈。

我们在开发时候,很多问题都是因为App越来越臃肿导致的。要回归田园App时代我们必须做到下面几点:

  1. 开发:像微型App一样便捷的开发调试(不依赖容器),只需要构建自己的Bundle。
    手机淘宝的客户端架构探索之路
    上图中左侧是生产的包的打包模型,右侧是开发模式上的打包模型。开发模式中可以把其中的一些组件拿出来,包括它的UI, Service, Library打包成一个APK,比如一个商品的详情,开发的时候就可以直接把商品详情这个Bundle安装到手机里面运行它,然后就可以调试手机里面的所有流程,但是这时不可避免的还会涉及到和其他模块的协同,这个时候就需要跨Bundle的调试,但是不可能要把他们全部构建,这样就不能保证很好构建速度。可以在调试的时候使用持续构建环境,事先把别的Bundle打包。比如在详情模块需要调用交易,可以到持续构建环境中下载一个Bundle,它也是一个APK,然后都安装在手机里面。这样就可以互相调用了,调试也不需要编译其他Bundle,只需要编译调试自己的Bundle。但是这里还有一些细节的问题到目前为止还没有解决,比如,共享的一些权限,还有两个Bundle调用服务的时候,其中一个Bundle的服务并没有export,如果是两个App又如何调用呢?这里Android天然提供给我们一个机制—-ShareUserId,它可以使两个Bundle公用一个UserId,使他们可以像同一个应用一样的来工作。有了一个机制,Bundle之间可以共享很多东西,比如权限集合。这样就可以保证在开发环境下,能够尽可能的接近生产环境下的运行模型。
  2. 集成:极简的集成模型,AAR的构建产物AWB,这个每个Bundle交付出来的微缩版的APK,集成的时候只要把这个APK放到我们最终的安装包里面,直接运行了。并不需要在集成阶段做整个App的重新编译的工作。集成只是一个简单的合并包的过程,所以它非常快。
  3. 测试:之所以我们传统的迭代不能快起来,瓶颈就在于集成测试。如果能够把集成测试成本足够低,我们的迭代周期可以缩小的足够短。如果要两种发一个版本的话,要把集成测试的时间缩短在两周以内。我们是重Bundle测试,Bundle自己的测试要非常完备,才会进入到集成阶段,而在集成阶段只要做非常轻量级的测试。Bundle的测试是对应着Bundle的迭代开发周期的,不会阻塞整个发布的周期。使得发布的迭代周期可以缩短。
  4. 部署:借助于Bundle的自动化在线部署机制。实现开发完,测试完就直接到用户手中。

敏捷

More fast and break things(Facebook的座右铭)
为了保证上面这点,我们在传统的Web开发中都可以做到,就是开发可以变得很快,测试可以变得很轻盈。如果遇到线上严重的问题,快速修复就是了。但是在Native开发中变得成本非常高。因此我们做了一个Hot Patch

  • 可以对线上严重问题进行快速修复(小时级的响应事件)
  • 可以对Android Framework代码打补丁
    (这种快速修复是通过AOP的编码方式来进行Fix,可以在Before / After / Replace某个方法过程中修复这个问题。)
  • 它还支撑了我们尚未公开的神秘项目Project Nish
  • 暂时只支持Dalvik,即将适配ART和阿里云OS

耗时两个月完成

我们耗时两个月完成了以上的改造,6月初上线以来

  • 集成Bundle:30 +
  • 改造服务:10 +(登录、缓存、网络访问层……)
  • Hot Patch修复线上严重故障10 + 起
  • Patch平均大小只有5KB
    但是过程中,最大的阵痛是生产构建打包系统的改造,开发环境打包是非常简单的,但是生产构建环境有很多复杂性在里面,比如涉及改写aapt实现资源分区。

思考:从『壶中界』走向『云和山的彼端』

Native App 能否像Web 网站一样随时部署,即用即取?

  • 资源(图片)的云端化+Cache化
  • 中间件的全面Bundle化,Bundle的细粒度化
  • 基于依赖结构的代码(Class)按需加载机制(类似Require.JS)
    手机淘宝的客户端架构探索之路
    像Web 一样轻盈的Native App !

(本文整理自:http://www.infoq.com/cn/presentations/taobao-mobile-client-architecture-road