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,重启