普通视图

发现新文章,点击刷新页面。
今天 — 2025年12月21日掘金 iOS

《Flutter全栈开发实战指南:从零到高级》- 26 -持续集成与部署

2025年12月20日 16:56

引言

代码写得再好,没有自动化的流水线,就像法拉利引擎装在牛车上!!!

什么是持续集成与部署?简单说就是:

  • 你写代码 → 自动测试 → 自动打包 → 自动发布
  • 就像工厂的流水线,代码进去,App出来

今天我们一起来搭建这条"代码流水线",让你的开发效率大幅提升!

一:CI/CD到底是什么?为什么每个团队都需要?

1.1 从手动操作到自动化流水线

先看看传统开发流程的痛点:

// 传统发布流程(手动版)
  1. 本地运行测试();       // 某些测试可能忘记运行
  2. 手动打包Android();    // 配置证书、签名、版本号...
  3. 手动打包iOS();        // 证书、描述文件、上架截图...
  4. 上传到测试平台();     // 找测试妹子要手机号
  5. 收集反馈修复bug();    // 来回沟通,效率低下
  6. 重复步骤1-5();        // 无限循环...

再看自动化流水线:

# 自动化发布流程(CI/CD版)
流程:
  1. 推送代码到GitHub/Gitlab  自动触发
  2. 运行所有测试  失败自动通知
  3. 打包所有平台  同时进行
  4. 分发到测试环境  自动分发给测试人员
  5. 发布到应用商店  条件触发

1.2 CI/CD的核心价值

很多新手觉得CI/CD是"大公司才需要的东西",其实完全错了!它解决的是这些痛点:

问题1:环境不一致

本地环境: Flutter 3.10, Dart 2.18, Mac M1
测试环境: Flutter 3.7, Dart 2.17, Windows
生产环境: ???

问题2:手动操作容易出错 之前遇到过同事把debug包发给了用户,因为打包时选错了构建变体。

问题3:反馈周期太长 代码提交 → 手动打包 → 发给测试 → 发现问题 → 已经过了半天

1.3 CI/CD的三个核心概念

graph LR
    A[代码提交] --> B[持续集成 CI]
    B --> C[持续交付 CD]
    C --> D[持续部署 CD]
    
    B --> E[自动构建]
    B --> F[自动测试]
    
    C --> G[自动打包]
    C --> H[自动发布到测试]
    
    D --> I[自动发布到生产]
    
    style A fill:#e3f2fd
    style B fill:#f3e5f5
    style C fill:#e8f5e8
    style D fill:#fff3e0

持续集成(CI):频繁集成代码到主干,每次集成都通过自动化测试

持续交付(CD):自动将代码打包成可部署的产物

持续部署(CD):自动将产物部署到生产环境

注意:两个CD虽然缩写一样,但含义不同。Continuous Delivery(持续交付)和 Continuous Deployment(持续部署)

二:GitHub Actions

我们以github为例,当然各公司有单独部署的gitlab,大同小异这里不在赘述。。。

2.1 GitHub Actions工作原理

GitHub Actions不是魔法,而是GitHub提供的自动化执行环境。想象一下:

graph LR
    A[你的代码仓库] --> B[事件推送/PR]
    B --> C[GitHub Actions服务器]
    C --> D[分配虚拟机]
    D --> E[你的工作流]
    E --> F[运行你的脚本]

    style A fill:#f9f,stroke:#333,stroke-width:1px
    style C fill:#9f9,stroke:#333,stroke-width:1px
    style E fill:#99f,stroke:#333,stroke-width:1px

核心组件解析

# 工作流组件关系图
工作流文件 (.github/workflows/ci.yml)
    ├── 触发器: 什么情况下运行 (push, pull_request)
    ├── 任务: 在什么环境下运行 (ubuntu-latest)
    └── 步骤: 具体执行什么 (安装Flutter、运行测试)

2.2 创建你的第一个工作流

别被吓到,其实创建一个基础的CI流程只需要5分钟:

  1. 在项目根目录创建文件夹
mkdir -p .github/workflows
  1. 创建CI配置文件
# .github/workflows/flutter-ci.yml
name: Flutter CI  # 工作流名称

# 触发条件:当有代码推送到main分支,或者有PR时
on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

# 设置权限
permissions:
  contents: read  # 只读权限,保证安全

# 工作流中的任务
jobs:
  # 任务1:运行测试
  test:
    # 运行在Ubuntu最新版
    runs-on: ubuntu-latest
    
    # 任务步骤
    steps:
      # 步骤1:检出代码
      - name: Checkout code
        uses: actions/checkout@v3
        
      # 步骤2:安装Flutter
      - name: Setup Flutter
        uses: subosito/flutter-action@v2
        with:
          flutter-version: '3.10.x'  # 指定Flutter版本
          channel: 'stable'          # 稳定版
        
      # 步骤3:获取依赖
      - name: Get dependencies
        run: flutter pub get
        
      # 步骤4:运行测试
      - name: Run tests
        run: flutter test
        
      # 步骤5:检查代码格式
      - name: Check formatting
        run: flutter format --set-exit-if-changed .
        
      # 步骤6:静态分析
      - name: Analyze code
        run: flutter analyze
  1. 提交并推送代码
git add .github/workflows/flutter-ci.yml
git commit -m "添加CI工作流"
git push origin main

推送到GitHub后,打开你的仓库页面,点击"Actions"标签,你会看到一个工作流正在运行!

2.3 GitHub Actions架构

graph TB
    subgraph "GitHub Actions架构"
        A[你的代码仓库] --> B[触发事件]
        B --> C[GitHub Actions Runner]
        
        subgraph "Runner执行环境"
            C --> D[创建虚拟机]
            D --> E[执行工作流]
            
            subgraph "工作流步骤"
                E --> F[检出代码]
                F --> G[环境配置]
                G --> H[执行脚本]
                H --> I[产出物]
            end
        end
        
        I --> J[结果反馈]
        J --> K[GitHub UI显示]
        J --> L[邮件/通知]
    end
    
    style A fill:#e3f2fd
    style C fill:#f3e5f5
    style E fill:#e8f5e8
    style I fill:#fff3e0

核心概念解释

  1. Runner:GitHub提供的虚拟机(或你自己的服务器),用来执行工作流
  2. Workflow:工作流,一个完整的自动化流程
  3. Job:任务,工作流中的独立单元
  4. Step:步骤,任务中的具体操作
  5. Action:可复用的操作单元,如"安装Flutter"

三:自动化测试流水线

3.1 为什么自动化测试如此重要?

功能上线前,全部功能手动测试耗时长,易出bug。加入自动化测试,有效减少bug率。

测试金字塔理论

        /\
       /  \      E2E测试(少量)
      /____\     
     /      \    集成测试(适中)
    /________\
   /          \  单元测试(大量)
  /____________\

对于Flutter,测试分为三层:

3.2 配置单元测试

单元测试是最基础的,测试单个函数或类:

# .github/workflows/unit-tests.yml
name: Unit Tests

on: [push, pull_request]

