ShadowSample

Introduction: 使用腾讯 Shadow 实现插件化的 Demo
More: Author   ReportBugs   
Tags:

使用腾讯 Shadow 实现插件化的 Demo

下面记录一下接入流程和遇到的问题,方便以后查看。

结构

整个 Demo 由 3 个工程构成,分别是宿主 App,插件管理 App,插件 App,三个工程相互完全独立。

|--HostApp //宿主
|
|--PluginManager  //负责加载插件,管理插件
|
|--BusinessApp  //插件 app
   |--plugin-app  //业务 app
   |--sample-loader  //配置了插件 Activity 到壳 Activity 的对应关系等
   |--sample-runtime  //定义了宿主中占位的 Activity 等

Shadow 主要接入流程

  1. Clone Shadow 的代码 Shdow Github 地址,然后编译,上传本地 maven 库或者远程 maven 库,以便后续应用,不建议直接引用 sdk 代码,里面的库分很多块,三个工程各自需要引用的模块不一样,操作不方便,用 maven 可以分别按需引用。
  2. 参照腾讯 Shadow 的 Sample 中 Maven 下面的 sample(结构也是一样的三个工程),分别创建工程,引用相应的库,实现必要的接口(占位的 Activity、Service,插件管理类等),分别编译通过。
  3. PluginManager 编译之后生成 apk 文件,BusinessApp 编译之后生成一个 zip 包,需要将他们分别放到宿主指定的文件目录之下,让宿主能够加载到。

    BusinessApp 编译用如下命令: gradlew packageDebugPlugin
    放文件的位置是宿主中配置的,pluginManager 的 apk 存放位置在初始化管理类的时候定义的,关键代码是 `FixedPathPmUpdater fixedPathPmUpdater

             = new FixedPathPmUpdater(new File(PLUGIN_MANAGER_APK_FILE_PATH));`
    

    插件 zip 包的位置可以在启动插件四大组件的时候传: bundle.putString("plugin_path", "/data/local/tmp/plugin-debug.zip"); 也可以直接在 PluginManager 中硬编码: InstalledPlugin installedPlugin = installPlugin(pluginZipPath, null, true); 在 PluginManager 里面根据 part_key 来判断是哪个插件,然后直接判断出地址要方便一些

  4. 安装宿主 App,测试是否可以加载插件 app 的页面、服务等。

==第二步的步骤很多==,可以参考这里

小注意点:
  1. 插件的包名和宿主的包名要一致
    1. 有一些类要求路径和类名是固定的,像 loader 里面的 CoreLoaderFactoryImpl,写的时候要注意,弄错了很难排查问题。
    2. 占位的 activity 在宿主的 manifest 里面注册,在插件的 runtime 里面声明,在插件的 loader 里面写对应关系,类似这些地方要注意,包类名要对应上,不要写错
遇到问题:

1.实际按照这个博客做的时候,最后从宿主 App 跳到插件的时候报了如下错,

java.lang.ClassCastException: Cannot cast androidx.core.app.CoreComponentFactory to com.tencent.shadow.core.runtime.ShadowAppComponentFactory

没能找到具体原因,最后重新建了一个 BusinessApp 工程引入官方 demo 中 Maven 下面的 Plugin-project,改了相关的配置,然后才测试通过的;

2.在插件的 app 配置时,业务 app 的 build.gradle 配置 loader 和 runtime 的 apk 路径已经插件模块的 partKey 等信息的时候,报了 apkName 无法识别的错误,没有解决,最后注释了这一行,后面也能测试通过,暂时不知道还有什么影响。

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 = 'my-plugin'
//businessName 相同的插件,context 获取的 Dir 是相同的。businessName 留空,表示和宿主相同业务,直接使用宿主的 Dir。
                        partKey = 'my-plugin'
                        buildTask = 'assemblePluginDebug'
//                        apkName = 'plugin-app-plugin-debug.apk'
                        apkPath = 'plugin-app/build/outputs/apk/plugin/debug/plugin-app-plugin-debug.apk'
                    }
                }
            }

            release {
                loaderApkConfig = new Tuple2('sample-loader-release.apk', ':sample-loader:assembleRelease')
                runtimeApkConfig = new Tuple2('sample-runtime-release.apk', ':sample-runtime:assembleRelease')
                pluginApks {
                    pluginApk1 {
                        businessName = 'my-plugin'
                        partKey = 'my-plugin'
                        buildTask = 'assemblePluginRelease'
//                        apkName = 'plugin-app-plugin-release.apk'
                        apkPath = 'plugin-app/build/outputs/apk/plugin/release/plugin-app-plugin-release.apk'
                    }
                }
            }
        }

        loaderApkProjectPath = 'sample-loader'

        runtimeApkProjectPath = 'sample-runtime'

        version = 4
        compactVersion = [1, 2, 3]
        uuidNickName = "1.1.5"
    }
}
Apps
About Me
GitHub: Trinea
Facebook: Dev Tools