【Android 开发典型场景设计】01. 初始化三方 SDK,支持状态监听与任务队列

244 阅读4分钟

经验是昂贵的老师,但愚人只能从经验学习。——《人月神话》

场景设计:依赖的三方 SDK 需要全局初始化

设想如下场景,在进行 Android 功能开发时,需要引入一个第三方 SDK,暂且将其命名为 AILib,即提供 AI 能力的组件。

在使用 SDK 的具体能力之前,通常要对其进行初始化,且在应用的全部生命进程内,有且仅有一次初始化。

AILib 的接口可以使用伪代码描述如下:

// AILib 接口

class AILib {
    fun initialize(context: Context) // ===> 初始化,可能需要传入 ApplicationContext
    
    fun askQuestion() {} // ===> 无返回值的接口
    
    fun askQuestionAndGetAnswer(): Answer {} // ===> 有返回值的接口   
}

该 SDK 进行初始化时,可能需要传入 Context 对象,此时必然应当是 ApplicationContext,因为 SDK 的生命周期是全应用。如果一个 SDK 要求跟随进程初始化,但初始化依赖 ActivityContext,这种设计是有问题的。

Application.onCreate() 中进行全局唯一初始化

对于全局唯一初始化,通常在进程启动时进行,由于 AI 能力并非应用强需求,在启动任务列表里属于 无依赖关系,且可以异步进行 那一类,故采用 IO线程异步初始化 的策略。

对此我们采用 Kotlin 提供的 object 单例 AILibManager,实现 全局唯一 的特性。

// 全局初始化,IO 异步
// ==================== Application 初始化核心 ====================
class MyApplication : Application() {

    override fun onCreate() {
        super.onCreate()
        // 在后台线程启动初始化(非阻塞式)
        CoroutineScope(Dispatchers.IO).launch {
            AILibManager.init(applicationContext)
        }
    }
}

// ==================== 库管理核心类,封装了原始的 AILib ====================
object AILibManager {
    private const val TAG = "AILibManager"
    
    private const val INIT_TIMEOUT = 5000L // 5秒超时
    // 使用原子变量保证可见性
    private val isInitialized = AtomicBoolean(false)
    private val mutex = Mutex() // 加锁防止重复进入初始化流程
    private var aiLib : AILib? = null

    // 异步初始化入口
    suspend fun init(context: Context) {
        if (!isInitialized.get()) {
            withTimeoutOrNull(INIT_TIMEOUT) {
                aiLib = AILib().apply { init(context) } // 实际初始化调用
            }?.also { isInitialized.set(true) } // 超时时间内完成,设置状态位
    }
    
    // 对外提供检测初始化状态功能
    fun checkInitialized() = isInitialized.get()

    // 带安全校验的功能调用
    fun <T> callWithValidation(block: LibA.() -> T): T {
        if (!isInitialized.get()) {
            throw IllegalStateException("libA must be initialized first")
        }
        return libA.block()
    }

}

变体1:对外开放监听初始化进度监听

如果在初始化任务尚未完成时,用户就进入到了依赖 AILib 能力的界面,并且尝试调用 AI 相关能力。此时应当在页面展示 Loading 状态,并且监听初始化完成后的广播。

是的,对此的第一反应是初始化完成后发送广播,在新页面先判断 checkInitialized() 如果未完成则显示 Loading 且监听广播。

但 Kotlin 提供了更优雅的实现方法,用来监听状态 —— StateFlow,它自带广播特性,且能保存最近一次的状态。

在上述代码里补充状态监控功能:

object AILibManager {
    ...
    // 不再需要 isInitialized 标志位
    
    private val _initState = MutableStateFlow<InitState>(InitState.Idle)
    val initState: StateFlow<InitState> = _initState // 开放给外部观测

    // 异步初始化入口
    suspend fun init(context: Context) {
        if (_initState.value != InitState.Idle) return
        
        _initState.value = InitState.Processing // 设置为“初始化中”
        
        // 执行初始化,也可增加超时逻辑
        try {
            aiLib = AILib().apply { init(context) }
            _initState.value = InitState.Success
        } catch (e: Exception) {
            _initState.value = InitState.Failed(e)
        }
    }
    
    // 状态密封类
    sealed class LibState {
        object Idle : LibAState()
        object Processing : LibAState()
        object Success : LibAState()
        data class Failed(val exception: Throwable) : LibAState()
    }
}

BaseActivity 中监测 AILib 初始化进程:

abstract class BaseActivity : AppCompatActivity() {
    // 初始化状态观察
    private fun setupLibAStateObserver() {
        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                AILibManager.initState.collect { state ->
                    when (state) {
                        AILibManager.LibState.Success -> onLibAReady()
                        is AILibManager.LibState.Failed -> showError(state.exception)
                        AILibManager.LibState.Processing -> showLoading()
                        AILibManager.LibState.Idle -> Unit
                    }
                }
            }
        }
    }

    override fun onCreate() {
        super.onCreate()
        setupLibAStateObserver()
    }

    abstract fun onAILibReady()
}

变体2:将初始化未完成时的接口调用,加入缓存待执行队列

上述代码里,如果 AILib 未初始化完成,是通过抛出 IllegalStateException 来通知调用者的。通常是由调用方自行控制重试。

如果设计 AILib 时,已经明确,初始化就是会耗费很长时间,并且需要将初始化阶段的任务缓存起来,待初始化完成后顺序执行。此时可以设计一个 pendingTasks 队列,用于存放初始化完成之前到达的任务。

个人并不推荐进行此类任务管理,这样强行附加给调用方失败和重试的逻辑,导致代码冗余度上升。SDKManager 理应只负责执行最基础的初始化任务,并将初始化进度和状态暴露给外界即可。

object AISdkManager {
    @Volatile
    private var isInitialized = false // 用 Volatile 或者是 AtomicBoolean
    
    // 并发队列,对象为高阶函数
    private val pendingTasks = CocurrentLinkedQueue<() -> Unit>()

    // 初始化入口(支持同步或异步调用)
    fun initialize(context: Context) {
        synchronized(this) {
            if (isInitialized) return
            appContext = context.applicationContext
            // 模拟耗时初始化操作(例如网络请求)
            ThirdPartySDK.init(appContext) // 假设 SDK 初始化是同步的
            isInitialized = true
            // 执行队列中的任务并清空
            drainQueue()
        }
    }

    // 执行并清空队列
    private fun drainQueue() {
        while (true) {
            val task = taskQueue.poll() ?: break
            task.invoke()
        }
    }
    
    // 通用执行方法(线程安全)
    fun execute(task: () -> Unit) {
        if (isInitialized) {
            task()
        } else {
            synchronized(this) { // 互斥锁,防止在初始化后清空队列时,继续向队列里添加
                if (isInitialized) {
                    task()
                } else {
                    taskQueue.add(task)
                }
            }
        }
    }
}