Atlas:手淘 Native 容器化框架和思考

阅读数:5702 2016 年 10 月 19 日

话题:移动Android语言 & 开发架构

在刚刚过去的云栖大会上,手淘宣布其移动容器化框架 Atlas 将于 2017 年年初开源,对这个框架,在过去团队对外部做过一些分享,外界也一直对其十分关注,到现在它终于即将开源了。

本文将介绍 Atlas 的设计思路和手淘对容器化、组件化和动态化上的思考,主要内容来自阿里巴巴资深技术专家倪生华(玄黎)在云栖大会上的分享。

Atlas 是什么

2013 年,手淘航母战略的制定,带来了业务和开发人员的翻倍膨胀。从不到 100 人猛增四五倍,同时业务数量大增,整个客户端的架构和发版节奏受到极大挑战,Atlas 作为之前手淘客户端的基础框架,进行了一次大的重构,形成了今天的 Atlas。

Atlas 是一个 Android 客户端容器化框架,主要提供了组件化、动态性、解耦化的支持。支持工程师在工程编码期、Apk 运行期以及后续运维修复期的各种问题。

  • 在工程期,实现工程独立开发,调试的功能,工程模块独立。
  • 在运行期,实现完整的组件生命周期的映射,类隔离等机制。
  • 在运维期,提供快速增量的更新修复能力,快速升级。

Atlas 是工程期和运行期共同起作用的框架,它的特点是尽量将一些工作放到工程期,这样来保证运行期的简单,稳定。

目前,Atlas 在淘系 App 的应用十分广泛,手淘自身超过 60+ 业务组件、20 个协作团队,以及百万行级别代码都在 Atlas 上运行,其快速迭代能力让应用的发布周期从每月到每周再到随时发布,在过去半年里就发布了 446 次。另外 Atlas 本身非常轻量,只有 90 多个类,支持大小型 App 开发,从大型的手淘到相对小型的阿里健康等都是用的这个框架。其稳定性也接受了考验,兼容 Android 4.x 以上系统版本。整体手淘的 Crash 率一直维持在万分之五左右,因为容器导致的 crash 占比小于百分之一。

从这个意义上来说,Atlas 首先要解决的问题是大规模团队的协作问题,诉求包括并行开发、快速迭代、工程解耦,然后解决的问题是客户端动态更新的问题。手淘内部思考的解决方案就是组件化。

Atlas 组件化实现

组件化,业界称为插件化,不过这里 Atlas 的组件化和现在的插件化有一些不一样的地方。组件化是需要去知道组件的功能,设计更规范。

(手淘 APK 包目录结构)

这是一个手机淘宝的 APK 包,第一层目录上与标准的 APK 是完全一样的,在 APP 会有很多的 so 文件,如果解开来看的话,它的结构类似于完整的 APK,但本身并不能独立运行,它跟很多插件化的差别是在运行期,它是运行在整个容器里的,每一个组件都是独立的 Bundle。

从模块来划分,手淘 APK 可以分为两层,上层是经过拆分的业务 Bundle,扫码、评价、详情,各个业务之间可以进行功能的调用,可以通过路由调度到其他业务方。下层是共享的底层中间件,向业务方开放各种能力,如网络库、图片库等,会在容器里进行统一地把控,这样做的好处是包做到尽可能小,第二是性能佳。

这一块是 Atlas 的整体设计,分为五层:

第一层我们称之为 Hack 层,包括 OS Hack toolkit & verifier,这里我们对系统能力做一些扩展,然后做一些安全校验。

第二层是 Bundle Framework,就是我们的容器基础框架,提供 Bundle 管理、加载、生命周期、安全等一些最基本的能力。

第三层是运行期管理层,包括清单,我们会把所有的 Bundle 和它们的能力列在一个清单上,在调用时方便查找;另外是版本管理,会对所有 Bundle 的版本进行管理;再就是代理,这里就是和业界一些插件化框架机制类似的地方,我们会代理系统的运行环境,让 Bundle 运行在我们的容器框架上;然后还有调试和监控工具,是为了方便工程期开发调试。

第四层是业务层了,这里我们向业务方暴露了一些接口,如框架生命周期、配置文件、工具库等等。

最上面一层是应用接入层,就是我们的业务代码了。

