Shadow插件化框架原理

sample项目调试

1
2
3
4
5
6
7
8
9
10
11
12
13
sample-host-debug.apk -> projects/sample/source/sample-host
├── sample-constant -> projects/sample/source/sample-constant
└── sample-host-lib -> projects/sample/source/sample-host-lib

pluginmanager.apk -> projects/sample/source/sample-manager
└── sample-constant -> projects/sample/source/sample-constant

sample-runtime-debug.apk -> projects/sample/source/sample-plugin/sample-runtime
└── activity-container -> projects/sdk/core/activity-container (maven)

sample-loader-debug.apk -> projects/sample/source/sample-plugin/sample-loader
├── sample-host-lib -> projects/sample/source/sample-host-lib (compileOnly)
└── sample-constant -> projects/sample/source/sample-constant

下载Shadow工程后,在根目录执行./gradlew build等待编译成功,然后通过adb安装:

1
adb install projects/sample/source/sample-host/build/outputs/apk/debug/sample-host-debug.apk

在sample-host-debug.apk的assets目录下包含pluginmanager.apkplugin-debug.zip,其中plugin-debug.zip包含的文件:

1
2
3
4
5
6
7
.
├── config.json
├── sample-app-plugin-debug.apk
├── sample-app-plugin-debug2.apk
├── sample-base-plugin-debug.apk
├── sample-loader-debug.apk
└── sample-runtime-debug.apk

config.json文件内容如下:

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
{
"compact_version": [
1,
2,
3
],
"pluginLoader": {
"apkName": "sample-loader-debug.apk",
"hash": "476180733ADFE69DDBE88CADAD473A4B"
},
"plugins": [
{
"partKey": "sample-plugin-app",
"apkName": "sample-app-plugin-debug.apk",
"dependsOn": [
"sample-base"
],
"businessName": "sample-plugin-app",
"hostWhiteList": [
"com.tencent.shadow.sample.host.lib"
],
"hash": "ECF851E261BF1B8D165D9AEB7C4C3699"
},
{
"partKey": "sample-plugin-app2",
"apkName": "sample-app-plugin-debug2.apk",
"dependsOn": [
"sample-base"
],
"businessName": "sample-plugin-app2",
"hostWhiteList": [
"com.tencent.shadow.sample.host.lib"
],
"hash": "ECF851E261BF1B8D165D9AEB7C4C3699"
},
{
"partKey": "sample-base",
"apkName": "sample-base-plugin-debug.apk",
"businessName": "sample-plugin-app",
"hostWhiteList": [
"com.tencent.shadow.sample.host.lib"
],
"hash": "18CA96C8EA499DC657ED6DE44B050728"
}
],
"runtime": {
"apkName": "sample-runtime-debug.apk",
"hash": "364BAB0EB1B8CD345D15D767D1FA8DC7"
},
"UUID": "F5574A0D-D937-42C0-93A6-35E190CCA955",
"version": 4,
"UUID_NickName": "1.1.5"
}

插件启动过程

在sample代码中启动插件Activity会通过一个跳板PluginLoadActivity,跳板Activity会展示插件loading view,从而避免插件启动过程中的黑屏给用户带来困惑。


zenuml
PluginLoadActivity."startPlugin run" {
    mPluginManager = HostApplication.loadPluginManager {
        PluginManager = Shadow.getPluginManager {
            new DynamicPluginManager()
        }
    }
    DynamicPluginManager.enter {
        updateManagerImpl {
            //从pluginmanager.apk中加载manager
            new SamplePluginManager()
        }
        SamplePluginManager.enter {
            onStartActivity {
                "展示pluginmanager.apk的loading" {
                    "👇① 子线程run"
                }
            }
        }
    }
}

zenuml
SamplePluginManager."☝① onStartActivity run" {
    FastPluginManager.installPlugin
    FastPluginManager.loadPlugin(sample-base) {
        loadPluginLoaderAndRuntime {
            bindPluginProcessService {
                "独立进程启动PluginProcessService"
            }
            loadRunTime {
                PpsController."👇① loadRuntime"
            }
            loadPluginLoader {
                PpsController."👇② loadPluginLoader"
            }
        }
        BinderPluginLoader."👇③ loadPlugin"
    }
    FastPluginManager.loadPlugin(sample-plugin-app)
    FastPluginManager.callApplicationOnCreate(sample-base) {
        BinderPluginLoader.callApplicationOnCreate
    }
    FastPluginManager.callApplicationOnCreate(sample-plugin-app)
    BinderPluginLoader.convertActivityIntent
    BinderPluginLoader.startActivityInPluginProcess
}

加载Runtime


