導讀
如果你沒有耐心,這篇文章對你來說可能是沉重的負擔,你可以直接頁內搜索“尾聲”,那里省略了階梯過程,直接是標準答案。
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<ProviderInfo> 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對象,達到貍貓換太子的效果。
<activity
android:name="net.wequick.small.A"
android:configChanges="0x40002fff" />
<activity
android:theme="@ref/0x0103000f"
android:name="net.wequick.small.A1"
android:configChanges="0x40002fff" />
<activity
android:name="net.wequick.small.A10"
android:launchMode="1"
android:configChanges="0x40002fff" />
<activity
android:name="net.wequick.small.A11"
android:launchMode="1"
android:configChanges="0x40002fff" />
<activity
android:name="net.wequick.small.A12"
android:launchMode="1"
android:configChanges="0x40002fff" />
<activity
android:name="net.wequick.small.A13"
android:launchMode="1"
android:configChanges="0x40002fff" />
<activity
android:name="net.wequick.small.A20"
android:launchMode="2"
android:configChanges="0x40002fff" />
<activity
android:name="net.wequick.small.A21"
android:launchMode="2"
android:configChanges="0x40002fff" />
<activity
android:name="net.wequick.small.A22"
android:launchMode="2"
android:configChanges="0x40002fff" />
<activity
android:name="net.wequick.small.A23"
android:launchMode="2"
android:configChanges="0x40002fff" />
<activity
android:name="net.wequick.small.A30"
android:launchMode="3"
android:configChanges="0x40002fff" />
<activity
android:name="net.wequick.small.A31"
android:launchMode="3"
android:configChanges="0x40002fff" />
<activity
android:name="net.wequick.small.A32"
android:launchMode="3"
android:configChanges="0x40002fff" />
<activity
android:name="net.wequick.small.A33"
android:launchMode="3"
android:configChanges="0x40002fff" />
對于當前進程,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<Map.Entry<String, List<IntentFilter>>> it =
sLoadedIntentFilters.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, List<IntentFilter>> entry = it.next();
List<IntentFilter> 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的啟動流程中做的手腳就已經分析完了??偨Y起來就是:
[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<String>();
// 解析的結果:主包所有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<Bundle> 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<String, LoadedApk>();
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<String, ActivityInfo>();
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<LoadedApk> 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<String, LoadedApk>();
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的主要邏輯已經全部詳細分析完成了?,F在總結一下Small代碼運作流程:
還有Small的精髓,Activity啟動流程的hook:
全部分析完成之后,我們可以看到也許Small并不是一個非常完美的插件化方案。它雖然號稱輕量級,但是仍然對四大組件的啟動流程侵入了很多自己的代碼。并且在整個過程中,大量使用了反射。無論從穩定性和性能來講,都會有消極的影響。但是不管如何,作者對四大組件啟動流程的理解仍然是值得我們學習的。
有的讀者看完之后也許有所感慨,回頭看一眼標題的時候肯定會感到疑惑。四百多個issue,為什么正文只字未提?筆者是不是標題黨?不是??!這里先奉上Small的github地址:
https://github.com/wequick/Small
以及官網:
http://code.wequick.net/Small
其實有很大部分問題都來自于編譯過程,也就是本文并未涉及的Gradle腳本部分。還有資源id分配的問題,也是在Gradle腳本中解決的。其實Small有這么多的issue,恰好說明很受關注。這里先留點念想,下一篇文章我們繼續分析。