这两天项目上要做 MVVM 和 DataBinding 的重构,所以插件化的文章就停了几天,后面会分享一下关于 MVVM 架构封装相关的文章。这篇文章我准备作为我插件化系列文章的最终章,我将分析目前最成熟最强大的插件化框架 Atlas 的一些基本流程的源码,以后如果有机会作更深入的研究,再进行插件化系列的更新。
Atlas 是手淘以及一系列阿里系 App 的插件化方案,实际上它又和其他插件化框架有一些区别,说的严谨点应该是一个容器化框架或者动态组件化框架,为什么这样说,后面会讲到。
Atlas 功能非常强大,技术也非常成熟,背后有阿里的一支技术实力非常强劲的团队在维护,刚开始学习 Atlas 的时候,我发现它官方的文档写的不是特别详细,导致一些配置我花了很多时间也没有完全搞明白,所以开始的印象不是很好。但自从我看过了官方的 Gitbook 源码文档后,我对 Atlas 团队的印象就彻底改观了,可以说,Gitbook 文档写的非常详细,而且逻辑清晰,实际上大家完全可以通过研读官方的源码文档,就可以对 Atlas 的整体启动流程和插件加载流程比较清楚了。
具体 Atlas 功能范围以及和一些常用插件化框架的对比,可以看我之前的文章。
要看这个文档,需要先把 atlas 从 Githud clone 下来,然后进入atlas-docs
目录,在终端输入
gitbook serve
最后在浏览器输入http://localhost:4000
即可进入 Gitbook 观看文档,当然提前你得配置下 Gitbook,这个请读者自行查阅步骤。这边文章也是根据官网的分析流程进行修改优化所得。言归正传,我们先来看下 Atlas 的启动过程。
Atlas 的启动过程
这里先说一下,为什么 Atlas 不算是真正意义上的插件化框架,而是一个动态组件化框架,主要原因是两者的原理有区别。
插件化的核心思想实际上是一种埋坑机制和借尸还魂,它会在宿主的 manifest 中预留很多的组件坑,在运行时,进行一系列的填坑操作,将插件的四大组件通过「借尸还魂」的手段来加载运行。
而 Atlas 有着本质区别,在编译期,每个插件 bundle 的 manifest 就已经写入到 apk 中了,所以运行的时候,不需要进行一些特殊操作,bundle 的组件的使用就像正常宿主中声明的组件一样,无需再进行额外处理了。
阿里的 Atlas 沙龙视频中也非常明确的讲述了 Atlas 的设计灵感就是 Android 系统本身,我们可以把 Android 系统看成一个容器化框架,那么 Android 系统中的所有 App 实际上就是一种插件,Android 系统本身是没有这些插件的,而作为开发者,我们实际上开发 App 就是开发插件的过程,通过发布,容器加载了插件,从而可以使用。
Atlas 就是按照这样的思想,他们试图将 Atlas 设计成这样的组件,我们所有的 bundle 就是插件。这是从一个特殊的角度来看待 Atlas 框架实现的一个效果。我们下面来看一张时序图,时序图能够帮我们更好的理清源码的调用逻辑和思路。
这张图描述了集成了 Atlas 框架后,整个 App 的启动流程,图来源于官网。通过一些技术手段,我们绕过了一些系统的步骤,达到我们期望的目的。
本文只关注步骤 1-5。
1. 入口
app 的入口是application
,这每一个 Android 开发者都了解,我们来看一下集成了 Atlas 后 application 是什么。代码来源于atlas-demo
。
<application
android:name=".DemoApplication"
android:allowBackup="true"
```
>
看起来,入口就应该是DemoApplication
,但我们看一下反编译 apk 后拿到的 manifest
<application
android:name="android.taobao.atlas.startup.AtlasBridgeApplication"
android:allowBackup="true"
```
>
很奇怪,实际的入口却是AtlasBridgeApplication
,原来,Atlas 在编译期就已经偷偷的替换了入口 application,但实际上,运行期依然会调用你自己写的 DemoApplication 的相关方法。我们来看下AtlasBridgeApplication
的源码。根据常规的启动流程,我们跟进方法attachBaseContext
。
protected void attachBaseContext(Context base){
super.attachBaseContext(base);
//一些逻辑
//1.构造 BridgeApplicationDelegate 对象
Class BridgeApplicationDelegateClazz =getBaseContext().getClassLoader.loadClass("android.taobao.atlas.bridge.BridgeApplicationDelegate");
Constructor<?> con = BridgeApplciationDelegateClazz.getConstructor(parTypes);
mBridgeApplicationDelegate = con.newInstance(this,getProcessName(...));
//2.执行 BridgeApplicationDelegate 的 attachBaseContext 方法
Method method = BridgeApplicationDelegateClazz.getDeclaredMethod("attachBaseContext");
method.invoke(mBridgeApplicationDelegate);
}
如果大家是看源码,这段代码还是比较复杂也比较长,但实际上我们抽取关键部分发现,它实际上就做了两件事:
- 反射构造 BridgeApplicationDelegate 实例
- 执行 BridgeApplicationDelegate 的 attachBaseContext 方法
这时候我们就要保持耐心,进入 BridgeApplicationDelegate 类,先看它的构造方法:
public BridgeApplicationDelegate(Application rawApplication ...){
mRawApplication = rawApplication;
PackageManagerDelegate.delegatepackageManager(
rawApplication.getBaseContext()
);
}
跟进方法delegatepackageManager
:
public static void delegatepackageManager(Context context){
mBaseContext = context;
1.反射pm
PackageManager manager = mBaseContext.getPackageManager();
Class ApplicationPackageManager = Class.forName("android.app.ApplicationPackageManager");
Field field = ApplicationPackageManager.getDeclaredField("mPM");
field.setAccessible(true);
Object rawPm = field.get(manager);
2.动态代理
Class IPackageManagerClass = Class.forName("android.content.pm.IPackageManager");
mPackageManagerProxyhandler = new PackageManagerProxyhandler(rawPm);
mProxyPm = Proxy.newProxyInstance(mBaseContext.getClassLoader,new Class[]{IPackageManagerClass},mPackageManagerProxyhandler);
3.替换pm
这段代码,注释写的比较清楚了,核心思想就是把系统的 pm 替换为我们实现的动态代理PackageManagerProxyhandler
,具体该动态代理的实现我们先不细讲,动态代理不熟悉的同学可以自己去查阅下资料,网上很多,我们现在继续看BridgeApplicationDelegate
中attachBaseContext
的实现。
2. BridgeApplicationDelegate.attachBaseContext
public void attachBaseContext(){
//2.1 hook 之前要准备的工作
AtlasHacks.defineAndVerify();
//2.2 回调预留接口
launcher.initBeforeAtlas(mRawApplication.getBaseContext());
//2.3 初始化 atlas
Atlas.getInstance().init(mRawApplication,mIsUpdated);
//2.4 处理 provider
AtlasHacks.ActivityThread$AppBindData_providers.set(mBoundApplication,null);
}
通过注释我们可以看到,方法被分成了四个部分,我们将一个个来看这四个步骤,需要提醒的是,大家需要记住现在的代码位置,对应好文章开头的时序图,理清我们整体的代码逻辑。
3. AtlasHacks.defineAndVerify
我们知道,Android 上所有的动态加载方案,有三个关键的地方是必须要处理的:
- 动态加载 class
- 动态加载资源
- 处理四大组件
为了实现这三个目标,我们需要在系统关键调用处进行 Hook,例如我们会通过对 ClassLoader 做一些手脚来进行动态加载 Class,四大组件的处理比较特殊,这在之前我们也提到了。
回到正题,我们来看看 Atlas 为了实现上述的要求做了什么。我们先看下AtlasHacks
这个类,这个类是用来定义 Atlas 需要 Hook 的类、方法、属性等字段。这段代码的静态成员变量的代码风格特别棒,大家可以学习一下,通过设置 AS 可以做到这样的效果。
//AtlasHacks.java
// Classes
public static HackedClass<Object> LoadedApk;
public static HackedClass<Object> ActivityThread;
public static HackedClass<android.content.res.Resources> Resources;
// Fields
public static HackedField<Object, Instrumentation> ActivityThread_mInstrumentation;
public static HackedField<Object, Application> LoadedApk_mApplication;
public static HackedField<Object, Resources> LoadedApk_mResources;
// Methods
public static HackedMethod ActivityThread_currentActivityThread;
public static HackedMethod AssetManager_addAssetPath;
public static HackedMethod Application_attach;
public static HackedField<Object, ClassLoader> LoadedApk_mClassLoader;
// Constructor
public static Hack.HackedConstructor PackageParser_constructor;
我们跟进方法defineAndVerify
函数:
//AtlasHacks.java
public static boolean defineAndVerify() throws AssertionArrayException{
allClasses();
allConstructors();
allFields();
allMethods();
}
这几个方法就是对之前定义字段进行赋值,例如allFields()
方法:
public static void allFields() throws HackAssertionException{
ActivityThread_mInsturmentation = ActivityThread.field("mInstrumentation").ofType(Instrumentation.class);
ActivityThread_mAllApplications = ActivityThread.field("mAllApplications").ofGenericType(ArrayList.class);
}
执行到这里,Atlas 框架的准备工作就完成了,下面就是整个框架的初始化。
4. 回调预留接口
这里的回调接口就是调用我们在 Atlas 配置的时候设置的preLaunch()
方法,该方法会在 Atlas 初始化之前运行的。我们来探索下 Atlas 是如何找到这个方法并调用的。
在时序图中的第 4 步,BridgeApplicationDelegate.attachBaseContext()
这个方法中,做了一个接口回调。
public void attachBaseContext(){
String preLaunchStr = (String)RuntimeVariables.getFrameworkProperty("preLaunch");
AtlasPreLauncher launcher = (AtlasPreLauncher) Class.forName(preLaunchStr).newInstance();
launcher.initBeforeAtlas(mRawApplication.getBaseContext());
}
通过preLaunch
字段读取类名,反射类上的initBeforeAtlas
方法,AtlasPreLauncher
实际上还是个接口,供接入者使用,在这个点上,Atlas 还没有对系统进行 hook,目前仍然是 Android 原生的运行环境。那么这个preLaunch
字段到底在哪里定义的呢?我们来反推一下。
进入RuntimeVariables.java
public static Object getFrameworkProperty(String fieldName){
Field field = FrameworkPropertiesClazz.getDeclaredField(fieldName);
return field.get(FrameworkPropertiesClazz);
}
我们跟进FrameworkProperties
发现这个类啥都没用,是个空实现,这种反常肯定有鬼,我们直接去看反编译的代码
public class FrameworkProperties{
public static String autoStartBundles;
public static String preLaunch;
static{
autoStartBundles = "com.taobao.firstbundle";
preLaunch = "com.taobao.demo.DemoPreLaunch";
}
}
看到这里我们就回想起来我们在 Gradle 配置的代码了,原来 Atlas 在 gradle 插件在编译的时候干了不少事。
tBuildConfig{
autoStartBundles = ['com.taobao.firstbundle']
preLaunch = 'com.taobao.demo.DemoPreLaunch'
}
这个部分牵扯开发-编译-运行三个阶段,下图可以帮助你捋一下关系。
5. atlas.init
准备工作做好之后,就是初始化了。我们回到Atlas.java
这个类中。
public void init(Application application,boolean reset){
//读取配置项
ApplicationInfo appInfo = mRawApplication.get...;
mRealApplicationName = appInfo.metaData.getString("REAL_APPLICATION");
boolean multidexEnable = appInfo.metaData.getBoolean("multidex_enable");
if(multidexEnable){
MultiDex.install(mRawApplication);
}
//...
}
这里读取了两个在编译期由 Atlas 插件写到 manifest 中的数据multidexEnable
和mRealApplicationName
,在 manifest 中它们是这样的:
<meta-data android:name="REAL_APPLICATION" android:value="com.taobao.demo.DemoApplication"/>
<meta-data android:name="multidex_enable" android:value="true"/>
multidexEnable 是 true,这个是在 gradle 中可配的。
mRealApplicationName 实际上是 DemoApplication,即 app 工程在 manifest 中指定的启动路径。下面我们继续看 init 方法。
public void init(Application application,boolean reset) {
//...
Atlas.getInstance().init(mRawApplication, mIsUpdated);
}
public void init(Application application,boolean reset) throws AssertionArrayException, Exception {
//...
//1. 换classloader
AndroidHack.injectClassLoader(packageName, newClassLoader);
//2. 换Instrumentatio
AndroidHack.injectInstrumentationHook(new InstrumentationHook(AndroidHack.getInstrumentation(), application.getBaseContext()));
//3. hook ams
try {
ActivityManagerDelegate activityManagerProxy = new ActivityManagerDelegate();
Object gDefault = null;
if(Build.VERSION.SDK_INT>25 || (Build.VERSION.SDK_INT==25&&Build.VERSION.PREVIEW_SDK_INT>0)){
gDefault=AtlasHacks.ActivityManager_IActivityManagerSingleton.get(AtlasHacks.ActivityManager.getmClass());
}else{
gDefault=AtlasHacks.ActivityManagerNative_gDefault.get(AtlasHacks.ActivityManagerNative.getmClass());
}
AtlasHacks.Singleton_mInstance.hijack(gDefault, activityManagerProxy);
}catch(Throwable e){}
//4. hook H
AndroidHack.hackH();
}
这里,注释也把具体步骤写了,主要是对系统关键地方进行了 hook,hook 的具体细节大家可以看源码或者田维术的 blog。
6. 处理 provider
在 2.4 步骤中,处理了 provider。
public void attachBaseContext(){
//...
//2.4 处理provider
Object mBoundApplication = AtlasHacks.ActivityThread_mBoundApplication.get(activityThread);
mBoundApplication_provider = AtlasHacks.ActivityThread$AppBindData_providers.get(mBoundApplication);
if(mBoundApplication_provider!=null && mBoundApplication_provider.size()>0){
AtlasHacks.ActivityThread$AppBindData_providers.set(mBoundApplication,null);
}
}
这里先读取 provider 数据,如果有的话,就从系统中删除,让系统认为 apk 并没有申请任何 provider,那么为啥要这么做呢,我们先回顾下 app 启动的流程:
上图中可以看到,第 4 步和第 7 步这两个关键调用之间,第 5 步调用了installContentProviders
:
private void installContentProviders(Context context, List<ProviderInfo> providers) {
for (ProviderInfo cpi : providers) {
installProvider(context, null, cpi,...);
}
}
收集了所有 provider 的信息,并且调用了installProvider
方法:
private IActivityManager.ContentProviderHolder installProvider(Context context,IActivityManager.ContentProviderHolder holder, ProviderInfo info,...) {
final java.lang.ClassLoader cl = c.getClassLoader();
localProvider = (ContentProvider)cl.loadClass(info.name).newInstance();
//...
}
这里的函数会根据 manifest 中登记的 provider 信息,实例化对象。但有些 provider 是存在于 bundle 中的,在 主 dex 中并不存在,如果不先清除掉 provider 的信息,进行延迟加载,程序就会出现ClassNotFind
崩溃,这就是为啥要有一个清除操作的原因。
未完待续。