zenuml
PluginProcessService."☝① (binder)loadRuntime" {
    InstalledApk = BinderUuidManager.getRuntime
    DynamicRuntime.loadRuntime {
        "runtime挂在ClassLoader parent"
    }
}

PluginProcessService类位于宿主apk中,但是是在独立进程中启动服务。
runtime加载后ClassLoader的结构是:BootClassLoader <- RuntimeClassLoader <- PathClassLoader

1
2
3
dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/~~ENGi6EgoGsbDZ8Ntp66PEg==/com.tencent.shadow.sample.host-tXgX3vZIGlZxM6yNcDzx3A==/base.apk"],nativeLibraryDirectories=[/data/app/~~ENGi6EgoGsbDZ8Ntp66PEg==/com.tencent.shadow.sample.host-tXgX3vZIGlZxM6yNcDzx3A==/lib/x86_64, /system/lib64, /system_ext/lib64]]]
RuntimeClassLoader[DexPathList[[zip file "/data/user/0/com.tencent.shadow.sample.host/files/ShadowPluginManager/UnpackedPlugin/test-dynamic-manager/696fd64a75befe2ff6a0c9e9f2e317e4/plugin-debug.zip/sample-runtime-debug.apk"],nativeLibraryDirectories=[/system/lib64, /system_ext/lib64]]]
java.lang.BootClassLoader@d43cbf0

加载Loader

loader和runtime一样都是在PluginProcessService中进行加载。


zenuml
PluginProcessService."☝② (binder)loadPluginLoader" {
    PluginProcessService."InstalledApk = BinderUuidManager.getPluginLoader"
    PluginLoaderImpl = LoaderImplLoader.load {
        "ApkClassLoader.loadClass(LoaderFactoryImpl)"
        LoaderFactoryImpl.buildLoader {
            new DynamicPluginLoader
            new PluginLoaderBinder(DynamicPluginLoader)
        }
    }
    PluginLoaderBinder.setUuidManager
}

加载插件


zenuml
DynamicPluginLoader."☝③ loadPlugin(binder)" {
    SamplePluginLoader.loadPlugin {
        "ShadowPluginLoader#loadPlugin" {
            "new PluginServiceManager"
            LoadPluginBloc.loadPlugin {
                PluginClassLoader = LoadApkBloc.loadPlugin
                "PluginManifest = PluginClassLoader.loadPluginManifest"
                "ApplicationInfo = CreatePluginApplicationInfoBloc.create"
                "new PluginPackageManagerImpl"
                "Resources = CreateResourceBloc.create"
                "new ShadowAppComponentFactory"
                "ShadowApplication = CreateApplicationBloc.createShadowApplication"
                ComponentManager.addPluginApkInfo
                "PluginParts注册到map"
                PluginPartInfoManager.addPluginInfo
            }
        }
    }
}

每个插件包中都包含一个com.tencent.shadow.core.manifest_parser.PluginManifest类,用户记录当前插件包中有那些activity、service、receiver、provider,加载插件后这些信息会被注册到ComponentManager

