Shadow插件化框架使用

说明

最近项目想要做模块动态升级,所以了解了最近还在维护的插件化框架Shadow.

shadow框架的官网的顶置issue,里面有非常多的关于框架的解析的文章。想要了解此框架,这个必看。

这里还是截取一张项目代码图。图片取自顶置的issue。

项目解读

shadow框架为了实现复杂的插件化框架本身也动态升级,做了很多复杂操作:

宿主本身只跟plugin-manager插件交互

来说一下plugin-manager插件,依赖core-manager,dynamic-manager。 core-manager: 1、插件信息的存储 2、插件信息的管理 3、 so、dex管理 4、插件包zip释放

dynamic-manager: 1、只提供最基础的 dex、 res、so 的释放的基础API,这些 API 的组合调用需要自己实现 2、只负责加载 业务插件运行需要的 loader 和runtime 插件,业务插件的加载由 loader 插件实 现

宿主和manager插件交互,是直接通过构造ApkClassLoader,加载manager插件,构造插件里面的PluginManagerImpl对象。具体可以看ManagerImplLoader类。

在构造PluginManagerImpl对象的时候,是通过调用manager插件固定类里面的固定方法com.tencent.shadow.dynamic.impl.ManagerFactoryImpl#buildManager,然后这个PluginManagerImpl最终也是我们自己实现的。

我们需要实现PluginManagerImpl,然后根据不同的意图,比如打开activity,启动service,来调用不同的core-manager,或者dynamic-manager的方法,比如安装插件、打开插件activity之类的。

总体而言,自由度比较大,但是弊端也很明显,我们自己也要做很多的工作。

调用插件类,需要通过manager插件和插件zip包里面的loader插件交互

对目前的shadow来说,宿主和manager插件在一个进程,插件和加载插件的loader插件在另一个进程。 所以目前调用插件类需要通过ipc的方式和loader插件交互。manager插件调用到loader插件之后,loader插件通过加载固定类的固定方法com.tencent.shadow.dynamic.loader.impl.CoreLoaderFactoryImpl#build,去构造ShadowPluginLoader插件加载逻辑类,我们需要在这里面去配置宿主占坑组件和插件组件的对应关系。

总体而言,自由度比较大,但是弊端也很明显,我们自己也要做很多的工作。这里例如VirtualApk框架,是根据解析插件的组件在manifest里面的配置,去自动寻找宿主合适的组件的,如果这个逻辑还得我们自己实现的话,也很麻烦。还有个问题在配置宿主占坑组件和插件里面的对应关系的时候,框架给的参数太少了,例如:

arduino 复制代码
public ComponentName onBindContainerActivity(ComponentName pluginActivity) {
    switch (pluginActivity.getClassName()) {
        /**
          * 这里配置对应的对应关系
          */
    }
    return new ComponentName(context, DEFAULT_ACTIVITY);
}

就拿这个方法来说,插件调用只传递来了一个ComponentName对象,里面有用的信息只有ClassName,我怎么根据一个ClassName去知道这个插件activity应该使用宿主的哪个占坑activity去对应呢,一个个的if else写死嘛,起码我要知道这个插件activity的启动模式,配置的主题等等参数,才能决定,所以这里设计的很不合理。可能shadow的逻辑是插件更新了,loader插件也要更新,所以写if else也没问题。

插件打包问题

shadow打包插件,对于manager插件来说就是一个单独的apk,打包之后加载即可,对于业务插件来说就麻烦了,业务插件想要加载需要有loader插件和runtime插件,难道我们每一个业务插件都需要带一个loader插件和runtime插件嘛,虽然loader插件和runtime的插件代码也确实比较小,每个业务插件有一个其实问题也不大,不过如果loader和runtime的代码都差不多的话,还是感觉不好,根据在issue里面找到的方案,shadow是使用UUID相同表示一组apk可以共用工作。这组apk里可以有一个runtime一个loader和多个插件apk。 基于此,如果我们有一些插件可以共用一组loader和runtime的话,可以只在某一个插件zip里面打包loader和runtime,其他的插件不打包,但是他们的uuid必须相同。 可以看这些issue: github.com/Tencent/Sha... github.com/Tencent/Sha... 具体配置如下:

ini 复制代码
//common插件里面包含了runtime和loader
shadow {
    transform {
        //useHostContext = ['abc']
    }
    packagePlugin {
        pluginTypes {
            debug {
                loaderApkConfig = new Tuple2('plugin_loader-debug.apk', ':plugin_loader:assembleDebug')
                runtimeApkConfig = new Tuple2('plugin_runtime-debug.apk', ':plugin_runtime:assembleDebug')
                pluginApks {
                    plugin_1 {
                        //businessName相同的插件,context获取的Dir是相同的。businessName留空,表示和宿主相同业务,直接使用宿主的Dir
                        businessName = ''
                        partKey = 'plugin_common'
                        buildTask = 'assemblePluginDebug'
                        apkPath = 'plugin_common_app/build/outputs/apk/plugin/debug/plugin_common_app-plugin-debug.apk'
                        hostWhiteList = ["com.blankj.utilcode.util",
                                         "com.blankj.utilcode.constant",
                        ]
                        //dependsOn = ['']
                    }
                }
            }

            release {
                loaderApkConfig = new Tuple2('plugin_loader-release.apk', ':plugin_loader:assembleRelease')
                runtimeApkConfig = new Tuple2('plugin_runtime-release.apk', ':plugin_runtime:assembleRelease')
                pluginApks {
                    plugin_1 {
                        businessName = ''
                        partKey = 'plugin_common'
                        buildTask = 'assemblePluginRelease'
                        apkPath = 'plugin_common_app/build/outputs/apk/plugin/debug/plugin_common_app-plugin-release.apk'
                        hostWhiteList = ["com.blankj.utilcode.util",
                                         "com.blankj.utilcode.constant",
                        ]
                        //dependsOn = ['']
                    }
                }
            }
        }

        uuid = "123567"
        loaderApkProjectPath = 'plugin_loader'
        runtimeApkProjectPath = 'plugin_runtime'

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

        version = 1
        compactVersion = [1]
        uuidNickName = "1.0.0"

    }
}

然后插件A里面如下配置:

ini 复制代码
shadow {
    transform {
        //useHostContext = ['abc']
    }
    packagePlugin {
        pluginTypes {
            debug {
                //这里不配置,最终的zip包里面就不会有loader和runtime了
                //loaderApkConfig = new Tuple2('plugin_loader-debug.apk', ':plugin_loader:assembleDebug')
                //runtimeApkConfig = new Tuple2('plugin_runtime-debug.apk', ':plugin_runtime:assembleDebug')
                pluginApks {
                    plugin_a {
                        //businessName相同的插件,context获取的Dir是相同的。businessName留空,表示和宿主相同业务,直接使用宿主的Dir
                        businessName = ''
                        partKey = 'plugin_a'
                        buildTask = 'assemblePluginDebug'
                        apkPath = 'plugina/build/outputs/apk/plugin/debug/plugina-plugin-debug.apk'
                        hostWhiteList = ["com.blankj.utilcode.util",
                                         "com.blankj.utilcode.constant",
                        ]
                        dependsOn = ['plugin_common']
                    }
                }
            }

            release {
                //loaderApkConfig = new Tuple2('plugin_loader-release.apk', ':plugin_loader:assembleRelease')
                //runtimeApkConfig = new Tuple2('plugin_runtime-release.apk', ':plugin_runtime:assembleRelease')
                pluginApks {
                    plugin_a {
                        businessName = ''
                        partKey = 'plugin_a'
                        buildTask = 'assemblePluginRelease'
                        apkPath = 'plugina/build/outputs/apk/plugin/debug/plugina-plugin-release.apk'
                        hostWhiteList = ["com.blankj.utilcode.util",
                                         "com.blankj.utilcode.constant",
                        ]
                        dependsOn = ['plugin_common']
                    }
                }
            }
        }

        uuid = "123567"
        loaderApkProjectPath = 'plugin_loader'
        runtimeApkProjectPath = 'plugin_runtime'

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

        version = 1
        compactVersion = [1]
        uuidNickName = "1.0.0"

    }
}