jobs:
  unit-tests:
    runs-on: ubuntu-latest
    
    strategy:
      matrix:
        # 在不同版本的Flutter上运行测试
        flutter: ['3.7.x', '3.10.x']
    
    steps:
      - name: Checkout code
        uses: actions/checkout@v3
        
      - name: Setup Flutter ${{ matrix.flutter }}
        uses: subosito/flutter-action@v2
        with:
          flutter-version: ${{ matrix.flutter }}
          
      - name: Get dependencies
        run: flutter pub get
        
      - name: Run unit tests
        run: |
          # 运行所有单元测试
          flutter test
          
          # 生成测试覆盖率报告
          flutter test --coverage
          
          # 上传覆盖率报告
          bash <(curl -s https://codecov.io/bash)

单元测试

// test/calculator_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/utils/calculator.dart';

void main() {
  group('以Calculator测试为例', () {
    late Calculator calculator;
    
    // 准备工作
    setUp(() {
      calculator = Calculator();
    });
    
    test('两个正数相加', () {
      expect(calculator.add(2, 3), 5);
    });
    
    test('正数与负数相加', () {
      expect(calculator.add(5, -3), 2);
    });
    
    test('除以零应该抛出异常', () {
      expect(() => calculator.divide(10, 0), throwsA(isA<ArgumentError>()));
    });
  });
}

3.3 配置集成测试

集成测试测试多个组件的交互:

# 集成测试工作流
jobs:
  integration-tests:
    runs-on: macos-latest  # iOS集成测试需要macOS
    
    steps:
      - name: Checkout code
        uses: actions/checkout@v3
        
      - name: Setup Flutter
        uses: subosito/flutter-action@v2
        
      - name: Get dependencies
        run: flutter pub get
        
      - name: Run integration tests
        run: |
          # 启动模拟器
          # flutter emulators --launch flutter_emulator
          
          # 运行集成测试
          flutter test integration_test/
          
      # 如果集成测试失败,上传截图辅助调试
      - name: Upload screenshots on failure
        if: failure()
        uses: actions/upload-artifact@v3
        with:
          name: integration-test-screenshots
          path: screenshots/

3.4 配置Widget测试

Widget测试测试UI组件:

jobs:
  widget-tests:
    runs-on: ubuntu-latest
    
    steps:
      - name: Checkout code
        uses: actions/checkout@v3
        
      - name: Setup Flutter
        uses: subosito/flutter-action@v2
        
      - name: Install dependencies
        run: |
          flutter pub get
          
      - name: Run widget tests
        run: |
          # 运行所有widget测试
          flutter test test/widget_test.dart
          
          # 或者运行特定目录
          flutter test test/widgets/

3.5 测试流水线

sequenceDiagram
    participant D as 开发者
    participant G as Git仓库
    participant CI as CI服务器
    participant UT as 单元测试服务
    participant WT as Widget测试服务
    participant IT as 集成测试服务
    participant R as 报告服务
    participant N as 通知服务
    
    D->>G: 推送代码
    G->>CI: 触发Webhook
    
    CI->>CI: 解析工作流配置
    CI->>CI: 分配测试资源
    
    par 并行执行
        CI->>UT: 启动单元测试
        UT->>UT: 准备环境
        UT->>UT: 执行测试
        UT->>UT: 分析覆盖率
        UT-->>CI: 返回结果
    and
        CI->>WT: 启动Widget测试
        WT->>WT: 准备UI环境
        WT->>WT: 执行测试
        WT->>WT: 截图对比
        WT-->>CI: 返回结果
    and
        CI->>IT: 启动集成测试
        IT->>IT: 准备设备
        IT->>IT: 执行测试
        IT->>IT: 端到端验证
        IT-->>CI: 返回结果
    end
    
    CI->>CI: 收集所有结果
    
    alt 所有测试通过
        CI->>R: 请求生成报告
        R->>R: 生成详细报告
        R-->>CI: 返回报告
        CI->>N: 发送成功通知
        N-->>D: 通知开发者
    else 有测试失败
        CI->>R: 请求生成错误报告
        R->>R: 生成错误报告
        R-->>CI: 返回报告
        CI->>N: 发送失败通知
        N-->>D: 警报开发者
    end

四:自动打包与发布流水线

4.1 Android自动打包

Android打包相对简单,但要注意签名问题:

# .github/workflows/android-build.yml
name: Android Build

on:
  push:
    tags:
      - 'v*'  # 只有打tag时才触发打包

jobs:
  build-android:
    runs-on: ubuntu-latest
    
    steps:
      - name: Checkout code
        uses: actions/checkout@v3
        
      - name: Setup Java
        uses: actions/setup-java@v3
        with:
          distribution: 'temurin'
          java-version: '17'
          
      - name: Setup Flutter
        uses: subosito/flutter-action@v2
        
      - name: Get dependencies
        run: flutter pub get
        
      - name: Setup keystore
        # 从GitHub Secrets读取签名密钥
        run: |
          echo "${{ secrets.ANDROID_KEYSTORE }}" > android/app/key.jks.base64
          base64 -d android/app/key.jks.base64 > android/app/key.jks
          
      - name: Build APK
        run: |
          # 构建Release版APK
          flutter build apk --release \
            --dart-define=APP_VERSION=${{ github.ref_name }} \
            --dart-define=BUILD_NUMBER=${{ github.run_number }}
            
      - name: Build App Bundle
        run: |
          # 构建App Bundle
          flutter build appbundle --release
          
      - name: Upload artifacts
        uses: actions/upload-artifact@v3
        with:
          name: android-build-${{ github.run_number }}
          path: |
            build/app/outputs/flutter-apk/app-release.apk
            build/app/outputs/bundle/release/app-release.aab

4.2 iOS自动打包

iOS打包相对复杂,需要苹果开发者账号:

# .github/workflows/ios-build.yml
name: iOS Build

on:
  push:
    tags:
      - 'v*'

jobs:
  build-ios:
    runs-on: macos-latest
    
    steps:
      - name: Checkout code
        uses: actions/checkout@v3
        
      - name: Setup Flutter
        uses: subosito/flutter-action@v2
        
      - name: Install CocoaPods
        run: |
          cd ios
          pod install
          
      - name: Setup Xcode
        run: |
          # 设置Xcode版本
          sudo xcode-select -s /Applications/Xcode_14.2.app
          
      - name: Setup provisioning profiles
        # 配置证书和描述文件
        env:
          BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE }}
          P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
          BUILD_PROVISION_PROFILE_BASE64: ${{ secrets.BUILD_PROVISION_PROFILE }}
          
        run: |
          # 导入证书
          echo $BUILD_CERTIFICATE_BASE64 | base64 --decode > certificate.p12
          
          # 创建钥匙链
          security create-keychain -p "" build.keychain
          security default-keychain -s build.keychain
          security unlock-keychain -p "" build.keychain
          
          # 导入证书到钥匙链
          security import certificate.p12 -k build.keychain \
            -P $P12_PASSWORD -T /usr/bin/codesign
          
          # 导入描述文件
          echo $BUILD_PROVISION_PROFILE_BASE64 | base64 --decode > profile.mobileprovision
          mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
          cp profile.mobileprovision ~/Library/MobileDevice/Provisioning\ Profiles/
          
      - name: Build iOS
        run: |
          # 构建iOS应用
          flutter build ipa --release \
            --export-options-plist=ios/ExportOptions.plist \
            --dart-define=APP_VERSION=${{ github.ref_name }} \
            --dart-define=BUILD_NUMBER=${{ github.run_number }}
            
      - name: Upload IPA
        uses: actions/upload-artifact@v3
        with:
          name: ios-build-${{ github.run_number }}
          path: build/ios/ipa/*.ipa

4.3 多环境构建配置

真实的项目通常有多个环境:

# 多环境构建配置
env:
  # 根据分支选择环境
  APP_ENV: ${{ github.ref == 'refs/heads/main' && 'production' || 'staging' }}
  APP_NAME: ${{ github.ref == 'refs/heads/main' && '生产' || '测试' }}

jobs:
  build:
    runs-on: ubuntu-latest
    
    strategy:
      matrix:
        # 同时构建多个Flavor
        flavor: [development, staging, production]
        platform: [android, ios]
        
    steps:
      - name: Checkout code
        uses: actions/checkout@v3
        
      - name: Setup Flutter
        uses: subosito/flutter-action@v2
        
      - name: Build ${{ matrix.platform }} for ${{ matrix.flavor }}
        run: |
          if [ "${{ matrix.platform }}" = "android" ]; then
            flutter build apk --flavor ${{ matrix.flavor }} --release
          else
            flutter build ipa --flavor ${{ matrix.flavor }} --release
          fi
          
      - name: Upload ${{ matrix.flavor }} build
        uses: actions/upload-artifact@v3
        with:
          name: ${{ matrix.platform }}-${{ matrix.flavor }}
          path: |
            build/app/outputs/flutter-apk/app-${{ matrix.flavor }}-release.apk
            build/ios/ipa/*.ipa

4.4 自动化发布到测试平台

构建完成后,自动分发给测试人员:

# 分发到测试平台
jobs:
  distribute:
    runs-on: ubuntu-latest
    needs: [build]  # 依赖build任务
    
    steps:
      - name: Download artifacts
        uses: actions/download-artifact@v3
        with:
          path: artifacts/
          
      - name: Upload to Firebase App Distribution
        # 分发到Firebase
        run: |
          # 安装Firebase CLI
          curl -sL https://firebase.tools | bash
          
          # 登录Firebase
          echo "${{ secrets.FIREBASE_TOKEN }}" > firebase_token.json
          
          # 分发Android APK
          firebase appdistribution:distribute artifacts/android-production/app-release.apk \
            --app ${{ secrets.FIREBASE_ANDROID_APP_ID }} \
            --groups "testers" \
            --release-notes-file CHANGELOG.md
            
      - name: Upload to TestFlight
        # iOS上传到TestFlight
        if: matrix.platform == 'ios'
        run: |
          # 使用altool上传到App Store Connect
          xcrun altool --upload-app \
            -f artifacts/ios-production/*.ipa \
            -t ios \
            --apiKey ${{ secrets.APPSTORE_API_KEY }} \
            --apiIssuer ${{ secrets.APPSTORE_API_ISSUER }}
            
      - name: Notify testers
        # 通知测试人员
        uses: 8398a7/action-slack@v3
        with:
          status: ${{ job.status }}
          fields: repo,message,commit,author,action,eventName,ref,workflow,job,took
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

4.5 打包发布流水线

gantt
    title Flutter打包发布流水线
    dateFormat HH:mm
    axisFormat %H:%M
    
    section 触发与准备
    代码提交检测 :00:00, 2m
    环境初始化 :00:02, 3m
    依赖安装 :00:05, 4m
    
    section Android构建
    Android环境准备 :00:05, 2m
    Android代码编译 :00:07, 6m
    Android代码签名 :00:13, 3m
    Android打包 :00:16, 2m
    
    section iOS构建
    iOS环境准备 :00:05, 3m
    iOS代码编译 :00:08, 8m
    iOS证书配置 :00:16, 4m
    iOS打包 :00:20, 3m
    
    section 测试分发
    上传到测试平台 :00:23, 5m
    测试人员通知 :00:28, 2m
    测试执行周期 :00:30, 30m
    
    section 生产发布
    测试结果评估 :01:00, 3m
    生产环境准备 :01:03, 5m
    提交到应用商店 :01:08, 10m
    商店审核等待 :01:18, 30m
    发布完成通知 :01:48, 2m
    
    section 环境配置管理
    密钥加载 :00:02, 3m
    环境变量设置 :00:05, 2m
    配置文件解析 :00:07, 3m
    版本号处理 :00:10, 2m

五:环境配置管理

5.1 为什么需要环境配置管理?

先看一个反面教材:我们项目早期,不同环境的API地址是硬编码的:

// 不推荐:硬编码配置
class ApiConfig {
  static const String baseUrl = 'https://api.production.com';
  // 测试时需要手动改成:'https://api.staging.com'
  // 很容易忘记改回来!
}

结果就是:测试时调用了生产接口,把测试数据插到了生产数据库!💥

5.2 多环境配置方案

方案一:基于Flavor的配置

// lib/config/flavors.dart
enum AppFlavor {
  development,
  staging,
  production,
}

class AppConfig {
  final AppFlavor flavor;
  final String appName;
  final String apiBaseUrl;
  final bool enableAnalytics;
  
  AppConfig({
    required this.flavor,
    required this.appName,
    required this.apiBaseUrl,
    required this.enableAnalytics,
  });
  
  // 根据Flavor创建配置
  factory AppConfig.fromFlavor(AppFlavor flavor) {
    switch (flavor) {
      case AppFlavor.development:
        return AppConfig(
          flavor: flavor,
          appName: 'MyApp Dev',
          apiBaseUrl: 'https://api.dev.xxxx.com',
          enableAnalytics: false,
        );
      case AppFlavor.staging:
        return AppConfig(
          flavor: flavor,
          appName: 'MyApp Staging',
          apiBaseUrl: 'https://api.staging.xxxx.com',
          enableAnalytics: true,
        );
      case AppFlavor.production:
        return AppConfig(
          flavor: flavor,
          appName: 'MyApp',
          apiBaseUrl: 'https://api.xxxx.com',
          enableAnalytics: true,
        );
    }
  }
}

方案二:使用dart-define传入配置

# CI配置中传入环境变量
- name: Build with environment variables
  run: |
    flutter build apk --release \
      --dart-define=APP_FLAVOR=production \
      --dart-define=API_BASE_URL=https://api.xxxx.com \
      --dart-define=ENABLE_ANALYTICS=true
// 在代码中读取环境变量
class EnvConfig {
  static const String flavor = String.fromEnvironment('APP_FLAVOR');
  static const String apiBaseUrl = String.fromEnvironment('API_BASE_URL');
  static const bool enableAnalytics = bool.fromEnvironment('ENABLE_ANALYTICS');
}

5.3 管理敏感信息

敏感信息绝不能写在代码里!

# 使用GitHub Secrets
steps:
  - name: Use secrets
    env:
      # 从Secrets读取
      API_KEY: ${{ secrets.API_KEY }}
      DATABASE_URL: ${{ secrets.DATABASE_URL }}
      SIGNING_KEY: ${{ secrets.ANDROID_SIGNING_KEY }}
      
    run: |
      # 在脚本中使用
      echo "API Key: $API_KEY"
      
      # 写入到配置文件
      echo "{ \"apiKey\": \"$API_KEY\" }" > config.json

如何设置Secrets

  1. 打开GitHub仓库 → Settings → Secrets and variables → Actions
  2. 点击"New repository secret"
  3. 输入名称和值

5.4 配置文件管理

推荐以下分层配置策略:

config/
├── .env.example          # 示例文件,不含真实值
├── .env.development      # 开发环境配置
├── .env.staging          # 测试环境配置
├── .env.production       # 生产环境配置
└── config_loader.dart    # 配置加载器
// config/config_loader.dart
import 'package:flutter_dotenv/flutter_dotenv.dart';

class ConfigLoader {
  static Future<void> load(String env) async {
    // 根据环境加载对应的配置文件
    await dotenv.load(fileName: '.env.$env');
  }
  
  static String get apiBaseUrl => dotenv.get('API_BASE_URL');
  static String get apiKey => dotenv.get('API_KEY');
  static bool get isDebug => dotenv.get('DEBUG') == 'true';
}

// main.dart
void main() async {
  // 根据编译模式选择环境
  const flavor = String.fromEnvironment('FLAVOR', defaultValue: 'development');
  
  await ConfigLoader.load(flavor);
  
  runApp(MyApp());
}

5.5 设计环境配置

graph TB
    subgraph &#34;环境配置管理架构&#34;
        A[配置来源] --> B[优先级]
        
        subgraph &#34;B[优先级]&#34;
            B1[1. 运行时环境变量] --> B2[最高优先级]
            B3[2. 配置文件] --> B4[中等优先级]
            B5[3. 默认值] --> B6[最低优先级]
        end
        
        A --> C[敏感信息处理]
        
        subgraph &#34;C[敏感信息处理]&#34;
            C1[密钥/密码] --> C2[GitHub Secrets]
            C3[API令牌] --> C4[环境变量注入]
            C5[数据库连接] --> C6[运行时获取]
        end
        
        A --> D[环境类型]
        
        subgraph &#34;D[环境类型]&#34;
            D1[开发环境] --> D2[本地调试]
            D3[测试环境] --> D4[CI/CD测试]
            D5[预发环境] --> D6[生产前验证]
            D7[生产环境] --> D8[线上用户]
        end
        
        B --> E[配置合并]
        C --> E
        D --> E
        
        E --> F[最终配置]
        
        F --> G[应用启动]
        F --> H[API调用]
        F --> I[功能开关]
    end
    
    subgraph &#34;安全实践&#34;
        J[永远不要提交] --> K[.env文件到Git]
        L[使用.gitignore] --> M[忽略敏感文件]
        N[定期轮换] --> O[密钥和令牌]
        P[最小权限原则] --> Q[仅授予必要权限]
    end
    
    style A fill:#e3f2fd
    style C fill:#f3e5f5
    style D fill:#e8f5e8
    style J fill:#fff3e0

六:常见CI/CD技巧

6.1 使用缓存加速构建

Flutter项目依赖下载很慢,使用缓存可以大幅提速:

jobs:
  build:
    runs-on: ubuntu-latest
    
    steps:
      - name: Checkout code
        uses: actions/checkout@v3
        
      - name: Cache Flutter dependencies
        uses: actions/cache@v3
        with:
          path: |
            /opt/hostedtoolcache/flutter
            ${{ github.workspace }}/.pub-cache
            ${{ github.workspace }}/build
          key: ${{ runner.os }}-flutter-${{ hashFiles('pubspec.lock') }}
          restore-keys: |
            ${{ runner.os }}-flutter-
            
      - name: Cache Android dependencies
        uses: actions/cache@v3
        with:
          path: |
            ~/.gradle/caches
            ~/.gradle/wrapper
          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
          restore-keys: |
            ${{ runner.os }}-gradle-

6.2 构建策略

同时测试多个配置组合:

jobs:
  test:
    runs-on: ${{ matrix.os }}
    
    strategy:
      matrix:
        # 定义
        os: [ubuntu-latest, macos-latest]
        flutter-version: ['3.7.x', '3.10.x']
    
        exclude:
          - os: macos-latest
            flutter-version: '3.7.x'
        # 包含特定组合
        include:
          - os: windows-latest
            flutter-version: '3.10.x'
            channel: 'beta'
            
    steps:
      - name: Test on ${{ matrix.os }} with Flutter ${{ matrix.flutter-version }}
        run: echo "Running tests..."

6.3 条件执行与工作流控制

jobs:
  deploy:
    # 只有特定分支才执行
    if: github.ref == 'refs/heads/main'
    
    runs-on: ubuntu-latest
    
    steps:
      - name: Check changed files
        # 只有特定文件改动才执行
        uses: dorny/paths-filter@v2
        id: changes
        with:
          filters: |
            src:
              - 'src/**'
            configs:
              - 'config/**'
              
      - name: Run if src changed
        if: steps.changes.outputs.src == 'true'
        run: echo "Source code changed"
        
      - name: Skip if only docs changed
        if: github.event_name == 'pull_request' && contains(github.event.pull_request.title, '[skip-ci]')
        run: |
          echo "Skipping CI due to [skip-ci] in PR title"
          exit 0

6.4 自定义Actions

当通用Actions不够用时,可以自定义:

# .github/actions/flutter-setup/action.yml
name: 'Flutter Setup with Custom Options'
description: 'Setup Flutter environment with custom configurations'

inputs:
  flutter-version:
    description: 'Flutter version'
    required: true
    default: 'stable'
  channel:
    description: 'Flutter channel'
    required: false
    default: 'stable'
  enable-web:
    description: 'Enable web support'
    required: false
    default: 'false'

runs:
  using: "composite"
  steps:
    - name: Setup Flutter
      uses: subosito/flutter-action@v2
      with:
        flutter-version: ${{ inputs.flutter-version }}
        channel: ${{ inputs.channel }}
        
    - name: Enable web if needed
      if: ${{ inputs.enable-web == 'true' }}
      shell: bash
      run: flutter config --enable-web
      
    - name: Install licenses
      shell: bash
      run: flutter doctor --android-licenses

七:为现有项目添加CI/CD

7.1 分析现有项目

如果我们有一个现成的Flutter应用,需要添加CI/CD:

项目结构:
my_flutter_app/
├── lib/
├── test/
├── android/
├── ios/
└── pubspec.yaml

当前问题

  1. 手动测试,经常漏测
  2. 打包需要20分钟,且容易出错
  3. 不同开发者环境不一致
  4. 发布流程繁琐

7.2 分阶段实施自动化

第一阶段:实现基础CI

  • 添加基础测试流水线
  • 代码质量检查
  • 配置GitHub Actions

第二阶段:自动化构建

  • Android自动打包
  • iOS自动打包
  • 多环境配置

第三阶段:自动化发布

  • 测试环境自动分发
  • 生产环境自动发布
  • 监控与告警

7.3 配置文件

# .github/workflows/ecommerce-ci.yml
name: E-commerce App CI/CD

on:
  push:
    branches: [develop]
  pull_request:
    branches: [main, develop]
  schedule:
    # 每天凌晨2点跑一遍测试
    - cron: '0 2 * * *'

jobs:
  # 代码质量
  quality-gate:
    runs-on: ubuntu-latest
    
    outputs:
      passed: ${{ steps.quality-check.outputs.passed }}
    
    steps:
      - uses: actions/checkout@v3
      
      - name: Quality Check
        id: quality-check
        run: |
          # 代码规范检查
          flutter analyze . || echo "::warning::Code analysis failed"
          
          # 检查测试覆盖率
          flutter test --coverage
          PERCENTAGE=$(lcov --summary coverage/lcov.info | grep lines | awk '{print $4}' | sed 's/%//')
          if (( $(echo "$PERCENTAGE < 80" | bc -l) )); then
            echo "::error::Test coverage $PERCENTAGE% is below 80% threshold"
            echo "passed=false" >> $GITHUB_OUTPUT
          else
            echo "passed=true" >> $GITHUB_OUTPUT
          fi
          
  # 集成测试
  integration-test:
    needs: quality-gate
    if: needs.quality-gate.outputs.passed == 'true'
    
    runs-on: macos-latest
    
    services:
      # 启动测试数据库
      postgres:
        image: postgres:14
        env:
          POSTGRES_PASSWORD: postgres
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
          
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup Flutter
        uses: subosito/flutter-action@v2
        
      - name: Run integration tests with database
        env:
          DATABASE_URL: postgres://postgres:postgres@postgres:5432/test_db
        run: |
          flutter test integration_test/ --dart-define=DATABASE_URL=$DATABASE_URL
          
  # 性能测试
  performance-test:
    needs: integration-test
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v3
      
      - name: Run performance benchmarks
        run: |
          # 运行性能测试
          flutter drive --target=test_driver/app_perf.dart
          
          # 分析性能数据
          dart analyze_performance.dart perf_data.json
          
      - name: Upload performance report
        uses: actions/upload-artifact@v3
        with:
          name: performance-report
          path: perf_report.json
          
  # 安全扫描
  security-scan:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v3
      
      - name: Run security scan
        uses: snyk/actions/dart@master
        env:
          SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
          
      - name: Check for secrets in code
        uses: trufflesecurity/trufflehog@main
        with:
          path: ./
          
  # 报告
  report:
    needs: [quality-gate, integration-test, performance-test, security-scan]
    runs-on: ubuntu-latest
    
    if: always()
    
    steps:
      - name: Generate CI/CD Report
        run: |
          echo "# CI/CD Run Report" > report.md
          echo "## Run: ${{ github.run_id }}" >> report.md
          echo "## Status: ${{ job.status }}" >> report.md
          echo "## Jobs:" >> report.md
          echo "- Quality Gate: ${{ needs.quality-gate.result }}" >> report.md
          echo "- Integration Test: ${{ needs.integration-test.result }}" >> report.md
          echo "- Performance Test: ${{ needs.performance-test.result }}" >> report.md
          echo "- Security Scan: ${{ needs.security-scan.result }}" >> report.md
          
      - name: Upload report
        uses: actions/upload-artifact@v3
        with:
          name: ci-cd-report
          path: report.md

7.4 流程优化

CI/CD不是一次性的,需要持续优化:

# 监控CI/CD性能
name: CI/CD Performance Monitoring

on:
  workflow_run:
    workflows: ["E-commerce App CI/CD"]
    types: [completed]

jobs:
  analyze-performance:
    runs-on: ubuntu-latest
    
    steps:
      - name: Download workflow artifacts
        uses: actions/github-script@v6
        with:
          script: |
            const { data: artifacts } = await github.rest.actions.listWorkflowRunArtifacts({
              owner: context.repo.owner,
              repo: context.repo.repo,
              run_id: context.payload.workflow_run.id,
            });
            
            // 分析执行时间
            const runDuration = new Date(context.payload.workflow_run.updated_at) - 
                               new Date(context.payload.workflow_run.run_started_at);
            
            console.log(`Workflow took ${runDuration / 1000} seconds`);
            
            // 发送到监控系统
            // ...
            
      - name: Send to monitoring
        run: |
          # 发送指标到Prometheus/Grafana
          echo "ci_duration_seconds $DURATION" | \
            curl -X POST -H "Content-Type: text/plain" \
            --data-binary @- http://monitoring.xxxx.com/metrics

八:常见问题

8.1 GitHub Actions常见问题

Q:工作流运行太慢怎么办?

A:优化手段:

# 1. 使用缓存
- uses: actions/cache@v3
  with:
    path: ~/.pub-cache
    key: ${{ runner.os }}-pub-${{ hashFiles('pubspec.lock') }}

# 2. 并行执行独立任务
jobs:
  test-android:
    runs-on: ubuntu-latest
  test-ios:
    runs-on: macos-latest
  # 两个任务会并行执行

# 3. 项目大可以考虑使用自托管Runner
runs-on: [self-hosted, linux, x64]

Q:iOS构建失败,证书问题?

A:iOS证书配置流程:

# 1. 导出开发证书
openssl pkcs12 -in certificate.p12 -out certificate.pem -nodes

# 2. 在GitHub Secrets中存储
# 使用base64编码
base64 -i certificate.p12 > certificate.txt

# 3. 在CI中还原
echo "${{ secrets.IOS_CERTIFICATE }}" | base64 --decode > certificate.p12
security import certificate.p12 -k build.keychain -P "${{ secrets.CERT_PASSWORD }}"

Q:如何调试失败的CI?

A:调试技巧:

# 1. 启用调试日志
run: |
  # 显示详细日志
  flutter build apk --verbose
  
  # 或使用环境变量
  env:
    FLUTTER_VERBOSE: true

# 2. 上传构建日志
- name: Upload build logs
  if: failure()
  uses: actions/upload-artifact@v3
  with:
    name: build-logs
    path: |
      ~/flutter/bin/cache/
      build/
      
# 3. 使用tmate进行SSH调试
- name: Setup tmate session
  uses: mxschmitt/action-tmate@v3
  if: failure() && github.ref == 'refs/heads/main'

8.2 Flutter问题

Q:不同版本兼容性?

A:版本管理策略:

# 使用版本测试兼容性
strategy:
  matrix:
    flutter-version: ['3.7.x', '3.10.x', 'stable']
    
# 在代码中检查版本
void checkFlutterVersion() {
  const minVersion = '3.7.0';
  final currentVersion = FlutterVersion.instance.version;
  
  if (Version.parse(currentVersion) < Version.parse(minVersion)) {
    throw Exception('Flutter version $minVersion or higher required');
  }
}

Q:Web构建失败?

A:Web构建配置:

# 确保启用Web支持
- name: Enable web
  run: flutter config --enable-web

# 构建Web版本
- name: Build for web
  run: |
    flutter build web \
      --web-renderer canvaskit \
      --release \
      --dart-define=FLUTTER_WEB_USE_SKIA=true
      
# 处理Web特定问题
- name: Fix web issues
  run: |
    # 清理缓存
    flutter clean
    
    # 更新Web引擎
    flutter precache --web

8.3 安全与权限问题

Q:如何管理敏感信息?

A:安全实践:

# 1. 使用环境级别的Secrets
env:
  SUPER_SECRET_KEY: ${{ secrets.PRODUCTION_KEY }}

# 2. 最小权限原则
permissions:
  contents: read
  packages: write  # 只有需要时才写
  
# 3. 使用临时凭证
- name: Configure AWS credentials
  uses: aws-actions/configure-aws-credentials@v1
  with:
    aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
    aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
    aws-region: us-east-1
    
# 4. 定期轮换密钥
# 设置提醒每月更新一次Secrets

最后

通过这篇教程我们掌握了Flutter CI/CD的核心知识,一个完美的流水线是一次次迭代出来的,需要不断优化。如果觉得文章对你有帮助,别忘了一键三连,支持一下


有任何问题或想法,欢迎在评论区交流讨论。

昨天 — 2025年12月20日掘金 iOS

Xcode 26还没有适配SceneDelegate的app建议尽早适配

作者 wvy
2025年12月19日 19:35

Xcode 26之前不需要多窗口的很多app没有适配SceneDelegate,升级到Xcode 26后运行没有问题,但是控制台有以下输出:

`UIScene` lifecycle will soon be required. Failure to adopt will result in an assert in the future.

UIApplicationDelegate 中的相关生命周期函数也有弃用标记:

/// Tells the delegate that the application has become active 
/// - Note: This method is not called if `UIScene` lifecycle has been adopted. 
- (void)applicationDidBecomeActive:(UIApplication *)application API_DEPRECATED("Use UIScene lifecycle and sceneDidBecomeActive(_:) from UISceneDelegate or the UIApplication.didBecomeActiveNotification instead.", ios(2.0, 26.0), tvos(9.0, 26.0), visionos(1.0, 26.0)) API_UNAVAILABLE(watchos);

建议尽早适配

方案举例

以下是我的适配方案,供大家参考

  • 兼容iOS13以下版本;
  • app只有单窗口场景。

1. 配置Info.plist

Delegate Class Name和Configuration Name 可自定义

image.png

2. 配置SceneDelegate

  • 创建SceneDelegate class 类名要和Info.plist中配置一致

image.png

  • appDelegate中实现代理
- (UISceneConfiguration *)application:(UIApplication *)application configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession options:(UISceneConnectionOptions *)options  API_AVAILABLE(ios(13.0)){
   //  name要和Info.plist中配置一致
  return [[UISceneConfiguration alloc] initWithName:@"Default Configuration" sessionRole:connectingSceneSession.role];
}

- (void)application:(UIApplication *)application didDiscardSceneSessions:(NSSet<UISceneSession *> *)sceneSessions  API_AVAILABLE(ios(13.0)){
  // 释放资源,单窗口app不用关注
}

3. 新建单例 AppLifecycleHelper 实现AppDelegate和SceneDelgate共享的方法

  • iOS 13 及以上需要在scene: willConnectToSession: options: 方法中创建Window,之前仍然在 didFinishLaunchingWithOptions:

AppDelegate:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    [AppLifecycleHelper sharedInstance].launchOptions = launchOptions;
     // ... 自定义逻辑
    if (@available(iOS 13, *)) {
 
    } else {
        [[AppLifecycleHelper sharedInstance] createKeyWindow];
    }
}

SceneDelgate:

URL冷启动APP时不调用openURLContexts方法,这里保存URL在DidBecomeActive处理

- (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions  API_AVAILABLE(ios(13.0)){
    [[AppLifecycleHelper sharedInstance] createKeyWindowWithScene:(UIWindowScene *)scene];
    // 通过url冷启动app,一般只有一个url 
    for (UIOpenURLContext *context **in** connectionOptions.URLContexts) {
        NSURL *URL = context.URL;
        if (URL && URL.absoluteString.length > 0) {
            self.launchUrl = URL;
        }
    }
}

AppLifecycleHelper:

- (void)createKeyWindow {
    UIWindow *window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
    [self setupMainWindow:window];
}

- (void)createKeyWindowWithScene:(UIWindowScene *)scene API_AVAILABLE(ios(13.0)) {
    UIWindow *window = [[UIWindow alloc] initWithWindowScene:scene];
    [self setupMainWindow:window];
}

- (void)setupMainWindow:(UIWindow *)window {
}
  • 实现SceneDelegate后appDelegate 中失效的方法

AppLifecycleHelper中实现,共享给两个DelegateClass

- (void)applicationDidBecomeActive:(UIApplication *)application {
    [[AppLifecycleHelper sharedInstance] appDidBecomeActive];
}

- (void)applicationWillResignActive:(UIApplication *)application {

}

- (void)applicationDidEnterBackground:(UIApplication *)application {

}

- (void)applicationWillEnterForeground:(UIApplication *)application {

}
  
 /// URL Scheme
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey, **id**> *)options {

}

/// 接力用户活动
- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray<**id**<UIUserActivityRestoring>> * _Nullable))restorationHandler {

}

/// 快捷方式点击
- (void)application:(UIApplication *)application performActionForShortcutItem:(UIApplicationShortcutItem *)shortcutItem completionHandler:(void (^)(BOOL))completionHandler API_AVAILABLE(ios(9.0)) {
}

SceneDelegate部分代码示例:


- (void)sceneDidBecomeActive:(UIScene *)scene  API_AVAILABLE(ios(13.0)){
    [[AppLifecycleHelper sharedInstance] appDidBecomeActiveWithLaunchUrl:self.launchUrl];
    // 清空冷启动时的url
    self.launchUrl = nil;
}

这个方法总结下来就是求同存异,由Helper提供SceneDelegate与AppDelegate相同或类似的方法,适合单窗口、且支持iOS 13以下的app;

另外注意URL Scheme冷启动app不会执行openURL需要记录URL,在合适的时机(一般是DidBecomeActive)处理。

UIWindowScene 使用指南:掌握 iOS 多窗口架构

作者 sweet丶
2025年12月19日 20:32

引言

在 iOS 13 之前,iOS 应用通常只有一个主窗口(UIWindow)。但随着 iPadOS 的推出和多任务处理需求的增加,Apple 引入了 UIWindowScene 架构,让单个应用可以同时管理多个窗口,每个窗口都有自己的场景(Scene)。本文将深入探讨 UIWindowScene 的核心概念和使用方法。

什么是 UIWindowScene?

UIWindowScene 是 iOS 13+ 中引入的新架构,它代表了应用程序用户界面的一个实例。每个场景都有自己的窗口、视图控制器层级和生命周期管理。

核心组件关系

UISceneSessionUIWindowSceneUIWindowUIViewControllerUISceneConfiguration

基础配置

1. 项目设置

首先需要在 Info.plist 中启用多场景支持:

<key>UIApplicationSceneManifest</key>
<dict>
    <key>UIApplicationSupportsMultipleScenes</key>
    <true/>
    <key>UISceneConfigurations</key>
    <dict>
        <key>UIWindowSceneSessionRoleApplication</key>
        <array>
            <dict>
                <key>UISceneConfigurationName</key>
                <string>Default Configuration</string>
                <key>UISceneDelegateClassName</key>
                <string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
                <key>UISceneStoryboardFile</key>
                <string>Main</string>
            </dict>
        </array>
    </dict>
</dict>

2. SceneDelegate 实现

import UIKit

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow?
    
    func scene(_ scene: UIScene, 
               willConnectTo session: UISceneSession, 
               options connectionOptions: UIScene.ConnectionOptions) {
        
        guard let windowScene = (scene as? UIWindowScene) else { return }
        
        window = UIWindow(windowScene: windowScene)
        window?.rootViewController = YourRootViewController()
        window?.makeKeyAndVisible()
        
        // 处理深度链接
        if let userActivity = connectionOptions.userActivities.first {
            self.scene(scene, continue: userActivity)
        }
    }
    
    func sceneDidDisconnect(_ scene: UIScene) {
        // 场景被系统释放时调用
    }
    
    func sceneDidBecomeActive(_ scene: UIScene) {
        // 场景变为活动状态时调用
    }
    
    func sceneWillResignActive(_ scene: UIScene) {
        // 场景即将变为非活动状态时调用
    }
    
    func sceneWillEnterForeground(_ scene: UIScene) {
        // 场景即将进入前台
    }
    
    func sceneDidEnterBackground(_ scene: UIScene) {
        // 场景进入后台
    }
}

创建和管理多个场景

1. 动态创建新窗口

class SceneManager {
    static func createNewScene(with userInfo: [String: Any]? = nil) {
        let activity = NSUserActivity(activityType: "com.yourapp.newWindow")
        activity.userInfo = userInfo
        activity.targetContentIdentifier = "newWindow"
        
        let options = UIScene.ActivationRequestOptions()
        options.requestingScene = UIApplication.shared.connectedScenes.first as? UIWindowScene
        
        UIApplication.shared.requestSceneSessionActivation(
            nil,
            userActivity: activity,
            options: options,
            errorHandler: { error in
                print("Failed to create new scene: \(error)")
            }
        )
    }
}

2. 场景配置管理

// 自定义场景配置
class CustomSceneDelegate: UIResponder, UIWindowSceneDelegate {
    static let configurationName = "CustomSceneConfiguration"
    
    func scene(_ scene: UIScene, 
               willConnectTo session: UISceneSession, 
               options connectionOptions: UIScene.ConnectionOptions) {
        
        guard let windowScene = scene as? UIWindowScene else { return }
        
        // 根据场景角色自定义配置
        if session.role == .windowApplication {
            configureApplicationWindow(scene: windowScene, 
                                      session: session, 
                                      options: connectionOptions)
        } else if session.role == .windowExternalDisplay {
            configureExternalDisplayWindow(scene: windowScene)
        }
    }
    
    private func configureApplicationWindow(scene: UIWindowScene,
                                          session: UISceneSession,
                                          options: UIScene.ConnectionOptions) {
        // 主窗口配置
        let window = UIWindow(windowScene: scene)
        
        // 根据用户活动恢复状态
        if let userActivity = options.userActivities.first {
            window.rootViewController = restoreViewController(from: userActivity)
        } else {
            window.rootViewController = UIViewController()
        }
        
        window.makeKeyAndVisible()
        self.window = window
    }
}

场景间通信与数据共享

1. 使用 UserActivity 传递数据

class DocumentViewController: UIViewController {
    var document: Document?
    
    func openInNewWindow() {
        guard let document = document else { return }
        
        let userActivity = NSUserActivity(activityType: "com.yourapp.editDocument")
        userActivity.title = "Editing \(document.title)"
        userActivity.userInfo = ["documentId": document.id]
        userActivity.targetContentIdentifier = document.id
        
        let options = UIScene.ActivationRequestOptions()
        UIApplication.shared.requestSceneSessionActivation(
            nil,
            userActivity: userActivity,
            options: options,
            errorHandler: nil
        )
    }
}

// 在 SceneDelegate 中处理
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
    guard let windowScene = scene as? UIWindowScene,
          let documentId = userActivity.userInfo?["documentId"] as? String else {
        return
    }
    
    let document = fetchDocument(by: documentId)
    let editorVC = DocumentEditorViewController(document: document)
    windowScene.windows.first?.rootViewController = editorVC
}

2. 使用通知中心通信

extension Notification.Name {
    static let documentDidChange = Notification.Name("documentDidChange")
    static let sceneDidBecomeActive = Notification.Name("sceneDidBecomeActive")
}

class DocumentManager {
    static let shared = DocumentManager()
    private init() {}
    
    func updateDocument(_ document: Document) {
        // 更新数据
        NotificationCenter.default.post(
            name: .documentDidChange,
            object: nil,
            userInfo: ["document": document]
        )
    }
}

高级功能

1. 外部显示器支持

class ExternalDisplayManager {
    static func setupExternalDisplay() {
        // 监听外部显示器连接
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(handleScreenConnect),
            name: UIScreen.didConnectNotification,
            object: nil
        )
    }
    
    @objc private static func handleScreenConnect(notification: Notification) {
        guard let newScreen = notification.object as? UIScreen,
              newScreen != UIScreen.main else { return }
        
        let options = UIScene.ActivationRequestOptions()
        options.requestingScene = UIApplication.shared.connectedScenes.first as? UIWindowScene
        
        let activity = NSUserActivity(activityType: "externalDisplay")
        UIApplication.shared.requestSceneSessionActivation(
            nil,
            userActivity: activity,
            options: options,
            errorHandler: nil
        )
    }
}

// 在 SceneDelegate 中配置外部显示器场景
func configureExternalDisplayWindow(scene: UIWindowScene) {
    let window = UIWindow(windowScene: scene)
    window.screen = UIScreen.screens.last // 使用外部显示器
    window.rootViewController = ExternalDisplayViewController()
    window.makeKeyAndVisible()
}

2. 场景状态保存与恢复

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    
    func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? {
        // 返回用于恢复场景状态的 activity
        let activity = NSUserActivity(activityType: "restoration")
        if let rootVC = window?.rootViewController as? Restorable {
            activity.addUserInfoEntries(from: rootVC.restorationInfo)
        }
        return activity
    }
    
    func scene(_ scene: UIScene, 
               willConnectTo session: UISceneSession, 
               options connectionOptions: UIScene.ConnectionOptions) {
        
        // 检查是否有保存的状态
        if let restorationActivity = session.stateRestorationActivity {
            restoreState(from: restorationActivity)
        }
    }
}

最佳实践

1. 内存管理

class MemoryAwareSceneDelegate: UIResponder, UIWindowSceneDelegate {
    
    func sceneDidEnterBackground(_ scene: UIScene) {
        // 释放不必要的资源
        if let vc = window?.rootViewController as? MemoryManageable {
            vc.releaseUnnecessaryResources()
        }
    }
    
    func sceneWillEnterForeground(_ scene: UIScene) {
        // 恢复必要的资源
        if let vc = window?.rootViewController as? MemoryManageable {
            vc.restoreResources()
        }
    }
}

2. 错误处理

enum SceneError: Error {
    case sceneCreationFailed
    case invalidConfiguration
    case resourceUnavailable
}

class RobustSceneManager {
    static func createSceneSafely(configuration: UISceneConfiguration,
                                completion: @escaping (Result<UIWindowScene, SceneError>) -> Void) {
        
        let options = UIScene.ActivationRequestOptions()
        
        UIApplication.shared.requestSceneSessionActivation(
            nil,
            userActivity: nil,
            options: options
        ) { error in
            if let error = error {
                completion(.failure(.sceneCreationFailed))
            } else {
                // 监控新场景创建
                DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
                    if let newScene = UIApplication.shared.connectedScenes
                        .compactMap({ $0 as? UIWindowScene })
                        .last {
                        completion(.success(newScene))
                    } else {
                        completion(.failure(.sceneCreationFailed))
                    }
                }
            }
        }
    }
}

调试技巧

1. 场景信息日志

extension UIWindowScene {
    func logSceneInfo() {
        print("""
        Scene Information:
        - Session: \(session)
        - Role: \(session.role)
        - Windows: \(windows.count)
        - Screen: \(screen)
        - Activation State: \(activationState)
        """)
    }
}

// 在 AppDelegate 中监控所有场景
func application(_ application: UIApplication, 
               configurationForConnecting connectingSceneSession: UISceneSession,
               options: UIScene.ConnectionOptions) -> UISceneConfiguration {
    
    print("Connecting scene: \(connectingSceneSession)")
    return UISceneConfiguration(
        name: "Default Configuration",
        sessionRole: connectingSceneSession.role
    )
}

2. 内存泄漏检测

class SceneLeakDetector {
    static var activeScenes: [String: WeakReference<UIWindowScene>] = [:]
    
    static func trackScene(_ scene: UIWindowScene) {
        let identifier = "\(ObjectIdentifier(scene).hashValue)"
        activeScenes[identifier] = WeakReference(object: scene)
        
        // 定期检查泄漏
        DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
            self.checkForLeaks()
        }
    }
    
    private static func checkForLeaks() {
        activeScenes = activeScenes.filter { $0.value.object != nil }
        print("Active scenes: \(activeScenes.count)")
    }
}

class WeakReference<T: AnyObject> {
    weak var object: T?
    init(object: T) {
        self.object = object
    }
}

兼容性考虑

1. 向后兼容 iOS 12

@available(iOS 13.0, *)
class ModernSceneDelegate: UIResponder, UIWindowSceneDelegate {
    // iOS 13+ 实现
}

class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?
    
    func application(_ application: UIApplication, 
                   didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        
        if #available(iOS 13.0, *) {
            // 使用场景架构
        } else {
            // 传统 UIWindow 设置
            window = UIWindow(frame: UIScreen.main.bounds)
            window?.rootViewController = UIViewController()
            window?.makeKeyAndVisible()
        }
        return true
    }
}

结语

UIWindowScene 架构为 iOS 应用带来了强大的多窗口支持,特别适合 iPadOS 和需要复杂多任务处理的应用。通过合理使用场景管理,可以:

  1. 提供更好的多任务体验
  2. 支持外部显示器
  3. 实现高效的状态保存与恢复
  4. 优化内存使用

虽然学习曲线较陡,但掌握 UIWindowScene 将显著提升应用的现代化水平和用户体验。


示例项目: 完整的示例代码可以在 GitHub 仓库 找到。

进一步阅读:

swift中的知识总结(一)

2025年12月19日 15:23

一、associatedtype的用法

在swift中,泛型T是一个非常强大的特性,它允许我们编写灵活且可复用的代码。而当我们在 协议(Protocol) 中需要使用泛型时,associatedtype 就派上了用场。

在 Swift 的协议中,我们无法直接使用泛型 <T>,但可以使用 associatedtype 关键字来声明一个占位类型,让协议在不确定具体类型的情况下仍然能够正常使用。

1、让协议支持不同数据类型的

protocol SomeProtocol {
    associatedtype SomeType // 声明一个占位类型 SomeType,但不指定具体类型。
    func doSomething(with value: SomeType)
}

// Int类型
protocol SomeProtocol {
    associatedtype Item
    mutating func doSomething(with value: Item)
    func getItem(at index: Int) -> Item
}

struct ContainerDemo: SomeProtocol {

    typealias Item = Int // 指定Item为Int类型
    private var items: [Int] = []

    mutating func doSomething(with value: Int) {
        items.append(value)
        print(value)
    }

    func getItem(at index: Int) -> Int {
        return items[index]
    }
}

// String类型
struct StringContainer: SomeProtocol {

    typealias Item = String
    private var items: [String] = []

    mutating func doSomething(with value: String) {
        items.append(value)
    }

    func getItem(at index: Int) -> String {
        return items[index]
    }
}

protocol StackProtocol {
    associatedtype Element
    mutating func push(_ item: Element)
    mutating func pop() -> Element?
}

struct IntStack: StackProtocol {

    typealias Element = Int
    private var stacks: [Int] = []

    mutating func push(_ item: Int) {
        stacks.append(item)
    }

    mutating func pop() -> Int? {
        return stacks.popLast()
    }
}

2、使用where关键词限定类型

有时候希望assocaitedtype只能是某种类型的子类或实现了某个协议。可以使用where关键字进行类型约束

protocol Summable {
    associatedtype Number: Numeric // 限定Number必须是Numeric协议的子类型( Int、Double)
     func sum(a: Number,b: Number) -> Number
}

struct myIntergerAddr: Summable {
     func sum(a: Int, b: Int) -> Int {
        return a + b
    }
}

// 使用泛型结构体遵循协议
struct myGenericSatck<T>: StackProtocol {
    
    private var elements: [T] = []
    var isEmpty: Bool {return elements.isEmpty}
    var count: Int {return elements.count}

    mutating func push(_ item: T) {
        elements.append(item)
    }

    mutating func pop() -> T? {
        return elements.popLast()
    }
}

3、associatedtype 与泛型的区别

比较项 associatedtype (协议中的泛型) 普通泛型
适用范围 只能用于 协议 可用于 类、结构体、函数
作用 让协议支持不确定的类型,由实现者决定具体类型 让类型/函数支持泛型
例子 protocol Container { associatedtype Item } struct Stack {}
限制 只能用于协议,不能直接实例化 适用于所有类型

4、什么时候使用 associatedtype

  • 当你需要创建一个通用的协议,但不想限定某个具体类型时。
  • 当不同的实现类需要指定不同的数据类型时。
  • 当你希望协议中的某些类型参数具备类型约束时(如 where 关键字)。

二、Subscript下标的用法

  • 是一种访问集合、列表或序列中元素成员的快捷方式。它允许你通过下标语法(使用方括号 [])来访问实例中的数据,而不需要调用方法。

  • 使用Subscript可以给任意类型(枚举、结构体、类)增加下标功能。

  • subscript的语法类似于实例方法,计算属性,本质就是方法

// demo1
struct TimesTable {
    let multiplier: Int

    subscript(index: Int) -> Int {
        return multiplier * index
    }
}

let threeTimesTable = TimesTable(multiplier: 3)
print(threeTimesTable[6])  // 输出: 18
    
// demo2
class MyPoint {
    var x = 0.0
    var y = 0.0
    subscript(index: Int) ->Double {
        set {
            if index == 0 {
                x = newValue
            } else if index == 1 {
                y = newValue
            }
        }

        get {
            if index == 0 {
                return x
            } else if (index == 1) {
                return y
            }
            return 0
        }
    }
}
 var mmpoint = MyPoint()
  mmpoint[0] = 11.1
  mmpoint[1] = 22.2

  print(mmpoint.x)
  print(mmpoint.y)
  print(mmpoint[0])
  print(mmpoint[1])
    
  // dem3
    struct Container {
    var items: [Int] = []
    
    // 单个整数下标
    subscript(index: Int) -> Int {
        return items[index]
    }
    
    // 范围下标
    subscript(range: Range<Int>) -> [Int] {
        return Array(items[range])
    }
    
    // 可变参数下标
    subscript(indices: Int...) -> [Int] {
        return indices.map { items[$0] }
    }
}

1、subscript中定义的返回值类型决定了
2、get方法的返回值类型 set方法中的newvalue的类型

3、subscript可以接受多个参数,并且类型任意

4、subscript可以没有set方法,但是必须要有get方法,如果只有get方法,可以省略get关键字

5、可以设置参数标签

6、下标可以是类型方法

三、swift中的迭代机制Sequence、collection、Iterator、AsyncSequence

image.png

在swift中,Sequence是一个协议,表示可以被逐一遍历的有序集合。一个符合Sequence协议的类型可以使用for-in循环迭代其所有元素。

Sequence是swift集合类型(Array,Dictionary、set等)的基础协议,许多高级功能如:map、filter、 reduce都依赖于它

常见的 Sequence 类型

许多 Swift 标准库类型都符合 Sequence 协议,例如:

Array:一个有序的集合。

Set:一个无序、唯一的集合。

Dictionary:键值对集合。

Range:连续的整数范围。

String:一个字符序列。

/// Sequence的核心定义
public protocol Sequence {
    /// 表示序列中元素的类型。
    associatedtype Element
    associatedtype Iterator: IteratorProtocol where Iterator.Element == Element
    /// 返回一个迭代器对象,该对象遵循 IteratorProtocol 协议,并提供逐一访问元素的功能。
    func makeIterator() -> Iterator
}

public protocol IteratorProtocol {
    associatedtype Element
    /// 每次调用时返回序列的下一个元素;如果没有更多元素可用,则返回 nil。
    mutating func next() -> Element?
}

总结:

1.Sequence只承诺“能生成迭代器”,不能保证反复便利,也不保证有count

2.迭代器几乎总是是struct:值语义保证“复制一份就从头开始”,不会意外共享状态

3.单趟序列完全合法;第二次makeIterator()可以返回空迭代器

// 可以创建自己的类型并使符合Sequence协议,只需要实现makeIterator()方法,并返回一个符合IteratorProtocol的迭代器
// 自定义一个从n倒数到0的序列
struct myCountDownDemo: Sequence {
    
    let start: Int
    func makeIterator() -> Iterator {
        Iterator(current: start)
    }

    struct Iterator: IteratorProtocol {
        var current: Int
    
        mutating func nex() -> Int? {
            guard current >= 0 else {return nil}
            defer {current -= 1}
            return current
        }
    }
}
// 调用了myArr.makeIterator()拿到一个迭代器 反复调用iterator.next() 返回的可选值解包后赋值给item
for n in myCountDownDemo(start: 3) {
     print(n)
}

let myArr = [1,5,6,8]
for item in myArr {
    print(item)
}
// for in 实际执行的是
var iterator = myArr.makeIterator()
while let element = iterator.next() {
    print(element)
}
    
// collection可以额外保证:多次遍历且顺序稳定,提供count、endIndex、下标访问,支持切片、前缀、后缀等默认实现
// 三种安全写法

// 方法一
todoItems.removeAll{$0 == "B"}

// 方法二 先记下索引,后删除
let indexsToRemove = todoItems.indices.filter{todoItems[$0] == "B"}
for i in indexsToRemove.reversed() {
    todoItems.remove(at: i)
}

// 方法三
todoItems = todoItems.filter{$0 != "B"}
//map
var numbersArr = [3,6,8]
let squares = numbersArr.map{$0 * $0}
print(squares) // 输出 [9,36,64]

// filter过滤列表中的元素
let eventNumbers = numbersArr.filter{ $0 % 2 == 0}
print(eventNumbers) // 输出[6,8]

// reduce将列表中所有元素组合成一个值
let sum = numbersArr.reduce(0 , +)
print(sum) // 输出17

// forEach对列表中的每个元素执行操作
numbersArr.forEach{print($0)}
协议 核心能力 特点与限制 常见实现
IteratorProtocol 通过 next() 方法单向、一次性地提供下一个元素 只进不退,遍历后即消耗。是所有迭代的基础。 通常作为 Sequence 的一部分实现,很少直接使用。
Sequence 可进行顺序迭代(如 for-in 循环),支持 mapfilterreduce 等操作 不一定可多次遍历,不保证通过下标访问元素 有限序列(如数组迭代器)、无限序列(如斐波那契数列生成器)
Collection 在 Sequence 基础上,可多次、非破坏性访问,并支持通过下标索引访问任意有效位置的元素 必须是有限的,并且索引操作的时间复杂度有明确规定(如 startIndexendIndex ArrayStringDictionarySet 以及自定义的集合类型。

AsyncSequence 是 Swift 并发模型的重要部分,特别适合处理:

  • 异步数据流(网络请求、文件读取)
  • 实时数据(传感器数据、消息推送)
  • 分页或懒加载数据
  • 长时间运行的数据生成任务

而 Sequence 更适合:

  • 内存中的集合操作
  • 同步数据处理
  • 简单的数据转换

选择依据:如果你的数据源是异步的或会产生延迟,使用 AsyncSequence;如果数据是同步可用的,使用 Sequence

// demo1
import Foundation

// 自定义异步序列
struct AsyncCountdown: AsyncSequence {
    typealias Element = Int
    
    let count: Int
    
    // 必须实现 makeAsyncIterator()
    func makeAsyncIterator() -> AsyncIterator {
        AsyncIterator(count: count)
    }
    
    // 异步迭代器
    struct AsyncIterator: AsyncIteratorProtocol {
        var count: Int
        
        // 注意:next() 是异步的!
        mutating func next() async -> Int? {
            guard count > 0 else { return nil }
            
            // 模拟异步等待
            await Task.sleep(1_000_000_000)  // 等待1秒
            
            let value = count
            count -= 1
            return value
        }
    }
}

// demo2
// 模拟从网络获取分页数据
struct PaginatedAPISequence: AsyncSequence {
    typealias Element = [String]
    
    let totalPages: Int
    let delay: UInt64
    
    func makeAsyncIterator() -> AsyncIterator {
        AsyncIterator(totalPages: totalPages, delay: delay)
    }
    
    struct AsyncIterator: AsyncIteratorProtocol {
        let totalPages: Int
        let delay: UInt64
        var currentPage = 0
        
        mutating func next() async throws -> [String]? {
            guard currentPage < totalPages else { return nil }
            
            // 模拟网络延迟
            await Task.sleep(delay)
            
            // 模拟获取数据
            let items = (0..<10).map { "Item \(currentPage * 10 + $0)" }
            currentPage += 1
            
            return items
        }
    }
}

// 使用
func fetchPaginatedData() async throws {
    let pageSize = 10
    let apiSequence = PaginatedAPISequence(totalPages: 5, delay: 500_000_000)
    
    for try await page in apiSequence {
        print("收到页面数据: \(page.count) 条")
        // 处理数据...
    }
}

GetX 状态管理实践

作者 feelingHy
2025年12月19日 14:30

下面内容只关注 GetxController / GetBuilder / Obx / 局部状态组件这些部分。


GetX 状态管理实践说明

本文介绍在项目中如何使用 GetxControllerGetBuilderObx / GetX 等组件来组织业务逻辑和控制 UI 更新。

GetxController 的角色与生命周期

GetxController 用来承载页面或模块的业务状态与逻辑,通常搭配 StatelessWidget 使用,无需再写 StatefulWidget。

  • 常用生命周期方法:
    • onInit:Controller 创建时调用,做依赖注入、初始请求、订阅等。
    • onReady:首帧渲染后调用,适合做需要 UI 已经渲染的操作(弹窗、导航等)。
    • onClose:Controller 销毁时调用,用于取消订阅、关闭 Stream、释放资源。

推荐习惯:

  • 把原来写在 StatefulWidget initState / dispose 里面的逻辑迁移到 Controller 的 onInit / onClose 中,UI 层尽量保持“傻瓜视图”。

GetX 中的两种状态管理方案

GetX 内置两类状态管理方式:简单状态管理(GetBuilder)与响应式状态管理(Obx / GetX)。

方案一:简单状态管理(GetBuilder + GetxController)

适用场景:不想使用 Rx 类型(.obs),希望显式控制刷新时机。

  • 写法示例:

    class CounterController extends GetxController {
      int count = 0;
    
      void increment() {
        count++;
        update(); // 手动触发使用该 controller 的 GetBuilder 重建
      }
    }
    
    class CounterPage extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        final controller = Get.put(CounterController());
    
        return Scaffold(
          body: Center(
            child: GetBuilder<CounterController>(
              builder: (c) => Text('Count: ${c.count}'),
            ),
          ),
          floatingActionButton: FloatingActionButton(
            onPressed: controller.increment,
          ),
        );
      }
    }
    
  • 特点:

    • 无需 .obs,状态是普通字段。
    • 只有调用 update() 的时候,使用该 Controller 的 GetBuilder 才会重建。
    • 适合页面级、大块 UI、不太频繁刷新场景。

方案二:响应式状态管理(Obx / GetX + Rx)

适用场景:已经在使用 .obs,或希望局部 UI 随状态变化自动刷新。

  • 写法示例:

    class CounterController extends GetxController {
      var count = 0.obs;
    
      void increment() => count++;
    }
    
    class CounterPage extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        final controller = Get.put(CounterController());
    
        return Scaffold(
          body: Center(
            child: Obx(() => Text('Count: ${controller.count}')),
            // 或
            // child: GetX<CounterController>(
            //   builder: (c) => Text('Count: ${c.count}'),
            // ),
          ),
          floatingActionButton: FloatingActionButton(
            onPressed: controller.increment,
          ),
        );
      }
    }
    
  • 特点:

    • 变量通过 .obs 变为 Rx 类型(如 RxInt、RxString)。
    • 一旦值变化,Obx / GetX 所在的小部件自动重建,无需写 update()
    • 适合高频、小区域更新,如计数器、进度、网络状态、Switch 等。

两种方案的混用

在同一个项目、同一个 Controller 中,可以同时使用:

  • 一部分状态使用普通字段 + GetBuilder
  • 一部分状态使用 .obs + Obx / GetX

经验规则:

  • 页面大块区域、刷新不频繁 → 优先 GetBuilder
  • 小范围、高频刷新 → 优先 Obx / GetX

GetBuilder 的生命周期回调

GetBuilder 本质上是一个 StatefulWidget,内部有完整的 State 生命周期,对外通过参数暴露部分回调:[1]

  • 常用回调参数:
    • initState:对应 State.initState,适合调用 Controller 方法、发请求等。
    • didChangeDependencies:父级依赖变化时触发,用得不多。
    • didUpdateWidget:父组件重建、参数改变时触发。
    • dispose:组件销毁时触发,适合释放本地资源。

示例:

GetBuilder<CounterController>(
  initState: (_) {
    // widget 创建时执行
  },
  dispose: (_) {
    // widget 销毁时执行
  },
  builder: (c) => Text('Count: ${c.count}'),
);

建议:

  • 页面 /模块的“生命周期逻辑”优先放在 Controller.onInit/onClose
  • 某个局部 Widget 有特别的创建 / 销毁逻辑时,再使用 GetBuilder 的 initState / dispose

局部状态组件:ValueBuilder 与 ObxValue

对于“只在一个小部件内部使用”的临时状态,可以使用局部状态组件,而不必放入 Controller:

  • ValueBuilder(简单本地状态):
    dart ValueBuilder<bool>( initialValue: false, builder: (value, update) => Switch( value: value, onChanged: update, // update(newValue) ), );

  • ObxValue(本地 Rx 状态):

    ObxValue<RxBool>(
      (data) => Switch(
        value: data.value,
        onChanged: data, // 相当于 (v) => data.value = v
      ),
      false.obs,
    );
    

使用建议:

  • 仅在该 Widget 内使用且与全局业务无关的状态 → 用 ValueBuilder / ObxValue
  • 会被多个 Widget 或页面共享的状态 → 放入 GetxController,再用 GetBuilder / Obx 监听。

快速选型表

需求场景 状态写法 UI 组件 刷新方式
不想用 Rx,页面级 / 大块区域 普通字段 GetBuilder 手动 update()
已使用 .obs,局部自动刷新 .obs(RxXX 类型) Obx / GetX 值变化自动刷新
单个小 widget 内部的临时简单状态 普通字段 ValueBuilder 调用 updateFn
单个小 widget 内部的临时响应式状态 .obs ObxValue 值变化自动刷新

在这种“页面加载时请求 API”的需求里,推荐把请求放在 GetxController 的生命周期 里做,而不是放在页面 Widget 里。

方案一:在 onInit 里请求

适合“只要创建了这个 Controller(进入页面)就立刻请求”的场景。

class ArticleController extends GetxController {
  int pageCount = 0;              // 简单状态
  var likeCount = 0.obs;          // 响应式状态
  var isFavorite = false.obs;
  var loading = false.obs;        // 加载状态
  var article = Rxn<Article>();   // 文章详情

  @override
  void onInit() {
    super.onInit();
    increasePageCount();  // 原来的逻辑
    fetchArticle();       // 页面加载时请求 API
  }

  Future<void> fetchArticle() async {
    loading.value = true;
    try {
      final data = await api.getArticleDetail(); // 这里调用你的 repository / service
      article.value = data;
      // article 是 Rx,使用 Obx 的地方会自动刷新
      // 如果你有依赖简单状态的 GetBuilder,需要的话再调用 update()
      // update();
    } finally {
      loading.value = false;
    }
  }

  void increasePageCount() {
    pageCount++;
    update(); // 刷新 GetBuilder
  }

  void like() => likeCount++;
  void toggleFavorite() => isFavorite.toggle();
}

页面里依然混用 GetBuilder + Obx:

class ArticlePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final controller = Get.put(ArticleController());

    return Scaffold(
      appBar: AppBar(title: const Text('Article Detail')),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          // 顶部浏览次数(简单状态)
          GetBuilder<ArticleController>(
            builder: (c) => Text('页面浏览次数:${c.pageCount}'),
          ),

          const SizedBox(height: 16),

          // 中间部分:加载中 / 内容(响应式状态)
          Obx(() {
            if (controller.loading.value) {
              return const CircularProgressIndicator();
            }
            final article = controller.article.value;
            if (article == null) {
              return const Text('暂无数据');
            }
            return Text(article.title); // 文章标题
          }),

          const SizedBox(height: 16),

          // 点赞 + 收藏(响应式状态)
          Obx(
            () => Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Text('点赞:${controller.likeCount}'),
                const SizedBox(width: 16),
                Icon(
                  controller.isFavorite.value
                      ? Icons.favorite
                      : Icons.favorite_border,
                  color: controller.isFavorite.value ? Colors.red : null,
                ),
              ],
            ),
          ),

          const SizedBox(height: 24),

          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              ElevatedButton(
                onPressed: controller.increasePageCount,
                child: const Text('增加浏览次数 (GetBuilder)'),
              ),
              const SizedBox(width: 16),
              ElevatedButton(
                onPressed: controller.like,
                child: const Text('点赞 (Obx)'),
              ),
              const SizedBox(width: 16),
              ElevatedButton(
                onPressed: controller.toggleFavorite,
                child: const Text('收藏切换 (Obx)'),
              ),
            ],
          ),
        ],
      ),
    );
  }
}

方案二:在 onReady 里请求(需要等页面渲染后)

如果你的 API 请求需要在“首帧 UI 出来之后”再做,比如要先弹一个对话框提示用户,将请求放在 onReady

@override
void onReady() {
  super.onReady();
  fetchArticle(); // 首帧渲染完成后请求
}

不再建议的做法

  • 不建议再在页面的 initState 里请求,而是优先放到 GetxController.onInit / onReady,这样视图层更干净,也更符合 GetX 推荐的结构。

Swift Array的写时复制

作者 Yakamoz
2025年12月19日 14:06

众所周知Swift中Array是值类型,如果其中元素为值类型和引用类型,分别会发生什么?

相关验证方法

检查不同层次的地址

// 1. 栈变量地址
withUnsafePointer(to: &array) {
    print("\(name) 栈地址: \($0)")
}

// 2. 堆缓冲区地址
array.withUnsafeBufferPointer {
    print("数组缓冲区地址: \(String(describing: $0.baseAddress))")
}
    
// 3. 元素地址(引用类型时比较)
if let first = array.first as? AnyObject {
    print("\(name)[0] 对象地址: \(ObjectIdentifier(first))")
    }
}

元素为引用类型

随便定义一个类,并创建列表1,然后直接赋值给列表2

class Person {
    var name: String
    init(name: String) { self.name = name }
}
var people1 = [Person(name: "Alice"), 
               Person(name: "Bob")]
var people2 = people1

withUnsafePointer打印此时两个数组的栈地址(指向数组的指针)

withUnsafePointer(to: &people1) { ptr in
    print("people1 地址: \(ptr)")
}

withUnsafePointer(to: &people2) { ptr in
    print("people2 地址: \(ptr)")

}
// 输出结果
// people1 地址: 0x000000010df001a0
// people2 地址: 0x000000010df001a8

确实是两个不同的数组指针(废话!),但是我们再通过withUnsafeBufferPointer获取数组缓冲区地址

people1.withUnsafeBufferPointer { buffer in
    if let baseAddress = buffer.baseAddress {
        print("people1缓冲区地址(堆): \(baseAddress)")
    }
}

people2.withUnsafeBufferPointer { buffer in
    if let baseAddress = buffer.baseAddress {
        print("people2缓冲区地址(堆): \(baseAddress)")
    }
}
// 输出结果
// people1缓冲区地址(堆): 0x000000014d2040c0
// people2缓冲区地址(堆): 0x000000014d2040c0

会发现指向的是同一块缓冲区

如果我们更改people2中元素的name,指针地址和缓冲区地址都没有任何变化(这里就不贴代码和打印结果了),但是如果新增元素

people2.append(Person(name: "newPerson"))
withUnsafePointer(to: &people2) { ptr in
    print("people2 地址: \(ptr)")
}

people2.withUnsafeBufferPointer { buffer in
    if let baseAddress = buffer.baseAddress {
        print("people2缓冲区地址(堆):\(baseAddress)")
    }
}
// 输出结果:
// people2 地址: 0x000000010df001a8
// people2缓冲区地址(堆): 0x000000014f404b10

指针地址没变,但是缓冲区地址变了!证明Swift中的数组是写时复制,新开辟了缓冲区。(删除同理)

但是缓冲区里存的是什么?打印下数组中的元素看看

/* people1
people1 元素对象地址:
[0]: 0x122b04570
[1]: 0x122b04590

people2 元素对象地址:
[0]: 0x122b04570
[1]: 0x122b04590
[2]: 0x122b05ea0
*/

