30分钟了解轻量级插件化方案--Small

插件化

Posted by Mio4kon on 2017-12-26

介绍

现有的插件化方案有很多,Small以轻量级著称,先看下官方的对比图:

更多对比:
https://github.com/wequick/Small/blob/master/Android/COMPARISION.md

插件化? 模块化?

入门

Demo

  • 新建一个宿主工程.
  • project-build中加入依赖:
1
2
3
4
5
6
7
8
9
dependencies {
classpath 'com.android.tools.build:gradle:2.2.3'
classpath 'net.wequick.tools.build:gradle-small:1.1.0-alpha2'
}
apply plugin: 'net.wequick.small'
small {
aarVersion = '1.1.0-alpha2'
}
  • 宿主工程Application调用Small.preSetUp(this)方法.
  • 编译公共库(宿主工程)./gradlew buildLib -q
  • 新建一个module(插件)工程,名称为App.xxx
  • 编译插件 ./gradlew buildBundle -q -Dbundle.arch=armeabi
  • 在宿主工程中assets资源文件中添加bundle.json路由配置文件:
1
2
3
4
5
6
7
8
9
{
"version": "1.0.0",
"bundles": [
{
"uri": "main",
"pkg": "com.mio.appmain"
}
]
}
  • 调用Small.setup打开插件工程:
1
2
3
4
5
6
Small.setUp(this, new Small.OnCompleteListener() {
@Override
public void onComplete() {
Small.openUri("main", MainActivity.this);
}
});

公共库插件模块

定义(二选一)

  1. 指定 Module name 为 lib.*
  2. Small DSL 中显式指明 bundles lib your_module_name

读取(二选一)

  1. 指定 Package name**.lib.***.lib*
  2. bundle.json中添加描述 "type": "lib"

公共库插件模块: 例如将theme,color,style抽取成 lib.style 库.

应用插件模块

定义(二选一)

  1. 指定 Module name 为 app.*
  2. Small DSL 中显式指明 bundles app your_module_name

读取(二选一)

  1. 指定 Package name**.app.***.app*
  2. bundle.json中添加描述 "type": "app"

应用插件模块: 就是各种的业务模块.

宿主分身模块

会编译到宿主中的模块.

需要使用宿主分身模块的场景:

  • 模块中需要使用宿主中资源和代码.
  • 必须在宿主Manifest注册的受限组件.

定义一个名为: app+*lib 库.

传递和接受参数

  • 传递

Small.openUri("detail?from=app.home", getContext());

  • 接受
1
2
3
4
Uri uri = Small.getUri(this);
if (uri != null) {
String from = uri.getQueryParameter("from");
}

原理分析

这里只是简单的带着疑问去分析源码,围绕着:

  • AndroidManifest.xml
  • bundle.json
  • libcom_mio_appmain.so

我们可以来个三连问.

如何启动插件中的Activity

Application的初始化方法preSetUp入手:

1
2
3
4
5
6
7
8
9
10
11
12
13
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);
}

这里Bundle.onCreateLaunchers会调用 LauncheronCreate生命周期.而唯一实现的只有ApkBundleLauncher.

查看 ApkBundleLauncheronCreate :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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
try {
f = thread.getClass().getDeclaredField("mH");
f.setAccessible(true);
Handler ah = (Handler) f.get(thread);
f = Handler.class.getDeclaredField("mCallback");
f.setAccessible(true);
f.set(ah, new ApkBundleLauncher.ActivityThreadHandlerCallback());
} catch (Exception e) {
throw new RuntimeException("Failed to replace message handler for thread: " + thread);
}

可以发现这里与其他部分插件一样都是通过欺骗AWS,来启动未注册的Activity.

不过这里用的是通过反射直接替换掉原来的Instrumentation对象.而不是通过反射替换掉ActivityManagerNative中的gDefault对象,并用动态代理hook的那种方式.

哪里加载bundle.json

我们知道在第一次使用的的时候需要调用Small.setUp的方法.我看看一下这个方法做了什么.

[Small.java]

1
2
3
4
5
6
public static void setUp(Context context, OnCompleteListener listener) {
..
Bundle.loadLaunchableBundles(listener);
sHasSetUp = true;
}

调用Bundle.loadLaunchableBundles

[Bundle.java]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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();
}
}

