阅读视图

发现新文章,点击刷新页面。

一文精通-Mixin特性

Dart Mixin 详细指南

1. 基础 Mixin 用法

1.1 基本 Mixin 定义和使用

dart

// 定义 Mixin
mixin LoggerMixin {
  String tag = 'Logger';
  
  void log(String message) {
    print('[$tag] $message');
  }
  
  void debug(String message) {
    print('[$tag] DEBUG: $message');
  }
}

mixin ValidatorMixin {
  bool validateEmail(String email) {
    return RegExp(r'^[^@]+@[^@]+.[^@]+').hasMatch(email);
  }
  
  bool validatePhone(String phone) {
    return RegExp(r'^[0-9]{10,11}$').hasMatch(phone);
  }
}

// 使用 Mixin
class UserService with LoggerMixin, ValidatorMixin {
  void registerUser(String email, String phone) {
    if (validateEmail(email) && validatePhone(phone)) {
      log('用户注册成功: $email');
    } else {
      debug('注册信息验证失败');
    }
  }
}

void main() {
  final service = UserService();
  service.registerUser('test@example.com', '13800138000');
}

2. Mixin 定义抽象方法

dart

mixin AuthenticationMixin {
  // 抽象方法 - 强制混入类实现
  Future<String> fetchToken();
  
  // 具体方法 - 可以使用抽象方法
  Future<Map<String, dynamic>> getProfile() async {
    final token = await fetchToken();
    log('使用 token: $token 获取用户资料');
    return {'name': '张三', 'token': token};
  }
  
  void log(String message) {
    print('[Auth] $message');
  }
}

class ApiService with AuthenticationMixin {
  @override
  Future<String> fetchToken() async {
    // 实现抽象方法
    await Future.delayed(Duration(milliseconds: 100));
    return 'jwt_token_123456';
  }
}

void main() async {
  final api = ApiService();
  final profile = await api.getProfile();
  print('用户资料: $profile');
}

3. 使用 on 关键字限制 Mixin 范围

dart

// 基类
abstract class Animal {
  String name;
  Animal(this.name);
  
  void eat() {
    print('$name 正在吃东西');
  }
}

// 只能用于 Animal 及其子类的 Mixin
mixin WalkerMixin on Animal {
  void walk() {
    print('$name 正在行走');
    eat(); // 可以访问宿主类的方法
  }
}

mixin SwimmerMixin on Animal {
  void swim() {
    print('$name 正在游泳');
  }
}

// 正确使用
class Dog extends Animal with WalkerMixin {
  Dog(String name) : super(name);
  
  void bark() {
    print('$name: 汪汪!');
  }
}

// 错误使用(编译错误):
// class Robot with WalkerMixin {} // 错误:WalkerMixin 只能用于 Animal

void main() {
  final dog = Dog('小黑');
  dog.walk();  // 小黑 正在行走
  dog.bark();  // 小黑: 汪汪!
  dog.eat();   // 小黑 正在吃东西
}

4. 多 Mixin 组合

dart

// 功能模块化 Mixin
mixin ApiClientMixin {
  Future<Map<String, dynamic>> get(String url) async {
    print('GET 请求: $url');
    await Future.delayed(Duration(milliseconds: 100));
    return {'status': 200, 'data': '响应数据'};
  }
}

mixin CacheMixin {
  final Map<String, dynamic> _cache = {};
  
  void cacheData(String key, dynamic data) {
    _cache[key] = data;
  }
  
  dynamic getCache(String key) => _cache[key];
}

mixin LoggingMixin {
  void logRequest(String method, String url) {
    print('[${DateTime.now()}] $method $url');
  }
}

// 组合多个 Mixin
class NetworkService with ApiClientMixin, CacheMixin, LoggingMixin {
  Future<Map<String, dynamic>> fetchWithCache(String url) async {
    final cached = getCache(url);
    if (cached != null) {
      print('使用缓存数据');
      return cached;
    }
    
    logRequest('GET', url);
    final response = await get(url);
    cacheData(url, response);
    
    return response;
  }
}

