Atlas手淘组件化框架的前世今生和未来的路

今天手淘技术团队宣布正式开源它们的容器框架Atlas,有兴趣的读者可以通过https://github.com/alibaba/atlas进行访问,也可以通过http://atlas.taobao.org/项目官网来了解技术文档。下面让Atlas团队来介绍下该项目的历史、原理和未来。

  Atlas是什么

Atlas是古希腊神话中的天神,是波士顿动力公司的机器人,借助搜索引擎,得以发现这个名词背后许许多多的含义。在手机淘宝,Atlas是一个扎根于Android客户端的一个组件化容器框架,相比神话中用手和头支撑起苍天的泰坦神族,Atlas在手淘默默无闻地承载着手淘上丰富业务的运行,伴随着数不清的功能在用户手中经历新老交替。

插件化的历史

在阿里,在手机淘宝,移动客户端的迭代更新可以说是整个移动互联网从新起到繁荣的见证。伴随着越来越多业务的诞生,越来越多的代码开始往小小的客户端涌入,而无论是iOS还是Android,各种客户端的体积也经历着快速的增长。而由于客户端的推广成本较高,各个业务线都有很强的需求接入手机淘宝客户端。所以在当时航母战略的背景下,迫切需要一种技术能把客户端化整为零,这些模块可以自由组合,并且当部分功能变更时只需要更新对应模块。

我们在2012年底的时候就开始研究Android上的插件化技术,并与2013年初把Atlas作为插件化框架接入手机淘宝。基于插件化的能力,一个普通的Android应用可以低成本地转为符合Atlas规范的插件,使Apk既可以以插件方式运行也可以独立安装运行。一个大的Android客户端项目可以分割成数个插件,做到代码隔离,降低了开发、维护、部署的成本。

当时插件化的方案可分为三类:Web、WebApp、Native。各种方案在手淘内都在有同学进行深入的研究。

Native形式的插件由于技术门槛较高,当时业界采用的并不多见。但是由于其比WebApp有良好的性能和体验,同时对于许多已经成型的小客户端推广的迫切需求,尤其值得我们去探索。

上图是当时插件化的技术原理的简图,其实跟现在业界的插件化方案主要的设计思路基本一致。Atlas作为一个模拟的Android运行环境,每个插件以一个独立的进程运行在各自的沙箱环境中。同时由于接入成本较低,其他的业务应用基本可以算是零成本的方式接入到手淘环境,同时也可以随时发布新的版本进行更新。

插件化的方案有其特有的优势,独立的进程保证了业务的绝对隔离,同时也便于隔离风险。聚划算、天猫、彩票一个个独立应用开始起降于手淘这架航母。甜蜜的成长总是短暂,插件化发展很快,随着手淘All In方案的深入,一些隐患开始展现。

  1. 性能:新进程的开辟极大影响每个业务的进入速度,同时由于手淘的插件并不是用完即走的场景,比如说从首页->聚划算->详情->店铺->下单各个环节是存在着一定的关联,各个插件的进程无法退出。而多进程的机制又极大的占用了内存,同时也引起许多用户的质疑
  2. 复用:插件的独立限制了许多中间件的复用,AIDL的方式并不适合中间件能力的输出,独立的apk直接进入的同时也携带了大量重复的二方库,也一方面极大增大的应用的体积
  3. 稳定:插件化需要一个模拟的Android运行环境,在Android多版本以及国产ROM的兼容中需要做大量的工作,同时多插件运行过程中在低端设备上很容易遇到进程被回收或者三方应用强杀的情况,没有良好的恢复机制会极大降低用户的体验

 组件化的诞生

伴随着All In的进行,在手淘内部发起了对大型app进行重构的计划。我们需要这样一个框架:

  1. 支持大量丰富业务的接入,同时业务之间能够保持清晰的边界,各自可以继续灵活迭代;
  2. 用一批统一的中间件去支撑起各种业务的底层功能,保持中间件代码的全面复用;
  3. 能够尽量保持对系统的低侵入,尊重原生运行机制以降低后期的维护成本;
  4. 在用户设备上尽量体现一个简单客户端的特性,同时特定的业务功能按需获取,保持体积的可控;

↑新容器化结构设想↑

基于对插件化框架对Android运行机制的理解,参考了OSGI在服务端框架,在开发IDE等领域”高复用、低耦合、可插拔”的优势,我们借鉴了OSGI规范开发了基于组件化的Atlas容器化框架。

