那个让我头疼的启动问题
事情要从我们公司的新项目说起。那时候我们的应用集成了好多第三方 SDK:推送、统计、地图、IM、广告...你懂的,现在的 App 不接十几个 SDK 都不好意思上线。
由于SDK初始化时间过长,导致Splash Activity首屏一直卡在白屏界面,还需要编写各种xml背景图进行填充,很是头疼。
最开始我的 Application 是这样的:
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
// 一堆初始化代码
Push.init(this) // 推送
Analytics.init(this) // 统计
Map.init(this) // 地图
IM.init(this) // 即时通讯
Ad.init(this) // 广告
// ... 还有更多
// 自己的业务初始化
initDatabase()
initConfig()
initUser()
}
}
结果就是:
- 用户点开 App 要等 3-5 秒才能看到界面
- 所有初始化都在主线程排队执行,一个卡住后面全等着
- 代码越来越乱,新同事根本不敢动这块代码
- 测试经常报 ANR(应用无响应)
寻找解决方案的折腾过程
第一站:多线程方案
我首先想到的是用多线程:
val thread1 = thread { Push.init(this) } //kotlin函数
val thread2 = thread { Analytics.init(this) }
// 启动所有线程...
结果发现问题更多:
- 线程创建开销大
- 依赖关系处理起来特别麻烦
- 异常处理很头疼
- 代码变得超级复杂
第二站:Jetpack AppStartup
然后我发现了 Google 的 AppStartup,心想官方出品应该很靠谱吧!
用了一段时间发现:
- 它只是把初始化代码从 Application 移到了各个 Initializer 里
- 关键问题:所有初始化还是在主线程执行的!
- 启动时间根本没减少,只是代码看起来整齐了点
- 复杂的依赖关系写起来还是很麻烦
AppStartup 更像是一个代码组织工具,而不是性能优化工具。
灵光一现:为什么不用协程?
就在我快要放弃的时候,突然想到:我们项目已经在用 Kotlin 协程了,为什么不用协程来解决这个问题呢?
协程的轻量级、灵活的线程调度、简洁的异步表达,不正是我们需要的吗?
于是,我开始动手写 startup-coroutine。
框架设计
核心想法:把初始化当成有依赖关系的任务
借鉴App Startup,但是它是串行初始化,我呢允许并行初始化。
我想象中的理想状态是:
- 每个初始化任务都是独立的
- 可以声明它依赖哪些其他任务
- 没有依赖关系的任务可以并行执行
- 使用拓扑排序自动处理依赖关系,按正确顺序执行
于是就有了 Initializer 接口:
interface Initializer<T> {
suspend fun init(app: Application, provider: DependenciesProvider): T
fun dependencies(): Listout Initializer<*>>> = emptyList()
}
线程调度的灵活控制
我提供了几种预设的线程策略:
AllIO:全在 IO 线程,适合耗时任务ExecuteOnIO:IO 线程执行,主线程创建框架AllMain:全在主线程,只适合轻量任务Default:默认推荐,框架在 IO 线程,任务在主线程
这样大家可以根据自己项目的实际情况选择。由于协程的 suspend 函数可以灵活的切换调度线程,所以可以全部在 Main 线程执行,然后具体的处理业务自由使用 withContext(Dispatchers.IO){...} 来切换。
BaseActivity 的封装
这个设计源于我们遇到的一个真实问题:
问题场景: 用户正在使用 App,突然来了个电话或者切换到其他应用,这时候系统可能因为内存不足把我们的应用进程给杀掉了。当用户再切回来时,系统会重新创建 Application 和用户最后看到的 Activity。
这时候就出问题了: Application 构建完成后,执行 Startup 的初始化。由于 Startup 是协程框架,它不阻塞 UI,于是还没有等待 Startup 初始化结束 Activity 就开始创建了。此时 Activity 在 onCreate 时可能会使用那些还没重新初始化的 SDK,结果就是各种空指针异常和崩溃。
我的解决方案: 在 BaseActivity 里统一监听初始化状态。
abstract class BaseActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState)
observeStartup() // 关键的一行!
}
protected open fun observeStartup() {
Startup.observe(this) { result ->
when (result) {
StartupResult.Idle -> showLoading() // 还没初始化,显示加载
StartupResult.Success -> {
hideLoading()
onInitFinished() // 初始化完成,继续正常流程
}
is StartupResult.Failure -> {
hideLoading()
// 处理初始化失败
}
}
}
}
}
这样设计的好处:
隐私协议处理的巧妙设计
当前隐私合规要求极为严格,许多 SDK 必须在用户明确同意隐私协议后,方可执行初始化操作。我的框架通过精巧的设计完美解决了这一痛点:
class MyApp : Application() {
companion object {
private lateinit var startup: Startup
fun startInit() {
startup.start()
}
}
override fun onCreate() {
super.onCreate()
//轻量级数据存储推荐直接初始化,用于进行判断逻辑.
SpUtils.init(this)
//构建初始化任务列表.
val initializer = listOf(
AdMobInit(),
AppConfigInit(),
FlutterEngineInit(),
FistInitializer(),
IMInit(),
MapSdkInit(),
ExceptionInit()
)
//构建startup
startup = Startup.Builder(this)
.setDispatchers(StartupDispatchers.AllIO)
.setDebug(true)
.add(initializer)
.build()
//推荐!!!
//如果App跳过了手动初始化,就必须做判断,只要条件允许第二次初始化的时候
//一定在Application中直接执行.
//这里涉及到了一些进程重启逻辑.
if (SpUtils.getBoolean("isAgreePrivacy",false)) {
startInit()
}
}
}
class SplashActivity : BaseActivity() {
private fun checkPrivacy() {
if (用户已同意隐私协议) {
// 开始初始化
MyApp.startInit()
} else {
// 显示隐私协议弹窗
showPrivacyDialog {
// 用户同意后开始初始化
MyApp.startInit()
}
}
}
}
实际效果怎么样?
引入 startup-coroutine 之后,应用表现得到了显著提升:
- 启动时间:应用冷启动耗时从原来的 3-5 秒大幅优化至 1-2 秒,用户感知更加流畅。
- 代码可维护性:架构清晰,新加入的同事也能轻松理解并添加新的初始化任务,降低了维护成本。
- 稳定性:彻底解决了因进程重启导致的初始化状态丢失问题,再也没有出现相关崩溃。
- 开发体验:在调试阶段,可以清晰地观察到每个任务的执行耗时及其依赖关系,便于性能瓶颈定位。
怎么集成使用?
集成过程超级简单,只需遵循以下步骤:
- 添加依赖:
implementation("com.github.Dboy233:startup-coroutine:最新版本")
- 定义初始化任务:
class MyInitializer : Initializer<Unit> {
//如果没有依赖可不重写此方法
override fun dependencies() = listOf(OtherInitializer::class)
override suspend fun init(app: Application, provider: DependenciesProvider) {
// 你的初始化代码
}
}
- 在 Application 中配置:
val startup = Startup.Builder(this)
.add(MyInitializer())
.setDispatchers(StartupDispatchers.ExecuteOnIO)
.build()
- 让 Activity 继承 BaseActivity(或者自行实现初始化监听逻辑)
实际应用日志
--- Startup Coroutine Dependency Graph ---
FlutterEngineInit
FistInitializer
ExceptionInit
AppConfigInit
└─ FistInitializer
AdMobInit
└─ AppConfigInit
IMInit
└─ AppConfigInit
MapSdkInit
└─ AppConfigInit
----------------------------------------
--- Startup Coroutine Performance Summary ---
>> Total Time: 2538ms | Status: FAILED
>> Dispatchers Mode: All-IO
>> Individual Task Durations:
- FlutterEngineInit | 2503ms | Thread: DefaultDispatcher-worker-5
- IMInit | 2000ms | Thread: DefaultDispatcher-worker-5
- AdMobInit | 1832ms | Thread: DefaultDispatcher-worker-4
- MapSdkInit | 601ms | Thread: DefaultDispatcher-worker-4
- AppConfigInit | 102ms | Thread: DefaultDispatcher-worker-5
- FistInitializer | 0ms | Thread: DefaultDispatcher-worker-4
- ExceptionInit | -1ms | Thread: Error
>> Task time is sum : 7037 ms
通过打印初始化结果,可以直观看到使用协程框架后,整体初始化时间从 7 秒优化到了 2.5 秒(文中“1坤秒”为幽默表达,实指 2.5 秒)。
效果立竿见影,关键在于其初始化过程不占用 UI 渲染线程,从而避免了界面过渡时出现卡顿或白屏现象。
如果你在 Log 中看到了两次日志输出,无需紧张,这并非任务执行了两次,而仅仅是打印机制导致的重复输出。
Log.i("StartupCoroutine", logContent.toString())
println(logContent.toString())
因为在 Test 阶段默认 Log 不输出内容,所以代码中使用了 println 再次打印以确保可见性。你只需要过滤 StartupCoroutine 日志即可获取准确信息。
写在最后
结语与开源邀请
开发 startup-coroutine 的过程让我深刻体会到:好的框架不是凭空想象出来的,而是从真实的业务痛点中生长出来的。我现在把这个框架开源出来,一方面是希望帮助到有同样问题的开发者,另一方面也是想听听大家的想法,看看能不能一起把它做得更好。
如果你也在为应用启动优化而烦恼,不妨试试 startup-coroutine。如果你有任何建议或者发现了什么问题,欢迎来 GitHub 给我提 Issue 或者 PR。
项目地址: https://github.com/Dboy233/startup-coroutine
希望我的这个小工具能帮到你,也期待听到你的使用反馈!
open class Startup private constructor(
private val implementation: IStartup
) : IStartup {
companion object {
private val _isInitialized = MutableLiveData(StartupResult.Idle)
fun observe(owner: LifecycleOwner, observer: Observer<StartupResult>) {
_isInitialized.observe(owner, observer)
}
fun isInitialized() = _isInitialized.value != StartupResult.Idle
fun initializedResult() = _isInitialized.value
fun reset() {
_isInitialized.postValue(StartupResult.Idle)
}
internal fun markInitializedResult(result: StartupResult) {
_isInitialized.postValue(result)
}
}
Startup.observe



