您现在的位置是:首页 > 文章详情

阿里的Atlas组件化框架

日期:2018-05-09点击:428

Atlas简介

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

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

Atlas是工程期和运行期共同起作用的框架,它尽量将一些工作放在工程期,这样保证运行期的简单、稳定。下面是Atlas组件化的一个框架原理图。
这里写图片描述

上图是手机淘宝的apk,第一层目录上与标准的apk是完全一样的。在App会有很多的so文件,每个so文件如果解开来看它的结构类似于完整的apk,但本身不能独立运行,它运行在整个容器里,每一个组件都是独立的Bundle。例如,手淘的模块层次划分如下图。
这里写图片描述
从模块来划分,手淘APK可以分为两层,上层是经过拆分的业务Bundle,扫码、评价、详情,各个业务之间可以进行功能的调用,可以通过路由调度到其他业务方。下层是共享的底层中间件,向业务方开放各种能力,如网络库、图片库等,会在容器里进行统一地把控,这样做的好处是包做到尽可能小,第二是性能佳。
这里写图片描述
Atlas的整体分为5层:
第一层称之为Hack层,包括OS Hack toolkit & verifier,这里对系统能力做一些扩展,然后做一些安全校验。

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

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

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

组件化技术细节

关于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就可以一直升级,而且是无感知的差量升级。

说明,本部分来源于手淘组件化的分享。官方链接地址为:
https://alibaba.github.io/atlas/index.html

Atlas项目实践

前面我们介绍了Atlas的一些原理性的东西,总的来说,Atlas就是利用远程Bundle的下发方式,为了减少apk的安装体积,Atlas项目使用bundle的加载方式。当用户安装没有Bundle的apk文件时,就从网上下载这个bundle的so,然后加载打开,下载逻辑使用app原生代码编写,加载用按需加载的策略。

Atlas接入

首先新建一个项目,然后新建几个module,如下图。
这里写图片描述

修改配置

1,把gradle改为3.3

