启动优化:冷启动与热启动、Application初始化优化、ContentProvider优化、启动黑白屏处理
启动优化,说白了就是让用户感觉「这App一点就开」。我见过太多App,冷启动要等两三秒,用户早没耐心了。今天咱们就把启动这件事彻底聊透。
冷启动 vs 热启动:到底差在哪?
先搞清楚这两个概念。冷启动,是进程从零开始创建。热启动,是进程还在后台,Activity只是重新走一遍生命周期。
我习惯这么记:冷启动是「从头再来」,热启动是「卷土重来」。
| 对比项 | 冷启动 | 热启动 |
|---|---|---|
| 进程是否存在 | 否 | 是 |
| Application是否重建 | 是 | 否 |
| 耗时瓶颈 | Application.onCreate + ContentProvider + 首帧渲染 | Activity.onCreate + 布局加载 |
| 优化重点 | 减少Application初始化工作量 | 减少Activity重建开销 |
你想想看,冷启动的每一步都是「硬骨头」。系统要加载类、初始化ContentProvider、执行Application.onCreate,然后才能走到Activity。每一步慢了,用户就多等一秒。
Application初始化优化:别把「全家桶」塞进去
我见过最夸张的项目,Application.onCreate里初始化了十几个SDK。什么推送、统计、地图、IM、广告……全堆在一起。结果冷启动直接飙到3秒。
我个人习惯的做法是:能懒加载就懒加载,能异步就异步。
核心原则:Application.onCreate里只做「必须同步完成」的事。其他统统往后挪。
哪些是必须同步的?比如崩溃收集、日志框架。这些需要在任何代码执行前就位。其他的,比如推送、广告、统计,完全可以放到首帧渲染之后。
代码示例:
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
// 必须同步:崩溃收集
CrashReport.init(this)
// 必须同步:日志框架
LogConfig.init(this)
// 异步初始化:其他SDK
InitTaskDispatcher.dispatch {
// 推送、统计、地图等
PushSDK.init(this@MyApp)
AnalyticsSDK.init(this@MyApp)
MapSDK.init(this@MyApp)
}
}
}
这里有个坑。我曾经把某个SDK的初始化放到异步线程,结果它内部用了HandlerThread,线程还没准备好就调用了,直接崩溃。所以异步初始化一定要做好「就绪检查」。
小技巧:可以用IdleHandler来延迟初始化。等主线程空闲了再干活,不影响首帧渲染。
ContentProvider优化:被忽视的启动杀手
很多人不知道,ContentProvider的初始化是在Application.onCreate之前的。也就是说,你每个ContentProvider的onCreate都会拖慢冷启动。
我记得有一次排查启动慢的问题,发现项目里集成了十几个第三方库,每个库都自带一个ContentProvider。光初始化这些Provider就花了800ms。
优化方案其实就两条路:
- 合并ContentProvider:把多个Provider合并成一个,减少初始化次数
- 使用懒加载:Provider的onCreate里只做轻量初始化,真正的逻辑延迟到使用时
代码示例:
class MergedProvider : ContentProvider() {
override fun onCreate(): Boolean {
// 只做轻量初始化
val context = context ?: return false
// 记录Provider初始化时间
StartupTimer.record("MergedProvider")
return true
}
// 真正的初始化放到使用时
fun lazyInit(context: Context) {
// 推送、统计等SDK的初始化
}
}
注意:不要随意移除第三方库的ContentProvider。有些库依赖Provider的初始化顺序,移除可能导致功能异常。建议先确认每个Provider的作用。
启动黑白屏处理:别让用户看到「空白」
冷启动时,系统会先显示一个空白窗口。这个窗口的颜色取决于你的主题。默认是白色或黑色,所以叫「白屏」或「黑屏」。
我见过最粗暴的做法是直接设置透明主题。结果呢?用户点击图标后,屏幕「愣」一下才出现内容,体验更差。
正确的做法是:用启动页主题「欺骗」用户。
具体来说,给启动Activity设置一个特殊的主题,背景用启动页的图片或颜色。这样系统显示空白窗口时,用户看到的是你的启动页,而不是刺眼的白屏。
代码示例:
<!-- styles.xml -->
<style name="SplashTheme" parent="Theme.AppCompat.NoActionBar">
<item name="android:windowBackground">@drawable/splash_bg</item>
<item name="android:windowFullscreen">true</item>
</style>
<!-- AndroidManifest.xml -->
<activity
android:name=".SplashActivity"
android:theme="@style/SplashTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
然后在SplashActivity的onCreate里,把主题切换回正常主题:
class SplashActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
// 在super.onCreate之前切换主题
setTheme(R.style.AppTheme)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_splash)
// 延迟跳转到主页面
window.decorView.postDelayed({
startActivity(Intent(this, MainActivity::class.java))
finish()
}, 500)
}
}
经验之谈:启动页的图片不要太大。我见过有人用一张2MB的PNG做背景,结果图片解码就花了200ms。建议用VectorDrawable或者纯色背景,加载几乎零开销。
知识体系总览
下面这张图,是我梳理的启动优化核心逻辑。你可以对照着看,哪一步还没做到位。
启动优化没有银弹。每个项目的情况都不一样,关键是要先「测量」,再「优化」。我习惯用系统自带的ReportFullyDrawn来打点,配合Systrace看具体耗时。找到瓶颈,再对症下药。
嗯,启动优化这块,能做到「用户无感知」就算及格了。剩下的,就是不断打磨细节。
公众号:蓝海资料掘金营,微信deep3321