1
2
3
4
5
6
7
8
9
static {
activities = new PluginManifest.ActivityInfo[] {
new PluginManifest.ActivityInfo("com.tencent.shadow.sample.plugin.app.lib.usecases.activity.TestActivityReCreate", 0, 0, 0, -1), new PluginManifest.ActivityInfo("com.tencent.shadow.sample.plugin.app.lib.usecases.activity.TestActivitySetTheme", 0, 0, 0, -1), new PluginManifest.ActivityInfo("com.tencent.shadow.sample.plugin.app.lib.usecases.activity.TestActivityOptionMenu", 0, 0, 0, -1), new PluginManifest.ActivityInfo("com.tencent.shadow.sample.plugin.app.lib.usecases.activity.TestActivityOnCreate", 0, 0, 0, -1), new PluginManifest.ActivityInfo("com.tencent.shadow.sample.plugin.app.lib.usecases.activity.TestActivityOrientation", 0, 1073742976, 16, 0), new PluginManifest.ActivityInfo("com.tencent.shadow.sample.plugin.app.lib.usecases.activity.TestActivityWindowSoftMode", 0, 0, 4, -1), new PluginManifest.ActivityInfo("com.tencent.shadow.sample.plugin.app.lib.usecases.provider.TestDBContentProviderActivity", 0, 0, 0, -1), new PluginManifest.ActivityInfo("com.tencent.shadow.sample.plugin.app.lib.usecases.activity.TestActivityReCreateBySystem", 0, 0, 0, -1), new PluginManifest.ActivityInfo("com.tencent.shadow.sample.plugin.app.lib.usecases.receiver.TestReceiverActivity", 0, 0, 0, -1), new PluginManifest.ActivityInfo("com.tencent.shadow.sample.plugin.app.lib.usecases.receiver.TestDynamicReceiverActivity", 0, 0, 0, -1),
new PluginManifest.ActivityInfo("com.tencent.shadow.sample.plugin.app.lib.usecases.fragment.TestDynamicFragmentActivity", 0, 0, 0, -1), new PluginManifest.ActivityInfo("com.tencent.shadow.sample.plugin.app.lib.usecases.fragment.TestXmlFragmentActivity", 0, 0, 0, -1), new PluginManifest.ActivityInfo("com.tencent.shadow.sample.plugin.app.lib.usecases.dialog.TestDialogActivity", 0, 0, 0, -1), new PluginManifest.ActivityInfo("com.tencent.shadow.sample.plugin.app.lib.usecases.packagemanager.TestPackageManagerActivity", 0, 0, 0, -1), new PluginManifest.ActivityInfo("com.tencent.shadow.sample.plugin.app.lib.usecases.provider.TestFileProviderActivity", 0, 0, 0, -1), new PluginManifest.ActivityInfo("com.tencent.shadow.sample.plugin.app.lib.usecases.application.TestApplicationActivity", 0, 0, 0, -1), new PluginManifest.ActivityInfo("com.tencent.shadow.sample.plugin.app.lib.usecases.context.ActivityContextSubDirTestActivity", 0, 0, 0, -1), new PluginManifest.ActivityInfo("com.tencent.shadow.sample.plugin.app.lib.usecases.context.ApplicationContextSubDirTestActivity", 0, 0, 0, -1), new PluginManifest.ActivityInfo("com.tencent.shadow.sample.plugin.app.lib.usecases.host_communication.PluginUseHostClassActivity", 0, 0, 0, -1), new PluginManifest.ActivityInfo("com.tencent.shadow.sample.plugin.app.lib.usecases.webview.WebViewActivity", 0, 0, 0, -1),
new PluginManifest.ActivityInfo("com.tencent.shadow.sample.plugin.app.lib.usecases.fragment.TestDialogFragmentActivity", 0, 0, 0, -1) };
services = new PluginManifest.ServiceInfo[] { new PluginManifest.ServiceInfo("com.tencent.shadow.sample.plugin.app.lib.usecases.service.HostAddPluginViewService") };
receivers = new PluginManifest.ReceiverInfo[] { new PluginManifest.ReceiverInfo("com.tencent.shadow.sample.plugin.app.lib.usecases.receiver.MyReceiver", new String[] { "com.tencent.test.action" }) };
providers = new PluginManifest.ProviderInfo[] { new PluginManifest.ProviderInfo("com.tencent.shadow.sample.plugin.app.lib.usecases.provider.TestProvider", "com.tencent.shadow.sample.host.provider.test", false) };
}
ShadowPluginLoadermPluginPartsMapComponentManagerPluginServiceManagerPluginContentProviderManagerSamplePluginLoaderPluginComponentLauncherstartActivity(...)startActivityForResult(...)startService(...)stopService(...)bindService(...)ShadowContext

宿主进程和插件进程binder

Shadow Binder


zenuml
BinderPluginLoader.startActivityInPluginProcess {
    PluginLoaderBinder."binder onTransact" {
        DynamicPluginLoader.startActivityInPluginProcess
    }
}

Activity插件方案

Shadow解决Activity等组件生命周期的方法解析这篇文章是插件框架作者对于Activity生命周期的介绍。

比如插件中有个MainActivity,如下:

ActivityBaseActivityMainActivity

经过Javassist处理后类图结构(替换代码实现在com.tencent.shadow.core.transform.specific.ActivityTransform):

ShadowActivityBaseActivityMainActivity

下面看下整个Activity的详细继承关系,包括插件Activity与壳子Activity的关系:

插件Activity继承关系

启动其他插件Activity

com.tencent.shadow.core.runtime.ShadowContext#startActivity


zenuml
ShadowContext.startActivity {
    ComponentManager.startActivity {
        "pluginIntent.toActivityContainerIntent"
        ShadowContext.superStartActivity
    }
}

Activity的启动时会把目标Activity替换成PluginDefaultProxyActivity,即PluginContainerActivity。比如,我们通过如下方式启动一个插件中的Activity:

1
2
3
Intent intent = new Intent();
intent.setClassName(this, "com.tencent.shadow.sample.plugin.app.lib.gallery.splash.SplashActivity");
startActivity(intent);

经过ComponentManager转换后的Intent是:

1
Intent { cmp=com.tencent.shadow.sample.host/com.tencent.shadow.sample.plugin.runtime.PluginDefaultProxyActivity (has extras) }

