2016-2017年是插件化遍地开花的一年,各家大厂都开源了自己的插件化框架、热修复技术,网上也已经有许多介绍和分析的文章。但是好像对Small源码的深入分析还不多,所以这里凭自己的理解来梳理一下Small整个插件化流程,帮助想使用Small的人来了解这个框架同时也是自己对这个框架知识点的一个巩固,同时文章里的一些术语可能是small专有的,阅读前可以去官网去了解下,最后有疏漏的地方欢迎大家评论拍砖。
Small框架原理简介
Small是Github上一款开源的插件化框架。实现Android插件化的核心技术是:动态加载类、动态加载资源和动态注册组件。
插件化原理:这里就简单的带入一下,因为各家的实现基础原理都是差不多,类的加载通过反射把插件包中的dex插入到BaseDexClassLoader
的pathList
数组中保证类能够正确被找到,资源也通过反射调用AssetManager
的addAssetPaths
方法保证资源能够被正确的加载,JNI中的so包也可以通过反射插入到BaseDexClassLoader
的nativeLibraryDirectories
数组中。当然由于用了很多的反射也需要适配的很多不同版本的API,以及各种国内手机厂商ROM,这会在后面详细介绍。
分离插件包的技术:gradle插件,用来实现small中的lib
和app
打包。将在下一篇详细介绍,gradle插件相关源码gradle-small-plugin。
Small初始化
在Application
的构造方法中执行Small.preSetUp(this)
。由于插件中所有的ContentProviders
必须在宿主中的AndroidManifest
中声明,此时它的install
过程会抛出ClassNotFoundException
,所以必须在这之前捕获这个异常对它进行延时安装。
public static void preSetUp(Application context) {
if (sContext != null) {
return;
}
sContext = context;
// Register default bundle launchers
registerLauncher(new ActivityLauncher());
registerLauncher(new ApkBundleLauncher());
registerLauncher(new WebBundleLauncher());
Bundle.onCreateLaunchers(context);
}
sContext
作为一个全局的Application静态变量,通过判断perSetUp在同一个进程中只可能初始化一次,然后可以看到执行了三次registerLauncher
方法来把三种不同的BundleLauncher
(包括ActivityLauncher
、ApkBundleLauncher
、WebBundleLauncher
)加入到一个ArrayList
中。
Small设置setUp
setUp是small功能的主入口,在github的sample代码中可以看到LaunchActivity
中onStart
方法中执行setUp
方法。
public static void setUp(Context context, OnCompleteListener listener) {
if (sContext == null) {
// Tips for CODE-BREAKING
throw new UnsupportedOperationException(
"Please call `Small.preSetUp' in your application first");
}
if (sHasSetUp) {
if (listener != null) {
listener.onComplete();
}
return;
}
Bundle.loadLaunchableBundles(listener);
sHasSetUp = true;
}
判断是否已经进行过前面的perSetUp
步骤,并且在这里setUp
完成后会有个标志sHasSetUp
确保应用进程存活的情况再次进入应用不会再走一遍setUp
的流程。接下来进入Bundle.loadLaunchableBundles(listenner)
方法。
/**
* Load bundles from manifest
*/
protected static void loadLaunchableBundles(Small.OnCompleteListener listener) {
Context context = Small.getContext();
boolean synchronous = (listener == null);
if (synchronous) {
loadBundles(context);
return;
}
// Asynchronous
if (sThread == null) {
sThread = new LoadBundleThread(context);
sHandler = new LoadBundleHandler(listener);
sThread.start();
}
}
small在这里会根据之前setUp
中是否有Listener进行同步和异步去加载bundle的区分,没有意外情况一般都会使用异步,因为里面dexload涉及了IO操作会比较耗时。small启动了一个线程去执行相关的加载bundle操作,以及用Handler来回调通知Listener是否已经加载完毕。
Small加载Bundle
根据步骤,先来查看加载Bundle的线程:
private static class LoadBundleThread extends Thread {
Context mContext;
public LoadBundleThread(Context context) {
mContext = context;
}
@Override
public void run() {
loadBundles(mContext);
sHandler.obtainMessage(MSG_COMPLETE).sendToTarget();
}
}
private static class LoadBundleHandler extends Handler {
private Small.OnCompleteListener mListener;
public LoadBundleHandler(Small.OnCompleteListener listener) {
mListener = listener;
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_COMPLETE:
if (mListener != null) {
mListener.onComplete();
}
mListener = null;
sThread = null;
sHandler = null;
break;
}
}
}
在LoadBundleThread
中调用了loadBundles(context)
和调用LoadBundleHandler
发送加载完成的message,通知Listener所有的插件已经加载完毕。接下来看下loadBundles
方法:
private static void loadBundles(Context context) {
JSONObject manifestData;
try {
File patchManifestFile = getPatchManifestFile();// 获取bundle.json
String manifestJson = getCacheManifest();//从SharedPreferences里尝试获取bundle.json
if (manifestJson != null) {
// Load from cache and save as patch
if (!patchManifestFile.exists()) patchManifestFile.createNewFile();
PrintWriter pw = new PrintWriter(new FileOutputStream(patchManifestFile));
pw.print(manifestJson);
pw.flush();
pw.close();
// Clear cache
setCacheManifest(null);
} else if (patchManifestFile.exists()) {
// Load from patch
BufferedReader br = new BufferedReader(new FileReader(patchManifestFile));
StringBuilder sb = new StringBuilder();
String line;
while ((line = br.readLine()) != null) {
sb.append(line);
}
br.close();
manifestJson = sb.toString();
} else {
// Load from built-in `assets/bundle.json'
InputStream builtinManifestStream = context.getAssets().open(BUNDLE_MANIFEST_NAME);
int builtinSize = builtinManifestStream.available();
byte[] buffer = new byte[builtinSize];
builtinManifestStream.read(buffer);
builtinManifestStream.close();
manifestJson = new String(buffer, 0, builtinSize);
}
// 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
方法得到读取补丁的patch所在的目录下bundle.json
文件。然后这里有一系列的判断,优先读取缓存中的bundle.json
并且覆盖patch中的,其次优先从patch读取文件,最后才会从工程目录中的assets中读取,第一次安装应该都是从assets中读取。接着读取到bundle.json
文件转换成Json对象然后解析把所有的Bundle插件包加载到List中传给loadBundles(List<Bundle>)
方法。在这里加载Bundle之前会先执行setupLaunchers(Context)
方法,它会根据之前注册的BundleLauncher
顺序来执行各自的setUp
方法,最后执行加载所有在json文件中列出的Bundles。
private static void loadBundles(List<Bundle> bundles) {
sPreloadBundles = bundles;
// Prepare bundle
// 遍历所有插件执行prepareForLaunch阶段后面会介绍
for (Bundle bundle : bundles) {
bundle.prepareForLaunch();
}
// Handle I/O
if (sIOActions != null) {
//创建一个固定线程数量的线程池,用来执行插件中Dex的加载
ExecutorService executor = Executors.newFixedThreadPool(sIOActions.size());
for (Runnable action : sIOActions) {
executor.execute(action);
}
executor.shutdown();
try {
if (!executor.awaitTermination(LOADING_TIMEOUT_MINUTES, TimeUnit.MINUTES)) {
throw new RuntimeException("Failed to load bundles! (TIMEOUT > "
+ LOADING_TIMEOUT_MINUTES + "minutes)");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
sIOActions = null;
}
// Wait for the things to be done on UI thread before `postSetUp`,
// as on 7.0+ we should wait a WebView been initialized. (#347)
while (sRunningUIActionCount != 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// Notify `postSetUp' to all launchers
for (BundleLauncher launcher : sBundleLaunchers) {
launcher.postSetUp();
}
// Wait for the things to be done on UI thread after `postSetUp`,
// like creating a bundle application.
while (sRunningUIActionCount != 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 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;
}
}
protected void prepareForLaunch() {
if (mIntent != null) return;
if (mApplicableLauncher == null && sBundleLaunchers != null) {
for (BundleLauncher launcher : sBundleLaunchers) {
if (launcher.resolveBundle(this)) {
mApplicableLauncher = launcher;
break;
}
}
}
}
这里先看下他们的父类一个抽象类BundleLauncher
,作者已经在类的开头注释中介绍的比较详细了这里简单梳理下它的lifecycle
:
-
onCreate
:在初始化Application之前hook应用中的一些方法和变量,由于调用会在Application的onCreate之前所以里面的执行方法必须比较轻量,以免产出ANR -
setUp
:做一些静态的初始化工作比如Launcher的初始化,准备需要加载的bundle -
preloadBundle
:处理bundle的校验和相关准备工作,验证包名、签名并收集bundle的version以便以后upgrade -
loadBundle
:对通过校验的bundle开始真正开始加载 -
postSetUp
:在加载bundle之后对把所有插件之中的Dex、Resource和NativeLib的信息,通过反射保证Bundle的内容正常的加入到应用中。 -
prelaunchBundle
:当使用small.openUri
来启动插件中的Activity前解析它的class和query参数。 -
launchBundle
:在prelaunchBundle
通过后,真正的去启动Activity
接着我们就可以很轻松的理解它的三个子类:
ActivityLauncher.java
这个类主要用于启动宿主Host中的Activity,或者是定义在bundle.json中没有指定pkg
字段的以及pkg
自定为main
的bundle,当启动Activity的时候会取bundle的uri
作为ClassName。这个子类并没有onCreate
阶段直接从setUp
开始
@Override
public void setUp(Context context) {
super.setUp(context);
// Read the registered classes in host's manifest file
PackageInfo pi;
try {
pi = context.getPackageManager().getPackageInfo(
context.getPackageName(), PackageManager.GET_ACTIVITIES);
} catch (PackageManager.NameNotFoundException ignored) {
// Never reach
return;
}
ActivityInfo[] as = pi.activities;
if (as != null) {
sActivityClasses = new HashSet<String>();
for (ActivityInfo ai : as) {
sActivityClasses.add(ai.name);
}
}
}
这里主要做的就是读取我们宿主app的manifest文件中activity的name字段并存储到一个HashSet里面,为后面的判断activity作用。然后是preloadBundle
阶段
@Override
public boolean preloadBundle(Bundle bundle) {
if (sActivityClasses == null) return false;
String pkg = bundle.getPackageName();
return (pkg == null || pkg.equals("main"));
}
通过传入的bundle取出pkg
字段,如前面类功能描述一致如果字段为空或者是main
则返回true
保证之后的resolveBundle
可以执行load操作,插件中的Activity不会出现pkg
字段为空或者main
的情况所以这里一般都会返回false
,同时resolveBundle
也不会去执行load操作。当我们调用Small.openUri
来启动Activity的时候会执行launchBundle
其中的prelaunchBundle
方法
@Override
public void prelaunchBundle(Bundle bundle) {
super.prelaunchBundle(bundle);
Intent intent = new Intent();
bundle.setIntent(intent);
// Intent extras - class
String activityName = bundle.getActivityName();
if (!sActivityClasses.contains(activityName)) {
if (activityName.endsWith("Activity")) {
throw new ActivityNotFoundException("Unable to find explicit activity class " +
"{ " + activityName + " }");
}
String tempActivityName = activityName + "Activity";
if (!sActivityClasses.contains(tempActivityName)) {
throw new ActivityNotFoundException("Unable to find explicit activity class " +
"{ " + activityName + "(Activity) }");
}
activityName = tempActivityName;
}
intent.setComponent(new ComponentName(Small.getContext(), activityName));
// Intent extras - params
String query = bundle.getQuery();
if (query != null) {
intent.putExtra(Small.KEY_QUERY, '?'+query);
}
}
在这里我们能通过bundle.getActivityName
获取bundle的入口Activity类名(如果uri
为空默认取MainActivity
)然后就是从之前收集的Activity的HashSet中查询是否包含了这个类同时设置Component和query条件。
ApkBundleLauncher.java
Small的核心代码都在这个类中,这个是Small实现插件化相关Hook的关键类,包括劫持Instrumentation
借壳启动Activity,加载插件dex、资源、jniLibs等。
onCreate阶段
在ApkBundleLauncher
中实现了onCreate
方法
@Override
public void onCreate(Application app) {
super.onCreate(app);
Object/*ActivityThread*/ thread;
List<ProviderInfo> providers;
Instrumentation base;
ApkBundleLauncher.InstrumentationWrapper wrapper;
Field f;
// Get activity thread
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);
}
// Inject message handler
ensureInjectMessageHandler(thread);
// Get providers
try {
f = thread.getClass().getDeclaredField("mBoundApplication");
f.setAccessible(true);
Object/*AppBindData*/ data = f.get(thread);
f = data.getClass().getDeclaredField("providers");
f.setAccessible(true);
providers = (List<ProviderInfo>) f.get(data);
} catch (Exception e) {
throw new RuntimeException("Failed to get providers from thread: " + thread);
}
sActivityThread = thread;
sProviders = providers;
sHostInstrumentation = base;
sBundleInstrumentation = wrapper;
}
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);
boolean needsInject = false;
if (sActivityThreadHandlerCallback == null) {
needsInject = true;
} else {
Object callback = f.get(ah);
if (callback != sActivityThreadHandlerCallback) {
needsInject = 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);
}
}
通过从ActivityThread
中反射取到Instrumentation
替换成我们自己的InstrumentationWrapper
来控制Activity的各个生命周期以及启动。拦截系统ActivityThread
中的Handler
变量mH
,在ActivityThreadHandlerCallback
中通过handle指定的msg.what
来处理LAUNCH_ACTIVITY
和CREATE_SERVICE
。 同时也获取所有注册过的Providers
用于后面的异常捕获然后延时安装。
接着先来看下Small是如何通过预注册一批虚拟Activity来确保插件中的Activity能够有正常的生命周期,其中控制生命周期和启动的Instrumentation
会被InstrumentationWrapper
反射替换,由于代码量比较多所以拆分各个小段来分析:
InstrumentationWrapper
替换系统的Instrumentation控制Activity的启动和它的生命周期方法,拦截Exception
(1).用宿主中预注册的STUB Activity替换真正启动的Activity。
/** @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) {
wrapIntent(intent);
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) {
wrapIntent(intent);
ensureInjectMessageHandler(sActivityThread);
return ReflectAccelerator.execStartActivity(mBase,
who, contextThread, token, target, intent, requestCode);
}
这里有API版本区别21以上和20以下中的execStartActivity
方法参数多了一个。然后是wrapIntent()
方法。
private void wrapIntent(Intent intent) {
ComponentName component = intent.getComponent();
String realClazz;
if (component == null) {
// Try to resolve the implicit action which has registered in host.
component = intent.resolveActivity(Small.getContext().getPackageManager());
if (component != null) {
// A system or host action, nothing to be done.
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;
ActivityInfo ai = sLoadedActivities.get(realClazz);
if (ai == null) return;
// Carry the real(plugin) class for incoming `newActivity' method.
intent.addCategory(REDIRECT_FLAG + realClazz);
String stubClazz = dequeueStubActivity(ai, realClazz);
intent.setComponent(new ComponentName(Small.getContext(), stubClazz));
}
首先拿到真正需要启动的ActivityIntent
中的ComponentName
来获取到其ClassName,通过判断如果是隐式Intent
然后去宿主的Manifest中寻找对应注册的Action或者它是一个系统级别的Action,small不做处理直接返回。假如Intent
注册在插件中调用resolveActivity()
方法,遍历插件中所有所有的IntentFilter
的一个集合,取到与之匹配自定义Action的ClassName并返回。如果是显式Intent
判断是否启动的是真正的Activity通过unwrapIntent()
方法确保启动的能够真正替换Activity成功。
然后通过ClassName找到对应的ActivityInfo
,这里small会把ClassName存在Intent
的category
中为了后面可以方便取出做一些逻辑判断包括前面提到的unwrapIntent()
方法会用到这个。
接着通过dequeueStubActivity()
方法获取一个可用的STUB Activity作为真正需要启动Activity的载体来绕过系统注册机制,并且把stub的ClassName设置给component,small会用一套预注册的STUB Activitys来保证所有的启动模式都被覆盖到,并且除了standard
模式每组都有4个Activity来复用。
<!-- Small中的AndroidManifest -->
<application>
<!-- Stub Activities -->
<!-- 1 standard mode -->
<activity android:name=".A" android:launchMode="standard"/>
<activity android:name=".A1" android:theme="@android:style/Theme.Translucent"/>
<!-- 4 singleTop mode -->
<activity android:name=".A10" android:launchMode="singleTop"/>
<activity android:name=".A11" android:launchMode="singleTop"/>
<activity android:name=".A12" android:launchMode="singleTop"/>
<activity android:name=".A13" android:launchMode="singleTop"/>
<!-- 4 singleTask mode -->
<activity android:name=".A20" android:launchMode="singleTask"/>
<activity android:name=".A21" android:launchMode="singleTask"/>
<activity android:name=".A22" android:launchMode="singleTask"/>
<activity android:name=".A23" android:launchMode="singleTask"/>
<!-- 4 singleInstance mode -->
<activity android:name=".A30" android:launchMode="singleInstance"/>
<activity android:name=".A31" android:launchMode="singleInstance"/>
<activity android:name=".A32" android:launchMode="singleInstance"/>
<activity android:name=".A33" android:launchMode="singleInstance"/>
<!-- Web Activity -->
<activity android:name=".webkit.WebActivity"
android:screenOrientation="portrait"
android:windowSoftInputMode="stateHidden|adjustPan"
android:hardwareAccelerated="true"/>
</application>
(2).在所有Activity的onCreate、onStop、onDestory生命周期都进行了hook。
先来看callActivityOnCreate
:
@Override
/** Prepare resources for REAL */
public void callActivityOnCreate(Activity activity, android.os.Bundle icicle) {
do {
if (sLoadedActivities == null) break;
ActivityInfo ai = sLoadedActivities.get(activity.getClass().getName());
if (ai == null) break;
applyActivityInfo(activity, ai);
ReflectAccelerator.ensureCacheResources();
} while (false);
sHostInstrumentation.callActivityOnCreate(activity, icicle);
// Reset activity instrumentation if it was modified by some other applications #245
if (sBundleInstrumentation != null) {
try {
Field f = Activity.class.getDeclaredField("mInstrumentation");
f.setAccessible(true);
Object instrumentation = f.get(activity);
if (instrumentation != sBundleInstrumentation) {
f.set(activity, sBundleInstrumentation);
}
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
small会在这里会把插件中启动的Activity相关ActivityInfo中的softInputMode
和screenOrientation
的通过代码进行设置,在API24及以上系统有时会把保存Resource的WeakReference释放导致资源不可用,所以还需要把Resource的缓存再次存入WeakReference。最后确保hook的instrumentation
没有在这个阶段被其他应用修改,再一次进行反射赋值。
再看callActivityOnStop
:
@Override
public void callActivityOnStop(Activity activity) {
sHostInstrumentation.callActivityOnStop(activity);
if (!Small.isUpgrading()) return;
// If is upgrading, we are going to kill self while application turn into background,
// and while we are back to foreground, all the things(code & layout) will be reload.
// Don't worry about the data missing in current activity, you can do all the backups
// with your activity's `onSaveInstanceState' and `onRestoreInstanceState'.
// Get all the processes of device (1)
ActivityManager am = (ActivityManager) activity.getSystemService(Context.ACTIVITY_SERVICE);
List<RunningAppProcessInfo> processes = am.getRunningAppProcesses();
if (processes == null) return;
// Gather all the processes of current application (2)
// Above 5.1.1, this may be equals to (1), on the safe side, we also
// filter the processes with current package name.
String pkg = activity.getApplicationContext().getPackageName();
final List<RunningAppProcessInfo> currentAppProcesses = new ArrayList<>(processes.size());
for (RunningAppProcessInfo p : processes) {
if (p.pkgList == null) continue;
boolean match = false;
int N = p.pkgList.length;
for (int i = 0; i < N; i++) {
if (p.pkgList[i].equals(pkg)) {
match = true;
break;
}
}
if (!match) continue;
currentAppProcesses.add(p);
}
if (currentAppProcesses.isEmpty()) return;
// The top process of current application processes.
RunningAppProcessInfo currentProcess = currentAppProcesses.get(0);
if (currentProcess.importance == RunningAppProcessInfo.IMPORTANCE_FOREGROUND) return;
// Seems should delay some time to ensure the activity can be successfully
// restarted after the application restart.
// FIXME: remove following thread if you find the better place to `killProcess'
new Thread() {
@Override
public void run() {
try {
sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (RunningAppProcessInfo p : currentAppProcesses) {
android.os.Process.killProcess(p.pid);
}
}
}.start();
}
作者已经给了详细的注释,主要为插件升级做一些相关处理,在Activity的onStop阶段并且应用退到了后台,当发现有插件升级时杀死自己的进程,保证用户再次进入应用时可以走setUp
逻辑保证更新的bundle能够被正确加载。最后callActivityOnDestroy
:
@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);
}
在这里如果插件启动的Activity不是LAUNCH_MULTIPLE
那么small会通过调用inqueueStubActivity
获取当前被使用的StubActivity
进行释放以便后面的复用。
(3).最后对异常的捕获也做了处理onException
:
@Override
public boolean onException(Object obj, Throwable e) {
if (sProviders != null && e.getClass().equals(ClassNotFoundException.class)) {
boolean errorOnInstallProvider = false;
StackTraceElement[] stacks = e.getStackTrace();
for (StackTraceElement st : stacks) {
if (st.getMethodName().equals("installProvider")) {
//拦截到installProvider方法中抛出的ClassNotFoundException
errorOnInstallProvider = true;
break;
}
}
if (errorOnInstallProvider) {
// We'll reinstall this content provider later, so just ignores it!!!
// FIXME: any better way to get the class name?
String msg = e.getMessage();
final String prefix = "Didn't find class \"";
if (msg.startsWith(prefix)) {
String providerClazz = msg.substring(prefix.length());
//获取到异常中的className
providerClazz = providerClazz.substring(0, providerClazz.indexOf("\""));
for (ProviderInfo info : sProviders) {
//对比之前收集Manifest中的providers
if (info.name.equals(providerClazz)) {
if (mLazyInitProviders == null) {
mLazyInitProviders = new ArrayList<ProviderInfo>();
}
//把匹配的provider加入到队列,等待后面延时去安装
mLazyInitProviders.add(info);
break;
}
}
}
return true;
}
}
return super.onException(obj, e);
}
由于Small
需要把插件包中的所有provider
预先注册在宿主的AndroidManifest
中,但是系统会在Application
的onCreate
生命周期执行之前根据注册的provider
信息进行install
,这样会导致类找不到而抛出异常,所以这里通过onException
拦截当前的ClassNotFoundException
并且判断如果是从方法installProvider
中抛出的话我们进行捕获处理不抛出异常,把异常中的provider
也就是在宿主中定义的其他插件包中的provider
的类名存进数组,在后续加载完毕插件的dex和处理完classloader之后再进行install
操作。
ActivityThreadHandlerCallback
通过反射在ActivityTread
中拿到主线程的Handler
的mH
,对Activity
、Service
、BroadcastReceiver
和Provider
的相关启动接受做一些处理。
private static class ActivityThreadHandlerCallback implements Handler.Callback {
private static final int LAUNCH_ACTIVITY = 100;//对应ActivityThread源码中的msg.what
private static final int CREATE_SERVICE = 114;//创建service的msg.what
@Override
public boolean handleMessage(Message msg) {
switch (msg.what) {
case LAUNCH_ACTIVITY:
redirectActivity(msg);
break;
case CREATE_SERVICE:
ensureServiceClassesLoadable(msg);
break;
default:
break;
}
return false;
}
/** 通过这个方法我们替换之前的stubActivity变成realActivity**/
private void redirectActivity(Message msg) {
Object/*ActivityClientRecord*/ r = msg.obj;
Intent intent = ReflectAccelerator.getIntent(r);
/** unwrapIntent方法会取到之前在InstrumentationWrapper中wrapperIntent时
在intent.category存入的realActivity_class **/
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);
ReflectAccelerator.setActivityInfo(r, targetInfo);
}
private void ensureServiceClassesLoadable(Message msg) {
// 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();
}
}
Small拦截了启动Activity和创建Service的Handler回调,通过redirectActivity
把StubActivity替换成我们真正需要启动的Activity,ensureServiceClassesLoadable
则保证在其他进程中的Service能够正确启动。
SetUp阶段
接下来是setUp
阶段:
@Override
public void setUp(Context context) {
super.setUp(context);
Field f;
// 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];
//拿到所有的Intent,进行stubActivity替换
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();
}
}
通过反射和动态代理处理所有通过TaskStackBuilder创建的pendingIntent,进行warpIntent
用StubActivity替换RealActivity。如果没有使用TaskStackBuilder创建PendingIntent,那么需要对Intent执行Small.wrapIntent(Intent)
。
preloadBundle阶段
preloadBundle
是通过父类SoBundleLauncher
实现的
@Override
public boolean preloadBundle(Bundle bundle) {
String packageName = bundle.getPackageName();
if (packageName == null) return false;
// 1.Check if supporting
String[] types = getSupportingTypes();
if (types == null) return false;
boolean supporting = false;
String bundleType = bundle.getType();
if (bundleType != null) {
// Consider user-defined type in `bundle.json'
for (String type : types) {
if (type.equals(bundleType)) {
supporting = true;
break;
}
}
} else {
// Consider explicit type specify in package name as following:
// - com.example.[type].any
// - com.example.[type]any
String[] pkgs = packageName.split("\\.");
int N = pkgs.length;
String aloneType = N > 1 ? pkgs[N - 2] : null;
String lastComponent = pkgs[N - 1];
for (String type : types) {
if ((aloneType != null && aloneType.equals(type))
|| lastComponent.startsWith(type)) {
supporting = true;
break;
}
}
}
if (!supporting) return false;
// 2.Initialize the extract path
File extractPath = getExtractPath(bundle);
if (extractPath != null) {
if (!extractPath.exists()) {
extractPath.mkdirs();
}
bundle.setExtractPath(extractPath);
}
// 3.Select the bundle entry-point, `built-in' or `patch'
File plugin = bundle.getBuiltinFile();
BundleParser parser = BundleParser.parsePackage(plugin, packageName);
File patch = bundle.getPatchFile();
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);
// 4.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
if (!parser.verifyAndExtract(bundle, this)) {
bundle.setEnabled(false);
return true; // Got it, but disabled
}
Small.setBundleLastModified(packageName, lastModified);
}
// 5.Record version code for upgrade
PackageInfo pluginInfo = parser.getPackageInfo();
bundle.setVersionCode(pluginInfo.versionCode);
bundle.setVersionName(pluginInfo.versionName);
return true;
}
跟着作者的注释看可以比较清晰的了解这里主要工作就是:
- 检测Bundle的类型,默认是
app
和lib
类型 - 初始化NativeLib的解压目录
- 查看Bundle是从本地插件包中加载还是从服务端下发的path Bundle中加载
- 检测Bundle是否被覆盖修改过,如果被修改则对内容进行校验和解压
- 收集Bundle的version便于以后插件包单独更新
loadBundle阶段
进过前面的一些列初始化和校验,终于开始加载Bundle插件包
@Override
public void loadBundle(Bundle bundle) {
String packageName = bundle.getPackageName();
BundleParser parser = bundle.getParser();
//收集Bundle中Manifest所有注册的Activitys
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) {
//实例化我们的LoadApk 里面存放一些插件的信息
apk = new LoadedApk();
apk.packageName = packageName;
apk.path = apkPath;
apk.nonResources = parser.isNonResources();
if (pluginInfo.applicationInfo != null) {
apk.applicationName = pluginInfo.applicationInfo.className;
}
apk.packagePath = bundle.getExtractPath();
apk.optDexFile = new File(apk.packagePath, FILE_DEX);
// Load dex
final LoadedApk fApk = apk;
//创建一个线程来进行loadDex,这个里涉及到dexOpt所以会比较耗时
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
// 设置NativeLibrary存放路径
String libDir = parser.getLibraryDirectory();
if (libDir != null) {
apk.libraryPath = new File(apk.packagePath, libDir);
}
sLoadedApks.put(packageName, apk);
}
if (pluginInfo.activities == null) {
bundle.setLaunchable(false);
return;
}
// Record activities for intent redirection
// 收集Bundle中的Activity为了之后能正确的替换StubActivity
if (sLoadedActivities == null) sLoadedActivities = new ConcurrentHashMap<String, ActivityInfo>();
for (ActivityInfo ai : pluginInfo.activities) {
sLoadedActivities.put(ai.name, ai);
}
// Record intent-filters for implicit action
// 收集隐式启动的intent-filter
ConcurrentHashMap<String, List<IntentFilter>> filters = parser.getIntentFilters();
if (filters != null) {
if (sLoadedIntentFilters == null) {
sLoadedIntentFilters = new ConcurrentHashMap<String, List<IntentFilter>>();
}
sLoadedIntentFilters.putAll(filters);
}
// Set entrance activity
bundle.setEntrance(parser.getDefaultActivityName());
}
根据Bundle配置的信息进行collectActivities
也就是读取你插件中manifest中的activity注册信息(包括packagename、name、theme、label、icon、launchermode、screenorientation、windowsoftinputmode以及hardwareaccelerate),还有intent-filter的信息。然后设置入口activity也就是LauncherActivity。这里最重要的就是LoadDex
,Bundle.postIO
中把所有Bundle的LoadDex操作都放到了一个Runnable
队列sIOActions
中,这些任务action会在一个线程池中并行执行。然后收集Bundle的Activity、intent-filter,设置插件默认启动Activity。
postSetUp阶段
当Bundle加载完毕之后会执行postSetUp
方法,把所有Bundle中的Dex、Resource和NativeLib通过反射Merge到宿主中。
@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
// 合并所有Bundle中的Resource
final Application app = Small.getContext();
String[] paths = new String[apks.size() + 1];
paths[0] = app.getPackageResourcePath(); // add host asset path
int i = 1;
for (LoadedApk apk : apks) {
if (apk.nonResources) continue; // ignores the empty entry to fix #62
paths[i++] = apk.path; // add plugin asset path
}
if (i != paths.length) {
paths = Arrays.copyOf(paths, i);
}
ReflectAccelerator.mergeResources(app, sActivityThread, paths);
// Merge all the dex into host's class loader
// 合并所有Bundle的Dex到宿主的ClassLoader中保证类都能被找到
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)) {
// 如果是更新插件,删除optDex文件,保证重新生成
if (apk.optDexFile.exists()) apk.optDexFile.delete();
Small.setBundleUpgraded(apk.packageName, false);
}
i++;
}
ReflectAccelerator.expandDexPathList(cl, dexPaths, dexFiles);
// Expand the native library directories for host class loader if plugin has any JNIs. (#79)
// 合并所有Bundle的NativeLibrary
List<File> libPathList = new ArrayList<File>();
for (LoadedApk apk : apks) {
if (apk.libraryPath != null) {
libPathList.add(apk.libraryPath);
}
}
if (libPathList.size() > 0) {
ReflectAccelerator.expandNativeLibraryDirectories(cl, libPathList);
}
// Trigger all the bundle application `onCreate' event
// 触发所有app.*中所有的Application onCreate方法
for (final LoadedApk apk : apks) {
String bundleApplicationName = apk.applicationName;
if (bundleApplicationName == null) continue;
try {
final Class applicationClass = Class.forName(bundleApplicationName);
//在线程池中新建一个线程来执行application oncreate方法
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
// 因为所有插件的Dex已经加载完毕,延时安装之前在异常中捕获的所有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;
sActivityThread = null;
}
这里会把之前所有的bundle
的Resource、Dex以及可能存在的NativeLibrary都merge到宿主中。然后通过调用Instrumentation.callApplicationOnCreate
触发所有bundle
的application
的oncreate
方法,之前收集的Provider
也会在这里进行延时安装。
AssetBundleLauncher
这个BundleLauncher加载在bundle.json
中非app
或lib
标记的插件包类型,可以是一个只负责加载assets
的插件包,它的子类WebBundleLauncher
就是负责加载Small中的WebActivity
的。执行过程只有prelaunchBundle
、loadBundle
和launchBundle
阶段,里面没有反射和Hook逻辑比较简单代码就不贴了。
Small更新插件
Sample中有详细的checkUpgrade
方法里面代码比较简单详细,这里就不展开了,需要结合项目的就是下载下来插件的安全性校验了防止被恶意替换。
总结
优点: 轻量级框架;作者和使用者在持续维护中,整体代码非常简洁易读,结构也很清晰;对原有项目(已经组件化的话)侵入非常少只需要使用它的规范命名项目名称再配置一下bundle.json文件即可。
缺点: 作为一个比较新的开源项目,坑肯定是不少的,需要在实际项目中去踩坑;没有插件延时加载的功能,需要在第一次安装时加载全部的插件包(虽然做了并行加载,如果想要再加速启动速度需要自己去实现延时加载);service无法动态加载需要自己实现;在一些插件之间资源依赖上有一定的限制(下面有提到)
Small的插件化的建议
贴下作者在issue中写的,对于像使用small进行插件化的同学请仔细阅读
基本原则
宿主中不要放业务逻辑。只做加载插件以及调起主插件的操作。
重构步骤
- 拆lib.* - 公共模块插件
把各个 第三方库 拆出来做成一个个lib.*插件模块,包括统计、地图、网络、图片等库。
把老项目积累的 业务公共代码 (utils)分离出来封装成一个lib.utils插件
把基础的样式、主题分离出来封装成一个lib.style插件
- 拆app.* - 业务模块插件
把业务模块拆成app.模块,他们可以依赖lib.模块,显示调用lib.*中的各个API
相对独立的业务模块先拆,比如“详情页”、“关于我们”,如果剩下的业务不好拆,先放一个插件里
如果都不好拆,先把全部业务做成一个app.main主插件
- 下沉宿主 - 宿主分身模块
宿主分身模块要求以app+*
格式命名,他们将被宿主
、lib.*
、app.*
模块自动依赖并允许这些模块透明地访问自己的代码或者资源。需要注意的是,分身模块最终是并入到宿主的而非插件,建议使用分身的情形有:
- 必须在宿主占坑的manifest,包括:
- 受限 Activity:
- 包含了暂不支持的属性:process, configChanges 等
- 可能使用 FLAG_ACTIVITY_CLEAR_TOP 标签来启动 (#415)
- 任何 Provider, Service, BroadcastReceiver
- 受限 Activity:
- 必须在宿主占坑的资源,包括:
- 转场动画
- 通知栏图标、自定义视图
- 桌面快捷方式图标
- 确信稳定的公共库与资源
- Sample示例
lib.style - 公共主题
lib.utils - 公共API、公共控件
lib.analytics - 第三方统计
app.main - 主插件
app.home - 首页
app.detail - 详情页
app+stub - 占坑组件、资源
更多技术文章同步在本人Blog,刚开始整理到这个Blog后续会更新更多内容。同时欢迎加入Small的技术讨论群(374601844)和加入开发
关于转载
欢迎转载此文,但是请务必在转载时加上原文作者&原文链接。谢谢