启动优化:冷启动与热启动、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或者纯色背景,加载几乎零开销。

知识体系总览

下面这张图,是我梳理的启动优化核心逻辑。你可以对照着看,哪一步还没做到位。

启动优化知识体系 冷启动 热启动 Application初始化优化 ContentProvider优化 启动黑白屏处理 异步初始化 懒加载 / IdleHandler 合并ContentProvider Provider懒加载 启动页主题 窗口背景替换 Activity重建优化 布局异步加载 ViewStub延迟加载 目标:首帧渲染 < 500ms

启动优化没有银弹。每个项目的情况都不一样,关键是要先「测量」,再「优化」。我习惯用系统自带的ReportFullyDrawn来打点,配合Systrace看具体耗时。找到瓶颈,再对症下药。

嗯,启动优化这块,能做到「用户无感知」就算及格了。剩下的,就是不断打磨细节。


公众号:蓝海资料掘金营,微信deep3321