资源的处理

资源的生成逻辑在com.tencent.shadow.core.loader.blocs.CreateResourceBloc#create中。
当Android版本高于8.1时,ApplicationInfo#sharedLibraryFiles添加了插件apk路径来实现:

1
2
3
4
5
6
7
8
9
sharedLibraryFiles=[
/system/framework/android.test.base.jar,
/product/app/WebViewGoogle/WebViewGoogle.apk,
/product/app/TrichromeLibrary/TrichromeLibrary.apk,
/product/overlay/EmulationPixel6/EmulationPixel6Overlay.apk,
/product/overlay/NavigationBarModeGestural/NavigationBarModeGesturalOverlay.apk,
/data/user/0/com.tencent.shadow.sample.host/files/ShadowPluginManager/UnpackedPlugin/test-dynamic-manager/c9329f69c23a75baeb87a9adb567fafe/plugin-debug.zip/sample-base-plugin-debug.apk]
sourceDir=/data/app/~~tt8RnZsUIq-XZDgF1_e6Gw==/com.tencent.shadow.sample.host-IUAuR_XOsVpJONVyM0Z26w==/base.apk
publicSourceDir=/data/app/~~tt8RnZsUIq-XZDgF1_e6Gw==/com.tencent.shadow.sample.host-IUAuR_XOsVpJONVyM0Z26w==/base.apk

然后通过packageManager.getResourcesForApplication(applicationInfo)来生成新的Resources。

如果Android版本高于8.1,则通过自定义MixResources同时持有宿主和插件的资源,当获取资源时首先获取宿主的,当获取不到时再从插件中获取。

资源句柄最终会赋值给com.tencent.shadow.core.runtime.ShadowContext#mPluginResources

为了解决合并资源后资源ID的冲突,Shadow把插件的资源ID的package-id字段给位了0x7E,可以看下sample-app项目下面的的build.gradle的配置:

1
2
3
4
// 将插件的资源ID分区改为和宿主0x7F不同的值
aaptOptions {
additionalParameters "--package-id", "0x7E", "--allow-reserved-package-id"
}

在Sample中添加插件

插件目录projects/sample/source/sample-plugin/my-plugin-app。需要注意的一点是shadow字段不要添加到my-plugin-app/build.gradle,而是要添加到projects/sample/source/sample-plugin/sample-app/build.gradle中,如下:

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
shadow {
packagePlugin {
pluginTypes {
debug {
loaderApkConfig = new Tuple2('sample-loader-debug.apk', ':sample-loader:assembleDebug')
runtimeApkConfig = new Tuple2('sample-runtime-debug.apk', ':sample-runtime:assembleDebug')
pluginApks {
pluginApk1 {
businessName = 'sample-plugin-app'
partKey = 'sample-plugin-app'
buildTask = ':sample-app:assemblePluginDebug'
apkPath = 'projects/sample/source/sample-plugin/sample-app/build/outputs/apk/plugin/debug/sample-app-plugin-debug.apk'
hostWhiteList = ["com.tencent.shadow.sample.host.lib"]
dependsOn = ['sample-base']
}
......
pluginApk3 { //这是我添加的插件
businessName = 'my-plugin-app'
partKey = 'my-plugin-app'
buildTask = ':my-plugin-app:assemblePluginDebug'
apkPath = 'projects/sample/source/sample-plugin/my-plugin-app/build/outputs/apk/plugin/debug/my-plugin-app-plugin-debug.apk'
hostWhiteList = ["com.tencent.shadow.sample.host.lib"]
}
}
}

release {
......
}
}

loaderApkProjectPath = 'projects/sample/source/sample-plugin/sample-loader'
runtimeApkProjectPath = 'projects/sample/source/sample-plugin/sample-runtime'

archiveSuffix = System.getenv("PluginSuffix") ?: ""
archivePrefix = 'plugin'
destinationDir = "${getRootProject().getBuildDir()}"

version = 4
compactVersion = [1, 2, 3]
uuidNickName = "1.1.5"
}
}

插件apk的applicationId的配置:

1
2
3
4
5
6
7
8
9
10
11
android {
defaultConfig {
applicationId 'me.rjy.android.shadow.app' //可以与宿主不同
}
// 将插件applicationId设置为和宿主相同
productFlavors {
plugin {
applicationId project.SAMPLE_HOST_APP_APPLICATION_ID //必须与宿主相同
}
}
......

参考资料

Home Wiki
Sample Readme
Shadow解决Activity等组件生命周期的方法解析
Android插件化框架-Shadow原理解析
腾讯插件框架Shadow解析之动态化和插件加载
Shadow的接入记录
Android Tencent Shadow 插件接入指南