很简单,如果有回调的话,就异步开启线程,最终也会调用loadBundles方法.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
private static void loadBundles(Context context) {
JSONObject manifestData;
try {
File patchManifestFile = getPatchManifestFile();
String manifestJson = getCacheManifest();
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其实就是我们配置的bundle.json路由文件.总之通过一些判断,并通过解析得到了一个
Manifest对象.

1
2
3
4
private static final class Manifest {
String version;
List<Bundle> bundles;
}

下面会执行比较关键的两个方法,setupLaunchersloadBundles. 这个问题暂时并不关心.

哪里加载了插件so文件

我们先看看上面的setupLaunchers方法.

1
2
3
4
5
6
7
protected static void setupLaunchers(Context context) {
if (sBundleLaunchers == null) return;
for (BundleLauncher launcher : sBundleLaunchers) {
launcher.setUp(context);
}
}

唔..调用之前那几个launchersetUp方法.我大致的看了下三个launchersetUp方法.发现并没有我想要的东西.

其实听名字也知道,应该在loadBundles中才会加载bundle对应的 so 文件.

[Bundle.java]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
private static void loadBundles(List<Bundle> bundles) {
sPreloadBundles = bundles;
// Prepare bundle
for (Bundle bundle : bundles) {
bundle.prepareForLaunch();
}
// Handle I/O
if (sIOActions != null) {
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;
}
}

先看 Prepare bundle做了什么.

1
2
3
4
5
6
7
8
9
10
11
12
protected void prepareForLaunch() {
if (mIntent != null) return;
if (mApplicableLauncher == null && sBundleLaunchers != null) {
for (BundleLauncher launcher : sBundleLaunchers) {
if (launcher.resolveBundle(this)) {
mApplicableLauncher = launcher;
break;
}
}
}
}

这个bundle对象是之前在构造Manifest的时候根据json文件新生成的.所有上面的逻辑会走到下面的for循环.这里它会通过launcher.resolveBundle匹配一个合适的Launcher,其方法实现是共同的BundleLauncher.java基类实现的.

[BundleLauncher.java]

1
2
3
4
5
6
public boolean resolveBundle(Bundle bundle) {
if (!preloadBundle(bundle)) return false;
loadBundle(bundle);
return true;
}

但是preloadBundle实现却不同,有兴趣的可以看看三个Launcher对这个方法的具体实现. 这里当LauncherApkBundleLauncher的时候才会走到loadBundle这个方法.

[ApkBundleLauncher.java]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
@Override
public void loadBundle(Bundle bundle) {
String packageName = bundle.getPackageName();
BundleParser parser = bundle.getParser();
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.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;
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) {
bundle.setLaunchable(false);
return;
}
// Record activities for intent redirection
if (sLoadedActivities == null) sLoadedActivities = new ConcurrentHashMap<String, ActivityInfo>();
for (ActivityInfo ai : pluginInfo.activities) {
sLoadedActivities.put(ai.name, ai);
}
// Record intent-filters for implicit action
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());
}

DexFile.loadDex(fApk.path, fApk.optDexFile.getPath(), 0)这句话便是加载我们的插件so文件.

哪里合并了插件资源并加载dex

根据之前的分析,发现对插件的主要操作launcher都在ApkBundleLauncher中.
回到之前的loadBundles的方法中,bundle.prepareForLaunch()之后,会调用如下方法:

1
2
3
4
// Notify `postSetUp' to all launchers
for (BundleLauncher launcher : sBundleLaunchers) {
launcher.postSetUp();
}

我们看下ApkBundleLauncherpostSetUp方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
@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
final Application app = Small.getContext();
String[] paths = new String[apks.size() + 1];
//添加宿主的asset path
paths[0] = app.getPackageResourcePath();
int i = 1;
for (LoadedApk apk : apks) {
if (apk.nonResources) continue; // ignores the empty entry to fix #62
//添加插件的asset path
paths[i++] = apk.path;
}
if (i != paths.length) {
paths = Arrays.copyOf(paths, i);
}
//合并资源
ReflectAccelerator.mergeResources(app, sActivityThread, paths);
// 合并插件的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)) {
// If upgraded, delete the opt dex file for recreating
if (apk.optDexFile.exists()) apk.optDexFile.delete();
Small.setBundleUpgraded(apk.packageName, false);
}
i++;
}
//扩充 path list
ReflectAccelerator.expandDexPathList(cl, dexPaths, dexFiles);
// JNI相关的,扩充 Library Directories
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);
}
// 调用所有插件的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 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);
}
}
// 释放资源
sLoadedApks = null;
sProviders = null;
sActivityThread = null;
}

可以发现这个方法做的东西还挺多的.完全解答了这个问题的疑问.

postSetUp方法主要做的以下几件事:

  1. 将宿主,插件的asset path合并.
  2. 将宿主,插件的dex文件合并到宿主classLoader中.
  3. 将宿主,插件的jnilib合并.
  4. 调用所有插件ApplicationonCreate方法.
  5. 初始化 lazy init providers.

至此Small从入门到深入这篇文章就结束了,现在插件化,模块化的技术方案已经很成熟了,而Small的确是一个比较轻量级的框架,不足的是到我写这篇博客为止,该框架还不支持gradle 3.0.