2,然后我们需要为Atlas添加一些配置,引用Atlas插件及依赖仓库,修改工程gradle文件。

 buildscript { repositories { jcenter()} dependencies { //不需要再依赖classpath "com.android.tools.build:gradle" classpath "com.taobao.android:atlasplugin:2.3.3.beta2" } }

3,修改app的build.gradle脚本,需要注意包名的对应关系。

// 需要放最上面初始化 group = "mmc.atlastest" version = getEnvValue("versionName", "1.0.0"); def apVersion = getEnvValue("apVersion", ""); apply plugin: 'com.android.application' apply plugin: 'com.taobao.atlas' android { compileSdkVersion 25 buildToolsVersion "25.0.2" defaultConfig { applicationId "mmc.atlastest" minSdkVersion 15 targetSdkVersion 25 versionCode 1 versionName version testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } } dependencies { compile fileTree(include: ['*.jar'], dir: 'libs') androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { exclude group: 'com.android.support', module: 'support-annotations' }) compile 'com.android.support:appcompat-v7:25.3.1' testCompile 'junit:junit:4.12' //atlas的依赖 compile('com.taobao.android:atlas_core:5.0.7@aar') { transitive = true } compile 'com.taobao.android:atlasupdate:1.1.4.7@aar' compile 'com.alibaba:fastjson:1.1.45.android@jar' //项目依赖 compile project(':librarybundle') compile project(':localbundle') compile project(':remotebundle') } //加入以下配置 atlas { atlasEnabled true tBuildConfig { // autoStartBundles = ['com.android.homebundle'] //自启动bundle配置 outOfApkBundles = ['remotebundle'] //远程module,列表来的,可填多个 preLaunch = 'mmc.atlastest.AtlasLaunch' //AppApplication启动之前调用,这个类下面放出代码 } patchConfigs { debug { createTPatch true } } buildTypes { debug { if (apVersion) { // 打差异补丁 gradlew assembleDebug -DapVersion=1.1.0 -DversionName=1.1.1 // 对应着本地maven仓库地址 .m2/repository/mmc/atlastest/AP-debug/1.1.4/AP-debug-1.1.4.ap baseApDependency "mmc.atlastest:AP-debug:${apVersion}@ap" patchConfig patchConfigs.debug } } } } String getEnvValue(key, defValue) { def val = System.getProperty(key); if (null != val) { return val; } val = System.getenv(key); if (null != val) { return val; } return defValue; } apply plugin: 'maven' apply plugin: 'maven-publish' publishing { // 指定仓库位置 repositories { mavenLocal() } publications { // 默认本地仓库地址 用户目录/.m2/repository/ maven(MavenPublication) { //读取ap目录上传maven artifact "${project.buildDir}/outputs/apk/${project.name}-debug.ap" //生成本地maven目录 groupId group artifactId "AP-debug" } } }

4,修改远程bundle和本地bundle的build.gradle脚本

apply plugin: 'com.android.library' apply plugin: 'com.taobao.atlas' atlas { bundleConfig{ awbBundle true } buildTypes { debug { baseApFile project.rootProject.file('app/build/outputs/apk/app-debug.ap') } } } //只添加上面的配置就行了,下面的是默认生成的 android { compileSdkVersion 25 buildToolsVersion "25.0.2" defaultConfig { minSdkVersion 15 targetSdkVersion 25 versionCode 1 versionName "1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } } dependencies { compile fileTree(include: ['*.jar'], dir: 'libs') androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { exclude group: 'com.android.support', module: 'support-annotations' }) compile 'com.android.support:appcompat-v7:25.3.1' testCompile 'junit:junit:4.12' //依赖lib中间bundle compile project(':librarybundle') }

5,在宿主app的application中添加如下代码。

public class DemoApplication extends Application { @Override public void onCreate() { super.onCreate(); Atlas.getInstance().setClassNotFoundInterceptorCallback(new ClassNotFoundInterceptorCallback() { @Override public Intent returnIntent(Intent intent) { final String className = intent.getComponent().getClassName(); final String bundleName = AtlasBundleInfoManager.instance().getBundleForComponet(className); if (!TextUtils.isEmpty(bundleName) && !AtlasBundleInfoManager.instance().isInternalBundle(bundleName)) { //远程bundle Activity activity = ActivityTaskMgr.getInstance().peekTopActivity(); File remoteBundleFile = new File(activity.getExternalCacheDir(),"lib" + bundleName.replace(".","_") + ".so"); String path = ""; if (remoteBundleFile.exists()){ path = remoteBundleFile.getAbsolutePath(); }else { Toast.makeText(activity, " 远程bundle不存在,请确定 : " + remoteBundleFile.getAbsolutePath() , Toast.LENGTH_LONG).show(); return intent; } PackageInfo info = activity.getPackageManager().getPackageArchiveInfo(path, 0); try { Atlas.getInstance().installBundle(info.packageName, new File(path)); } catch (BundleException e) { Toast.makeText(activity, " 远程bundle 安装失败," + e.getMessage() , Toast.LENGTH_LONG).show(); e.printStackTrace(); } activity.startActivities(new Intent[]{intent}); } return intent; } }); } }

6、在app新建一个类AtlasLaunch,继承AtlasPreLauncher。

public class AtlasLaunch implements AtlasPreLauncher { @Override public void initBeforeAtlas(Context context) { } }

项目结构

然后写app的基本功能,示例如下图。
这里写图片描述
下面是宿主中具体的跳转逻辑实现。

public class MainActivity extends BaseActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } //打开远程bundle public void remote(View view){ Intent intent = new Intent(); intent.setClassName(view.getContext(), "mmc.remotebundle.RemoteActivity"); startActivity(intent); } //打开本地bundle public void local(View view){ Intent intent = new Intent(); intent.setClassName(view.getContext(), "mmc.localbundle.LocalActivity"); startActivity(intent); } //更新补丁 public void update(View view){ new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(Void... voids) { update(); return null; } @Override protected void onPostExecute(Void aVoid) { Toast.makeText(MainActivity.this, "更新完成,请重启", Toast.LENGTH_LONG).show(); } }.execute(); } private void update(){ File updateInfo = new File(getExternalCacheDir(), "update.json"); if (!updateInfo.exists()) { showToast("更新信息不存在,请先 执行 buildTpatch.sh"); return; } String jsonStr = new String(FileUtils.readFile(updateInfo)); UpdateInfo info = JSON.parseObject(jsonStr, UpdateInfo.class); File patchFile = new File(getExternalCacheDir(), "patch-" + info.updateVersion + "@" + info.baseVersion + ".tpatch"); try { AtlasUpdater.update(info, patchFile); } catch (Throwable e) { e.printStackTrace(); showToast("更新失败, " + e.getMessage()); } } }

安装运行项目,就可以看到如下图所示的效果。
这里写图片描述
此时还有以下工具需要完成:
1,这个时候点击远程bundle会弹出说没有so文件,因为还没打so包呢
2、点击本地bundle,是可以跳转到那个本地bundle页面
3、点击更新补丁,会提示更新信息不存在

打远程bundle的so文件

下面要打包出远程bundle的so文件,补丁的差异包和更新说明。

1,打so文件,每个远程都会生成一个so文件的。打开AS的Terminal,输入:gradlew clean assembleDebug publish
,然后回车,如下图:
这里写图片描述
正常的话,会提示下面的正确信息。
这里写图片描述
成功的话,app的build文件夹里,会生成这个so文件,这个就是远程bundle的so文件,把这个文件放到手机内存卡Android/data/mmc.atlastest/cache 文件夹里面,然后再打开app,点击“远程Bundle”,这个时候就能跳转过去了。
这里写图片描述

更新补丁,打差异包和更新说明

接着第一步,然后修改版本号,对本地Bundle进行文字修改,对app主项目也可以修改。
这里写图片描述
修改后,在Terminal里面输入:gradlew clean assembleDebug -DapVersion=1.0.0 -DversionName=1.0.1
回车,成功后会生成补丁差异包和更新说明,如下图:
这里写图片描述
把红色圈中的两个文件,放到手机内存卡Android/data/mmc.atlastest/cache 文件夹里面,然后点击“更新补丁”,过一会,提示更新成功后,就退出杀死app,再打开就是后面修改的内容了。

Atlas使用步骤总结

1、配置好,安装1.0.0的app
2、用命令“gradlew clean assembleDebug publish”打AP,得到远程Bundle的so文件
3、修改版本号,修改版本内容
4、用命令“gradlew clean assembleDebug -DapVersion=1.0.0 -DversionName=1.0.1”打差异包补丁和更新说明
5、把上面得到的三个文件放到app的cache目录里面
6、运行更新方法,杀死app,重启

附件:阿里的Atlas组件化框架
Atlas组件化框架官网文档

原文链接:https://yq.aliyun.com/articles/591863
关注公众号

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。

持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。

转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。

文章评论

共有0条评论来说两句吧...

文章二维码

扫描即可查看该文章

点击排行

推荐阅读

最新文章