纯纯看番-插件化实现
为了方便源的升级和维护,也为了能让第三方开发者一起加入进来实现更多的源,从 4.0.3 版本开始,纯纯看番更新了插件化,可以使用安装拓展apk的方式来新增和更新纯纯看番的番剧源。而在此之前对于源的更新和升级,都需要发布一个完整纯纯看番版本,比较麻烦。
序言
对于纯纯看番拓展的这种场景,是插件化中最简单的一种。实际上就是功能的接口在宿主 app,而插件里有功能接口的实现,最终宿主加载插件里的实现而以接口的形式调用和管理。这种方式不涉及代码热修复和四大组件占位等问题,只需类加载和反射即可。下面将介绍纯纯看番的插件后业务逻辑。
整体框架
架构图:
分为三层,分别是业务层,番源层和拓展层。
- 拓展层负责加载用户安装的插件app的相关数据。具体为使用 PathClassLoader 加载插件 apk 中的代码。
- 番源层负责将拓展层获取的相关代码打包成源对象,具体流程为加载,迁移和装配。其中装配会根据用户的源配置(开关,排序等)来封装成 SourceBundle 供业务使用,该对象后面会介绍。
- 业务层为具体的业务,对于插件中为一个一个 Component,而纯纯看番中的业务层最终通过 SourceBundle 获取需要的插件中的 Component 对象,来实现业务功能。
该架构将番源层和拓展层分开,业务只依赖番源层而不依赖拓展层。后续可以很方便的新增新的源加载方式,例如如果后续需要支持 js 加载源,则业务层和拓展层源有代码可以不需要做任何改动,只需要新增一个 js加载层,将 js 文件封装成源对象并由番源层一起装配进 SourceBundle 即可。
将业务层和番源层分开,每个 Conpoment 都使用开闭原则限制,可以更好的保证番剧源的向下兼容性。当需要新增功能时直接新增 Component 类型而没适配的插件原有的 Component 不受影响。
下面开始介绍纯纯看番插件化相关模块和代码,以源库版本 1.3 为例:
扩展加载 extensionLoader
首先是扩展实体类,使用密封类代表不同状态:
/**
* Created by HeYanLe on 2023/2/19 16:16.
* https://github.com/heyanLE
*/
sealed class Extension {
abstract val label: String
abstract val pkgName: String
abstract val versionName: String
abstract val versionCode: Long
abstract val libVersion: Int
abstract val readme: String?
abstract val icon: Drawable?
data class Installed(
override val label: String,
override val pkgName: String,
override val versionName: String,
override val versionCode: Long,
override val libVersion: Int,
override val readme: String?,
override val icon: Drawable?,
val sources: List<Source>,
val resources: Resources?,
): Extension()
data class InstallError(
override val label: String,
override val pkgName: String,
override val versionName: String,
override val versionCode: Long,
override val libVersion: Int,
override val readme: String?,
override val icon: Drawable?,
val exception: Exception?,
val errMsg: String,
): Extension()
}
相关代码位于 :extension:extension-core 模块中 com.heyanle.extension_load.ExtensionLoader 路径,首先是常量:
/**
* Created by HeYanLe on 2023/2/21 21:50.
* https://github.com/heyanLE
*/
object ExtensionLoader {
private const val EXTENSION_FEATURE = "easybangumi.extension"
private const val METADATA_SOURCE_CLASS = "easybangumi.extension.source"
private const val METADATA_SOURCE_LIB_VERSION = "easybangumi.extension.lib.version"
private const val METADATA_README = "easybangumi.extension.readme"
// 当前容器支持的 扩展库 版本区间
private const val LIB_VERSION_MIN = 1
private const val LIB_VERSION_MAX = 2
private const val PACKAGE_FLAGS =
PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNATURES
}
其中 metadata 为拓展 apk 中 AndroidManifest 文件相关数据,例如:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-feature android:name="easybangumi.extension" android:required="true" />
<application
android:allowBackup="true"
android:icon="@mipmap/app_logo"
android:label="[APP 名称]"
android:supportsRtl="true">
<!--readme-->
<meta-data
android:name="easybangumi.extension.readme"
android:value="[扩展描述]" />
<!--libVersion-->
<meta-data
android:name="easybangumi.extension.lib.version"
android:value="1" />
<!--source-->
<meta-data
android:name="easybangumi.extension.source"
android:value="com.heyanle.easybangumi_extension.EasySourceFactory"/>
<!--为了让本体能找到需要加-->
<activity android:name="com.heyanle.extension_api.NoneActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.ANSWER" />
<data android:host="com.heyanle.easybangumi"
android:scheme="source"/>
</intent-filter>
</activity>
</application>
<queries>
<package android:name="com.heyanle.easybangumi" />
</queries>
</manifest>
然后是加载扩展代码:
/**
* Created by HeYanLe on 2023/2/21 21:50.
* https://github.com/heyanLE
*/
object ExtensionLoader{
/**
* 获取扩展列表
*/
@SuppressLint("QueryPermissionsNeeded")
fun getAllExtension(context: Context): List<Extension> {
val pkgManager = context.packageManager
val installedPkgs =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
pkgManager.getInstalledPackages(PackageManager.PackageInfoFlags.of(PACKAGE_FLAGS.toLong()))
} else {
pkgManager.getInstalledPackages(PACKAGE_FLAGS)
}
val extPkgs = installedPkgs.filter {
it.packageName.loge("ExtensionLoader")
isPackageAnExtension(it) }
if (extPkgs.isEmpty()) return emptyList()
return extPkgs.map {
// 加载
innerLoadExtension(context,pkgManager, it.packageName)
}.filterIsInstance<Extension>()
}
}
这里使用 packageManager 获取当前设备所有安装的 app 列表后使用 map 流式处理调用 innerLoadExtension 方法加载成 Extension 对象。其中 innerLoadExtension 方法:
/**
* Created by HeYanLe on 2023/2/21 21:50.
* https://github.com/heyanLE
*/
object ExtensionLoader{
fun innerLoadExtension(
context: Context, pkgManager: PackageManager, pkgName: String,
): Extension? {
return kotlin.runCatching {
val pkgInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
pkgManager.getPackageInfo(
pkgName, PackageManager.PackageInfoFlags.of(PACKAGE_FLAGS.toLong())
)
} else {
pkgManager.getPackageInfo(pkgName, PACKAGE_FLAGS)
}
val appInfo = pkgManager.getApplicationInfo(pkgInfo.packageName, PackageManager.GET_META_DATA)
val classLoader = PathClassLoader(appInfo.sourceDir, null, context.classLoader)
val extName =
pkgManager.getApplicationLabel(appInfo).toString().substringAfter("EasyBangumi: ")
val versionName = pkgInfo.versionName
val versionCode = PackageInfoCompat.getLongVersionCode(pkgInfo)
// Validate lib version
val libVersion = appInfo.metaData.getInt(METADATA_SOURCE_LIB_VERSION)
val readme = appInfo.metaData.getString(METADATA_README)
// 库版本管理
if (libVersion < LIB_VERSION_MIN) {
"Lib version is ${libVersion}, while only versions " + "${LIB_VERSION_MIN} to ${LIB_VERSION_MAX} are allowed".loge("ExtensionLoader")
return Extension.InstallError(
label = extName,
pkgName = pkgInfo.packageName,
versionName = versionName,
versionCode = versionCode,
libVersion = libVersion,
readme = readme,
icon = kotlin.runCatching { pkgManager.getApplicationIcon(pkgInfo.packageName) }
.getOrNull(),
errMsg = "拓展版本过旧",
exception = null,
)
}
if (libVersion > LIB_VERSION_MAX) {
"Lib version is ${libVersion}, while only versions " + "${LIB_VERSION_MIN} to ${LIB_VERSION_MAX} are allowed".loge("ExtensionLoader")
return Extension.InstallError(
label = extName,
pkgName = pkgInfo.packageName,
versionName = versionName,
versionCode = versionCode,
libVersion = libVersion,
readme = readme,
icon = kotlin.runCatching { pkgManager.getApplicationIcon(pkgInfo.packageName) }
.getOrNull(),
errMsg = "纯纯看番本体 APP 版本过旧",
exception = null,
)
}
// 获取工厂
val sources = (appInfo.metaData.getString(METADATA_SOURCE_CLASS) ?: "").split(";").map {
val sourceClass = it.trim()
if (sourceClass.startsWith(".")) {
pkgInfo.packageName + sourceClass
} else {
sourceClass
}
}.flatMap {
try {
// 使用 pathClassLoader 加载
when (val obj = Class.forName(it, false, classLoader).newInstance()) {
// 构造 source 对象
is Source -> listOf(obj)
is SourceFactory -> obj.create()
else -> throw Exception("Unknown source class type! ${obj.javaClass}")
}
} catch (e: Exception) {
"Extension load error: ${extName}".loge("ExtensionLoader")
e.printStackTrace()
return Extension.InstallError(
label = extName,
pkgName = pkgInfo.packageName,
versionName = versionName,
versionCode = versionCode,
libVersion = libVersion,
readme = readme,
icon = kotlin.runCatching { pkgManager.getApplicationIcon(pkgInfo.packageName) }
.getOrNull(),
errMsg = "加载异常",
exception = e,
)
}
}
.map {
// 如果是 扩展的源,则在 key 中加入 包名,避免重复
if(it is ExtensionSource) {
it.packageName = pkgInfo.packageName
}
it
}
return Extension.Installed(
label = extName,
pkgName = pkgInfo.packageName,
versionName = versionName,
versionCode = versionCode,
libVersion = libVersion,
readme = readme,
// 头像处理后面说吗
icon = kotlin.runCatching { pkgManager.getApplicationIcon(pkgInfo.packageName) }
.getOrNull(),
sources = sources,
resources = pkgManager.getResourcesForApplication(appInfo),
)
}.getOrElse {
it.printStackTrace()
null
}
}
private fun isPackageAnExtension(pkgInfo: PackageInfo): Boolean {
return pkgInfo.reqFeatures.orEmpty().any { it.name == EXTENSION_FEATURE }
}
}
其中关于源头像和扩展源后面说明,这里主要是加载 app 对应 mate 数据,然后进行库版本检查,获取源工厂创建等操作。
扩展管理 extensionController
上面介绍的 Loader 只是加载扩展,最终会返回一个 List<Extension>
对象,而所有扩展的管理代码在同模块下的ExtensionController,先来看看整体架构:
/**
* Created by HeYanLe on 2023/2/21 22:22.
* https://github.com/heyanLE
*/
object ExtensionController {
// 当前源加载状态定义
sealed class ExtensionState {
data object None : ExtensionState()
data object Loading : ExtensionState()
class Extensions(
val extensions: List<Extension>
) : ExtensionState()
}
// 使用 mutableFlow 维护状态
private val _installedExtensionsFlow = MutableStateFlow<ExtensionState>(
ExtensionState.None
)
val installedExtensionsFlow = _installedExtensionsFlow.asStateFlow()
// 广播是否初始化
private val receiverInit = AtomicBoolean(false)
// 协程上下文
private val loadScope = MainScope()
@Volatile
private var lastJob: Job? = null
// 初始化
fun init(context: Context) {
if (receiverInit.compareAndSet(false, true)) {
ExtensionInstallReceiver(ReceiverListener()).register(context)
}
innerLoad(context)
}
// 加载
private fun innerLoad(context: Context){
lastJob?.cancel()
lastJob = loadScope.launch (Dispatchers.IO){
_installedExtensionsFlow.update {
ExtensionState.Loading
}
// 添加 挂起点
yield()
val extensions = ExtensionLoader.getAllExtension(context)
yield()
if(isActive){
_installedExtensionsFlow.update {
ExtensionState.Extensions(extensions)
}
}
}
}
// 收到软件安装或卸载广播后直接重新加载
class ReceiverListener : ExtensionInstallReceiver.Listener {
override fun onExtensionInstalled(context: Context, pkgName: String) {
innerLoad(context)
}
override fun onExtensionUpdated(context: Context, pkgName: String) {
innerLoad(context)
}
override fun onPackageUninstalled(context: Context, pkgName: String) {
innerLoad(context)
}
}
}
还是比较好理解的,然后是广播相关:
首先是软件安装卸载的广播接收抽取,位于 com.heyanle.extension_load.ExtensionInstallReceiver
/**
* Created by HeYanLe on 2023/2/19 22:41.
* https://github.com/heyanLE
*/
class ExtensionInstallReceiver (private val listener: Listener):
BroadcastReceiver(){
/**
* Registers this broadcast receiver
*/
fun register(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
context.registerReceiver(this, filter, Context.RECEIVER_EXPORTED)
}else{
context.registerReceiver(this, filter)
}
}
/**
* Returns the intent filter this receiver should subscribe to.
*/
private val filter
get() = IntentFilter().apply {
addAction(Intent.ACTION_PACKAGE_ADDED)
addAction(Intent.ACTION_PACKAGE_REPLACED)
addAction(Intent.ACTION_PACKAGE_REMOVED)
addDataScheme("package")
}
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
Intent.ACTION_PACKAGE_ADDED -> {
getPackageNameFromIntent(intent)?.let {
listener.onExtensionInstalled(context, it)
}
}
Intent.ACTION_PACKAGE_REPLACED -> {
getPackageNameFromIntent(intent)?.let {
listener.onExtensionUpdated(context, it)
}
}
Intent.ACTION_PACKAGE_REMOVED -> {
getPackageNameFromIntent(intent)?.let {
listener.onPackageUninstalled(context, it)
}
}
}
}
/**
* Returns the package name of the installed, updated or removed application.
*/
private fun getPackageNameFromIntent(intent: Intent?): String? {
return intent?.data?.encodedSchemeSpecificPart ?: return null
}
/**
* Listener that receives extension installation events.
*/
interface Listener {
fun onExtensionInstalled(context: Context, pkgName: String)
fun onExtensionUpdated(context: Context, pkgName: String)
fun onPackageUninstalled(context: Context, pkgName: String)
}
}
源抽象 source-api
source-api 模块位于源码 :source:soure-api 路径,该模块主要是相关的接口,具体有三部分:源接口,组件接口和实体类:
结果抽象
@Keep
sealed class SourceResult<T> {
data class Complete<T>(
val data: T
) : SourceResult<T>()
data class Error<T>(
val throwable: Throwable,
val isParserError: Boolean = false
) : SourceResult<T>()
inline fun complete(block: (SourceResult.Complete<T>) -> Unit): SourceResult<T> {
if (this is Complete) {
block(this)
}
return this
}
inline fun error(block: (SourceResult.Error<T>) -> Unit): SourceResult<T> {
if (this is Error) {
block(this)
}
return this
}
}
@Keep
suspend fun <T> withResult(
context: CoroutineContext = EmptyCoroutineContext,
block: suspend () -> T
): SourceResult<T> {
return try {
withContext(context) {
SourceResult.Complete(block())
}
} catch (e: ParserException) {
e.printStackTrace()
SourceResult.Error<T>(e, true)
} catch (e: Exception) {
e.printStackTrace()
SourceResult.Error<T>(e, false)
}
}
@Keep
class ParserException(
override val message: String?
) : Exception()
其他业务接口就不一一介绍了。
源装配 sourceController
com.heyanle.easybangumi4.source.SourceController 大体结构:
/**
* Created by HeYanLe on 2023/8/27 15:35.
* https://github.com/heyanLE
*/
class SourceController(
// 配置注入
private val sourcePreferences: SourcePreferences,
// 迁移业务注入
private val migrationController: SourceMigrationController,
) {
// 使用单线程线程池保证有序性
private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
private val scope = CoroutineScope(SupervisorJob() + dispatcher)
// 其中 SourcePreferences.LocalSourceConfig 是源配置,包括开关,排序等
private val _sourceLibraryFlow =
MutableStateFlow<List<Pair<Source, SourcePreferences.LocalSourceConfig>>>(emptyList())
val sourceLibraryFlow = _sourceLibraryFlow.asStateFlow()
// 源当前状态
sealed class SourceState {
data object None : SourceState()
data object Loading : SourceState()
data class Migrating(
val source: List<Source>,
) : SourceState()
data class Completely(
val sourceBundle: SourceBundle,
) : SourceState()
}
private val _sourceState = MutableStateFlow<SourceState>(SourceState.None)
val sourceState = _sourceState.asStateFlow()
// 新配置
fun newConfig(map: Map<String, SourcePreferences.LocalSourceConfig>) {
sourcePreferences.configs.set(map)
}
fun newConfig(config: SourcePreferences.LocalSourceConfig){
val map = sourcePreferences.configs.get().toMutableMap()
map[config.key] = config
sourcePreferences.configs.set(map)
}
}
有两个 flow,其中 sourceLibraryFlow 维护源与其对应用户配置的状态,sourceState 维护当前 sourceBundle 的状态。
然后是使用 combine 处理数据流动:
/**
* Created by HeYanLe on 2023/8/27 15:35.
* https://github.com/heyanLE
*/
class SourceController(
private val sourcePreferences: SourcePreferences,
private val migrationController: SourceMigrationController,
) {
init {
scope.launch {
// 将源用户配置,迁移的 source 与 安装的扩展 三个 flow 进行 combine
combine(
migrationController.migratingSource.stateIn(scope),
sourcePreferences.configs.stateIn(scope = scope),
ExtensionController.installedExtensionsFlow
) { migrating, configs, extensionState ->
// 迁移中作为一个单独状态
if (migrating.isNotEmpty()) {
return@combine SourceState.Migrating(migrating.toList())
}
// 根据扩展状态进行处理
when (
extensionState
) {
is ExtensionController.ExtensionState.Extensions -> {
val sources =
extensionState.extensions.filterIsInstance<Extension.Installed>()
.flatMap {
it.sources
}
// 先找出需要迁移的
val migrationSources = sources.filter {
migrationController.needMigrate(it)
}
// 调用 realMap 整理用户配置,并更新到 sourceLibraryFlow
val rc = realConfig(sources, configs)
val l = sources.flatMap {
val config =
rc[it.key]
?: return@flatMap emptyList<Pair<Source, SourcePreferences.LocalSourceConfig>>()
listOf(it to config)
}.sortedBy { it.second.order }
_sourceLibraryFlow.update {
l
}
if (migrationSources.isNotEmpty()) {
// 进行迁移
migrationController.migration(migrationSources)
SourceState.Migrating(migrationSources)
} else {
// 加载成功,根据开关和排序装配到 SourceBundle
SourceState.Completely(SourceBundle(sources.filter {
rc[it.key]?.enable ?: true
}.sortedBy {
rc[it.key]?.order ?: Int.MAX_VALUE
}))
}
}
// 加载中透传
is ExtensionController.ExtensionState.Loading -> {
SourceState.Loading
}
else -> {
SourceState.None
}
}
}.collectLatest { state ->
// 更新到 _sourceState
state.loge(TAG)
_sourceState.update {
state
}
}
}
}
// 整理配置和源
private fun realConfig(
list: List<Source>,
current: Map<String, SourcePreferences.LocalSourceConfig>,
): Map<String, SourcePreferences.LocalSourceConfig> {
val cacheList = hashMapOf<String, Source>()
// 先去重,如果 source 的 key 一致则按照 versionCode 高的加载
list.forEach {
if ((cacheList[it.key]?.versionCode ?: -1) < it.versionCode) {
cacheList[it.key] = it
}
}
val configs = current.toMutableMap()
// 为新增的源新增配置实体
cacheList.iterator().forEach {
if (!configs.containsKey(it.key)) {
// 未排序状态
configs[it.key] = SourcePreferences.LocalSourceConfig(it.key, true, Int.MAX_VALUE)
}
}
return configs
}
}
可以看到最终提供给业务的是一个 SourceBundle 状态,其维护了一系列 key 到 component 的映射,还是比较简单的
/**
* Created by HeYanLe on 2023/2/22 20:41.
* https://github.com/heyanLE
*/
class SourceBundle(
list: List<Source>
) {
companion object {
val NONE = SourceBundle(emptyList())
}
// 使用 linkedMap 保证有序
private val sourceMap = linkedMapOf<String, Source>()
private val configMap = linkedMapOf<String, ConfigComponent>()
private val iconMap = linkedMapOf<String, IconSource>()
private val playMap = linkedMapOf<String, PlayComponent>()
private val pageMap = linkedMapOf<String, PageComponent>()
private val searchMap = linkedMapOf<String, SearchComponent>()
private val detailedMap = linkedMapOf<String, DetailedComponent>()
private val updateMap = linkedMapOf<String, UpdateComponent>()
//private val migrateMap = linkedMapOf<String, MiSou>()
init {
list.forEach {
register(it)
}
}
private fun register(source: Source) {
if (!sourceMap.containsKey(source.key)
|| sourceMap[source.key]!!.versionCode < source.versionCode
) {
sourceMap[source.key] = source
if (source is IconSource) {
iconMap[source.key] = source
}else{
iconMap.remove(source.key)
}
playMap.remove(source.key)
pageMap.remove(source.key)
searchMap.remove(source.key)
detailedMap.remove(source.key)
updateMap.remove(source.key)
configMap.remove(source.key)
val components = arrayListOf<Component>()
(source as? Component)?.let {
components.add(it)
}
components.addAll(source.components())
components.forEach {
if (it is PlayComponent) {
it.loge("SourceBundle")
playMap[it.source.key] = it
}
if(it is PageComponent) {
pageMap[it.source.key] = it
}
if(it is SearchComponent) {
searchMap[it.source.key] = it
}
if(it is DetailedComponent) {
it.loge("SourceBundle")
detailedMap[it.source.key] = it
}
if(it is UpdateComponent) {
updateMap[it.source.key] = it
}
if(it is ConfigComponent) {
configMap[it.source.key] = it
}
}
}
}
fun sources(): List<Source> {
val res = ArrayList<Source>()
res.addAll(sourceMap.values)
return res
}
fun source(key: String): Source? {
return sourceMap[key]
}
fun page(key: String): PageComponent? {
return pageMap[key]
}
fun pages(): List<PageComponent> {
return pageMap.toList().map {
it.second
}
}
fun search(key: String): SearchComponent? {
return searchMap[key]
}
fun searches(): List<SearchComponent> {
return searchMap.toList().map {
it.second
}
}
fun config(key: String): ConfigComponent? {
return configMap[key]
}
fun icon(key: String): IconSource? {
return iconMap[key]
}
fun play(key: String): PlayComponent? {
key.loge("SourceBundle")
return playMap[key]
}
fun detailed(key: String): DetailedComponent? {
key.loge("SourceBundle")
return detailedMap[key]
}
fun update(key: String): UpdateComponent? {
return updateMap[key]
}
fun empty(): Boolean {
return sourceMap.isEmpty()
}
}
然后是 getter 抽取,如果业务只想要简单获取,直接注入 SourceStateGetter 即可,不需要注入 SourceController,这里使用了 Injket,具体可以看技术积累篇:
/**
* Created by heyanlin on 2023/10/2.
*/
class SourceStateGetter(
private val sourceController: SourceController,
) {
// 阻塞到下一个番剧源就绪状态
suspend fun awaitBundle(): SourceBundle {
return sourceController.sourceState.filterIsInstance<SourceController.SourceState.Completely>().first().sourceBundle
}
fun flowBundle(): Flow<SourceBundle> {
return sourceController.sourceState.filterIsInstance<SourceController.SourceState.Completely>().map { it.sourceBundle }
}
fun flowState(): StateFlow<SourceController.SourceState> {
return sourceController.sourceState
}
}
源迁移 SourceMigrationController
首先是相关抽象,如果源支持迁移,则 Source 需要实现 MigrationSource 接口,CartoonSummary 是一部番的最少标识,由 url,id,source 三个联合主键组成。实际上需要迁移的场景只有用户收藏的番剧,并且插件中只需要提供标识的迁移,后续本体会根据新的摘要再次调用 Componment 读取新的数据进行存储。
/**
* Created by HeYanLe on 2023/8/5 17:22.
* https://github.com/heyanLE
*/
interface MigrateSource: Source {
/**
* @param oldVersionCode 旧版的 source version code
* @return 是否迁移番剧
*/
fun needMigrate(oldVersionCode: Int): Boolean {
return false
}
/**
* 迁移后会重新调用 detailedComponent 获取新的详情
* 如果迁移到一半用户杀进程,则再次启动会再次触发迁移,此时可能会传入部分已迁移的数据
* @param oldCartoonSummaryList 旧版中收藏番的摘要
* @param oldVersionCode 旧版的 source version code
* @return 迁移后的 map
*/
suspend fun onMigrate(oldCartoonSummaryList: List<CartoonSummary>, oldVersionCode: Int): List<CartoonSummary> {
throw IllegalStateException("source no onMigrate: (${this})")
}
}
除此之外还有用户配置更新,这里的源配置不是之前的源配置,而是源本身支持的配置,例如有的源支持用户自己指定 host 等。因为这个配置本身是可选实现,因此放到 Component 中:
/**
* Created by HeYanLe on 2023/8/4 22:56.
* https://github.com/heyanLE
*/
@Keep
interface ConfigComponent : Component {
// 获取配置列表
fun configs(): List<SourceConfig>
/**
* @param oldVersionCode 旧版的 source version code
* @return 是否需要触发配置更新
*/
fun needMigrate(oldVersionCode: Int): Boolean {
return false
}
/**
* 当 needMigrate 返回 true 时会调用该方法更新配置
* @param oldMap 旧版数据的 map
* @param oldVersionCode 旧版的 source version code
* @return 迁移后的 map
*/
fun onMigrate(oldMap: Map<String, String>, oldVersionCode: Int): Map<String, String> {
return if (needMigrate(oldVersionCode)) {
throw IllegalStateException("source config need migrate but no onMigrate: (${source})")
} else {
oldMap
}
}
}
然后先来看看 SourceMigrationController 代码:其中
/**
* 源更新配置
* Created by HeYanLe on 2023/8/5 20:14.
* https://github.com/heyanLE
*/
class SourceMigrationController(
private val context: Application,
private val sourcePreferences: SourcePreferences,
private val cartoonStarDao: CartoonStarDao,
) {
// 单线程线程池
private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
private val scope = CoroutineScope(SupervisorJob() + dispatcher)
// 迁移中的源 set
private val _migratingSource: MutableStateFlow<Set<Source>> = MutableStateFlow(emptySet())
val migratingSource = _migratingSource.asStateFlow()
// log,用于调试
private val _logs: MutableStateFlow<List<String>> = MutableStateFlow(emptyList())
val logs = _logs.asStateFlow()
fun migration(
list: List<Source>,
) {
list.forEach {
scope.launch {
innerMigration(it)
}
}
}
fun needMigrate(source: Source): Boolean {
// 之前加载源的版本
val vp = sourcePreferences.getLastVersion(source)
// 源没支持迁移
if (source !is MigrateSource) {
return false
}
// 源表示不用迁移
if (!source.needMigrate(vp.get())) {
return false
}
return true
}
// 迁移实现
private suspend fun innerMigration(
source: Source
) {
if (_migratingSource.value.contains(source)) {
return
}
// 源没支持迁移
if (source !is MigrateSource) {
return
}
// 源表示不用迁移
val vp = sourcePreferences.getLastVersion(source)
if (!source.needMigrate(vp.get())) {
vp.set(source.versionCode)
return
}
// 这里跳过 sourceBundle 直接从 source 对象获取 component
val detailed = source.components().filterIsInstance<DetailedComponent>().firstOrNull()
val config = source.components().filterIsInstance<ConfigComponent>().firstOrNull()
if (detailed == null && config == null) {
return
}
// 开始迁移
_migratingSource.update {
it + source
}
log("${source.label}@${source.key} 开始数据升级 ${vp.get()} -> ${source.versionCode}")
if (detailed != null) {
log("收藏番剧数据升级开始")
// 收藏迁移
val stars = cartoonStarDao.getAllBySource(source.key)
val summaries = stars.map {
CartoonSummary(it.id, it.source, it.url)
}
// 切换新的 summary
val newSummaries = source.onMigrate(summaries, vp.get())
log("summary 更新 ${summaries.size} -> ${newSummaries.size}")
log("开始拉取番剧详细信息")
val newStars = newSummaries.flatMap {
val res = detailed.getAll(it)
when (res) {
is SourceResult.Complete -> {
log("拉取成功 ${it.id} ${res.data.first.title}")
listOf(
CartoonStar.fromCartoon(
res.data.first,
source.label,
res.data.second,
stars.find { it.toIdentify() == res.data.first.toIdentify() }?.tags?:""
)
)
}
else -> {
log("拉取失败 ${it.id}")
emptyList<CartoonStar>()
}
}
}
log("更新数据库 ${stars.size} -> ${newStars.size}")
cartoonStarDao.migration(stars, newStars)
}
if (config != null) {
// 源配置的迁移
log("开始源配置迁移")
// 源配置迁移
val hekv = SourcePreferenceHelper.of(context, source).hekv()
val oldMap = hekv.map()
val newMap = config.onMigrate(oldMap, vp.get())
oldMap.iterator().forEach {
hekv.remove(it.key)
}
newMap.iterator().forEach {
hekv.put(it.key, it.value)
}
}
vp.set(source.versionCode)
_migratingSource.update {
it - source
}
log("${source.label}@${source.key} 数据升级完成 ${source.versionCode}")
}
fun clear() {
_logs.update {
emptyList()
}
}
private fun log(log: String) {
_logs.update {
it + log
}
}
}
至此,插件后底层建筑已经设置完毕,现在业务可以自己更新 Component 接口并使用 getter 获取 bundle 进而加载插件里提供的 Component 接口。