得出结论。虽然缓冲区确实开了新的,但是引用类型的元素还是不会被复制,相当于只是开了一块新地址存引用类型元素的指针而已。

结论:

  1. Array是值类型
  2. 赋值副本Array时发生逻辑复制(新的数组指针 在栈上),修改副本中的元素也会更改到原Array中的元素
  3. 修复副本Array时才实际复制堆缓冲区

元素为值类型

如果真的能读到值类型,相信也能看懂直接用代码解释了

var array1 = ["AAA", "BBB", "CCC"]
var array2 = array1

// 输出结果:

// 栈地址验证,不同
// array1 栈地址: 0x00000001101d0058
// array2 栈地址: 0x00000001101d0060

// 缓冲区 暂时相同
// array1 缓冲区地址: 0x0000000129b04440
// array2 缓冲区地址: 0x0000000129b04440

此时修改元素再查看,array2已经开辟新的缓冲区,就不重复贴新增和删除的代码了,结果也是如此。

array2[0] = "new AAA"

// 输出结果:
// array1 缓冲区地址: 0x0000000129b04440
// array2 缓冲区地址: 0x0000000129b0d950

但是!修改了array2并没有像array1那样影响到同一个元素,现在用下面的方法验证下数组中的元素,打印修改后的结果

array1.withUnsafeBufferPointer { buffer in
    if let baseAddress = buffer.baseAddress {
        for i in 0..<buffer.count {
            let elementAddress = baseAddress + i
            print("array[\(i)] 地址: \(elementAddress), 值: \(elementAddress.pointee)")
        }
    }
}