void main() async {
  final service = NetworkService();
  final result1 = await service.fetchWithCache('/api/user');
  final result2 = await service.fetchWithCache('/api/user'); // 第二次使用缓存
}

5. 同名方法冲突与线性化顺序

dart

mixin A {
  String message = '来自A';
  
  void show() {
    print('A.show(): $message');
  }
  
  void methodA() {
    print('A.methodA()');
  }
}

mixin B {
  String message = '来自B';
  
  void show() {
    print('B.show(): $message');
  }
  
  void methodB() {
    print('B.methodB()');
  }
}

mixin C {
  String message = '来自C';
  
  void show() {
    print('C.show(): $message');
  }
}

// 父类
class Base {
  String message = '来自Base';
  
  void show() {
    print('Base.show(): $message');
  }
}

// 混入顺序:Base -> A -> B -> C(最后混入的优先级最高)
class MyClass extends Base with A, B, C {
  // 可以通过super调用线性化链中的方法
  @override
  void show() {
    super.show(); // 调用C的show方法
    print('MyClass.show() 完成');
  }
}

// 线性化顺序验证
class AnotherClass with C, B, A {
  // 顺序:Object -> C -> B -> A
  void test() {
    show(); // 调用A的show(最后混入)
    print(message); // 输出:来自A
  }
}

void main() {
  print('=== MyClass 测试 ===');
  final obj1 = MyClass();
  obj1.show();    // 调用C.show(),因为C最后混入
  print(obj1.message); // 输出:来自C
  
  print('\n=== AnotherClass 测试 ===');
  final obj2 = AnotherClass();
  obj2.test();
  
  print('\n=== 方法调用链 ===');
  obj1.methodA(); // 可以调用
  obj1.methodB(); // 可以调用
  
  // 验证类型
  print('\n=== 类型检查 ===');
  print(obj1 is Base); // true
  print(obj1 is A);    // true
  print(obj1 is B);    // true
  print(obj1 is C);    // true
}

6. 复杂的线性化顺序示例

dart

class Base {
  void execute() => print('Base.execute()');
}

mixin Mixin1 {
  void execute() {
    print('Mixin1.execute() - 开始');
    super.execute();
    print('Mixin1.execute() - 结束');
  }
}

mixin Mixin2 {
  void execute() {
    print('Mixin2.execute() - 开始');
    super.execute();
    print('Mixin2.execute() - 结束');
  }
}

mixin Mixin3 {
  void execute() {
    print('Mixin3.execute() - 开始');
    super.execute();
    print('Mixin3.execute() - 结束');
  }
}

class MyService extends Base with Mixin1, Mixin2, Mixin3 {
  @override
  void execute() {
    print('MyService.execute() - 开始');
    super.execute(); // 调用链:Mixin3 -> Mixin2 -> Mixin1 -> Base
    print('MyService.execute() - 结束');
  }
}

void main() {
  final service = MyService();
  service.execute();
  
  // 输出顺序:
  // MyService.execute() - 开始
  // Mixin3.execute() - 开始
  // Mixin2.execute() - 开始
  // Mixin1.execute() - 开始
  // Base.execute()
  // Mixin1.execute() - 结束
  // Mixin2.execute() - 结束
  // Mixin3.execute() - 结束
  // MyService.execute() - 结束
}

7. 工厂模式与 Mixin

dart

// 可序列化接口
abstract class Serializable {
  Map<String, dynamic> toJson();
}

// Mixin 提供序列化功能
mixin JsonSerializableMixin implements Serializable {
  @override
  Map<String, dynamic> toJson() {
    final json = <String, dynamic>{};
    
    // 使用反射获取所有字段(实际项目中可能需要 dart:mirrors 或代码生成)
    // 这里简化处理
    for (final field in _getFields()) {
      json[field] = _getFieldValue(field);
    }
    
    return json;
  }
  
  List<String> _getFields() {
    // 实际实现应使用反射
    return [];
  }
  
  dynamic _getFieldValue(String field) {
    // 实际实现应使用反射
    return null;
  }
}