插件依赖问题

shadow block里面的配置,可以通过hostWhiteList配置可以访问宿主的哪些类。但是还是有一些情况需要注意。

  • 插件依赖通过参数dependsOn控制,可以是多个,内容填写插件的partKey
  • 可以通过在参数hostWhiteList配置可以访问宿主的类,默认情况,插件不能访问宿主
  • 插件A dependsOn 插件B,那么插件Shadow会将插件B的ClassLoader作为插件A的parent
  • 插件A dependsOn 插件B,那么插件A配置的hostWhiteList就不起作用了,需要在插件B里面配置
  • 插件A dependsOn 插件B,目前并不支持插件A访问插件B的资源
  • 宿主要访问插件里面的类比较麻烦

具体官方这篇文章也有介绍Shadow对插件包管理的设计.

具体使用

综合上面的一些描述,我们其实是可以发现,shadow插件化框架是有不少问题的,官方自己的介绍文章里面也说了一些,总体要是直接使用起来其实是很不方便的。 使用shadow,我们最看中的是实现插件化还是没用什么反射。那我们可以按照自己要求进行二次定制。

nodynamic模式

官方Demo里面其实有nodynamic的sample的。所谓nodynamic就是插件化框架本身不需要升级,我们直接在宿主里面加载插件。对于shadow来说,就是不需要manager插件了,把loader和runtime插件打包到宿主里面。 我们封装一个sdk给宿主使用,sdk里面直接包含loader和runtime。

首先引入依赖:
kotlin 复制代码
//把loader和runtime打包到宿主,不用插件框架自身的升级
//common
implementation "com.tencent.shadow.core:common:$shadow_version"
//包含core:runtime和core:load-parameters
implementation "com.tencent.shadow.core:loader:$shadow_version"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.5.32"
//承载插件的容器,runtime
implementation "com.tencent.shadow.core:activity-container:$shadow_version"
//数据库管理插件的
implementation "com.tencent.shadow.core:manager:$shadow_version"

这里之所以我们引入了manger,是因为后续封装过程使用到了manger里面的一些封装好的数据结构。

后续就是一些对shadow的loader sdk的一些封装了。这里就不展示代码了。

对gradle插件进行修改

这一节的内容假定你已经会写gradle插件了,不会的话需要先了解这方面的知识。

由于我们把loader和runtime打入宿主了,不需要之前复杂的插件信息了。但是我们仍然需要知道当前加载的插件的插件信息,没有插件信息怎么去加载呢。我们最终最少只需要如下的插件信息即可。

ini 复制代码
shadow {
    pluginInfo {
        pluginKey = 'plugina'
        version = android.defaultConfig.versionCode
        hostWhiteList = [
                "com.blankj.utilcode.util",
                "com.blankj.utilcode.constant",
        ]
        dependsOn = [
                "plugin_common_app"
        ]
    }
}

然后我们需要修改shadow的gradle插件,在构建完成插件apk之后,随即生成插件信息的json。

scss 复制代码
class ShadowPlugin : Plugin<Project> {

    ...

    override fun apply(project: Project) {
        project.afterEvaluate {
            onEachPluginVariant(project) { pluginVariant ->
                checkAaptPackageIdConfig(pluginVariant)
                val appExtension: AppExtension = project.extensions.getByType(AppExtension::class.java)

                //这里是我们新增的代码,其他代码没改
                createPluginInfoTasks(project, shadowExtension, pluginVariant)

                createGeneratePluginManifestTasks(project, appExtension, pluginVariant)
            }
        }
    }

