阿里的Atlas组件化框架

Atlas简介

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

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

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

上图是手机淘宝的apk,第一层目录上与标准的apk是完全一样的。在App会有很多的so文件,每个so文件如果解开来看它的结构类似于完整的apk,但本身不能独立运行,它运行在整个容器里,每一个组件都是独立的Bundle。例如,手淘的模块层次划分如下图。
阿里的Atlas组件化框架
从模块来划分,手淘APK可以分为两层,上层是经过拆分的业务Bundle,扫码、评价、详情,各个业务之间可以进行功能的调用,可以通过路由调度到其他业务方。下层是共享的底层中间件,向业务方开放各种能力,如网络库、图片库等,会在容器里进行统一地把控,这样做的好处是包做到尽可能小,第二是性能佳。
阿里的Atlas组件化框架
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信息清单。
阿里的Atlas组件化框架
第二个是类加载,这里利用Delegate ClassLoader来动态加载组件的类。Delegate ClassLoader先查找宿主Bundle的PathClassLoader,然后根据前面的BundleList找到对应的BundleClassLoader。
阿里的Atlas组件化框架
第三个是资源,会用自己的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里去。
阿里的Atlas组件化框架
最后是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,主要做方法的修改,在实际中可以两个一起用。在工程构建期适配之后,可以做到一套代码两套方案通用。该方案的原理图如下:
阿里的Atlas组件化框架
自研动态部署功能实现原理,首先,对于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,如下图。
阿里的Atlas组件化框架

修改配置

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的基本功能,示例如下图。
阿里的Atlas组件化框架
下面是宿主中具体的跳转逻辑实现。

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());
       }
   }

}

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

打远程bundle的so文件

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

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

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

接着第一步,然后修改版本号,对本地Bundle进行文字修改,对app主项目也可以修改。
阿里的Atlas组件化框架
修改后,在Terminal里面输入:gradlew clean assembleDebug -DapVersion=1.0.0 -DversionName=1.0.1
回车,成功后会生成补丁差异包和更新说明,如下图:
阿里的Atlas组件化框架
把红色圈中的两个文件,放到手机内存卡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组件化框架官网文档

上一篇:当数据中心碰上云计算


下一篇:Hexo类型博客集成打赏功能