19、Android性能优化之启动优化:Application初始化、懒加载、启动页白屏处理、启动耗时分析

启动优化,说白了就是让用户少等一会儿。

我刚开始做Android开发那会儿,觉得App能跑起来就行。直到有一次,产品经理拿着秒表站在我工位旁边,说“你这个App启动要3秒,竞品只要1秒”。嗯,从那以后,我就把启动优化当成了必修课。

今天咱们聊聊启动优化的几个核心点:Application初始化、懒加载、白屏处理、还有耗时分析。这些事做对了,用户体验能上一个台阶。

19.1 启动流程回顾:系统到底干了啥?

先简单过一下启动流程。你点一下桌面图标,系统会做这几件事:

  • 创建进程(如果App还没跑起来)
  • 创建Application对象
  • 执行Application.onCreate()
  • 创建启动Activity
  • 执行Activity.onCreate()、onStart()、onResume()
  • 渲染第一帧画面

这里面最耗时的,往往是Application.onCreate()里的初始化操作。很多团队喜欢一股脑把所有SDK都放在这里初始化,结果就是启动慢得像蜗牛。

核心观点:启动优化的本质,就是把非必要的初始化往后挪,把必要的初始化做快。

Android App 启动流程(简化版) 点击图标 创建进程 Application.onCreate() Activity启动 渲染 ~10ms ~50ms ~500ms~2s ~100ms ~16ms Application.onCreate() 是启动优化的主战场 这里放的东西越多,启动越慢 优化目标:把非必要初始化挪到后面去

19.2 Application初始化:别什么都往里塞

我见过最夸张的Application,onCreate里初始化了30多个SDK。有推送的、有统计的、有地图的、有图片加载的……结果启动耗时直奔3秒。

其实很多SDK根本不需要在启动时初始化。比如图片加载库,用户还没看到图片呢,你急什么?

19.2.1 分类策略:哪些必须提前初始化?

类别 举例 初始化时机
必须提前 崩溃收集、日志系统、核心路由 Application.onCreate()
可以延迟 图片加载、网络库、数据库 首次使用时
可以异步 统计SDK、推送SDK、广告SDK 子线程初始化

我的习惯:我会在Application里维护一个初始化优先级列表。只有P0级别的才放在主线程同步初始化,其他的要么异步,要么懒加载。

19.2.2 代码示例:按优先级初始化

public class MyApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        
        // P0:必须同步初始化
        initCrashReport();      // 崩溃收集,必须最早
        initLogSystem();        // 日志系统
        
        // P1:可以异步初始化
        new Thread(() -> {
            initPushSDK();      // 推送,不阻塞启动
            initStatSDK();      // 统计,不阻塞启动
        }).start();
        
        // P2:懒加载,首次使用时才初始化
        // 图片加载库、数据库等放到使用时再初始化
    }
}

注意:异步初始化要注意线程安全。我曾经遇到过推送SDK在子线程初始化时,刚好主线程调用了它的某个方法,结果就崩了。建议加个初始化完成的回调或者标志位。

19.3 懒加载:用的时候再干活

懒加载,说白了就是“按需加载”。用户没用到的东西,你就不该初始化。

我举个例子。很多App都有个“用户协议”弹窗,但用户第一次启动时根本不会点开看。那这个弹窗的WebView初始化,完全可以等到用户点击“查看协议”时再做。

19.3.1 懒加载的常见场景

  • 图片加载库:Glide、Picasso等,等用户看到图片时再初始化
  • 数据库:Room、GreenDAO等,等第一次查询时再初始化
  • 第三方SDK:地图、支付、分享等,等用户用到对应功能时再初始化
  • 复杂组件:WebView、视频播放器等,按需创建

19.3.2 代码示例:懒加载实现

public class ImageLoaderManager {
    private static volatile boolean initialized = false;
    
    public static void init() {
        if (initialized) return;
        synchronized (ImageLoaderManager.class) {
            if (initialized) return;
            // 真正的初始化代码
            Glide.init(context);
            initialized = true;
        }
    }
    
    public static void loadImage(String url, ImageView view) {
        init();  // 首次加载时自动初始化
        Glide.with(view).load(url).into(view);
    }
}

