
先把话说在前面在 Flutter 项目里改 App 名称、包名、应用图标本身不涉及什么高深的技术但它绝对是对细心程度的一次小考。原因很简单——这三样东西看上去是改个字符串的事实际上分散在工程不同的配置文件里每一处都有自己的优先规则你漏掉任何一个地方最终效果都会以奇奇怪怪的方式反馈给你要么桌面图标没变要么 iOS 显示的名字死活改不过来要么新包名在模拟器上装不上去更别提后续接推送、接支付时被旧包名卡住的场面。这篇文章我按 Flutter 实际项目的操作顺序来写不绕弯子。先带你认识名称、包名、图标在 Android 和 iOS 两边分别由谁负责然后逐一给出可直接照做的修改步骤最后是改完之后必须检查的连锁清单。很多人只改了一个 applicationId 就以为大功告成结果代码里还藏着一堆旧包名第三方 SDK 直接在初始化阶段报错这种坑我见得太多了。你跟着这篇文章走至少能避开八成以上的改名雷区。1. 别急着动手先搞懂名称、包名、图标在项目里到底对应哪些东西新手最容易懵的地方就是没搞清概念就到处改配置。这里我先用大白话把三个概念拆开因为很多人恰恰是栽在最基础的定义上。1.1 名称的真相桌面名字、工程名字、应用显示名不是同一个东西在 Flutter 项目里pubspec.yaml 里的name是给 Dart 包管理器用的它决定你代码里import package:xxx/xxx.dart的路径前缀。这个字段跟用户手机桌面上看到的 App 名字没有任何关系但它经常被新手当成应用名称去改。真正决定用户看到的名字的在 Android 端是AndroidManifest.xml里的android:label在 iOS 端是Info.plist里的CFBundleDisplayName。这两个才是你上架后显示在桌面、应用商店、系统设置里的名字。还有一层很多人不知道Android 的AndroidManifest.xml里application和activity都可以设置android:label。application 的 label 是默认值activity 的 label 如果单独写了会覆盖 application 的设置。所以有时候你明明改了 application 的 label桌面图标下方的名字还是旧样多半就是 activity 那一层也有个 label 在捣乱。iOS 那边则是CFBundleDisplayName和CFBundleName并存前者是显示名字后者是短名系统一般在找不到前者时才用后者兜底。1.2 包名的真相Android 的 applicationId 与 namespace 是两套逻辑Android 里包名这个词最容易被误解。一个 App 在 Android 生态里至少有三个包名级别的标识applicationId最终生成 APK/AAB 之后的市场标识决定了你在应用商店的唯一身份也是很多 SDK 校验身份的凭据。它在android/app/build.gradle里配置。namespace编译期资源类R和BuildConfig的生成路径也是清单文件里各组件完整路径的前缀。它也在build.gradle里配置。package老项目旧版本 Android Gradle PluginAGP会在AndroidManifest.xml根节点写一个package属性。AGP 7.0 之后这个属性已经被废弃强制迁移到 namespace旧项目如果还带着它改包名时如果不一起处理会出问题。更关键的是还有一个物理包路径你的MainActivity.kt文件放在哪个目录下、代码里写的package com.xxx.xxx是什么。这个路径必须跟 manifest 中引用的 activity 完整类名、跟 namespace 保持逻辑一致否则编译能过安装后一启动就崩报ClassNotFoundException。iOS 那边相对简单只有一个Bundle Identifier在 Xcode 的 General 或 Build Settings 里配置。但它的影响面比 Android 只多不少推送证书、支付能力、Universal Links、Sign in with Apple全部跟这个字符串绑定。1.3 图标的真相不同系统版本会读取不同图标文件应用图标在 Android 端至少分成两套体系8.0API 26以下的系统读取mipmap-mdpi/hdpi/xhdpi/xxhdpi/xxxhdpi下的ic_launcher.png8.0 以上则优先读取mipmap-anydpi-v26/ic_launcher.xml这套 XML 引用的是 Adaptive Icon自适应图标由背景层和前景层组成系统会自动帮你做圆角、缩放和遮罩处理。所以很多人在 Android 上只替换了mipmap-xxxhdpi的 PNG结果发现 Android 12 手机上显示的还是旧图标就是因为系统实际走的是 Adaptive Icon 那套 XML。iOS 端的图标则统一放在Assets.xcassets里的AppIcon.appiconset目录下Xcode 会根据Contents.json里的尺寸描述自动去匹配。这块如果用纯手动方式你得准备一二十张各种尺寸的图很烦后面我会讲怎么用工具一次搞定。2. Android 端修改实录从 Manifest 到 Gradle 的完整链路Android 端的修改我按名称、包名、图标三个操作拆开写每一步都会说清楚当你在改什么以及为什么必须这样做。2.1 改名称android:label 的正确改法步骤其实非常简单打开android/app/src/main/AndroidManifest.xmlapplication android:labelapp_name android:iconmipmap/ic_launcher ...直接写字符串也可以但正规做法是把这个值定义在android/app/src/main/res/values/strings.xml里resources string nameapp_name你的应用新名字/string /resources这样做的收益是后续做多语言、做渠道包时都能复用。改完 strings.xml 之后注意检查两件事AndroidManifest.xml里activity节点是否也设置了android:label如果有就一起改。如果你项目里配置了buildFlavor或按渠道拆分过 manifest记得每个 flavor 的 manifest 都要检查。很多人的隐藏坑是项目里引用了第三方的启动屏插件、桌面快捷方式插件它们会生成独立的 label 配置你在主 manifest 里改了某次构建时插件又把自己的默认值合入进来导致明明改了却又回来了。这种情况下优先去插件配置文件里找一找有没有android:label相关的字段。2.2 改包名applicationId、namespace、MainActivity 位置三联动改包名最忌讳的是只改一个地方。我这里给出一套经过多次验证的顺序第一步打开android/app/build.gradle修改这两个字段android { namespace com.yourcompany.yourapp defaultConfig { applicationId com.yourcompany.yourapp ... } }如果你只是想上架、接 SDK只改 applicationId 和 namespace 通常就够了。因为 SDK 和商店校验用的是 applicationId它俩是独立于代码结构的存在。但接下来有个问题namespace改了之后系统在清单文件里拼接 activity 完整路径时用的前缀变了你的MainActivity.kt如果还躺在旧的包路径下启动就会找不到类。所以第二步移动 Kotlin 源文件。默认情况下 Flutter 创建的工程结构是android/app/src/main/kotlin/com/example/yourapp/MainActivity.kt如果你把namespace改成了com.yourcompany.yourapp那么源文件的物理路径就需要同步调整为android/app/src/main/kotlin/com/yourcompany/yourapp/MainActivity.kt同时在文件头部把package com.example.yourapp改成package com.yourcompany.yourapp。这里有一个旧项目的坑如果你是从 Flutter 早期版本一路升级上来的源码路径可能是android/app/src/main/java/...注意看清楚自己项目里到底是kotlin还是java目录别建错了路径。第三步确认AndroidManifest.xml里的 activity 写法。老写法会写完整类名例如activity android:namecom.example.yourapp.MainActivity ...这种写法在改了 namespace 之后必须同步改。新写法推荐直接用相对名.MainActivityactivity android:name.MainActivity ...用相对名时系统会自动用namespace作为前缀补全这样以后你无论怎么改 namespace只要源文件路径跟着变动manifest 都不用再动。第四步全局扫描项目里残留的旧包名。这一步最容易被忽略CtrlShiftF全局搜一下com.example.yourapp看看还有没有代码、配置、资源文件里存在它。特别是google-services.json、firebase_options.dart、各种原生插件在MainActivity里写的硬编码字符串。做完这些后执行一次干净构建flutter clean flutter pub get flutter run为什么要flutter clean因为 Android 的 R 类、BuildConfig 都是在编译期按 namespace 生成的旧缓存的 R 类路径会和新 namespace 冲突不清干净容易出现编译报错但不知道错在哪的诡异情况。2.3 图标替换mipmap 系列与 Adaptive Icon 的处理如果你的项目还兼容 Android 8.0 以下版本最朴素的做法是准备五张不同分辨率的 PNG目录推荐尺寸mipmap-mdpi48x48mipmap-hdpi72x72mipmap-xhdpi96x96mipmap-xxhdpi144x144mipmap-xxxhdpi192x192把同名的ic_launcher.png原样替换掉即可。但如果你放任 Android 8.0 以上的设备不管它们会优先读 Adaptive Icon。简单粗暴的方案是你直接把mipmap-anydpi-v26/ic_launcher.xml、ic_launcher_round.xml里的引用也改成指向mipmap/ic_launcher这样相当于告诉系统我没有自适应图标你直接用普通 PNG 吧。这样做兼容性没问题只是 Android 8 系统会自动对图片做遮罩裁剪可能切掉边缘内容。想让新图标在 Android 12 上也有完美表现正确的方案是用 flutter_launcher_icons 生成一套 Adaptive Icon我会在第 4 章详细讲。这里先提醒你一个原则源图标不要自带圆角不要自带透明度遮罩。Android 系统本身就会对图标做圆角处理你加了圆角反而会出现双层圆角看起来特别廉价。3. iOS 端修改实录Info.plist、Bundle Identifier 与 AppIcon 的三重确认iOS 端的操作比 Android 简单但坑非常集中在你以为改了却没生效这件事上。我先给一个基本前提改 iOS 相关配置一定用 Xcode 打开ios/Runner.xcworkspace不要直接改文件目录然后用 Android Studio 跑因为.pbxproj工程的引用关系容易在纯文件操作下出错。3.1 显示名称CFBundleDisplayName 最容易被忽略的覆盖规则打开ios/Runner/Info.plist你会看到类似内容keyCFBundleDisplayName/key string你的应用新名字/string如果项目是 flutter create 默认生成的这里甚至根本没有CFBundleDisplayName只有一个CFBundleName它的值通常指向工程名。你需要在 Xcode 里找到Runnertarget在 General - Identity 里直接修改Display NameXcode 会自动帮你写入CFBundleDisplayName。你也可以手动在 Info.plist 里加这个键两者效果一样。这里有一个非常隐蔽的坑如果你的项目配置了InfoPlist.strings用于多语言本地化 App 名称那么本地化文件里的值会优先于 Info.plist。常见表现就是你改了主 Info.plist清理重装之后桌面上的名字还是旧的。检查方法打开ios/Runner目录看看有没有InfoPlist.strings文件尤其是en.lproj、zh-Hans.lproj这类多语言目录如果有在里面一样要把CFBundleDisplayName改成新名字CFBundleDisplayName 你的应用新名字;3.2 Bundle Identifier改了它之后签名和描述文件全都要跟着改在 Xcode 里选中Runnertarget切到Signing Capabilities最上面一项就是Bundle Identifier比如com.example.yourapp。改成新值例如com.yourcompany.yourapp。改完之后你以为完事了不是还需要检查Build Settings里搜索PRODUCT_BUNDLE_IDENTIFIER确认没有其他 target比如 Widget Extension、Notification Service Extension也引用旧值只要有 extension每个 target 都要单独改。真机调试时Xcode 会自动帮你注册新的 App ID 并生成描述文件前提是你登录的开发者账号有权限。如果遇到 No profiles found 之类的报错需要手动去开发者后台添加 App ID。项目里如果配置了推送、Universal Links、Sign in with Apple这些能力跟 Bundle Identifier 是一对一绑定的。改了 Bundle Identifier 后必须在开发者后台重新为新的 ID 配置对应能力否则真机上跑起来这些功能全部静默失效。3.3 AppIconAssets 里的图标替换与 alpha 通道陷阱默认图标在Assets.xcassets的AppIcon.appiconset目录下里面是几十张不同尺寸的 PNG。手动替换的方法是打开这个目录的Contents.json对照尺寸逐个覆盖最烦的是还要保证每张图的命名跟 Contents.json 里的文件名对应。这里我不推荐手搓直接用 flutter_launcher_icons 生成即可。但如果你的项目图标比较复杂必须手动处理至少记住 iOS 图标的两个铁律不允许有 alpha 通道。App Store Connect 在上传时会对图标做校验带透明通道的图标直接报错报错内容通常类似 Invalid App Store Icon. The App Store Icon in the asset catalog xcassets cant be transparent nor contain an alpha channel。1024x1024 的 Marketing 图标必须提供就是 App Store 商店页面展示用的那张大图包含在所有 iPhone/iPad 图标尺寸集合的末尾。带 alpha 检查的办法最简单的就是打开预览工具查看图片是否透明或者用命令行sips -g hasAlpha icon.png输出hasAlpha: no才合格。如果带 alpha常见的处理方式是用工具去掉透明通道并填充纯色背景。4. 别一张张手搓了用 flutter_launcher_icons 一次生成全平台图标我记得第一次给 Flutter 项目换图标的时候老老实实准备了一堆尺寸Android 五张、iOS 二十多张手动覆盖完眼睛都花了。后来接触了flutter_launcher_icons这个包基本上把多尺寸图标生成这件事变成了一个命令的事。下面给出完整配置过程照抄即可。4.1 为什么建议用自动化脚本而不是手动切图图标的本质矛盾在于你在设计稿里看到的是一个方方正正的设计图但安卓的 Adaptive Icon 需要前景层、背景层分开iOS 又在不同尺寸上对圆角遮罩有不同的渲染规则。如果手动切你不仅要把每张图切成正确尺寸还要考虑前景层的安全区问题——图标主体如果不居中、占比太小最后呈现出来的效果千奇百怪。flutter_launcher_icons 这个包的价值在于它根据你提供的源图自动生成所有平台的图标并且对 Adaptive Icon 的前景层做缩放处理保证主体内容在一个安全可视区域内。它的原理不复杂就是读取你的配置然后调用脚本生成 PNG、覆盖对应工程文件。但正因为是脚本批量操作你反而需要在源图质量上把关不然错误会被放大到所有尺寸上。4.2 pubspec.yaml 中的图标配置第一步在pubspec.yaml的dev_dependencies里加入依赖dev_dependencies: flutter_launcher_icons: ^0.14.1第二步在 pubspec.yaml 末尾新增配置块flutter_launcher_icons: android: true ios: true image_path: assets/icon/app_icon.png adaptive_icon_background: #1E88E5 adaptive_icon_foreground: assets/icon/app_icon_foreground.png min_sdk_android: 21 remove_alpha_ios: true字段解释image_path主源图路径建议1024x1024以上这张图会被用来生成普通图标。这条路径是自动生成 Android 普通图标和 iOS 默认图标的源。adaptive_icon_background自适应图标的背景色或背景图。填色值最省事也可以用图片路径。adaptive_icon_foreground自适应图标的前景图。和背景层叠加系统裁剪成圆角、圆形等各种形状。remove_alpha_ios自动去掉 iOS 图标的 alpha 通道防止上架校验失败。min_sdk_android最低 Android 版本21 代表 Android 5.0如果项目 minSdk 更低要相应调整。第三步执行生成命令flutter pub get dart run flutter_launcher_icons执行完成之后你会看到 Android 的mipmap-*目录下的ic_launcher.png被批量替换iOS 的AppIcon.appiconset里也生成了全套图标。4.3 生成后的工程变化与验收生成完毕之后不要只看成功日志建议做一次验证Android 侧确认mipmap-anydpi-v26/ic_launcher.xml存在并且内容引用的是mipmap/ic_launcher。如果你之前手动改过图标这个文件可能被破坏flutter_launcher_icons 不一定能自动修复需要手动检查。iOS 侧打开Assets.xcassets/AppIcon.appiconset/Contents.json看看 1024 的 Marketing 图标是否生成了。有些旧版本工具默认不生成 Marketing 图标需要手动补一张。然后执行一遍flutter run --release重新安装而不是直接热重启。安装之后切到桌面强杀 App 图标缓存。Android 上如果桌面图标没刷新试一试重启手机或换个启动器主题这是系统桌面缓存的老毛病。我用这个工具踩过的最大坑是adaptive_icon_foreground和主image_path使用同一张图。源图是有大面积背景的设计稿把它当前景层放进 Adaptive Icon 之后主体内容过小背景被遮罩裁掉一大圈最终图标看起来非常空。正确做法是单独给前景层准备一张内容放大、占画面 60% 以上的 PNG。5. 改完之后别急着打包这里有一份收尾自查清单名字、包名、图标都改完编译跑通后我强烈建议你不要立刻提单按下面这份清单做一遍全局排查。这里面的每一项都是我或身边同事真实踩过的坑。5.1 第三方 SDK 后台的包名绑定Android 端的applicationId是 SDK 做身份识别的锚点。常见的有地图 SDK 的 Key 通常绑定包名 签名证书指纹推送 SDK 需要你上传新的包名支付 SDK 的回调 URL 和包名也绑定在一起。如果你改完包名后发现某个 SDK 在初始化时报错、或者初始化成功但不回调第一反应就应该是去对应后台看包名配置。iOS 端同理推送证书、Apple Pay 的 Merchant ID 都跟 Bundle ID 相关。特别提醒如果你用的是云函数、动态链接这类需要校验 Apple App Site Association 的能力记得新的域名文件里的 bundle id 列表也要更新。5.2 ContentProvider 与 authorities 冲突这个坑多出现在 Android 端。很多第三方 SDK最常见的是推送、统计类 SDK会在 manifest 里注册 ContentProvider并声明android:authorities。这个值的全局唯一性要求非常高但同一个 SDK 在不同包名的应用里authorities 通常写死了自己的默认值。当你改了包名就可能出现两个应用共用同一个 authorities的冲突安装时直接报 INSTALL_FAILED_CONFLICTING_PROVIDER。解决思路在AndroidManifest.xml中手动覆盖有冲突的 authorities或者在 Gradle 配置里通过 manifestPlaceholder 动态注入包名前缀defaultConfig { manifestPlaceholders [ appAuthRedirectScheme: applicationId ] }很多 SDK 文档会提供这种占位符方案你要做的就是在改完包名后重新审视一遍 manifest 合并日志app/build/outputs/logs/manifest-merger-*.txt看看有没有奇怪的 authorities 冲突。5.3 iOS 签名与描述文件前面提到 Bundle Identifier 改了之后签名要重做这里再说细一点。Xcode 的 Automatic Signing 在大多数情况下会帮你自动注册新的 App ID 并创建描述文件但遇到公司账号下 App ID 上限用完、或者有通配符描述文件时自动注册可能失败。手动创建描述文件时注意选择正确的 Certificate 和 Devices。这里最容易出现的错乱是电脑上同时存在旧包名的描述文件Xcode 选错了签名那一步会报 No profiles for 新包名 were found。5.4 代码和资源里残留的旧包名我在第 2.2 节就让你全局搜一遍旧包名这里再放到清单里强调。除了最常见的google-services.json还要注意以下几类位置android/app/src/debug/AndroidManifest.xml、android/app/src/profile/AndroidManifest.xml这类的分环境 manifest里面可能有大段完整类名引用。原生代码里的硬编码字符串比如友盟、极光等 SDK 的 channel 配置。Dart 层配置文件例如 firebase_options.dart 里每一项的appId和projectId如果你升级了 Firebase 项目它不是一串包名替换那么简单而是要从 Firebase 控制台导出整套新配置覆盖。iOS 的Info.plist里某些第三方 SDK 的配置字段比如 URL Scheme通常写的是com.yourcompany.xxx格式保持和 Bundle ID 一致才能正确唤起。5.5 多环境包名Debug 和 Release 分开的好处如果你经常在同一台测试机上同时装 debug 包和 release 包你会发现它们同名、同图标装一个把另一个覆盖掉非常影响排查。这种情况建议在build.gradle里给 debug 包加后缀buildTypes { debug { applicationIdSuffix .debug } release { // release 的 applicationId 保持正式包名 } }这样同一台手机可以同时安装 debug 包和 release 包两个应用互不覆盖而且图标和名称可以完全一致不会混淆。接推送测试时也能清楚地区分当前是哪个环境的包。5.6 命名一致性与团队协作最后讲一个团队层面的建议改包名的动作最好由项目负责人统一操作并提交合并不要每个人在自己分支上改。因为在多分支并行开发时包名一旦冲突合并代码后的 manifest 合并阶段会产生非常多难以定位的构建错误。另外设计同学给你的图标源文件务必保留一份带图层的高清源文件不要只留扁平化后的 PNG下次需要生成新尺寸图标时有源文件意味着可以随时调整安全边距和出图比例。就我个人而言现在每新建一个 Flutter 项目在flutter create阶段就会把包名、组织、项目名全部定好用的命令是flutter create --org com.yourcompany --project-name your_app .这样一开始生成的com.example就不会出现也就省掉后续所有改包名的连锁操作。这个习惯我是从接第三个外包项目时才养成的——前面两个项目都是上线前才想起来改名白白折腾了好几个晚上。如果你现在还没到那一步希望这篇文章能帮你把后面那几个折腾的晚上省下来。