当前位置: 首页 > 科技观察

Atlas:手淘原生容器化框架与思考

时间:2023-03-21 10:55:11 科技观察

在刚刚过去的云栖大会上,手淘宣布其移动容器化框架Atlas将于2017年初开源。过去分享,外界一直密切关注,现在终于要开源了。本文将介绍Atlas的设计思路和淘宝对容器化、组件化、动态化的思考。主要内容来源于阿里巴巴资深技术专家倪胜华(李璇)在2016杭州云栖大会上的分享。什么是阿特拉斯?2013年,手机淘宝航母战略的制定,带来了业务和开发人员的成倍增长。从不到一百人,增加了四五倍。与此同时,商家数量也大幅增加。整个客户端的结构和发布节奏都受到很大的挑战。今天的阿特拉斯。Atlas是一个Android客户端容器化框架,主要提供组件化、动态化、解耦等方面的支持。支持工程师在工程编码期、Apk运行期、后续运维修复期各种问题。项目期间,实现了项目的自主开发和调试功能,项目模块相互独立。在运行时,实现完整的组件生命周期映射、类隔离等机制。在运维期,提供快速增量更新修复能力,快速升级。Atlas是一个项目期和运营期协同工作的框架。其特点是尽量将部分工作放在项目期,以保证运行期的简单性和稳定性。目前Atlas广泛应用于淘宝APP。60多个业务组件、20个协作团队、数百万行代码都在Atlas上运行。其快速迭代能力使得应用发布周期从每月每周变为随时,在过去六个月中已经发布了446次。另外,Atlas本身非常轻量级,只有90多个类,支持大大小小的App开发。该框架用于从大型移动购物到相对较小的阿里健康的所有领域。其稳定性也经过测试,兼容Android4.x及以上系统版本。手淘的整体崩溃率一直维持在万分之五左右,因为容器导致的崩溃率不到1%。从这个意义上讲,Atlas首先需要解决大规模团队协作的问题。需求包括并行开发、快速迭代、工程解耦,那么要解决的问题就是客户端的动态更新问题。手机淘宝内部思路的解决方案是组件化。Atlas的组件化实现了组件化,业界称之为插件化,但是Atlas的组件化与现在的插件化有些区别。组件化就是需要知道组件的功能,设计更加规范。(手机淘宝APK包目录结构)这是手机淘宝的APK包。顶级目录与标准APK完全相同。APP里面会有很多so文件。结构类似于一个完整的APK,但不能独立运行。它和很多插件的区别在于,它在运行时运行在整个容器中,每个组件都是一个独立的bundle。从模块上来说,手淘APK可以分为两层。上层是拆分业务捆绑、扫码、评价、详情。各业务之间可以调用函数,也可以通过路由分发给其他业务方。下层是共享的底层中间件,向业务方开放各种能力,如网络库、图片库等,并会在容器中进行统一控制。这样做的好处是包应该尽可能小。二是性能好。这部分是Atlas的整体设计,分为五层:第一层我们称为Hack层,包括OSHacktoolkit&verifier,这里我们做一些系统能力的扩展,然后做一些安全验证。第二层是BundleFramework,它是我们的基础容器框架,提供Bundle管理、加载、生命周期、安全等一些最基础的能力。第三层是运行时管理层,包括列表。我们会把所有的Bundle和他们的capabilities列在一个列表中,调用的时候很容易找到;另一个是版本管理,它会管理所有Bundle的版本;然后是Proxy,这里类似于业界的一些插件框架机制。我们会代理系统的运行环境,让Bundle运行在我们的容器框架上;然后是调试和监控工具,方便项目期间的开发和调试。第四层是业务层。这里我们将一些接口暴露给业务端,比如框架生命周期、配置文件、工具库等。最上层是应用访问层,也就是我们的业务代码。因此,Atlas作为一个框架,提供了比较完善的能力。业务层的开发可以在框架生命周期的各个环节做一些自定义的动作,可以自由调用系统、框架,甚至其他组件发布的能力。上面在容器级别以更一般的方式讨论了组件化的技术细节。接下来,我们将讨论一些具体的细节。Bundle的生命周期会提供细粒度的节点。比如下面是Bundle从加载到运行的一个循环:startInstall:开始加载。这个时候框架会做一些事情,拷贝文件,释放lib,加载Bundle;已安装:加载完成。这时候框架会注入资源路径,创建类加载器;resolved:解析完成后,框架会检查组件配置是否合法,是否可以解析;active:运行组件,即开始运行组件Bundle;启动:运行成功。组件化涉及到的第一个问题是Manifest处理。一是因为来源很多,有hostManifest、AarManifest和componentManifest。另外,不同组件的Manifest经常变化,需要我们灵活应对。这里的做法是合并项目期间的所有Manifests。这里需要注意的是,Bundle的依赖是单独合并的,因为这涉及到依赖仲裁的问题。***解析每个Bundle的MergeManifest,得到整个package的BundleInfoList,也就是我们上面说的Bundle信息列表。二是类加载,其中DelegateClassLoader用于动态加载组件类。DelegateClassLoader首先查找宿主Bundle的PathClassLoader,然后根据前面的BundleList找到对应的BundleClassLoader。三是资源。我们将系统的资源替换成自己的DelegeteResources,安装时Bundle的资源会一一添加到AssertPath中。由于添加Bundle的顺序不固定,不分区会导致资源查找混乱。另外,Dalvik和ART上资源搜索过程的顺序是不一样的。另外,小米等系统会重写自己的资源,所以我们会适配不同的机型,后期或者转发添加AssetsPath。系统AssetManager是一个Singletons,默认向后追加。如果它们向前追加,则需要重新创建AssetsManager对象。同样,动态部署maindex以达到替换原有资源的目的时,插入顺序必须与查找顺序一致。还需要注意的是,每次更新resourceTable时,必须保证apkresource和runtime系统资源,如webview、bundleresource等已经添加成功,并且唯一且顺序正确。不同bundle的资源之间可能存在命名冲突。我们采用比较简单的方式,将各自的bundle分配给不同的ID,保证所有业务资源不会冲突,尽量在项目期解决问题。很多代码中,通过反射调用整个资源,在5.0以上的系统是没有问题的。它只会找到第一个。业务代码,过去怎么写,今天还是怎么写。在组件性能方面,我们引入了按需加载,因为手淘APK有70多个Bundle,每个用户实际使用时只需要5个或10个Bundle,所以不需要加载所有的Bundle。Bundles之间通过Android原生的四个组件进行隔离和交互,这样可以更好的解耦Bundles。我们所有的调用条目都基于BundleInfolist。根据这个列表信息,我们可以得到组件所在的Bundle。如果需要加载,我们会进行install、dexopt等操作。另外,为了解决组件依赖的问题,定义了两种新的组件格式Awb(businessbundle)和solib(solibrary)。前者与AAR一致,但不添加本地lib,在构建时做依赖仲裁区分,而后者是对Nativeso库的依赖。awb其实就是AAR,只是修改了后缀。如果你的包放在hostbundle里,就用AAR,如果是componentbundle,就用Awb。对于业务bundle的依赖,我们会在构建期间分别打包hostbundle、业务bundle和它们的依赖,然后按照最短路径和***语句原则进行树仲裁,获取各自需要的依赖捆。打包的时候会把依赖的库放到各自的Bundle中。***是APK构建,我们对它做了比较大的调整。上图中,左边的部分其实是一个标准的APK构建流程,包括加工、编译、签名。我们之间的区别在于Awb需要特殊对待。awb的资源是根据host的resource.ap_和package中的资源构建的。R文件由BundleR资源和主机的R资源合并而成。然后我们进行AaptModify,给每个awb分配不同的packageId,然后统一混淆,产生每个AWB的Dex,打包成APK,签名后复制到libs,重命名为so文件,然后合并到taobaoAPK。这就是我们整个组件化过程。Atlas动态化在容器框架内。组件化和动态化相辅相成。组件只是解决了解耦的问题,但是如果我们想要随时发送包,就必须让容器框架具备动态化的能力。我们完成了Atlas的组件化之后,我们提供了动态支持。动态化的好处之一是减小了包的大小。运行后我们可以下载一些包到应用程序中。另一个是动态发布和修复的能力。Atlas是一种增量动态解决方案,提供动态部署能力,主要目标是动态发布服务和修复问题。它基于淘宝自研的差分算法,主bundle基于ClassLoader机制,业务bundle基于差分merge,支持所有业务类型。此外,Atlas还支持Andfix作为插件。目标是快速修复故障。它的原理是基于Nativehook,主要是方法修饰。在实践中,两者可以结合使用。项目建设期适配后,一套规范可与两套解决方案通用。自研动态部署功能的实现原理,首先,对于DexPatch的生成,我们通过修改Dex的字节码来实现,将Dex文件转成Smali,解析其中的ClassDef和ClassDataMethod结构,进行删除和添加,修改类,然后通过Diff处理得到差异文件,再通过Merge处理生成patch。二是整个资源补丁的生成,分为两部分。一个是业务bundle,本来就是一个持续加载的过程。实现起来比较简单,可以通过Md5diff/BSDiff获取。对于mainbundle,由于Android本身有限制,所有资源必须在basepackage中,添加新资源不会生效。所以一种做法是在打包的时候预留大量的空资源。此外,更新现有资源是通过资源覆盖来完成的。***,如果增加了新的业务,就会增加一个新的Activity。我们的做法是先在Manifest中嵌入一个StubActivity,然后在Instrumentation.execStartActivity()阶段替换它。同时配合IntentsetFlag模拟Activity启动方式,继续startActivity,然后System_server进程处理,更新ActivityStack,创建binder,通知ActivityThread创建实例。***我们在ActivityThread的handler中进行拦截,更新ActivityInfo等信息,创建目标Activity。另外,在工程实践中,由于patch的生成会涉及到Dex和资源的baseline,所以我们在部署的时候每次发布APK包的时候都会把AP(baselinepackage)发布给Maven。AP基线包包含影响基线的所有项目。文件,***是AndroidAPK,其次是Mapping.txt,***是Dependency.txt,所以整个构建的速度会很快。所以在我们的方式中,版本升级是不一样的。比如今天淘宝会更新详情,出一个版本。这个版本可能不是应用市场的版本,而是一个补丁包。业务版本的动态部署,我们是同步的,5.3.0到5.3.1再到5.3.2,这样的好处是只要容器版本不升级,只要有需求,补丁一直可以升级,和升级没有感知上的区别。外设优化点***说说我们的外设优化点。为什么到今天才讲开源,在做的过程中还是遇到了很多问题。最重要的一点是Bundle重复资源的合并。因为我们发现因为host的问题,难免会出现冲突,包括图片资源,我们会把它们放在整个host类里面。二是Bundle依赖验证。如果以前是code的话,是编译出来的,但是因为今天是binary,所以这个问题会留在现场,看API会不会影响Bundle。三是类库的“瘦身”,因为手淘依赖的中间件库太多,导致手淘本身非常臃肿,方法众多;所以在打包的时候,有一个对类库进行剪裁和优化的过程。方法的数量。第四种是依赖导致的,依赖于查询库。五是做DexFile等,用于混淆Mapping。***是开源准备。我们会在项目期和运营期开源,通过云服务提供机制。阿里百川将提供Atlas研发支撑能力,包括快速生成、发布、回滚、监控等能力。