array2.withUnsafeBufferPointer { buffer in
    if let baseAddress = buffer.baseAddress {
        for i in 0..<buffer.count {
            let elementAddress = baseAddress + i
            print("array[\(i)] 地址: \(elementAddress), 值: \(elementAddress.pointee)")
        }
    }
}
/* 输出结果:
array[0] 地址: 0x0000000127504170, 值: AAA
array[1] 地址: 0x0000000127504180, 值: BBB
array[2] 地址: 0x0000000127504190, 值: CCC


array[0] 地址: 0x000000012750ba70, 值: newAAA
array[1] 地址: 0x000000012750ba80, 值: BBB
array[2] 地址: 0x000000012750ba90, 值: CCC
*/

元素地址不同,值也不同

小总结

元素类型 值类型 引用类型
赋值 逻辑复制 逻辑复制
缓冲区共享 初始共享 初始共享
元素独立性 完全独立 共享对象
写时复制触发 修改时 修改结构时候(增删)
内存影响 元素复制 只复制指针

SwiftUI 中的 compositingGroup():真正含义与渲染原理

作者 汉秋
2025年12月19日 11:14

在学习 SwiftUI 的过程中,很多人第一次看到 compositingGroup() 都会被官方文档这句话绕晕:

Use compositingGroup() to apply effects to a parent view before applying effects to this view.

“让父 View 的效果先于子 View 的效果生效”  —— 这句话如果按字面理解,几乎一定会误解。

本文将从 渲染顺序、效果作用范围、实际示例 三个角度,彻底讲清楚 compositingGroup() 到底解决了什么问题。


一句话结论(先记住)

compositingGroup() 会创建一个“合成边界”:

  • 没有它:父 View 的合成效果会被「拆分」并逐个作用到子 View
  • 有了它:子 View 会先整体合成,再统一应用父 View 的合成效果

⚠️ 它改变的不是 modifier 的书写顺序,而是“效果的作用范围”。


SwiftUI 默认的渲染行为(最关键)

先看一个最简单的例子:

VStack {
    Text("A")
    Text("B")
}
.opacity(0.5)

看起来是对 VStack 设置了透明度

但 SwiftUI 实际做的是:

Text("A") -> opacity 0.5
Text("B") -> opacity 0.5
再进行叠加

也就是说:

  • opacity 并没有作为一个“整体效果”存在
  • 而是被 拆分后逐个应用到子 View

这就是很多「透明度叠加变脏」「blur 看起来不对劲」的根源。


compositingGroup() 做了什么?

加上 compositingGroup()

VStack {
    Text("A")
    Text("B")
}
.compositingGroup()
.opacity(0.5)

SwiftUI 的渲染流程会变成:

VStack
 ├─ Text("A")
 └─ Text("B")

先合成为一张离屏图像

对这张图像应用 opacity 0.5

关键变化只有一句话

父 View 的合成类效果不再下发到子 View。


那官方说的“父 View 的效果先于子 View 的效果”是什么意思?

这句话并不是时间顺序,而是:

父 View 的合成效果不会参与子 View 的内部计算。

换句话说:

  • 子 View 内部的 blur / color / mask 先完成
  • 父 View 的 opacity / blendMode 再整体生效

而不是交叉、叠加、重复计算。


一个典型示例:blur + opacity

❌ 没有 compositingGroup

ZStack {
    Text("Hello")
    Text("Hello")
        .blur(radius: 5)
}
.opacity(0.5)

实际效果:

  1. 第二个 Text 先 blur
  2. 两个 Text 分别被 opacity 影响
  3. 模糊区域再次参与透明度混合
  4. 结果:画面更糊、更脏

✅ 使用 compositingGroup

ZStack {
    Text("Hello")
    Text("Hello")
        .blur(radius: 5)
}
.compositingGroup()
.opacity(0.5)

渲染流程变为:

  1. 子 View 内部:blur 只影响指定的 Text
  2. ZStack 合成完成
  3. 整体统一 opacity 0.5

📌 blur 不再被“二次污染”


compositingGroup() 常见适用场景

1️⃣ 半透明容器(避免透明度叠加)

VStack {
    ...
}
.compositingGroup()
.opacity(0.8)

2️⃣ blendMode 视觉异常

ZStack {
    ...
}
.compositingGroup()
.blendMode(.multiply)

3️⃣ 动画 + blur / scale / opacity

.content
.compositingGroup()
.transition(.opacity)