避坑指南:双检锁(Double-Checked Locking)是懒加载的经典写法。但要注意,如果初始化方法里有耗时操作,第一次调用时还是会卡一下。所以懒加载适合那些“初始化快”或者“用户感知不强”的场景。

19.4 启动页白屏处理:别让用户看到空白

你有没有遇到过这种情况:点开App,先看到一个白屏或者黑屏,过一两秒才出现启动页?

这就是白屏问题。原因很简单:系统在加载Application和Activity的时候,还没开始渲染布局,所以显示的是Window的背景色。

19.4.1 解决方案:设置启动主题

最常用的方法,就是给启动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里,再把主题切换回正常主题:

@Override
protected void onCreate(Bundle savedInstanceState) {
    // 先设置正常主题
    setTheme(R.style.AppTheme);
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_splash);
    
    // 其他初始化...
}

原理:系统在加载Activity之前,会先显示Window的背景。我们把背景设置成启动页的样子,用户就看不到白屏了。等Activity真正加载完成,再显示正常的布局。

19.4.2 另一种思路:使用启动页图片

有些团队会直接用一张图片作为windowBackground,这样连Activity都不用创建,系统直接显示图片。但缺点是没法做动画,也没法加载数据。

注意:windowBackground的图片不能太大,否则加载图片本身就会耗时。我建议用矢量图或者压缩过的PNG,控制在100KB以内。

19.5 启动耗时分析:用数据说话

优化之前,先得知道慢在哪里。我习惯用这几个工具来分析启动耗时:

19.5.1 常用分析工具

工具 用途 使用方式
Android Studio Profiler 查看CPU、内存、网络耗时 直接运行Profiler,录制启动过程
adb shell am start -W 获取启动时间 命令行执行,返回totalTime
自定义打点 精确测量每个阶段的耗时 在代码中插入System.currentTimeMillis()
TraceView / Systrace 查看方法调用栈和耗时 生成trace文件,分析热点方法

19.5.2 使用adb命令测量启动时间

# 冷启动测试
adb shell am start -W -n com.example.app/.SplashActivity

# 输出示例
# ThisTime: 1200    // 当前Activity启动耗时
# TotalTime: 1500   // 整个应用启动耗时
# WaitTime: 1600    // 包含系统调度的时间

我一般看TotalTime,这个值代表从Application.onCreate到Activity第一帧渲染完成的时间。如果超过2秒,就得好好优化了。

19.5.3 自定义打点:精确到毫秒

public class LaunchTimer {
    private static long startTime;
    
    public static void start() {
        startTime = System.currentTimeMillis();
    }
    
    public static void mark(String tag) {
        long now = System.currentTimeMillis();
        Log.d("LaunchTimer", tag + ": " + (now - startTime) + "ms");
    }
}

// 在Application.onCreate开头调用
LaunchTimer.start();

// 在各个初始化阶段调用
LaunchTimer.mark("崩溃收集初始化完成");
LaunchTimer.mark("日志系统初始化完成");
LaunchTimer.mark("推送SDK初始化完成");

我的经验:打点信息最好输出到日志文件,方便对比不同版本的启动耗时。我曾经用这个方式发现某个版本启动慢了300ms,一查是第三方SDK偷偷加了个同步初始化。

19.6 综合优化策略:从整体到细节

启动优化不是单一技巧,而是一套组合拳。我总结了一个优化清单:

  1. 减少Application.onCreate的同步初始化——能异步就异步,能懒加载就懒加载
  2. 使用启动主题解决白屏——让用户第一眼就看到内容
  3. 优化布局层级——减少嵌套,使用ConstraintLayout
  4. 延迟加载非首屏内容——比如ViewStub、Fragment懒加载
  5. 使用启动器框架——比如Alpha启动器,按依赖关系并行初始化
  6. 定期用Profiler分析——每次发版前跑一遍启动耗时

一句话总结:启动优化的核心,就是让用户尽快看到可交互的界面。所有非必要的操作,都往后挪。

好了,关于启动优化就聊这么多。这些方法我在多个项目里验证过,效果都不错。你可以在自己的项目里试试,看看启动时间能缩短多少。


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