纯纯看番-插件化实现

为了方便源的升级和维护,也为了能让第三方开发者一起加入进来实现更多的源,从 4.0.3 版本开始,纯纯看番更新了插件化,可以使用安装拓展apk的方式来新增和更新纯纯看番的番剧源。而在此之前对于源的更新和升级,都需要发布一个完整纯纯看番版本,比较麻烦。

序言

对于纯纯看番拓展的这种场景,是插件化中最简单的一种。实际上就是功能的接口在宿主 app,而插件里有功能接口的实现,最终宿主加载插件里的实现而以接口的形式调用和管理。这种方式不涉及代码热修复和四大组件占位等问题,只需类加载和反射即可。下面将介绍纯纯看番的插件后业务逻辑。

整体框架

架构图:

9d48b688af748b8620f9f2759f3ee166

分为三层,分别是业务层,番源层和拓展层。

  1. 拓展层负责加载用户安装的插件app的相关数据。具体为使用 PathClassLoader 加载插件 apk 中的代码。
  2. 番源层负责将拓展层获取的相关代码打包成源对象,具体流程为加载,迁移和装配。其中装配会根据用户的源配置(开关,排序等)来封装成 SourceBundle 供业务使用,该对象后面会介绍。
  3. 业务层为具体的业务,对于插件中为一个一个 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 路径,该模块主要是相关的接口,具体有三部分:源接口,组件接口和实体类:

image

结果抽象

@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 接口。