    /**
     * 创建根据用户的配置生成插件信息的task
     */
    private fun createPluginInfoTasks(
        project: Project, shadowExtension: ShadowExtension, pluginVariant: ApplicationVariant
    ) {
        val extension = shadowExtension.pluginInfo
        if (extension.pluginKey.isNotBlank()) {
            //System.err.println("${project.name} pluginInfo===>$extension")
            pluginVariant.outputs?.all { output ->
                //因为前面已经过滤过了,所有这里基本一定是ApkVariantOutputImpl
                if (output is ApkVariantOutputImpl) {
                    //NormalDebug
                    val full = pluginVariant.name.capitalize()
                    //Normal
                    val favor = pluginVariant.flavorName.capitalize()
                    //Debug
                    val type = pluginVariant.buildType.name.capitalize()
                    //System.err.println("name=$full output=${output.outputFile.absolutePath}")
                    //assembleNormalDebug
                    val assembleTask = project.tasks.getByName("assemble$full")
                    assembleTask.doFirst { task ->
                        //直接在doFirst里面操作即可
                        //System.err.println("${task.name} doFirst")
                        //{
                        //    "partKey": "",
                        //    "apkName": "",
                        //    "version": 100,
                        //    "dependsOn": ["",""],
                        //    "hostWhiteList": ["",""]
                        //}
                        //写入outputs的config.json
                        val config = JSONObject()
                        config["pluginKey"] = extension.pluginKey
                        config["apkName"] = output.outputFile.name
                        config["version"] = extension.version
                        if (extension.dependsOn.isNotEmpty()) {
                            val dependsOnJson = JSONArray()
                            for (k in extension.dependsOn) {
                                dependsOnJson.add(k)
                            }
                            config["dependsOn"] = dependsOnJson
                        }
                        if (extension.hostWhiteList.isNotEmpty()) {
                            val hostWhiteListJson = JSONArray()
                            for (k in extension.hostWhiteList) {
                                hostWhiteListJson.add(k)
                            }
                            config["hostWhiteList"] = hostWhiteListJson
                        }
                        val file = File(output.outputFile.parentFile, "config.json")
                        //System.err.println("config json file=" + file.absolutePath)
                        project.logger.info("config json file=" + file.absolutePath)
                        val bizWriter = BufferedWriter(FileWriter(file))
                        bizWriter.write(config.toJSONString())
                        bizWriter.flush()
                        bizWriter.close()
                    }
                }
            }
        }
    }

}

当然ShadowExtension我们需要修改

kotlin 复制代码
open class ShadowExtension {
    var transformConfig = TransformConfig()
    fun transform(action: Action<in TransformConfig>) {
        action.execute(transformConfig)
    }

    var pluginInfo = PluginInfoConfig()
    fun pluginInfo(action: Action<in PluginInfoConfig>) {
        action.execute(pluginInfo)
    }
}

//新增PluginInfoConfig类

open class PluginInfoConfig {
    /**
     * 插件我们认为key是唯一的
     */
    var pluginKey = ""
    var apkName = ""

    /**
     * 插件的版本每次如果升级的话,表示是一个新插件
     */
    var version = -1
    var dependsOn: Array<String> = emptyArray()
    var hostWhiteList: Array<String> = emptyArray()

    constructor() {
    }

}

这样我们即在assemblePluginRelease(Debug)的时候生成了插件信息json,路径和生成apk的路径在同一个位置/build/outputs/plugin/release(debug)/config.json。

json 复制代码
{"apkName":"plugina-plugin-debug.apk","dependsOn":["plugin_common_app"],"pluginKey":"plugina","hostWhiteList":["com.blankj.utilcode.util","com.blankj.utilcode.constant"],"version":100}

当然这里生成的插件信息是某一个插件的,如果我们需要把几个插件合并在一起去下载或者内置到host里面,我们需要写个脚本把这每个插件的config.json合并一下,变成一个数组即可,当然这个代码也很简单,这里就不放出那个脚本了。

修改CreateResourceBloc支持插件依赖插件的时候也能依赖插件的资源。

修改CreateResourceBloc即可。

kotlin 复制代码
object CreateResourceBloc {

