19、Android性能优化之启动优化:Application初始化、懒加载、启动页白屏处理、启动耗时分析
启动优化,说白了就是让用户少等一会儿。
我刚开始做Android开发那会儿,觉得App能跑起来就行。直到有一次,产品经理拿着秒表站在我工位旁边,说“你这个App启动要3秒,竞品只要1秒”。嗯,从那以后,我就把启动优化当成了必修课。
今天咱们聊聊启动优化的几个核心点:Application初始化、懒加载、白屏处理、还有耗时分析。这些事做对了,用户体验能上一个台阶。
19.1 启动流程回顾:系统到底干了啥?
先简单过一下启动流程。你点一下桌面图标,系统会做这几件事:
- 创建进程(如果App还没跑起来)
- 创建Application对象
- 执行Application.onCreate()
- 创建启动Activity
- 执行Activity.onCreate()、onStart()、onResume()
- 渲染第一帧画面
这里面最耗时的,往往是Application.onCreate()里的初始化操作。很多团队喜欢一股脑把所有SDK都放在这里初始化,结果就是启动慢得像蜗牛。
核心观点:启动优化的本质,就是把非必要的初始化往后挪,把必要的初始化做快。
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 综合优化策略:从整体到细节
启动优化不是单一技巧,而是一套组合拳。我总结了一个优化清单:
- 减少Application.onCreate的同步初始化——能异步就异步,能懒加载就懒加载
- 使用启动主题解决白屏——让用户第一眼就看到内容
- 优化布局层级——减少嵌套,使用ConstraintLayout
- 延迟加载非首屏内容——比如ViewStub、Fragment懒加载
- 使用启动器框架——比如Alpha启动器,按依赖关系并行初始化
- 定期用Profiler分析——每次发版前跑一遍启动耗时
一句话总结:启动优化的核心,就是让用户尽快看到可交互的界面。所有非必要的操作,都往后挪。
好了,关于启动优化就聊这么多。这些方法我在多个项目里验证过,效果都不错。你可以在自己的项目里试试,看看启动时间能缩短多少。
公众号:蓝海资料掘金营,微信deep3321