可显著减少闪烁、重影问题。


compositingGroup vs drawingGroup

对比项 compositingGroup drawingGroup
是否离屏渲染
是否使用 Metal
主要目的 控制合成效果作用范围 性能 / 特效加速
常见问题 解决视觉叠加 解决复杂绘制性能

📌 compositingGroup 关注“视觉正确性”,drawingGroup 更偏向“性能”。


记忆口诀(非常实用)

要“整体效果”,用 compositingGroup;
不想被子 View 叠加污染,也用 compositingGroup。


总结

  • compositingGroup() 并不会改变 modifier 的书写顺序
  • 它创建了一个 合成边界(compositing boundary)
  • 阻止父 View 的合成效果被拆分并下发到子 View
  • 在 opacity、blur、blendMode、动画场景中极其重要

如果你在 SwiftUI 中遇到:

  • 透明度看起来“不对”
  • blur 过重
  • 动画时出现重影

👉 第一时间就该想到 compositingGroup()


希望这篇文章能帮你真正理解 SwiftUI 背后的渲染逻辑。

iOS 循环引用篇 菜鸟都能看懂

作者 dongczlu
2025年12月19日 10:50

iOS 内存管理完整补充知识

从对象到类、从结构体到元类、从 C++ 到内存分布区、到手机硬件内存的完整知识线


目录

  1. ARC 自动引用计数详细机制
  2. 内存对齐与对象大小
  3. Tagged Pointer 技术
  4. Mach-O 文件结构与内存映射
  5. AutoreleasePool 与 RunLoop 关系
  6. 堆分配策略与内存碎片
  7. 栈基础与栈溢出
  8. 类/元类查找链与方法缓存
  9. OC vs C++ 内存模型差异
  10. 虚拟内存与物理内存映射
  11. Weak 表实现与性能

0. 引用计数基础概念(小白必读)

0.1 什么是引用计数?

引用计数 = 记录"有多少个地方在使用这个对象"的数字

生活化比喻:图书馆借书

想象一下图书馆的书:

一本书(对象):
- 被借出时:借书人数 = 1
- 又有人借:借书人数 = 2
- 有人还书:借书人数 = 1
- 所有人还完:借书人数 = 0 → 书可以放回仓库(对象被释放)

OC 对象也是一样:

  • 对象被创建:引用计数 = 1
  • 有人强引用它:引用计数 +1
  • 有人不再引用:引用计数 -1
  • 引用计数 = 0:对象被释放(内存回收)

0.2 "引用计数加1"是什么意思?

"引用计数加1" = 又多了一个地方在使用这个对象

代码示例
// 步骤 1:创建对象
NSObject *obj = [[NSObject alloc] init];
// 此时:obj 指向的对象,引用计数 = 1
// 意思:有 1 个地方在使用这个对象(就是 obj 这个变量)

// 步骤 2:另一个变量也指向这个对象
NSObject *obj2 = obj;  // 强引用赋值
// 此时:obj 指向的对象,引用计数 = 2
// 意思:有 2 个地方在使用这个对象(obj 和 obj2)

// 步骤 3:obj 不再指向这个对象
obj = nil;
// 此时:obj 指向的对象,引用计数 = 1
// 意思:还有 1 个地方在使用(obj2 还在用)

// 步骤 4:obj2 也不再指向
obj2 = nil;
// 此时:引用计数 = 0
// 意思:没有地方在使用这个对象了 → 对象被释放!

0.3 "self 引用计数加1"具体指什么?

"self 引用计数加1" = 又多了一个地方在强引用 self 这个对象

示例 1:普通赋值
@interface ViewController : UIViewController
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // self 的引用计数 = 1(假设只有系统在引用它)
    
    // 创建一个强引用
    ViewController *anotherRef = self;  // 强引用赋值
    // 此时:self 的引用计数 = 2
    // 意思:有 2 个地方在强引用 self(系统 + anotherRef)
    
    // anotherRef 不再引用
    anotherRef = nil;
    // 此时:self 的引用计数 = 1(恢复)
}

@end
示例 2:Block 捕获 self(关键!)
- (void)viewDidLoad {
    [super viewDidLoad];
    
    // self 的引用计数 = 1
    
    // ❌ 情况 A:block 直接捕获 self
    self.block = ^{
        [self doSomething];  // block 强引用 self
    };
    // 此时:self 的引用计数 = 2
    // 原因:self 强引用 block,block 强引用 self
    // 形成循环:self → block → self(循环引用!)
    
    // ✅ 情况 B:block 捕获 weakSelf
    __weak typeof(self) weakSelf = self;
    self.block = ^{
        [weakSelf doSomething];  // block 弱引用 self(不增加引用计数)
    };
    // 此时:self 的引用计数 = 1(没有增加!)
    // 原因:weakSelf 是弱引用,不会让引用计数 +1
}
示例 3:Weak-Strong Dance 中的引用计数变化
- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 初始状态:self 引用计数 = 1
    
    __weak typeof(self) weakSelf = self;
    // 此时:self 引用计数 = 1(weakSelf 不增加引用计数)
    
    self.block = ^{
        // block 被创建,捕获了 weakSelf(弱引用)
        // 此时:self 引用计数 = 1(仍然没有增加)
        
        // block 执行时:
        __strong typeof(weakSelf) strongSelf = weakSelf;
        // 此时:self 引用计数 = 2(strongSelf 强引用,+1)
        // 意思:又多了一个地方在强引用 self(就是 strongSelf)
        
        [strongSelf doSomething];
        
        // block 执行完,strongSelf 作用域结束
        // 此时:self 引用计数 = 1(strongSelf 释放,-1)
        // 意思:strongSelf 不再引用 self,引用计数恢复
    };
    
    // 最终:self 引用计数 = 1(block 只弱引用 self,不增加引用计数)
}

0.4 引用计数的"加1"和"减1"是怎么实现的?

底层实现(简化理解)
// 伪代码:引用计数的实现
struct NSObject {
    int retainCount;  // 引用计数(实际可能不在对象里,在 side table)
};

// retain(加1)
- (id)retain {
    retainCount++;  // 引用计数 +1
    return self;
}

// release(减1)
- (void)release {
    retainCount--;  // 引用计数 -1
    if (retainCount == 0) {
        [self dealloc];  // 引用计数为 0,释放对象
    }
}
ARC 自动插入 retain/release
// 你写的代码
NSObject *obj = [[NSObject alloc] init];
NSObject *obj2 = obj;

// 编译器实际生成的代码(伪代码)
NSObject *obj = [[NSObject alloc] init];  // retainCount = 1
NSObject *obj2 = [obj retain];            // retainCount = 2(自动插入 retain)
// ... 使用 ...
[obj release];                             // retainCount = 1(自动插入 release)
[obj2 release];                            // retainCount = 0,对象释放

0.5 常见误区澄清

误区 1:指针变量本身不占引用计数
NSObject *obj = [[NSObject alloc] init];
// obj 这个指针变量本身不占引用计数
// 引用计数是对象自己的属性,不是指针的属性

// 多个指针指向同一个对象
NSObject *obj1 = [[NSObject alloc] init];  // 对象引用计数 = 1
NSObject *obj2 = obj1;                      // 对象引用计数 = 2(不是 obj2 的引用计数)
NSObject *obj3 = obj1;                      // 对象引用计数 = 3(不是 obj3 的引用计数)

// 所有指针都指向同一个对象,所以这个对象的引用计数 = 3
误区 2:weak 引用不增加引用计数
NSObject *obj = [[NSObject alloc] init];  // 引用计数 = 1
__weak NSObject *weakObj = obj;           // 引用计数 = 1(没有增加!)
__strong NSObject *strongObj = obj;        // 引用计数 = 2(增加了!)

// weak 引用不会让引用计数 +1
// 只有 strong 引用才会让引用计数 +1
误区 3:引用计数不是对象的"数量"
// ❌ 错误理解:引用计数 = 对象的数量
NSObject *obj1 = [[NSObject alloc] init];  // 1 个对象,引用计数 = 1
NSObject *obj2 = [[NSObject alloc] init];  // 2 个对象,引用计数 = 1(每个对象都是 1)

// ✅ 正确理解:引用计数 = 指向这个对象的强引用数量
NSObject *obj = [[NSObject alloc] init];  // 1 个对象
NSObject *ref1 = obj;                      // 对象引用计数 = 2(2 个强引用指向它)
NSObject *ref2 = obj;                      // 对象引用计数 = 3(3 个强引用指向它)

0.6 面试一句话总结

"引用计数加1" = 又多了一个强引用指向这个对象,对象的引用计数数值 +1

关键点:

  • 引用计数是对象的属性,不是指针的属性
  • 只有 strong 引用才会让引用计数 +1
  • weak 引用不会让引用计数 +1
  • 引用计数 = 0 时,对象被释放

1. ARC 自动引用计数详细机制

1.1 ARC 在编译时做了什么?

ARC 不是运行时技术,而是编译时技术!

编译器会在编译阶段自动插入 retainreleaseautorelease 调用。

示例代码对比

MRC 时代(手动):

// MRC 代码
- (void)example {
    NSObject *obj = [[NSObject alloc] init];  // 引用计数 = 1
    [obj retain];                             // 引用计数 = 2
    [obj release];                            // 引用计数 = 1
    [obj release];                            // 引用计数 = 0,对象被释放
}

ARC 时代(自动):

// ARC 代码(你写的)
- (void)example {
    NSObject *obj = [[NSObject alloc] init];
    // 编译器自动在方法结束前插入 [obj release];
}

编译器转换后的伪代码:

// 编译器实际生成的代码
- (void)example {
    NSObject *obj = [[NSObject alloc] init];  // 引用计数 = 1
    // ... 你的代码 ...
    [obj release];  // ← 编译器自动插入!
}

1.2 ARC 的 retain/release 插入规则

规则 1:赋值时自动 retain
NSObject *obj1 = [[NSObject alloc] init];  // 引用计数 = 1
NSObject *obj2 = obj1;                      // obj2 强引用,引用计数 = 2
// 编译器自动插入:obj2 = [obj1 retain];
规则 2:变量作用域结束时自动 release
- (void)example {
    NSObject *obj = [[NSObject alloc] init];  // 引用计数 = 1
    // ... 使用 obj ...
    // 编译器在方法结束前自动插入:[obj release];
}
规则 3:属性赋值时自动管理
@property (strong, nonatomic) NSObject *obj;

- (void)setObj:(NSObject *)obj {
    if (_obj != obj) {
        [_obj release];      // 编译器自动插入:释放旧值
        _obj = [obj retain]; // 编译器自动插入:持有新值
    }
}

1.3 什么是循环引用?(核心概念)

1.3.1 用生活例子理解"两个对象互相引用"

想象两个好朋友互相借钱:

小明 和 小红:

小明说:"我借了小红 100 元,小红必须还我,我才能还别人"
小红说:"我借了小明 100 元,小明必须还我,我才能还别人"

结果:两个人互相等待对方还钱,永远还不完!
这就是"互相引用"的问题。

在代码中:

对象 A 说:"我强引用了对象 BB 必须存在,我才能存在"
对象 B 说:"我强引用了对象 AA 必须存在,我才能存在"

结果:两个对象互相等待对方释放,永远释放不了!
这就是"循环引用"。

1.3.2 循环引用的图示

正常情况(没有循环):

对象 A(引用计数 = 1)
  ↑
  │ 强引用
  │
变量 a

对象 B(引用计数 = 1)
  ↑
  │ 强引用
  │
变量 b

结果:a = nil 时,A 被释放;b = nil 时,B 被释放 ✅

循环引用情况:

对象 A(引用计数 = 2)
  ↑              ↑
  │              │
  │ 强引用        │ 强引用(来自 B)
  │              │
变量 a        对象 B(引用计数 = 2)
                ↑              ↑
                │              │
                │ 强引用        │ 强引用(来自 A)
                │              │
              变量 b        对象 A(引用计数 = 2)
                              ↑
                              │
                              │(形成循环!)
                              │
                           对象 B(引用计数 = 2

问题:

  • 即使 a = nilb = nil,A 和 B 的引用计数都还是 1(因为互相引用)
  • 引用计数永远不会变成 0
  • 对象永远不会被释放 → 内存泄漏!

1.3.3 代码示例:两个对象互相引用
// 定义两个类
@interface PersonA : NSObject
@property (strong, nonatomic) PersonB *personB;  // A 强引用 B
@end

@interface PersonB : NSObject
@property (strong, nonatomic) PersonA *personA;  // B 强引用 A
@end

// 使用
PersonA *a = [[PersonA alloc] init];  // A 引用计数 = 1
PersonB *b = [[PersonB alloc] init];  // B 引用计数 = 1

a.personB = b;  // B 引用计数 = 2(A 强引用 B)
b.personA = a;  // A 引用计数 = 2(B 强引用 A)

// 此时:
// A 引用计数 = 2(变量 a + B.personA)
// B 引用计数 = 2(变量 b + A.personB)

a = nil;  // A 引用计数 = 1(还有 B.personA 在引用)
b = nil;  // B 引用计数 = 1(还有 A.personB 在引用)

// 问题:A 和 B 的引用计数都是 1,永远不会变成 0
// 结果:A 和 B 永远不会被释放 → 内存泄漏!

图示:

初始:
变量 a → PersonA(引用计数 = 1)
变量 b → PersonB(引用计数 = 1)

互相引用后:
变量 a → PersonA(引用计数 = 2)← PersonB.personA
         ↓ PersonA.personB
变量 b → PersonB(引用计数 = 2)← PersonA.personB
         ↑ PersonB.personA
         │
         └───────────┘(形成循环!)

a = nil, b = nil 后:
PersonA(引用计数 = 1)← PersonB.personA
         ↓ PersonA.personB
PersonB(引用计数 = 1)← PersonA.personB
         ↑ PersonB.personA
         │
         └───────────┘(循环还在,无法释放!)

1.3.4 如何打破循环引用?

方法:把其中一个强引用改成弱引用

// ✅ 正确:B 弱引用 A
@interface PersonA : NSObject
@property (strong, nonatomic) PersonB *personB;  // A 强引用 B
@end

@interface PersonB : NSObject
@property (weak, nonatomic) PersonA *personA;    // B 弱引用 A(关键!)
@end

// 使用
PersonA *a = [[PersonA alloc] init];  // A 引用计数 = 1
PersonB *b = [[PersonB alloc] init];  // B 引用计数 = 1

a.personB = b;  // B 引用计数 = 2(A 强引用 B)
b.personA = a;  // A 引用计数 = 1(B 弱引用 A,不增加引用计数)

// 此时:
// A 引用计数 = 1(只有变量 a)
// B 引用计数 = 2(变量 b + A.personB)

a = nil;  // A 引用计数 = 0 → A 被释放!
          // B.personA 自动变成 nil(weak 的特性)

b = nil;  // B 引用计数 = 1(还有 A.personB?不对,A 已经释放了)
          // 实际上,A 释放时,A.personB 也被释放
          // 所以 B 引用计数 = 0 → B 被释放!

// 结果:两个对象都能正常释放 ✅

图示(打破循环后):

变量 a → PersonA(引用计数 = 1)
         ↓ PersonA.personB(强引用)
变量 b → PersonB(引用计数 = 2)
         ↑ PersonB.personA(弱引用,不增加引用计数)

a = nil 后:
PersonA(引用计数 = 0)→ 被释放!
         ↓ PersonA.personB 也被释放
PersonB(引用计数 = 1)← 只有变量 b
         ↑ PersonB.personA = nil(自动置 nil)

b = nil 后:
PersonB(引用计数 = 0)→ 被释放!✅

1.3.5 循环引用的核心理解

循环引用 = 两个或多个对象互相强引用,形成闭环,导致都无法释放

关键点:

  1. 必须是"强引用" :weak 引用不会形成循环引用
  2. 必须是"互相" :A → B → A(闭环)
  3. 结果:引用计数永远不会变成 0,对象永远不会被释放

解决方法:

  • 把循环中的至少一个强引用改成弱引用
  • 或者手动断开循环(设置为 nil)

1.3.6 循环引用会导致什么?(重要!)

🚨 循环引用的后果

1. 内存泄漏(Memory Leak)

最直接的后果:对象永远不会被释放,占用内存越来越多

@interface ViewController : UIViewController
@property (copy, nonatomic) void (^block)(void);
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // ❌ 循环引用
    self.block = ^{
        [self doSomething];  // block 强引用 self
    };
    // self 强引用 block,block 强引用 self → 循环引用
}

@end

// 使用场景:
ViewController *vc = [[ViewController alloc] init];
[self.navigationController pushViewController:vc animated:YES];
// 用户返回上一页
[self.navigationController popViewControllerAnimated:YES];

// 问题:
// vc 应该被释放,但因为循环引用,vc 无法释放
// 内存泄漏!vc 占用的内存永远不会回收

影响:

  • 内存占用持续增长
  • 长时间运行后可能导致内存不足
  • 应用可能被系统杀死(OOM - Out of Memory)

2. dealloc 永远不会被调用

dealloc 方法不会被调用,清理代码不会执行

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // ❌ 循环引用
    self.block = ^{
        [self doSomething];
    };
}

- (void)dealloc {
    NSLog(@"ViewController 被释放");  // ❌ 永远不会打印!
    // 清理代码不会执行
    [[NSNotificationCenter defaultCenter] removeObserver:self];
    [self.timer invalidate];
    // 这些清理代码都不会执行!
}

@end

影响:

  • 资源无法释放(通知观察者、定时器、网络请求等)
  • 可能导致其他问题(通知重复接收、定时器继续运行等)

3. 通知观察者无法移除
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 添加通知观察者
    [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(handleNotification:)
                                                     name:@"SomeNotification"
                                                   object:nil];
    
    // ❌ 循环引用
    self.block = ^{
        [self doSomething];
    };
}

- (void)dealloc {
    // ❌ 永远不会执行!
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

// 问题:
// ViewController 无法释放
// 通知观察者无法移除
// 即使 ViewController 已经不在屏幕上,仍然会接收通知
// 可能导致崩溃或逻辑错误

4. 定时器无法停止
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // ❌ 循环引用
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0
                                                  target:self  // timer 强引用 self
                                                selector:@selector(timerAction)
                                                userInfo:nil
                                                 repeats:YES];
    // self 强引用 timer,timer 强引用 self → 循环引用
}

- (void)dealloc {
    // ❌ 永远不会执行!
    [self.timer invalidate];  // 定时器无法停止
    self.timer = nil;
}

// 问题:
// ViewController 无法释放
// 定时器继续运行,即使 ViewController 已经不在屏幕上
// 定时器回调可能访问已销毁的视图,导致崩溃

5. 网络请求回调可能继续执行
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // ❌ 循环引用
    [NetworkManager requestWithCompletion:^(NSData *data) {
        [self handleResponse:data];  // block 强引用 self
    }];
    // 如果 NetworkManager 也强引用这个 block,可能形成循环引用
}

- (void)dealloc {
    // ❌ 永远不会执行!
    // 清理代码不会执行
}

// 问题:
// ViewController 无法释放
// 网络请求完成后,回调可能访问已销毁的视图
// 可能导致崩溃或逻辑错误

6. KVO 观察者无法移除
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self.model addObserver:self
                 forKeyPath:@"value"
                    options:NSKeyValueObservingOptionNew
                    context:nil];
    
    // ❌ 循环引用
    self.block = ^{
        [self doSomething];
    };
}

- (void)dealloc {
    // ❌ 永远不会执行!
    [self.model removeObserver:self forKeyPath:@"value"];
}

// 问题:
// ViewController 无法释放
// KVO 观察者无法移除
// 如果 model 被释放,可能导致崩溃

📊 循环引用的影响总结

影响 说明 严重程度
内存泄漏 对象无法释放,内存持续增长 ⚠️⚠️⚠️ 严重
dealloc 不执行 清理代码不会执行 ⚠️⚠️⚠️ 严重
通知无法移除 继续接收通知,可能导致崩溃 ⚠️⚠️ 中等
定时器无法停止 定时器继续运行,可能访问已销毁对象 ⚠️⚠️ 中等
网络回调继续执行 回调可能访问已销毁对象 ⚠️⚠️ 中等
KVO 无法移除 可能导致崩溃 ⚠️⚠️ 中等

🔍 如何检测循环引用?