// 使用 Mixin 增强类的功能
class User with JsonSerializableMixin {
  final String name;
  final int age;
  
  User(this.name, this.age);
  
  @override
  List<String> _getFields() => ['name', 'age'];
  
  @override
  dynamic _getFieldValue(String field) {
    switch (field) {
      case 'name': return name;
      case 'age': return age;
      default: return null;
    }
  }
}

void main() {
  final user = User('张三', 25);
  print(user.toJson()); // {name: 张三, age: 25}
}

8. 依赖注入模式中的 Mixin

dart

// 服务定位器 Mixin
mixin ServiceLocatorMixin {
  final Map<Type, Object> _services = {};
  
  void registerService<T>(T service) {
    _services[T] = service;
  }
  
  T getService<T>() {
    final service = _services[T];
    if (service == null) {
      throw StateError('未找到服务: $T');
    }
    return service as T;
  }
}

// 网络服务
class NetworkService {
  Future<String> fetchData() async {
    await Future.delayed(Duration(milliseconds: 100));
    return '网络数据';
  }
}

// 数据库服务
class DatabaseService {
  Future<String> queryData() async {
    await Future.delayed(Duration(milliseconds: 50));
    return '数据库数据';
  }
}

// 使用 Mixin 的应用类
class MyApp with ServiceLocatorMixin {
  MyApp() {
    // 注册服务
    registerService(NetworkService());
    registerService(DatabaseService());
  }
  
  Future<void> run() async {
    final network = getService<NetworkService>();
    final database = getService<DatabaseService>();
    
    final results = await Future.wait([
      network.fetchData(),
      database.queryData(),
    ]);
    
    print('结果: $results');
  }
}

void main() async {
  final app = MyApp();
  await app.run();
}

9. Mixin 最佳实践示例

dart

// 1. 单一职责的 Mixin
mixin EquatableMixin<T> {
  bool equals(T other);
  
  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is T && equals(other);
      
  @override
  int get hashCode => toString().hashCode;
}

mixin CloneableMixin<T> {
  T clone();
}

// 2. 带生命周期的 Mixin
mixin LifecycleMixin {
  bool _isInitialized = false;
  
  void initialize() {
    if (!_isInitialized) {
      _onInit();
      _isInitialized = true;
    }
  }
  
  void dispose() {
    if (_isInitialized) {
      _onDispose();
      _isInitialized = false;
    }
  }
  
  // 钩子方法
  void _onInit() {}
  void _onDispose() {}
}

// 3. 可观察的 Mixin
mixin ObservableMixin {
  final List<Function()> _listeners = [];
  
  void addListener(Function() listener) {
    _listeners.add(listener);
  }
  
  void removeListener(Function() listener) {
    _listeners.remove(listener);
  }
  
  void notifyListeners() {
    for (final listener in _listeners) {
      listener();
    }
  }
}

// 使用多个 Mixin 的模型类
class UserModel with EquatableMixin<UserModel>, CloneableMixin<UserModel>, ObservableMixin {
  String name;
  int age;
  
  UserModel(this.name, this.age);
  
  @override
  bool equals(UserModel other) =>
      name == other.name && age == other.age;
      
  @override
  UserModel clone() => UserModel(name, age);
  
  void updateName(String newName) {
    name = newName;
    notifyListeners(); // 通知观察者
  }
  
  @override
  String toString() => 'User(name: $name, age: $age)';
}

void main() {
  final user1 = UserModel('Alice', 30);
  final user2 = UserModel('Alice', 30);
  final user3 = user1.clone();
  
  print('user1 == user2: ${user1 == user2}'); // true
  print('user1 == user3: ${user1 == user3}'); // true
  
  // 添加监听器
  user1.addListener(() {
    print('用户数据已更新!');
  });
  
  user1.updateName('Bob'); // 触发监听器
}

Mixin 详细总结

