阐明

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

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

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

Shadow插件化框架使用

项目解读

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里边的装备,去主动寻觅宿主合适的组件的,假设这个逻辑还得咱们自己完成的话,也很麻烦。还有个问题在装备宿主占坑组件和插件里边的对应关系的时候,结构给的参数太少了,例如:

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… 详细装备如下:

//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里边如下装备:

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。

首要引进依靠:
//把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打入宿主了,不需求之前复杂的插件信息了。可是咱们依然需求知道当前加载的插件的插件信息,没有插件信息怎样去加载呢。咱们终究最少只需求如下的插件信息即可。

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。

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咱们需求修正

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。

{"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即可。

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。