🚀 25 - 发布与打包

阶段五:实战项目 · 第25课
✅ 验证通过

📖 核心概念

开发完成只是起点,将App交付到用户手中才是终点。本课覆盖Flutter App从签名配置到商店上架的完整发布流程。你将学会创建签名密钥、配置构建变体、搭建CI/CD自动化管线,以及应对各应用商店的审核要求。

🔑 签名配置

1. Android签名密钥

# 1. 创建上传密钥(upload keystore)
keytool -genkeypair -v \
  -keystore upload-keystore.jks \
  -keyalg RSA -keysize 2048 -validity 10000 \
  -alias upload \
  -storepass YOUR_STORE_PASSWORD \
  -keypass YOUR_KEY_PASSWORD

# 2. 查看密钥信息
keytool -list -v -keystore upload-keystore.jks -alias upload

# 3. 生成SHA-1和SHA-256指纹(Firebase/Google Sign-In需要)
keytool -list -v \
  -keystore upload-keystore.jks \
  -alias upload \
  -storepass YOUR_STORE_PASSWORD

2. Android签名配置文件

// android/key.properties (不要提交到版本控制!)
storePassword=YOUR_STORE_PASSWORD
keyPassword=YOUR_KEY_PASSWORD
keyAlias=upload
storeFile=/path/to/upload-keystore.jks

// android/app/build.gradle 签名配置
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
    keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}