方法 1:检查 dealloc 是否被调用
- (void)dealloc {
    NSLog(@"✅ ViewController 被释放");  // 如果没打印,说明有循环引用
}
方法 2:使用 Instruments 的 Leaks 工具
  1. 打开 Xcode
  2. Product → Profile(或 Cmd + I)
  3. 选择 Leaks
  4. 运行应用,执行可能产生循环引用的操作
  5. 查看是否有内存泄漏
方法 3:使用 Xcode Memory Graph
  1. 运行应用
  2. 在 Debug Navigator 中点击 Memory Graph
  3. 查看对象是否正常释放
方法 4:使用 MLeaksFinder(第三方工具)

自动检测内存泄漏,在开发阶段就能发现问题。


✅ 如何避免循环引用?

  1. 使用 weak 引用:在 block、delegate、通知等场景使用 weak
  2. 及时断开引用:在不需要时手动设置为 nil
  3. 使用 weak-strong dance:在 block 中使用 weak-strong dance 模式
  4. 代码审查:定期检查代码,特别是 block、delegate、通知等场景

1.4 循环引用的典型场景

场景 1:self ↔ block
// ❌ 错误:循环引用
@interface ViewController ()
@property (nonatomic, copy) void (^block)(void);
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    
    // self 强引用 block
    self.block = ^{
        // block 强引用 self(捕获了 self)
        [self doSomething];  // ← 形成循环引用!
    };
}
@end

// ✅ 正确:使用 weak-strong dance
- (void)viewDidLoad {
    [super viewDidLoad];
    
    __weak typeof(self) weakSelf = self;
    self.block = ^{
        __strong typeof(weakSelf) strongSelf = weakSelf;
        if (!strongSelf) return;
        [strongSelf doSomething];
    };
}

🔍 Weak-Strong Dance 详细解释:

第一步:__weak typeof(self) weakSelf = self;
__weak typeof(self) weakSelf = self;

作用:

  • 创建一个 weak 指针指向 self
  • 不增加 self 的引用计数
  • 如果 self 被释放,weakSelf 会自动变成 nil

内存状态:

self 的引用计数 = 1(假设只有这里引用)
weakSelf → 指向 self(但不增加引用计数)

为什么需要 weak?

  • 如果 block 里直接用 self,block 会强引用 self
  • 形成循环:selfblockself(循环引用!)
  • weakSelf 后,block 只弱引用 self,打破循环

第二步:在 block 内部使用 __strong typeof(weakSelf) strongSelf = weakSelf;
self.block = ^{
    __strong typeof(weakSelf) strongSelf = weakSelf;
    // ...
};

作用:

  • 在 block 执行时,把 weakSelf 转成 strongSelf(强引用)
  • 如果 weakSelfnilstrongSelf 也是 nil
  • 如果 weakSelf 不是 nilstrongSelf增加引用计数,保证执行期间 self 不会被释放

内存状态变化:

情况 A:block 执行时,self 还存在

执行前:
self 引用计数 = 1
weakSelf → self(弱引用)

执行时(进入 block):
strongSelf = weakSelf;  // strongSelf 强引用 self
self 引用计数 = 2  ← 增加了!

执行中:
[self doSomething];  // 安全!self 不会被释放

执行后(block 结束):
strongSelf 作用域结束,自动 release
self 引用计数 = 1  ← 恢复

情况 B:block 执行时,self 已经被释放

执行前:
self 引用计数 = 0,已被释放
weakSelf = nil(自动置 nil)

执行时(进入 block):
strongSelf = weakSelf;  // strongSelf = nil
if (!strongSelf) return;  // 直接返回,不执行后续代码

第三步:if (!strongSelf) return;
if (!strongSelf) return;

作用:

  • 安全检查:如果 self 已经被释放,weakSelfnilstrongSelf 也是 nil
  • 直接返回,避免后续代码访问已释放的对象

为什么需要这个检查?

  • 虽然访问 nil 对象在 OC 中是安全的(不会崩溃),但逻辑上不应该执行
  • 提前返回,避免执行无意义的代码

第四步:使用 strongSelf 而不是 weakSelf
[strongSelf doSomething];  // ✅ 正确
// [weakSelf doSomething];  // ⚠️ 理论上可以,但不推荐

为什么用 strongSelf

关键原因:防止执行中途被释放

// ❌ 危险:只用 weakSelf
self.block = ^{
    __weak typeof(self) weakSelf = self;
    if (!weakSelf) return;
    
    // 假设 doSomething 执行时间很长
    [weakSelf doSomething];  // 执行到一半...
    
    // 如果此时 self 被释放了(其他强引用都断了)
    // weakSelf 变成 nil,但代码还在执行!
    [weakSelf doAnotherThing];  // 可能访问 nil
};

// ✅ 安全:使用 strongSelf
self.block = ^{
    __weak typeof(self) weakSelf = self;
    __strong typeof(weakSelf) strongSelf = weakSelf;
    if (!strongSelf) return;
    
    // strongSelf 强引用 self,保证整个 block 执行期间 self 不会被释放
    [strongSelf doSomething];      // self 引用计数 = 2,安全
    [strongSelf doAnotherThing];   // self 引用计数 = 2,安全
    // block 结束,strongSelf 释放,self 引用计数 = 1
};

完整执行流程示例

@interface ViewController ()
@property (nonatomic, copy) void (^block)(void);
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 步骤 1:创建 weak 引用
    __weak typeof(self) weakSelf = self;
    // 此时:self 引用计数 = 1,weakSelf → self(弱引用)
    
    // 步骤 2:创建 block(捕获 weakSelf,不是 self)
    self.block = ^{
        // 步骤 3:block 执行时,转为 strong 引用
        __strong typeof(weakSelf) strongSelf = weakSelf;
        
        // 步骤 4:安全检查
        if (!strongSelf) {
            NSLog(@"self 已被释放,不执行");
            return;
        }
        
        // 步骤 5:使用 strongSelf(保证执行期间 self 不会被释放)
        NSLog(@"执行任务,self 引用计数 = %lu", [strongSelf retainCount]);
        [strongSelf doSomething];
        
        // 步骤 6:block 结束,strongSelf 自动释放
        // self 引用计数恢复
    };
    
    // 步骤 7:viewDidLoad 结束,但 block 还在(被 self.block 持有)
}

- (void)doSomething {
    NSLog(@"执行任务");
}

- (void)dealloc {
    NSLog(@"ViewController 被释放");
    // 如果 block 还在,这里不会被调用(因为循环引用)
    // 如果用了 weak-strong dance,这里会被调用
}

@end

常见问题解答

Q1:为什么不能直接用 weakSelf

// ❌ 不推荐
self.block = ^{
    __weak typeof(self) weakSelf = self;
    [weakSelf doSomething];  // 执行中途 self 可能被释放
};

答案: 虽然不会崩溃(OC 对 nil 消息安全),但执行中途 self 可能被释放,导致逻辑错误。


Q2:strongSelf 会不会又造成循环引用?为什么 block 里用了 strong 修饰,不也是强引用 self 吗?

答案:不会! 这是最关键的理解点!

关键理解:block 捕获的是什么?

重要:block 捕获的是 weakSelf(弱引用),不是 strongSelf

__weak typeof(self) weakSelf = self;  // 步骤 1:创建 weak 引用

self.block = ^{
    // 步骤 2:block 捕获的是 weakSelf(弱引用)
    // block 内部结构(伪代码):
    // struct Block {
    //     __weak typeof(self) weakSelf;  // ← block 捕获的是这个!
    //     void (*invoke)(...);
    // };
    
    // 步骤 3:block 执行时,才创建 strongSelf(局部变量)
    __strong typeof(weakSelf) strongSelf = weakSelf;
    // strongSelf 是 block 执行时才创建的,不是 block 捕获的!
};
详细解释:为什么不会形成循环引用?

情况 A:如果 block 直接捕获 self(会形成循环引用)

// ❌ 错误:block 捕获 self(强引用)
self.block = ^{
    [self doSomething];  // block 捕获 self(强引用)
};

// 内存关系:
// self → block(强引用)
// block → self(强引用,因为捕获了 self)
// 形成循环:self → block → self ❌

情况 B:block 捕获 weakSelf,执行时创建 strongSelf(不会形成循环引用)

// ✅ 正确:block 捕获 weakSelf(弱引用)
__weak typeof(self) weakSelf = self;

self.block = ^{
    // block 捕获的是 weakSelf(弱引用),不是 self!
    // 所以:block → weakSelf(弱引用,不增加引用计数)
    
    // strongSelf 是 block 执行时才创建的局部变量
    __strong typeof(weakSelf) strongSelf = weakSelf;
    // strongSelf 不是 block 捕获的,是执行时的临时变量
};

// 内存关系:
// self → block(强引用)
// block → weakSelf(弱引用,不增加引用计数)✅
// block 执行时:strongSelf → self(临时强引用,执行完就释放)✅
// 没有循环!✅
用图示理解

错误情况(会循环引用):

self ──→ block ──→ self(强引用)
  ↑                  │
  └──────────────────┘(循环!)

正确情况(不会循环引用):

self ──→ block ──→ weakSelf ──→ self(弱引用,不形成循环)
  ↑
  └──────────────────────────────┘(没有循环!)

block 执行时:
self ──→ block ──→ weakSelf ──→ self(弱引用)
  ↑                              ↑
  │                              │
  └──────────────────────────────┘
                                 │
                            strongSelf(临时强引用,执行完就释放)
关键点总结
  1. block 捕获的是什么?

    • block 捕获的是 weakSelf(弱引用),不是 strongSelf
    • 所以 block 不强引用 self,不会形成循环
  2. strongSelf 是什么?

    • strongSelf 是 block 执行时才创建的局部变量
    • 不是 block 捕获的,是执行时的临时强引用
    • block 执行完,strongSelf 就释放了
  3. 为什么不会形成循环?

    • 循环引用的关键是:block 本身是否强引用 self
    • 因为 block 捕获的是 weakSelf(弱引用),所以 block 不强引用 self
    • strongSelf 只是执行时的临时强引用,不会形成持久的循环
完整的内存关系图
创建阶段:
self(引用计数 = 1)
  ↓ 强引用
block(捕获 weakSelf,弱引用 self)
  ↓ 弱引用(不增加引用计数)
weakSelf → self(引用计数 = 1,没有增加)

执行阶段(block 被调用):
self(引用计数 = 1)
  ↓ 强引用
block
  ↓ 弱引用
weakSelf → self(引用计数 = 1)
  ↓
strongSelf(局部变量,强引用 self)
  ↓ 强引用(临时)
self(引用计数 = 2,临时增加)

执行结束:
strongSelf 释放 → self(引用计数 = 1,恢复)
block 仍然存在,但只弱引用 self(不形成循环)

答案:不会! 因为:

  • block 捕获的是 weakSelf(弱引用),不是 strongSelf
  • strongSelf局部变量,只在 block 执行期间存在
  • block 执行完,strongSelf 自动释放
  • 不会形成持久的循环引用,因为 block 本身不强引用 self

Q3:什么时候 weakSelf 会变成 nil

答案:self 的所有强引用都断开时:

// 场景:ViewController 被 pop 或 dismiss
[self.navigationController popViewControllerAnimated:YES];
// 此时如果 self 没有其他强引用,会被释放
// weakSelf 自动变成 nil

Q4:可以简化成这样吗?

// ⚠️ 简化版(不推荐,但某些场景可用)
__weak typeof(self) weakSelf = self;
self.block = ^{
    [weakSelf doSomething];  // 直接使用 weakSelf
};

答案:

  • 简单场景可以:如果 doSomething 执行很快,且不涉及多步操作
  • 复杂场景不行:如果 block 执行时间长,或有多步操作,必须用 strongSelf 保证执行期间对象不被释放

面试标准答案(一句话总结)

Weak-Strong Dance 的作用:

  1. weakSelf:打破循环引用,让 block 不强持有 self
  2. strongSelf:在 block 执行期间强持有 self,防止执行中途被释放
  3. if (!strongSelf) return:安全检查,如果 self 已释放则提前返回

核心思想: 用弱引用打破循环,用临时强引用保证执行安全。


场景 2:NSTimer 循环引用
// ❌ 错误:循环引用
@interface ViewController ()
@property (nonatomic, strong) NSTimer *timer;
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    
    // self 强引用 timer
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0
                                                  target:self  // ← timer 强引用 self
                                                selector:@selector(timerAction)
                                                userInfo:nil
                                                 repeats:YES];
    // 形成循环:self → timer → self
}

// ✅ 正确:使用中间对象或 block-based API
- (void)viewDidLoad {
    [super viewDidLoad];
    
    __weak typeof(self) weakSelf = self;
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0
                                                 repeats:YES
                                                   block:^(NSTimer * _Nonnull timer) {
        __strong typeof(weakSelf) strongSelf = weakSelf;
        if (!strongSelf) return;
        [strongSelf timerAction];
    }];
}

- (void)dealloc {
    [self.timer invalidate];  // 必须手动停止
    self.timer = nil;
}
场景 3:通知观察者循环引用
// ⚠️ iOS 9+ 后通知中心会弱引用观察者,但业务代码仍需注意
- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 如果 self 强引用通知,通知回调里又用 self,可能形成循环
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(handleNotification:)
                                                 name:@"SomeNotification"
                                               object:nil];
}

- (void)dealloc {
    // 必须移除观察者
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

1.4 Weak 表的工作机制

Weak 表是什么?

Weak 表 = 一张全局的哈希表,记录所有 weak 指针

// 伪代码:Weak 表的结构
struct WeakTable {
    // key: 对象的地址
    // value: 所有指向这个对象的 weak 指针数组
    HashMap<对象地址, Array<weak指针地址>>;
};
Weak 指针的工作流程
NSObject *obj = [[NSObject alloc] init];  // 引用计数 = 1
__weak NSObject *weakObj = obj;            // 引用计数仍 = 1

// 步骤 1:weakObj 被注册到 Weak 表
// Weak 表记录:obj 的地址 → [weakObj 的地址]

obj = nil;  // 引用计数 = 0,对象即将被释放

// 步骤 2:对象释放时,系统遍历 Weak 表
// 找到所有指向这个对象的 weak 指针
// 步骤 3:把所有 weak 指针置为 nil
// weakObj 现在 = nil(安全!)
面试常问:Weak 表如何实现?

答案要点:

  1. 全局哈希表:以对象地址为 key,存储所有指向它的 weak 指针
  2. 对象释放时:遍历 Weak 表,找到所有相关 weak 指针,置为 nil
  3. 性能优化:使用哈希表,查找是 O(1) 平均时间复杂度

2. 内存对齐与对象大小

2.1 什么是内存对齐?

内存对齐 = 数据在内存中的起始地址必须是某个数的倍数

对齐规则(64 位系统)
  • 基本类型对齐

    • char:1 字节对齐
    • short:2 字节对齐
    • int:4 字节对齐
    • long / 指针:8 字节对齐
    • double:8 字节对齐
  • 结构体对齐

    • 结构体整体大小必须是最大成员对齐值的倍数
    • 结构体起始地址必须是最大成员对齐值的倍数
示例:结构体内存对齐
struct Example {
    char a;      // 1 字节,偏移 0
    // 填充 3 字节(padding)
    int b;       // 4 字节,偏移 4(必须是 4 的倍数)
    char c;      // 1 字节,偏移 8
    // 填充 7 字节(padding)
    double d;    // 8 字节,偏移 16(必须是 8 的倍数)
};
// 总大小 = 24 字节(必须是 8 的倍数)

// 验证
NSLog(@"Size: %lu", sizeof(struct Example));  // 输出:24

2.2 OC 对象的内存对齐

对象内存布局
@interface Person : NSObject {
    @public
    char _name;      // 1 字节
    int _age;        // 4 字节
    double _height;  // 8 字节
}
@end

// 内存布局(64 位系统):
// [isa 指针: 8 字节] [padding: 0] 
// [_name: 1 字节] [padding: 3 字节]
// [_age: 4 字节]
// [padding: 4 字节](为了 double 对齐)
// [_height: 8 字节]
// 总大小 = 8 + 4 + 4 + 8 = 24 字节(必须是 8 的倍数)
查看对象实际大小
Person *p = [[Person alloc] init];

// 方法 1:实例大小(对齐后)
size_t instanceSize = class_getInstanceSize([Person class]);
NSLog(@"Instance size: %zu", instanceSize);  // 输出:24

// 方法 2:实际分配大小(系统可能分配更多)
size_t mallocSize = malloc_size((__bridge const void *)p);
NSLog(@"Malloc size: %zu", mallocSize);  // 可能输出:32(系统额外分配)

2.3 编译器如何插入 Padding?

@interface Example : NSObject {
    char a;      // 偏移 8(isa 后),大小 1
    // 编译器插入 padding: 3 字节
    int b;       // 偏移 12,大小 4
    char c;      // 偏移 16,大小 1
    // 编译器插入 padding: 7 字节(为了 double 对齐)
    double d;    // 偏移 24,大小 8
}
@end

// 编译器优化:调整成员顺序可以减少 padding
@interface OptimizedExample : NSObject {
    double d;    // 偏移 8,大小 8(最大对齐值)
    int b;       // 偏移 16,大小 4
    char a;      // 偏移 20,大小 1
    char c;      // 偏移 21,大小 1
    // padding: 6 字节(为了整体 8 字节对齐)
}
@end
// 优化后总大小可能更小!

2.4 面试常问点

Q:为什么需要内存对齐?

答案要点:

  1. CPU 读取效率:未对齐的数据可能需要多次内存访问
  2. 硬件要求:某些 CPU 架构要求数据必须对齐,否则崩溃
  3. 缓存行优化:对齐的数据更容易放入 CPU 缓存行

3. Tagged Pointer 技术

3.1 什么是 Tagged Pointer?

Tagged Pointer = 把小数据直接编码进指针里,不占用堆内存

传统方式 vs Tagged Pointer
// 传统方式(64 位系统)
NSNumber *num1 = @(42);
// 内存布局:
// 指针变量(栈上,8 字节)→ 指向堆上的 NSNumber 对象(至少 16 字节)
// 总占用:8 + 16 = 24 字节

// Tagged Pointer 方式
NSNumber *num2 = @(42);
// 内存布局:
// 指针变量(栈上,8 字节),但指针里直接存了 42 的值!
// 总占用:8 字节(节省 16 字节!)

3.2 Tagged Pointer 的识别

NSNumber *num1 = @(42);
NSNumber *num2 = @(1000000);  // 大数字

// 判断是否是 Tagged Pointer
NSLog(@"num1 is Tagged: %d", _objc_isTaggedPointer((__bridge void *)num1));
// 输出:1(是 Tagged Pointer)

NSLog(@"num2 is Tagged: %d", _objc_isTaggedPointer((__bridge void *)num2));
// 输出:0(不是,因为数字太大)

3.3 哪些对象支持 Tagged Pointer?

  • NSNumber:小整数(通常 < 2^60)
  • NSDate:时间戳在某个范围内
  • NSString:短字符串(通常 < 7 个字符,ASCII)
  • NSIndexPath:某些 iOS 版本
示例:NSString 的 Tagged Pointer
NSString *str1 = @"abc";           // Tagged Pointer
NSString *str2 = @"abcdefghijkl";  // 普通对象(堆上)

// 验证
NSLog(@"str1 pointer: %p", str1);  // 指针值看起来很奇怪(有 tag 位)
NSLog(@"str2 pointer: %p", str2);  // 正常的堆地址

// 查看实际内容
NSLog(@"str1: %@", str1);  // 正常输出
NSLog(@"str2: %@", str2);  // 正常输出

3.4 Tagged Pointer 的优势

  1. 节省内存:不需要堆分配
  2. 提高性能:不需要引用计数管理
  3. 减少碎片:不占用堆空间

3.5 面试常问点

Q:Tagged Pointer 如何工作?

答案要点:

  1. 利用指针的未使用位:64 位指针只用 48 位,剩余位用来存 tag 和数据
  2. 特殊标记位:最低位通常是 1,表示这是 Tagged Pointer
  3. 类型编码:用几个位表示类型(NSNumber/NSString/NSDate 等)
  4. 数据编码:剩余位存实际数据

4. Mach-O 文件结构与内存映射

4.1 Mach-O 是什么?

Mach-O = macOS/iOS 的可执行文件格式

类似于:

  • Windows:.exe(PE 格式)
  • Linux:ELF 格式
  • macOS/iOS:.app(Mach-O 格式)

4.2 Mach-O 的基本结构

Mach-O 文件
├── Header(文件头)
│   ├── 魔数(标识文件类型)
│   ├── CPU 架构(arm64/x86_64)
│   └── 加载命令数量
│
├── Load Commands(加载命令)
│   ├── 代码段位置
│   ├── 数据段位置
│   └── 动态库依赖
│
└── Data(数据区)
    ├── __TEXT(代码段)
    │   ├── 可执行代码
    │   └── 常量字符串
    │
    └── __DATA(数据段)
        ├── 全局变量
        ├── 静态变量
        └── 类元数据

4.3 主要段(Segment)详解

__TEXT 段(代码段)

特点:只读(Read-Only)、可执行(Executable)

// 这些内容在 __TEXT 段:

// 1. 可执行代码
- (void)example {
    NSLog(@"Hello");  // 这行代码编译后的机器指令在 __TEXT 段
}

// 2. 常量字符串
NSString *str = @"Hello";  // @"Hello" 在 __TEXT 段

// 3. 常量数据
const int kValue = 100;  // 在 __TEXT 段
__DATA 段(数据段)

特点:可读写(Read-Write)

// 这些内容在 __DATA 段:

// 1. 全局变量
int globalVar = 10;  // 在 __DATA 段

// 2. 静态变量
static int staticVar = 20;  // 在 __DATA 段

// 3. 类元数据(运行时注册)
@interface MyClass : NSObject
@end
// MyClass 的类对象信息在 __DATA 段

4.4 类对象在 Mach-O 中的位置

@interface Person : NSObject
@end

// 编译后,Person 类的信息存储在:
// 1. __TEXT 段:方法实现(机器码)
// 2. __DATA 段:类对象结构
//    - isa 指针
//    - superclass 指针
//    - 方法列表指针
//    - 属性列表指针
//    - 协议列表指针

4.5 静态库 vs 动态库的内存映射

静态库(.a 文件)
// 静态库的代码被直接链接进主可执行文件
// 内存映射:
// 主可执行文件的 __TEXT 段包含静态库的代码
// 主可执行文件的 __DATA 段包含静态库的数据
动态库(.dylib / .framework)
// 动态库由 dyld(动态链接器)在运行时加载
// 内存映射:
// 1. dyld 读取动态库的 Mach-O 文件
// 2. 将 __TEXT 段映射到内存(只读、可执行)
// 3. 将 __DATA 段映射到内存(可读写)
// 4. 每个进程共享同一份 __TEXT 段(节省内存)
// 5. 每个进程有独立的 __DATA 段副本

4.6 面试常问点

Q:类对象在哪里?

答案要点:

  1. 编译时:类信息写在 Mach-O 的 __DATA 段
  2. 运行时:dyld 加载 Mach-O,将类信息注册到 runtime
  3. 内存位置:类对象在进程的虚拟地址空间中(具体地址由 ASLR 随机化)

5. AutoreleasePool 与 RunLoop 关系

5.1 AutoreleasePool 是什么?

AutoreleasePool = 延迟释放池,让对象"晚一点"释放

// 传统 release(立即释放)
NSObject *obj = [[NSObject alloc] init];
[obj release];  // 立即释放,引用计数 = 0

// Autorelease(延迟释放)
NSObject *obj = [[NSObject alloc] init];
[obj autorelease];  // 加入自动释放池,等池子结束时才 release

5.2 AutoreleasePool 的结构

// AutoreleasePool 是一个栈结构
@autoreleasepool {
    // Pool 1(外层)
    @autoreleasepool {
        // Pool 2(内层)
        NSObject *obj = [[NSObject alloc] init];
        // obj 被加入 Pool 2
    }
    // Pool 2 结束,obj 被释放
}
// Pool 1 结束

5.3 RunLoop 与 AutoreleasePool 的关系

主线程的隐式 AutoreleasePool
// 主线程的 RunLoop 结构(简化)
void mainRunLoop() {
    while (appIsRunning) {
        @autoreleasepool {  // ← 系统自动创建
            // 处理事件
            handleEvents();
            // 处理定时器
            handleTimers();
            // 处理 Source
            handleSources();
        }
        // 池子结束,释放所有 autorelease 的对象
    }
}

关键点:

  • 主线程的每个 RunLoop 周期都有一个隐式的 @autoreleasepool
  • 当 RunLoop 进入休眠或结束一个周期时,池子会 drain(释放所有对象)
子线程没有隐式 AutoreleasePool
// ❌ 错误:子线程大量创建对象
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    for (int i = 0; i < 10000; i++) {
        NSObject *obj = [[NSObject alloc] init];
        // obj 被 autorelease,但没有池子,会积压!
    }
});