下图是基于组件化框架的系统结构

  • 最底层的tookit verifier全面罗列了上层需要反射使用的注入和代理的Api,会在应用启动时先进性全局性的校验,以避免程序运行中遇到不兼容的情况;
  • 往上Bundle Framework负责组件的安装 更新 操作以及管理所有组件的生命周期,这里组件的边界隔离就遵循了OSGI的规范,每个组件分配独立的classloader,同时组件有各自的资源,每个资源在构建期间由AAPT分配独立的package ID;
  • Runtime层 主要包括清单管理、版本管理、以及系统代理三大块:
    1. 版本:每个组件在构建期间就由构建插件分配自己的版本号,同时安装期间也有各自的版本目录,每个bundle的启动加载都需要经过版本的校验,组件发生更新同时也下发最新的版本信息。依托版本管理机制组件的热更新能力水到渠成。
    2. 清单:OSGI规范中每个组件通常通过OSGI.MF来暴露自身的component,这是与Atlas容器所不同的地方。在Android设备上,多文件的形式很容易受IO异常的影响干扰bundle正常运行,所以我们采用了构建期间集中生成清单的方式,清单里面记录bundle所有的component(Android四大组件),依赖、packagename等内容。
    3. 代理:各个系统关键点的注入使得bundle可以做到按需加载,避免了像原生MultiDex方案由于首次启动时多dex同步安装而造成UI卡顿的情况,代理层的核心DelegateClassLoader负责类的查找和路由,DelegateResource管理所有bundle的资源,它们在容器启动时进行注入,并在运行过程中随着bundle的不断载入进行更新。
  • 接入层 简单是美-复杂的东西留给自己。为了方便,Atlas容器有自身独立的Application负责启动,同时在构建期间会由插件替换应用原有的Application。运行期间应用首先由AtlasBridgeApplication负责启动,并在容器启动完毕后全权代理给应用真正的Application;同时对需要自定义和由外部决策的功能,容器开放接口由接入方简单设置。

解耦和依赖

依托classloader,组件的独立性有了充分的保证,同时由于BundleClassloader保持了对PatchClassLoader的引用,使得宿主的中间件就如同普通APK开发一样可以被组件简单调用;同时组件之间除了基于AIDL的服务调用,组件之间提供了静态依赖和运行时依赖,相互依赖的组件可以通过配置打成,这在一定程度上使某些偏功能的代码或者UI的重用成为可能,同时配置的方式使组件之间的关系可以清晰追溯。

性能的演进

  1. 异步按需:由于每个组件是一个小型Apk的结构,每个组件安装使其涉及到文件的拷贝、native lib的解压以及校验载入等过程。特别是Service等后台component触发组件安装和前台Activity引发组件安装并行时UI的流畅很受影响。为了降低对UI的影响,每个组件的安装都在一个统一的异步安装线程中进行。Activity、Service、Receiver等的发起都被进行了异步的处理。
  2. 解释执行组件的代码在安装后还需要dexopt才能使用,这一过程在ART上的时间消耗尤为明显,为了降低用户等待时间,Atlas框架对dalivk系统上首次使用bundle时关闭了verify,在ART系统上首次使用时关闭了dex2oat走解释执行,保障bundle首次进入尽可能地高效,同时后台通过异步任务走原生的dexopt过程,为下次使用做好准备。

动态性的增强

  1. 远程组件:插件化时代的按需下载并没有被废弃,独立的远程组件可以满足用户的个性化需求。基于用户的操作和清单机制远程组件在用户需要时只需要简单地等待或者授权就可以从远端拉倒本地。同时当用户设备因为空间紧张时容器也可以清理掉一些长期不用的组件以释放拥挤的空间
  2. 动态部署:动态部署是容器一个最重要的功能。基于此,业务可以灵活发布自己的需求;有故障的业务可以及时修复或者回滚;同时动态部署的快速覆盖能力在灰度等场景下可以更快地收到所需的效果。动态部署的范围也通过改造进行扩大,从最初的组件部署到现在支持除Atlas容器小部分核心代码外的所有代码,部署patch包的体积和性能的改进也在不断的进行改进。

diff

每次大版本的发版,集成平台会保留该次发版的AP(Ap中记录所有参与构建的二方库的版本,组件的依赖,混淆的mapping等各种信息),在手淘接下来每周一次的动态部署迭代中,动态部署的构建会与之前的dex进行字节码级别的diff,生成tpatch包,最终下发到用户手机的patch仅包含变化class组成的dex和更改或者新增的资源文件

Bundle的Merge

patch收到后,客户端的merge过程会根据patch信息,取到source 组件,source dex和patch dex进行dex合并,合成新的dex,同时变化的资源覆盖老的资源最终形成新的组件。