特性总结

特性 说明
定义方式 使用 mixin 关键字定义
使用方式 使用 with 关键字混入到类中
继承限制 每个类只能继承一个父类,但可以混入多个 Mixin
实例化 Mixin 不能被实例化,只能被混入
构造函数 Mixin 不能声明构造函数(无参构造函数除外)
抽象方法 可以包含抽象方法,强制宿主类实现
范围限制 可以使用 on 关键字限制 Mixin 只能用于特定类
线性化顺序 混入顺序决定方法调用优先级(最后混入的优先级最高)
类型系统 Mixin 在类型系统中是透明的,宿主类拥有 Mixin 的所有接口

使用场景

  1. 横切关注点(Cross-cutting Concerns)

    • 日志记录、权限验证、性能监控
    • 数据验证、格式转换
  2. 功能组合(Feature Composition)

    • UI 组件的功能组合
    • 服务类的功能增强
  3. 接口增强(Interface Enhancement)

    • 为现有类添加额外功能而不修改原始类
    • 实现装饰器模式
  4. 代码复用(Code Reuse)

    • 将通用逻辑抽离为可复用模块
    • 避免重复代码

优点

  1. 灵活性高:可以组合多个 Mixin,实现类似多继承的效果
  2. 解耦性强:功能模块化,职责单一
  3. 避免钻石问题:通过线性化顺序解决多继承中的歧义问题
  4. 类型安全:编译时检查,运行时性能好
  5. 易于测试:可以单独测试 Mixin 的功能

缺点

  1. 理解成本:线性化顺序需要理解
  2. 调试困难:方法调用链可能较长
  3. 过度使用风险:可能导致类结构复杂
  4. 命名冲突:不同 Mixin 的同名方法可能冲突

最佳实践

  1. 单一职责:每个 Mixin 只负责一个明确的功能
  2. 命名清晰:使用 Mixin 后缀,如 LoggerMixin
  3. 适度使用:避免过度使用导致代码难以理解
  4. 文档注释:说明 Mixin 的作用和使用方式
  5. 考虑替代方案:有时继承或组合可能是更好的选择

与相关概念的对比

概念 与 Mixin 的区别
抽象类 可以有构造函数、可以有状态;Mixin 不能有构造函数
接口 只定义契约,不提供实现;Mixin 可以提供实现
扩展方法 在类外部添加方法;Mixin 在类内部添加
继承 单继承,强调 "is-a" 关系;Mixin 强调 "has-a" 或 "can-do" 关系

Mixin 是 Dart 语言中非常强大的特性,合理使用可以让代码更加模块化、可复用和可维护。

1. 什么是 Mixin?它的主要作用是什么?

精准回答:
"Mixin 是 Dart 中一种代码复用机制,它允许一个类通过 with 关键字混入一个或多个独立的功能模块。Mixin 的主要作用是解决 Dart 单继承的限制,实现类似多继承的效果,让代码更加模块化和可复用。"

加分点:

  • 强调 "代码复用机制" 而非 "继承机制"
  • 提到 "单继承限制" 和 "类似多继承"
  • 说明主要使用场景:横向功能扩展

2. Mixin 和继承、接口有什么区别?

精准回答(表格对比):

特性 Mixin 继承 接口
关系 "具有" 功能 (has-a) "是一个" (is-a) "能做什么" (can-do)
数量 可多个 单继承 可实现多个
实现 可包含具体实现 可包含具体实现 只定义契约
构造函数 不能有(除无参) 可以有 不能有
关键字 with extends implements

详细补充:
"Mixin 强调的是功能组合,让类获得某些能力;继承强调的是父子关系;接口强调的是契约实现。Mixin 提供了比接口更灵活的实现复用,又避免了传统多继承的复杂性。"

3. Mixin 的线性化顺序是什么?如何确定?

精准回答:
"Mixin 的线性化顺序遵循以下规则:

  1. 从继承链的最顶端开始
  2. 按照 with 关键字后 Mixin 的声明顺序,从左到右处理
  3. 最后混入的 Mixin 优先级最高