// ✅ 正确:手动创建 AutoreleasePool
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    @autoreleasepool {
        for (int i = 0; i < 10000; i++) {
            NSObject *obj = [[NSObject alloc] init];
            // obj 在池子结束时释放
        }
    }
    // 或者更细粒度:
    for (int i = 0; i < 10000; i++) {
        @autoreleasepool {
            NSObject *obj = [[NSObject alloc] init];
            // 每次循环结束就释放
        }
    }
});

5.4 什么时候需要手动创建 AutoreleasePool?

场景 1:子线程大量创建对象
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    @autoreleasepool {
        // 大量临时对象
        for (int i = 0; i < 100000; i++) {
            NSString *str = [NSString stringWithFormat:@"%d", i];
            // 使用 str...
        }
    }
    // 池子结束,所有临时对象立即释放,降低峰值内存
});
场景 2:大循环中创建临时对象
// ❌ 不好:所有临时对象积压到外层池子
for (int i = 0; i < 10000; i++) {
    NSMutableArray *arr = [NSMutableArray array];  // autorelease
    // 使用 arr...
}

// ✅ 好:每次循环结束就释放
for (int i = 0; i < 10000; i++) {
    @autoreleasepool {
        NSMutableArray *arr = [NSMutableArray array];
        // 使用 arr...
    }
    // arr 立即释放
}

5.5 面试常问点

Q:为什么子线程需要手动创建 AutoreleasePool?

答案要点:

  1. 主线程:RunLoop 自动创建和销毁 AutoreleasePool
  2. 子线程:没有 RunLoop(或 RunLoop 不活跃),没有隐式池子
  3. 后果:autorelease 的对象会积压,直到线程结束才释放,导致内存峰值过高
  4. 解决:手动创建 @autoreleasepool,及时释放临时对象

6. 堆分配策略与内存碎片

6.1 堆内存分配器(malloc)

iOS 使用 jemalloc 或类似的分配器管理堆内存。

分配策略(简化)
堆内存分配器
├── Tiny 区(< 16 字节)
│   └── 快速分配,固定大小块
│
├── Small 区(16 字节 ~ 几 KB)
│   └── 按大小分类的块池
│
└── Large 区(> 几 KB)
    └── 直接 mmap 分配

6.2 内存碎片问题

什么是内存碎片?
// 场景:频繁分配和释放不同大小的对象

// 1. 分配 100 字节
void *p1 = malloc(100);

// 2. 分配 200 字节
void *p2 = malloc(200);

// 3. 释放 p1(100 字节的空洞)
free(p1);

// 4. 现在想分配 150 字节
void *p3 = malloc(150);
// 问题:p1 的空洞只有 100 字节,不够!
// 只能从其他地方分配,导致碎片
如何减少碎片?

策略 1:对象池(Object Pool)

// 复用对象,而不是频繁创建和销毁
@interface ObjectPool : NSObject
+ (instancetype)sharedPool;
- (id)getObject;
- (void)returnObject:(id)obj;
@end

// 使用
ObjectPool *pool = [ObjectPool sharedPool];
MyObject *obj = [pool getObject];
// 使用 obj...
[pool returnObject:obj];  // 归还,而不是释放

策略 2:批量分配

// 一次性分配大块内存,自己管理
void *buffer = malloc(1024 * 1024);  // 1MB
// 自己在这 1MB 里分配小对象
// 减少系统 malloc 调用次数

6.3 面试常问点

Q:如何优化内存分配性能?

答案要点:

  1. 对象池:复用对象,减少分配/释放次数
  2. 批量分配:一次性分配大块内存,自己管理
  3. 避免频繁小对象分配:合并小对象,或使用结构体
  4. 使用 AutoreleasePool:及时释放临时对象,降低峰值

7. 栈基础与栈溢出

7.1 栈的基本概念

栈 = 函数调用的"工作区"

void functionA() {
    int a = 10;  // 在栈上
    functionB();
}

void functionB() {
    int b = 20;  // 在栈上
    functionC();
}

void functionC() {
    int c = 30;  // 在栈上
}

// 调用栈(从下往上):
// [functionA 的栈帧: a = 10]
// [functionB 的栈帧: b = 20]
// [functionC 的栈帧: c = 30]  ← 栈顶

7.2 iOS 线程栈大小

// 主线程栈大小:通常 1MB
// 子线程栈大小:通常 512KB(可配置)

// 创建自定义栈大小的线程
NSThread *thread = [[NSThread alloc] initWithTarget:self
                                            selector:@selector(threadMain)
                                              object:nil];
thread.stackSize = 1024 * 1024;  // 1MB
[thread start];

7.3 栈溢出的常见原因

原因 1:无限递归
// ❌ 错误:无限递归
- (void)recursive {
    int localVar[1000];  // 大局部变量
    [self recursive];    // 无限递归,栈帧不断增长
    // 最终:栈溢出(Stack Overflow)
}

// ✅ 正确:有终止条件
- (void)recursiveWithDepth:(int)depth {
    if (depth <= 0) return;  // 终止条件
    
    int localVar[1000];
    [self recursiveWithDepth:depth - 1];
}
原因 2:大局部变量
// ❌ 危险:大数组在栈上
- (void)example {
    int hugeArray[1000000];  // 4MB 在栈上!
    // 可能栈溢出
}

// ✅ 安全:大数组在堆上
- (void)example {
    int *hugeArray = malloc(1000000 * sizeof(int));  // 堆上
    // 使用...
    free(hugeArray);
}

7.4 面试常问点

Q:栈溢出如何避免?

答案要点:

  1. 避免无限递归:确保递归有终止条件
  2. 大变量用堆:大数组、大结构体用 malloc 或对象
  3. 限制递归深度:设置最大递归深度
  4. 增加栈大小pthread_attr_setstacksize(不推荐,治标不治本)

8. 类/元类查找链与方法缓存

8.1 方法查找流程(完整版)

// 调用:[obj methodName]

// 步骤 1:通过 isa 找到类对象
Class cls = object_getClass(obj);  // obj->isa

// 步骤 2:在类对象的方法列表中查找
Method method = class_getInstanceMethod(cls, @selector(methodName));

// 步骤 3:如果没找到,沿 superclass 链向上查找
while (cls && !method) {
    cls = class_getSuperclass(cls);
    method = class_getInstanceMethod(cls, @selector(methodName));
}

// 步骤 4:如果找到,调用 method->imp(函数指针)

8.2 方法缓存(Method Cache)

为什么需要缓存?

方法查找需要遍历类的方法列表,如果每次都查找,性能很差。

缓存机制:

// 伪代码:方法缓存结构
struct MethodCache {
    // 哈希表:selector → IMP
    HashMap<Selector, IMP> cache;
};

// 查找流程(带缓存):
IMP imp = cache.get(selector);
if (imp) {
    return imp;  // 缓存命中,直接返回
} else {
    // 缓存未命中,查找方法列表
    imp = findMethodInClass(selector);
    cache.set(selector, imp);  // 加入缓存
    return imp;
}

8.3 类方法 vs 实例方法

@interface Person : NSObject
- (void)instanceMethod;  // 实例方法
+ (void)classMethod;     // 类方法
@end

// 调用实例方法
Person *p = [[Person alloc] init];
[p instanceMethod];
// 查找路径:p->isa(Person 类)→ 查找实例方法列表

// 调用类方法
[Person classMethod];
// 查找路径:Person 类对象->isa(Person 元类)→ 查找类方法列表

8.4 元类链(完整)

// 元类链(简化)
Person 实例
  └─ isa → Person 类对象
           ├─ isa → Person 元类
           │        ├─ isa → NSObject 元类
           │        │        └─ isa → NSObject 元类(根元类指向自己)
           │        └─ superclass → NSObject 元类
           └─ superclass → NSObject 类对象
                            └─ isa → NSObject 元类

8.5 面试常问点

Q:方法查找的完整流程?

答案要点:

  1. 实例方法:对象 isa → 类对象 → 方法列表 → superclass 链向上查找
  2. 类方法:类对象 isa → 元类 → 方法列表 → 元类的 superclass 链向上查找
  3. 缓存优化:查找结果缓存到 MethodCache,下次直接命中
  4. 消息转发:如果最终没找到,进入消息转发机制(forwardingTargetForSelector: 等)

9. OC vs C++ 内存模型差异

9.1 对象创建位置

Objective-C
// OC 对象总是在堆上
NSObject *obj = [[NSObject alloc] init];
// obj 是指针(栈上),指向堆上的对象
C++
// C++ 对象可以在栈上
class MyClass {
public:
    int value;
};

void example() {
    MyClass obj;  // 栈上对象
    obj.value = 10;
}  // obj 自动析构

// 也可以在堆上
MyClass *obj = new MyClass();  // 堆上对象
delete obj;  // 手动释放

9.2 内存管理方式

Objective-C:引用计数
NSObject *obj1 = [[NSObject alloc] init];  // 引用计数 = 1
NSObject *obj2 = obj1;                      // 引用计数 = 2
obj1 = nil;                                 // 引用计数 = 1
obj2 = nil;                                 // 引用计数 = 0,对象释放
C++:RAII(资源获取即初始化)
class MyClass {
public:
    MyClass() { /* 构造 */ }
    ~MyClass() { /* 析构,自动调用 */ }
};

void example() {
    MyClass obj;  // 构造
    // 使用 obj...
}  // 自动析构(栈上对象)

// 堆上对象需要手动管理
MyClass *obj = new MyClass();
delete obj;  // 手动析构

9.3 多态实现方式

Objective-C:isa 指针 + 消息发送
@interface Animal : NSObject
- (void)speak;
@end

@interface Dog : Animal
- (void)speak;  // 重写
@end

Animal *animal = [[Dog alloc] init];
[animal speak];  // 运行时查找,调用 Dog 的 speak
// 通过 isa 指针找到实际类型
C++:虚函数表(vtable)
class Animal {
public:
    virtual void speak() { /* 基类实现 */ }
    // 有虚函数,对象有 vptr(虚函数表指针)
};

class Dog : public Animal {
public:
    void speak() override { /* 派生类实现 */ }
};

Animal *animal = new Dog();
animal->speak();  // 通过 vptr 找到虚函数表,调用 Dog::speak

9.4 Objective-C++ 混编注意事项

// Objective-C++ 文件(.mm)

// OC 对象
NSObject *obj = [[NSObject alloc] init];

// C++ 对象
std::vector<int> vec;
vec.push_back(1);

// ⚠️ 注意:C++ 异常不能穿越 OC 代码
// 如果 C++ 代码抛异常,必须在 C++ 代码里捕获

9.5 面试常问点

Q:OC 和 C++ 的内存管理有什么区别?

答案要点:

  1. OC:引用计数(ARC),对象在堆上,通过 isa 实现多态
  2. C++ :RAII,对象可在栈/堆,通过虚函数表实现多态
  3. OC:自动管理(ARC),但需注意循环引用
  4. C++ :手动管理(new/delete)或智能指针(shared_ptr/unique_ptr)

10. 虚拟内存与物理内存映射

10.1 什么是虚拟内存?

虚拟内存 = 进程看到的"假地址空间"

进程视角(虚拟地址):
0x00000000 ──────────┐
                     │
0x10000000 ──────────┤ 代码段
                     │
0x20000000 ──────────┤ 数据段
                     │
0x30000000 ──────────┤ 堆
                     │
0x40000000 ──────────┤ 栈
                     │
0x7FFFFFFF ──────────┘

实际物理内存:
[物理地址 0x1000] ← 可能映射到虚拟地址 0x10000000
[物理地址 0x2000] ← 可能映射到虚拟地址 0x20000000
...

10.2 页(Page)的概念

页 = 内存管理的最小单位(通常 4KB 或 16KB)

// 虚拟地址空间被分成页
虚拟地址:0x10000000 - 0x10000FFF  → 页 1
虚拟地址:0x10001000 - 0x10001FFF  → 页 2
虚拟地址:0x10002000 - 0x10002FFF  → 页 3

// 每页可以独立映射到物理内存1 → 物理页 A2 → 物理页 B3 → 未映射(访问会触发缺页异常)

10.3 页表(Page Table)

页表 = 虚拟地址到物理地址的映射表

虚拟地址:0x10000000
         ↓
    页表查找
         ↓
物理地址:0x50000000

10.4 写时复制(Copy-On-Write, COW)

// 场景:fork 进程或复制大对象

// 1. 父进程有数据
NSMutableArray *arr = [NSMutableArray arrayWithObjects:@1, @2, nil];

// 2. 子进程 fork(或复制)
// 此时:父子进程共享同一份物理内存(只读)

// 3. 子进程修改数据
[arr addObject:@3];

// 4. 触发写时复制
// 系统复制物理页,子进程有自己的副本
// 现在:父子进程有独立的物理内存

10.5 代码段页共享

// 多个进程运行同一个 App
进程 A:加载 MyApp
进程 B:加载 MyApp

// 代码段(__TEXT)的物理页被共享
// 节省物理内存!

10.6 ASLR(地址空间布局随机化)

// 没有 ASLR(固定地址)
代码段起始:0x10000000(固定)

// 有 ASLR(随机地址)
进程 1 代码段起始:0x10001234(随机)
进程 2 代码段起始:0x10005678(随机)

// 目的:防止攻击者预测地址

10.7 面试常问点

Q:虚拟内存的作用?

答案要点:

  1. 地址空间隔离:每个进程有独立的虚拟地址空间
  2. 内存保护:不同段有不同的读写执行权限
  3. 按需加载:只有访问的页才映射到物理内存
  4. 共享内存:多个进程可以共享代码段的物理页
  5. 安全性:ASLR 随机化地址,防止攻击

11. Weak 表实现与性能

11.1 Weak 表的底层结构

// 伪代码:Weak 表结构
struct WeakTable {
    // 全局哈希表
    // key: 对象的地址(作为弱引用目标)
    // value: 指向这个对象的所有 weak 指针的数组
    HashMap<void *, Array<void **>> weakReferences;
};

// 示例:
NSObject *obj = [[NSObject alloc] init];
__weak NSObject *weak1 = obj;
__weak NSObject *weak2 = obj;

// Weak 表记录:
// obj 的地址 → [weak1 的地址, weak2 的地址]

11.2 Weak 指针注册流程

NSObject *obj = [[NSObject alloc] init];  // 对象创建
__weak NSObject *weakObj = obj;            // weak 指针赋值

// 系统内部操作(伪代码):
void weak_assign(id *location, id newObj) {
    // 1. 如果之前有 weak 指针,先移除
    if (*location) {
        removeWeakReference(*location, location);
    }
    
    // 2. 设置新的 weak 指针
    *location = newObj;
    
    // 3. 如果新对象不为 nil,注册到 Weak 表
    if (newObj) {
        addWeakReference(newObj, location);
    }
}

11.3 对象释放时的 Weak 清理

// 对象释放流程(伪代码)
void object_release(id obj) {
    // 1. 引用计数减 1
    if (retainCount(obj) > 1) {
        retainCount(obj)--;
        return;
    }
    
    // 2. 引用计数为 0,准备释放
    // 3. 查找 Weak 表,找到所有指向这个对象的 weak 指针
    Array<void **> weakRefs = getWeakReferences(obj);
    
    // 4. 把所有 weak 指针置为 nil
    for (void **weakPtr in weakRefs) {
        *weakPtr = nil;
    }
    
    // 5. 从 Weak 表中移除记录
    removeWeakTableEntry(obj);
    
    // 6. 释放对象内存
    free(obj);
}

11.4 Weak 表的性能考虑

优势
  1. 哈希表查找:O(1) 平均时间复杂度
  2. 批量清理:对象释放时一次性清理所有 weak 指针
潜在开销
// 场景:大量 weak 指针指向同一个对象
NSObject *obj = [[NSObject alloc] init];

for (int i = 0; i < 10000; i++) {
    __weak NSObject *weak = obj;  // 每个 weak 都注册到 Weak 表
}

