Uso del marco del complemento Shadow

Autor: CCmañana

ilustrar

Recientemente, el proyecto quiere hacer actualizaciones dinámicas de módulos, por lo que aprendí sobre el marco de complemento Shadow.

El número principal del sitio web oficial del framework shadow contiene muchos artículos sobre el análisis del framework. Si quieres entender este marco, esta es una visita obligada.

Aquí todavía hay una captura de pantalla del código del proyecto. La imagen está tomada de la edición superior.

Proyecto de Interpretación

Para realizar la actualización dinámica del marco de complemento complejo en sí mismo, el marco de sombra ha realizado muchas operaciones complicadas:

El host en sí solo interactúa con el complemento del administrador de complementos.

Hablemos del complemento plugin-manager, que depende de core-manager y dynamic-manager. core-manager:
1. Almacenamiento de información de complementos
2. Gestión de información de complementos
3. Por lo tanto, gestión de dex
4. Liberación del zip del paquete de complementos

administrador dinámico:
1. Solo proporciona las API más básicas para el lanzamiento de dex, res, etc. Las llamadas combinadas de estas API deben ser implementadas por usted mismo. 2. Solo es responsable de cargar
el cargador y el tiempo de ejecución. -ins necesarios para el funcionamiento de los complementos comerciales. La carga de los complementos comerciales se realiza mediante la implementación del complemento del cargador.

La interacción entre el host y el complemento del administrador se realiza directamente a través de la construcción ApkClassLoader, la carga del complemento del administrador y la construcción de los objetos en el complemento PluginManagerImpl. Puedes ver ManagerImplLoaderla categoría para más detalles.

Al construir PluginManagerImplun objeto, es llamando al método fijo en la clase fija del complemento del administrador com.tencent.shadow.dynamic.impl.ManagerFactoryImpl#buildManager, y luego esto lo PluginManagerImplrealizamos finalmente nosotros mismos.

Necesitamos implementar PluginManagerImply luego llamar a diferentes métodos de acuerdo con diferentes intenciones, como abrir una actividad e iniciar un servicio, core-managercomo dynamic-managerinstalar un complemento, abrir una actividad de complemento y similares.

En términos generales, el grado de libertad es relativamente grande, pero las desventajas también son obvias y tenemos que hacer mucho trabajo nosotros mismos.

Para llamar a la clase del complemento, debe interactuar con el complemento del cargador en el paquete zip del complemento a través del complemento del administrador.

Para la sombra actual, el host y el complemento del administrador están en un proceso, y el complemento y el complemento del cargador que carga el complemento están en otro proceso. Por lo tanto, actualmente llamar a la clase de complemento debe interactuar con el complemento del cargador a través de ipc. Después de que el complemento del administrador llama al complemento del cargador, el complemento del cargador com.tencent.shadow.dynamic.loader.impl.CoreLoaderFactoryImpl#buildconstruye ShadowPluginLoaderla clase lógica de carga del complemento a través del método fijo de carga de la clase fija. Necesitamos configurar la relación correspondiente entre el componente de ocupación del host y el componente de complemento.

En términos generales, el grado de libertad es relativamente grande, pero las desventajas también son obvias y tenemos que hacer mucho trabajo nosotros mismos. Aquí, VirtualApkel marco, por ejemplo, encuentra automáticamente los componentes adecuados para el host en función de la configuración de los componentes del complemento de análisis en el manifiesto Si tenemos que implementar esta lógica nosotros mismos, será muy problemático. Otro problema es que al configurar la relación correspondiente entre el componente de ocupación del host y el complemento, el marco proporciona muy pocos parámetros, por ejemplo:

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

Tome este método como ejemplo, la llamada del complemento solo pasa un ComponentNameobjeto y solo hay información útil en él ClassName. ¿Cómo puedo ClassNamesaber qué actividad del host debe usar esta actividad del complemento para corresponder a él en función de uno, y si más está escrito uno por uno Bueno, al menos necesito saber el modo de inicio de esta actividad de complemento, el tema configurado y otros parámetros antes de que pueda decidir, por lo que el diseño aquí es muy irrazonable. Tal vez la lógica de la sombra es que el complemento se actualiza y el complemento del cargador también se actualiza, por lo que está bien escribir si no.

Problema de empaquetado del complemento

