作者: CCトゥモロー
説明する
最近、プロジェクトでは動的なモジュールのアップグレードを行う必要があるため、プラグイン フレームワーク Shadow について知りました。
Shadow Framework の公式 Web サイトのトップ号には、フレームワークの分析に関する記事が多数掲載されています。このフレームワークを理解したい場合は、必見です。
以下はプロジェクト コードのスクリーンショットです。写真はトップ号から撮影したものです。
通訳プロジェクト
複雑なプラグイン フレームワーク自体の動的なアップグレードを実現するために、シャドウ フレームワークは多くの複雑な操作を実行しました。
ホスト自体はプラグインマネージャープラグインとのみ対話します。
core-manager と Dynamic-manager に依存する plugin-manager プラグインについて話しましょう。core-manager:
1. プラグイン情報の保管
2. プラグイン情報の管理
3. つまり、dex 管理
4. プラグインパッケージ zip のリリース
Dynamic-manager:
1. dex、res などのリリース用の最も基本的な API のみを提供します。これらの API の組み合わせ呼び出しは自分で実装する必要があります。 2. ローダーとランタイム プラグのロードのみを担当します
。業務プラグインの動作に必要な -ins 業務プラグインの読み込みは、ローダープラグインの実装によって行われます。
ホストとマネージャー プラグインの間の対話は、構築ApkClassLoader
、マネージャー プラグインのロード、プラグイン内のオブジェクトの構築を通じて直接行われますPluginManagerImpl
。ManagerImplLoader
詳細はカテゴリーをご覧ください。
オブジェクトを構築する際はPluginManagerImpl
、マネージャープラグインの固定クラス内の固定メソッドを呼び出すことでcom.tencent.shadow.dynamic.impl.ManagerFactoryImpl#buildManager
、PluginManagerImpl
最終的には自分たちで実現します。
を実装し、プラグインのインストールやプラグイン アクティビティのオープンなど、アクティビティを開いてサービスを開始するなど、さまざまな目的に応じてさまざまPluginManagerImpl
なメソッドを呼び出す必要があります。core-manager
dynamic-manager
一般に、自由度は比較的大きいですが、デメリットも明らかであり、自分たちで多くの作業を行わなければなりません。
プラグイン クラスを呼び出すには、マネージャー プラグインを介してプラグイン zip パッケージ内のローダー プラグインと対話する必要があります。
現在のシャドウの場合、ホストとマネージャー プラグインは 1 つのプロセスにあり、プラグインとプラグインをロードするローダー プラグインは別のプロセスにあります。したがって、現在呼び出しているプラグイン クラスは、ipc を介してローダー プラグインと対話する必要があります。マネージャー プラグインがローダー プラグインを呼び出した後、ローダー プラグインは固定クラスをロードする固定メソッドを通じてプラグイン ロード ロジック クラスをcom.tencent.shadow.dynamic.loader.impl.CoreLoaderFactoryImpl#build
構築します。ホスト占有コンポーネントとホスト占有コンポーネント間の対応関係を設定する必要があります。ShadowPluginLoader
プラグインコンポーネント。
一般に、自由度は比較的大きいですが、デメリットも明らかであり、自分たちで多くの作業を行わなければなりません。ここで、VirtualApk
たとえばフレームワークは、マニフェストにある解析プラグインのコンポーネントの構成に基づいて、ホストに適したコンポーネントを自動的に見つけ出しますが、このロジックを自分で実装する必要がある場合、非常に面倒になります。もう 1 つの問題は、ホスト占有コンポーネントとプラグインの間の対応関係を構成するときに、フレームワークが提供するパラメータが少なすぎることです。たとえば、次のようになります。
public ComponentName onBindContainerActivity(ComponentName pluginActivity) {
switch (pluginActivity.getClassName()) {
/**
* 这里配置对应的对应关系
*/
}
return new ComponentName(context, DEFAULT_ACTIVITY);
}
このメソッドを例にとると、プラグイン呼び出しではオブジェクトのみが渡されComponentName
、その中には有用な情報のみが含まれています。このプラグイン アクティビティがホストのどのアクティビティに基づいて対応するかを知るにはどうすればよいですかClassName
。ClassName
まあ、少なくともこのプラグインアクティビティの起動モード、設定されたテーマ、その他のパラメータを知る必要があるので、ここでの設計は非常に無理があります。おそらくシャドウのロジックは、プラグインが更新され、ローダープラグインも更新されるので、if else と書いても問題ないということです。
プラグインのパッケージ化の問題
シャドウ パッケージング プラグインは、マネージャー プラグイン用の別の apk であり、パッケージ化後にロードできますが、ビジネス プラグインにとっては面倒です。ビジネス プラグインがロードしたい場合は、ローダー プラグインが必要です-in とランタイム プラグイン。ローダー プラグインとランタイム プラグインのコードは確かに比較的小さいため、ビジネス プラグインごとに 1 つずつ用意することは大きな問題ではありませんが、ローダー コードとランタイム コードが類似している場合は、それでも気分が悪いです。問題で見つかった解決策によれば、シャドウは同じ UUID を使用して、 apk のグループが連携して動作できることを示します。この APK のグループには、ランタイム、ローダー、および複数のプラグイン APK を含めることができます。これに基づいて、ローダーとランタイムのセットを共有できるプラグインがいくつかある場合、ローダーとランタイムを特定のプラグイン zip にパッケージ化することしかできず、他のプラグインはパッケージ化されませんが、それらの uuid は同じである必要があります。これらの問題が確認できます: 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"
}
}
プラグインの依存関係の問題
シャドウ ブロックの設定では、hostWhiteList を通じてホストのどのクラスにアクセスできるかを設定できます。しかし、注意が必要な状況もまだいくつかあります。
- プラグインの依存関係はパラメータ dependOn によって制御されます。パラメータは複数にすることができ、コンテンツはプラグインの partKey に入力されます。
- パラメータ hostWhiteList を設定することで、ホストにアクセスできるクラスを設定できます。デフォルトでは、プラグインはホストにアクセスできません
- プラグイン A がプラグイン B に依存すると、プラグイン Shadow はプラグイン B の ClassLoader をプラグイン A の親として使用します。
- プラグイン A がプラグイン B に依存している場合、プラグイン A で構成された hostWhiteList は機能しないため、プラグイン B で構成する必要があります。
- プラグイン A はプラグイン B に依存しています。現在、プラグイン A がプラグイン B のリソースにアクセスすることはサポートされていません。
- ホストがプラグイン内のクラスにアクセスするのは面倒
具体的な用途
上記の説明から、シャドウ プラグイン フレームワークには多くの問題があることがわかります。公式の紹介記事にもいくつかの問題点が記載されています。一般的に、直接使用するのは非常に不便です。影を使用して、私たちが最も興味があるのは、プラグインまたは無駄な反射を実現することです。その後、独自の要件に応じて二次カスタマイズを実行できます。
動的モード
実際、公式デモには nodynamic サンプルがあります。いわゆる nodynamic は、プラグイン フレームワーク自体をアップグレードする必要がなく、プラグインをホストに直接ロードすることを意味します。シャドウの場合、マネージャー プラグインは必要なく、ローダーとランタイム プラグインがホストにパッケージ化されます。ホストが使用できるように SDK をパッケージ化します。SDK にはローダーとランタイムが直接含まれています。
まず依存関係を導入します。
//把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"
ここでマネージャーを紹介する理由は、後続のカプセル化プロセスでマネージャー内のいくつかのカプセル化されたデータ構造が使用されるためです。
フォローアップは、シャドウ ローダー SDK のカプセル化です。コードはここでは示されていません。
Gradleプラグインを変更する
このセクションの内容は、gradle プラグインの作成方法をすでに知っていることを前提としていますが、そうでない場合は、まずこの知識を理解する必要があります。
ローダーとランタイムをホストにインポートしたので、以前は複雑なプラグイン情報は必要ありませんでした。ただし、現在ロードされているプラグインのプラグイン情報と、プラグイン情報なしでプラグインをロードする方法を知る必要があります。最終的に必要なのは、少なくとも次のプラグイン情報だけです。
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}
もちろん、ここで生成されるプラグイン情報は特定のプラグインに属しますが、複数のプラグインをマージしてホストにダウンロードまたはビルドする必要がある場合は、その config.json をマージするスクリプトを記述する必要があります。各プラグインを 1 つにまとめた配列で十分です もちろんコードも非常にシンプルなので、ここではスクリプトは公開しません。
プラグインに依存する場合にプラグイン リソースにも依存できるプラグインをサポートするように 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が共通プラグインに依存している場合、appcompatは共通プラグイン内にあり、webview付きのActivityはAppCompatActivityにできません。