// 对象释放时,需要清理 10000 个 weak 指针
// 虽然还是 O(n),但 n 可能很大

11.5 面试常问点

Q:Weak 表如何实现?性能如何?

答案要点:

  1. 数据结构:全局哈希表,key 是对象地址,value 是 weak 指针数组
  2. 注册:weak 指针赋值时,注册到 Weak 表
  3. 清理:对象释放时,遍历 Weak 表,把所有 weak 指针置 nil
  4. 性能:哈希表查找 O(1),但大量 weak 指针时清理可能较慢
  5. 优化:系统有优化机制,实际性能通常可接受

总结:完整知识线回顾

从对象到硬件内存的完整路径

1. 代码层面
   └─ OC 对象(Person *p = [[Person alloc] init])
       ├─ isa 指针 → 类对象
       ├─ 成员变量(内存对齐)
       └─ 引用计数管理

2. 运行时层面
   └─ 类对象 / 元类
       ├─ 方法列表
       ├─ 属性列表
       └─ 方法缓存

3. 内存布局层面
   └─ 虚拟地址空间
       ├─ 代码段(__TEXT):类的方法实现
       ├─ 数据段(__DATA):类对象、全局变量
       ├─ 堆:对象实例
       └─ 栈:局部变量、函数调用

4. 系统层面
   └─ 虚拟内存 → 物理内存映射
       ├─ 页表映射
       ├─ ASLR 随机化
       └─ 写时复制

5. 硬件层面
   └─ 物理内存(RAM)
       └─ CPU 缓存(L1/L2/L3)

面试重点检查清单

  • ARC 的 retain/release 插入规则
  • 循环引用的典型场景和解决方案
  • Weak 表的工作机制
  • 内存对齐规则和对象大小计算
  • Tagged Pointer 的原理和优势
  • Mach-O 文件结构和段的作用
  • AutoreleasePool 与 RunLoop 的关系
  • 堆分配策略和内存碎片
  • 栈溢出原因和避免方法
  • 类/元类查找链和方法缓存
  • OC vs C++ 内存模型差异
  • 虚拟内存到物理内存的映射
  • Weak 表的实现和性能

祝你面试顺利! 🚀

昨天以前掘金 iOS

SwiftUI 中的 @ViewBuilder 全面解析

作者 汉秋
2025年12月19日 10:17

SwiftUI 中的 @ViewBuilder 全面解析

在 SwiftUI 的世界里,@ViewBuilder 是一个你每天都在用,却可能从未认真了解过的核心机制

很多 SwiftUI 看起来“像写 DSL 一样优雅”的代码,其实都离不开它。

本文将从为什么需要它、它解决了什么问题、如何使用、常见坑点几个维度,系统性地介绍 @ViewBuilder,适合 SwiftUI 初学者到中级开发者 阅读。


一、问题的起点:Swift 只能返回一个值

在 Swift 中,函数或计算属性只能返回一个值

但在 SwiftUI 中,我们却经常写出这样的代码:

var body: some View {
    Text("Hello")
    Image(systemName: "star")
    Button("Tap") { }
}

表面看起来像是“返回了多个 View”,这在普通 Swift 函数里是不可能的

那 SwiftUI 是怎么做到的?

答案就是: @ViewBuilder


二、@ViewBuilder 是什么

@ViewBuilder 是 Swift 的一种 Result Builder(结果构建器)

它的核心职责只有一个:

把多行 View 表达式,组合成一个 View 返回。

你写的代码是这样:

Text("A")
Text("B")
Text("C")

编译器在背后会帮你组合成类似:

TupleView<(Text, Text, Text)>

但这些具体类型对开发者是隐藏的,你只需要关心:

可以像写布局一样写 View,而不是手动拼装结构。


三、为什么你很少看到 @ViewBuilder

因为 SwiftUI 已经帮你加好了。

例如:

struct ContentView: View {
    var body: some View {
        Text("Hello")
        Text("World")
    }
}

实际上等价于:

struct ContentView: View {
    @ViewBuilder
    var body: some View {
        Text("Hello")
        Text("World")
    }
}

👉 body 天生就支持多 View 与条件语法


四、@ViewBuilder 支持哪些能力

1️⃣ 多个 View

@ViewBuilder
var content: some View {
    Text("Title")
    Text("Subtitle")
}

2️⃣ if / else 条件渲染(非常重要)

没有 @ViewBuilder,下面代码是非法的:

func makeView(flag: Bool) -> some View {
    if flag {
        Text("Yes")
    } else {
        Text("No")
    }
}

使用 @ViewBuilder 后:

@ViewBuilder
func makeView(flag: Bool) -> some View {
    if flag {
        Text("Yes")
    } else {
        Text("No")
    }
}

👉 这正是 SwiftUI 条件 UI 渲染的基础能力


3️⃣ 只有 if(没有 else

@ViewBuilder
var body: some View {
    Text("Always Visible")

    if isLogin {
        Text("Welcome")
    }
}

当条件不成立时,SwiftUI 会自动插入一个 EmptyView


4️⃣ switch

@ViewBuilder
func stateView(_ state: LoadState) -> some View {
    switch state {
    case .loading:
        ProgressView()
    case .success:
        Text("Success")
    case .error:
        Text("Error")
    }
}

五、最常见的使用场景

1️⃣ 自定义组件的内容闭包

struct Card<Content: View>: View {
    let content: Content

    init(@ViewBuilder content: () -> Content) {
        self.content = content()
    }

    var body: some View {
        VStack(spacing: 8) {
            content
        }
        .padding()
        .background(.gray.opacity(0.2))
        .cornerRadius(12)
    }
}

使用时:

Card {
    Text("Title")
    Text("Subtitle")
}

👉 这正是 SwiftUI 组件化体验优秀的原因之一。


2️⃣ 模仿系统 API(如 .sheet / .toolbar

func customOverlay<Content: View>(
    @ViewBuilder content: () -> Content
) -> some View {
    overlay {
        content()
    }
}

六、常见坑点(非常容易踩)

❌ 1. 不能写普通逻辑代码

@ViewBuilder
var body: some View {
    let count = 10 // ❌ 编译错误
    Text("(count)")
}

原因是:

@ViewBuilder 只接受 生成 View 的表达式

✅ 正确方式:

var count: Int { 10 }

@ViewBuilder
var body: some View {
    Text("(count)")
}

❌ 2. 不能直接使用 for 循环

@ViewBuilder
var body: some View {
    for i in 0..<3 { // ❌
        Text("(i)")
    }
}

✅ 正确方式:

ForEach(0..<3, id: .self) { i in
    Text("(i)")
}

七、什么时候需要主动使用 @ViewBuilder

当你遇到以下情况时,就该考虑它:

  • 希望一个函数 / 闭包返回 多个 View
  • 需要在返回 View 时使用 if / else / switch
  • 编写 可组合的自定义组件

简单判断法则:

“这个 API 是否应该像 SwiftUI 一样写 UI?”

如果答案是「是」,那基本就需要 @ViewBuilder


八、总结

  • @ViewBuilder 是 SwiftUI 的核心基础设施
  • 它让 Swift 支持 声明式 UI 语法
  • 条件渲染、多 View 组合、本质都依赖它
  • 写组件时,合理使用 @ViewBuilder 能极大提升 API 体验

一句话总结:

没有 @ViewBuilder,就没有今天的 SwiftUI。


如果你觉得这篇文章有帮助,欢迎点赞 / 收藏 / 交流 🙌

后续也可以深入聊:

  • ViewBuilder 源码实现
  • @ViewBuilder 与 @ToolbarContentBuilder 的区别
  • SwiftUI 新数据流(@Observable / @Bindable)下的最佳实践

1V1 社交精准收割 3.6 亿!40 款马甲包 + 国内社交难度堪比史诗级!

作者 iOS研究院
2025年12月18日 21:06

背景

“她说明年就结婚,转头就把我拉黑了!”2024 年 9 月,山东鱼台县居民王某攥着手机账单冲进警局,声音颤抖。这位常年打工攒下 5 万积蓄的单身汉,从未想过自己在 “念梦”“冬梦” 两款交友 App 上邂逅的 “化妆品店老板娘”,竟是一场精心设计的骗局。

三个月里,这位昵称 “为你而来” 的 “女神” 温柔体贴,频频描绘二人未来的家,却以 “解锁视频聊天”“线下见面需充值刷亲密度” 为由,分三次榨干了他的全部积蓄。当王某停止充值后,昔日热情的恋人瞬间蒸发,只留下 27177 元、9592 元、13794 元三笔冰冷的充值记录。他不知道的是,自己只是这场 3.6 亿诈骗大案中,上千名受害者之一。

40 款马甲包背后:堪比上市公司的诈骗 “工厂”

山东济宁公安破获特大网络交友诈骗案,40余款App全是陷阱。王某的报警,像一把钥匙打开了潘多拉魔盒。警方顺着涉诈 App 的线索深挖,一个隐藏在合法公司外壳下的犯罪集团逐渐浮出水面。团伙头目王某某是正规大学毕业生,曾因运营 “来遇” App 涉诈被查处,却在 2023 年卷土重来,注册多家空壳公司,一口气推出 40 余款交友 App,形成 “换汤不换药” 的马甲矩阵。

这个诈骗团伙的运作模式堪称 “产业化”:运营部负责招募培训 5000 余名女聊手,定制从 “初遇暧昧” 到 “诱导充值” 的全套话术;客服部专门安抚投诉用户,用 “系统维护”“亲密度未达标” 等借口掩盖骗局;甚至设立法务部,钻法律空子规避监管。女聊手们则按照统一剧本,虚构 “单身富婆”“温柔贤妻” 等人设,精准瞄准三、四线城市的大龄单身男性,用暧昧言语和虚假承诺编织情感牢笼。

更令人咋舌的是平台设计的 “吸血机制”:文字消息 10-100 金币 / 条,视频通话 100-2000 金币 / 分钟,充值 1 元仅能兑换 100 金币。女聊手与公司按 4:6 分成,为了多赚钱,她们会用平台发放的免费金币给用户刷礼物,制造 “双向奔赴” 的假象,引诱受害者不断充值。警方后续查获的聊天记录显示,团伙内部流传着 “养鱼玩法拉高点,大哥刷一你刷两” 的黑暗话术。

62 亿条数据剥茧:千人跨省追缉 15 天破局

“这不是零散诈骗,是有组织、有预谋的犯罪网络。” 济宁市公安局迅速成立 “10.14” 专案组,抽调百余名警力攻坚。面对团伙设置的多层数据加密、定期删除证据、核心骨干分散办公等障碍,民警自主编写分析程序,从 8T 容量、超 62 亿条聊天记录和资金明细中抽丝剥茧。

合规化势在必行

立足当前行业大环境,存量社交产品必须将合规化置于开发工作的核心首位。

若不存在关键性的功能迭代需求,建议尽量减少版本更新频次,甚至暂停更新,以此规避审核环节可能出现的风险,避免给产品运营增添不必要的阻碍。

当前国内市场的恶性竞争态势,必然会导致社交类产品在App Store平台面临更严峻的监管压力与发展困境。因此,尽早布局出海业务、开拓海外新市场,已成这类产品突破发展瓶颈的关键方向

合规化的价值懂的无需多言,不懂得多说无益。

遵守规则,方得长治久安,最后祝大家大吉大利,今晚过审!

相关推荐

# 苹果开发者续费大坑及成功续费方案!亲测有效

# AppStore敏感词排查手册,多维度分析Guideline 2.3.1隐藏功能,轻松过审。

# 如何主动提防苹果3.2f的进攻,自查防御手册(代码篇)

# 如何主动提防苹果3.2f的进攻,自查防御手册(ASO篇)

# 苹果加急审核是“绿色通道”还是“死亡陷阱”?

# 苹果开发者邮箱,突然收到11.2通知严重么?

# 不想被苹果卡审最好错开这两个提审时间

# 手撕苹果审核4.3是代码问题还是设计问题?

# 有幸和Appstore审核人员进行了一场视频会议特此记录。

【iOS】如何在 iOS 26 的UITabBarController中使用自定义TabBar

2025年12月18日 16:19

Demo地址:ClassicTabBarUsingDemo(主要实现代码可搜索“📌”查看)

前言

苹果自 iOS 26 起就使用全新的UI --- Liquid Glass,导致很多系统组件也被迫强制使用,首当其冲就是UITabBarController,对于很多喜欢使用自定义TabBar的开发者来说,这很是无奈:

  • 强行给你套个玻璃罩子

那如何在 iOS 26UITabBarController继续使用自定义TabBar呢?这里介绍一下两种方案。

方案一

来自大佬网友分享的方案 💪

  1. 自定义TabBar使用UITabBar,通过KVC设置(老方法):
setValue(customTabBar, forKeyPath: "tabBar")
  1. 重写UITabBaraddSubviewaddGestureRecognizer方法:
- (void)addSubview:(UIView *)view {
    if ([view isKindOfClass:NSClassFromString(@"UIKit._UITabBarPlatterView")]) {
        view.hidden = YES;
    }
    [super addSubview:view];
}

- (void)addGestureRecognizer:(UIGestureRecognizer *)gestureRecognizer {
    if ([gestureRecognizer isKindOfClass:NSClassFromString(@"_UIContinuousSelectionGestureRecognizer")]) {
        gestureRecognizer.enabled = NO;
    }
    [super addGestureRecognizer:gestureRecognizer];
}

解释一下:

_UITabBarPlatterView这个是显示当前Tab的玻璃罩子:

  • 把它隐藏掉就行了

_UIContinuousSelectionGestureRecognizer这个是系统用来处理TabBar切换时的动画手势,触发时会在TabBar上添加_UIPortalView这个跟随手势的玻璃罩子:

  • 同样把它禁止掉就行了

这样就相当于把UITabBar的液态玻璃“移除”掉了,是可以实现以往的显示效果👏。

只不过这个方案在pop手势滑动时,TabBar会被「置顶」显示:

  • 这是苹果新UI的显示逻辑,暂时无法改动

这跟我的预期还差了一点,我是希望连pop手势也能像以前那样:

接下来介绍另一个方案,虽然麻烦很多,但能兼顾pop手势。

方案二

经观察,以往TabBar的显示效果,个人猜测系统是把TabBar放到当前子VC的view上:

按照这个思路可以这么实现:

  1. 首先自定义TabBar要使用UIView(如果使用的是私自改造的UITabBar,得换成UIView了),并且隐藏系统TabBar。
class MainTabBarController: UITabBarController {
    ......
    
    /// 自定义TabBar
    private let customTabBar = WLTabBar()
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        // 隐藏系统TabBar
        setTabBarHidden(true, animated: false)
    }
    
    ......
}

  1. TabBarController及其子VC都创建一个专门存放自定义TabBar的容器,且层级必须是最顶层(之后添加的子视图都得插到TabBar容器的下面)。
class BaseViewController: UIViewController {
    /// 专门存放自定义TabBar的容器
    private let tabBarContainer = TabBarContainer()

    override func viewDidLoad() {
        super.viewDidLoad()
        
        tabBarContainer.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(tabBarContainer)
        NSLayoutConstraint.activate([
            tabBarContainer.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            tabBarContainer.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            tabBarContainer.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            tabBarContainer.heightAnchor.constraint(equalToConstant: Env.tabBarFullH) // 下巴+TabBar高度
        ])
    }
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        // 层级必须是最顶层
        view.bringSubviewToFront(tabBarContainer)
    }
    
    // 将自定义TabBar放到自己的TabBar容器上
    func addTabBar(_ tabBar: UIView) {
        tabBar.superview?.isUserInteractionEnabled = false
        tabBarContainer.addSubview(tabBar)
        tabBarContainer.isUserInteractionEnabled = true
    }
}
  1. 最后,TabBarController当前显示哪个子VC,就把自定义TabBar放到对应子VC的TabBar容器上,这样则不会影响pushpresent其他VC。

OK,完事了😗。

注意点

核心实现就是以上3点,接下来讲一下注意点。

上面说到,TabBarController也得创建一个TabBar容器,这主要是用来切换子VC的:

在切换子VC前,自定义TabBar必须先放到TabBarController的TabBar容器上,切换后再放到目标子VC的TabBar容器上。

🤔为什么?

一般子VC的内容都是懒加载(看到才构建),如果是很复杂的界面,不免会有卡顿的情况,如果直接把自定义TabBar丢过去,TabBar会闪烁一下,效果不太好;另外自 iOS 18 起切换子VC会带有默认的系统动画,其动画作用于子VC的view上,即便该子VC早就构建好,立马转移TabBar也会闪烁一下。

因此个人建议先把自定义TabBarTabBarControllerTabBar容器上(层级在所有子VC的view之上),延时一下(确保子VC完全构建好且已完全显示,同时避免被系统动画影响)再放到目标子VC的TabBar容器上,这样就能完美实现切换效果了。

核心代码如下:

// MARK: - 挪动TabBar到目标子VC
private extension MainTabBarController {
    func moveTabBar(from sourceIdx: Int, to targetIdx: Int) {
        guard Env.isUsingLiquidGlassUI else { return }
        
        // #1 取消上一次的延时操作
        moveTabBarWorkItem?.cancel()
        moveTabBarWorkItem = nil
        
        guard let viewControllers, viewControllers.count > 0 else {
            addTabBar(customTabBar)
            return
        }
        
        guard sourceIdx != targetIdx else {
            _moveTabBar(to: targetIdx)
            return
        }
        
        // #2 如果「当前子VC」现在不是处于栈顶,就把tabBar直接挪到「目标子VC」
        let sourceNavCtr = viewControllers[sourceIdx] as? UINavigationController
        if (sourceNavCtr?.viewControllers.count ?? 0) > 1 {
            _moveTabBar(to: targetIdx)
            return
        }
        
        // #3 能来这里说明「当前子VC」正处于栈顶,如果「目标子VC」此时也处于栈顶,就把tabBar放到层级顶部(不受系统切换动画的影响)
        let targetNavCtr = viewControllers[targetIdx] as? UINavigationController
        if (targetNavCtr?.viewControllers.count ?? 0) == 1 {
            addTabBar(customTabBar)
        } else {
            _moveTabBar(to: sourceIdx)
        }
        
        // #3.1 延迟0.5s后再放入到「目标子VC」,给VC有足够时间去初始化和显示(可完美实现旧UI的效果;中途切换会取消这个延时操作#1)
        moveTabBarWorkItem = Asyncs.mainDelay(0.5) { [weak self] in
            guard let self, self.selectedIndex == targetIdx else { return }
            self.moveTabBarWorkItem = nil
            self._moveTabBar(to: targetIdx)
        }
    }
    
    func _moveTabBar(to index: Int) {
        let tab = MainTab(index: index)
        switch tab {
        case .videoHub:
            videoHubVC.addTabBar(customTabBar)
        case .channel:
            channelVC.addTabBar(customTabBar)
        case .live:
            liveVC.addTabBar(customTabBar)
        case .mine:
            mineVC.addTabBar(customTabBar)
        }
    }
}

如果想移除系统切换动画可以这么做:

// MARK: - <WLTabBarDelegate>
extension MainTabBarController: WLTabBarDelegate {
    func tabBar(_ tabBar: WLTabBar!, didSelectItemAt index: Int) {
        moveTabBar(from: selectedIndex, to: index)
        // 想移除系统自带的切换动画就👇🏻
        UIView.performWithoutAnimation {
            self.selectedIndex = index
        }
    }
}

小结

方案一是比较激进的魔改方案,直接把系统的玻璃罩子和手势给移除掉了,缺点是如果苹果以后改动了这些私有类名或行为,可能会导致失效。

方案二是我能想到最完美的方案了,起码不用自定义UITabBarController,简单粗暴,个人感觉能应付80%的应用场景吧,除非你有非常特殊的过场动画需要挪动TabBar的。

以上就是我的方案了,起码不用自定义UITabBarController,简单粗暴,个人感觉能应付80%的应用场景吧,除非你有非常特殊的过场动画需要挪动TabBar的。

更多细节可以参考Demo,以上两种方案都有提供,只需要在WLTabBar.h中选择使用哪一种父类并注释另一个即可:

@interface WLTabBar : UITabBar // 方案一
@interface WLTabBar : UIView // 方案二

希望苹果以后能推出兼容自定义TabBar的API,那就不用这样魔改了😩。

❌
❌