中间件部署

动态部署能力的增强少不了对底层中间件的支持,部署的机制也与bundle截然不同

1. 中间件代码部署

Dalvik和Art在merge过程中都会形成新patch zip,zip中dex以多dex的方式命名,patch dex为classes.dex,原始apk中的classes.dex以此根据序号+1放入zip中,不同的是dalvik由于preVerify的限制写入zip中的源dex会先经过处理,剔除在patchdex中已有的class,确保被patch的dex无法被打上preVerify的标签。

安装过程中则多个dex依次dexopt,同时插入到dexpatch的第一位。而ART由于无preVerify校验,但是Android到ART后原有的dexopt会改为dex2oat.

运行时代码由原来的解释执行改为直接运行native代码,之前的使用过程中发现单纯得往前面追加patch的dex并不能完全解决动态部署的问题,dex2oat的过程优化了class的执行代码,比如说内敛,虚函数的校验等。就有可能在运行过程中直接在native层执行老的优化过的class代码而不是从新patch的dex中load新的class去使用。

所以在ART上类似Dalivk,放入源dex,只是源dex不会做任何处理,然后合并在一起做一遍dex2oat,这样能使新的class覆盖老的class参与dex2oat并完成优化的过程,

2. 中间件资源部署

资源部署基于overlay机制达成,不过dalivk上这个机制并不支持新增资源,在overlay的包里面如果读到了一个资源,dalvik系统会去校验该资源ID在base中值,如果不存在则抛错,所以动态部署为了利用这一特性同时支持新增资源的需求,在打基线包的时候就在每个不同的type里面预留了128个资源ID供后续动态部署使用。

同时打动态部署的patch时会以之前的ID分配的内容作为输入,保证已有的资源分配到的ID保持不变,同时如果资源没有发生变更,则剔除该资源,所以aapt dump resource patch的时候我们看到的INVALID TYPE 的资源段就表示没有发生变更的资源,如果有发生了变更,且之前有这个资源,就会在原有的资源ID分配过去,同时新的资源文件打入patch包或者新的资源文本写入arsc,如果是新增的资源,则使用预留的资源段进行分配。整个替换过程如下图所示

Atlas动态部署的能力随着手淘的需求不断进行着演进,随着整个框架的开源,自然需求会变得更加广泛。比如在手淘内部一周一发版的节奏下,动态部署对新增component的某些需求并不强烈,而开源后此类功能可能会尤为重要,所以接下来容器的更新迭代中此类的功能也会及时上线。拥抱开源社区会是Atlas重新审视自己,继续发展的关键一步。

未来的路

相比于iOS,Android的推陈出新的速度是极快的。Google在Android系统的改进深入到方方面面,组件化的发展也需要紧跟Android步伐。

Atlas起步的时候MultiDex还未出来,解决方法数的问题就能带来足够的愉悦。现在Atlas基于组件化减少主dex方法数的同时,也支持主dex原生MultiDex的能力。目前Atlas内部集成了MultiDex的能力,外部只需要打开MultiDex开关即可。这么做的目的一方面避免外部初始化实际的误解,另一方面我们也意识到原生的mulitdex在性能上还有很多优化的点,我们会在后面及时进行优化。

在开发调试方面,Atlas容器框架基于动态部署的能力支持单个组件的独立编译调试,但是相比instantrun的速度,目前还有很大提高。而基于Instant Run改造的单组件秒级编译也已经在紧锣密鼓的开发中,后续基于框架的独立调试插件也将会在Android Studio插件库中上线。

Android系统机制的演进也鞭策着动态部署前行。比如16年Android 7.0的混合编译当时也给Atlas造成了不小的挑战,服务于亿级的UV,动态部署的能力的覆盖面、到达率都还有进一步提升的空间,相比andfix等热部署修复方案,动态部署需要重启来打到更新的能力可能也需要有新的转变。

市面上许许多多的容器框架,基本思路就大致两三种。Atlas本身也没有特别新的技术,有的是基于已有的知识进行不断的重组和优化。在手淘内部,追求更高的稳定性和性能,亿级的UV需要及其稳定的容器来承载,同时Atlas也不断尝试新的方案,在创新与实用之间不断权衡。

Atlas是一个结构简明的容器框架,通过尽量简单的思路去解决移动应用从开发到运维中的的桎梏,让开发中的复杂简单化,让维护中的复杂简单化,让开发从流水线上忙碌的工作回到专注于对最终目标的精耕细作,抛开嘈杂的干扰,拨开虚无缥缈,到达云和山的彼端!