所以 Atlas 作为一个框架提供了相对完整的能力,业务层的开发可以在框架生命周期的各个环节做一些自定义的动作,也可以自由的调用系统、框架,乃至其它组件释放的能力。

组件化技术细节

前面讲的是容器层面的比较概要的东西,下面我们会讲一些具体的细节。

关于 Bundle 的生命周期会提供细粒度的节点,比如下面是一个 Bundle 从加载到运行的周期:

  • startInstall:开始加载。这个时候框架会做一些拷贝文件、释放 lib、加载 Bundle 的事情;
  • Installed:加载完毕。这时框架会注入资源路径,创建 class loader;
  • resolved:解析完毕,框架会检查组件配置是否合法,是否能被解析;
  • active:运行组件,即开始运行组件 Bundle;
  • started:运行成功。

组件化涉及到的第一个问题是 Manifest 处理,一个是因为来源很多,有宿主 Manifest、Aar Manifest 以及组件 Manifest,另外不同组件的 Manifest 经常发生变化,要求我们灵活地去处理。这里的做法是在工程期将所有的 Manifest 进行 Merge 操作,这里需要注意的是 Bundle 的依赖单独 Merge,因为这里涉及到依赖仲裁的问题。最后解析各个 Bundle 的 Merge Manifest,得到整包的 BundleInfoList,就是上面我们提到的 Bundle 信息清单。

第二个是类加载,这里利用 Delegate ClassLoader 来动态加载组件的类。Delegate ClassLoader 先查找宿主 Bundle 的 PathClassLoader,然后根据前面的 BundleList 找到对应的 BundleClassLoader.

第三个是资源,我们会用自己的 DelegeteResources 替换掉系统的 resource,Bundle 的资源会逐个在安装的时候添加到 AssertPath,由于添加 Bundle 的顺序非固定,不分区会导致资源查找错乱。

另外,Dalvik 和 ART 上的资源查找过程顺序是不一样的,加上小米等系统会重写自己的 resources,所以我们会适配不同的机型,往后追加 AssetsPath 或者往前追加,系统 AssetManager 是个单例,默认往后追加,如果往前追加,则需要重新创建 AssetsManager 对象,同样主 dex 动态部署的时候要达到替换原有 resource 的目的,必须保证插入顺序与查找顺序一致。

还有需要注意的是,每次更新 resourceTable 的时候,必须保证 apkresource,runtime 的系统 resource,例如 webview,bundle resource 都已经添加成功,而且唯一,顺序正确。

不同 Bundle 的资源可能发生命名冲突,我们是用了一种相对来说简单的方法,将各自的 Bundle 分配成不同的 ID,保证所有的业务资源不会产生冲突,尽量将问题放到工程期解决。在很多代码里,通过反射来调用整个资源,在 5.0 以上的系统是没有问题的,它只找第一个,对业务代码而言,原来是怎么写的,今天还是怎么去写。

关于组件化性能这一部分,我们引入了按需加载,因为手淘 APK 有 70 多个 Bundle,每个用户真正用的时候只需要 5 或 10 个,所以不需要加载所有的 Bundle。Bundle 之间进行隔离,通过 Android 四大原生组件进行交互,这样 Bundle 之间可以比较好的解耦。我们所有调用的入口都是基于 BundleInfolist 去做的,根据这个清单信息,得到组件所在 Bundle,如果需要加载,我们就进行 install、dexopt 等操作。

另外,对于解决组件依赖问题,定义了两种新的组件格式 Awb(业务 Bundle)和 solib(so 库),前者与 AAR 一致,不过不添加本地 lib,在构建的时候做依赖仲裁区分,后者是 Native so 库的依赖。Awb 其实就是 AAR,只是后缀修改了,如果你的包放在宿主 Bundle 就用 AAR,如果是组件 Bundle 就用 Awb。

对于业务 Bundle 的依赖,我们在构建期会将宿主 Bundle 和业务 Bundle 及其依赖分别打包,然后按照最短路径、第一声明原则进行树状仲裁,得到每个 Bundle 需要的依赖,在打包的时候会将依赖库放到各自的 Bundle 里去。