    /**
     * 现在插件不能
     */
    fun create(
        archiveFilePath: String,
        hostAppContext: Context,
        loadParameters: LoadParameters,
        pluginPartsMap: MutableMap<String, PluginParts>
    ): Resources {
        ...
        if (Build.VERSION.SDK_INT > MAX_API_FOR_MIX_RESOURCES) {
            fillApplicationInfoForNewerApi(
                applicationInfo,
                hostApplicationInfo,
                archiveFilePath,
                loadParameters,
                pluginPartsMap
            )
        } else {
            fillApplicationInfoForLowerApi(
                applicationInfo,
                hostApplicationInfo,
                archiveFilePath,
                loadParameters,
                pluginPartsMap
            )
        }
        ...
    }

    private fun fillApplicationInfoForNewerApi(
        applicationInfo: ApplicationInfo,
        hostApplicationInfo: ApplicationInfo,
        pluginApkPath: String,
        loadParameters: LoadParameters,
        pluginPartsMap: MutableMap<String, PluginParts>
    ) {
        ...
        // hostSharedLibraryFiles中可能有webview通过私有api注入的webview.apk
        val hostSharedLibraryFiles = hostApplicationInfo.sharedLibraryFiles
        val paths = arrayListOf<String>()
        val dependsOn = loadParameters.dependsOn
        if (dependsOn != null && dependsOn.isNotEmpty()) {
            dependsOn.forEach {
                pluginPartsMap[it]?.apply {
                    paths.add(pluginPackageManager.archiveFilePath)
                }
            }
        }
        val otherApksAddToResources =
            if (hostSharedLibraryFiles == null)
                arrayOf(
                    *paths.toTypedArray(),
                    pluginApkPath
                )
            else
                arrayOf(
                    *hostSharedLibraryFiles,
                    *paths.toTypedArray(),
                    pluginApkPath
                )

        applicationInfo.sharedLibraryFiles = otherApksAddToResources
    }

    /**
     * API 25及以下系统,单独构造插件资源
     */
    private fun fillApplicationInfoForLowerApi(
        applicationInfo: ApplicationInfo,
        hostApplicationInfo: ApplicationInfo,
        pluginApkPath: String,
        loadParameters: LoadParameters,
        pluginPartsMap: MutableMap<String, PluginParts>
    ) {
        applicationInfo.publicSourceDir = pluginApkPath
        applicationInfo.sourceDir = pluginApkPath
        val hostSharedLibraryFiles = hostApplicationInfo.sharedLibraryFiles
        val paths = arrayListOf<String>()
        val dependsOn = loadParameters.dependsOn
        if (dependsOn != null && dependsOn.isNotEmpty()) {
            dependsOn.forEach {
                pluginPartsMap[it]?.apply {
                    paths.add(pluginPackageManager.archiveFilePath)
                }
            }
        }
        val otherApksAddToResources = if (hostSharedLibraryFiles == null) {
            arrayOf(*paths.toTypedArray())
        } else {
            arrayOf(
                *paths.toTypedArray(),
                *hostSharedLibraryFiles
            )
        }
        applicationInfo.sharedLibraryFiles = otherApksAddToResources
    }

}

改动其实不多,不过我测试下来,假如插件A依赖common插件,appcompat在common插件里面,有webview的Activity不能是AppCompatActivity。

相关推荐
每次的天空6 小时前
Android学习总结之算法篇五(字符串)
android·学习·算法
Gracker7 小时前
Android Weekly #202513
android
张拭心9 小时前
工作九年程序员的三月小结
android·前端
每次的天空9 小时前
Flutter学习总结之Android渲染对比
android·学习·flutter
鸿蒙布道师11 小时前
鸿蒙NEXT开发土司工具类(ArkTs)
android·ios·华为·harmonyos·arkts·鸿蒙系统·huawei
智想天开12 小时前
11.使用依赖注入容器实现松耦合
android
yunteng52113 小时前
音视频(四)android编译
android·ffmpeg·音视频·x264·x265
tangweiguo0305198713 小时前
(kotlin) Android 13 高版本 图片选择、显示与裁剪功能实现
android·开发语言·kotlin
匹马夕阳13 小时前
(一)前端程序员转安卓开发分析和规划建议
android·前端
Kika写代码13 小时前
【Android】UI开发:XML布局与Jetpack Compose的全面对比指南
android·xml·ui