线性化算法:  深度优先,从左到右,不重复。"

示例说明:

dart

class A {}
mixin B {}
mixin C {}
class D extends A with B, C {}
// 线性化顺序:A → B → C → D
// 方法查找顺序:D → C → B → A → Object

4. Mixin 可以包含抽象方法吗?有什么作用?

精准回答:
"可以。Mixin 中包含抽象方法的主要作用是:

  1. 强制约束:强制混入类必须实现某些方法
  2. 模板方法模式:在 Mixin 中定义算法骨架,抽象方法由混入类具体实现
  3. 依赖注入:要求宿主类提供必要的依赖或实现"

示例:

dart

mixin ValidatorMixin {
  bool validate(String input); // 抽象方法
  void validateAndProcess(String input) {
    if (validate(input)) {
      // 处理逻辑
    }
  }
}

5. on 关键字在 Mixin 中有什么作用?

精准回答:
"on 关键字用于限制 Mixin 的使用范围,确保 Mixin 只能用于特定类型或其子类。主要有两个作用:

  1. 类型安全:防止误用,确保 Mixin 只在合适的上下文中使用
  2. 访问宿主类成员:可以安全地访问宿主类的方法和属性"

示例:

dart

mixin Walker on Animal {
  void walk() {
    move(); // 可以安全调用 Animal 的方法
  }
}
// 只能用于 Animal 及其子类

6. 多个 Mixin 有同名方法时如何解决冲突?

精准回答:
"Dart 通过线性化顺序解决同名方法冲突:

  1. 最后混入的优先级最高:线性化链中靠后的覆盖前面的
  2. 可以使用 super:调用线性化链中下一个实现
  3. 可以重写覆盖:在宿主类中重写方法进行统一处理

这是编译时确定的,不会产生运行时歧义。"

冲突解决示例:

dart

class MyClass with A, B {
  @override
  void conflictMethod() {
    // 调用特定 Mixin 的方法
    super.conflictMethod(); // 调用 B 的实现
  }
}

7. Mixin 可以有构造函数吗?为什么?

精准回答:
"Mixin 不能声明有参数的构造函数,只能有默认的无参构造函数。这是因为:

  1. 初始化顺序问题:多个 Mixin 的构造函数调用顺序难以确定
  2. 简化设计:避免复杂的初始化逻辑冲突
  3. 职责分离:Mixin 应该专注于功能实现,而不是对象构建

如果需要初始化逻辑,可以使用初始化方法配合调用。"

8. Mixin 在实际项目中有哪些典型应用场景?

精准回答(结合实际经验):
"在实际项目中,我主要将 Mixin 用于:

  1. 横切关注点(Cross-cutting Concerns)

    • 日志记录、性能监控、异常处理
    • 权限验证、数据校验
  2. UI 组件功能组合

    dart

    class Button with HoverEffect, RippleEffect, TooltipMixin {}
    
  3. 服务层功能增强

    dart

    class ApiService with CacheMixin, RetryMixin, LoggingMixin {}
    
  4. 设计模式实现

    • 装饰器模式:动态添加功能
    • 策略模式:算法切换"

9. Mixin 的优缺点是什么?

精准回答:
优点:

  1. 灵活复用:突破单继承限制
  2. 模块化:功能分离,职责单一
  3. 避免重复:DRY 原则
  4. 组合优于继承:更灵活的设计

缺点:

  1. 理解成本:线性化顺序需要理解
  2. 调试困难:调用链可能很深
  3. 命名冲突:需要合理设计
  4. 过度使用风险:可能导致 "瑞士军刀" 类

10. 什么时候应该使用 Mixin?什么时候不应该使用?

精准回答:
"应该使用 Mixin 的情况:

  1. 需要横向复用功能时
  2. 功能相对独立,不依赖过多上下文
  3. 多个类需要相同功能但类型层次不同时
  4. 需要动态组合功能时

