Flutter包发布避坑指南:从审核被拒到成功上架
Flutter包发布避坑指南:从审核被拒到成功上架
Flutter 项目开发完成后,打包上架是最后一步,但往往也是问题最多的一步。iOS 的 App Store 审核、Android 各应用市场的合规要求,打包过程中的证书、签名、权限配置,每一个环节都可能出岔子。
整理一下从打包到上架的完整流程,以及常见的坑和解决方案。
iOS 打包
证书和描述文件
iOS 打包需要三种证书/文件:
- 开发证书:
ios/development_certificate.p12,用于真机调试 - 发布证书:
ios/distribution_certificate.p12,用于上架 - 描述文件:
ios/*.mobileprovision,关联证书和 App ID
常见问题:
问题 1:证书过期
iOS 开发证书有效期一年,描述文件有效期一年且不能跨证书使用。证书过期后打包会失败:
Code Signing Failed: No certificate found in the default keychain
解决方案:到 Apple Developer 后台重新创建证书和描述文件。创建后需要重新配置 Xcode 的 Signing & Capabilities。
问题 2:Bundle Identifier 冲突
每个 App 的 Bundle Identifier 必须唯一,包括不同团队账号下的 App。如果提示:
The bundle identifier "com.example.app" is already taken
解决方案:
- 到 Apple Developer 后台检查是否已有这个 Bundle ID
- 如果有但不是你的,联系原所有者转移或删除
- 如果是自己的,检查 Xcode 里的 Team 配置是否正确
问题 3:Team 选错了
Xcode Signing 里有个 "Team" 下拉框,如果选错了团队或者个人账号,会导致签名不一致。打包前确认:
- App Store Connect 上的 App 和 Xcode 里的 Team 是同一个
- Development 和 Distribution 的 Team 配置要分别检查
Xcode 构建
用命令行打包 iOS:
# 清理旧的构建
flutter clean
# 构建 iOS release
flutter build ios --release --no-codesign
--no-codesign 是关键:如果不加这个参数,Flutter 会尝试自动签名,在 CI 环境里很容易失败。
构建成功后产物在 build/ios/iphoneos/Runner.app。
上传到 App Store Connect
上传用 xcrun altool:
xcrun altool --upload-app \
-type ios \
-file build/ios/ipa/Runner.ipa \
-username "your@email.com" \
-password "app-specific-password"
注意:从 2021 年开始,上传 App Store Connect 必须用 App-Specific Password,不能用 Apple ID 密码。
创建 App-Specific Password:
- 登录 appleid.apple.com
- 安全 → App-Specific Passwords → 生成
- 记住这个密码,一次性使用
iOS 审核被拒的常见原因
原因 1:隐私权限描述缺失或不清晰
如果 App 使用了定位、相机、相册、麦克风、通讯录等权限,必须在 Info.plist 里添加对应的用途描述:
<key>NSLocationWhenInUseUsageDescription</key>
<string>我们需要获取您的位置来提供附近的服务</string>
<key>NSCameraUsageDescription</key>
<string>我们需要使用相机来拍摄商品照片</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>我们需要访问相册来选择商品图片</string>
<key>NSMicrophoneUsageDescription</key>
<string>我们需要使用麦克风来录制语音</string>
审核被拒的典型提示:We noticed that your app requests the user's permission to access their [camera], but the purpose you provided in the usage string does not clearly explain why the app needs this information.
原因 2:第三方 SDK 收集用户数据未披露
App Store Connect 的 "Privacy" 部分要求披露所有第三方 SDK 收集的数据类型:
- 位置
- 联系人
- 健康
- 财务
- 等等
如果使用了第三方 SDK(如友盟统计、极光推送),需要确认这些 SDK 会收集哪些数据,并在 App Store Connect 隐私页面如实填写。
原因 3:应用截图尺寸不对
App Store Connect 要求不同机型的截图尺寸:
- iPhone 6.5 英寸:1284 x 2778 像素
- iPhone 5.5 英寸:1242 x 2208 像素
- iPad Pro 12.9 英寸:2048 x 2732 像素
截图必须是真实设备截图,不能用模拟器截图(会被检测拒绝)。
原因 4:IPv6 网络支持
App Store 要求 App 必须支持 IPv6-only 网络环境。Flutter 的 HTTP 库默认支持 IPv6,但如果有自定义网络代码,需要检查。
Android 打包
签名配置
Android 的签名比 iOS 简单,但更容易出问题的是签名密钥管理。
签名密钥生成:
keytool -genkey -v -keystore ~/release.jks \
-keyalg RSA -keysize 2048 -validity 10000 \
-alias release
在 android/app/build.gradle 配置签名:
signingConfigs {
release {
storeFile file('path/to/release.jks')
storePassword 'your_store_password'
keyAlias 'release'
keyPassword 'your_key_password'
}
}
buildTypes {
release {
signingConfig signingConfigs.release
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
重要:不要把 keystore 文件和密码提交到代码仓库。建议用环境变量或者 gradle.properties 配置:
# gradle.properties
KEYSTORE_FILE=release.jks
KEYSTORE_PASSWORD=xxx
KEY_ALIAS=release
KEY_PASSWORD=xxx
signingConfigs {
release {
storeFile file(KEYSTORE_FILE)
storePassword KEYSTORE_PASSWORD
keyAlias KEY_ALIAS
keyPassword KEY_PASSWORD
}
}
多渠道打包
Android 通常需要按渠道打包(华为、小米、应用宝等),可以用 productFlavors 配置:
flavorDimensions "store"
productFlavors {
huawei {
dimension "store"
applicationIdSuffix ".huawei"
manifestPlaceholders["CHANNEL_NAME"] = "huawei"
}
xiaomi {
dimension "store"
applicationIdSuffix ".xiaomi"
manifestPlaceholders["CHANNEL_NAME"] = "xiaomi"
}
official {
dimension "store"
// 官方渠道不加 suffix
}
}
Manifest 里的渠道标识:
<meta-data
android:name="CHANNEL"
android:value="${CHANNEL_NAME}" />
Flutter 代码获取渠道:
const channel = String.fromEnvironment('CHANNEL', defaultValue: 'official');
打包命令:
flutter build apk --release -t flavorName
Android 11 (API 30+) 适配
Android 11 引入了更严格的权限管理,有几个常见问题:
问题 1:包可见性
如果 App 使用了自定义 ContentProvider,但 Android 11 上找不到,检查 AndroidManifest.xml:
<!-- 如果需要查询其他 App 的包 -->
<queries>
<package android:name="com.other.app" />
</queries>
问题 2:后台位置权限
Android 11 要求后台位置权限必须单独申请:
<!-- 精确位置(前台)-->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<!-- 模糊位置(前台)-->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<!-- 后台位置(需要额外申请)-->
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
用户必须先授予前台权限,才能申请后台权限。
Android 应用市场审核
华为应用市场
华为是审核最严格的市场之一,常见被拒原因:
- 隐私政策缺失或不合规:必须有独立的隐私政策页面,且页面内容要覆盖所有权限用途
- 应用截图包含敏感信息:截图里不能有电话号码、微信号等联系方式
- 应用名称或图标和他方商标近似:有品牌侵权风险
小米应用市场
小米对角标(热门、推荐等)使用有严格要求,不能在应用内自行添加这些角标。
应用宝
应用宝要求所有游戏必须申请版号,非游戏应用也需要提供软件著作权证书。
Flutter 特有的打包问题
资源文件路径
Flutter 的资源文件在 pubspec.yaml 里声明:
flutter:
assets:
- assets/images/
- assets/data/
fonts:
- family: MyFont
fonts:
- asset: assets/fonts/MyFont.ttf
资源路径是相对 pubspec.yaml 的路径,不是 Dart 文件的路径。如果资源找不到,检查:
- 路径是否正确
- 是否有尾随斜杠(
assets/images/vsassets/images) - 资源文件是否在项目根目录下(不能在
lib/子目录里)
原生库依赖
Flutter 插件如果有原生 iOS/Android 代码,需要注意:
iOS:检查 Podfile 的 platform 版本,确保不低于插件要求的最低版本:
platform :ios, '12.0' # 不能低于 12.0
Android:检查 android/app/build.gradle 的 minSdkVersion:
defaultConfig {
minSdkVersion 21 # 不能低于 21
}
热更新和代码推送
Flutter 不支持真正的热更新(代码替换),但可以通过以下方案实现动态更新:
- flutter_update:托管差量包,用户下载后替换 Dart 代码
- 应用内跳转:如果只是运营活动,可以用 WebView 或 RN/小程序方案
如果有人告诉你 Flutter 可以热更新 iOS App Store 应用,那是在说谎或者混淆概念。App Store 禁止任何形式的代码更新,所有动态代码执行都会被检测并下架。
总结
打包上架的核心避坑点:
- 证书管理:证书和描述文件要提前检查有效期,过期前及时更新
- 权限声明:所有权限都要有对应的 Info.plist 用途描述,审核时才不会被拒
- 签名安全:keystore 文件和密码不要提交到代码仓库
- 渠道适配:Android 多渠道配置要测试每个渠道的包是否正常
- 隐私合规:隐私政策页面必须真实有效,覆盖所有数据收集行为
- 资源路径:资源文件路径要正确,pubspec.yaml 声明要完整
上架只是开始,后续的版本更新、证书续期、隐私合规都是持续的工作。建议把打包流程写成自动化脚本,减少手动操作带来的出错概率。