最后是 APK 构建,我们对它做了比较大的调整。上面的图中,其实左边这一部分是一个标准的 APK 的构建过程,包括处理,编译,到签名的过程。我们这个不同的地方是多了 Awb 需要特殊处理,其中 Awb 的资源根据宿主的 resource.ap_ 和包内资源构建,R 文件由 Bundle R 资源和宿主 R 资源合并而来,然后我们对 Aapt 进行了修改,对每个 awb 分配不同的 packageId,然后进行统一混淆,生产各个 AWB 的 Dex,打包为 APK,签名之后复制到 libs,改名为 so 文件,然后合并到 taobao APK. 这就是我们组件化的整个过程。

Atlas 动态化

在一个容器框架内,组件化和动态化是相辅相成的,组件只是解决了解耦的问题,但我们如果想要随时发包,就必须让容器框架具备动态化能力。我们在完成了 Atlas 的组件化之后,做了动态化的支持。动态化的好处一个是包的大小缩减,我们可以将一些包在运行后下载到应用中,另一个是具备动态发版和修复能力。

增量动态化方案

Atlas 提供了动态部署的能力,主要目标是动态业务发布,以及问题修复。它基于手淘自研差量算法,主 Bundle 基于 ClassLoader 机制,业务 Bundle 基于差量 merge,支持全业务类型。

另外,Atlas 也支持 Andfix 作为插件使用,目标是快速故障修复,它的原理基于 Native hook,主要做方法的修改,在实际中可以两个一起用。在工程构建期适配之后,可以做到一套代码两套方案通用。

自研动态部署功能实现原理,首先,对于 Dex Patch 的生成,我们通过修改 Dex 的字节码实现,将 Dex 文件转为 Smali,对其中的 ClassDef 和 ClassDataMethod 结构体进行分析,可以实现删除、新增、修改类,然后通过 Diff 处理得到差量文件,再通过 Merge 处理即生成补丁。

其次是整个资源 Patch 的生成,分为两块,一个是业务 Bundle,本来是一个不断加载的过程,它实现起来会比较简单,通过 Md5 diff/BSDiff 即可得到。对于主 Bundle,因为安卓本身有一个限制,所有的资源必须得在 base 包里,新增一个资源是不生效的。所以一个做法是在打包的时候预留很多空资源。另外更新已有的资源则通过资源覆盖来完成。

最后,如果新加业务的话,会新加 Activity,我们的做法首先在 Manifest 预埋一个 StubActivity,然后在 Instrumentation.execStartActivity() 阶段进行替换,同时配合 Intent setFlag 模拟 Activity launch mode 并继续 startActivity,接着 System_server 进程进行处理,更新 ActivityStack,创建 binder,并通知 ActivityThread 进行实例创建,最后我们在 ActivityThread 的 handler 里面进行拦截,更新 ActivityInfo 等信息,创建目标 Activity。

另外在工程实践上,因为补丁的生成会涉及到 Dex 和资源的基线,我们会在部署的时候,每次发布 APK 包同步发布 AP(基线包)到 Maven,AP 基线包里是所有影响基线的文件,第一是安卓 APK,第二是 Mapping.txt,最后是 Dependency.txt,这样的话整个构建的速度会非常的快。

所以我们这种方式,版本的升级是不同的方式。比如今天手淘的详情要更新,会发布版本,这个版本可能不是到应用市场的版本,而是一个 Patch 包。业务版本的动态部署,我们是同步的,5.3.0 到 5.3.1 到 5.3.2,这样一个好处是只要容器版本没有升级,只要有需求,patch 就可以一直升级,而且是无感知的差量升级。

周边优化点

最后来讲讲我们的周边优化点,为什么到今天才说要开源,做的过程当中还是遇到了不少问题。

第一点是 Bundle 的重复资源合并。因为我们发现,因为宿主问题,必然而然会出现冲突的问题,包括图片资源,我们会放到整个宿主类目中去。

第二是 Bundle 的依赖校验,以前是代码的话,是编译过的,但因为今天是二进制,这个问题会遗留到现场去,所以会看看 API 是否会影响 Bundle。

第三是类库“瘦身”,因为手淘依赖的各种中间件类库太多了,导致手淘本身很臃肿,方法数很大;所以打包的时候对类库有一个裁剪的过程,优化方法数。

第四是依赖导致的,依赖查询库。

第五是做 Dex File 等,进行混淆 Mapping。

最后是开源准备中,我们在工程期、运行期都会去做开源,并且将机制通过云服务的方式提供出来,阿里百川会提供 Atlas 的研发支撑能力,包括快捷的生成,发布,回滚,监控等能力。

点击链接,关注阿里百川更多开源产品。