每个做 Android 开发的几乎都绕不开闪屏页这个“第一道门”。我刚开始自己做 App 的时候也以为闪屏页就是放张图、跳个转后来真正从零写起来才发现里面有主题切换、倒计时、任务栈、冷启动优化一堆细节。这篇我就拿实际做过的项目为例把从创建工程到闪屏页跳转主界面的完整过程拆开讲顺便把各种踩过的坑一起写在里面。1. 闪屏页不是“花架子”先想清楚要解决什么问题1.1 闪屏页与启动页的区别很多新手容易把“闪屏页”和“启动页”混在一起。简单说闪屏页是 App 点击图标后、进入首页前展示的那一屏它承担的是“缓冲”和“分流”的作用而启动页通常指 App 冷启动过程中系统展示的 window 背景它在进程创建阶段就出现一闪而过。我见过不少项目直接把闪屏页做成一个三秒倒计时然后跳转的界面这种做法没有错但容易忽略核心问题闪屏页存在的意义是让用户在等待启动完成时不觉得“卡”和“白屏”同时利用这段时间完成必要的初始化操作比如检查登录状态、加载本地配置、预热数据等。如果你只是放一张图等固定时间后跳转用户会觉得“这个 App 怎么这么慢每次进来都要等”。反过来如果完全不设闪屏页冷启动时的白屏或桌面图标跳转会非常硌眼。所以闪屏页的定位应该是“有目的的等待”不是单纯拖延时间的装饰品。1.2 做闪屏页前必须明确的三个需求动手写代码前建议先问自己三个问题。第一个问题是闪屏页停留时间是固定还是可变固定时间好写比如 2 秒后跳转可变时间则需要结合首屏数据是否准备好比如 MainActivity 的布局必须等某些数据到位后再进入这时候闪屏页就得等核心数据的初始化结果。第二个问题是是否需要倒计时跳过很多工具类、小程序类 App 会把广告位放在闪屏页上并提供倒计时“跳过”按钮。我们自己做项目时即便不接广告也可以保留跳过逻辑因为用户第二次、第三次打开 App 时往往不希望每次都等满 3 秒。第三个问题是闪屏页跳转是否要受登录状态影响如果 App 有登录机制通常的逻辑是未登录进登录页已登录进主页。建议把这一层判断放在闪屏页的跳转方法里而不是放在主页里判断这样职责更清晰。想清楚这三点后面写代码才有方向。2. 项目准备Android Studio 环境与工程初始化2.1 环境与工具版本怎么选做这个实战项目建议用的就是 Android Studio这是目前最主流、也最省事的 Android 开发工具。安装时需要注意 JDK 版本要和 Gradle、AGP 版本匹配否则一创建工程就会报一堆版本冲突。我这次用的组合是Android Studio Koala 或更高版本大家根据自己机器装就行JDK 17Gradle 8.2 以上Android Gradle Plugin 8.1 以上targetSdk 34minSdk 24这个组合在创建空 Activity 工程时基本不会出问题。如果你电脑上已经装过低版本 JDK 或者开了多个 Android Studio 版本建议在项目根目录的 gradle-wrapper.properties 里明确指定 Gradle 版本不要让它乱飘。创建工程时选择 “Empty Views Activity” 或 “Empty Activity” 都可以。从零写 App 嘛尽量不要选带太多模板代码的工程比如 Material3 模板会生成一堆主题和导航依赖新手容易看懵。我们这次选一个尽量干净的基础模板然后自己手动加依赖。2.2 工程结构怎么规划规划结构之前先明确一点如果只是做闪屏页这种小功能没必要一上来就套 MVP、MVVM 全家桶。但也不能把所有类都扔在一个包底下否则后面扩展会乱。我习惯在 java 包下按业务模块分包比如activity存放 Activityadapter存放列表适配器model存放数据模型utils存放工具类common存放常量、基类、公共组件闪屏页相关代码我会放到 activity 包下命名成 SplashActivity。这样后面做登录页 LoginActivity、主页 MainActivity 的时候结构一目了然。布局文件统一放 res/layout命名严格按照“activity_xxx.xml”这类前缀来比如 activity_splash.xml。资源文件命名这件事看起来小但项目一旦变大命名混乱会让人很崩溃。我吃过这个亏后来改项目的时候满屏都是 image1.xml只能一个个猜。3. 闪屏页核心实现从布局到 Activity3.1 布局文件怎么写才不“土”闪屏页布局不必复杂但要有品牌感。通常由一个全屏的背景色、一张居中的 Logo 图片、底部版本号和版权信息组成。基础结构大致是这样?xml version1.0 encodingutf-8? FrameLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_heightmatch_parent android:backgroundcolor/splash_bg ImageView android:idid/iv_logo android:layout_widthwrap_content android:layout_heightwrap_content android:layout_gravitycenter android:srcdrawable/ic_logo / TextView android:idid/tv_version android:layout_widthwrap_content android:layout_heightwrap_content android:layout_gravitycenter_horizontal|bottom android:layout_marginBottom40dp android:textV1.0.0 android:textColor#FFFFFF android:textSize14sp / TextView android:idid/tv_skip android:layout_widthwrap_content android:layout_heightwrap_content android:layout_gravityend|top android:layout_margin20dp android:padding8dp android:text跳过 3s android:textColor#FFFFFF android:textSize14sp android:visibilitygone / /FrameLayout这里我解释几个容易忽略的地方。为什么用 FrameLayout 做根容器因为闪屏页所有元素都重叠摆放且一般不需要复杂的线性排列FrameLayout 最省资源、逻辑最简单。版本号为什么放在底部而不是正中间因为大部分手机桌面图标和 App Logo 视觉重心偏上底部放版本号既占位置又能传递“当前版本”这个信息一旦用户反馈问题你截图就能快速确认对方是不是旧版本。跳过按钮初始设为 gone只有当你决定启用倒计时跳过功能时才让它显示否则一开始就弹一个“跳过”按钮在那里用户可能误触反而体验不好。3.2 Activity 逻辑与倒计时实现SplashActivity 的核心工作有三件事初始化、倒计时、跳转。我们先把倒计时逻辑写出来。很多人喜欢用 TimerTask 或者 postDelayed但如果你需要在倒计时过程中实时更新“跳过 3s”这样的文案建议用 CountDownTimer它是系统自带的倒计时工具既省电又好取消。public class SplashActivity extends AppCompatActivity { private CountDownTimer mCountDownTimer; private static final long TOTAL_COUNT 3000L; private static final long INTERVAL 1000L; private TextView mTvSkip; Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_splash); mTvSkip findViewById(R.id.tv_skip); startCountDown(); } private void startCountDown() { mCountDownTimer new CountDownTimer(TOTAL_COUNT, INTERVAL) { Override public void onTick(long millisUntilFinished) { mTvSkip.setText(跳过 (millisUntilFinished / 1000 1) s); } Override public void onFinish() { goToMain(); } }.start(); } private void goToMain() { Intent intent new Intent(SplashActivity.this, MainActivity.class); startActivity(intent); finish(); } Override protected void onDestroy() { super.onDestroy(); if (mCountDownTimer ! null) { mCountDownTimer.cancel(); } } }这里有个细节onTick 中显示的秒数是millisUntilFinished / 1000 1。因为 CountDownTimer 第一次回调时传入的是 3000如果直接除以 1000显示的数字是 3这样一开始显示 “跳过 3s” 没问题但到最后一秒数值变成 1看起来好像只剩 1 秒了但实际在 onFinish 里才会跳转所以显示上会存在“1s 停留两次”的感觉。加 1 是为了让用户从 3、2、1 正常倒数。如果你不希望显示倒计时完全可以把这个按钮隐藏仅保留 get 延迟跳转。另外注意 onDestroy 里一定要 cancel 掉倒计时否则 Activity 销毁后 Timer 还在跑轻则内存泄漏重则引发空指针。3.3 状态栏与主题的细节处理闪屏页是全屏展示的所以状态栏一般要处理一下。如果你用的是深色背景图状态栏图标和文字建议调成白色如果是浅色背景就保留深色图标。在 styles.xml 里可以定义一套 SplashTheme继承 Theme.AppCompat.Light.NoActionBar再单独设置状态栏颜色style nameTheme.Splash parentTheme.AppCompat.Light.NoActionBar item nameandroid:statusBarColorcolor/splash_bg/item item nameandroid:windowBackgrounddrawable/splash_bg_layer/item /style然后 AndroidManifest.xml 中SplashActivity 的 theme 用 Theme.SplashMainActivity 用正常主题。这样一个 App 可以有两套主题有效避免闪屏页加载瞬间出现白色闪屏。这里我要重点提一下windowBackground。很多闪屏页“闪白”的问题根源就是 Activity 启动前系统先绘制一个默认白色背景等 App 的布局加载完成后才替换掉。你在 SplashTheme 里配置一个和闪屏页背景一致的 drawable系统一启动就直接画这个背景视觉上就没有白屏过渡了。layer-list xmlns:androidhttp://schemas.android.com/apk/res/android item android:drawablecolor/splash_bg / item bitmap android:gravitycenter android:srcdrawable/ic_logo / /item /layer-list这段代码可以用一张可绘制的 layer-list 作为虚拟闪屏背景在 Activity 尚未创建时就展示。让 Logo 先出现在 window 背景上等布局创建后无缝衔接。如果你希望完全由布局接管这个 drawable 可以和布局保持一致尽可能减少视觉突变。4. 从闪屏页到主界面的跳转意图与任务栈4.1 跳转的写法与登录状态判断跳转用到 Intent这大家都知道。关键是怎么“带着条件跳”。如果当前 App 有登录逻辑建议在 goToMain 里判断一下持久化存储的登录标记比如 SharedPreferences:private void goToMain() { boolean isLogin SPUtils.getBoolean(is_login, false); Intent intent; if (isLogin) { intent new Intent(SplashActivity.this, MainActivity.class); } else { intent new Intent(SplashActivity.this, LoginActivity.class); } startActivity(intent); finish(); }把登录判断放在 SplashActivity 里好处是入口统一首页不需要再处理“因为没登录所以跳走”的逻辑。而且闪屏页一定会执行一次是天然的网关位置。还有一个细节startActivity的时候要不要加FLAG_ACTIVITY_CLEAR_TASK这类标记如果你是从闪屏页跳主界面然后关闭闪屏页其实不需要。但如果你担心用户按返回键会回到闪屏页可以在跳转后执行finish()同时因为你是在闪屏页 finish系统自然不会返回到它。如果后面闪屏页还会作为登录逻辑的分支跳来跳去建议直接用Intent intent new Intent(SplashActivity.this, MainActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); startActivity(intent);这样新任务栈中只有一个 MainActivity用户按返回键时不会退到闪屏也不会退到其他中间页。4.2 任务栈与返回键处理闪屏页还有一个容易踩坑的地方用户点击图标后进入闪屏页快速按返回键App 应该退出而不是继续跳主界面。CountDownTimer 在 onFinish 里没有检查 Activity 是否 isFinishing可能就会出现“用户已经退出但 Timer 还是触发跳转把已经销毁的页面又拉起来”的情况。最稳妥的办法是Override public void onFinish() { if (isFinishing() || isDestroyed()) { return; } goToMain(); }同时在 onBackPressed 里如果用户点击返回键直接 cancel 倒计时并 finishOverride public void onBackPressed() { if (mCountDownTimer ! null) { mCountDownTimer.cancel(); } super.onBackPressed(); }这样返回键处理就和闪屏逻辑解耦了用户返回后不会被“阴魂不散”的倒计时拽回主界面。对于任务栈我额外说一点Android 的 back stack 是后进先出。如果从 SplashActivity 启动 MainActivity然后在 SplashActivity 里调用 finish那么这个时刻栈里只有 MainActivity这是最理想的。直接启动 MainActivity 但不 finish会导致任务栈里还留着一个已显示的 SplashActivity用户按返回键时会看到闪屏页重新出现这就恶心了。所以“先 startActivity再 finish”或“使用 CLEAR_TASK 标志”二选一一定要做。5. 常见问题与排查技巧实录5.1 闪屏页总是“闪白”一下怎么破这个问题在 Android 12 以上尤其明显因为系统强制启动画面默认使用了 App 的图标有时候你的闪屏页还没加载出来系统启动画面已经显示了两者衔接不自然。如果你希望 App 从点击图标开始就是自己的闪屏视觉需要在 Android 12 及以上的启动画面中配置 themes.xml 中的windowSplashScreenBackground、windowSplashScreenAnimatedIcon这些属性尽量让系统启动画面和自定义闪屏页色调一致。否则你看到的现象就是先出现系统默认的白底图标再跳到自己的闪屏页明显“闪了一下”。不过你说“闪白”还有另一个常见原因windowBackground 没有配置。前面提到的 SplashTheme 里的windowBackground只对 SplashActivity 生效如果你的 MainActivity 主题没有设置窗口背景而 MainActivity 冷启动时又要加载大量布局就会在闪屏页跳转后先白一下再显示首页。解决方法是给 MainActivity 的主题也设置一个接近其背景色的 windowBackground而不要在 Activity 里把所有 View 都堆在一个布局里。5.2 冷启动时间优化闪屏页不是缓存有些新手为了“让闪屏页好看”在 onCreate 里写一堆耗时逻辑比如联网请求、读数据库、加载大图全选在闪屏页。这样做的结果就是冷启动时间被拉长用户看到闪屏页的时间也加长体验反而变差。实际上闪屏页是给用户看的“表面工作”真正要做的是把耗时任务挪到子线程或延迟加载。比如初始化 SDK、读取账号信息可以放到Application里异步执行或者在闪屏页期间通过在后台线程完成不要阻塞 UI。如果想看自家项目冷启动到底多长时间可以通过 adb 命令查看adb shell am start -W -n 包名/包名.activity.SplashActivity输出结果里有TotalTime可以清晰看到从点击到第一个窗口完成绘制的时间。优化目标是尽量控制在 1 秒以内。如果你的 TotalTime 明显偏高优先检查 Application 里的初始化和闪屏页布局的复杂程度。5.3 倒计时造成的内存泄漏与崩溃CountDownTimer 虽然方便但它的内部实现用到了 Handler。如果你在 Activity 的 onCreate 里创建 CountDownTimer而没有在 onDestroy 里 cancel即使 Activity 已经销毁了Handler 消息仍可能持有这个 Activity 的引用导致内存泄漏。更严重的是如果你的闪屏页在 onFinish 回调中调用了 findViewById 或者直接操作 TextView而这时 Activity 已经销毁就会引起空指针崩溃。所以建议做法是进入 onTick 和 onFinish 时先判断 Activity 状态确保没有销毁再去操作 UI。另外如果你用了 ViewBinding也要小心在销毁后的回调里访问 binding 对象。我一般会在 onDestroy 里把 binding 置空并在回调里判断binding null再执行逻辑。为了保险可以把 CountDownTimer 换成 Handler.postDelayed同样记得在 onDestroy 里 removeCallbacks。两者差别不大只看哪个顺手。5.4 其他经验模拟器、真机与启动图标开发阶段很多人喜欢用模拟器跑 App。模拟器冷启动速度明显比真机慢这会让闪屏页的“等待感”更强有时候误以为倒计时没走完其实是模拟器性能问题。建议以真机为主要调试目标。启动图标方面如果你用默认的机器人头像做闪屏 Logo会显得很糙。自己做项目时建议用一张 512x512 的矢量图或 png 来充当 Logo。也不需要多高技巧可以用工具画一个简单的字母样式效果就比默认图标好很多。另外注意不要忘记在 AndroidManifest 里把闪屏页设置成launcheractivityactivity android:name.activity.SplashActivity android:exportedtrue android:themestyle/Theme.Splash intent-filter action android:nameandroid.intent.action.MAIN / category android:nameandroid.intent.category.LAUNCHER / /intent-filter /activityAndroid 12 之后exported属性必须显式声明否则直接编译报错。这也是很多新手第一次做闪屏页时最常碰到的拦路虎。5.5 闪屏页跳转后的动画问题跳转页面时可以用 overridePendingTransition 来设置淡入淡出动画比如startActivity(intent); finish(); overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);要注意这个方法的调用时机一定要在 startActivity 之后、finish 之后调用否则动画可能不生效。如果你不想让闪屏页跳转太生硬淡入淡出是最稳妥的选择。市面上有些 App 用缩放动画但自己做的时候没有必要过度设计保持简洁更重要。6. 实战中的个人心得与几个“后悔没早知道”的点这个闪屏页功能看起来简单但我第一次写的时候也折腾了不少时间。我把几个“后悔没早知道”的经验集中说一下。第一先把主题和 windowBackground 配好再写布局。如果你先去写布局文件再去配主题白白多踩一次闪白的坑。正确顺序应该是先建 SplashTheme设置状态栏和窗口背景色然后再写 Activity 和布局一上来就把冷启动衔接问题处理掉。第二倒计时只做一次复用。后面如果要加广告位需要在 onTick 里更新跳过按钮文案但倒计时总时间不能和业务逻辑强绑定。我建议把倒计时逻辑封装成一个工具类传入时长、间隔和回调这样以后在多个页面都能复用不用复制粘贴。第三不要忘记适配刘海屏和挖孔屏。状态栏高度在不同手机上差别很大如果你的 Logo 太靠上容易被前置摄像头遮挡。我做闪屏页时会用下面这种方式把布局移到安全区域之外android:fitsSystemWindowstrue或者动态获取状态栏高度给顶部布局留 padding。具体视设计稿而定但至少要意识到这个适配问题的存在。第四真机调试时插着 USB 调试线运行 App你会发现启动速度比用户正常安装后的冷启动慢因为调试模式有很多额外开销。如果要做启动性能评估建议发布一个 release 包来测试或者至少拔掉数据线跑一次。最后我想说的是闪屏页在整个 App 体验里的地位被严重低估了。它是用户对产品的第一印象也是决定用户是否愿意等待的重要缓冲。如果把闪屏页做成“卡顿三秒”那用户大概率会想卸载如果做成“流畅进入”即使后台初始化还没完全好也能给人很爽的感觉。我自己做这个项目时后期又接了一个“品牌宣传图 跳过”的功能就是基于上面这套 SplashActivity 改的核心代码没动多少只加了一个 Banner 容器和广告请求逻辑。所以现在把基础打牢后面扩展都很顺。希望这篇文章能帮你顺利搞定闪屏页少走一些我走过的弯路。