那个让我头疼的启动问题
事情要从我们公司的新项目说起。那时候我们的应用集成了好多第三方 SDK:推送、统计、地图、IM、广告...你懂的,现在的 App 不接十几个 SDK 都不好意思上线。
由于SDK初始化时间过长,导致Splash Activity首屏一直卡在白屏界面,还需要编写各种xml背景图进行填充,很是头疼。
最开始我的 Application 是这样的:
kotlin
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(应用无响应)
寻找解决方案的折腾过程
第一站:多线程方案
我首先想到的是用多线程:
kotlin
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 接口:
kotlin
interface Initializer<T> {
suspend fun init(app: Application, provider: DependenciesProvider): T
fun dependencies(): List<KClass<out 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 里统一监听初始化状态。
kotlin
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 先初始化再使用
- 统一加载状态:所有页面都有统一的加载体验
- 错误统一处理:初始化失败时有统一的错误处理机制
- 代码复用:每个 Activity 都不用自己写初始化监听逻辑
Startup.observe内部是使用LiveData实现的.目的是为了每个页面监听的时候都可以拿到初始化数据信息.
kotlin
open class Startup private constructor(
private val implementation: IStartup
) : IStartup {
companion object {
private val _isInitialized = MutableLiveData<StartupResult>(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)
}
}
隐私协议处理的巧妙设计
现在隐私合规要求很严格,很多 SDK 必须在用户同意隐私协议后才能初始化。我的框架也很好的解决了这个问题:
kotlin
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 秒
- 代码可维护性:新同事也能轻松添加新的初始化任务
- 稳定性:再也没有因为进程重启导致的崩溃
- 开发体验:调试时可以清楚看到每个任务的执行时间和依赖关系
怎么集成使用?
集成超级简单:
- 添加依赖:
kotlin
implementation("com.github.Dboy233:startup-coroutine:最新版本")
- 定义初始化任务:
kotlin
class MyInitializer : Initializer<Unit> {
//如果没有依赖可不重写此方法
override fun dependencies() = listOf(OtherInitializer::class)
override suspend fun init(app: Application, provider: DependenciesProvider) {
// 你的初始化代码
}
}
- 在 Application 中配置:
kotlin
val startup = Startup.Builder(this)
.add(MyInitializer())
.setDispatchers(StartupDispatchers.ExecuteOnIO)
.build()
- 让 Activity 继承 BaseActivity(或者自己实现初始化监听)
实际应用日志
text
--- 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秒优化到了1坤秒(2.5秒)。
效果还是立竿见影的,关键是它的初始化不占用UI渲染时间,让UI过度不出现卡顿白屏。
如果你在Log中看到了两次日志输出,不要紧张,它不是执行了两次,而仅仅只是打印两次。
vbscript
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
希望我的这个小工具能帮到你,也期待听到你的使用反馈!