不应该使用 Mixin 的情况:

  1. 功能之间有强耦合时
  2. 需要初始化复杂状态时
  3. 功能是类的核心职责时(应该用继承)
  4. 简单的工具方法(考虑用扩展方法)"

11. Mixin 和扩展方法(Extension Methods)有什么区别?

精准回答:
"两者都用于扩展类型功能,但适用场景不同:

方面 Mixin 扩展方法
作用域 类内部 类外部
访问权限 可访问私有成员 只能访问公开成员
适用性 需要状态时 纯函数操作时
使用方式 with 关键字 extension 关键字

扩展方法适合为现有类添加静态工具方法,Mixin 适合为类添加有状态的复杂功能。"

12. 如何处理 Mixin 之间的依赖关系?

精准回答:
"处理 Mixin 依赖关系的几种策略:

  1. 使用 on 限制:确保 Mixin 只在合适的上下文中使用
  2. 接口抽象:通过抽象方法定义依赖契约
  3. 组合模式:让一个 Mixin 依赖另一个 Mixin
  4. 依赖查找:通过服务定位器获取依赖

最佳实践:  保持 Mixin 尽可能独立,依赖通过抽象定义。"

高级面试问题回答技巧

技术深度展示:

当被问到复杂问题时,展示对底层机制的理解:

示例回答:
"Mixin 的线性化机制实际上是编译时进行的,Dart 编译器会生成一个线性的类层次结构。从实现角度看,Mixin 会被编译为普通的类,然后通过代理模式将方法调用转发到正确的实现。"

结合实际项目:

"在我之前的电商项目中,我们使用 Mixin 实现了购物车的各种行为:

  • WithCacheMixin:缓存商品信息
  • WithValidationMixin:验证库存和价格
  • WithAnalyticsMixin:记录用户行为
    这样每个业务模块都可以按需组合功能。"

展示设计思考:

"在设计 Mixin 时,我遵循 SOLID 原则:

  • 单一职责:每个 Mixin 只做一件事
  • 开闭原则:通过 Mixin 扩展而非修改
  • 接口隔离:定义清晰的抽象方法
  • 依赖倒置:依赖抽象而非具体实现"

常见陷阱与解决方案

陷阱 1:状态共享问题

问题:  "多个类混入同一个 Mixin 会共享状态吗?"

回答:  "不会。每个实例都有自己的 Mixin 状态副本。Mixin 中的字段在编译时会复制到宿主类中,每个实例独立。"

陷阱 2:初始化顺序

问题:  "如果多个 Mixin 都需要初始化怎么办?"

回答:  "使用初始化方法模式:

dart

mixin Initializable {
  void initialize() {
    // 初始化逻辑
  }
}

class MyClass with A, B {
  void init() {
    // 按需调用初始化
    (this as A).initialize();
    (this as B).initialize();
  }
}

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

引言

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

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

  • 你写代码 → 自动测试 → 自动打包 → 自动发布
  • 就像工厂的流水线,代码进去,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 &#34;GitHub Actions架构&#34;
        A[你的代码仓库] --> B[触发事件]
        B --> C[GitHub Actions Runner]
        
        subgraph &#34;Runner执行环境&#34;
            C --> D[创建虚拟机]
            D --> E[执行工作流]
            
            subgraph &#34;工作流步骤&#34;
                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的核心知识,一个完美的流水线是一次次迭代出来的,需要不断优化。如果觉得文章对你有帮助,别忘了一键三连,支持一下


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

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

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 多窗口架构

引言

在 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中的知识总结(一)

一、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 状态管理实践

下面内容只关注 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的写时复制

众所周知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():真正含义与渲染原理

在学习 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 循环引用篇 菜鸟都能看懂

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 表的实现和性能

祝你面试顺利! 🚀

SwiftUI 中的 @ViewBuilder 全面解析

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 款马甲包 + 国内社交难度堪比史诗级!

背景

“她说明年就结婚,转头就把我拉黑了!”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

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,那就不用这样魔改了😩。

❌