android {
    // ...

    signingConfigs {
        release {
            keyAlias keystoreProperties['keyAlias']
            keyPassword keystoreProperties['keyPassword']
            storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
            storePassword keystoreProperties['storePassword']
        }
    }

    buildTypes {
        release {
            signingConfig signingConfigs.release
            minifyEnabled true
            shrinkResources true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

3. iOS签名与证书

# iOS签名流程(需要Apple Developer账号)
# 1. 创建证书签名请求(CSR)
# 在"钥匙串访问" → 证书助理 → 从证书颁发机构请求证书

# 2. 在Apple Developer创建:
# - Development Certificate (开发)
# - Distribution Certificate (发布)
# - Provisioning Profile (配置描述文件)

# 3. Xcode自动签名(推荐)
# Xcode → Signing & Capabilities →
#   ✅ Automatically manage signing
#   Team: 选择你的开发者团队
#   Bundle Identifier: com.yourcompany.appname

# 4. 手动签名(企业分发等场景)
# 下载.cer证书 → 双击安装到钥匙串
# 下载.mobileprovision → 放到 ~/Library/MobileDevice/Provisioning Profiles/
⚠️ 安全提醒:

📦 构建与渠道包

4. Flutter构建命令

# Android APK (单个通用包)
flutter build apk --release

# Android App Bundle (推荐,Google Play要求)
flutter build appbundle --release

# 分架构APK (体积更小)
flutter build apk --split-per-abi
# 输出:
#   app-armeabi-v7a-release.apk   (~5MB)
#   app-arm64-v8a-release.apk     (~6MB)
#   app-x86_64-release.apk        (~6MB)

# iOS
flutter build ios --release
# 或直接用Xcode: Product → Archive

# 查看包大小分析
flutter build apk --analyze-size
flutter build appbundle --analyze-size

5. 多渠道打包

// android/app/build.gradle - 多渠道配置
android {
    flavorDimensions "channel"

    productFlavors {
        google {
            dimension "channel"
            applicationIdSuffix ".google"
            manifestPlaceholders = [CHANNEL: "google"]
        }
        huawei {
            dimension "channel"
            applicationIdSuffix ".huawei"
            manifestPlaceholders = [CHANNEL: "huawei"]
        }
        xiaomi {
            dimension "channel"
            applicationIdSuffix ".xiaomi"
            manifestPlaceholders = [CHANNEL: "xiaomi"]
        }
        oppo {
            dimension "channel"
            applicationIdSuffix ".oppo"
            manifestPlaceholders = [CHANNEL: "oppo"]
        }
    }
}

// android/app/src/main/AndroidManifest.xml
// 在application标签内添加渠道meta-data
<meta-data
    android:name="CHANNEL"
    android:value="${CHANNEL}" />

// Dart端读取渠道信息
import 'package:flutter/services.dart';

class ChannelInfo {
  static const _channel = MethodChannel('app_info');

  static Future<String> getChannel() async {
    try {
      return await _channel.invokeMethod('getChannel') ?? 'unknown';
    } on PlatformException {
      return 'unknown';
    }
  }
}

// 构建各渠道包
// flutter build apk --flavor google
// flutter build apk --flavor huawei
// flutter build appbundle --flavor google

🔄 CI/CD自动化

6. GitHub Actions配置

# .github/workflows/build.yml
name: Flutter CI/CD

on:
  push:
    branches: [main, release/*]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: subosito/flutter-action@v2
        with:
          flutter-version: '3.22.0'
          channel: 'stable'

      - name: Install dependencies
        run: flutter pub get

      - name: Run tests
        run: flutter test --coverage

      - name: Check formatting
        run: dart format --set-exit-if-changed .

      - name: Analyze code
        run: flutter analyze --no-fatal-infos

  build-android:
    needs: test
    runs-on: ubuntu-latest
    if: startsWith(github.ref, 'refs/heads/release/')
    steps:
      - uses: actions/checkout@v4
      - uses: subosito/flutter-action@v2
        with:
          flutter-version: '3.22.0'

      - name: Decode keystore
        run: |
          echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > android/app/upload-keystore.jks
          echo "storePassword=${{ secrets.KEYSTORE_PASSWORD }}" > android/key.properties
          echo "keyPassword=${{ secrets.KEY_PASSWORD }}" >> android/key.properties
          echo "keyAlias=upload" >> android/key.properties
          echo "storeFile=upload-keystore.jks" >> android/key.properties

      - name: Build AAB
        run: flutter build appbundle --flavor google --release

      - name: Upload to Google Play
        uses: r0adkll/upload-google-play@v1
        with:
          serviceAccountJsonPlainText: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT }}
          packageName: com.yourcompany.appname
          releaseFiles: build/app/outputs/bundle/googleRelease/app-google-release.aab
          track: internal  # internal, alpha, beta, production
          whatsNewDirectory: distribution/whatsnew

  build-ios:
    needs: test
    runs-on: macos-latest
    if: startsWith(github.ref, 'refs/heads/release/')
    steps:
      - uses: actions/checkout@v4
      - uses: subosito/flutter-action@v2
        with:
          flutter-version: '3.22.0'

      - name: Install Apple certificates
        uses: apple-actions/import-codesign-certs@v2
        with:
          p12-file-base64: ${{ secrets.IOS_CERTIFICATE_BASE64 }}
          p12-password: ${{ secrets.IOS_CERTIFICATE_PASSWORD }}

      - name: Install provisioning profile
        run: |
          mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
          echo "${{ secrets.IOS_PROVISION_PROFILE_BASE64 }}" | base64 -d > ~/Library/MobileDevice/Provisioning\ Profiles/profile.mobileprovision

      - name: Build IPA
        run: |
          flutter build ios --release --no-codesign
          xcodebuild -workspace ios/Runner.xcworkspace \
            -scheme Runner \
            -configuration Release \
            -archivePath build/Runner.xcarchive \
            archive \
            CODE_SIGN_IDENTITY="Apple Distribution" \
            DEVELOPMENT_TEAM="${{ secrets.DEVELOPMENT_TEAM }}"
          xcodebuild -exportArchive \
            -archivePath build/Runner.xcarchive \
            -exportOptionsPlist ios/ExportOptions.plist \
            -exportPath build/ipa

      - name: Upload to App Store Connect
        run: |
          xcrun altool --upload-app \
            --type ios \
            --file build/ipa/Runner.ipa \
            --apiKey "${{ secrets.APP_STORE_API_KEY }}" \
            --apiIssuer "${{ secrets.APP_STORE_API_ISSUER }}"

🏪 商店上架

7. 上架检查清单

平台要求注意事项
Google PlayAAB格式、目标API 33+、隐私政策App Signing、内容分级、数据安全声明
App StoreIPA格式、Xcode构建、Review GuidelinesHuman Interface Guidelines、隐私清单、ATS
华为应用市场APK/AAB、隐私声明华为账号体系、HMS Core适配
小米/OPPO/vivoAPK、隐私合规各平台开发者认证流程不同

8. 版本管理与发布策略

// pubspec.yaml 版本号管理
# format: major.minor.patch+buildNumber
version: 1.2.3+45  # 1.2.3是版本号, 45是构建号

// Dart代码中获取版本号
import 'package:package_info_plus/package_info_plus.dart';

class AppInfo {
  static Future<String> get version async {
    final info = await PackageInfo.fromPlatform();
    return '${info.version}+${info.buildNumber}';
  }
}

// Semantic Versioning策略:
// MAJOR: 不兼容的API变更(2.0.0)
// MINOR: 向后兼容的功能新增(1.1.0)
// PATCH: 向后兼容的问题修复(1.0.1)
// Build Number: 每次构建递增(CI自动维护)
💡 发布最佳实践:

📊 应用分析监控

8. Firebase Analytics与Crashlytics

发布后的App需要数据驱动——了解用户行为、监控崩溃、优化性能。

// Firebase Analytics 埋点
class AnalyticsService {
  final FirebaseAnalytics _analytics = FirebaseAnalytics.instance;

  // 页面浏览
  Future<void> logPageView(String pageName) async {
    await _analytics.logEvent(
      name: 'page_view',
      parameters: {'page_name': pageName},
    );
  }

  // 自定义事件
  Future<void> logAddToCart(Product product, int quantity) async {
    await _analytics.logEvent(
      name: 'add_to_cart',
      parameters: {
        'item_id': product.id,
        'item_name': product.name,
        'item_category': product.category,
        'price': product.price,
        'quantity': quantity,
      },
    );
  }

  // 购买完成
  Future<void> logPurchase(Order order) async {
    await _analytics.logPurchase(
      value: order.total,
      currency: 'CNY',
      items: order.items.map((i) => AnalyticsEventItem(
        itemId: i.product.id,
        itemName: i.product.name,
        itemCategory: i.product.category,
        price: i.product.price,
        quantity: i.quantity,\      )).toList(),
    );
  }

  // 设置用户属性
  Future<void> setUserProperty(String name, String value) async {
    await _analytics.setUserProperty(name: name, value: value);
  }
}

// Crashlytics 崩溃监控
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();

  // 初始化Crashlytics
  FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterFatalError;
  PlatformDispatcher.instance.onError = (error, stack) {
    FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
    return true;
  };

  runApp(const MyApp());
}

// 自定义错误上报
try {
  await processPayment(order);
} catch (e, stack) {
  await FirebaseCrashlytics.instance.recordError(
    e, stack,
    reason: '支付失败: order=${order.id}',
    information: ['用户ID: ${user.id}', '订单金额: ${order.total}'],
  );
}

9. 性能监控与优化

// Flutter Performance Overlay
// 在MaterialApp中开启
MaterialApp(
  showPerformanceOverlay: kDebugMode,  // 仅调试模式
  // ...
);

// 自定义性能追踪
class PerformanceTrace {
  static Future<T> trace<T>(String name, Future<T> Function() fn) async {
    final stopwatch = Stopwatch()..start();
    try {
      final result = await fn();
      stopwatch.stop();
      // 上报性能数据
      await FirebaseAnalytics.instance.logEvent(
        name: 'performance',
        parameters: {
          'trace_name': name,
          'duration_ms': stopwatch.elapsedMilliseconds,
        },
      );
      return result;
    } catch (e) {
      stopwatch.stop();
      debugPrint('❌ $name failed in ${stopwatch.elapsedMilliseconds}ms');
      rethrow;
    }
  }
}

// 使用示例
final products = await PerformanceTrace.trace('load_products', () {
  return ProductService().getProducts(page: 1);
});

📝 练习

  1. 为你的App配置完整的GitHub Actions CI/CD——自动测试+构建+分发到内部测试轨道
  2. 实现应用内更新检查——对比当前版本和最新版本,提示用户更新
  3. 配置Firebase App Distribution——将测试包自动分发给内部测试人员
  4. 创建多语言商店信息——使用fastlane为每个商店自动生成截图和描述

🏆 成就解锁:发布大师

你已掌握App发布的完整流程!

下一课预告:社交App——Feed流、关注系统、通知推送、图片分享,综合运用所有技能!