启动框架分阶段初始化:background/activity 两类 StartType 的设计与实践

Android 应用启动通常经历进程创建、Application 初始化、首个 Activity 创建、首帧绘制、首屏数据加载等阶段。早期工程常见做法是在 Application 的 onCreate 中直接调用一串初始化方法,业务增长后问题逐渐暴露:任务顺序靠人工维护,耗时难以统计,依赖关系不透明,主线程和后台任务混在一起。某个 SDK 初始化耗时增加,可能直接影响冷启动;某个业务模块新增初始化,也可能无意中提前拉起大量类和资源。

我们项目里把启动链路拆成三层来看。第一层在各 App 壳的 Application 中注入品牌、包名、版本、站点类型等差异化信息;第二层在 AppCommon 的 MainApplication 中处理进程判断、站点配置、语言包装、Firebase 延迟初始化、前后台生命周期注册;第三层由 AppStartUpManager 统一编排基础设施启动任务。这里最值得讲的,是 StartType.TYPE_BACKGROUNDStartType.TYPE_ACTIVITY 的边界。

一个容易被忽略的关键判断

Application 启动不等于用户主动打开应用。进程可能因为推送、系统恢复、内容提供者、后台任务、动态模块服务等原因被拉起,此时如果直接跑完整前台初始化,会浪费资源,甚至让某些依赖 Activity 的 SDK 在错误时机执行。

我们的处理思路是:在 attachBaseContext 阶段先判断是否主进程、是否”无 Activity 启动场景”;在 onCreate 阶段根据这个结果选择 TYPE_BACKGROUND 或等待首个非启动占位 Activity 创建后再切到 TYPE_ACTIVITY。这相当于把 Application 启动拆成”进程可用”和”界面可用”两个状态。

脱敏后的判断逻辑可以抽象为:

class MainApplication : Application() {
    private var mainProcess = false
    private var noVisibleEntry = false

    override fun attachBaseContext(base: Context) {
        mainProcess = ProcessInspector.isMainProcess(base)
        noVisibleEntry = LaunchInspector.isStartedWithoutActivity(base)

        if (mainProcess && !noVisibleEntry) {
            StartupPerf.markProcessStart()
        }
        super.attachBaseContext(wrapLocaleAndSite(base))
    }

    override fun onCreate() {
        super.onCreate()
        StartupManager.initProcessConfig(this)

        if (!mainProcess || noVisibleEntry) {
            StartupManager.start(this, StartType.BACKGROUND)
            return
        }

        registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacksAdapter() {
            override fun onActivityCreated(activity: Activity, state: Bundle?) {
                if (!activity.isPlaceholderLauncher()) {
                    StartupManager.start(this@MainApplication, StartType.ACTIVITY)
                }
            }
        })
    }
}

这段逻辑的重点不是具体 API,而是判断顺序:先识别进程,再识别是否真的进入前台,再决定启动任务集合。后台启动只做进程级最小能力,例如基础日志、必要配置、轻量网络安全组件;前台启动才补齐页面路由、预取、主题、调试面板、依赖 Activity 的三方能力。

任务模型:让依赖显式化

启动框架的核心是把初始化逻辑从”调用顺序”升级为”可调度模型”。每个任务声明自己的执行阶段、线程要求、依赖任务和降级策略:

enum class StartType { BACKGROUND, ACTIVITY }

interface StartupTask {
    val id: String
    val type: StartType
    val dependsOn: List<String>
    val threadMode: ThreadMode
    val required: Boolean

    fun run(context: StartupContext)
}

background 任务在 Application 阶段注册并启动,适合处理不需要页面上下文的工作,例如轻量配置读取、基础日志、崩溃保护、线程池准备。activity 任务在首个 Activity 可用后执行,适合处理窗口适配、页面观察、路由拦截器安装、需要 Activity Result 的能力。

调度器按阶段启动任务,先筛选当前 StartType,再根据依赖关系执行。对于后台线程任务,可以并发执行;对于主线程任务,需要排队并控制耗时:

class StartupScheduler(
    private val tasks: List<StartupTask>,
    private val executor: TaskExecutor,
    private val monitor: StartupMonitor
) {
    fun start(type: StartType, context: StartupContext) {
        val selected = tasks.filter { it.type == type }
        val sorted = topologicalSort(selected, tasks)

        sorted.forEach { task ->
            executor.run(task.threadMode) {
                monitor.trace(task.id, type) {
                    try {
                        task.run(context)
                    } catch (error: Throwable) {
                        handleFailure(task, error)
                    }
                }
            }
        }
    }

    private fun handleFailure(task: StartupTask, error: Throwable) {
        if (task.required) throw error
        monitor.recordIgnoredFailure(task.id, error)
    }
}

异常策略需要分层:崩溃采集、关键配置、合规相关初始化是强依赖;埋点增强、预加载、体验优化通常为弱依赖。弱依赖失败要记录,但不应阻止用户进入 App。

启动性能纳入埋点闭环

另一个值得强调的点是启动性能的可观测性。我们在 attachBaseContext 附近记录启动起点,在 Activity 生命周期中记录关键页面创建和首个 resume,再把 perf 事件交给统一埋点系统。这样”分阶段 + 可观测 + 主/子进程隔离 + 页面出现后补初始化”形成组合拳,而不是只做异步初始化。

任务注册也可以由模块提供,最后在宿主侧聚合。为了避免任务泛滥,我们要求每个任务声明预算原因:

@StartupBudget(maxCostMs = 20, reason = "路由表需要在首个页面交互前完成安装")
class RouteInstallTask : StartupTask { ... }

预算不是为了制造形式感,而是让新增启动任务必须说明价值和成本。后续性能回归时,团队也可以按预算快速定位异常任务。

落地中要注意的几点

不要把 background 理解为”随便放后台线程”。后台任务同样会占用 CPU、IO 和锁资源,过多并发可能影响首屏渲染。启动阶段的后台线程池需要限制并发数。

activity 任务要注意幂等。横竖屏切换、多窗口、深链启动、任务栈恢复都可能导致 Activity 生命周期多次触发。框架应该提供 startOnce 语义,任务自身也要能承受重复调用。

多进程场景要提前设计。任务可以声明适用进程,例如主进程、推送进程、独立 Web 进程。默认所有进程都执行完整启动任务,通常是性能和稳定性隐患。


分阶段初始化的本质,是把启动任务从”调用顺序”升级为”可调度模型”。它需要任务模型、依赖表达、线程调度、异常降级、耗时监控和评审纪律共同配合。只有这样,启动优化才不会停留在一次性清理,而能成为长期可维护的工程能力。