来聊聊Flutter开发里一个非常基础、但又有点绕的话题启动页Splash Screen和App图标的替换。很多人一提到这个第一反应是去装个flutter_native_splash或者换个图标相关的插件但实际用过就会发现插件解决不了所有问题遇到Android 12的启动屏新规范时还容易水土不服。这篇文章我打算完全绕开插件手把手带你在Flutter项目的Android原生目录里把图标和启动页一次性搞定。全程手动修改原生资源不引入任何额外依赖适合想彻底搞懂原理、或者对包体积和插件维护有顾虑的开发者参考。1. 为什么我最终放弃了启动页和图标的相关插件先说结论插件不是不能用而是能不用就不用。尤其是只涉及静态启动图和图标替换这种固定需求插件的重复封装反而成了负担。1.1 插件方案的三个隐藏成本很多新手习惯直接搜flutter_native_splash装完之后跑一下命令就完事了。表面上挺方便但长期维护的时候会有几个问题。第一个问题是依赖链延长。插件本质上是帮你自动化修改原生文件但它的生成逻辑是黑盒的。哪天Flutter升级了Gradle插件版本或者Android SDK的编译目标变了插件的兼容性未必第一时间跟上。我遇到过一次原生Gradle从7.x升到8.x之后插件重新生成的资源文件里居然残留了旧的values-v31配置导致Android 12设备上启动屏样式错乱排查了半天才发现是插件覆盖不干净。第二个问题是定制能力受限。插件通常给你几个参数背景颜色、图片路径、图标大小。但如果我想让启动页顶部是一个居中的Logo、底部再来一行版本号或者根据不同渠道包显示不同的启动图插件的配置文件就会变得非常臃肿甚至要写额外的原生代码去绕过它。第三个问题更实际很多插件作者已经不再活跃维护了。图标替换这种需求更简单粗暴很多时候大家直接去改AndroidManifest.xml里的android:icon根本不需要插件。我们手动操作原生目录等于把所有控制权拿回自己手里目录结构、资源命名、样式配置全部可见可改出了问题也能直接定位到具体文件。1.2 手动方案真正适合的场景我建议下面这些人优先考虑手动方案对包体积敏感的项目。一个插件即使很小也有编译期产物和AAR依赖能省则省。启动图需要跟随主题、深色模式或渠道变化的项目。原生资源目录天然支持values-night、values-v31这种限定符插件很难做到全场景覆盖。团队里有原生开发能力的项目。既然能看明白launch_background.xml就没必要让插件在中间做翻译层。纯粹是静态Logo 纯色背景的项目。这种需求手动改两张图一个XML就完了十分钟搞定。当然如果你团队里完全没有原生开发经验而且需求就是换个背景图这种极简场景那用插件确实是最快的。但如果想少踩坑尤其是适配Android 12的分屏和启动动画我建议还是把手动方案搞明白。2. 动手之前先搞清楚 Flutter 冷启动时 Android 原生层到底做了什么很多同学改启动页失败不是因为不会改文件名而是没搞懂Android系统在Flutter引擎启动之前到底展示什么。这段逻辑不捋清楚后面验证阶段会一头雾水。2.1 冷启动、暖启动与热重启的区别先说个容易混淆的概念。我们在开发时喜欢按R键热重启Hot Restart它很快因为Dart代码在几秒内就能完成重新编译注入。但热重启不会重新走Android原生的启动流程——你看到的闪屏其实是Flutter侧的firstFrame自然延迟跟我们要改的Android原生启动页不是一回事。真正要关心的是冷启动Cold Start也就是从桌面点图标到Flutter引擎初始化完成并能渲染第一帧的整个过程。在这个窗口里屏幕上显示的不是Flutter的东西而是Android系统根据Theme和SplashScreen属性绘制出来的原生启动页。换句话说你要改的启动页是Android系统画的不是Flutter画的。2.2 系统窗口、SplashScreen 和 launch_background.xml 三者关系Android系统启动App的时候会先创建一个窗口Window里面展示的内容由启动主题Launch Theme来决定。在Flutter项目里默认的启动主题长这样!-- android/app/src/main/res/values/styles.xml -- resources style nameLaunchTheme parentandroid:style/Theme.Light.NoTitleBar item nameandroid:windowBackgrounddrawable/launch_background/item /style style nameNormalTheme parentandroid:style/Theme.Light.NoTitleBar item nameandroid:windowBackground?android:colorBackground/item /style /resources注意看LaunchTheme里指定了windowBackground为drawable/launch_background也就是说系统启动时是把launch_background.xml这个drawable当窗口背景画在屏幕上的。而真正的系统SplashScreenAPIAndroid 12是另一套机制它负责把这张图配合动画平滑过渡到App主界面。所以我的理解是这样Android 11及以下启动画面就是windowBackground一张图而已App主界面出来后直接切换过渡比较生硬。Android 12及以上系统在App进程启动前的极早期阶段就接管了启动画面你设置的windowSplashScreenBackground、windowSplashScreenAnimatedIcon等属性会被系统用来绘制启动效果launch_background.xml依然存在但负责的是App侧接收系统交接后的背景。因此手动适配时不能只改一个文件而是要让老版本用的launch_background.xml、新版本用的样式属性、以及主题配置协同工作。这个组合拳打好了才能保证从Android 8到Android 15都不穿帮。2.3 Flutter 引擎接管的瞬间还是会有白屏很多人改完启动页发现在App真正显示之前有一瞬间的白屏其实是漏了NormalTheme。流程是这样的Android系统启动页显示 - 引擎初始化完成 - Flutter渲染窗口替换系统窗口。在引擎初始化完成但Flutter第一帧还没画出来的时候窗口会切换到NormalTheme。如果NormalTheme里没有指定背景或者指定的是白色就会产生一个白屏闪烁。解决办法是在NormalTheme里也设置和启动页背景相近的颜色style nameNormalTheme parentandroid:style/Theme.Light.NoTitleBar item nameandroid:windowBackgroundcolor/launch_background_color/item /style这样系统窗口被替换的前后背景色保持一致视觉上就没有断层了。3. App 图标替换先弄懂 mipmap 资源目录与自适应图标机制图标看起来就是换张图但Android的图标体系在数年前就升级成自适应图标Adaptive Icon了藏着不少容易踩的细节。3.1 一套图标 vs 多尺寸图标的存放规则Flutter默认模板里图标存放在android/app/src/main/res/mipmap-*目录下res/ ├── mipmap-hdpi/ic_launcher.png (48x48) ├── mipmap-mdpi/ic_launcher.png (72x72) ├── mipmap-xhdpi/ic_launcher.png (96x96) ├── mipmap-xxhdpi/ic_launcher.png (144x144) ├── mipmap-xxxhdpi/ic_launcher.png (192x192) ├── mipmap-anydpi-v26/ic_launcher.xml (自适应图标) └── drawable-v21/launch_background.xml核心规则是不是把一张大图硬塞进去而是每个密度目录都放对应尺寸的PNG。系统会按设备屏幕密度自动选择合适的尺寸。如果你只放一张192px的图在mipmap-xxxhdpi里中低端设备拿不到合适的图片运行时缩放容易产生模糊或内存浪费。各尺寸对应关系我整理成表格方便你对照资源目录屏幕密度dpi图标建议像素mipmap-mdpi160dpi48x48mipmap-hdpi240dpi72x72mipmap-xhdpi320dpi96x96mipmap-xxhdpi480dpi144x144mipmap-xxxhdpi640dpi192x1923.2 自适应图标Android 8.0 之后的裁剪逻辑Android 8.0API 26引入了自适应图标也就是ic_launcher.xml这种XML文件它会指定前景图层和背景图层!-- android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml -- adaptive-icon xmlns:androidhttp://schemas.android.com/apk/res/android background android:drawablecolor/ic_launcher_background / foreground android:drawabledrawable/ic_launcher_foreground / /adaptive-icon系统会在不同厂商的手机上对前景图做不同形状的裁剪圆形、圆角矩形、方角等所以前景图层四周必须预留安全区通常建议核心图案只占中间66%左右的区域即108x108像素中的中心72x72区域。否则边缘的图案容易被裁掉很多人在部分设备上发现桌面图标边缘被切了一刀就是这个原因。手动替换图标有两种路线路线一直接用非自适应图标。最简单粗暴真的只需要一张png和manifest对应关系改好就收工。缺点是Android 8.0会强制把图标包进模拟的自适应层可能因为系统裁剪而显示效果不可控。路线二按自适应图标要求准备前景和背景。把一张logo图拆成底图前景处理好安全区。这样Android 8.0的设备上图标跟随系统外观老设备用普通png兜底。我比较推荐路线二因为现在自适应图标几乎是标配了。实在不想做前景图的话也可以直接沿用统一大小的完整PNG但记得四周留白充足。3.3 修改Manifest指向新的图标资源如果不想沿用默认的ic_launcher命名可以起一个更语义化的名字比如ic_app_logo然后在AndroidManifest.xml里改指向application android:iconmipmap/ic_app_logo android:label你的App名注意如果只改了android:icon桌面上新图标生效了但最近任务列表里显示的图标图标可能还走roundIcon。所以要么同时改android:roundIcon要么删掉roundIcon属性让它默认取android:icon。这个细节容易漏。提示Android 13新增了android:monochrome属性用于单色主题图标如果你有精力可以单独提供一张alpha图。不过这不影响App正常使用属于锦上添花。4. 启动页改造launch_background.xml 与 Android 12 新规范并存启动页这块最麻烦的其实是新旧兼容。直接Google搜到的很多教程还停留在改launch_background.xml的阶段但Android 12的SplashScreen已经被系统接管了旧方法只改XML根本动不了新系统的启动画面。4.1 老版本做法layer-list 组合窗口背景在Android 11及以下launch_background.xml默认是这样!-- android/app/src/main/res/drawable/launch_background.xml -- layer-list xmlns:androidhttp://schemas.android.com/apk/res/android android:opacityopaque item android:drawablecolor/launch_background_color / item bitmap android:gravitycenter android:srcdrawable/launch_screen_logo / /item /layer-list注意看它用的不是普通Imageview而是一个layer-list。第一层是纯色背景第二层是居中的Logo位图。因为启动页本质上是一个Drawable所以不能用layout只能通过这种图层拼接的方式完成居中显示。背景塞了一张大图会导致启动时内存飙升、显示卡顿甚至出现中间短暂黑屏的问题这点后面踩坑章节会具体说。4.2 Android 12 的新机制系统级SplashScreenAndroid 12开始windowBackground这套旧方案就失效了。系统引入了一套新的SplashScreen机制具体表现在主题样式上!-- android/app/src/main/res/values-v31/styles.xml -- resources style nameLaunchTheme parentandroid:style/Theme.Light.NoTitleBar item nameandroid:windowSplashScreenBackgroundcolor/launch_background_color/item item nameandroid:windowSplashScreenAnimatedIcondrawable/launch_screen_logo/item item nameandroid:windowSplashScreenAnimationDuration200/item item namepostSplashScreenThemestyle/NormalTheme/item /style /resources这里有几个容易理解偏差的地方item nameandroid:windowSplashScreenBackground控制启动页背景色。item nameandroid:windowSplashScreenAnimatedIcon控制中间显示的Logo。注意它接受的是drawable所以你也可以放动画资源。item namepostSplashScreenTheme控制启动动画结束后过渡到的主题。不配这个属性系统就不知道切换到哪个主题容易闪白。如果你没见过values-v31目录那八成是插件改出来的或者项目用的模板比较新。手动适配就得自己创建这个目录。4.3 为什么只改XML的教程在Android 12上翻车我见过不少人拿着旧教程改launch_background.xml里的图片改完在Android 11上测试正常结果在Android 12手机上毫无变化。其实不是没改对而是Android 12根本不读那套windowBackground了你改的东西在新系统上压根没触发。所以正确的姿势是双管齐下保证drawable/launch_background.xml和values/styles.xml里的LaunchTheme配置正确服务于Android 11及以下的老设备。创建values-v31/styles.xml配置windowSplashScreen*系列属性服务于Android 12及以上设备。两者并存互不干扰。系统的SplashScreen机制会自动读取主题号也可以用代码检测当前系统版本动态处理。不过实测静态配置已经能覆盖绝大多数场景。5. 完整替换实操从图标到启动页的一步步原生改造先把最核心的目录结构梳理出来。实操的前提是明确自己要改哪几个文件避免漏改。5.1 实际操作前建议先备份与明确工作目录建议先拷贝一份工程或者用版本管理工具把当前状态提交一次防止改崩了回退麻烦。工作目录集中在android/app/src/main/res/和android/app/src/main/AndroidManifest.xml基本上不需要动Kotlin/Java代码。同时准备素材时要注意PNG要带透明通道的Logo文件不要用带背景色的JPG因为启动页底色会用纯色层来垫底Logo只需要透明背景的图即可。如果只有一张大logo我习惯用图像处理软件先等比压缩生成多个尺寸。5.2 第一步统一图标资源并适配Manifest以漏斗型App为例动态生成res/mipmap-*/ic_app_logo.png资源尺寸就按上面表格来。然后写前景图和背景色!-- android/app/src/main/res/mipmap-anydpi-v26/ic_app_logo.xml -- adaptive-icon xmlns:androidhttp://schemas.android.com/apk/res/android background android:drawablecolor/ic_logo_background / foreground android:drawabledrawable/ic_logo_foreground / /adaptive-icon!-- android/app/src/main/res/values/colors.xml -- resources color nameic_logo_background#FFFFFF/color /resourcesic_logo_foreground是从原logo中央66%区域裁出的核心图案放到res/drawable/ic_logo_foreground.png即可。需要注意它不是一张全幅图片理想状态是中间有个图案、四周透明。再改Manifestapplication android:iconmipmap/ic_app_logo android:roundIconmipmap/ic_app_logo ...如果你是Android 13还想兼顾单色图标可以加monochrome属性指向一张alpha图不做也不影响启动逻辑。5.3 第二步在 drawable 中准备启动页背景和Logo自定义启动页本质上要准备两类资源启动页背景色定义在res/values/colors.xml里。居中的Logo图放在res/drawable/目录命名比如launch_screen_logo.png。注意drawable目录没有按密度拆分所以一张图即可。但如果Logo在不同屏幕上显得过大/过小可以通过位图的gravity和width/height来控制不过drawable bitXML不支持尺寸缩放属性需要借助layer-list中的item宽高或者直接改图本身。5.4 第三步改写 launch_background.xml 和 styles.xml在res/drawable/launch_background.xml中写入layer-list xmlns:androidhttp://schemas.android.com/apk/res/android android:opacityopaque item android:drawablecolor/launch_background_color / item bitmap android:gravitycenter android:srcdrawable/launch_screen_logo / /item /layer-list然后在res/values/styles.xml中确保LaunchTheme的windowBackground指向这张图style nameLaunchTheme parentandroid:style/Theme.Light.NoTitleBar item nameandroid:windowBackgrounddrawable/launch_background/item /style再顺手把NormalTheme的背景也改成同一个颜色防止过渡白屏style nameNormalTheme parentandroid:style/Theme.Light.NoTitleBar item nameandroid:windowBackgroundcolor/launch_background_color/item /style5.5 第四步适配Android 12启动画面样式创建res/values-v31/styles.xml填上这套配置resources style nameLaunchTheme parentandroid:style/Theme.Light.NoTitleBar item nameandroid:windowSplashScreenBackgroundcolor/launch_background_color/item item nameandroid:windowSplashScreenAnimatedIcondrawable/launch_screen_logo/item item nameandroid:windowSplashScreenAnimationDuration200/item item namepostSplashScreenThemestyle/NormalTheme/item /style /resources如果项目本身还用了AppCompat或Material主题postSplashScreenTheme这里就要指向App的正式主题比如style/Theme.AppCompat.Light.NoActionBar具体看项目实际情况。不加这一项Android 12上启动动画结束可能会黑一下再进App。5.6 第五步状态栏与深色模式的协调处理启动页上经常遇到状态栏颜色和启动背景不一致的问题。Flutter默认是浅色状态栏图标如果你的启动背景是深色状态栏上时间电量的黑色文字就会看不清。在launch_background.xml没法直接控制状态栏得在styles.xml里加item nameandroid:windowLightStatusBartrue/item竖排的话还要注意部分设备有打孔屏启动Logo如果太靠上会被摄像头挡住。建议Logo中心位置在屏幕垂直中线偏下一点或者干脆控制在dimens里偏移。当然这在纯XML里做不到得用layer-list的android:top和android:bottom来自定偏移或者直接用FrameLayout在原生Activity层做启动页后续再说。5.7 深色模式适配values-night 目录如果App支持深色模式最好建立一个res/values-night/colors.xml把启动背景色替换成深色避免深夜打开App时突然一片刺眼的白。同时mipmap-anydpi-v26/ic_app_logo.xml里的背景色也可以根据深色模式换但自适应图标的背景色通常在values-night/colors.xml里定义同名颜色值系统会自动切换。6. 替换后的构建与验证如何确保真的生效改完了不是直接flutter run就完了里面有挺多小坑。6.1 为什么有时候改了资源但桌面图标不变最常见的原因是启动器Launcher图标缓存。桌面放的是系统缓存的图标加载过一次之后就不怎么刷新了。即使你改了资源桌面上看到的还是旧图。解决办法是卸载App重装或者换一个启动器看看。有的手机厂商ROM还会在最近任务界面缓存应用图标那个缓存更顽固可能要重启桌面进程。反正验证图标时不要只盯着一处桌面、最近任务、应用管理三个入口都看一眼。6.2 冷启动测试的正确姿势开发时跑flutter run默认hot restart这会绕过Android原生的启动流程。验证启动页必须冷启动也就是完全杀掉App进程再点桌面图标。有的同学说我杀掉了啊为什么还是看旧启动图——有可能改的launch_background.xml被Gradle增量编译缓存了。在android/目录下执行一次./gradlew clean或者直接删掉整个build/目录再重新构建确保资源被重新打包。我之前遇到过新素材文件拷进去了但build目录下旧文件还在的情况Gradle增量构建没重新压缩资源调试了好半天才发现是缓存问题。6.3 不同Android版本下的预期表现Android版本启动页表现主要配置来源7.0 - 8.0纯色背景居中LogoApp首帧完成后直接切换launch_background.xml8.1 - 11同上但图标资源走自适应图标规范launch_background.xml adaptive icon12 - 13系统级启动画面图标从中心缩放展开动画App自定义图标背景values-v31/styles.xml14同上默认再增加图标轻微缩放效果values-v31/styles.xml6.4 通过 adb 命令快速冷启动验证在改完资源后我习惯直接命令行打包installDebug然后通过am start启动这样能跳过一些开发工具的额外启动逻辑adb shell am force-stop 你的包名 adb shell am start -n 你的包名/你的包名.MainActivityforce-stop确保进程死掉然后am start冷启动。整个过程中手机屏幕会按实际系统逻辑显示启动页几秒钟就能判断是否生效。比反复在IDE里点运行要直观。7. 手动替换的踩坑记录白屏、图标虚、启动延迟这部分我踩过的坑比较典型按优先级整理出来。7.1 启动页Logo被拉伸变形很多人直接把方形大图塞进bitmap标签里然后发现启动页上Logo不是等比缩放而是被拉伸变了形。原因在于bitmap的gravity设置不对。android:gravitycenter只能保证居中但不会等比缩放图。如果原图和显示区域宽高比不一致就会拉伸。要等比缩放应该用scaleType语义的android:gravityfill加inside之类的组合其实在XML位图里没有直接的scaleType。最靠谱的做法是素材准备时就把图片尺寸调成目标显示比例的倍数比如显示区域230x230dp导出图就做460x460px放到drawable-xhdpi然后gravitycenter就不会变形了。还有更简单的思路把Logo图片的四周按比例加透明padding让画面整体宽高比接近1:1源图无论什么比例放进去都相对可控。但根本上还是建议导出几种细分的尺寸图不要指望系统自动缩放。7.2 状态栏颜色双重标准启动页背景设成了深蓝色但状态栏却是浅色系统图标时间、信号、电量变成深色糊在一起根本看不清。这个问题在Android 11和12的过渡期特别明显Android 12上有的ROM自动就深色图标了有的还是黑的显示效果就取决于系统版本。解决方式是在LaunchTheme中按需固定item nameandroid:windowLightStatusBarfalse/item同时在NormalTheme及其他正式主题里保持一致。但需要注意有些ROM强行重写状态栏样式这个只能靠厂商适配无法全局控制。7.3 背景图没打android:opacityopaque导致的闪屏卡顿layer-list的根节点如果忘了写android:opacityopaque系统会认为这个drawable是半透明的启动时就要做透明混合计算视觉上可能一闪一闪的严重时启动耗时都会变长。加上opaque等于告诉系统这张图完全不透明可以走不透明渲染的快速路径。同理背景层也不建议使用半透明颜色值尽量用不透明的纯色。7.4 Android 12启动图标一瞬间被拉伸的别扭动画windowSplashScreenAnimatedIcon设置的图标尺寸有讲究。Android 12的SplashScreen系统会自动把图标从中心放大缩放如果你的图标是那种带底色的大方块动画时会看到明显方形边框位移观感很生硬。我的建议是给windowSplashScreenAnimatedIcon准备一个没有背景色的透明前景透明Logo系统动画会柔和很多。如果实在想带底色那就把底色做成圆角矩形或圆形缩小成216x216dp的规格提供给系统不然动画非常出戏。7.5 占用FrameLayout实现复杂启动布局时的取舍纯XML的launch_background只能做纯色居中位图这种组合如果启动页想加底部版权信息、版本号、整体上下分栏这种复杂布局就得另起炉灶比如在MainActivity的onCreate里先setContentView一个自定义启动布局等Flutter第一帧回调后再切回来。但这样做有一个代价原生层和Flutter层之间可能出现短暂的空窗。我实际项目里做过一次类似原生启动布局展示 - Flutter引擎初始化完毕 - 切换ContentView 的流程切换瞬间总是很容易露白。调试了很久还是在两个布局的背景色上做一致化处理才解决。我的建议是能静态表达的布局尽量静态化只要启动页需求不复杂layer-list其实都扛得住。真到了必须复杂布局的程度再考虑原生接管但那样就要写生命周期协作代码了属于更大的工程。7.6 验证时机不对导致误判改完资源后如果用flutter run附加调试器运行启动过程会被调试模式拖慢有时候看启动页感觉卡时间特别长误以为是配置问题。想准确验证启动页的真实效果应该用flutter build apk --release装一个正式签名的包在完全非调试状态下测。同时注意release模式下首帧渲染速度比debug快很多如果release包启动页一闪而过看不清楚Logo那是正常现象。可以用adb shell screenrecord录屏回放逐帧看动画细节。这是最有效的验证手段。8. 最后再分享一点手动方案的使用体会所有步骤走完最直观的收益就是android目录下所有改动都在眼前肉眼可见地知道系统启动流程里每一步用到了哪个资源。出问题排查也快不像插件那样还得去翻插件的生成代码。实际操作中我还有一个习惯把启动页相关的颜色和属性单独命名集中放在values/colors.xml等资源文件里后面想改品牌色只改一处就好不用满目录搜#开头的色值。这个习惯帮我省了不少事因为App后续迭代中品牌换色是最常见不过的需求。另外每次跑release的安装测试包时我都会顺手确认一下flutter build apk后生成的字节码资源里的launch_background.xml确实是我最新的版本有时候IDE的缓存会让人产生改了吗怎么没变的错觉直接翻输出目录里的文件最快。整个过程中没有依赖任何插件构建流程清晰可控后续哪怕升级Flutter版本也不会因为某个第三方启动页插件不兼容而被迫锁版本。对我来说这就是手动方案最大的价值。