导读
如果你没有耐心,这篇文章对你来说可能是沉重的负担,你可以直接页内搜索“尾声”,那里省略了阶梯过程,直接是标准答案。
Small是一个专注于插件化的框架,特点是轻盈简洁,便于定制。轻盈体现在什么地方呢?如下图:
这就是Android Small Library的类结构。这在动辄上百K的第三方插件库群体中,简直是清流般的存在。你可以说它是小学生级别的代码数量,但不可否认的是,麻雀虽小,五脏俱全。对于插件化这一老生常谈的问题,Small用较小的代码量,交待了清楚了其背后的原理和运作机制。
闲言少叙,让我们翻开这本精致的教材吧!
Small的Lib部分代码比较少,但其实它通过Gradle脚本侵入了较多的打包的过程,这一点在这篇文章先按下不表。Lib部分我们首先从全局初始化开始:
【一】初始化
[ CODE 1 ]
public class Application extends android.app.Application {
public Application() {
// This should be the very first of the application lifecycle.
// It's also ahead of the installing of content providers by what we can avoid
// the ClassNotFound exception on if the provider is unimplemented in the host.
Small.preSetUp(this);
}
@Override
public void onCreate() {
super.onCreate();
// Optional
Small.setBaseUri("http://code.wequick.net/small-sample/");
Small.setLoadFromAssets(BuildConfig.LOAD_FROM_ASSETS);
}
}
看看Small.preSetUp()做了什么:
[ CODE 1.1 ]
public static void preSetUp(Application context) {
...
// 用全局的数据结构保存了3个Launcher
registerLauncher(new ActivityLauncher());
registerLauncher(new ApkBundleLauncher());
registerLauncher(new WebBundleLauncher());
Bundle.onCreateLaunchers(context);
}
这里有3个重要的类,ActivityLauncher,ApkBundleLauncher,WebBundleLauncher。这3个类是Small的灵魂。ActivityLauncher负责对宿主Activity启动进行处理,ApkBundleLauncher则是对插件Activity的启动进行处理。WebBundleLauncher是针对7.0系统新增的处理模块,这里不是重点,我们后续也基本会无视它。
在 Bundle.onCreateLaunchers()方法中,依次调用了每个launcher的onCreate()方法。我们依次分析3个Launcher的onCreate()方法。
只有ActivityLauncher没有实现onCreate(),看看ApkBundleLauncher的:
[ CODE 1.1.1 ]
@Override
public void onCreate(Application app) {
super.onCreate(app);
Object/*ActivityThread*/ thread;
List providers;
Instrumentation base;
ApkBundleLauncher.InstrumentationWrapper wrapper;
Field f;
// 获得当前的ActivityThread对象
// 通过ActivityThread.currentActivityThread()和Application.mLoadedApk.mActivityThread来获得
thread = ReflectAccelerator.getActivityThread(app);
// Replace instrumentation
try {
f = thread.getClass().getDeclaredField("mInstrumentation");
f.setAccessible(true);
base = (Instrumentation) f.get(thread);
wrapper = new ApkBundleLauncher.InstrumentationWrapper(base);
f.set(thread, wrapper);
} catch (Exception e) {
throw new RuntimeException("Failed to replace instrumentation for thread: " + thread);
}
// 替换ActivityThread的mH的mCallback,后面会有分析
ensureInjectMessageHandler(thread);
// Get content providers
...
}
对于这一段代码,我们首先需要知道Small允许插件在自己的AndroidManifest自声明Activity的原理。Small预先在宿主应用中声明一定数量的空壳Activity(占桩),然后hook Activity的启动流程,将宿主占桩的activity替换为插件的activity对象,达到狸猫换太子的效果。
对于当前进程,ActivityThread对象是唯一的。在ActivityThread对象内持有一个Instrumentation对象。所有start activity的指令最终都会调用到Instrumentation.execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target, Intent intent, int requestCode, Bundle options)方法中。因此,只要Instrumentation对象是唯一的(进程唯一),那么hook就可行。
万幸的是,Instrumentation确实是唯一的(不唯一也要强行唯一,大不了替换所有的Instrumentation)。持有Instrumentation对象的地方有多处,但五湖四海Instrumentation对象都来自同一个母亲——ActivityThread。比如Activity的mInstrumentation成员实际上是在Activity实例化以后,调用attach()方法,从ActivityThread中传递过来的。
很明显的,上面的代码中,ApkBundleLauncher.InstrumentationWrapper是启动插件activity的幕后黑手。我们先跟进去看看再说:
[ CODE 1.1.1.1 ]
/**
* Class for redirect activity from Stub(AndroidManifest.xml) to Real(Plugin)
*/
protected static class InstrumentationWrapper extends Instrumentation
implements InstrumentationInternal {
private Instrumentation mBase; // 原始的Instrumentation
// 占桩activity的数量。这里写死似乎并不太好,可以在编译阶段写入参数就更好了。对于插件比较复杂的应用,4个可能不够用
private static final int STUB_ACTIVITIES_COUNT = 4;
public InstrumentationWrapper(Instrumentation base) {
mBase = base;
}
// 前面分析过了,这就是关键的execStartActivity方法
/** @Override V21+
* Wrap activity from REAL to STUB */
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, android.os.Bundle options) {
// 拦截插件activity的注册,将插件activity替换为占桩activity,使之“合法化”
wrapIntent(intent);
// 拦截注册后的实例化过程,使得插件activity得到实例化
ensureInjectMessageHandler(sActivityThread);
return ReflectAccelerator.execStartActivity(mBase,
who, contextThread, token, target, intent, requestCode, options);
}
/** @Override V20-
* Wrap activity from REAL to STUB */
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode) {
...
}
@Override
/** Prepare resources for REAL */
public void callActivityOnCreate(Activity activity, android.os.Bundle icicle) {
...
}
@Override
public void callActivityOnStop(Activity activity) {
sHostInstrumentation.callActivityOnStop(activity);
// 当activity不可见时(即onStop回调时),做如下检查:
// 如果Small正在加载插件,那么检查当前进程是否位于前台,如果不是,那么直接结束进程(android.os.Process.killProcess())。
// 这样做的目的是,杀死进程之后冷启动才可以加载新的类和新的资源
...
}
// 这一步尤为关键。在插件activity销毁时,将对应的占桩activity释放出来。
// 占桩activity是有限的(这里写死了4个),如果不释放,后续启动插件activity就会因为找不到占桩activity而失败
@Override
public void callActivityOnDestroy(Activity activity) {
do {
if (sLoadedActivities == null) break;
String realClazz = activity.getClass().getName();
ActivityInfo ai = sLoadedActivities.get(realClazz);
if (ai == null) break;
inqueueStubActivity(ai, realClazz);
} while (false);
sHostInstrumentation.callActivityOnDestroy(activity);
}
@Override
public boolean onException(Object obj, Throwable e) {
...
// 当ContentProvider install failed的时候回调。
// 这里的处理是将加载失败的provider添加到mLazyInitProviders当中,后面进行加载
return super.onException(obj, e);
}
// 偷换activity全名
private void wrapIntent(Intent intent) {
ComponentName component = intent.getComponent();
String realClazz;
// 隐式查找activity
if (component == null) {
// Try to resolve the implicit action which has registered in host.
component = intent.resolveActivity(Small.getContext().getPackageManager());
if (component != null) {
// 非插件activity,当然不管了
return;
}
// Try to resolve the implicit action which has registered in bundles.
realClazz = resolveActivity(intent);
if (realClazz == null) {
// Cannot resolved, nothing to be done.
return;
}
} else {
realClazz = component.getClassName();
if (realClazz.startsWith(STUB_ACTIVITY_PREFIX)) {
// Re-wrap to ensure the launch mode works.
realClazz = unwrapIntent(intent);
}
}
if (sLoadedActivities == null) return;
// sLoadedActivities在后面会进行初始化,保存所有插件activity全名和ActivityInfo的对应关系
ActivityInfo ai = sLoadedActivities.get(realClazz);
if (ai == null) return;
// 原本的category的字段被换成了特殊记号 + 插件activity全名,方便后面识别
intent.addCategory(REDIRECT_FLAG + realClazz);
// 找到一个可用的位于宿主的占桩activity,并返回该占桩activity全名
String stubClazz = dequeueStubActivity(ai, realClazz);
intent.setComponent(new ComponentName(Small.getContext(), stubClazz));
}
private String resolveActivity(Intent intent) {
// sLoadedIntentFilters会在后面进行初始化,存放所有从插件解析的activity信息
if (sLoadedIntentFilters == null) return null;
Iterator>> it =
sLoadedIntentFilters.entrySet().iterator();
while (it.hasNext()) {
Map.Entry> entry = it.next();
List filters = entry.getValue();
for (IntentFilter filter : filters) {
if (filter.hasAction(Intent.ACTION_VIEW)) {
// TODO: match uri
}
// 必须定义Intent.CATEGORY_DEFAULT,否则无法隐式匹配
if (filter.hasCategory(Intent.CATEGORY_DEFAULT)) {
// 这里的匹配方式非常有趣
// 由于插件activity并非由系统(PackageManagerService)官方解析的,所以无法借助PMS来进行隐式匹配
// 于是作者偷了一下懒,要想隐式启动,就定义一个独一无二的action来标记吧
if (filter.hasAction(intent.getAction())) {
// hit
return entry.getKey();
}
}
}
}
return null;
}
private String[] mStubQueue;
/** Get an usable stub activity clazz from real activity */
private String dequeueStubActivity(ActivityInfo ai, String realActivityClazz) {
if (ai.launchMode == ActivityInfo.LAUNCH_MULTIPLE) {
// In standard mode, the stub activity is reusable.
// Cause the `windowIsTranslucent' attribute cannot be dynamically set,
// We should choose the STUB activity with translucent or not here.
Resources.Theme theme = Small.getContext().getResources().newTheme();
theme.applyStyle(ai.getThemeResource(), true);
TypedArray sa = theme.obtainStyledAttributes(
new int[] { android.R.attr.windowIsTranslucent });
boolean translucent = sa.getBoolean(0, false);
sa.recycle();
// 这里返回的值是:pkgName + ".A1"。这个activity对专门对应每次实例化新的activity的占桩activity
return translucent ? STUB_ACTIVITY_TRANSLUCENT : STUB_ACTIVITY_PREFIX;
}
// 如果能查找到满足条件的空的占桩activity,则返回全名,反之,返回null
int availableId = -1;
int stubId = -1;
int countForMode = STUB_ACTIVITIES_COUNT;
int countForAll = countForMode * 3; // 3=[singleTop, singleTask, singleInstance]
if (mStubQueue == null) {
// Lazy init
mStubQueue = new String[countForAll];
}
int offset = (ai.launchMode - 1) * countForMode;
for (int i = 0; i < countForMode; i++) {
String usedActivityClazz = mStubQueue[i + offset];
if (usedActivityClazz == null) {
if (availableId == -1) availableId = i;
} else if (usedActivityClazz.equals(realActivityClazz)) {
stubId = i;
}
}
if (stubId != -1) {
availableId = stubId;
} else if (availableId != -1) {
mStubQueue[availableId + offset] = realActivityClazz;
} else {
// TODO:
Log.e(TAG, "Launch mode " + ai.launchMode + " is full");
}
return STUB_ACTIVITY_PREFIX + ai.launchMode + availableId;
}
/** Unbind the stub activity from real activity */
private void inqueueStubActivity(ActivityInfo ai, String realActivityClazz) {
if (ai.launchMode == ActivityInfo.LAUNCH_MULTIPLE) return;
if (mStubQueue == null) return;
int countForMode = STUB_ACTIVITIES_COUNT;
int offset = (ai.launchMode - 1) * countForMode;
for (int i = 0; i < countForMode; i++) {
String stubClazz = mStubQueue[i + offset];
if (stubClazz != null && stubClazz.equals(realActivityClazz)) {
mStubQueue[i + offset] = null;
break;
}
}
}
上面的几段代码调用了一个方法ensureInjectMessageHandler()(见[ CODE 1.1.1.2 ]),这方法也非常关键。在上面的InstrumentationWrapper中,我们hook了execStartActivity()方法,这个方法是本地进程和AMS交互的起点。AMS认证,注册,管理的实际上是占桩的宿主activity。在这一步,我们成功瞒天过海,通过了AMS的考验。接下来,AMS经过一系列处理之后,会通过Binder通信回调本地进程的ActivityThread,通过ActivityThread的mH发送一个消息,在消息的处理逻辑里面,才开始真正实例化activity。这个时候我们hook消息处理的逻辑,使其真正实例化的是插件的activity,而不是占桩的activity。这样,插件activity就不再是没有出生证明的黑户,可以健康茁壮地成长了(拥有完整的activity生命周期)。
[ CODE 1.1.1.2 ]
private static void ensureInjectMessageHandler(Object thread) {
try {
Field f = thread.getClass().getDeclaredField("mH");
f.setAccessible(true);
Handler ah = (Handler) f.get(thread);
f = Handler.class.getDeclaredField("mCallback");
f.setAccessible(true);
...
if (needsInject) {
// Inject message handler
sActivityThreadHandlerCallback = new ActivityThreadHandlerCallback();
f.set(ah, sActivityThreadHandlerCallback);
}
} catch (Exception e) {
throw new RuntimeException("Failed to replace message handler for thread: " + thread);
}
}
接下来跟踪ActivityThreadHandlerCallback,看看是如何实例化插件activity的:
[ CODE 1.1.1.1.1 ]
/**
* Class for restore activity info from Stub to Real
*/
private static class ActivityThreadHandlerCallback implements Handler.Callback {
private static final int LAUNCH_ACTIVITY = 100;
private static final int CREATE_SERVICE = 114;
private static final int CONFIGURATION_CHANGED = 118;
private static final int ACTIVITY_CONFIGURATION_CHANGED = 125;
private Configuration mApplicationConfig;
// 处理AMS回执的消息
@Override
public boolean handleMessage(Message msg) {
switch (msg.what) {
case LAUNCH_ACTIVITY:
redirectActivity(msg);
break;
case CREATE_SERVICE:
ensureServiceClassesLoadable(msg);
break;
case CONFIGURATION_CHANGED:
recordConfigChanges(msg);
break;
case ACTIVITY_CONFIGURATION_CHANGED:
return relaunchActivityIfNeeded(msg);
default:
break;
}
return false;
}
private void redirectActivity(Message msg) {
Object/*ActivityClientRecord*/ r = msg.obj;
Intent intent = ReflectAccelerator.getIntent(r);
// 这里根据前面“特殊记号 + 插件activity全名”的格式,还原出插件activity的全名
String targetClass = unwrapIntent(intent);
boolean hasSetUp = Small.hasSetUp();
if (targetClass == null) {
// The activity was register in the host.
if (hasSetUp) return; // nothing to do
if (intent.hasCategory(Intent.CATEGORY_LAUNCHER)) {
// The launcher activity will setup Small.
return;
}
// Launching an activity in remote process. Set up Small for it.
Small.setUpOnDemand();
return;
}
if (!hasSetUp) {
// Restarting an activity after application recreated,
// maybe upgrading or somehow the application was killed in background.
Small.setUp();
}
// Replace with the REAL activityInfo
ActivityInfo targetInfo = sLoadedActivities.get(targetClass);
// 把插件activity的全名塞进去了
ReflectAccelerator.setActivityInfo(r, targetInfo);
}
private void ensureServiceClassesLoadable(Message msg) {
Object/*ActivityThread$CreateServiceData*/ data = msg.obj;
ServiceInfo info = ReflectAccelerator.getServiceInfo(data);
if (info == null) return;
String appProcessName = Small.getContext().getApplicationInfo().processName;
if (!appProcessName.equals(info.processName)) {
// Cause Small is only setup in current application process, if a service is specified
// with a different process('android:process=xx'), then we should also setup Small for
// that process so that the service classes can be successfully loaded.
Small.setUpOnDemand();
} else {
// The application might be started up by a background service
if (Small.isFirstSetUp()) {
Log.e(TAG, "Starting service before Small has setup, this might block the main thread!");
}
Small.setUpOnDemand();
}
}
private void recordConfigChanges(Message msg) {
mApplicationConfig = (Configuration) msg.obj;
}
private boolean relaunchActivityIfNeeded(Message msg) {
// 和sLoadedActivities比较配置信息是否已经更新。如果已经更新,那么relaunchActivity
}
}
我们知道ActivityThread内部的mH是一个Handler。Handler处理消息的优先级如下。我们看到优先级最高的是Message自带的callback。如果Message没有设置callback,那么将消息分发给Handler的callback,根据callback handleMessage()的返回值来确定是否回调Handler的handleMessage()。系统处理launch activity的逻辑都在mH的handleMessage()当中。所以只需要在mH的mCallback提前把ActivityInfo替换掉就可以了。但是如果要借助系统来初始化activity,那么一定要在callback的handleMessage()中返回false。
/**
* Handle system messages here.
*/
public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}
到目前为止,Small在activity的启动流程中做的手脚就已经分析完了。总结起来就是:
[1] 在向AMS注册activity之前把插件activity的全名替换成占桩的activity全名,使之合法化;
[2] AMS回调实例化启动的activity之前,把占桩的activity替换成插件activity,于是真正实例化的就是插件activity;
[3] 非ActivityInfo.LAUNCH_MULTIPLE模式的占桩activity是有限的,在插件activity销毁时,需要归还占桩activity。
接下来回到[ CODE 1 ]。在Application的onCreate()方法中,设置了base uri,并写入了配置LOAD_FROM_ASSETS。如果为true,那么加载apk插件,反之加载so插件。apk插件或者so插件是可以在打包时配置的,可以自由控制。
在Application的启动阶段,仅仅是为hook做了准备,提供了hook环境,但是并没有真正开始对插件的处理,仅有的耗时操作也就是反射替换了,整个过程还是比较环保的,基本上做到了插件懒加载。
【二】加载插件
做完热身运动之后,就开始真正的加载插件了。加载插件的动作一般由Small.setUp()触发:
[ CODE 2 ]
Small.setUp(LaunchActivity.this, new net.wequick.small.Small.OnCompleteListener() {
@Override
public void onComplete() {
Small.openUri("main", LaunchActivity.this);
}
}
});
跟进setUp()方法,真正进入主逻辑的是Bundle.loadBundles()方法:
[ CODE 2.1 ]
private static void loadBundles(Context context) {
JSONObject manifestData;
try {
// 读取bundle.json的内容,bundle.json见后面的代码段
File patchManifestFile = getPatchManifestFile();
// 从SharedPreferences中读取bundle.json内容,SP起了缓存作用
String manifestJson = getCacheManifest();
...
// Parse manifest file
manifestData = new JSONObject(manifestJson);
} catch (Exception e) {
e.printStackTrace();
return;
}
Manifest manifest = parseManifest(manifestData);
if (manifest == null) return;
setupLaunchers(context);
loadBundles(manifest.bundles);
}
在上面的getPatchManifestFile()方法读取了一个叫做bundle.json的文件。这个文件也比较关键,它是我们配置插件的“首选项”文件。我们贴一下官方demo里面这个文件的内容:
{
"version": "1.0.0",
"bundles": [
{
"uri": "lib.utils",
"pkg": "net.wequick.example.small.lib.utils"
},
{
"uri": "lib.style",
"pkg": "com.example.mysmall.lib.style"
},
{
"uri": "lib.analytics",
"pkg": "net.wequick.example.lib.analytics"
},
{
"uri": "main",
"pkg": "net.wequick.example.small.app.main"
},
{
"uri": "home",
"pkg": "net.wequick.example.small.app.home"
},
{
"uri": "mine",
"pkg": "net.wequick.example.small.app.mine"
},
{
"uri": "detail",
"pkg": "net.wequick.example.small.app.detail",
"rules": {
"sub": "Sub"
}
},
{
"uri": "stub",
"type": "app",
"pkg": "net.wequick.example.small.appok_if_stub"
},
{
"uri": "about",
"pkg": "net.wequick.example.small.web.about"
}
]
}
包名不用多说,uri是我们启动各个插件四大组件的钥匙,type则是模块类型,Small定义了4种类型:host(宿主模块),stub(宿主拆分模块,属于主包,但是拆分成了独立module),lib(公共依赖库),app(插件模块,也就是我们重点研究的对象)。
回到[ CODE 2.1 ],继续看setupLaunchers()方法。这个方法依次调用了ActivityLauncher、ApkBundleLauncher和WebBundleLauncher类的setUp()方法。
首先是ActivityLauncher.setUp():
[ CODE 2.1.1 ]
@Override
public void setUp(Context context) {
super.setUp(context);
// 从宿主的AndroidManifest读取宿主所有注册的activity信息
File sourceFile = new File(context.getApplicationInfo().sourceDir);
// 具体的解析类
// 这个类里面几乎解析了所有AndroidManifest的元素:包名,版本号,主题,Application类名
// 而下面的collectActivities()方法则搜集了所有的activity信息,重点搜集的是activity的全名和对应的intent-filter
BundleParser parser = BundleParser.parsePackage(sourceFile, context.getPackageName());
parser.collectActivities();
ActivityInfo[] as = parser.getPackageInfo().activities;
if (as != null) {
sActivityClasses = new HashSet();
// 解析的结果:主包所有activity的全名列表
for (ActivityInfo ai : as) {
sActivityClasses.add(ai.name);
}
}
}
ActivityLauncher相对简单,接下来是ApkBundleLauncher.setUp():
[ CODE 2.1.2 ]
@Override
public void setUp(Context context) {
super.setUp(context);
Field f;
// 这里使用了“AOP(面向切面编程)技术”,其实就是使用动态代理,hook TaskStackBuilder.IMPL.getPendingIntent()方法。
// 如果是使用PendingIntent来启动插件activity,那么替换类名。
// 这个场景似曾相识,没错,看看[ CODE 1.1.1.1 ]就明白了!
// AOP for pending intent
try {
f = TaskStackBuilder.class.getDeclaredField("IMPL");
f.setAccessible(true);
final Object impl = f.get(TaskStackBuilder.class);
InvocationHandler aop = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Intent[] intents = (Intent[]) args[1];
for (Intent intent : intents) {
sBundleInstrumentation.wrapIntent(intent);
intent.setAction(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_LAUNCHER);
}
return method.invoke(impl, args);
}
};
Object newImpl = Proxy.newProxyInstance(context.getClassLoader(), impl.getClass().getInterfaces(), aop);
f.set(TaskStackBuilder.class, newImpl);
} catch (Exception ignored) {
ignored.printStackTrace();
}
}
看了ApkBundleLauncher.setUp()的代码松了口气,代码量很小。但是这里也有一个问题,为什么这一步没有放到ApkBundleLauncher.onCreate()中一起做?
回到[ CODE 2.1 ],继续loadBundles(manifest.bundles)方法。这个方法做了很多事情:
[ CODE 2.1.3 ]
// 参数bundles是从bundle.json中解析得到的内容
private static void loadBundles(List bundles) {
sPreloadBundles = bundles;
// Prepare bundle
for (Bundle bundle : bundles) {
bundle.prepareForLaunch();
}
// Handle I/O
if (sIOActions != null) {
// 使用线程池,执行完所有的sIOActions
}
...
// Notify `postSetUp' to all launchers
for (BundleLauncher launcher : sBundleLaunchers) {
launcher.postSetUp();
}
// Free all unused temporary variables
for (Bundle bundle : bundles) {
if (bundle.parser != null) {
bundle.parser.close();
bundle.parser = null;
}
bundle.mBuiltinFile = null;
bundle.mExtractPath = null;
}
}
首先,针对每个bundle(即每个在bundle.json里面注册的模块信息),遍历找到能够解析它的BundleLauncher。ActivityLauncher只能解析“main”(宿主)的,ApkBundleLauncher则能解析“lib”和“app”的。在bundle.prepareForLaunch()的调用过程中,会依次回调每一个BundleLauncher(ActivityLauncher、ApkBundleLauncher、WebBundleLauncher均继承自BundleLauncher)的resolveBundle()方法:
public boolean resolveBundle(Bundle bundle) {
if (!preloadBundle(bundle)) return false;
loadBundle(bundle);
return true;
}
ApkBundleLauncher对preloadBundle的处理比较特殊,实际是由SoBundleLauncher.preloadBundle()实现的。我们戳进去看看:
[ CODE 2.1.3.1 ]
@Override
public boolean preloadBundle(Bundle bundle) {
...
// 检查是否支持(是否lib或者app)
...
// 版本比较,用较新版本的插件
File plugin = bundle.getBuiltinFile(); // 之前使用过的插件(可能是旧版本的)
// 这里的BundleParser好像在哪里见过,ActivityLauncher里面它就露脸了。用处是解析AndroidManifest几乎所有的信息
BundleParser parser = BundleParser.parsePackage(plugin, packageName);
File patch = bundle.getPatchFile(); // 直接下载下来在特定目录的apk或者so
BundleParser patchParser = BundleParser.parsePackage(patch, packageName);
if (parser == null) {
if (patchParser == null) {
return false;
} else {
parser = patchParser; // use patch
plugin = patch;
}
} else if (patchParser != null) {
if (patchParser.getPackageInfo().versionCode <= parser.getPackageInfo().versionCode) {
Log.d(TAG, "Patch file should be later than built-in!");
patch.delete();
} else {
parser = patchParser; // use patch
plugin = patch;
}
}
bundle.setParser(parser);
// Check if the plugin has not been modified
long lastModified = plugin.lastModified();
long savedLastModified = Small.getBundleLastModified(packageName);
if (savedLastModified != lastModified) {
// If modified, verify (and extract) each file entry for the bundle
// 有新的插件,那么进行CRC验证(保证传输过程中未被破坏)以及证书验证(保证来源合法)
if (!parser.verifyAndExtract(bundle, this)) {
bundle.setEnabled(false);
return true; // Got it, but disabled
}
Small.setBundleLastModified(packageName, lastModified);
}
// Record version code for upgrade
PackageInfo pluginInfo = parser.getPackageInfo();
bundle.setVersionCode(pluginInfo.versionCode);
bundle.setVersionName(pluginInfo.versionName);
return true;
}
我们可以看到,上面这个方法主要是进行了版本处理和合法校验。
接下来调用每个BundleLauncher的loadBundle()方法,重点是ApkBundleLauncher的实现:
[ CODE 2.1.3.1 ]
@Override
public void loadBundle(Bundle bundle) {
String packageName = bundle.getPackageName();
BundleParser parser = bundle.getParser();
// 这里似曾相识,在ActivityLauncher的setUp()方法中做过一模一样的搜集过程。
parser.collectActivities();
PackageInfo pluginInfo = parser.getPackageInfo();
// Load the bundle
String apkPath = parser.getSourcePath();
// 初始化apk映射信息,非常重要
if (sLoadedApks == null) sLoadedApks = new ConcurrentHashMap();
LoadedApk apk = sLoadedApks.get(packageName);
// 第一次肯定是空的
if (apk == null) {
apk = new LoadedApk();
// 初始化apk变量
...
// Load dex
final LoadedApk fApk = apk;
// 注意这里postIO()是将IO任务置入sIOActions任务队列中,这个队列后面会用到
Bundle.postIO(new Runnable() {
@Override
public void run() {
try {
fApk.dexFile = DexFile.loadDex(fApk.path, fApk.optDexFile.getPath(), 0);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
});
// Extract native libraries with specify ABI
String libDir = parser.getLibraryDirectory();
if (libDir != null) {
apk.libraryPath = new File(apk.packagePath, libDir);
}
sLoadedApks.put(packageName, apk);
}
...
// Record activities for intent redirection
if (sLoadedActivities == null) sLoadedActivities = new ConcurrentHashMap();
for (ActivityInfo ai : pluginInfo.activities) {
sLoadedActivities.put(ai.name, ai);
}
// 记录intent filter
...
}
// Set entrance activity
bundle.setEntrance(parser.getDefaultActivityName());
}
上面的loadBundle()主要是搜集activity信息,初始化插件apk信息,提交加载dex的任务,初始化一个很重要的变量sLoadedActivities,最后再设置一些信息。
回到[ CODE 2.1.3 ],往下来到Handle I/O部分。这里执行了刚刚提交的DexFile.loadDex()方法,将插件dex加载进来。然后又来到一个重要的方法:回调各个BundleLauncher的postSetUp()方法:
[ CODE 2.1.3.2 ]
@Override
public void postSetUp() {
super.postSetUp();
if (sLoadedApks == null) {
Log.e(TAG, "Could not find any APK bundles!");
return;
}
Collection apks = sLoadedApks.values();
// Merge all the resources in bundles and replace the host one
// 上面这句注释已经说得很明白了,把宿主的资源和插件的资源做合并,然后替换宿主的资源
// 这里合并,指的是把宿主和插件的资源路径(apk路径)合成数组,再调用AssetManager.addAssetPaths()方法
final Application app = Small.getContext();
// +1是因为要加入宿主的resource path
String[] paths = new String[apks.size() + 1];
paths[0] = app.getPackageResourcePath(); // add host asset path
...
// 替换资源操作:1. 实例化AssetManager;2. AssetManager.addAssetPaths(paths);
// 3. 反射替换系统所有持有AssetManager对象的地方。主要的工作量在第三步
ReflectAccelerator.mergeResources(app, sActivityThread, paths);
// Merge all the dex into host's class loader
ClassLoader cl = app.getClassLoader();
i = 0;
int N = apks.size();
String[] dexPaths = new String[N];
DexFile[] dexFiles = new DexFile[N];
for (LoadedApk apk : apks) {
dexPaths[i] = apk.path;
dexFiles[i] = apk.dexFile;
if (Small.getBundleUpgraded(apk.packageName)) {
// If upgraded, delete the opt dex file for recreating
if (apk.optDexFile.exists()) apk.optDexFile.delete();
Small.setBundleUpgraded(apk.packageName, false);
}
i++;
}
// 采用dex插桩的方式,加载插件的dex。
// 3.2版本后,都是采用找到BaseDexClassLoader的dexElements成员,
// 而dexElements是一个dalvik.system.DexPathList$Element数组。将插件的Elements放在dexElements数组元素的前面即可。
ReflectAccelerator.expandDexPathList(cl, dexPaths, dexFiles);
// Expand the native library directories for host class loader if plugin has any JNIs. (#79)
// 原理同上
...
ReflectAccelerator.expandNativeLibraryDirectories(cl, libPathList);
// Trigger all the bundle application `onCreate' event
// 回调所有插件Application的onCreate,这个细节都不放过,可以说是非常良心了
for (final LoadedApk apk : apks) {
String bundleApplicationName = apk.applicationName;
if (bundleApplicationName == null) continue;
try {
final Class applicationClass = Class.forName(bundleApplicationName);
Bundle.postUI(new Runnable() {
@Override
public void run() {
try {
BundleApplicationContext appContext = new BundleApplicationContext(app, apk);
Application bundleApplication = Instrumentation.newApplication(
applicationClass, appContext);
sHostInstrumentation.callApplicationOnCreate(bundleApplication);
} catch (Exception e) {
e.printStackTrace();
}
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
// Lazy init content providers
if (mLazyInitProviders != null) {
try {
Method m = sActivityThread.getClass().getDeclaredMethod(
"installContentProviders", Context.class, List.class);
m.setAccessible(true);
m.invoke(sActivityThread, app, mLazyInitProviders);
} catch (Exception e) {
throw new RuntimeException("Failed to lazy init content providers: " + mLazyInitProviders);
}
}
// Free temporary variables
sLoadedApks = null;
sProviders = null;
}
这个方法做了很多事情,(1)合并宿主资源和插件资源,并将合并后的AssetManager替换宿主中所有用到AssetManager的地方;(2)加载dex,用插桩的方式将插件dex放在dexElements的最前面,这样加载插件里面的类时,就会从dexElements中找到目标类。这里没有利用双亲委派原则,子classLoader加载插件dex的方式,感兴趣的读者可以去搜搜看另一种实现方式;(3)合并so,原理类似;(4)回调Application的onCreate。这一步可以看出插件是可以定义Application的,很有意思。但是这么做也有局限,因为插件可以懒加载,于是插件的Application的创建并不是和宿主Application创建同时进行的——这在很多情况下,尤其是插件完全独立于宿主的情况下会有歧义;(5)Content Provider加载。
至此,我们可以看到,四大组件,Small支持Activity和ContentProvider。
【三】启动Activity
那么,接下来就是要真正使用了。假设我们去打开一个插件的activity:
[ CODE 3 ]
// 这个“main”是哪里来的?参看前面bundle.json的文件内容
Small.openUri("main", context);
在对Uri进行解析的过程中,会首先把传入的Uri关键字拼接上我们在Application的onCreate中初始化的base uri。然后对uri进行判断,如果拼接后的Uri不是以“http”、“https”、“file”为scheme,那么Small不予处理。这也要求我们定义base uri的时候不能随心所欲。接着将此Uri寻找能够匹配的bundle(所有的bundle是之前从bundle.json中解析出来的),其实就是寻找能匹配的app(宿主)或者插件。那么,Uri的匹配规则是什么呢?我们假设bundle.json解析得到的Uri叫做声明Uri,把请求的Uri叫做请求Uri,那么:
- 请求Uri必须以声明Uri开头。请注意,这里的请求Uri是指base uri + 实际请求Uri。比如Small.openUri("main", context),Uri就是base uri + "main"。一般情况下,请求Uri和声明Uri两者是等价的关系;
- 不管请求Uri和声明Uri是否是完全等价关系,都必须满足:请求Uri = 声明Uri + rules里面定义的某个key。在解析bundle。json过程中会得到一个默认的rule,key-value形式如同:""-"Your value"。在请求Uri和声明Uri等价的情况下,就会默认匹配到这个rule;
- 如果value不为null,这个时候其实就已经匹配了。如果bundle.json里面没有定义rule,也会匹配。这时候Value其实是"",而不是null。最后,记录下来要匹配的path是上面的value值,而query是上面的请求Uri中“?”后面query params的部分。实际query会更复杂一些,这里不深入了。
一般情况下,path就是rules中定义的某个匹配的Value,query是空的。
匹配到合适的Uri之后,也就找到了能解析当前Uri的bundle,然后就能找到可以处理此Uri的BundleLauncher,即ActivityLauncher或者ApkBundleLauncher。顺理成章的,调用BundleLauncher的launchBundle()方法。首先看一下ActivityLauncher的launchBundle()方法,也即,看看如果要启动宿主包的Activity应该怎么做:
[ CODE 3.1 ]
@Override
public void launchBundle(Bundle bundle, Context context) {
prelaunchBundle(bundle);
super.launchBundle(bundle, context);
}
@Override
public void prelaunchBundle(Bundle bundle) {
// super是空实现
super.prelaunchBundle(bundle);
Intent intent = new Intent();
bundle.setIntent(intent);
// Intent extras - class
String activityName = bundle.getActivityName();
...
intent.setComponent(new ComponentName(Small.getContext(), activityName));
// Intent extras - params
String query = bundle.getQuery();
if (query != null) {
intent.putExtra(Small.KEY_QUERY, '?'+query);
}
}
上面的prelaunchBundle()方法调用了Bundle.getActivityName()。我们看一下它是怎么把activity name返回的:
[ CODE 3.1.1 ]
protected String getActivityName() {
String activityName = path;
String pkg = mPackageName != null ? mPackageName : Small.getContext().getPackageName();
char c = activityName.charAt(0);
if (c == '.') {
activityName = pkg + activityName;
} else if (c >= 'A' && c <= 'Z') {
activityName = pkg + '.' + activityName;
}
return activityName;
}
就是包名 + path。这又给我们命名activity提了要求,必须以包名开头,不然还是会找不到启动的activity。
取得Activity名称之后,执行super.launchBundle(bundle, context),也就是:
[ CODE 3.1.2 ]
public void launchBundle(Bundle bundle, Context context) {
if (context instanceof Activity) {
Activity activity = (Activity) context;
if (shouldFinishPreviousActivity(activity)) {
activity.finish();
}
activity.startActivityForResult(bundle.getIntent(), Small.REQUEST_CODE_DEFAULT);
} else {
context.startActivity(bundle.getIntent());
}
}
直接就启动了,把处理过程交给早先hook的Instrumentation和mH。
启动宿主activity是非常简单的,因为基本不需要额外的处理。接着看看ApkBundleLauncher的launchBundle():
[ CODE 3.2 ]
@Override
public void loadBundle(Bundle bundle) {
String packageName = bundle.getPackageName();
BundleParser parser = bundle.getParser();
// 这里似曾相识,在ActivityLauncher的setUp()方法中做过一模一样的搜集过程。
parser.collectActivities();
PackageInfo pluginInfo = parser.getPackageInfo();
// Load the bundle
String apkPath = parser.getSourcePath();
if (sLoadedApks == null) sLoadedApks = new ConcurrentHashMap();
LoadedApk apk = sLoadedApks.get(packageName);
if (apk == null) {
apk = new LoadedApk();
// apk 初始化...
// Load dex
final LoadedApk fApk = apk;
Bundle.postIO(new Runnable() {
@Override
public void run() {
try {
fApk.dexFile = DexFile.loadDex(fApk.path, fApk.optDexFile.getPath(), 0);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
});
// Extract native libraries with specify ABI
String libDir = parser.getLibraryDirectory();
if (libDir != null) {
apk.libraryPath = new File(apk.packagePath, libDir);
}
sLoadedApks.put(packageName, apk);
}
if (pluginInfo.activities == null) {
return;
}
// 进行一些初始化设置,包括Launcher activity等
...
}
这一步主要是进行了插件所有activity信息的搜集,然后加载dex,最后进行一些初始化设置。
完成如上逻辑之后,仍然调用BundleLauncher.launchBundle()方法(参见[ CODE 3.1.2 ]),顺利启动插件activity。
【四】尾声
至此,Small的主要逻辑已经全部详细分析完成了。现在总结一下Small代码运作流程:
还有Small的精髓,Activity启动流程的hook:
全部分析完成之后,我们可以看到也许Small并不是一个非常完美的插件化方案。它虽然号称轻量级,但是仍然对四大组件的启动流程侵入了很多自己的代码。并且在整个过程中,大量使用了反射。无论从稳定性和性能来讲,都会有消极的影响。但是不管如何,作者对四大组件启动流程的理解仍然是值得我们学习的。
有的读者看完之后也许有所感慨,回头看一眼标题的时候肯定会感到疑惑。四百多个issue,为什么正文只字未提?笔者是不是标题党?不是!!这里先奉上Small的github地址:
https://github.com/wequick/Small
以及官网:
http://code.wequick.net/Small
其实有很大部分问题都来自于编译过程,也就是本文并未涉及的Gradle脚本部分。还有资源id分配的问题,也是在Gradle脚本中解决的。其实Small有这么多的issue,恰好说明很受关注。这里先留点念想,下一篇文章我们继续分析。