El complemento de empaquetado oculto es un apk separado para el complemento de administrador, que se puede cargar después del empaquetado, pero es problemático para el complemento empresarial. Si el complemento empresarial quiere cargarse, necesita un complemento de carga. -in y un complemento de tiempo de ejecución. ¿Es cierto que cada uno de nuestros complementos comerciales tiene necesidad de traer un complemento de carga y un complemento de tiempo de ejecución, aunque el código del complemento de carga y el complemento de tiempo de ejecución es de hecho, relativamente pequeño, no es un gran problema tener uno para cada complemento comercial, pero si los códigos del cargador y del tiempo de ejecución son similares, todavía se siente mal, de acuerdo con la solución encontrada en el problema, la sombra usa el mismo UUID para indicar que un grupo de aplicaciones puede funcionar juntas. Este grupo de apk puede tener un tiempo de ejecución, un cargador y múltiples apks de complementos. En base a esto, si tenemos algunos complementos que pueden compartir un conjunto de cargador y tiempo de ejecución, solo podemos empaquetar el cargador y el tiempo de ejecución en un complemento zip determinado, y otros complementos no se empaquetarán, pero sus uuid deben ser los mismos. Puede ver estos problemas: github.com/Tencent/Sha... La configuración específica es la siguiente:

//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"

    }
}

Luego, el complemento A se configura de la siguiente manera:

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"

    }
}

Problema de dependencia del complemento

La configuración en el bloque de sombra puede configurar a qué clases del host se puede acceder a través de hostWhiteList. Pero todavía hay algunas situaciones que necesitan atención.

  • Las dependencias de los complementos están controladas por el parámetro dependOn, que puede ser múltiple, y el contenido completa la partKey del complemento.
  • Puede configurar la clase que puede acceder al host configurando el parámetro hostWhiteList. De forma predeterminada, el complemento no puede acceder al host.
  • El complemento A depende del complemento B, luego el complemento Shadow utilizará el ClassLoader del complemento B como principal del complemento A.
  • El complemento A depende del complemento B, entonces la hostWhiteList configurada en el complemento A no funcionará y debe configurarse en el complemento B
  • El complemento A depende del complemento B, actualmente no es compatible con el complemento A para acceder a los recursos del complemento B
  • Es problemático para el host acceder a las clases en el complemento.

Uso específico

En base a las descripciones anteriores, podemos encontrar que el marco del complemento shadow tiene muchos problemas. El artículo de introducción oficial también mencionó algunos problemas. En términos generales, es muy inconveniente usarlo directamente. Usando la sombra, lo que más nos interesa es lograr un complemento o un reflejo inútil. Luego podemos realizar una personalización secundaria de acuerdo con nuestros propios requisitos.

modo no dinámico

En realidad, no hay muestras dinámicas en la demostración oficial. El llamado nodynamic significa que el marco del complemento en sí no necesita ser actualizado, y cargamos directamente el complemento en el host. Para la sombra, no se necesita el complemento del administrador, y los complementos del cargador y del tiempo de ejecución se empaquetan en el host. Empaquetamos un sdk para que lo use el host, y el sdk contiene directamente el cargador y el tiempo de ejecución.

Primero introduce las dependencias:
//把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"

La razón por la que presentamos el administrador aquí es porque el proceso de encapsulación posterior utiliza algunas estructuras de datos encapsuladas en el administrador.

El seguimiento es una encapsulación del sdk del cargador de sombras. El código no se muestra aquí.

Modificar el complemento de gradle

El contenido de esta sección asume que ya sabe cómo escribir complementos de Gradle. Si no es así, primero debe comprender este conocimiento.

Dado que hemos importado el cargador y el tiempo de ejecución al host, no necesitamos la complicada información del complemento antes. Pero aún necesitamos conocer la información del complemento cargado actualmente, cómo cargarlo sin la información del complemento. Al final, solo necesitamos al menos la siguiente información del complemento.

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

Luego, debemos modificar el complemento de gradle de shadow.Después de compilar el apk del complemento, el json de la información del complemento se genera de inmediato.

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()
                    }
                }
            }
        }
    }

}

Por supuesto que necesitamos modificar 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() {
    }

}

De esta forma, generamos json de información del complemento cuando ensamblamosPluginRelease(Debug), y la ruta y la ruta para generar apk están en la misma ubicación /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}

Por supuesto, la información del complemento generada aquí pertenece a un determinado complemento. Si necesitamos fusionar varios complementos para descargarlos o compilarlos en el host, debemos escribir un script para fusionar el config.json de cada complemento en uno Una matriz es suficiente Por supuesto, el código también es muy simple, por lo que el script no se lanzará aquí.

Modifique CreateResourceBloc para admitir complementos que también pueden depender de recursos de complementos cuando dependen de complementos.

Simplemente modifique 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
    }

}

No hay muchos cambios, pero después de probarlo, si el complemento A depende del complemento común, appcompat está en el complemento común y la Actividad con vista web no puede ser AppCompatActivity.

Supongo que te gusta

Origin blog.csdn.net/weixin_61845324/article/details/132146096
Recomendado
Clasificación