普通视图

发现新文章,点击刷新页面。
昨天 — 2025年9月26日掘金 iOS

深入探索Swift的Subscript机制和最佳实践

作者 willlzq
2025年9月26日 16:39

一、为什么要用Subscript[下标]?

优点:

Swift的下标(Subscript)是一种强大的语言特性,它允许开发者通过类似于数组索引的语法来访问类型的元素或属性。下标提供了一种简洁、直观的方式来操作集合、列表、序列以及自定义数据结构,大大提高了代码的可读性和易用性。

缺点:

  • 1:subscript重载的参数个数(>=1),参数类型,返回类型,理论上可以随意,也可以随便。
  • 2:调用方不知道api提供者内部subscript重载方式,也不好做函数注释,xcode也.不出来。调用者纯粹靠吓瞎猜,查资料
  • 3:一些系统内置的语法糖,subscript的实现简直是一个黑箱,可怕的还有多个重载版本,更可怕的是重载决策放在c++实现的,作者找了好久也不知道它内置的选择逻辑,并拿出c++的证据。所以作者只能从swift提案找线索。本文的一个目的就是弄明白它的重载版本选择策略。

这些缺点和优点在@dynamicMemberLookup与@propertyWrapper上体现非常明显

下标特性的提案历史与贡献人

Swift的下标机制经历了多次演进和增强,以下是几个关键的提案:

  1. 静态下标(Static and class subscripts) - SE-0254

    • 提案人:Becca Royal-Gordon
    • 实现版本:Swift 5.1
    • 核心思想:允许在类型本身而不是实例上定义下标,使语言更加一致和灵活
    • 提案链接
  2. 泛型下标(Generic Subscripts) - SE-0148

    • 允许下标方法使用泛型参数,增强了下标机制的灵活性和可重用性
    • 提案链接
  3. 动态成员查找下标扩展(Allow Additional Arguments to @dynamicMemberLookup Subscripts) - SE-0484

    • 提案人:Itai Ferber
    • 实现版本:较新版本的Swift
    • 核心思想:允许动态成员查找下标接受额外的默认参数
    • 提案链接
  4. C函数作为下标导入(Import C functions as subscript methods)

    • 允许将C函数通过swift_name属性导入为Swift下标
    • 讨论链接

这些提案的贡献者们通过不断完善下标机制,使Swift语言在保持简洁性的同时,提供了更强大的表达能力。

二、Subscripts的语法特征与机制

基本语法

Swift下标使用subscript关键字定义,可以包含getter和setter方法:

subscript(index: Int) -> Element {
    get {
        // 返回与index对应的元素
    }
    set(newValue) {
        // 设置与index对应的元素为newValue
    }
}

下标重载机制

Swift允许为一个类型定义多个下标方法,通过参数类型和标签的不同来区分,这就是下标重载:

struct Matrix {
    let rows: Int, columns: Int
    var grid: [Double]
    
    init(rows: Int, columns: Int) {
        self.rows = rows
        self.columns = columns
        self.grid = Array(repeating: 0.0, count: rows * columns)
    }
    
    // 基本下标:通过行和列访问
    subscript(row: Int, column: Int) -> Double {
        get {
            assert(indexIsValid(row: row, column: column), "Index out of range")
            return grid[(row * columns) + column]
        }
        set {
            assert(indexIsValid(row: row, column: column), "Index out of range")
            grid[(row * columns) + column] = newValue
        }
    }
    
    // 重载下标:通过单个索引访问
    subscript(index: Int) -> Double {
        get {
            assert(index >= 0 && index < grid.count, "Index out of range")
            return grid[index]
        }
        set {
            assert(index >= 0 && index < grid.count, "Index out of range")
            grid[index] = newValue
        }
    }
    
    private func indexIsValid(row: Int, column: Int) -> Bool {
        return row >= 0 && row < rows && column >= 0 && column < columns
    }
}

KeyPath版本下标

Swift支持使用KeyPath作为下标参数,这提供了一种类型安全的方式来动态访问属性:

struct Container<T> {
    private var value: T
    
    init(_ value: T) {
        self.value = value
    }
    
    // KeyPath版本下标
    subscript<U>(keyPath: KeyPath<T, U>) -> U {
        return value[keyPath: keyPath]
    }
    
    // WritableKeyPath版本下标,支持修改
    subscript<U>(keyPath: WritableKeyPath<T, U>) -> U {
        get {
            return value[keyPath: keyPath]
        }
        set {
            value[keyPath: keyPath] = newValue
        }
    }
}

// 使用示例
struct Person {
    var name: String
    var age: Int
}

var person = Person(name: "Alice", age: 30)
var container = Container(person)

print(container[\.name])  // 输出: Alice
container[\.age] = 31     // 修改age属性
print(container[\.age])   // 输出: 31

下标优先级

当多个下标方法都可以匹配一个下标表达式时,Swift编译器会根据以下规则确定优先级:

  1. 参数类型完全匹配的下标优先于需要类型转换的下标
  2. 非泛型下标优先于泛型下标
  3. 具体类型参数的下标优先于协议类型参数的下标

三、静态下标类型与swift_name attribute风格下标

静态下标类型

静态下标是Swift 5.1中引入的特性(SE-0254),允许开发者在类型级别定义下标,而不是在实例级别。

静态下标的基本语法

struct Math {
    // 静态下标:计算平方数
    static subscript(n: Int) -> Int {
        return n * n
    }
    
    // 静态下标:计算阶乘
    static subscript(factorial n: Int) -> Int {
        guard n >= 0 else { return 0 }
        return n <= 1 ? 1 : n * Math[factorial: n - 1]
    }
}

// 使用静态下标
let squareOfFive = Math[5]           // 结果: 25
let factorialOfFive = Math[factorial: 5]  // 结果: 120

静态下标的C++内部实现

在Swift编译器源码中,静态下标的处理机制与实例下标有明显区别。以下是关键代码片段:

// 检查下标是否为静态成员
bool SubscriptDecl::isStatic() const {
  return getStorageKind() == StorageKind::Static;
}

// 处理静态下标的查找逻辑
void TypeChecker::lookupDirect(/* 参数 */) {
  // ...
  if (auto subscript = dyn_cast<SubscriptDecl>(member)) {
    if (subscript->isStatic()) {
      // 静态下标查找逻辑
      // ...
    } else {
      // 实例下标查找逻辑
      // ...
    }
  }
  // ...
}

静态下标的应用场景

  1. 类型工具方法:提供与类型相关的便捷计算或查找功能
  2. 类型级缓存:实现全局缓存机制
  3. 命名空间隔离:在enum中实现命名空间隔离的功能

swift_name attribute风格下标

swift_name attribute允许开发者自定义C/Objective-C函数在Swift中的导入名称,包括将C函数导入为Swift下标。

基本用法

// Objective-C代码
@interface NSString (SubscriptAdditions)
// 使用swift_name自定义下标名称
- (unichar)characterAtIndex:(NSUInteger)index 
    __attribute__((swift_name("subscript(_:)")));
@end

在Swift中,这将被导入为:

// 在Swift中使用
let str = "Hello"
let char = str[1]  // 相当于调用characterAtIndex:1

自定义下标名称格式

swift_name attribute支持多种格式来自定义下标名称:

  1. 基本格式subscript(<参数标签>:<参数类型>, ...)
  2. 带参数标签的格式subscript(label:arg:)
  3. getter/setter格式getter:TypeName.subscript(...)setter:TypeName.subscript(...:newValue:)

C函数转换为Swift下标示例

// C代码
void* getElement(void* array, size_t index) {
    // 实现细节
}

void setElement(void* array, size_t index, void* element) {
    // 实现细节
}

// 使用swift_name转换为下标
void* getElement(void* array, size_t index) 
    __attribute__((swift_name("getter:Array.subscript(_:)")));

void setElement(void* array, size_t index, void* element) 
    __attribute__((swift_name("setter:Array.subscript(_:)")));

在Swift中,这些函数将作为下标使用:

// 在Swift中使用
let element = array[5]  // 调用getElement
array[5] = newValue     // 调用setElement

四、@dynamicMemberLookup与@propertyWrapper的下标实现机制

@dynamicMemberLookup的下标实现

@dynamicMemberLookup是Swift中一个强大的特性,允许类型通过下标方法动态响应属性访问。这一特性在Swift 5.1版本中通过SE-0252提案进一步增强,支持了基于KeyPath的动态成员查找功能。

动态成员查找下标形式

根据Keypath Dynamic Member Lookup提案@dynamicMemberLookup支持以下四种下标形式:

  1. 字符串参数形式:最基础的动态成员查找
subscript(dynamicMember member: String) -> ValueType {
    // 通过字符串名称实现动态属性访问
}
  1. KeyPath参数形式:提供只读访问的类型安全版本
subscript<U>(dynamicMember member: KeyPath<T, U>) -> U {
    // 通过KeyPath实现类型安全的只读属性访问
}
  1. WritableKeyPath参数形式:支持读写操作的类型安全版本
subscript<U>(dynamicMember member: WritableKeyPath<T, U>) -> U {
    get { /* 读取实现 */ }
    set { /* 写入实现 */ }
}
  1. ReferenceWritableKeyPath参数形式:针对类实例的引用类型安全版本
subscript<U>(dynamicMember member: ReferenceWritableKeyPath<T, U>) -> U {
    get { /* 读取实现 */ }
    set { /* 写入实现 */ }
}

以下是一个结合了KeyPath和WritableKeyPath的实际示例,展示了如何创建一个类似Lens的结构来实现属性访问和修改:

struct Point {
  let x: Int
  var y: Int
}

@dynamicMemberLookup 
struct Lens<T> {
  var obj: T

  init(_ obj: T) {
    self.obj = obj
  }

  subscript<U>(dynamicMember member: KeyPath<T, U>) -> Lens<U> {
    get { return Lens<U>(obj[keyPath: member]) }
  }

  subscript<U>(dynamicMember member: WritableKeyPath<T, U>) -> Lens<U> {
    get { return Lens<U>(obj[keyPath: member]) }
    set { obj[keyPath: member] = newValue.obj }
  }
}

// 使用示例
var lens = Lens(Point(x: 0, y: 0))
_ = lens.x // 调用 KeyPath 版本的下标
lens.y = Lens(10) // 调用 WritableKeyPath 版本的下标

现在让我们深入了解Swift编译器内部是如何实现这些特性的:

1. 验证逻辑

// TypeCheckAttr.cpp中的关键验证逻辑
void AttributeChecker::visitDynamicMemberLookupAttr(DynamicMemberLookupAttr *attr) {
  // 仅允许用于标称类型
  auto decl = cast<NominalTypeDecl>(D);
  auto type = decl->getDeclaredType();
  auto &ctx = decl->getASTContext();
  
  // 查找符合条件的subscript(dynamicMember:)方法
  auto subscriptName = DeclName(ctx, DeclBaseName::createSubscript(), ctx.Id_dynamicMember);
  auto candidates = TypeChecker::lookupMember(decl, type, subscriptName);
  
  // 验证候选方法是否有效
  // ...
}

2. 字符串动态成员查找验证

// 验证字符串动态成员查找的有效性
bool swift::isValidStringDynamicMemberLookup(SubscriptDecl *decl, DeclContext *DC, bool ignoreLabel) {
  auto &ctx = decl->getASTContext();
  // 要求:
  // 1. 下标方法只有一个非可变参数
  // 2. 参数类型符合ExpressibleByStringLiteral协议
  if (!hasSingleNonVariadicParam(decl, ctx.Id_dynamicMember, ignoreLabel))
    return false;

  const auto *param = decl->getIndices()->get(0);
  auto paramType = param->getType();

  auto stringLitProto = ctx.getProtocol(KnownProtocolKind::ExpressibleByStringLiteral);

  // 检查参数类型是否符合ExpressibleByStringLiteral协议
  return (bool)TypeChecker::conformsToProtocol(paramType, stringLitProto, DC, ConformanceCheckOptions());
}

3. 键路径动态成员查找验证

// 验证键路径动态成员查找的有效性
bool swift::isValidKeyPathDynamicMemberLookup(SubscriptDecl *decl, bool ignoreLabel) {
  auto &ctx = decl->getASTContext();
  if (!hasSingleNonVariadicParam(decl, ctx.Id_dynamicMember, ignoreLabel))
    return false;

  const auto *param = decl->getIndices()->get(0);
  if (auto NTD = param->getInterfaceType()->getAnyNominal()) {
    // 参数类型必须是KeyPath、WritableKeyPath或ReferenceWritableKeyPath
    return NTD == ctx.getKeyPathDecl() ||
           NTD == ctx.getWritableKeyPathDecl() ||
           NTD == ctx.getReferenceWritableKeyPathDecl();
  }
  return false;
}

@propertyWrapper的下标实现

@propertyWrapper是Swift 5.1中引入的一个重要特性(SE-0258提案),它允许开发者定义自定义的属性访问行为。除了基本的wrappedValue实现外,还支持通过特殊的静态下标提供更灵活的访问方式。

属性包装器内置静态下标

根据Property Wrappers提案,属性包装器可以定义一个特殊的静态下标来控制属性的访问行为。这个静态下标具有以下形式:

static subscript<EnclosingSelf>(
  _enclosingInstance observed: EnclosingSelf,
  wrapped wrappedKeyPath: ReferenceWritableKeyPath<EnclosingSelf, Value>,
  storage storageKeyPath: ReferenceWritableKeyPath<EnclosingSelf, Self>
) -> Value {
  get {
    // 自定义获取逻辑
  }
  set {
    // 自定义设置逻辑
  }
}

让我们详细解释这个静态下标的参数:

  1. _enclosingInstance observed:这是包装属性所属的实例,泛型参数EnclosingSelf表示包装类型所在的封闭类型。

  2. wrapped wrappedKeyPath:这是一个键路径,指向被包装的属性本身,用于在需要时访问或修改原始属性。

  3. storage storageKeyPath:这是一个键路径,指向存储包装器实例的属性,用于访问或修改包装器的状态。

这种静态下标的调用方式是隐式的,当访问被@propertyWrapper标记的属性时,编译器会自动转换为对这个静态下标的调用。

以下是一个实际示例,展示如何使用这种静态下标来实现自定义访问行为:

@propertyWrapper
struct Delayed<Value> {
    private var _value: Value?
    private let defaultValue: Value
    
    init(wrappedValue: Value) {
        self.defaultValue = wrappedValue
        self._value = nil
    }
    
    var wrappedValue: Value {
        get { _value ?? defaultValue }
        set { _value = newValue }
    }
    
    // 静态下标实现
    static subscript<EnclosingSelf>(
        _enclosingInstance observed: EnclosingSelf,
        wrapped wrappedKeyPath: ReferenceWritableKeyPath<EnclosingSelf, Value>,
        storage storageKeyPath: ReferenceWritableKeyPath<EnclosingSelf, Self>
    ) -> Value {
        get {
            let wrapper = observed[keyPath: storageKeyPath]
            return wrapper._value ?? wrapper.defaultValue
        }
        set {
            //wrappedKeyPath和storageKeyPath路径不一样
            //wrappedKeyPath是User的2个包装对象name,age,如:ReferenceWritableKeyPath<User, Int>
            //storageKeyPath是Delayed的是_value 如: WritableKeyPath<User,  Delayed<Int>>
            var wrapped_obj = observed[keyPath: wrappedKeyPath]
            print("wrapped_obj-->\r\n\(wrapped_obj)")
            withUnsafePointer(to: &wrapped_obj) { pointer in
                   print("wrapped_obj的内存地址: \(pointer)")
            }
            var storage_obj = observed[keyPath: storageKeyPath]
            print("storage_obj-->\r\n\(storage_obj)")
            withUnsafePointer(to: &storage_obj) { pointer in
                   print("storage_obj的内存地址: \(pointer)")
              }
            observed[keyPath: storageKeyPath]._value = newValue
        }
    }
}


// 使用示例
class User {
    @Delayed var name: String = "Anonymous"
    @Delayed var age: Int = 0
}

let user = User() 
print(user.name) // 会调用静态下标getter,输出: Accessing property 和 Anonymous
user.age = 30    // 会调用静态下标setter,输出: Modifying property to: 30

现在,让我们深入了解Swift编译器内部是如何实现这些特性的:

1. 属性包装器验证逻辑

// PropertyWrapperTypeInfoRequest的求值逻辑
llvm::Expected<PropertyWrapperTypeInfo>
PropertyWrapperTypeInfoRequest::evaluate(Evaluator &eval, NominalTypeDecl *nominal) const {
  // 必须有@propertyWrapper属性
  if (!nominal->getAttrs().hasAttribute<PropertyWrapperAttr>()) {
    return PropertyWrapperTypeInfo();
  }

  // 查找名为"wrappedValue"的非静态属性
  ASTContext &ctx = nominal->getASTContext();
  auto valueVar = findValueProperty(ctx, nominal, ctx.Id_wrappedValue, /*allowMissing=*/false);
  if (!valueVar)
    return PropertyWrapperTypeInfo();
  
  // ...其他验证和初始化逻辑
}

2. 静态下标查找逻辑

// 查找用于访问封闭实例的静态下标
static SubscriptDecl *findEnclosingSelfSubscript(ASTContext &ctx,
                                               NominalTypeDecl *nominal,
                                               Identifier propertyName) {
  Identifier argNames[] = {
    ctx.Id_enclosingInstance,
    propertyName,
    ctx.Id_storage
  };
  DeclName subscriptName(ctx, DeclBaseName::createSubscript(), argNames);

  SmallVector<SubscriptDecl *, 2> subscripts;
  for (auto member : nominal->lookupDirect(subscriptName)) {
    auto subscript = dyn_cast<SubscriptDecl>(member);
    if (!subscript)
      continue;

    if (subscript->isInstanceMember())  // 必须是静态成员
      continue;

    if (subscript->getDeclContext() != nominal)
      continue;

    subscripts.push_back(subscript);
  }
  
  // ...验证和返回逻辑
}

3. PropertyWrapperTypeInfo结构体

struct PropertyWrapperTypeInfo {
  // ...其他成员
  
  /// 用于访问类实例属性的静态下标(替代wrappedValue)
  SubscriptDecl *enclosingInstanceWrappedSubscript = nullptr;

  /// 用于访问类实例属性的静态下标(替代projectedValue)
  SubscriptDecl *enclosingInstanceProjectedSubscript = nullptr;
  
  // ...
};

五、自定义实现下标的最佳实践

1. 尽可能严格约束类型

为下标参数和返回值添加明确的类型约束,提高代码的类型安全性和可读性:

// 不好的实现
subscript(_ index: Any) -> Any? {
    // ...
}

// 更好的实现
subscript<T: Collection>(_ indices: T) -> [Element] where T.Element == Int {
    // 确保indices中的元素都是Int类型
    return indices.compactMap { self[$0] }
}

2. 尽可能使用KeyPath

当实现动态成员访问时,优先考虑使用KeyPath而非字符串,以获得更好的类型安全性和编译时检查:

// 基于字符串的实现(类型不安全)
@dynamicMemberLookup
struct StringBasedContainer {
    subscript(dynamicMember name: String) -> Any? {
        // 通过字符串查找值
        // ...
    }
}

// 基于KeyPath的实现(类型安全)
@dynamicMemberLookup
struct KeyPathBasedContainer<T> {
    private let value: T
    
    init(_ value: T) {
        self.value = value
    }
    
    subscript<U>(dynamicMember keyPath: KeyPath<T, U>) -> U {
        return value[keyPath: keyPath]
    }
}

3. 为下标实现设置有意义的别名

使用参数标签为下标提供更具描述性的名称,使代码意图更加清晰:

extension Array {
    // 没有参数标签的下标
    subscript(_ indices: [Int]) -> [Element] {
        return indices.compactMap { self[$0] }
    }
    
    // 有描述性参数标签的下标(更好)
    subscript(elementsAt indices: [Int]) -> [Element] {
        return indices.compactMap { self[$0] }
    }
    
    // 带默认参数的下标
    subscript(safe index: Int, defaultValue: Element? = nil) -> Element? {
        return indices.contains(index) ? self[index] : defaultValue
    }
}

4. 考虑性能优化

对于频繁调用的下标,考虑添加适当的缓存机制或优化访问路径:

struct ExpensiveComputation {
    private var cache: [Int: ResultType] = [:]
    
    subscript(index: Int) -> ResultType {
        // 检查缓存
        if let cachedResult = cache[index] {
            return cachedResult
        }
        
        // 执行昂贵的计算
        let result = performExpensiveComputation(for: index)
        
        // 缓存结果
        cache[index] = result
        
        return result
    }
    
    private func performExpensiveComputation(for index: Int) -> ResultType {
        // 复杂计算逻辑
        // ...
    }
}

5. 处理边界情况

确保下标实现能够优雅地处理边界情况和非法输入:

extension Collection {
    subscript(safe index: Index) -> Element? {
        return indices.contains(index) ? self[index] : nil
    }
}

// 使用示例
let array = [1, 2, 3]
let validElement = array[safe: 1]  // 返回2
let invalidElement = array[safe: 10]  // 返回nil,避免越界崩溃

通过遵循这些最佳实践,开发者可以创建更加安全、高效、易用的下标实现,充分发挥Swift下标机制的强大功能。

苹果海外老账号续费,踩了个大坑!

作者 CocoaKier
2025年9月26日 13:40

我们有个香港苹果公司老账号要续费了,我像以前一样,用招行的VISA卡去网页端续费,可能无论怎么输,就是各种报错。

地区选“中国大陆”,卡号会飘红字“您输入的信用卡在中国大陆无效,请提供在中国大陆有效的信用卡”。

地区选“香港”,会弹窗报错“无法更新付款信息,请联系Apple支持了解更多信息”或“NoAuthorization”。

图片.png

图片.png

联系苹果,苹果说我们公司是香港的,必须用“纯血”香港的卡续费。可是我去年刚用招行的VISA卡续费成功过啊,难道是苹果后台的改版了?

最后,折腾了很久,苹果电话都打了2轮。最后,我自己找到了原因。

罪魁祸首竟然是苹果开发者后台的“自动续订”开关! 我不知道什么时候,把这个开关勾上了。

图片.png

当“自动续订”开关打开时,苹果开发者网站顶部会显示“您当前账户未绑定绑定信用卡或借记卡,无法完成自动续订”的横幅提醒,点“续费”会跳转到苹果AppleID网站account.apple.com/)。 这个网站你以为你是在“续费”,实际上你是在往账号持有人这个个人AppleID上绑定支付方式,就会出现文章开头的各种报错。

当“自动续订”开关关闭时,不会有横幅提醒,点击“续费”会跳转,苹果开发者网站developer.apple.com) 续费页面。 这个网站就可以像往常一样,输入中国的VISA卡进行续费了。

FAQ
1、老账号,每年都需要手动续费,能不能迁移到自动续费
——可以。自动续订又分为通过网站走信用卡/借记卡方式(只有部分国家和地区支持),和通过Apple Developer App方式。这两种方式都要求,开发者的国家和地区(注意不是账号持有人的国家和地区)与续费方式国家和地区保持一致。比如我这个案例,香港的开发者账号。如果走网页自动续费,可选的有:纯血香港信用卡/借记卡、HKAlipay(需要香港手机号和绑香港卡)。如果走Apple Developer App续订,可以用另外的香港地区的AppleID绑定香港的支付方式后,在Apple Developer App里点“续订”进行苹果订阅。注意:换成订阅模式是不可逆的,换成订阅模式后,是没法换回网页手动续费的

但对于我来说,无论哪种自动续费方式,都要有香港银行卡。所以,还是老老实实每年用中国VISA卡在网页上手动续费吧!

2、我的账号持有人是中国的,公司是香港的,支付方式选“中国大陆”后,付款方式里就有“支付宝”、“微信”了,并且可以成功绑上去,到时候会自动续费吗?
——我不确定。修改Apple的付款方式时,AppleID的地区会影响可用的支付方式。我这次案例就是这种情况,我的账号持有人是中国大陆,公司是香港,在AppleID网上地区选“中国大陆”后,能绑上中国的支付宝。但第二天还是收到了苹果的账号快到期邮件提醒,“我们注意到没有与你的苹果ID相关的账单信息,所以你的苹果开发者计划会员资格不能自动更新。要更新您的会员资格,请在计划更新会员资格之前的任何时间使用有效的信用卡/借记卡更新您的Apple ID”。所以,过期日这个香港账号会不会自动扣中国支付宝的钱,不得而知。但从邮件以及咨询苹果(苹果说支付方式要和公司地区保持一致)的情况来看,大概率不会成功。因为我们有线上包在跑,不敢冒这个险。如果你有尝试,欢迎留言告知。

【官方文档】会员资格计划续订

雪山飞狐之 Swift 6.2 并发秘典:@concurrent 的江湖往事

2025年9月26日 10:13

在这里插入图片描述

引子

雪山顶上,寒风如刀。

胡斐手持一部《Swift 6.2 心法》,眉头拧成了疙瘩 —— 前日他为解码雪山派传承的「数据秘籍」,用了nonisolated函数,却忽在主线程卡顿,忽又在后台线程乱走,险些让田归农趁机盗走秘籍。

在这里插入图片描述

正当他愁眉不展时,一道苍劲的声音从身后传来:「胡兄弟,此乃并发之惑也,若想破局,需先懂nonisolated的根基,再悟 @concurrent 的妙谛。」

在本篇江湖夜话中,各位少侠将学到如下内容:

  • 引子
  • 📜 第一回:nonisolated 函数的「门派归属」之谜
  • 🧩 第二回:nonisolated (nonsending):给独行客立「规矩」
  • ⚡ 第三回:@concurrent 登场:定向「派活」的武林令牌
  • 🎯 第四回:何时用 @concurrent?江湖人的「分寸感」
  • 🔚 尾声:令牌入鞘,心法永存

来者正是「打遍天下无敌手」的苗人凤,身旁还站着医术与代码双绝的程灵素。三人围坐火塘,一场关于 Swift 6.2 并发江湖的讲解,就此展开。


📜 第一回:nonisolated 函数的「门派归属」之谜

苗人凤指尖划过《Swift 心法》,开门见山:「胡兄弟,你可知nonisolated函数为何『性情不定』?此函数本就不属任何『actor 门派』,在 Swift 6.1 或 6.2 默认设置下,它就像江湖中游荡的独行客,去哪干活全凭规矩 —— 若是带了async关键字,便会直奔『全局执行器』(背景线程);若是没带,就跟着调用它的『门派弟子』(调用者 actor)走。」

在这里插入图片描述

程灵素一旁补充,随手在火塘边的石片上写了段代码:

// 带async的nonisolated函数:如同独行客去后山练功,绝不占主线程(主actor)
nonisolated 
func decode<T: Decodable>(_ data: Data) async throws -> T {
  // 此处解码操作,全程在背景线程执行,主线程可安心处理用户交互(比如接招防御)
}

// 不带async的nonisolated函数:跟着调用者走,调用者在主actor,它就去主actor
nonisolated 
func decode<T: Decodable>(_ data: Data) throws -> T {
  // 若从主actor调用(比如主线程处理UI时调用),此函数就会在主actor运行,可能造成卡顿
}

胡斐一拍大腿:「原来如此!前日我解码时,先调用了带 async 的版本,后台跑得顺畅;后来删了 async,却在主 actor 卡住,让田归农钻了空子 —— 这差别也太让人一头雾水了!」

🧩 第二回:nonisolated (nonsending):给独行客立「规矩」

苗人凤闻言点头:「此乃 Swift 江湖的旧疾,故 6.2 版本新增了nonisolated(nonsending) 这一『规矩』,专为统一函数的『行事风格』而来。」

他指着石片上的新代码,解释道:「凡是标了nonisolated(nonsending) 的函数,不管带不带async,都像入了『固定门派』,永远跟着调用者的『执行器』走 —— 调用者在主 actor,它就去主 actor;调用者在后台 actor,它就去后台。这样一来,函数的行为便一目了然,再无之前的混乱。」

在这里插入图片描述

程灵素接过话头,语气中带着一丝严谨:「更重要的是,它能减少『并发风险』。你想,若函数总往『全局执行器』跑,就像不断开辟新的『练功场』(隔离域),多个线程同时碰同一个『秘籍数据』(状态),很容易造成数据错乱 —— 而nonisolated(nonsending) 让函数留在原『练功场』,数据不用跨域传递,自然不用强制遵守Sendable协议,省了不少麻烦。」

她又写下一段代码,标注得清清楚楚:

// nonisolated(nonsending)函数:规矩森严,永远跟着调用者走
nonisolated(nonsending) 
func decode<T: Decodable>(_ data: Data) async throws -> T {
  // 无论是否有async,调用者在哪个actor,此函数就跑在哪个actor
  // 避免开辟新隔离域,减少数据并发访问的风险,代码逻辑也更易梳理
}

胡斐若有所思:「如此说来,若把所有 nonisolated 函数都设为默认nonsending,岂不是更省心?」

「正是!」苗人凤抚须而笑,「Swift 6.2 有个『NonIsolatedNonSendingByDefault』开关,打开后所有显式或隐式的 nonisolated 函数,都会默认遵循nonsending规矩。只是这般一来,若真要让函数去『全局执行器』干活,又该如何?」

在这里插入图片描述

话音刚落,程灵素眼中闪过一丝亮光:「这便要请出今日的主角 ——@concurrent了。」

⚡ 第三回:@concurrent 登场:定向「派活」的武林令牌

苗人凤站起身,拿起一块刻着「并发」二字的木牌,郑重道:「胡兄弟,@concurrent就像这令牌 —— 持有它的函数,会被明确派往『全局执行器』(背景线程),且自动获得nonisolated身份,无需你再额外标注。」

他随即在石片上写下关键代码,每一笔都力透石背:

// @concurrent函数:自带「去全局执行器」令牌,无需额外写nonisolated
@concurrent
func decode<T: Decodable>(_ data: Data) async throws -> T {
  // 此函数必定在背景线程执行,专治「主线程卡顿」的毛病
  let decoder = JSONDecoder()
  return try decoder.decode(T.self, from: data)
}

「更妙的是,它还能在『门派内部』使用。」程灵素补充道,比如主 actor 或自定义 actor 中的函数,只要没明确标注隔离属性,都能挂这令牌:

// 主actor中的@concurrent函数:虽在主actor类里,却去背景线程干活
@MainActor
class DataViewModel {
  @concurrent
  func decode<T: Decodable>(_ data: Data) async throws -> T {
    // 类本身属主actor,但此函数靠@concurrent去了背景线程,不阻塞UI
  }
}

// 自定义actor中的@concurrent函数:同理,脱离actor去全局执行器
actor DataViewModel {
  @concurrent
  func decode<T: Decodable>(_ data: Data) async throws -> T {
    // 不占用actor的资源,让actor专注处理其他任务
  }
}

胡斐忽然问道:「那若是函数已经明确了隔离属性,比如标了 **@MainActor ** 或 nonisolated (nonsending),还能挂这令牌吗?」

在这里插入图片描述

苗人凤摇头,语气严肃:「万万不可!这就像一个人已经入了『武当派』,又怎能同时拿『少林令牌』?两者隔离规则冲突,Swift 江湖绝不允许这般混乱。」他指着石片上的错误示例说道:

// 错误1:@concurrent与@MainActor冲突,一个要去全局,一个要留主actor

@concurrent @MainActor

func decode\<T: Decodable>(\_ data: Data) async throws -> T { /\* ... \*/ }

// 错误2:@concurrent与nonisolated(nonsending)冲突,一个去全局,一个跟调用者

@concurrent nonisolated(nonsending)

func decode\<T: Decodable>(\_ data: Data) async throws -> T { /\* ... \*/ }

🎯 第四回:何时用 @concurrent?江湖人的「分寸感」

火塘的柴火噼啪作响,胡斐看着手中的令牌,又问:「既然 @concurrent 能去背景线程,那是不是所有函数都该挂这令牌?」

程灵素闻言轻笑:「胡兄弟,你这是把并发当『蛮力』了。江湖中,武功并非越多越好,而是要恰到好处——@concurrent 也是如此,用对了是救场,用错了反成累赘。」

在这里插入图片描述

她举了个例子:比如网络请求函数,本身带await,调用时会「暂停」,让调用者去做其他事,根本不用 @concurrent:

class Networking {
  // 网络请求函数:自带await,调用时会暂停,主actor可趁机处理用户输入
  func loadData(from url: URL) async throws -> Data {
    let (data, response) = try await URLSession.shared.data(from: url)
    // 等待网络响应时,主actor不会被阻塞,无需@concurrent
    return data
  }
}

苗人凤接过话头,继续道:「但若是遇到『耗时操作』,比如解码大量数据,@concurrent 就该出手了。你看这段代码 ——」

class Networking {
  // 普通函数:跟着调用者跑,若在主actor会卡顿
  func getFeed() async throws -> Feed {
    let data = try await loadData(from: Feed.endpoint)
    // 若decode在主actor,解码大量数据时会阻塞UI
    let feed: Feed = try await decode(data)
    return feed
  }

  // 加上@concurrent:去背景线程解码,不卡主actor
  @concurrent
  func decode<T: Decodable>(_ data: Data) async throws -> T {
    let decoder = JSONDecoder()
    // 耗时的解码操作在背景线程执行,主actor安心处理UI
    return try decoder.decode(T.self, from: data)
  }
}

他顿了顿,目光深邃:「胡兄弟,记住 —— 并发的真谛不是『越多越好』,而是『按需分配』。就像你练『胡家刀法』,不是每一招都用尽全力,而是该快则快,该稳则稳。@concurrent 只该用在『真需要并发』的地方,比如耗时的计算、解码。而其余时候,守着nonisolated(nonsending) 的规矩,反而让代码更稳、更少 bug。」

🔚 尾声:令牌入鞘,心法永存

雪风渐歇,火塘的余温暖了三人的心。

胡斐收起《Swift 心法》,手中的 @concurrent「令牌」仿佛有了重量 —— 他终于明白,Swift 并发不是炫技的工具,而是像江湖规矩一样,守得住分寸,才能写出「稳如泰山」的代码。

苗人凤看着他的神情,微微一笑:「江湖路远,代码亦如武功。今日这 @concurrent 的道理,你若记牢了,日后面对再复杂的并发难题,也能游刃有余。」

程灵素补充道:「更重要的是,这心法还能『未雨绸缪』—— 如今用 @concurrent 标记函数,不仅明确了你的意图,更是为未来的 Swift 版本铺路,算得上是『一举两得』。」

在这里插入图片描述

胡斐拱手致谢,转身望向雪山深处。此刻他心中已无困惑,只待明日用新学的并发心法,重解雪山秘籍,让田归农之流再无可乘之机。而那 @concurrent 的令牌,也已悄悄入鞘,等待着在真正需要它的时刻,再展锋芒。

—— 毕竟,真正的江湖高手,从不会为了用武功而用武功;真正的程序员,也从不会为了用并发而用并发。

那么,各位秃头少侠你们也了然了吗?

感谢观赏,我们下次再会吧!8-)

昨天以前掘金 iOS

Xcode26-iOS26适配

2025年9月25日 17:34

前两天苹果发布了Xcode26、iOS26正式版本;因为没有强制要求适配,原计划忙完手上的事情再去适配。但是最近发包审核反馈在iOS26上闪退了。我人麻了,想躺平,奈何苹果推着我进步啊。赶忙下载Xcode26,升级iOS26进行排查,也没有复现审核反馈的闪退情况。不过发现确实有需要适配的地方。下面就慢慢来适配吧。

Xcode(我模拟器呢)

image.png

因为项目比较老,有一些库在模拟器上只支持x86_64架构使用,所以我用模拟只用Rosetta的,但是Xcode26默认下载iOS26模拟器只支持arm64架构。(哥哥们有没有什么办法可以,一并调整兼容这些老库啊)

目前的解决办法是不通过Xcode去下载iOS26.0,因为默认下载的是“Apple Silicon”版本的,通过命名行去下载“Universal”版本。估计明年就不行了,苹果说了:macOS Tahoe(版本号macOS 26)将是英特尔芯片Mac的最后一次重大系统更新,是不是明年就没有“Universal”版本,全是苹果心

  1. 先删除Xcode 默认下载的iOS26.0 “Apple Silicon”

image.png

  1. 通过命令行下载iOS26.0 “Universal”
xcodebuild -downloadPlatform iOS -architectureVariant universal

image.png

  1. 然后退出关闭Xcode,重新打开,就有了

image.png

image.png

UI(真的好看么?又短又细)

我Tabbar变短了,还加了液态玻璃的交互效果,Switch变细了,也加了液态玻璃的交互效果

录屏2025-09-2517.17.10-迅捷PDF转换器.-迅捷PDF转换器.gif

录屏2025-09-2517.29.49-迅捷PDF转换器.-迅捷PDF转换器.gif

目前解决方案是,info.plist中添加UIDesignRequiresCompatibility关闭它,估计明年就不行了,苹果又说了:计划在下一个版本移除这个选项

<key>UIDesignRequiresCompatibility</key> <true/>

image.png

image.png

iOS 系统获取 C++ 崩溃堆栈 - 撒花完结篇

作者 yuec
2025年9月24日 19:13

背景

在 C++ 中,当一个异常被抛出(throw)但未被任何 catch 块捕获时,程序会调用 std::terminate() 函数。为了在程序异常终止前执行自定义的异常监听,C++ 标准库提供了 std::set_terminate() 函数。它允许我们注册一个自定义的终止处理程序(Termination Handler),这个处理程序将在 std::terminate() 被调用时执行。

KSCrash 正是利用了这一机制来捕获未处理的 C++ 异常:

static void install()
{
    KSCM_InstalledState expectedState = KSCM_NotInstalled;
    if (!atomic_compare_exchange_strong(&g_state.installedState, &expectedState, KSCM_Installed)) {
        return;
    }

    kssc_initCursor(&g_stackCursor, NULL, NULL);
    g_state.originalTerminateHandler = std::set_terminate(CPPExceptionTerminate);
}

在 iOS 平台上,捕获 C++ 未处理异常并获取完整堆栈信息面临着独特的挑战。这并非 C++ 语言本身的问题,而是源于两大系统框架的底层机制:GCD 和 RunLoop。

iOS 的主线程运行在 RunLoop 中,而后台任务和异步操作则大量依赖 GCD 进行调度。为了保证框架自身的稳定性和健壮性,这些框架在调用我们的业务代码(可能是 C++)时,通常会用 try catch 捕获异常。

示例 _dispatch_client_callout 的实现:

_dispatch_client_callout(void *ctxt, dispatch_function_t f)
{
@try {
return f(ctxt);
}
@catch (...) {
objc_terminate();
}
}

当我们的 C++ 代码抛出异常时,它不会直接传播到顶层触发我们设置的terminate_handler。相反,它会先被 libdispatch 的 _dispatch_client_callout 或 RunLoop 的内部机制捕获。框架捕获到这个它无法处理的 C++ 异常后,会认为这是一个无法恢复的致命错误。它会选择直接调用 std::terminate() 或进行 rethrow。此时的调用堆栈已经位于系统库内部,原始的 C++ 异常上下文(即异常发生的位置和堆栈)已经丢失。

如果不做额外处理,当 C++ 异常被 GCD 或 RunLoop 捕获后,我们最终得到的崩溃堆栈如下所示。这份堆栈对于定位问题根源几乎没有任何帮助。

#0 0x00000001ecb3f42c in __pthread_kill ()
#1 0x00000002008dec0c in pthread_kill ()
#2 0x00000001ab9e2ba0 in abort ()
#3 0x00000002007fcca4 in abort_message ()
#4 0x00000002007ece40 in demangling_terminate_handler ()
#5 0x000000019b925e3c in _objc_terminate ()
#6 0x00000002007fc068 in std::__terminate ()
#7 0x00000002007fc00c in std::terminate ()
#8 0x000000019b930afc in objc_terminate ()
#9 0x0000000107aae7d0 in _dispatch_client_callout ()
#10 0x0000000107ab130c in _dispatch_queue_override_invoke ()
#11 0x0000000107ac2ae4 in _dispatch_root_queue_drain ()
#12 0x0000000107ac34d8 in _dispatch_worker_thread2 ()
#13 0x00000002008db8f8 in _pthread_wqthread ()

传统方案

使用 fishhook hook __cxa_throw 方法,保留堆栈并建立和抛出异常的映射关系,在 terminate 回调里面取之前的保留的堆栈信息。

struct rebinding item = { 0 }
item.name = "__cxa_throw";
item.replacement = (void *)fishhook_new_cxa_throw;
item.replaced = (void **)&origin_cxa_throw;
ks_rebind_symbols(&item, 1);

fishhook_new_cxa_throw 会在每次 C++ 异常抛出时会执行如下操作:

  • 捕获堆栈:在当前上下文中捕获完整的调用堆栈,这是“第一现场”信息。
  • 建立映射:将捕获到的堆栈与正在被抛出的异常对象 (thrown_exception) 关联起来,并存储在一个全局的数据结构中。
  • 调用原始函数:完成信息保存后,调用原始的 origin_cxa_throw,让异常流程继续进行,不影响程序原有逻辑。
static void fishhook_new_cxa_throw(void *thrown_exception, void *tinfo, void (*dest)(void *)) {
    /*** 捕获堆栈 建立映射 ***/
    origin_cxa_throw(thrown_exception, tinfo, dest);
}

我们设置的 terminate_handler 可以根据当前的异常对象,从全局存储中取出之前保存好的完整堆栈信息。

这个依赖于动态符号替换的方案在 iOS 15 及更高版本中再次遭遇了挑战。为了提升安全性和启动性能,苹果引入了新的动态链接机制——chained fixups。这个机制通过一种指针链的方式预先计算和链接了系统库的符号地址,绕过了传统的 dyld 绑定流程。导致 fishhook 这类依赖于修改 __DATA 段符号指针的工具,在尝试 Hook 系统库(如 libc++abi.dylib)导出的符号时会失效。系统不再通过可修改的指针来查找 __cxa_throw,而是直接跳转到硬编码的地址,我们的钩子函数因此完全不会被触发。

因此业界需要寻找新的、不依赖传统符号 Hook 的方法来应对 iOS 上的 C++ 异常捕获问题。

替代方案

__cxa_throw 被 chained fixups 封堵后,我们需要寻找一个新的、不受其影响的拦截点。答案隐藏在 C++ 异常处理的底层机制中。chained fixups 加固了系统库之间的符号链接,但它并不影响主二进制文件(我们的 App)对系统符号的调用,也不影响我们 Hook 自己二进制文件内的符号。这为我们保留了一些操作空间。

当一个异常被抛出,底层的 libunwind 库会启动一个两阶段的栈回溯过程(详细过程可参考 juejin.cn/post/733192…

Phase 1: Search (搜索阶段):libunwind 会从异常抛出点开始,逆向遍历调用栈的每一帧 (stack frame)。对于每一帧,它会调用一个名为 "Personality Routine" (个性化例程) 的函数。这个函数就像一个“本地向导”,负责告知 libunwind 当前栈帧是否有能力处理这个异常(即是否存在匹配的 catch 块)。

Phase 2: Cleanup (清理阶段):一旦在搜索阶段找到了能处理异常的 catch 块,libunwind 就会进入清理阶段,再次回溯到该栈帧,并沿途析构所有局部对象。

在 Seach 阶段(Cleanup 阶段线程上下文已经发生改变)当栈回溯到属于我们主二进制文件的栈帧时,它调用的 Personality Routine 也在我们的主二进制文件内。

在 ARM 架构下,编译器通常只会生成少数几个固定的 Personality Routine(如 __gxx_personality_v0)。我们只需要在 App 启动时,用 fishhook 将这几个函数替换成我们自己的版本。

通过 Hook Personality Routine,我们将拦截点从异常的“抛出”瞬间,后移到了“寻找 catch 块”的途中。这种方法巧妙地绕过了 chained fixups 的限制,使得在绝大部分场景下(只要调用栈中包含我们 App 的代码),我们都能在 iOS 15+ 系统上重新获得第一现场的 C++ 异常堆栈。

针对主二进制文件中的 __gxx_personality_v0 符号进行重绑定(Rebinding)。

struct rebinding r;
r.name = "__gxx_personality_v0";
r.replacement = (void *)new_gxx_personality_v0;
r.replaced = (void **)&original_gxx_personality_v0;

// 仅对主可执行文件进行重绑定(hard code 是为了简单写这个 demo)
const struct mach_header_64 *header = (const struct mach_header_64 *)_dyld_get_image_header(4);
intptr_t slide = _dyld_get_image_vmaddr_slide(4);
ks_rebind_symbols_image((void *)header, slide, &r, 1);

自定义 Personality Routine:new_gxx_personality_v0 函数是我们实现的核心。

static _Unwind_Reason_Code (*original_gxx_personality_v0)(int version,
                                                          _Unwind_Action actions,
                                                          uint64_t exceptionClass,
                                                          struct _Unwind_Exception *exceptionObject,
                                                          struct _Unwind_Context *context);

static _Unwind_Reason_Code new_gxx_personality_v0(int version,
                                                  _Unwind_Action actions,
                                                  uint64_t exceptionClass,
                                                  struct _Unwind_Exception *exceptionObject,
                                                  struct _Unwind_Context *context) {
    
    if ((actions & _UA_SEARCH_PHASE) != 0) {
/*** 捕获堆栈 建立映射 去重***/
    }
    
    if (original_gxx_personality_v0) {
        return original_gxx_personality_v0(version, actions, exceptionClass, exceptionObject, context);
    }

    return _URC_CONTINUE_UNWIND;
}

成功捕获到了一份信息详尽的 C++ 异常堆栈。

替代新方案于传统方案的总结对比:

特性 Hook __cxa_throw (传统方案) Hook Personality Routine (新方案)
Hook 范围 全部已加载镜像(上千个) 仅主二进制文件 (1 个)
Hook 目标 __cxa_throw 符号 __gxx_personality_v0 等少数符号
实现复杂度 高,需遍历所有镜像 低,目标明确
性能影响 存在启动时开销 可忽略不计
iOS 15+ 兼容性 失效 有效

来自 Gemini 的肯定:

这种从“广撒网”到“精准打击”的转变,不仅是应对系统限制的无奈之举,更是一次技术方案上的巨大飞跃,体现了对底层原理深入理解所带来的优雅与高效。

系统支持

在这场开发者与系统机制的长期博弈之后,苹果最终为这个问题画上了句号。从 libdispatch-1521.100.80 版本开始,_dispatch_client_callout 的实现被彻底重构,从根本上解决了 C++ 异常堆栈丢失的问题。新的实现:告别 try...catch。通过自定义 ___dispatch_noexcept_personality 方法,硬编码返回 _URC_FATAL_PHASE1_ERROR。

// The .cfi_personality directive is used to control the personality routine
// (used for exception handling) encoded in the CFI (Call Frame Information).
// We use that directive to override the normal personality routine with one
// that always reports an error, leading the Phase 1 of unwinding to abort the
// program.
//
// The encoding we use here is 155, which is 'indirect pcrel sdata4'
// (DW_EH_PE_indirect | DW_EH_PE_pcrel | DW_EH_PE_sdata4). This is known to
// work for x86_64 and arm64.
#define OVERRIDE_PERSONALITY_ASSEMBLY() \
__asm__(".cfi_personality 155, ___dispatch_noexcept_personality")

#undef _dispatch_client_callout
extern "C" void
_dispatch_client_callout(void *ctxt, dispatch_function_t f)
{
OVERRIDE_PERSONALITY_ASSEMBLY();
f(ctxt);
__asm__ __volatile__("");  // prevent tailcall
}

extern "C" __attribute__((used)) _Unwind_Reason_Code
__dispatch_noexcept_personality(int version, _Unwind_Action action,
uint64_t exceptionClass, struct _Unwind_Exception *exceptionObject,
struct _Unwind_Context *context)
{
(void)version;
(void)action;
(void)exceptionClass;
(void)exceptionObject;
(void)context;
return _URC_FATAL_PHASE1_ERROR;
}

当 Seach 阶段遍历到 _dispatch_client_callout 方法时,对应的 personality routine 方法会返回 _URC_FATAL_PHASE1_ERROR,收到这个值 Seach 阶段会停止遍历,执行 terminate handler,此时保留了抛异常的第一现场。线下测试 iOS 26 系统,runloop 内抛出的 C++ 异常目前也可以通过 terminate handler 获取崩溃的第一现场。

总结

随着 libdispatch 的官方更新,苹果为这个困扰开发者多年的问题画上了句号。这是否意味着我们之前探索的替代方案 —— 巧妙 Hook Personality Routine 的设计失去了价值?当然答案并非如此。

技术方案总有其生命周期,会被更优的设计、甚至平台的原生支持所替代。但我们面对问题时,那种对底层原理的渴求、对未知领域的探索、以及在逆境中寻求突破的整个过程,其价值超越了任何单一解决方案。这次探索的真正产出,不是一个临时的 Hook 方案,应用价值也远不止于获取 C++ 崩溃堆栈——它为我们揭示了更多系统底层的可能性(尽管具体应用暂不便详述)。

当未来出现新的、未知的问题时,真正能让我们披荆斩棘的,正是这些沉淀下来的系统性知识、第一性原理的思考方式和坚韧的探索精神。这,才是技术演进中永不“过时”的核心资产。

iOS 开发核心知识点全解析(面试必备)

作者 JQShan
2025年9月24日 16:02

iOS 开发的深度面试往往围绕运行时机制、内存管理、多线程、视图渲染、架构设计等核心领域展开。本文将系统梳理这些领域的高频问题,并提供清晰、完整、可直接用于面试的答案,帮助开发者构建扎实的知识体系。

一、Runtime 核心原理

Runtime(运行时)是 OC 的灵魂,负责对象的创建、方法调用、消息转发等底层操作。其开源源码可参考苹果官方的objc4仓库。

1. Runtime 内存模型(isa、对象、类、metaclass)

OC 的内存模型以isa 指针为核心,串联起实例对象、类对象、元类(metaclass)  三层结构,每层对应不同的结构体,存储不同信息。

(1)核心结构关系

plaintext

实例对象(Instance)-> 类对象(Class)-> 元类(Metaclass)-> 根元类(Root Metaclass)
  • 实例对象(Instance) :存储成员变量(ivar)  的值,其isa指针指向对应的类对象
    结构体简化:

    objc

    struct Instance {
        Class isa; // 指向类对象
        // 成员变量的值(如NSString *name; int age;)
    };
    
  • 类对象(Class) :存储实例方法(-method)、属性(property)、协议(protocol) ,其isa指针指向元类,同时包含指向父类的superclass指针。
    结构体核心依赖class_data_bits_t,内部通过data()方法获取class_rw_t(可读写数据):

    objc

    struct objc_class {
        Class isa;         // 指向元类
        Class superclass;  // 指向父类
        class_data_bits_t bits; // 存储类的核心数据
    };
    
  • 元类(Metaclass) :存储类方法(+method) ,其isa指针指向根元类(如NSObject的元类),superclass指向父类的元类。
    元类的本质是 “类的类”—— 因为类对象也是 OC 对象(可调用+method),需要元类来管理其方法。

  • 根元类(Root Metaclass) :所有元类的最终父类(如NSObject的元类),其isa指针指向自身superclass指向根类(如NSObject)。

(2)isa 指针的作用

  • 本质是Class类型的指针,用于定位对象的 “所属类”

    • 实例对象的isa → 类对象(确定实例能调用哪些实例方法);
    • 类对象的isa → 元类(确定类能调用哪些类方法)。
  • 64 位系统中,isa 指针通过位掩码存储额外信息(如对象是否在堆上、引用计数等),需通过ISA_MASK提取真实的类地址。

2. 为什么要设计 metaclass?

核心目的是解决 “类方法的存储归属” 问题

  • OC 中,实例方法的调用依赖实例对象的isa找到类对象,类对象存储实例方法列表;
  • 类方法(如+alloc)的调用者是 “类对象”,而类对象本身也是 OC 对象(可被isa指向),因此需要一个专门的 “类”(元类)来存储类方法列表。
  • 若没有 metaclass,类方法将无处存储,导致[NSObject alloc]这类调用无法实现。

3. class_copyIvarList & class_copyPropertyList 区别

两者均用于获取类的成员信息,但针对的对象和返回内容完全不同,核心区别如下:

对比维度 class_copyIvarList class_copyPropertyList
获取的内容 成员变量(ivar) 属性(property)
本质区别 编译时定义的 “底层变量”(如_name 封装后的 “属性”(含 setter/getter)
是否包含合成变量 是(如@property合成的_name 是(直接返回属性本身)
访问权限 可获取私有 ivar(如类内部定义的int _age 仅获取属性(私有 property 也可获取)
返回类型 Ivar *(成员变量指针数组) objc_property_t *(属性指针数组)

示例
若类定义为@interface Person : NSObject { int _weight; } @property (nonatomic, copy) NSString *name; @end,则:

  • class_copyIvarList返回_weight_name(合成的 ivar);
  • class_copyPropertyList仅返回name(属性)。

4. class_rw_t 和 class_ro_t 的区别

两者均是类对象的核心数据结构,存储方法、属性、协议等信息,但核心区别在于读写权限和初始化时机

对比维度 class_rw_t(Read-Write) class_ro_t(Read-Only)
读写权限 可读写(运行时可修改) 只读(编译时确定,不可修改)
初始化时机 运行时(类第一次被使用时初始化) 编译时(编译器生成,存储在 Mach-O 的__DATA段)
存储内容 包含class_ro_t的指针 + 运行时添加的方法 / 属性 / 协议(如 Category 的内容) 编译时确定的 “固定信息”:初始方法列表、属性列表、协议列表、成员变量信息
核心作用 支持动态添加内容(如 Category、Method Swizzle) 存储类的 “静态基础信息”,确保编译后不可篡改

关系class_rw_t内部有一个const class_ro_t *ro指针,指向类的只读基础数据;运行时动态添加的内容(如 Category 的方法)会直接存入class_rw_t

5. Category 加载流程 & 方法优先级

Category(分类)是 OC 中动态扩展类功能的核心机制,其加载和方法调用有严格的顺序规则。

(1)Category 加载流程(运行时阶段)

  1. 编译时:编译器将 Category 编译为category_t结构体,存储分类的方法列表、属性列表、协议列表,以及所属的类名。
    category_t结构体简化:

    objc

    struct category_t {
        const char *name;       // 所属类名
        classref_t cls;         // 所属类(运行时绑定)
        struct method_list_t *instance_methods; // 实例方法
        struct method_list_t *class_methods;    // 类方法
        struct protocol_list_t *protocols;      // 协议
        struct property_list_t *properties;     // 属性
    };
    
  2. 运行时(map_images 阶段)

    • dyld(动态链接器)加载完所有类和分类后,调用_objc_init初始化 Runtime;
    • Runtime 通过_processCatlist遍历所有category_t,将分类的方法、属性、协议合并到所属类的class_rw_t(实例方法合并到类的instance_methods,类方法合并到元类的class_methods)。
  3. 合并规则
    分类的方法会插入到类原有方法列表的前面(而非替换),因此分类方法会 “覆盖” 类的同名方法(实际是优先调用)。

(2)Category 的 load 方法加载顺序

+load方法是 Category 中特殊的方法,不遵循消息转发机制,由 Runtime 直接调用,顺序规则如下:

  1. 类的 load 先于分类的 load:先调用所有类的+load(父类 → 子类),再调用所有分类的+load
  2. 同类分类的 load 按编译顺序:Xcode 编译时,后添加到项目的分类,其+load先被调用(可通过 “Build Phases → Compile Sources” 调整顺序);
  3. 不同类分类的 load 按类的加载顺序:依赖类的加载顺序(如 A 类依赖 B 类,则 B 类的分类+load先调用)。

(3)Category 同名方法的调用顺序

当多个分类(或类与分类)有同名方法时,调用顺序遵循 “后编译的分类优先”:

  1. 分类方法覆盖类的同名方法(因分类方法在方法列表前面);

  2. 多个分类的同名方法,后编译的分类方法先被调用(编译顺序可通过 Xcode 调整);

  3. 父类分类的方法优先级低于子类的分类(因子类的类加载晚于父类)。

注意:分类无法覆盖+load+initialize方法(+initialize遵循消息转发,会先调用父类的)。

6. Category & Extension 区别 + 能否给 NSObject 添加 Extension?

(1)核心区别

对比维度 Category(分类) Extension(扩展)
能否添加成员变量 不能(仅能添加方法、属性、协议,属性不会自动合成 ivar,需手动关联) 能(可添加私有成员变量、方法、属性)
可见性 公开(需在.h 中声明,或匿名分类在.m 中) 私有(仅在定义的.m 文件中可见)
编译时机 运行时合并到类中 编译时作为类的一部分(与类同时编译)
是否需要实现 可单独实现(.m 文件) 必须在类的.m 文件中实现(否则编译报错)
核心用途 扩展已有类的功能(如给 UIView 加分类) 给类添加私有成员(如在.m 中隐藏细节)

(2)能否给 NSObject 添加 Extension?

不能直接添加,原因如下:

  • Extension 是类的 “一部分”,必须在类的定义文件(.m)中声明和实现

  • NSObject 是系统类,开发者无法修改其.m 文件,因此无法直接为其添加 Extension;

  • 若强行在自己的文件中声明@interface NSObject () { int _myVar; } @end,编译时会报错(“Category is not allowed on 'NSObject'”)。

替代方案:若需给 NSObject 添加私有成员,可通过 “匿名分类 + 关联对象” 实现,或自定义 NSObject 的子类。

7. 消息转发机制 + 与其他语言对比

OC 的方法调用本质是 “发送消息”(objc_msgSend),当消息无法被接收者处理时,会触发消息转发机制,避免崩溃。

(1)消息转发三阶段(完整流程)

在进入转发前,会先进行方法查找

  1. 从接收者的类的缓存(cache_t)  中查找方法(快速查找);

  2. 缓存未命中,从类的class_rw_t的方法列表中查找,若未找到则递归查找父类(直到根类NSObject);

  3. 若所有父类均未找到,进入动态方法解析 → 快速转发 → 慢速转发三阶段。

具体转发流程:

  1. 动态方法解析(Resolve)

    • 调用+resolveInstanceMethod:(实例方法)或+resolveClassMethod:(类方法),允许开发者动态添加方法实现
    • 示例:若[person run]未实现,可在resolveInstanceMethod中用class_addMethod添加run的实现;
    • 若返回YES,则重新发起消息查找;若返回NO,进入下一阶段。
  2. 快速转发(Fast Forwarding)

    • 调用-forwardingTargetForSelector:,允许开发者将消息转发给其他对象(“替身”);
    • 示例:返回self.otherObject,则消息会转发给otherObject处理;
    • 若返回非nil,则消息转发给该对象;若返回nil,进入下一阶段。
  3. 慢速转发(Slow Forwarding)

    • 调用-methodSignatureForSelector:,获取方法签名(返回值类型、参数类型);
    • 若返回nil,则触发崩溃(unrecognized selector sent to instance);
    • 若返回有效签名,调用-forwardInvocation:,开发者可在该方法中自定义消息处理逻辑(如转发给多个对象、记录日志)。

(2)与其他语言(如 Java)的消息机制对比

对比维度 OC(消息转发) Java(方法调用)
绑定时机 运行时绑定(动态) 编译时绑定(静态,除非用反射)
方法不存在的处理 触发消息转发,可自定义处理(避免崩溃) 编译时报错(若未声明)或运行时抛NoSuchMethodError
灵活性 高(支持动态添加方法、转发消息) 低(需提前声明方法,反射仅能绕过编译检查)
性能 略低(运行时查找和转发有开销) 高(编译时确定方法地址)
崩溃风险 可通过转发避免崩溃 无法避免(除非用 try-catch 捕获异常)

8. 方法调用前的准备:消息查找流程

在 “动态解析→消息转发” 之前,Runtime 会先执行消息查找流程(分为快速查找和慢速查找),这是方法调用的核心前置步骤:

  1. 快速查找(缓存查找)

    • 调用objc_msgSend时,先从接收者的类的cache_t(缓存)中查找方法;
    • cache_t是哈希表,key 为SEL(方法选择器),value 为IMP(方法实现指针);
    • 若找到IMP,直接跳转到实现执行;若未找到,进入慢速查找。
  2. 慢速查找(方法列表查找)

    • 从类的class_rw_tmethod_list中遍历查找SEL(按方法列表顺序);
    • 若未找到,递归查找父类的method_list(直到根类NSObject);
    • 若找到,将SELIMP存入当前类的cache_t(缓存,供下次快速查找),然后执行IMP
    • 若所有父类均未找到,进入动态方法解析和消息转发。

9. IMP、SEL、Method 的区别和使用场景

三者是 Runtime 中描述 “方法” 的核心概念,关系为:Method包含SELIMPSEL是方法标识,IMP是方法实现地址。

概念 定义 本质 核心作用 使用场景
SEL 方法选择器(typedef const struct objc_selector *SEL; 字符串(方法名的哈希值) 唯一标识一个方法(如@selector(run) 方法调用(objc_msgSend(person, @selector(run)))、判断方法是否存在([person respondsToSelector:@selector(run)]
IMP 方法实现指针(typedef id (*IMP)(id, SEL, ...); 函数指针 指向方法的具体实现代码 直接调用方法(跳过消息查找,如IMP imp = [person methodForSelector:@selector(run)]; imp(person, @selector(run));)、Method Swizzle
Method 方法结构体(typedef struct objc_method *Method; 包含 SEL、IMP、方法签名 封装方法的完整信息 获取方法详情(如method_getName(method)获取 SEL、method_getImplementation(method)获取 IMP)、动态添加方法(class_addMethod

关系示例
Method method = class_getInstanceMethod([Person class], @selector(run));
SEL sel = method_getName(method); // 获取 SEL
IMP imp = method_getImplementation(method); // 获取 IMP

10. load、initialize 方法的区别(含继承关系)

+load+initialize是类初始化时的两个特殊方法,但触发时机、调用逻辑、继承行为完全不同。

(1)核心区别

对比维度 +load 方法 +initialize 方法
触发时机 类 / 分类被加载到内存时(dyld 阶段) 类第一次接收消息时(如[Person alloc]
调用方式 Runtime 直接调用(不经过 objc_msgSend) 经过消息转发(objc_msgSend)
是否自动调用父类 是(父类 load 先于子类 load) 否(仅当子类未实现时,才调用父类)
分类是否覆盖 否(类和分类的 load 都会调用) 是(分类的 initialize 会覆盖类的)
调用次数 仅一次(类加载时) 仅一次(类第一次使用时)
线程安全 是(Runtime 加锁,串行调用) 否(需手动加锁,避免多线程调用)

(2)继承关系中的区别

  • +load
    父类的+load先于子类的+load调用,且所有类的 load 调用完后,才调用分类的 load
    示例:NSObject → Person(子类) → Person+Category1 → Person+Category2(按编译顺序)。

  • +initialize
    仅当子类未实现+initialize时,才会调用父类的+initialize(因消息转发会先查找子类,子类未实现则找父类)。
    示例:

    objc

    @interface Father : NSObject @end
    @implementation Father
    + (void)initialize { NSLog(@"Father initialize"); }
    @end
    
    @interface Son : Father @end
    @implementation Son
    // 未实现initialize
    @end
    
    // 调用 [Son alloc] 时,会先调用 Father 的 initialize(因Son未实现)
    

    若子类实现了+initialize,则仅调用子类的,父类的不会被调用(除非父类单独被使用)。

11. 消息转发机制的优劣

(1)优点

  1. 灵活性高:允许动态添加方法、转发消息,适配复杂场景(如 “多继承” 模拟、解耦);
  2. 容错性强:可捕获 “未实现的方法”,避免崩溃(如在forwardInvocation中记录日志或返回默认值);
  3. 支持 AOP(面向切面编程) :通过转发机制在方法调用前后插入逻辑(如埋点、权限校验)。

(2)缺点

  1. 性能开销:消息查找(缓存→方法列表→父类)+ 转发(三阶段)会增加运行时开销,频繁触发会影响性能;
  2. 调试难度大:方法调用链路长,崩溃时的调用栈可能不完整(如转发后崩溃,难以定位原始调用者);
  3. 可读性差:动态转发逻辑隐藏在底层,代码维护成本高(如新人难以理解 “为什么未实现的方法能执行”)。

二、内存管理

iOS 内存管理的核心是引用计数,Runtime 通过SideTableautoreleasepool等机制实现自动管理,ARC 则进一步简化了开发者的操作。

1. weak 的实现原理 + SideTable 结构

weak是 OC 中用于避免循环引用的弱引用机制,其核心是通过SideTable管理弱引用表,确保对象释放时自动将weak指针置为nil

(1)SideTable 结构

SideTable是 Runtime 中的全局哈希表,每个对象的引用计数和弱引用均由SideTable管理,结构简化如下:

objc

struct SideTable {
    spinlock_t slock;          // 自旋锁(保证线程安全)
    RefcountMap refcnts;       // 引用计数表(key:对象指针,value:引用计数)
    weak_table_t weak_table;   // 弱引用表(存储所有指向该对象的weak指针)
};
  • spinlock_t:轻量级锁,适用于短时间持有(如修改引用计数时),避免线程竞争;
  • RefcountMapstd::unordered_map<DisguisedPtr<objc_object>, size_t>,存储对象的引用计数;
  • weak_table_t:弱引用表,结构为std::unordered_map<DisguisedPtr<objc_object>, weak_entry_t>weak_entry_t内部存储所有指向该对象的weak指针数组。

(2)weak 实现原理

  1. weak 指针赋值时(如__weak Person *weakP = person;):

    • Runtime 通过objc_storeWeak(&weakP, person)weakP添加到person对应的SideTableweak_table中;
    • personnil,则直接将weakP置为nil(不操作SideTable)。
  2. 对象释放时dealloc阶段):

    • 调用objc_clear_deallocating,从SideTable中找到该对象的weak_entry_t
    • 遍历weak_entry_t中的所有weak指针,将其置为nil
    • weak_table中删除该weak_entry_t,并清空引用计数表中的条目。
  3. 核心优势weak指针不会增加对象的引用计数,且对象释放时自动置为nil,避免野指针访问。

2. 关联对象的应用 + 系统实现 + 内存管理

关联对象(Associated Object)是 Category 中 “间接添加成员变量” 的机制,通过 Runtime API 将对象与另一个对象关联。

(1)关联对象的应用

  • 给 Category 添加 “成员变量” :Category 不能直接添加 ivar,但可通过关联对象存储数据;

  • 解耦数据存储:如给 UIView 关联一个NSString *identifier,无需继承 UIView;

  • 临时存储上下文:如网络请求回调中,将请求参数与回调 block 关联。

示例

objc

// 给UIView添加分类,关联identifier
@interface UIView (Identifier)
@property (nonatomic, copy) NSString *identifier;
@end

@implementation UIView (Identifier)
static const void *kIdentifierKey = &kIdentifierKey;

- (void)setIdentifier:(NSString *)identifier {
    // 关联对象:key=kIdentifierKey,value=identifier,策略=OBJC_ASSOCIATION_COPY_NONATOMIC
    objc_setAssociatedObject(self, kIdentifierKey, identifier, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)identifier {
    return objc_getAssociatedObject(self, kIdentifierKey);
}
@end

(2)系统实现原理

关联对象的管理依赖 Runtime 内部的全局哈希表,核心结构如下:

  1. AssociationsManager:单例管理器,持有AssociationsHashMap,并通过自旋锁保证线程安全;

  2. AssociationsHashMapunordered_map<DisguisedPtr<objc_object>, ObjectAssociationMap>,key 是 “被关联的对象”(如 UIView 实例),value 是该对象的关联表;

  3. ObjectAssociationMapunordered_map<void *, ObjcAssociation>,key 是开发者定义的key(如kIdentifierKey),value 是ObjcAssociation(存储关联值和内存管理策略);

  4. ObjcAssociation:存储关联值(id _value)和内存管理策略(objc_AssociationPolicy)。

操作流程

  • objc_setAssociatedObject:通过AssociationsManager找到AssociationsHashMap,根据 “被关联对象” 找到ObjectAssociationMap,存入keyObjcAssociation
  • objc_getAssociatedObject:反向查找,根据 “被关联对象” 和key获取ObjcAssociation中的_value
  • objc_removeAssociatedObjects:删除 “被关联对象” 对应的ObjectAssociationMap

(3)关联对象的内存管理

关联对象的内存管理由objc_AssociationPolicy(关联策略)决定,策略对应 ARC 下的内存语义:

关联策略 内存语义(ARC) 作用
OBJC_ASSOCIATION_ASSIGN assign 弱引用,不 retain,对象释放后变为野指针
OBJC_ASSOCIATION_RETAIN_NONATOMIC strong(非原子) retain 关联值,线程不安全
OBJC_ASSOCIATION_COPY_NONATOMIC copy(非原子) copy 关联值,线程不安全
OBJC_ASSOCIATION_RETAIN strong(原子) retain 关联值,线程安全
OBJC_ASSOCIATION_COPY copy(原子) copy 关联值,线程安全

释放时机

  • 当 “被关联对象” 释放时(dealloc),Runtime 会自动调用objc_removeAssociatedObjects,根据关联策略释放关联值(如retain策略会release关联值);
  • 也可手动调用objc_removeAssociatedObjects移除所有关联值。

(4)关联对象如何实现 weak 属性

关联对象本身不支持weak策略(OBJC_ASSOCIATION_ASSIGNassign,非weak),但可通过弱引用容器实现:

  1. 自定义一个WeakContainer类,内部用__weak持有目标对象;

  2. WeakContainer实例作为关联值,策略设为OBJC_ASSOCIATION_RETAIN_NONATOMIC

  3. 访问时从WeakContainer中获取__weak对象,实现弱引用效果。

示例

objc

// 弱引用容器
@interface WeakContainer : NSObject
@property (nonatomic, weak) id value;
@end

@implementation WeakContainer
@end

// 关联时使用容器
- (void)setWeakValue:(id)weakValue {
    WeakContainer *container = [WeakContainer new];
    container.value = weakValue;
    objc_setAssociatedObject(self, kWeakValueKey, container, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (id)weakValue {
    WeakContainer *container = objc_getAssociatedObject(self, kWeakValueKey);
    return container.value; // 弱引用,对象释放后为nil
}

3. Autoreleasepool 原理 + 数据结构

Autoreleasepool(自动释放池)是 iOS 中管理临时对象内存的机制,通过延迟释放对象,避免频繁调用release

(1)核心原理

  • 作用:收集调用autorelease的对象,在Autoreleasepool销毁时,对池内所有对象调用release

  • 触发时机

    1. 主线程:RunLoop 的每个循环周期结束时(如kCFRunLoopBeforeWaiting),自动销毁并重建Autoreleasepool
    2. 子线程:需手动创建@autoreleasepool {},否则对象可能无法及时释放;
    3. 手动销毁:@autoreleasepool {}代码块执行完毕时,池内对象被release

(2)数据结构

Autoreleasepool基于双向链表实现,核心结构是AutoreleasePoolPage

  • AutoreleasePoolPage:每个 Page 是 4096 字节(一页内存),结构简化如下:

    objc

    class AutoreleasePoolPage {
        static const size_t SIZE = 4096; // 4KB
        AutoreleasePoolPage *next;       // 下一个Page(链表节点)
        AutoreleasePoolPage *prev;       // 上一个Page
        id *begin;                       // Page内存储对象的起始地址
        id *end;                         // Page内存储对象的结束地址
        id *top;                         // 当前存储对象的下一个位置(栈指针)
        pthread_t thread;                // 所属线程(每个线程对应一个Page链表)
    };
    
  • Page 链表:当一个 Page 装满(top == end)时,创建新的 Page 并加入链表;

  • POOL_BOUNDARY:哨兵对象,标记Autoreleasepool的边界。@autoreleasepool {}会在开始时压入POOL_BOUNDARY,结束时从top向下遍历,直到遇到POOL_BOUNDARY,对中间所有对象调用release,并将top重置到POOL_BOUNDARY之后。

(3)操作流程

  1. 创建Autoreleasepool:调用objc_autoreleasePoolPush(),压入POOL_BOUNDARY,返回其地址;
  2. 对象调用autorelease:调用objc_autorelease(),将对象指针存入当前 Page 的top位置,top自增;若当前 Page 满,创建新 Page 并继续存储;
  3. 销毁Autoreleasepool:调用objc_autoreleasePoolPop(POOL_BOUNDARY),从top向下遍历,对每个对象调用release,直到遇到POOL_BOUNDARY,并调整top指针。

4. ARC 实现原理 + 优化

ARC(Automatic Reference Counting)是编译器和 Runtime 协作的自动内存管理机制,核心是 “编译器自动插入引用计数操作代码”。

(1)ARC 实现原理

  1. 编译器层面

    • 分析代码中对象的生命周期,在合适的位置自动插入retainreleaseautorelease

    • 例如:

      objc

      // ARC代码
      - (void)test {
          Person *p = [[Person alloc] init]; // 编译器自动插入 [p retain](实际alloc返回的对象引用计数为1,无需retain)
      } // 函数结束时,编译器自动插入 [p release]
      
    • 遵循 “谁持有,谁释放” 原则:局部变量出作用域时释放,成员变量在对象dealloc时释放。

  2. Runtime 层面

    • 提供objc_retainobjc_releaseobjc_autorelease等 API,供编译器插入调用;
    • 通过SideTable管理引用计数,确保线程安全;
    • 处理weak指针(如对象释放时置为nil)。

(2)ARC 对 retain & release 的优化

ARC 通过编译器和 Runtime 优化,减少不必要的retain/release操作,提升性能:

  1. 返回值优化(NRVO - Named Return Value Optimization)
    若函数返回局部对象,编译器直接将对象的所有权转移给调用者,避免插入autoreleaseretain
    示例:

    objc

    - (Person *)createPerson {
        Person *p = [[Person alloc] init]; // 局部对象
        return p; // ARC优化:直接返回p,无需autorelease
    }
    // 调用者:Person *p = [self createPerson]; 无需retain
    
  2. Toll-Free Bridging 优化
    当 Core Foundation 对象(如CFStringRef)与 OC 对象(如NSString)桥接时,ARC 自动管理引用计数,避免手动调用CFRetain/CFRelease
    示例:NSString *str = (__bridge_transfer NSString *)CFStringCreateWithCString(...);__bridge_transfer让 ARC 接管 CF 对象的释放。

  3. 局部变量优化
    若局部变量仅在当前作用域使用,且无外部引用,编译器会省略retain/release(如循环内的临时对象)。

  4. 零成本异常处理
    MRC 中,异常抛出时需手动处理release;ARC 中,编译器通过@try/@finally自动插入release,且优化了异常处理的性能开销。

5. ARC 下的内存泄漏场景

ARC 虽自动管理内存,但仍存在以下常见泄漏场景:

  1. 循环引用

    • Block 与 self 循环引用self持有 block,block 持有self(如self.block = ^{ [self doSomething]; };);
      解决:用__weak typeof(self) weakSelf = self;打破循环。
    • ** delegate 循环引用 **:若delegatestrong修饰(如@property (nonatomic, strong) id<Delegate> delegate;),会导致委托方与被委托方循环引用;
      解决:delegateweak修饰。
    • 容器与对象循环引用:对象持有容器,容器存储对象(如self.array = @[self];);
      解决:用weak容器(如NSArray存储WeakContainer)。
  2. NSTimer 未 invalidate
    NSTimerretaintarget(如self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(tick) userInfo:nil repeats:YES];),若self持有timer,会形成循环引用;
    解决:在dealloc或页面销毁时调用[self.timer invalidate]; self.timer = nil;

  3. AFN 请求未取消
    AFN 的NSURLSessionDataTaskretain其回调 block,若 block 持有self,且请求未取消,self会一直被持有;
    解决:页面销毁时调用[task cancel];

  4. 缓存未清理
    全局缓存(如NSCache、单例中的NSDictionary)存储大量对象,且未设置过期策略,导致对象无法释放;
    解决:设置缓存上限(如cache.countLimit = 100;),或在内存警告时清理缓存。

  5. 子线程未退出
    子线程中开启 RunLoop 且未手动停止(如CFRunLoopRun();),导致子线程一直存活,持有其内部的对象;
    解决:调用CFRunLoopStop(CFRunLoopGetCurrent());停止 RunLoop。

三、NSNotification 机制

NSNotificationCenter(通知中心)是 iOS 中跨模块通信的核心机制,基于 “发布 - 订阅” 模式实现。

1. 实现原理(结构设计、存储关系)

NSNotificationCenter的核心是通知表,存储 “通知名 - 观察者 - 处理方法” 的映射关系,结构设计如下:

(1)核心结构

  • 通知表(_notificationTable)NSNotificationCenter内部维护一个哈希表,key 为**通知名(NSString * ,value 为NSMapTable(存储该通知名对应的所有观察者);
  • 观察者表(NSMapTable) :key 为观察者(id) ,value 为NSMutableArray(存储该观察者订阅该通知的所有 “处理条目”);
  • 处理条目(_NotificationObserver) :每个条目包含selector(处理方法)、object(通知发送者过滤条件)、queue(指定线程处理通知)、context(上下文)。

(2)name & observer & SEL 的关系

  • 多对多关系:一个通知名(name)可被多个观察者(observer)订阅,一个观察者可订阅多个通知名;
  • 过滤逻辑:订阅时指定object,则仅接收该object发送的通知;若objectnil,则接收所有发送者的该通知;
  • 处理逻辑:当通知被发布时,NSNotificationCenter根据通知名找到所有观察者,遍历处理条目,若object匹配,调用objc_msgSend(observer, selector, notification)

2. 通知的发送是同步还是异步?

默认是同步的

  • 调用postNotificationName:object:userInfo:时,NSNotificationCenter会在当前线程中立即遍历所有匹配的观察者,同步调用其selector

  • 若某个观察者的selector执行耗时,会阻塞当前线程(包括主线程,导致 UI 卡顿)。

异步发送方式
需手动将通知发布逻辑放入异步队列,例如:

objc

// 在子线程异步发布通知
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    [[NSNotificationCenter defaultCenter] postNotificationName:@"TestNotification" object:nil];
});

// 或在主线程异步处理通知(观察者侧)
[[NSNotificationCenter defaultCenter] addObserverForName:@"TestNotification" object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) {
    // 主线程异步处理
}];

3. NSNotificationCenter 接收与发送是否在同一线程?如何异步发送?

(1)线程一致性

  • 默认情况:接收通知的线程与发送通知的线程完全一致
    示例:在子线程 A 调用postNotification,则所有观察者的selector会在子线程 A 执行;在主线程调用,则在主线程执行。
  • 例外情况:若订阅时指定了queue(如addObserverForName:object:queue:usingBlock:),则通知会在指定的queue对应的线程执行;
    示例:指定queue:[NSOperationQueue mainQueue],则无论通知在哪个线程发布,都会在主线程执行 block。

(2)异步发送通知的两种方式

  1. 发布侧异步:将postNotification放入异步队列,让发布操作不阻塞当前线程;

    objc

    dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
        [[NSNotificationCenter defaultCenter] postNotificationName:@"AsyncPost" object:nil];
    });
    
  2. 接收侧异步:订阅时指定异步队列,让处理逻辑在后台线程执行;

    objc

    // 接收通知的block在全局队列执行(异步)
    [[NSNotificationCenter defaultCenter] addObserverForName:@"AsyncReceive" object:nil queue:[[NSOperationQueue alloc] init] usingBlock:^(NSNotification * _Nonnull note) {
        // 耗时处理(如解析数据)
    }];
    

4. NSNotificationQueue 是异步还是同步?在哪个线程响应?

NSNotificationQueue(通知队列)是对NSNotificationCenter的扩展,核心作用是延迟发送通知合并重复通知,其发送方式和线程规则如下:

(1)同步 / 异步特性

  • 默认是异步的NSNotificationQueue不会立即发送通知,而是将通知加入队列,在RunLoop 的特定模式下批量发送;

  • 支持三种发送模式(NSPostingStyle):

    • NSPostWhenIdle:RunLoop 空闲时发送(如无事件处理时);
    • NSPostASAP:RunLoop 下一次循环时发送(尽快发送,但不阻塞当前 RunLoop);
    • NSPostNow:立即发送(同步,等价于NSNotificationCenter的直接发布)。

(2)响应线程

  • 与发布通知的线程一致NSNotificationQueue的通知最终由NSNotificationCenter发送,因此响应线程与NSNotificationQueue所在的线程一致;
  • 示例:在主线程创建NSNotificationQueue并添加通知,则通知会在主线程的 RunLoop 空闲时发送,观察者在主线程响应;在子线程创建,则在子线程响应。

5. NSNotificationQueue 和 RunLoop 的关系

NSNotificationQueue完全依赖RunLoop实现延迟发送,核心交互如下:

  1. 通知入队:调用enqueueNotification:postingStyle:coalesceMask:forModes:时,NSNotificationQueue将通知存储在内部队列,并注册一个RunLoop 观察者(CFRunLoopObserverRef)
  2. RunLoop 触发:当 RunLoop 进入指定的模式(如NSDefaultRunLoopMode)且满足发送条件(如NSPostWhenIdle对应 RunLoop 空闲)时,RunLoop 观察者触发回调;
  3. 批量发送NSNotificationQueue从队列中取出所有符合条件的通知,调用NSNotificationCenterpostNotification批量发送;
  4. 合并通知:若设置了coalesceMask(如NSNotificationCoalescingOnName),则

记录一次鸿蒙JSVM崩溃定位修复

作者 nullLululi
2025年9月24日 15:27

背景

我们的跨端方案需要在鸿蒙上动态执行js代码,类似RN。鸿蒙提供了JSVM解决方案,JSVM套壳V8。但在运行过程出现一个JSVM内部的崩溃。这篇文章主要记录了如何使用调试方法、少量汇编知识定位到系统库的崩溃原因

现象

使用jsvm执行js脚本,发生了WhiteToGreyAndPush崩溃,堆栈如下:

Reason:Signal:SIGSEGV(SEGV_MAPERR)@0xbebebebebebebec6 
[libjsvm.so] v8::internal::MarkingBarrier::WhiteToGreyAndPush(v8::internal::HeapObject) Disassembly:201
[libjsvm.so] v8::internal::MarkingBarrier::Write(v8::internal::HeapObject, v8::internal::FullHeapObjectSlot, v8::internal::HeapObject) 0x0000005f572087bc
[libjsvm.so] v8::internal::Factory::CodeBuilder::BuildInternal(bool) 0x0000005f5718766c
[libjsvm.so] v8::internal::compiler::CodeGenerator::FinalizeCode() 0x0000005f578bd380
[libjsvm.so] auto v8::internal::compiler::PipelineImpl::Run<v8::internal::compiler::FinalizeCodePhase>() 0x0000005f57b25c3c
[libjsvm.so] v8::internal::compiler::PipelineImpl::FinalizeCode(bool) 0x0000005f57b19208
[libjsvm.so] v8::internal::compiler::PipelineCompilationJob::FinalizeJobImpl(v8::internal::Isolate*) 0x0000005f57b1903c
[libjsvm.so] v8::internal::Compiler::FinalizeTurbofanCompilationJob(v8::internal::TurbofanCompilationJob*, v8::internal::Isolate*) 0x0000005f57065b40
[libjsvm.so] v8::internal::OptimizingCompileDispatcher::InstallOptimizedFunctions() 0x0000005f570b3b24
[libjsvm.so] v8::internal::StackGuard::HandleInterrupts() 0x0000005f57149b20
[libjsvm.so] v8::internal::Runtime_StackGuard(int, unsigned long*, v8::internal::Isolate*) 0x0000005f57592604
[libjsvm.so] Builtins_CEntry_Return1_ArgvOnStack_NoBuiltinExit 0x0000005f56af20b8
[libjsvm.so] Builtins_ProxyConstructor 0x0000005f56b53190
[libjsvm.so] Builtins_JSBuiltinsConstructStub 0x0000005f56a6677c
[??] ?? 0x0000005f3f4c9b84
Reason:Signal:SIGSEGV(SEGV_MAPERR)@0xbebebebebebebec6 
[libjsvm.so] v8::internal::MarkingBarrier::WhiteToGreyAndPush(v8::internal::HeapObject) Disassembly:201
[libjsvm.so] v8::internal::MarkingBarrier::Write(v8::internal::HeapObject, v8::internal::FullHeapObjectSlot, v8::internal::HeapObject) 0x0000005f37f887bc
[libjsvm.so] v8::internal::Dictionary<v8::internal::NameDictionary, v8::internal::NameDictionaryShape>::SetEntry(v8::internal::InternalIndex, v8::internal::Object, v8::internal::Object, v8::internal::PropertyDetails) 0x0000005f381d2c0c
[libjsvm.so] v8::internal::Handle<v8::internal::NameDictionary> v8::internal::Dictionary<v8::internal::NameDictionary, v8::internal::NameDictionaryShape>::Add<v8::internal::Isolate, (v8::internal::AllocationType)0>(v8::internal::Isolate*, v8::internal::Handle<v8::internal::NameDictionary>, v8::internal::Handle<v8::internal::Name>, v8::internal::Handle<v8::internal::Object>, v8::internal::PropertyDetails, v8::internal::InternalIndex*) 0x0000005f381d32dc
[libjsvm.so] v8::internal::BaseNameDictionary<v8::internal::NameDictionary, v8::internal::NameDictionaryShape>::Add(v8::internal::Isolate*, v8::internal::Handle<v8::internal::NameDictionary>, v8::internal::Handle<v8::internal::Name>, v8::internal::Handle<v8::internal::Object>, v8::internal::PropertyDetails, v8::internal::InternalIndex*) 0x0000005f381d2e64
[libjsvm.so] v8::internal::JSObject::MigrateToMap(v8::internal::Isolate*, v8::internal::Handle<v8::internal::JSObject>, v8::internal::Handle<v8::internal::Map>, int) 0x0000005f3815bd74
[libjsvm.so] v8::internal::JSObject::OptimizeAsPrototype(v8::internal::Handle<v8::internal::JSObject>, bool) 0x0000005f3815fa68
[libjsvm.so] v8::internal::Map::SetPrototype(v8::internal::Isolate*, v8::internal::Handle<v8::internal::Map>, v8::internal::Handle<v8::internal::HeapObject>, bool) 0x0000005f381b389c
[libjsvm.so] v8::internal::JSFunction::SetInitialMap(v8::internal::Isolate*, v8::internal::Handle<v8::internal::JSFunction>, v8::internal::Handle<v8::internal::Map>, v8::internal::Handle<v8::internal::HeapObject>, v8::internal::Handle<v8::internal::HeapObject>) 0x0000005f3814a2a4
[libjsvm.so] v8::internal::ApiNatives::CreateApiFunction(v8::internal::Isolate*, v8::internal::Handle<v8::internal::NativeContext>, v8::internal::Handle<v8::internal::FunctionTemplateInfo>, v8::internal::Handle<v8::internal::Object>, v8::internal::InstanceType, v8::internal::MaybeHandle<v8::internal::Name>) 0x0000005f37d14208
[libjsvm.so] v8::internal::(anonymous namespace)::InstantiateFunction(v8::internal::Isolate*, v8::internal::Handle<v8::internal::NativeContext>, v8::internal::Handle<v8::internal::FunctionTemplateInfo>, v8::internal::MaybeHandle<v8::internal::Name>) 0x0000005f37d122ac
[libjsvm.so] v8::internal::ApiNatives::InstantiateFunction(v8::internal::Handle<v8::internal::FunctionTemplateInfo>, v8::internal::MaybeHandle<v8::internal::Name>) 0x0000005f37d12c4c
[libjsvm.so] v8::FunctionTemplate::GetFunction(v8::Local<v8::Context>) 0x0000005f37d318a4
[libjsvm.so] v8::Function::New(v8::Local<v8::Context>, void (*)(v8::FunctionCallbackInfo<v8::Value> const&), v8::Local<v8::Value>, int, v8::ConstructorBehavior, v8::SideEffectType) 0x0000005f37d317e0
[libjsvm.so] OH_JSVM_CreateFunction 0x0000005f37569780

定位

坏的MarkingBarrier

复现崩溃: image.png 可以看到是在指令ldr     x20, [x23, #0x8]出现的崩溃,打印x23,发现x23是一个非法指针0xbebebebebebebebe。

结合上句ldr     x23, [x0, #0x50],可知x23是x0偏移0x50的属性。根据c++的编译规则,x0是 MarkingBarrier::WhiteToGreyAndPush 的第一个参数,指向this,也就是 MarkingBarrier。

对 MarkingBarrier::WhiteToGreyAndPush 打断点,查看正常情况下 MarkingBarrier 内容:

image.png

image.png

同上文,x0是第一个参数,也就是this,在WhiteToGreyAndPush里指当前的MarkingBarrier。在memory view中查看MarkingBarrier(0x0000006185bc3640)内容:

image.png 可以看到当前MarkingBarrier偏移0x50值是正常的,所以这次运行不会有问题。这里我们设置个watchpoint,观察MarkingBarrier偏移0x50值是否什么时候被改坏了:

(lldb) watchpoint set expression 0x0000006185bc3690
Watchpoint created: Watchpoint 1: addr = 0x6185bc3690 size = 8 state = enabled type = w
    new value: 418855532128

断点位置 = 0x0000006185bc3640(MarkingBarrier) + 0x50 = 0x0000006185bc3690

可以看到0x0000006185bc3690当前的值是 418855532128,换算成16进制是0x6185bc3660,也和我们在memory view中看到的一致。

命中watchpoint后可以看到 image.png

断点上一句str     x12, [x11, #0x50]指令的意思是把x12存储到x11偏移0x50的位置,打印x11、x12:

(lldb) p/x $x12
(unsigned long) $3 = 0x0000006185bc3660

(lldb) p/x $x11
(unsigned long) $4 = 0x0000006185bc3640

可以看到x11指向的就是前面的 MarkingBarrier 对象,x12打印的则跟之前 0x0000006185bc3690 位置值一致,说明这里给它赋了一个跟之前一样的值,不用管它。继续运行后触发了崩溃:

image.png 打印出x0(MarkingBarrier),发现当前的 MarkingBarrier 地址是 0x00000061860d8f40,跟之前我们断点到的 0x0000006185bc3640 是两个对象。查看0x00000061860d8f40内存:

image.png 可以发现后面的 MarkingBarrier 偏移0x50是坏的,也就是 MarkingBarrier 不是被改坏的,而是被赋值了一个坏的

MarkingBarrier来源

下载v8源码:github.com/v8/v8,这里我用的… 12.0.46版本。

查看崩溃堆栈二中的 Dictionary::SetEntry 源码,尝试寻找MarkingBarrier来源:

template <typename Derived, typename Shape>
void Dictionary<Derived, Shape>::SetEntry(InternalIndex entry,
                                          Tagged<Object> key,
                                          Tagged<Object> value,
                                          PropertyDetails details) {
  DCHECK(Dictionary::kEntrySize == 2 || Dictionary::kEntrySize == 3);
  DCHECK(!IsName(key) || details.dictionary_index() > 0);
  int index = DerivedHashTable::EntryToIndex(entry);
  DisallowGarbageCollection no_gc;
  WriteBarrierMode mode = this->GetWriteBarrierMode(no_gc);
  this->set(index + Derived::kEntryKeyIndex, key, mode);
  this->set(index + Derived::kEntryValueIndex, value, mode);
  if (Shape::kHasDetails) DetailsAtPut(entry, details);
}

其中this->set方法实现如下(Dictionary继承链路:Dictionary -> HashTable -> HashTableBase -> FixedArray):

void FixedArray::set(int index, Tagged<Object> value, WriteBarrierMode mode) {
  DCHECK_NE(map(), GetReadOnlyRoots().fixed_cow_array_map());
  DCHECK_LT(static_cast<unsigned>(index), static_cast<unsigned>(length()));
  int offset = OffsetOfElementAt(index);
  RELAXED_WRITE_FIELD(*this, offset, value);
  CONDITIONAL_WRITE_BARRIER(*this, offset, value, mode);
}

CONDITIONAL_WRITE_BARRIER是个宏定义,定义如下:

#define CONDITIONAL_WRITE_BARRIER(object, offset, value, mode)             \
  do {                                                                     \
    DCHECK_NOT_NULL(GetHeapFromWritableObject(object));                    \
    CombinedWriteBarrier(object, (object)->RawField(offset), value, mode); \
  } while (false)
#endif

CombinedWriteBarrier及相关函数的关键实现如下:

inline void CombinedWriteBarrier(Tagged<HeapObject> host, MaybeObjectSlot slot,
                                 MaybeObject value, WriteBarrierMode mode) {
  ...
  heap_internals::CombinedWriteBarrierInternal(host, HeapObjectSlot(slot), value_object, mode);
}

inline void CombinedWriteBarrierInternal(Tagged<HeapObject> host,
                                         HeapObjectSlot slot,
                                         Tagged<HeapObject> value,
                                         WriteBarrierMode mode) {
...
  if (V8_UNLIKELY(is_marking)) {
    WriteBarrier::MarkingSlow(host, HeapObjectSlot(slot), value);
  }
}

void WriteBarrier::MarkingSlow(Tagged<HeapObject> host, HeapObjectSlot slot,
                               Tagged<HeapObject> value) {
  MarkingBarrier* marking_barrier = CurrentMarkingBarrier(host);
  marking_barrier->Write(host, slot, value);
}

可以看到是通过 CurrentMarkingBarrier 方法取的MarkingBarrier对象,再看看CurrentMarkingBarrier实现:

namespace {
thread_local MarkingBarrier* current_marking_barrier = nullptr;
}  // namespace

MarkingBarrier* WriteBarrier::CurrentMarkingBarrier(
    Tagged<HeapObject> verification_candidate) {
  MarkingBarrier* marking_barrier = current_marking_barrier;
  DCHECK_NOT_NULL(marking_barrier);
  ...
  return marking_barrier;
}

这里用了 thread_local 保存 MarkingBarrier,也就是 current_marking_barrier 指针被修改指向了坏的MarkingBarrier,导致了上文的崩溃。寻找current_marking_barrier赋值逻辑:

MarkingBarrier* WriteBarrier::SetForThread(MarkingBarrier* marking_barrier) {
  MarkingBarrier* existing = current_marking_barrier;
  current_marking_barrier = marking_barrier;
  return existing;
}

对WriteBarrier::SetForThread打断点:

image.png

因为崩溃是在主线程,有很多非主线程的current_marking_barrier修改这里不用管,继续运行,直到触发主线程的 current_marking_barrier 修改:

image.png 这里没展示堆栈,我们可以用register read查看lr寄存器的值:

(lldb) register read
General Purpose Registers:
        x0 = 0x0000000000000000
        x1 = 0x0000000000000020
        x2 = 0x0000000000000020
        x3 = 0x0000005aeb606a40  ld-musl-aarch64.so.1`memset
        x4 = 0x0000005cee8a7f30
        x5 = 0x0000000000000001
        x6 = 0x0000007fddc54000
        x7 = 0x0000000000000001
        x8 = 0x0000000000000000
        x9 = 0x0000000000000000
       x10 = 0x0000005aeb606a40  ld-musl-aarch64.so.1`memset
       x11 = 0x000000000b18d225
       x12 = 0xffffffffffffffff
       x13 = 0x0000000000000000
       x14 = 0x0000007fffffffff
       x15 = 0x0000007fffffffff
       x16 = 0x0000005aecaf4fd8  
       x17 = 0x0000005aeb71d450  ld-musl-aarch64.so.1`tss_get
       x18 = 0xffff000000000006
       x19 = 0x0000007b6f867800
       x20 = 0x0000005cee8a7e50
       x21 = 0x0000007b6f874c40
       x22 = 0x0000005cf0298650
       x23 = 0x0000007b76a68800
       x24 = 0x0000000000000000
       x25 = 0x0000005aeb96c570  ld-musl-aarch64.so.1`ohos_malloc_hook_shared_library
       x26 = 0x0000000000000007
       x27 = 0x0000007b714fb7e8  libace_napi.z.so`ArkNativeEngine::napiProfilerEnabled
       x28 = 0x0000007fde44d3a0
        fp = 0x0000007fde44a880
        lr = 0x0000007c95cc05a8  libjsvm.so`v8::internal::Isolate::Enter() + 232
        sp = 0x0000007fde44a880
        pc = 0x0000007c95d2c2ac  libjsvm.so`v8::internal::WriteBarrier::SetForThread(v8::internal::MarkingBarrier*)
      cpsr = 0x20001000

继续运行,lr寄存器分别收集到下面的调用者:

libjsvm.so`v8::internal::Isolate::Enter() + 232

libjsvm.so`v8::internal::Isolate::Init(v8::internal::SnapshotData*, v8::internal::SnapshotData*, v8::internal::Snapshot

libjsvm.so`v8::Isolate::Initialize(v8::Isolate*, v8::Isolate::CreateParams const&) + 496

libjsvm.so`OH_JSVM_CreateVM + 332

除了v8的Isolate逻辑,这里出现一个熟悉的调用者OH_JSVM_CreateVM。OH_JSVM_CreateVM完整调用堆栈如下:

    [libjsvm.so] v8::internal::WriteBarrier::SetForThread(v8::internal::MarkingBarrier*) Disassembly:401
    [??] ?? 0x004f007b25fa494c(OH_JSVM_CreateVM)
    [libmylib.so] MyEngineInstance::MyEngineInstance(napi_env__*, napi_value__*, napi_value__*, std::__n1::basic_string<char, std::__n1::char_traits<char>, std::__n1::allocator<char>> const&, unsigned long const&, std::__n1::basic_string<char, std::__n1::char_traits<char>, std::__n1::allocator<char>> const&) MyEngineInstance.cpp:686
    [libmylib.so] std::__n1::__shared_ptr_emplace<MyEngineInstance, std::__n1::allocator<MyEngineInstance>>::__shared_ptr_emplace[abi:v15004]<napi_env__*&, napi_value__*&, napi_value__*&, char const (&) [1], unsigned long&, std::__n1::basic_string<char, std::__n1::char_traits<char>, std::__n1::allocator<char>>&>(std::__n1::allocator<MyEngineInstance>, napi_env__*&, napi_value__*&, napi_value__*&, char const (&) [1], unsigned long&, std::__n1::basic_string<char, std::__n1::char_traits<char>, std::__n1::allocator<char>>&) shared_ptr.h:294
    [libmylib.so] std::__n1::shared_ptr<MyEngineInstance> std::__n1::allocate_shared[abi:v15004]<MyEngineInstance, std::__n1::allocator<MyEngineInstance>, napi_env__*&, napi_value__*&, napi_value__*&, char const (&) [1], unsigned long&, std::__n1::basic_string<char, std::__n1::char_traits<char>, std::__n1::allocator<char>>&, void>(std::__n1::allocator<MyEngineInstance> const&, napi_env__*&, napi_value__*&, napi_value__*&, char const (&) [1], unsigned long&, std::__n1::basic_string<char, std::__n1::char_traits<char>, std::__n1::allocator<char>>&) shared_ptr.h:953
    [libmylib.so] std::__n1::shared_ptr<MyEngineInstance> std::__n1::make_shared[abi:v15004]<MyEngineInstance, napi_env__*&, napi_value__*&, napi_value__*&, char const (&) [1], unsigned long&, std::__n1::basic_string<char, std::__n1::char_traits<char>, std::__n1::allocator<char>>&, void>(napi_env__*&, napi_value__*&, napi_value__*&, char const (&) [1], unsigned long&, std::__n1::basic_string<char, std::__n1::char_traits<char>, std::__n1::allocator<char>>&) shared_ptr.h:962
    [libmylib.so] createMyEngine(napi_env__*, napi_callback_info__*) napi_init.cpp:82
    [libace_napi.z.so] panda::JSValueRef ArkNativeFunctionCallBack<true>(panda::JsiRuntimeCallInfo*) 0x0000007a07ebdebc
    [JIT(0x777c880400)] RTStub_PushCallRangeAndDispatchNative 0x0000007a1c874eb0
    [JIT(0x777c880400)] BCStubInterpreterRoutine 0x0000007a1c48c6bc

这里createMyEngine是在新建MyEngineInstance,同时会创建新的jsvm vm。新的jsvm vm会有自己的Isolate,修改了 thread_local 的 current_marking_barrier 指向。主线程同时运行多个jsvm vm时,它们共享一个 current_marking_barrier,从而引发了问题。

解决方案

使用多线程方案,每个线程只运行一个JSVM vm。JSVM套壳V8,从分析结果看V8就无法做到同线程运行多vm实例。

Swift Concurrency 中的 Threads 与 Tasks

作者 CodingFisher
2025年9月23日 09:56

Swift Concurrency 中的 Threads 与 Tasks

Swift Concurrency 的引入彻底改变了我们编写异步代码的方式。它用更抽象、更安全的任务(Task)模型替代了传统的直接线程管理,旨在提高性能、减少错误并简化代码。理解线程(Threads)和任务(Tasks)之间的区别,是掌握现代 Swift 并发编程的关键。

1. 线程(Threads):系统级资源

线程是操作系统能够进行运算调度的最小单位。它包含在进程之中,是进程中的实际运作单位。

1.1 线程的特点

  • 系统资源:线程由操作系统内核管理和调度,创建、销毁和上下文切换开销较大。

  • 并发执行:多线程允许程序中的多个操作并发(Concurrently) 执行, potentially improving performance on multi-core systems。

  • 传统痛点

  • 高内存开销:每个线程都需要分配独立的栈空间等内存资源。

  • 上下文切换成本:当线程数量超过 CPU 核心数时,操作系统需要频繁切换线程,消耗大量 CPU 资源。

  • 优先级反转(Priority Inversion):低优先级任务可能阻塞高优先级任务的执行。

  • 线程爆炸(Thread Explosion):过度创建线程会导致系统资源耗尽、性能急剧下降甚至崩溃。

在 Grand Central Dispatch (GCD) 时代,开发者需要显式地将任务分发到主队列或全局后台队列,并时刻警惕这些线程管理问题。

2. 任务(Tasks):更高层次的抽象

Swift Concurrency 引入了 任务(Task) 作为执行异步工作的基本单位。一个任务代表一段可以异步执行的代码。

2.1 任务的特点

  • 异步工作单元:一个 Task 封装了一段异步操作的逻辑。

  • 不绑定特定线程:Task 被提交到 Swift 的协作式线程池(Cooperative Thread Pool) 中执行,由运行时系统动态地分配到任何可用的线程上,而不是绑定到某个特定线程。

  • 结构化并发:Task 提供了结构化的生命周期管理,包括取消、优先级和错误传播。子任务会继承父任务的优先级和上下文,并确保在其父任务完成之前完成。

  • 挂起与恢复:Task 可以在 await 关键字标记的挂起点(Suspension Point) 挂起,释放当前线程以供其他任务使用,并在异步操作完成后在某个线程上恢复执行(很可能不是原来的线程)。

2.2 任务的创建方式

Swift Concurrency 提供了几种创建任务的方式:

  1. Task 初始化器:最常用的方式,用于在非异步上下文中启动一个新的异步任务。

Task {

// 这里是异步上下文

let result = await someAsyncFunction()

print(result)

}

  1. async let 绑定:允许同时启动多个异步操作,并稍后等待它们的结果。

func fetchMultipleData() async {

async let data1 = fetchData(from: url1)

async let data2 = fetchData(from: url2)

// 两个请求同时进行

let results = await (data1, data2) // 等待两者完成

}

  1. 任务组(Task Group):用于动态创建一组并发的子任务,并等待所有子任务完成。

func processImages(from urls: [URL]) async throws -> [Image] {

try await withThrowingTaskGroup(of: Image.self) { group in

for url in urls {

group.addTask { try await downloadAndProcessImage(from: url) }

}

// 收集所有子任务的结果

return await group.reduce(into: []) { $0.append($1) }

}

}

3. Swift 的协作式线程池(Cooperative Thread Pool)

Swift Concurrency 的高效核心在于其协作式线程池

3.1 工作原理

  • 线程数量固定:线程池创建的线程数量通常与当前设备的 CPU 物理核心数相同(例如,iPhone 16 Pro 是 6 核,则线程池大小约为 6)。这避免了过度创建线程。

  • 协作而非抢占:线程池中的线程不会像传统线程那样被操作系统强制抢占式调度。相反,任务需要主动协作(Cooperate),在适当的时机(即 await 挂起点)主动挂起,释放线程给其他任务使用。

  • 高效调度:运行时系统负责将大量的 Task 高效地调度到数量有限的线程上执行。当一个任务在 await 处挂起时,线程不会空等,而是立刻去执行其他已经就绪的任务。

3.2 挂起与恢复(Suspension and Resumption)

这是理解 Swift Concurrency 非阻塞特性的关键。


struct ThreadingDemonstrator {

private func firstTask() async throws {

print("Task 1 started on thread: \(Thread.current)")

try await Task.sleep(for: .seconds(2)) // 🛑 挂起点

print("Task 1 resumed on thread: \(Thread.current)")

}

  


private func secondTask() async {

print("Task 2 started on thread: \(Thread.current)")

}

  


func demonstrate() {

Task {

try await firstTask()

}

Task {

await secondTask()

}

}

}

可能的输出


Task 1 started on thread: <NSThread: 0x600001752200>{number = 3, name = (null)}

Task 2 started on thread: <NSThread: 0x6000017b03c0>{number = 8, name = (null)}

Task 1 resumed on thread: <NSThread: 0x60000176ecc0>{number = 7, name = (null)}

解读输出

  1. Task 1 开始在线程 3 上执行。

  2. 遇到 await Task.sleep时,Task 1 被挂起线程 3 被释放

  3. 运行时系统调度 Task 2 开始执行,它可能被分配到空闲的线程 8 上。

  4. 2 秒后,Task 1 的睡眠结束,变为就绪状态。运行时系统安排它恢复执行,但可能分配到了另一个空闲的线程 7 上。

这个过程完美展示了 Task 与 Thread 的“多对一”关系以及挂起/恢复机制如何实现线程的高效复用。

4. 与 Grand Central Dispatch (GCD) 的对比

虽然 GCD 非常强大且成熟,但 Swift Concurrency 在其基础上提供了更现代的抽象。

| 方面 | Grand Central Dispatch (GCD) | Swift Concurrency |

| :------------------ | :------------------------------------------------------------- | :-------------------------------------------------------------- |

| 抽象核心 | 队列(DispatchQueue) | 任务(Task) |

| 线程模型 | 动态创建线程,数量可能远超过 CPU 核心数,可能导致线程爆炸。 | 协作式线程池,线程数 ≈ CPU 核心数,从根本上避免线程爆炸。 |

| 阻塞与挂起 | 提交到队列的 Block 会阻塞底层线程(如果内部执行同步操作)。 | 在 await挂起任务释放底层线程,不会阻塞。 |

| 性能 | 优秀,但线程过多时上下文切换开销大。 | 更优,极少的线程处理大量任务,减少上下文切换,CPU 更高效。 |

| 语法与可读性 | 基于闭包的回调,嵌套地狱(Callback Hell)风险。 | 线性化的 async/await 语法,代码更清晰、更易读。 |

| 状态管理 | 需要手动处理引用循环([weak self])。 | 结构化并发减少了循环引用风险。 |

| 安全性 | 需要开发者自己避免数据竞争(Data Race)。 | 通过 ActorSendable 协议在编译时提供数据竞争安全。 |

4.1 性能对比:线程更少,性能更好?

这听起来有悖常理,但却是事实。GCD 的线程爆炸问题会导致内存压力增大和大量的上下文切换,反而消耗了 CPU 资源,使得真正用于执行任务的 CPU 周期减少。

Swift Concurrency 的协作式模型通过以下方式提升效率:

  • Continuations:挂起任务时,其状态(局部变量、执行位置等)被保存为一个 Continuation 对象。线程本身被释放,可以立即去执行其他任务。这比传统的线程阻塞和唤醒要轻量得多。

  • 始终前进:线程池中的线程几乎总是在执行有效工作,而不是空转或忙于切换。这使得单位时间内可以完成更多工作。

5. 常见误区与澄清

在从 GCD 转向 Swift Concurrency 时,需要扭转一些“线程思维”。

| 误区 | 正解 |

| :---------------------------------------- | :------------------------------------------------------------------------------------------------ |

| 每个 Task 都会创建一个新线程 | Task 与线程是多对一的关系。大量 Task 共享一个小的线程池。 |

| await 会阻塞当前线程 | await挂起当前 Task,并释放当前线程供其他 Task 使用。这是非阻塞的。 |

| Task 会按创建顺序执行 | Task 的执行顺序没有保证,取决于运行时系统的调度策略、优先级和挂起点。 |

| 必须在主线程上更新 UI | ✅ 正确。但在 Swift Concurrency 中,更推荐使用 @MainActor 来隔离 UI 相关代码,而不是手动派发到主队列。 |

6. 从“线程思维”到“任务思维”

开发者需要实现一个思维转变:

| 线程思维 (GCD Mindset) | 任务思维 (Task Mindset) |

| :----------------------------------------- | :------------------------------------------------------ |

| “这段重计算要放到后台线程。” | “这段计算是个异步任务,系统会帮我调度。” |

| “完成后需要手动派发回主线程更新 UI。” | “用 @MainActor 标记这个函数,确保它在主线程运行。” |

| “创建太多并发队列会不会导致线程爆炸?” | “线程数量由系统自动管理,我只需专注业务逻辑和创建合理的 Task。” |

7. 实践中的差异:Thread.sleep 与 Task.sleep

这个例子能深刻体现阻塞与挂起的区别。

  • Thread.sleep(forTimeInterval:):这是一个阻塞式调用。它会使当前所在的线程停止工作指定的时间。如果这个线程是协作线程池中的一员,它就相当于被“卡住了”,无法为其他任务服务,减少了有效工作线程数。

  • Task.sleep(for:):这是一个非阻塞式挂起。它会使当前 Task 挂起指定的时间,但当前任务所占用的线程会立刻被释放,并返回线程池中为其他就绪的 Task 服务。时间到后,Task 会被重新调度到某个可用线程上恢复执行。

结论:在 Swift Concurrency 中,绝对不要使用 Thread.sleep,它会破坏协作模型。始终使用 Task.sleep

8. 如何选择:Swift Concurrency 还是 GCD?

尽管 Swift Concurrency 更现代,但 GCD 仍有其价值。

  • 使用 Swift Concurrency (Task) 当:

  • 项目基于 Swift 5.5+。

  • 想要更安全、更易读的异步代码(async/await)。

  • 希望获得更好的性能并避免线程问题。

  • 需要利用 Actor 等数据竞争安全特性。

  • 使用 Grand Central Dispatch (GCD) 当:

  • 维护旧的、大规模使用 GCD 的代码库,迁移成本高。

  • 需要进行非常底层的线程控制(虽然绝大多数场景不需要)。

  • 与某些高度依赖 GCD 的 C API 或旧框架交互。

混合使用:在实际项目中,两者可以共存。你可以在 Swift Concurrency 的 Task 内部使用 DispatchQueue 进行特定的操作,但要注意避免不必要的线程跳跃和性能损耗。

9. 深入底层:任务、作业与执行器(Tasks, Jobs, Executors)

为了更深入地理解,可以了解一些运行时概念:

  • 作业 (Job):任务是比 Task 更小的执行单位。一个 Task 在编译时会被分解成多个连续的 Job。每个 Job 是一个同步执行的代码块,位于两个 await 挂起点之间。Job 是运行时系统实际调度的单位。

  • 执行器 (Executor):是一个服务,负责接收被调度的 Job 并安排线程来执行它。系统提供了全局的并发执行器(负责一般任务)和主执行器(负责 @MainActor 任务)。开发者通常不需要直接与之交互。

总结

Swift Concurrency 中的 ThreadsTasks 是不同层次的概念:

  • Thread系统级的底层资源,由操作系统管理,创建和切换开销大。Swift Concurrency 建立在线程之上,但开发者不再需要直接与之交互。

  • Task语言级的高层抽象,代表一个异步工作单元。它帮助开发者摆脱繁琐且易错的线程管理,专注于业务逻辑。

Swift Concurrency 的核心优势在于其协作式线程池模型和挂起/恢复机制。它通过以下方式实现高效并发:

  1. 限制线程数量(与 CPU 核心数一致),避免线程爆炸。

  2. 使用 await 作为挂起点,任务在此主动释放线程,实现非阻塞。

  3. 利用 Continuations 保存挂起状态,实现任务在不同线程上的恢复。

  4. 通过 Actor 和结构化并发提供编译期的数据竞争安全

最终,开发者应从“线程思维”转向“任务思维”,信任运行时系统会做出最优的线程调度决策,从而编写出更清晰、更安全、更高效的高并发代码。

原文:xuanhu.info/projects/it…

Flutter DataGrid,如何进行数据分组更简单?

作者 CodingFisher
2025年9月24日 09:52

原文:xuanhu.info/projects/it…

Flutter DataGrid,如何进行数据分组更简单?

在现代移动应用开发中,数据表格是展示结构化信息的重要组件。Syncfusion 的 Flutter DataGrid(SfDataGrid)提供了强大的数据分组功能,允许开发者根据特定条件对数据进行组织和分类,形成层次化结构,极大提升了数据的可读性和用户体验。

数据分组的核心概念与价值

数据分组是一种将具有相同特征或特定字段值的数据记录组织在一起的技术。在 SfDataGrid 中,分组功能可以:

  • 提高数据可读性:通过将相关记录归类,减少视觉混乱

  • 增强数据分析能力:用户可以快速识别模式和趋势

  • 优化用户交互:支持展开/折叠操作,便于导航大量数据

  • 提供摘要信息:每个分组可以显示汇总数据,如计数、求和等

当启用分组功能时,SfDataGrid 会为每个分组创建一个标题摘要行(CaptionSummaryRow),显示在该组的顶部,包含该组的标题摘要值。

实现数据分组的基本步骤

1. 创建数据模型

首先定义数据模型,这是绑定到 DataGrid 的基础:


class Employee {

Employee(this.id, this.name, this.designation, this.salary);

final int id;

final String name;

final String designation;

final int salary;

}

2. 准备数据源

创建数据集合并在 initState() 中初始化:


List<Employee> employees = <Employee>[];

  


@override

void initState() {

super.initState();

employees = getEmployeeData();

// 其他初始化代码

}

3. 创建 DataGridSource

DataGridSource 是 SfDataGrid 和数据模型之间的桥梁,负责提供数据和处理分组逻辑:


class EmployeeDataSource extends DataGridSource {

EmployeeDataSource({required List<Employee> employeeData}) {

dataGridRows = employeeData

.map<DataGridRow>((e) => DataGridRow(cells: [

DataGridCell<int>(columnName: 'ID', value: e.id),

DataGridCell<String>(columnName: 'Name', value: e.name),

DataGridCell<String>(columnName: 'Designation', value: e.designation),

DataGridCell<double>(columnName: 'Salary', value: e.salary),

]))

.toList();

}

List<DataGridRow> dataGridRows = [];

@override

List<DataGridRow> get rows => dataGridRows;

@override

DataGridRowAdapter buildRow(DataGridRow row) {

return DataGridRowAdapter(

cells: row.getCells().map<Widget>((e) {

return Container(

alignment: Alignment.center,

padding: EdgeInsets.all(8),

child: Text(e.value.toString()),

);

}).toList());

}

}

4. 启用分组功能

通过 DataGridSource 的 addColumnGroup 方法启用列分组:


@override

void initState() {

super.initState();

employees = getEmployeeData();

employeeDataSource = EmployeeDataSource(employeeData: employees);

// 添加列分组 - 按职位分组并排序

employeeDataSource.addColumnGroup(

ColumnGroup(name: 'Designation', sortGroupRows: true)

);

}

ColumnGroup 对象包含两个主要属性:

  • name: 要分组的 GridColumn 的列名

  • sortGroupRows: 确定是否按升序对分组列进行排序

5. 配置 SfDataGrid 组件

在 UI 中配置 SfDataGrid 组件:


@override

Widget build(BuildContext context) {

return Scaffold(

appBar: AppBar(title: const Text('员工数据表')),

body: SfDataGrid(

source: employeeDataSource,

allowExpandCollapseGroup: true, // 允许展开/折叠分组

columns: <GridColumn>[

GridColumn(

columnName: 'ID',

label: Container(

padding: EdgeInsets.all(8),

alignment: Alignment.center,

child: Text('ID')

)

),

GridColumn(

columnName: 'Name',

label: Container(child: Text('姓名'))

),

GridColumn(

columnName: 'Designation',

label: Container(child: Text('职位', overflow: TextOverflow.ellipsis))

),

GridColumn(

columnName: 'Salary',

label: Container(child: Text('薪资'))

),

]

)

);

}

自定义分组标题显示

默认情况下,DataGrid 不会显示分组的标题摘要值。要显示这些值,需要重写 DataGridSource.buildGroupCaptionCellWidget 方法:


@override

Widget? buildGroupCaptionCellWidget(

RowColumnIndex rowColumnIndex, String summaryValue) {

return Container(

padding: EdgeInsets.symmetric(horizontal: 12, vertical: 15),

child: Text(summaryValue) // 显示分组摘要值

);

}

此方法接收标题摘要值作为参数,并返回包含摘要值的 widget。

分组操作管理

添加分组

如前面所示,使用 addColumnGroup 方法添加分组:


employeeDataSource.addColumnGroup(

ColumnGroup(name: 'Salary', sortGroupRows: false)

);

移除分组

要移除特定列的分组,使用 removeColumnGroup 方法:


ColumnGroup? group = employeeDataSource.groupedColumns

.firstWhereOrNull((element) => element.name == 'Salary');

if (group != null) {

employeeDataSource.removeColumnGroup(group);

}

清除所有分组

要一次性清除所有分组,调用 clearColumnGroups 方法:


employeeDataSource.clearColumnGroups();

多级分组实现

SfDataGrid 支持对多个列进行分组,创建层次化的树状结构。数据首先根据添加到 DataGridSource.addColumnGroup 属性的第一列进行分组,随后添加的每个新列都会在现有分组的基础上进行进一步分组。


@override

void initState() {

super.initState();

employees = getEmployeeData();

employeeDataSource = EmployeeDataSource(employeeData: employees);

// 添加多级分组

employeeDataSource.addColumnGroup(ColumnGroup(name: 'Designation', sortGroupRows: true));

employeeDataSource.addColumnGroup(ColumnGroup(name: 'Salary', sortGroupRows: false));

}

这种多级分组功能使您可以创建复杂的数据层次结构,例如先按部门分组,然后在每个部门内按薪资范围分组。

分组交互回调

SfDataGrid 提供了多个回调函数来通知分组的不同阶段:

GroupExpanding 回调

当分组即将展开时触发,可以返回 false 来阻止展开操作:


SfDataGrid(

source: employeeDataSource,

groupExpanding: (group) {

print('分组展开: ${group.key}');

print('分组级别: ${group.groupLevel}');

return true; // 返回 true 允许展开,false 阻止展开

},

// 其他配置...

)

GroupExpanded 回调

当分组完全展开后触发:


SfDataGrid(

source: employeeDataSource,

groupExpanded: (group) {

// 分组已展开后的处理逻辑

},

// 其他配置...

)

GroupCollapsing 回调

当分组即将折叠时触发,可以返回 false 来阻止折叠操作:


SfDataGrid(

source: employeeDataSource,

groupCollapsing: (group) {

return true; // 返回 true 允许折叠,false 阻止折叠

},

// 其他配置...

)

GroupCollapsed 回调

当分组完全折叠后触发:


SfDataGrid(

source: employeeDataSource,

groupCollapsed: (group) {

// 分组已折叠后的处理逻辑

},

// 其他配置...

)

这些回调函数提供了对分组交互的细粒度控制,使您能够根据业务需求实现自定义逻辑。

自定义分组逻辑

当标准分组技术无法满足特定需求时,SfDataGrid 支持使用自定义逻辑对列进行分组。这可以通过重写 DataGridSource.performGrouping 方法来实现。


@override

String performGrouping(String columnName, DataGridRow row) {

if (columnName == 'Salary') {

final double salary = row

.getCells()

.firstWhereOrNull(

(DataGridCell cell) => cell.columnName == columnName)!

.value;

// 自定义薪资分组逻辑

if (salary > 100000) {

return '高薪资';

} else if (salary > 50000) {

return '中等薪资';

} else {

return '一般薪资';

}

}

return super.performGrouping(columnName, row);

}

这种方法特别适用于需要基于复杂条件或计算值进行分组的场景。

启用分组展开/折叠功能

分组展开和折叠功能可以通过将 SfDataGrid.allowExpandCollapseGroup 属性设置为 true 来启用。此属性的默认值为 false。


SfDataGrid(

source: employeeDataSource,

allowExpandCollapseGroup: true, // 启用展开/折叠功能

// 其他配置...

)

解决分组中的常见问题

获取正确的行数据

在使用分组数据时,开发者可能会遇到一个问题:当尝试使用 onSelectionChanged 或 onCellTapped 事件检索行数据时,返回的是分组数据的第二行而不是第一行。

这个问题的原因是 SfDataGrid 的分组功能为每个分组创建了一个摘要行,而这些摘要行被上述事件视为常规数据行。

解决方案:需要在处理 onSelectionChanged 或 onCellTapped 事件中的行数据时,对分组摘要行进行特殊处理:


onSelectionChanged: (details) {

var selectedRows = details.selectedRows;

if (selectedRows.isNotEmpty) {

var firstSelectedRow = selectedRows[0];

if (firstSelectedRow.groupIndex != null) {

// 选择了分组摘要行,找到对应的数据行索引

int dataRowIndex = firstSelectedRow.rowIndex - firstSelectedRow.groupIndex - 1;

// 现在可以安全地从正确的行检索数据

var rowData = _data[dataRowIndex];

} else {

// 正常情况 - 未涉及分组

var rowData = _data[firstSelectedRow.rowIndex];

}

}

}

这种方法确保无论用户选择的是普通数据行还是分组摘要行,都能获取到正确的数据。

高级功能与技巧

自定义分组图标

可以自定义每个分组级别的展开/折叠图标,通过使用 SfDataGrid.CaptionSummaryTemplate 属性实现:


// 示例:自定义分组图标(基于 .NET MAUI 实现,Flutter 类似)

<syncfusion:SfDataGrid.CaptionSummaryTemplate>

<DataTemplate>

<Grid Padding="5" HorizontalOptions="FillAndExpand">

<Grid.ColumnDefinitions>

<ColumnDefinition Width="Auto"></ColumnDefinition>

<ColumnDefinition Width="*"></ColumnDefinition>

</Grid.ColumnDefinitions>

<Label Grid.Column="1" Text="{Binding Converter={StaticResource SummaryConverter}}">

</Label>

<Image Grid.Column="0" WidthRequest="35"

Source="{Binding Converter={StaticResource SummaryIcon}}"

VerticalOptions="Center" HorizontalOptions="End" HeightRequest="20">

</Image>

</Grid>

</DataTemplate>

</syncfusion:SfDataGrid.CaptionSummaryTemplate>

需要相应的转换器来提供不同的图标资源。

多列汇总显示

SfDataGrid 支持在汇总行中显示多个列的摘要信息:


tableSummaryRows: [

GridTableSummaryRow(

showSummaryInRow: false,

columns: [

const GridSummaryColumn(

name: 'Sum',

columnName: 'salary',

summaryType: GridSummaryType.sum),

const GridSummaryColumn(

name: 'Count',

columnName: 'id',

summaryType: GridSummaryType.count),

const GridSummaryColumn(

name: 'AvgExperience',

columnName: 'experience',

summaryType: GridSummaryType.average),

],

position: GridTableSummaryRowPosition.bottom

)

]

然后通过 buildTableSummaryCellWidget 方法自定义摘要显示:


@override

Widget? buildTableSummaryCellWidget(

GridTableSummaryRow summaryRow,

GridSummaryColumn? summaryColumn,

RowColumnIndex rowColumnIndex,

String summaryValue) {

// 列名到摘要标签的映射

final summaryLabels = {

'id': '计数: $summaryValue',

'experience': '平均: $summaryValue',

'salary': '总和: ${formatter.format(double.tryParse(summaryValue))}',

};

// 获取摘要列的文本

String displayText = summaryLabels[summaryColumn?.columnName] ?? '';

return Center(

child: Container(

padding: const EdgeInsets.all(10.0),

child: Text(displayText),

),

);

}

这种方法允许在每个分组中显示多个列的汇总信息,提供更丰富的数据分析能力。

性能优化建议

当处理大量数据时,考虑以下性能优化建议:

  1. 使用分页:对于大型数据集,实现分页机制以提高性能

  2. 延迟加载:只在需要时加载数据,特别是对于展开的分组

  3. 简化构建逻辑:确保 buildRow 和 buildGroupCaptionCellWidget 方法高效执行

  4. 避免频繁操作:减少对分组列的频繁添加和移除操作

实际应用案例:费用跟踪器

一个很好的数据分组实际应用是费用跟踪器应用。在这种应用中,可以按照以下方式组织数据:

  • 按类型分组:收入和支出

  • 按日期范围分组:每日、每周、每月交易

  • 按类别分组:食品、交通、娱乐等


// 示例:费用跟踪器的多级分组

@override

void initState() {

super.initState();

transactions = getTransactionData();

transactionDataSource = TransactionDataSource(transactionData: transactions);

// 添加多级分组

transactionDataSource.addColumnGroup(

ColumnGroup(name: 'Type', sortGroupRows: true));

transactionDataSource.addColumnGroup(

ColumnGroup(name: 'Category', sortGroupRows: true));

transactionDataSource.addColumnGroup(

ColumnGroup(name: 'Date', sortGroupRows: false));

}

这种分组方式使用户能够快速分析他们的消费模式,识别主要支出类别,并监控收入来源。

总结

Syncfusion Flutter DataGrid 提供了强大而灵活的数据分组功能,可以显著增强数据密集型应用程序的可用性和功能性。通过本文的全面指南,您应该能够:

  1. 理解核心概念:掌握数据分组的基本原理和价值主张

  2. 实现基本分组:通过 ColumnGroup 和 DataGridSource 启用基本分组功能

  3. 自定义分组显示:重写 buildGroupCaptionCellWidget 方法自定义分组标题

  4. 管理分组操作:动态添加、移除和清除分组

  5. 实现多级分组:创建层次化的数据分组结构

  6. 处理分组交互:使用分组回调函数实现自定义交互逻辑

  7. 应用自定义分组逻辑:通过 performGrouping 方法实现高级分组需求

  8. 解决常见问题:正确处理分组行中的数据检索问题

  9. 使用高级功能:实现分组图标自定义和多列汇总显示

原文:xuanhu.info/projects/it…

更现代、更安全:Swift Synchronization 框架与 Mutex 锁

作者 CodingFisher
2025年9月24日 09:48

原文:xuanhu.info/projects/it…

更现代、更安全:Swift Synchronization 框架与 Mutex 锁

Swift 6 引入了全新的 Synchronization 框架,其中 Mutex(互斥锁)作为现代锁机制的核心组件,为线程安全的数据访问提供了简洁而高效的解决方案。与传统锁不同,Mutex 强制执行严格的所有权规则:只有获取锁的线程才能释放它。框架提供的 withLock 方法支持安全的可变访问,并无缝集成于 Swift 并发模型之中,因其无条件符合 Sendable 协议。这使得包装非 Sendable 类型变得安全,无需承担 Actor 的开销。虽然 Actor 在异步场景中表现优异,但 Mutex 填补了同步即时访问需求与遗留代码兼容性的空白。

1. Swift 锁机制的演进历程

在深入 Mutex 之前,回顾 Swift 并发处理的发展有助于理解其设计初衷。多线程编程中,锁是基本的同步工具,用于保护大段代码以确保正确性。macOS 和 iOS 提供了基础互斥锁,Foundation 框架还定义了特定场景的变体。

早期方案:NSLock 与 GCD

初始阶段,开发者通常使用 NSLock 或 GCD 串行队列保护共享资源:


// 使用 NSLock

class Counter {

private var value = 0

private var lock = NSLock()

func increment() {

lock.lock()

defer { lock.unlock() }

value += 1

}

}

  


// 使用串行队列

class Counter {

private var value = 0

private let serialQueue = DispatchQueue(label: "com.example.serialQueue")

func increment() {

serialQueue.sync {

value += 1

}

}

}

NSLock 需谨慎处理解锁(尽管 defer 可辅助),而 GCD 队列在某些场景显得笨重。

现代方案:Actor 模型

Swift 5.5 引入 Actor,简化了状态安全管理:


actor Counter {

private var count = 0

func increment() {

count += 1

}

}

Actor 编译器保障了并发安全,但所有方法调用必须异步(需 await),这在同步上下文中可能不便。

Mutex 的诞生

Swift 6 的 Synchronization 框架推出 Mutex,结合了传统锁的简单性与现代 Swift 的安全性:


import Synchronization

  


final class Counter {

private let mutex = Mutex(0) // 包装整型状态

func increment() {

mutex.withLock { value in

value += 1

}

}

func get() -> Int {

mutex.withLock { $0 }

}

}

此 API 无需 async/await,简洁且高性能。

2. Mutex 的设计原理与核心特性

2.1 严格所有权与线程安全

Mutex 遵循互斥锁原则:仅由获取锁的线程释放。这避免了传统锁中潜在的所有权混乱问题。其 withLock 方法签名如下:


func withLock<R>(_ body: (inout sending State) -> sending R) -> sending R

  • inout sending:允许状态在闭包内临时转移至其他隔离域。

  • sending 返回值:确保返回值可安全传递到其他隔离域。

2.2 无缝集成 Swift 并发

作为无条件 Sendable 的类型,Mutex 可安全用于并发环境,即使包装非 Sendable 类型也能通过编译器检查,无需 @unchecked Sendable

2.3 与传统锁的性能对比

测试表明,Mutex 在高并发场景下性能显著优于其他机制。以下是对 1000 万次并发累加操作的性能数据:

| 同步机制 | 耗时(秒) | 相对性能 |

|-------------------------|----------------|--------------|

| Mutex | 3.65 | 100% (基准) |

| OSAllocatedUnfairLock | 4.42 | 83% |

| Actor | 7.51 | 49% |

| NSLock | 8.31 | 44% |

| DispatchQueue | 9.28 | 39% |

MutexActor 快约一倍,使其成为性能敏感场景的理想选择。

3. 如何使用 Mutex

3.1 基础用法

Mutex 初始化时包装一个值,并通过 withLock 安全访问:


import Synchronization

  


class SharedResource {

private let mutex = Mutex()

func setValue(_ key: String, data: Data) {

mutex.withLock { dict in

dict[key] = data

}

}

func getValue(_ key: String) -> Data? {

mutex.withLock { dict in

dict[key]

}

}

}

3.2 保护复杂数据结构

Mutex 适用于各种数据类型,包括复杂结构:


final class ThreadSafeCache<T> {

private let mutex: Mutex<[String: T]>

init() {

mutex = Mutex([:])

}

func update(_ key: String, value: T) {

mutex.withLock { cache in

cache[key] = value

}

}

func removeAll() {

mutex.withLock { cache in

cache.removeAll()

}

}

}

3.3 泛型支持

Mutex 支持泛型,增加灵活性:


final class ThreadSafeBox<T> {

private let mutex: Mutex<T>

init(_ value: T) {

mutex = Mutex(value)

}

func update(_ transform: (inout T) -> Void) {

mutex.withLock { value in

transform(&value)

}

}

func get() -> T {

mutex.withLock { $0 }

}

}

  


// 使用示例

let box = ThreadSafeBox([1, 2, 3])

box.update { array in

array.append(4)

}

let currentArray = box.get() // [1, 2, 3, 4]

4. 避免常见陷阱

4.1 死锁预防

withLock 闭包内再次调用同一 Mutex 会导致死锁:


// ❌ 错误示例:死锁风险

mutex.withLock { value in

// 某些操作...

mutex.withLock { _ in // 💥 死锁!

// 更多操作

}

}

  


// ✅ 正确做法:提取公共逻辑

private func doSomething(_ value: inout Int) {

// 共享逻辑

}

  


func method1() {

mutex.withLock { value in

doSomething(&value)

}

}

  


func method2() {

mutex.withLock { value in

doSomething(&value)

}

}

4.2 值类型注意事项

类似 pthread_mutex_t,Swift 的 Mutex 是值类型,但通过封装避免了传统 C 互斥锁的初始化问题:


// Swift 的 Mutex 无需复杂初始化

let mutex = Mutex(0) // 简单且安全

5. Mutex 与 Actor 的对比选择

5.1 适用场景

  • Mutex 更适合

  • 需要同步 API(避免频繁 await

  • 性能敏感的应用场景

  • 保护简单共享状态

  • 与现有同步代码集成

  • Actor 更适合

  • 复杂的状态管理

  • 需要与 async/await 生态系统深度集成

  • 依赖编译器进行并发安全检查

  • 长时间运行的操作

5.2 性能考量

如前所述,Mutex 在同步操作中性能显著优于 Actor,使其成为需要低延迟访问的场景的首选。

5.3 代码风格差异


// Mutex 方式(同步)

class DataManager {

private let mutex = Mutex(Data())

func processData() {

mutex.withLock { data in

// 立即处理数据

data.transform()

}

}

}

  


// Actor 方式(异步)

actor DataManager {

private var data = Data()

func processData() async {

// 必须 await

data.transform()

}

}

6. 同步框架中的其他工具

6.1 Atomic 操作

除了 MutexSynchronization 框架还提供 Atomic 类型,用于基本类型的原子操作:


import Synchronization

  


let counter = Atomic(0)

  


// 原子增加

counter.add(1, ordering: .relaxed)

  


// 原子读取

let value = counter.load(ordering: .relaxed)

  


// 比较并交换

let exchanged = counter.compareExchange(

expected: 0,

desired: 1,

ordering: .relaxed

)

Atomic 性能优于 Mutex,但仅适用于基本类型简单操作。

6.2 与传统锁的互操作性

Mutex 可与传统锁机制(如 NSLockpthread_mutex_t)共存,便于逐步迁移现有代码base。

7. 实际应用案例

7.1 线程安全缓存实现


final class ThreadSafeImageCache {

private let mutex = Mutex()

private let queue = DispatchQueue(label: "image.cache.queue", attributes: .concurrent)

func image(forKey key: String) -> UIImage? {

mutex.withLock { cache in

cache[key]

}

}

func setImage(_ image: UIImage, forKey key: String) {

mutex.withLock { cache in

cache[key] = image

}

}

func clear() {

mutex.withLock { cache in

cache.removeAll()

}

}

}

7.2 高性能计数器


final class HighPerformanceCounter {

private let mutex = Mutex(0)

func increment() -> Int {

mutex.withLock { value in

value += 1

return value

}

}

func reset() {

mutex.withLock { value in

value = 0

}

}

}

7.3 遗留代码集成


// 传统 Objective-C 兼容代码

class LegacyIntegration {

private var mutex = Mutex(NSMutableDictionary())

func safeUpdate(key: String, value: Any) {

mutex.withLock { dict in

dict[key] = value

}

}

func threadSafeGet(key: String) -> Any? {

mutex.withLock { dict in

dict[key]

}

}

}

8. 性能优化技巧

8.1 减少锁持有时间

尽可能缩短锁的持有时间,提升并发性能:


// ❌ 不佳实践:长时间持有锁

mutex.withLock { data in

let result = performTimeConsumingOperation(data)

updateData(data, with: result)

notifyAllObservers()

}

  


// ✅ 最佳实践:最小化锁范围

let temporaryCopy = mutex.withLock { $0 }

let result = performTimeConsumingOperation(temporaryCopy)

mutex.withLock { data in

updateData(data, with: result)

}

notifyAllObservers() // 在锁外执行通知

8.2 避免锁嵌套

尽量避免锁嵌套,如需多锁,确保固定顺序获取:


// 定义锁获取顺序常量

enum LockOrder {

case first, second

}

  


func safeMultipleLockAccess() {

// 始终按相同顺序获取锁

lock1.withLock {

lock2.withLock {

// 关键区域

}

}

}

9. 调试与测试

9.1 死锁检测

使用 Xcode 的 Thread Sanitizer 检测潜在死锁。配置 Scheme:

  1. 编辑 Scheme

  2. 选择 "Run" 配置

  3. 在 "Diagnostics" 中启用 "Thread Sanitizer"

9.2 单元测试中的 Mutex

测试 Mutex 保护代码时,使用并发测试案例:


func testConcurrentAccess() async {

let counter = ThreadSafeCounter()

let taskCount = 1000

await withTaskGroup(of: Void.self) { group in

for _ in 0..<taskCount {

group.addTask {

counter.increment()

}

}

}

let finalCount = counter.get()

XCTAssertEqual(finalCount, taskCount)

}

10. 迁移策略

10.1 从 NSLock 迁移


// 旧代码

class OldCounter {

private var value = 0

private var lock = NSLock()

func increment() {

lock.lock()

defer { lock.unlock() }

value += 1

}

}

  


// 新代码

class NewCounter {

private let mutex = Mutex(0)

func increment() {

mutex.withLock { value in

value += 1

}

}

}

10.2 从 GCD 迁移


// 旧代码

class GCDCounter {

private var value = 0

private let queue = DispatchQueue(label: "counter.queue")

func increment() {

queue.sync {

value += 1

}

}

}

  


// 新代码

class MutexCounter {

private let mutex = Mutex(0)

func increment() {

mutex.withLock { value in

value += 1

}

}

}

11. 兼容性考虑

11.1 平台可用性

Synchronization 框架要求:

  • iOS 18+

  • macOS 15+

  • tvOS 18+

  • watchOS 11+

  • Swift 6.0+

11.2 向后兼容策略

对于需要支持旧系统的项目,可使用条件编译:


#if canImport(Synchronization)

import Synchronization

  


class ModernCounter {

private let mutex = Mutex(0)

// Mutex 实现

}

#else

class FallbackCounter {

private var value = 0

private let lock = NSLock()

func increment() {

lock.lock()

defer { lock.unlock() }

value += 1

}

}

#endif

12. 总结

Swift 6 的 Synchronization 框架及其 Mutex 类型标志着 Swift 并发编程的重要进化。它提供了传统锁机制的现代替代方案,兼具性能、安全性和易用性。

12.1 关键优势

  1. 卓越性能:在高并发场景下显著优于 Actor 和其他传统锁机制

  2. 线程安全:严格的所有权模型防止常见并发错误

  3. API 简洁withLock 方法提供安全、直观的接口

  4. 无缝集成:与 Swift 并发模型原生兼容,无条件 Sendable

  5. 泛型支持:灵活适用于各种数据类型

12.2 适用场景指南

| 场景 | 推荐方案 | 理由 |

|------------------------|-----------------------|------------------------------------|

| 简单原子操作 | Atomic | 最佳性能,专为基本类型设计 |

| 共享状态保护 | Mutex | 平衡性能与灵活性 |

| 复杂异步逻辑 | Actor | 编译器保障的安全性和集成度 |

| 遗留代码兼容 | NSLock/GCD | 无需迁移现有稳定代码 |

12.3 未来展望

随着 Swift 并发模型的持续发展,Synchronization 框架预计将扩展更多功能:

  • 更多锁变体(读写锁、条件锁等)

  • 增强的调试和检测工具

  • 与硬件特性深度集成的原子操作

Mutex 并非万能解决方案,但是现代 Swift 开发中不可或缺的工具。明智地选择同步机制——在简单保护场景选择 Mutex,复杂异步逻辑选择 Actor,基本原子操作选择 Atomic——将助你构建高效、可靠的并发应用。


总结

Swift 6 的 Synchronization 框架通过引入现代 Mutex 实现,显著提升了同步编程的体验和性能。其严格的所有权模型、无缝的 Swift 并发集成以及优异的性能表现,使其成为共享状态保护的理想选择。虽然 Actor 在复杂异步场景中仍有价值,但 Mutex 在同步访问和性能关键场景中展现出明显优势。开发者应根据具体需求选择合适的工具,结合 Atomic 进行基本操作,以实现最佳并发性能和代码质量。

原文:xuanhu.info/projects/it…

SwiftUI Redux 中子 Reducer 调用其他 Reducer 的方法

作者 littleplayer
2025年9月23日 23:44

SwiftUI Redux 中子 Reducer 调用其他 Reducer 的方法

在 SwiftUI Redux 架构中,有几种常见的方式来实现子 Reducer 之间的通信和调用。

1. 通过 Action 链式调用

这是最符合 Redux 哲学的方式,通过分发 Action 来触发其他 Reducer:

// 定义 Action
enum AppAction {
    case user(UserAction)
    case settings(SettingsAction)
    case crossModuleAction(CrossModuleAction)
}

enum CrossModuleAction {
    case userUpdatedAndNeedSettingsRefresh
}

// 主 Reducer
func appReducer(state: inout AppState, action: AppAction) -> Void {
    switch action {
    case .user(let userAction):
        userReducer(state: &state.user, action: userAction)
        
        // 检查是否需要触发其他模块的 Action
        if case .userProfileUpdated = userAction {
            // 分发跨模块 Action
            return appReducer(state: &state, action: .crossModuleAction(.userUpdatedAndNeedSettingsRefresh))
        }
        
    case .settings(let settingsAction):
        settingsReducer(state: &state.settings, action: settingsAction)
        
    case .crossModuleAction(let crossAction):
        handleCrossModuleAction(state: &state, action: crossAction)
    }
}

// 处理跨模块 Action
func handleCrossModuleAction(state: inout AppState, action: CrossModuleAction) {
    switch action {
    case .userUpdatedAndNeedSettingsRefresh:
        // 调用 settings reducer 的刷新方法
        settingsReducer(state: &state.settings, action: .refreshBasedOnUserChange)
    }
}

2. 使用 Effect 系统(类似 Redux-Observable)

实现一个 Effect 系统来处理副作用和跨 Reducer 调用:

struct Effect<Action> {
    let run: (@escaping (Action) -> Void) -> Void
}

// Reducer 返回 (state, effect) 元组
typealias Reducer<State, Action> = (inout State, Action) -> [Effect<Action>]

func userReducer(state: inout UserState, action: UserAction) -> [Effect<AppAction>] {
    switch action {
    case .loginSuccess:
        state.isLoggedIn = true
        // 返回一个 effect 来触发其他 reducer
        return [Effect { dispatch in
            dispatch(.settings(.loadUserSpecificSettings))
            dispatch(.notifications(.setupForUser)))
        }]
    default:
        return []
    }
}

3. 组合式 Reducer 设计

创建可以相互调用的组合式 Reducer:

protocol Reducible {
    associatedtype State
    associatedtype Action
    static func reduce(state: inout State, action: Action) -> SideEffects<Action>
}

enum SideEffects<Action> {
    case none
    case effect(Action)
    case effects([Action])
    
    func map<T>(_ transform: (Action) -> T) -> SideEffects<T> {
        // 实现 map 逻辑
    }
}

// 用户模块 Reducer
enum UserReducer: Reducible {
    static func reduce(state: inout UserState, action: UserAction) -> SideEffects<AppAction> {
        switch action {
        case .updateProfile:
            state.profile = action.newProfile
            // 返回需要触发的其他 Action
            return .effect(.settings(.updateUserProfile(action.newProfile)))
        default:
            return .none
        }
    }
}

// 主 Reducer 组合器
struct AppReducer {
    static func reduce(state: inout AppState, action: AppAction) {
        let sideEffects: SideEffects<AppAction>
        
        switch action {
        case .user(let userAction):
            sideEffects = UserReducer.reduce(state: &state.user, action: userAction)
            
        case .settings(let settingsAction):
            sideEffects = SettingsReducer.reduce(state: &state.settings, action: settingsAction)
        }
        
        // 处理 side effects
        handleSideEffects(sideEffects, state: &state)
    }
    
    private static func handleSideEffects(_ sideEffects: SideEffects<AppAction>, state: inout AppState) {
        switch sideEffects {
        case .effect(let action):
            reduce(state: &state, action: action)
        case .effects(let actions):
            actions.forEach { reduce(state: &state, action: $0) }
        case .none:
            break
        }
    }
}

4. 使用 Middleware 进行通信

通过 Middleware 来拦截 Action 并触发其他 Action:

struct CrossModuleMiddleware: Middleware {
    typealias State = AppState
    
    func process(state: State, action: AppAction, dispatch: @escaping (AppAction) -> Void) {
        switch action {
        case .user(.logout):
            // 用户登出时,清理所有相关模块
            dispatch(.settings(.resetToDefaults))
            dispatch(.notifications(.clearAll))
            dispatch(.cart(.clearCart))
            
        case .user(.loginSuccess(let user)):
            // 用户登录成功,初始化相关模块
            dispatch(.settings(.loadUserPreferences(user.id)))
            dispatch(.notifications(.registerDeviceToken))
            
        default:
            break
        }
    }
}

// Middleware 协议
protocol Middleware {
    associatedtype State
    func process(state: State, action: AppAction, dispatch: @escaping (AppAction) -> Void)
}

5. 依赖注入式 Reducer

为 Reducer 提供调用其他 Reducer 的能力:

struct ReducerEnvironment {
    let dispatch: (AppAction) -> Void
    let getState: () -> AppState
}

func userReducer(environment: ReducerEnvironment) -> (inout UserState, UserAction) -> Void {
    return { state, action in
        switch action {
        case .purchaseCompleted(let product):
            state.purchasedProducts.append(product)
            // 通过 environment 调用其他 reducer
            environment.dispatch(.settings(.unlockFeature(product.featureId)))
            environment.dispatch(.analytics(.trackPurchase(product)))
        default:
            break
        }
    }
}

6. 实际应用示例

// 完整的应用示例
class Store: ObservableObject {
    @Published private(set) var state: AppState
    private let reducer: (inout AppState, AppAction) -> Void
    private let middlewares: [Middleware]
    
    init(state: AppState, reducer: @escaping (inout AppState, AppAction) -> Void, middlewares: [Middleware]) {
        self.state = state
        self.reducer = reducer
        self.middlewares = middlewares
    }
    
    func dispatch(_ action: AppAction) {
        // 先执行 middleware
        middlewares.forEach { $0.process(state: state, action: action, dispatch: self.dispatch) }
        
        // 执行 reducer
        reducer(&state, action)
    }
}

// 在 SwiftUI 中使用
struct ContentView: View {
    @EnvironmentObject var store: Store
    
    var body: some View {
        Button("Login") {
            store.dispatch(.user(.login(username: "user", password: "pass")))
            // 这会自动触发相关的 settings 和 notifications actions
        }
    }
}

最佳实践建议

  1. 优先使用 Action 链式调用 - 最符合 Redux 单向数据流原则
  2. 对于复杂副作用使用 Effect 系统 - 更适合异步操作和复杂逻辑
  3. 避免直接函数调用 - 保持 Reducer 的纯净性和可测试性
  4. 使用 Middleware 处理横切关注点 - 如日志、分析、错误处理等

选择哪种方式取决于你的应用复杂度和团队偏好。简单的应用可以使用基本的 Action 链式调用,复杂应用可以考虑实现完整的 Effect 系统。

仿swiftUI一步步使用声明式语法创建NSCollectionLayoutGroup

作者 willlzq
2025年9月23日 19:07
image.png

前言

当我们在 SwiftUI 中编写如下代码时,ViewBuilder 的 buildBlock 方法就在幕后工作:

VStack {
    Text("Hello")
    Image(systemName: "star")
    if showDetail {
        Text("Detail view") // 对应 TrueContent
    } else {
        Text("Summary view") // 对应 FalseContent
        Button("Tap me") {}
    }
}

这种声明式语法让界面构建变得如此清晰直观,那么在 Swift 原生语言中是否也可以实现类似的效果呢?答案是肯定的!

在本文中,我将带领大家一步步实现一个仿 SwiftUI 声明式语法的布局库,用于更优雅地创建 NSCollectionLayoutGroup,这个库正是基于 Swift 的三大语法特性实现的。

Swift 声明式语法的三大利器

1. 布局构建器:@resultBuilder

@resultBuilder 是实现声明式语法的核心,它允许我们以更自然的方式组合多个元素,而不需要使用逗号分隔。在 Swift 5.4 之前,它被称为 @_functionBuilder

2. 逃逸闭包:@escaping

通过逃逸闭包,我们可以在函数尾部使用大括号 {} 创建一个结构化的代码块,这使得我们的 API 调用看起来更像 JSON 结构,支持无限嵌套。

3. 链式语法

链式语法允许我们在一行代码中进行多次方法调用,只要每个方法都返回 self 实例。

在这三大利器中,@resultBuilder 是最为基础和核心的,它为整个声明式语法奠定了基础。

核心难点解析

使用 @resultBuilder 实现声明式语法时,如果想要支持 iffor 循环语法,就必须实现 buildEitherbuildArray 方法。这要求我们的核心数据结构必须支持递归包含自己类型的列表。

LayoutBoxConfig 类为例:

public class LayoutBoxConfig {
    // 其他属性...
    private var isExpression: Bool = false
    private var selflist: [LayoutBoxConfig] = []
    
    // 用于构建列表的初始化方法
    public init(list: [LayoutBoxConfig]) {
        self.isExpression = true
        self.selflist = list
    }
    
    // 获取所有子项目的递归方法
    public func subItems() -> [LayoutBoxConfig] {
        var  list:[LayoutBoxConfig] = []
        for item in self.selflist {
            if item.isExpression {
                //使用递归模式,获取所有的子项目
                list.append(contentsOf: item.subItems())
            }else {
                list.append(item)
            }
        }
        return list.count == 0 ? [self] : list
    }
}

这种设计允许我们在 @resultBuilder 中这样处理:

public static func buildArray(_ components: [LayoutBoxConfig]) -> LayoutBoxConfig {
    // 处理 for 循环
    LayoutBoxConfig(list: components)
}

public static func buildEither(first component: LayoutBoxConfig) -> LayoutBoxConfig {
    // 处理 if 分支
    component
}

public static func buildEither(second component: LayoutBoxConfig) -> LayoutBoxConfig {
    // 处理 else 分支
    component
}

实现步骤详解

步骤 1: 定义基础数据结构

首先,我们需要定义一些基础的数据结构来表示布局元素:

/// 布局盒子类型枚举
public enum LayoutBoxType {
    case item // 单个单元格布局
    case group // 组合的布局组
}

/// 布局组方向枚举
public enum GroupDirection {
    case horizontal // 水平方向排列
    case vertical // 垂直方向排列
}

步骤 2: 创建布局配置基类

接下来,创建 LayoutBoxConfig 基类,它是所有布局元素的基础:

@MainActor public class LayoutBoxConfig {
    // 边缘间距类型定义
    public typealias EdgeSpacing = (leading: NSCollectionLayoutSpacing?, 
                                  top: NSCollectionLayoutSpacing?, 
                                  trailing: NSCollectionLayoutSpacing?, 
                                  bottom: NSCollectionLayoutSpacing?)
    
    var boxType: LayoutBoxType = .item
    var itemSize: NSCollectionLayoutSize
    var insets: NSDirectionalEdgeInsets?
    var edges: EdgeSpacing?
    private var isExpression: Bool = false
    private var selflist: [LayoutBoxConfig] = []
    
    // 初始化方法和其他功能...
}

步骤 3: 实现 @resultBuilder

现在,我们来实现核心的 LayoutBuilder 结构体:

@MainActor @resultBuilder
public struct LayoutBuilder {
    public static func buildBlock(_ components: LayoutBoxConfig...) -> LayoutBoxConfig {
        if components.count == 1 {
            components.first!
        } else {
            LayoutBoxConfig(list: components)
        }
    }
    
    public static func buildEither(first component: LayoutBoxConfig) -> LayoutBoxConfig {
        component
    }
    
    public static func buildEither(second component: LayoutBoxConfig) -> LayoutBoxConfig {
        component
    }
    
    public static func buildArray(_ components: [LayoutBoxConfig]) -> LayoutBoxConfig {
        LayoutBoxConfig(list: components)
    }
}

步骤 4: 创建具体的布局元素类

接下来,创建 ItemLayoutBoxGroupLayoutBox 类:

/// 项目布局盒子类 - 表示单个单元格
@MainActor
public class ItemLayoutBox: LayoutBoxConfig {
    var columns: Int = 1
    
    public init(columns: Int = 1, width: NSCollectionLayoutDimension, height: NSCollectionLayoutDimension) {
        super.init(width: width, height: height)
        self.boxType = .item
        self.columns = columns
    }
    
    public func toBuild() -> [NSCollectionLayoutItem] {
        let item = NSCollectionLayoutItem(layoutSize: self.itemSize)
        config(item: item)
        return [NSCollectionLayoutItem](repeating: item, count: self.columns)
    }
}

/// 组布局盒子类 - 表示可以包含多个项目或子组的布局组
@MainActor
public class GroupLayoutBox: LayoutBoxConfig {
    var direction: GroupDirection = .horizontal
    var space: NSCollectionLayoutSpacing?
    private var subitems: [LayoutBoxConfig] = []
    
    @discardableResult
    public init(direction: GroupDirection = .horizontal,
                width: NSCollectionLayoutDimension,
                height: NSCollectionLayoutDimension,
                @LayoutBuilder _ builder: () -> LayoutBoxConfig) {
        super.init(width: width, height: height)
        self.boxType = .group
        self.direction = direction
        self.subitems = builder().subItems()
    }
    
    // 其他方法...
}

步骤 5: 添加链式语法支持

为了支持链式语法,我们需要在 LayoutBoxConfig 扩展中添加一系列返回 self 的方法:

public extension LayoutBoxConfig {
    @discardableResult
    func boxType(_ boxType: LayoutBoxType) -> Self {
        self.boxType = boxType
        return self
    }
    
    @discardableResult
    func insets(_ insets: NSDirectionalEdgeInsets) -> Self {
        self.insets = insets
        return self
    }
    
    // 更多链式方法...
}

实际应用示例

现在,让我们看看如何使用这个库来创建复杂的布局:

示例 1: 创建嵌套组布局

@MainActor static func Example1() -> NSCollectionLayoutSection {
    // 创建嵌套组,包含两个子项:一个2列项目和一个垂直子组
    let nestedGroup = GroupLayoutBox(width: .fractionalWidth(1.0), height: .fractionalHeight(0.4)) {
        // 2列的水平项目组,占父容器30%宽度
        ItemLayoutBox(columns: 2, width: .w(0.3), height: .h(1.0))
            .insets(space: 10)
        
        // 垂直子组,占父容器40%宽度
        GroupLayoutBox(direction: .vertical, width: .fractionalWidth(0.4), height: .fractionalHeight(1.0)) {
            // 子组内的2列项目,占子组100%宽度和30%高度
            ItemLayoutBox(columns: 2, width: .fractionalWidth(1.0), height: .fractionalHeight(0.3))
                .insets(NSDirectionalEdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10))
        }
    }
        .insets(NSDirectionalEdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10))
        .toBuild()
    
    // 创建并返回基于该组的section
    let section = NSCollectionLayoutSection(group: nestedGroup)
    return section
}

示例 2: 支持 if 和 for 循环

@MainActor static func Example3() -> NSCollectionLayoutSection {
    // 测试for 和 if 语法
    let testForAndif = true
    let group = GroupLayoutBox(direction: .horizontal, width: .w(1.0), height: .absolute(120)) {
        if testForAndif {
            ItemLayoutBox(columns: 1, width: .w(0.20), height: .h(1.0)).insets(space: 20)
            ItemLayoutBox(columns: 1, width: .w(0.20), height: .h(1.0)).insets(space: 10)
            
            for i in 0..<2 {
                ItemLayoutBox(columns: 1, width: .w(0.20), height: .h(1.0)).insets(space: CGFloat(i) * 10)
            }
            
            ItemLayoutBox(columns: 1, width: .w(0.1), height: .h(1.0)).insets(space: 1)
            ItemLayoutBox(columns: 1, width: .w(0.1), height: .h(1.0)).insets(space: 5)
        } else {
            for i in 0..<10 {
                ItemLayoutBox(columns: 1, width: .w(0.1), height: .h(1.0)).insets(space: CGFloat(i) * 0.5)
            }
        }
    }
        .leading(.flexible(10)).trailing(.flexible(10)).top(.flexible(10)).bottom(.flexible(10))
        .toBuild()
    
    let section = NSCollectionLayoutSection(group: group)
    return section
}

示例 3: 多级嵌套【彩虹表格】

     @MainActor static func Example4() -> UICollectionViewLayout {
        //🌈表格
        let group = GroupLayoutBox(direction:.vertical, width: .w(1.0), height: .h(1.0)) {
            GroupLayoutBox(direction: .horizontal, width: .w(1.0), height: .h(0.4)) {
                GroupLayoutBox(direction: .vertical, width: .w(2.0/3.0), height: .h(1.0)) {
                    ItemLayoutBox(columns: 1, width: .w(1.0), height: .h(2.0/3.0)).insets(space: 2)
                    ItemLayoutBox(columns: 1, width: .w(1.0), height: .h(1.0/3.0)).insets(space: 2)
                }
                GroupLayoutBox(direction: .vertical, width: .w(1.0/3.0), height: .h(1.0)) {
                    ItemLayoutBox(columns: 1, width: .w(1.0), height: .h(1.0/3.0)).insets(space: 2)
                    ItemLayoutBox(columns: 1, width: .w(1.0), height: .h(2.0/3.0)).insets(space: 2)
                }
            }
            GroupLayoutBox(direction: .horizontal, width: .w(1.0), height: .h(1.0/7)) {
                ItemLayoutBox(columns: 1, width: .w(1.0/3.0), height: .h(1.0)).insets(space: 2)
                ItemLayoutBox(columns: 1, width: .w(2.0/3.0), height: .h(1.0)).insets(space: 2)
            }
            GroupLayoutBox(direction: .horizontal, width: .w(1.0), height: .h(3.0/7)) {
                ItemLayoutBox(columns: 1, width: .w(2.0/3.0), height: .h(1)).insets(space: 2)
                GroupLayoutBox(direction: .vertical, width: .w(1.0/3.0), height: .h(1)) {
                    ItemLayoutBox(columns: 3, width: .w(1.0), height: .h(1.0/3.0)).insets(space: 2)
                }
            }
        }.insets(space: 5)
            .toBuild()
        // 创建并返回基于该组的section
        let section = NSCollectionLayoutSection(group: group)
        let layout = UICollectionViewCompositionalLayout(section: section)
        return layout
    }

关于LayoutBox

image.png LayoutBox是一个优雅的Swift库,用于iOS开发,通过声明式语法简化UICollectionViewCompositionalLayout的创建过程。它提供了一种简洁、直观的方式来构建复杂的集合视图布局,使您能够专注于应用程序的业务逻辑而非繁琐的布局代码

Simulator Screenshot - iPhone 16 - 2025-09-24 at 10.13.42.pngSimulator Screenshot - iPhone 16 - 2025-09-23 at 14.54.37.pngSimulator Screenshot - iPhone 16 - 2025-09-23 at 17.22.54.pngSimulator Screenshot - iPhone 16 - 2025-09-23 at 17.55.35.pngSimulator Screenshot - iPhone 16 - 2025-09-24 at 10.14.13.png

安装

Swift Package Manager 在Xcode中,选择File > Add Packages...,然后输入以下URL:

github.com/willlzq/Lay…

总结

通过 Swift 的 @resultBuilder、逃逸闭包和链式语法这三大利器,我们成功实现了一个仿 SwiftUI 风格的声明式布局库。这个库让创建 NSCollectionLayoutGroup 变得更加直观和优雅,大大提高了代码的可读性和可维护性。

核心要点回顾:

  1. @resultBuilder 是实现声明式语法的基础
  2. 支持递归的结构设计是实现 iffor 语法的关键
  3. 逃逸闭包让 API 调用更加结构化
  4. 链式语法提供了流畅的配置体验

这种声明式语法不仅可以用于布局,还可以应用到很多其他场景,比如构建 attributed string、创建复杂的配置对象等。希望本文能给你带来一些启发,让你的代码变得更加优雅和直观!

参考资料

UICollection Compositional Layout全详解: kingnight.github.io/programming…

深入理解 Swift @resultBuilder:从 SwiftUI 到通用 DSL 的完全指南: jishuzhan.net/article/196…

Swift 入门到实战:写给 iOS 新人的实用技巧

作者 unravel2025
2025年9月23日 11:27

一定要学好 Optional

Swift 的 Optional 不是“语法糖”,而是安全屏障。

变量“可能有值也可能没有”时,编译器强制你显式处理,把空指针崩溃消灭在编译期。

三种常见玩法

玩法 代码示例 场景说明
可选绑定 if let name = nameLabel.text { } 安全解包,作用域内 name 为非可选
空合运算符 let count = cartCount ?? 0 nil 时给默认值
可选链 let upper = user.profile?.nickname?.uppercased() 任意环节为 nil 时整条返回 nil

初学者易踩的坑

// 错误:强制解包 ! 只用于“绝对确定有值”
let price = dict["price"] as! Double   // 一旦 key 不存在直接崩溃

// 正确:先判断再转
if let price = dict["price"] as? Double {
    print("价格:\(price)")
}

语法差异速览(Swift vs Java / C++ / Python)

差异点 Swift 写法 对比说明
句末分号 可省 C/Java 必须写;Swift 只在同一行多条语句时需要
类型推断 let x = 5 Java 必须 int x = 5;
枚举带值 enum Barcode{ case qr(String) } Java 枚举无法直接存关联值
错误处理 do-try-catch Java checked exception 必须声明 throws
字符串插值 "你好,\(name)" C++ 需要 stringstreamprintf
区间匹配 case 1..<5: Python/Java 无原生区间模式匹配

数据类型怎么选?

类型 典型场景 内存/精度提示
Int 计数、索引 64 位,与现代 CPU 寄存器宽度一致
Double 金融、地理坐标 优先用 Double,Float 仅在大量 3D 顶点且 GPU 限制时使用
String 用户输入、JSON 值类型,拷贝时采用写时优化(COW, Copy-On-Write)
Array 有序列表 值语义,多线程环境下无需额外加锁
Set 去重、交集运算 元素需满足 Hashable 协议以保证哈希值正确
Enum 有限状态 关联值可携带 payload(有效载荷),替代继承更轻量

自定义类型示例

enum UserRole: String {
    case admin, editor, viewer
}

struct User {
    let id: Int
    var role: UserRole   // 比裸 String 安全、可维护
}

// 使用
let me = User(id: 1, role: .admin)

控制流 & 性能小贴士

  1. 避免“金字塔”式嵌套:

    把多重条件合并,或使用 guard let 提前返回。

  2. 循环里别直接改集合:

    用高阶函数 filter / map / reduce,可读且易并行。

  3. switch exhaustive 检查:

    对 enum 切换时,default 分支能省但未来新增 case 会漏,建议不开 default,让编译器提醒你把所有 case 写完。

函数与闭包:从“会写”到“写好”

函数是第一等公民

func makeAdder(offset: Int) -> (Int) -> Int {
    // 返回一个闭包
    return { $0 + offset }
}
let add5 = makeAdder(offset: 5)
print(add5(10))   // 15

闭包捕获列表防内存泄漏

class Demo {
    var value = 0
    lazy var closure: () -> Int = { [weak self] in
        // 不加 [weak self] 可能循环引用
        return (self?.value ?? 0) + 1
    }
}

错误处理:别只用 print(error)

  1. 自定义错误类型
enum APIError: Error {
    case invalidURL
    case statusCode(Int)
}
  1. 异步错误一并处理
do {
    let data = try await fetchUser()
} catch APIError.invalidURL {
    // 具体错误具体提示
}

工程篇:Xcode、MVVM、调试与真机

MVVM 骨架(伪代码)

// Model
struct Article: Codable { ... }

// ViewModel
final class ArticleListVM: ObservableObject {
    @Published private(set) var articles: [Article] = []
    func load() async {
        articles = await API.getArticles()
    }
}

// View (SwiftUI)
struct ArticleListView: View {
    @StateObject var vm = ArticleListVM()
    var body: some View {
        List(vm.articles) { ... }
            .task { await vm.load() }
    }
}

调试三板斧

  1. 条件断点:右键断点 → Condition 填入 index == 5
  2. LLDB 实时改值:expr luckyNumber = 88
  3. Thread Sanitizer:Product → Scheme → Diagnostics → 勾选 “Thread Sanitizer”,神仙打架的并发 bug 现形。

真机必做清单

  • Apple ID 加入 Xcode Preferences
  • Bundle Identifier 与描述文件匹配
  • 打开 “Developer Mode” 在 iPhone 设置 → 隐私与安全
  • 用 Network Link Conditioner 模拟弱网,提前发现超时/重试逻辑缺陷。

总结

  1. Optional 与类型安全是 Swift 的“护城河”,越早养成“不强制解包”习惯,越少凌晨修 Crash。
  2. 值类型(struct/enum)在移动设备上比引用类型更省内存,且线程安全天然优势。
  3. 高阶函数 + 链式调用能让集合操作像 SQL 一样声明式,可读性提升 > 50%。
  4. 错误处理要“具体化”,do-try-catch 配合自定义错误类型,调试日志一目了然。
  5. 调试时先想“如何重现”,再用工具验证假设;乱打断点等于大海捞针。
  6. 模拟器只解决 70% 问题,真机 + 弱网 + 低电量才是用户真实环境。

Swift 还能做什么?

场景 技术点 一句话提示
服务端(Vapor) Swift Concurrency 同一门语言写全栈,模型复用
数据科学 Swift for TensorFlow 利用 value semantic 加速自动微分
嵌入式/物联网 Swift on ARM Linux 内存安全驱动硬件,减少 C 指针隐患
跨端逻辑共享 Swift + Kotlin Multiplatform 把业务模型编译成 .framework 给 Android 调用
机器学习本地推理 Core ML + Swift 模型量化后跑在 Neural Engine,比 GPU 省电

学习资料

  1. moldstud.com/articles/p-…
  2. www.swift.org/

Swift 实现 DLNA 投屏功能:完整技术解析与实践指南

作者 CuiXg
2025年9月23日 10:27

1. 引言

DLNA(Digital Living Network Alliance)是一种允许在家庭网络中共享媒体内容的技术标准。通过 DLNA,用户可以将手机、平板等设备上的视频、音频和图片内容投射到电视、音响等大屏设备上播放。本文将详细介绍如何使用 Swift 实现一个完整的 DLNA 投屏功能。

2. DLNA 投屏原理

2.1 DLNA 架构组成

DLNA 系统主要由三个组件构成:

  • DMS(Digital Media Server) :媒体服务器,存储媒体文件
  • DMR(Digital Media Renderer) :媒体渲染器,播放媒体内容
  • DMC(Digital Media Controller) :媒体控制器,控制播放流程

我们的 Swift 实现主要扮演 DMC 角色,控制 DMR 设备播放媒体。

2.2 投屏流程

  1. 设备发现:通过 SSDP 协议搜索网络中的 DLNA 设备
  2. 设备描述:获取设备的服务能力和控制地址
  3. 媒体传输:通过 AVTransport 服务设置播放内容
  4. 播放控制:通过 RenderingControl 服务控制音量、播放状态等

3. 核心代码结构解析

3.1 主控制器:CNDLNA

class CNDLNA {
    private let UPnPServer = CNDLNAUPnPServer()
    private let UPnPRenderer = CNDLNAUPnPRenderer()
    
    // 单例模式
    static var dlna: CNDLNA?
    class func shared() -> CNDLNA {
        if let temp = dlna {
            return temp
        } else {
            dlna = CNDLNA()
            return dlna!
        }
    }
    
    // 开始搜索设备
    func cn_startSearch() {
        UPnPServer.cn_start()
    }
    
    // 选择投屏设备
    func cn_setDevice(withUUID deviceUUIDString) {
        if let deviceInfo = UPnPServer.cn_getDevice(deviceUUID) {
            UPnPRenderer.cn_setDevice(deviceInfo)
        }
    }
    
    // 投屏播放
    func cn_play(withUrl urlStrStringtitleStringcreatorString) {
        UPnPRenderer.cn_setAVTransportURL(urlStr, title: title, creator: creator)
    }
}

3.2 设备发现:CNDLNAUPnPServer

设备发现基于 SSDP(Simple Service Discovery Protocol)协议:

class CNDLNAUPnPServerNSObject {
    private let ssdpAddres = "239.255.255.250"
    private let ssdpPort: UInt16 = 1900
    private var udpSocket: GCDAsyncUdpSocket?
    
    func cn_getSearchString() -> String {
        return "M-SEARCH * HTTP/1.1\r\nHOST: (ssdpAddres):(ssdpPort)\r\nMAN: "ssdp:discover"\r\nMX: 2\r\nST: (serviceType_AVTransport)\r\n\r\n"
    }
    
    func cn_search() {
        if let sendData = self.cn_getSearchString().data(using: .utf8) {
            self.udpSocket?.send(sendData, toHost: ssdpAddres, port: ssdpPort, withTimeout: -1, tag: 1)
        }
    }
}

3.3 设备控制:CNDLNAUPnPRenderer

设备控制通过 SOAP 协议发送 XML 格式的指令:

class CNDLNAUPnPRenderer {
    func cn_setAVTransportURL(_ urlStrStringtitleStringcreatorString) {
        let action = CNDLNAUPnPAction(action: "SetAVTransportURI")
        action.cn_setArgumentValue("0", forName: "InstanceID")
        action.cn_setArgumentValue(urlStr, forName: "CurrentURI")
        action.cn_setArgumentValue(self.cn_createMetaData(urlStr: urlStr, title: title, creator: creator), forName: "CurrentURIMetaData")
        self.cn_post(action)
    }
    
    private func cn_post(_ actionCNDLNAUPnPAction) {
        guard let _device = device else { return }
        let session = URLSession.shared
        if let url = URL(string: action.cn_getPostUrl(withModel: _device)) {
            let postXML = action.cn_getPostXMLString()
            var request = URLRequest(url: url)
            request.httpMethod = "POST"
            request.addValue("text/xml", forHTTPHeaderField: "Content-Type")
            request.addValue(action.cn_getSOAPAction(), forHTTPHeaderField: "SOAPAction")
            request.httpBody = postXML.data(using: .utf8)
            // 发送请求...
        }
    }
}

4. 关键实现细节

4.1 SOAP 消息构建

class CNDLNAUPnPAction {
    func cn_getPostXMLString() -> String {
        let xmlElement = CNXMLDocument(name: "s:Envelope")
        xmlElement.cn_addAttribute(CNXMLDocument(name: "s:encodingStyle", value: "http://schemas.xmlsoap.org/soap/encoding/"))
        xmlElement.cn_addAttribute(CNXMLDocument(name: "xmlns:s", value: "http://schemas.xmlsoap.org/soap/envelope/"))
        xmlElement.cn_addAttribute(CNXMLDocument(name: "xmlns:u", value: self.cn_getServiceTypeValue()))
        
        let command = CNXMLDocument(name: "s:Body")
        command.cn_addChild(_xmlDocument)
        xmlElement.cn_addChild(command)
        return xmlElement.cn_getXMLString()
    }
}

4.2 媒体元数据生成

根据媒体类型生成不同的 DIDL-Lite 元数据:

private func cn_createMetaData(urlStrStringtitleStringcreatorString) -> String {
    let template = self.cn_getMetaDataTemplate(forUrl: urlStr)
    return String(format: template, title, creator, urlStr)
}

private func cn_getMetaDataTemplate(forUrl urlStringString) -> String {
    let lowercaseUrl = urlString.lowercased()
    if lowercaseUrl.contains(".mp4") || lowercaseUrl.contains("video/") {
        return videoTemplate
    }
    if lowercaseUrl.contains(".mp3") || lowercaseUrl.contains("audio/") {
        return audioTemplate
    }
    if lowercaseUrl.contains(".jpg") || lowercaseUrl.contains("image/") {
        return imageTemplate
    }
    return videoTemplate
}

5. 使用示例

5.1 基本使用流程

// 获取 DLNA 实例
let dlna = CNDLNA.shared()

// 设置代理接收回调
dlna.delegate = self

// 开始搜索设备
dlna.cn_startSearch()

// 选择设备(在代理回调中获取设备列表后)
dlna.cn_setDevice(withUUID: deviceUUID)

// 投屏播放视频
dlna.cn_play(withUrl"http://example.com/video.mp4"             title"示例视频"             creator"用户名")

5.2 实现代理方法

extension ViewControllerCNDLNADelegate {
    func cn_dlna(_ dlnaCNDLNAsearchDevicesChange devices: [CNDLNADeviceInfo]) {
        // 更新设备列表UI
        self.devices = devices
        self.tableView.reloadData()
    }
    
    func cn_dlnaPlay(_ dlnaCNDLNA) {
        // 投屏开始播放
        print("投屏播放开始")
    }
    
    func cn_dlna(_ dlnaCNDLNAerrorError?) {
        // 错误处理
        if let error = error {
            print("DLNA错误: (error.localizedDescription)")
        }
    }
}

6. 注意事项与优化建议

6.1 网络权限

在 iOS 中使用 DLNA 需要确保应用有网络访问权限,在 Info.plist 中添加:

<key>NSLocalNetworkUsageDescription</key>
<string>需要访问本地网络以发现DLNA设备</string>

6.2 性能优化

  • 设备搜索使用合适的超时时间,避免长时间占用资源
  • 使用合适的队列处理网络回调,避免阻塞主线程
  • 合理管理 UDP socket 的生命周期

6.3 兼容性处理

  • 不同厂商的 DLNA 设备可能有细微差异,需要测试兼容性
  • 处理设备离线、网络异常等边界情况

7. 总结

本文详细介绍了如何使用 Swift 实现 DLNA 投屏功能,涵盖了设备发现、连接、媒体传输和播放控制等核心环节。通过这个实现,开发者可以轻松地将 DLNA 投屏功能集成到自己的 iOS 应用中,为用户提供更好的跨设备媒体体验。

完整的代码实现提供了良好的扩展性,开发者可以根据需要添加更多功能,如播放列表管理、播放进度同步等高级特性。

Swift各种构造器

作者 dpwang
2025年9月23日 10:07

构造器

使用构造器来实现构造过程,构造器可以看做是用来创建新实例的特殊方法。

构造过程:是使用类、结构体或者枚举类型的实例之前的准备过程。在新的实例可用之前必须执行这个过程,具体操作包括设置实例中每个储存类型属性的初始值和执行其它必须设置或初始化的工作。

swift与OC的构造器不同,OC是先调用父类的init再写自己的, 但是到了Swift里面, 我们却先初始化自己, 再初始化父类, 是相反的,swift中构造器无需返回值,主要任务是保证新实例在第一次使用前完成正确的初始化

默认构造器

如果结构体或类的所有属性都有默认值,同时没有自定义的构造器,那么swift会给这些结构体或类提供一个默认构造器。这个默认的构造器将简单的创建一个所有属性值都设置为默认的实例。

结构体的逐一成员构造器

结构体如果没有定义任何自定义构造器,它们将自动获得一个逐一成员构造器。

struct AA {
    var a = 1
    var b = "2"
    var c = 3.0
}

image.png

值类型的构造器代理

  • 构造器可以通过调用其它构造器来完成实例的部分构造过程,这一过程称为构造器代理
  • 构造代理对值类型和引用类型来说不太一样, 值类型因为不支持继承, 所以只会用自己写的构造器来代理, 从而相对更简单. 类则会有从父类继承构造器的情况要考虑, 不过还是那句话, 所有存储属性在构造器中都完成初始化就可以.
  • 如果你为某个值类型定义了一个自定义的构造器,你将无法访问到默认构造器(如果是结构体,还将无法访问逐一成员构造器)
struct Size {
    var width = 0.0  
    var height = 0.0
}
struct Point {
    var x = 0.0
    var y = 0.0
}
struct Rect {
    var origin = Point()
    var size = Size()
    
    init(){}    
    
    init(origin:Point,size:Size)
    {
        self.origin = origin
        self.size = size
    }
    
    init(center:Point,size:Size)
    {
        let originX = center.x-(size.width / 2)
        let originY = center.y-(size.height / 2)
        // 构造器代理
        self.init(origin: Point.init(x: originX, y: originY), size: size)
    }
}

自定义构造器

通过输入参数和可选类型的属性来自定义构造器

class SomeOne {
    var a: Int
    var b: String?
    
    // 带参数的内部名称和外部名称的构造器
    init(a aa: Int, b bb: String?) {
        a = aa
        b = bb
    }
    
    // 带参数的外部名称的构造器
    init(aa: Int, bb: String?) {
        a = aa
        b = bb
    }
    
    // 不带参数的外部名称的构造器
    init(_ a: Int, _ b: String?) {
        a = a
        b = b
    }
    
    // 带默认参数值的构造器
    // 注意:虽然这个构造器的每个参数都带有默认值可以省略参数,但是SomeOne() 调用的不是这个构造器
    init(aaa: Int = 1, bbb: String? = nil {
        a = aaa
        b = bbb
    }
}

Extension

扩展可以向已有类型添加新的构造器。这可以让你扩展其它类型,将你自己的定制类型作为构造器参数,或者提供该类型的原始实现中没有包含的额外初始化选项。

扩展能向类中添加新的便利构造器,但是它们不能向类中添加新的指定构造器或析构函数。指定构造器和析构函数必须总是由原始的类实现来提供。

extension Rect {
// 上面的构造器代理调用方式可以转移到extension中
    init(center:Point,size:Size)
    {
        let originX = center.x-(size.width / 2)
        let originY = center.y-(size.height / 2)
        self.init(origin: Point.init(x: originX, y: originY), size: size)
    }
}


extension SomeOne {
    // 引用类型只能添加便利构造器
    convenience init(absA: Int) {
        self.init()
        a = abs(absA)
    }
}

类的构造过程

  • 类里面的所有存储型属性,包括所有继承自父类的属性,都必须在构造过程中设置初始值
  • Swift 为类类型提供了两种构造器来确保实例中所有存储型属性都能获得初始值,它们分别是指定构造器便利构造器

指定构造器(designated initializer)

指定构造器是类中最主要的构造器。每个类至少有一个指定构造器,一个指定构造器必须真正完成所有存储属性的初始化,并根据父类链(如果有)往上调用父类的构造器来实现父类的初始化

init(parameters) {
     statements
}

便利构造器(convenience initializer)

便利构造器是类中比较次要的。你可以定义便利构造器来调用同一个类中的其它构造器,并为其参数提供默认值,但是构造链必须到一个指定构造器为结束

convenience init(parameters) {
    self.init(parameters)
     statements
}

类的构造器代理规则

1.每个类至少要有一个指定构造器

2.指定构造器必须继承父类的指定构造器

3.便利构造器必须调用同一个类中的一个其它构造器

4.便利构造器必须以调用一个指定构造器为结束

image.png

两段式构造过程

Swift 中类的构造过程包含两个阶段。第一个阶段,类中的每个存储型属性赋一个初始值。当每个存储型属性的初始值被赋值后,第二阶段开始,它给每个类一次机会,在新实例准备使用之前进一步自定义它们的存储型属性。

两段式构造过程的使用让构造过程更安全,同时在整个类层级结构中给予了每个类完全的灵活性。两段式构造过程可以防止属性值在初始化之前被访问,也可以防止属性被另外一个构造器意外地赋予不同的值。

Swift 编译器将执行4种有效的安全检查,以确保两段式构造过程不出错地完成:

  1. 指定构造器必须保证它所在类的所有属性都必须先初始化完成,之后才能将其它构造任务向上代理给父类中的构造器。
  2. 指定构造器必须在为继承的属性设置新值之前向上代理调用父类构造器。如果没这么做,指定构造器赋予的新值将被父类中的构造器所覆盖。
  3. 便利构造器必须为任意属性(包括所有同类中定义的)赋新值之前代理调用其它构造器。如果没这么做,便利构造器赋予的新值将被该类的指定构造器所覆盖。
  4. 构造器在第一阶段构造完成之前,不能调用任何实例方法,不能读取任何实例属性的值,不能引用 self 作为一个值。

以下是基于上述安全检查的两段式构造过程展示:

阶段 1
  • 类的某个指定构造器或便利构造器被调用。
  • 完成类的新实例内存的分配,但此时内存还没有被初始化。
  • 指定构造器确保其所在类引入的所有存储型属性都已赋初值。存储型属性所属的内存完成初始化。
  • 指定构造器切换到父类的构造器,对其存储属性完成相同的任务。
  • 这个过程沿着类的继承链一直往上执行,直到到达继承链的最顶部。
  • 当到达了继承链最顶部,而且继承链的最后一个类已确保所有的存储型属性都已经赋值,这个实例的内存被认为已经完全初始化。此时阶段 1 完成。
阶段 2
  • 从继承链顶部往下,继承链中每个类的指定构造器都有机会进一步自定义实例。构造器此时可以访问 self、修改它的属性并调用实例方法等等。
  • 最终,继承链中任意的便利构造器有机会自定义实例和使用 self
class Super {

    var a: Int = 1
    var b: Int
    init() {
        // 第一阶段
        // 构造器确保其所在类引入的所有存储型属性都已赋初值。存储型属性所属的内存完成初始化。
        b = 2
    }
}


class Sub: Super {

    var c: String
    override init() {
        // 第一阶段
        // 需要保证在向上调用之前本类所有的属性都必须先初始化完成,因此不能写在super.init()之后
        c = "3"
        super.init()
        // 第二阶段
        // 此时可以引用self,并修改它的属性
        self.a = 11
        self.b = 22
        self.c = "33"
    }
}

构造器的继承和重写

类默认是不继承父类的构造器,除了:

  • 子类中没有定义指定构造器,则子类继承父类的全部指定构造器。

  • 子类提供了父类的所有指定构造器的实现,则子类自动继承父类的所有便利构造器。

class Super {

    init() {
        //...
    }

    init(total: Int) {
        //...
    }

    convenience init(a: Int, b: Int) {
        self.init(total: a + b)
        //...
    }
}

class Sub: Super {
    // 没有定义指定构造器
}

screenshot-20220907-154849.png

class Sub: Super {
    // 未提供了父类的所有指定构造器的实现
    
    override init() {
        super.init()
        //...
    }
}

screenshot-20220907-155651.png

class Sub: Super {
    // 提供了父类的所有指定构造器的实现
    
    override init() {
        super.init()
        //...
    }
    override init(total: Int) {
        super.init(total: total)
        //...
    }
}

screenshot-20220907-154849.png

Swift 6.2 来了 | 肘子的 Swift 周报 #0103

作者 东坡肘子
2025年9月23日 08:04

issue103.webp

weekly.fatbobman.com 订阅本周报的电子邮件版本。访问我的博客 肘子的 Swift 记事本 查看更多的文章。加入 Discord 社区,与 2000+ 中文开发者深入交流 Swift、SwiftUI 开发体验。

Swift 6.2 来了

在 Swift 6.0 发布一年后,Swift 6 迎来了第二个重要版本更新。除了备受关注的 Default Actor Isolation 外,Swift 6.2 还带来了诸多实用的新功能

相比语言能力的提升,我更欣赏 Swift 团队在工具链方面所做的努力:VS Code 插件获得官方认证、swift-syntax 支持预编译包等。这些更新为第三方编辑器带来更可靠的支持,也实实在在地提升了构建效率与可维护性。

然而,作为 Swift 开发者,我们不得不面对一个长期存在的现实困境:Swift 仍然紧密依附于苹果的产品发布节奏。无论是新版本发布时机与 Xcode 的强绑定,还是 Xcode 工具链与官方开源版本间的差异,都在提醒我们:开源的 Swift,距离真正的“开放”还有一段距离。

苹果是 Swift 的缔造者,也是迄今最主要的贡献者,这点无可否认。但或许,只有当 Swift 在形式上逐步摆脱对苹果年度节奏的依赖,建立起独立的发布机制与治理结构,才能真正激发社区的参与热情,也才能让这门语言在更广阔的平台与领域中实现它应有的潜力。

前一期内容全部周报列表

近期推荐

iOS 17+ SwiftUI TextField 内存泄漏分析

有开发者发现,自 iOS 17 起在包含 TextField 的视图中出现了一个可稳定复现的内存泄漏问题(截至 iOS 26 仍未修复):即便视图已经销毁,UITextField 及其关联的环境对象仍会滞留在内存中,直到另一个输入源被激活才会延迟释放。这个问题不仅影响 SwiftUI,同样存在于 UIKit。Kyle Ye 在本文中深入分析了其根本原因 —— 来自 AutoFillUI 框架中的 AFUITargetDetectionController 引起的引用保留,并提供了包括 .autocorrectionDisabled(true) 在内的多种实用应对方案。


深入消化 Swift Actor 使用建议 (Zettelkasten for Programmers: Processing Swift Actor Usage Advice in Depth)

本文是 Christian Tietze 对 Matt Massicotte 上周文章《When should you use an actor?》的回应。在赞同 Matt 提出的 Actor 使用三原则的基础上,Christian 进一步指出:Swift 中的 actor 是一种昂贵且语义明确的并发工具,只有在确实满足特定技术与设计前提时才值得引入。否则,应优先考虑更轻量、明确的手段来实现并发与隔离。

Actor 作为 Swift 新并发模型中的重要组成部分,何时使用、如何使用,仍需更多项目经验的积累与总结。而像这样的理性探讨与实践反思,正是构建现代 Swift 并发知识体系中最珍贵的材料。


Swift 中的功能开关 (Feature flags in Swift)

在项目开发中,许多功能通常只在特定构建模式(如 Debug、TestFlight 或 Release)中启用。在本文中,Majid Jabrayilov 分享了他的实践经验:通过结合构建配置与 FeatureFlags 模型,并借助 @Entry 属性包装器将功能开关注入 SwiftUI 的 EnvironmentValues,开发者可以实现更快速的开发流程、更灵活的测试手段以及更安全的功能上线方式。

对采用 trunk-based 开发流程的 Swift 项目尤其值得参考。


Liquid Glass 设计系统三原则 (The Northern Stars of Liquid Glass)

本文是 Danny Bolella 对 Apple 人机界面指南(HIG)中围绕 Liquid Glass 所提出的三大设计原则 —— Hierarchy(层级)、Harmony(协调)、Consistency(一致性) 的深入解读。Danny 不仅阐释了每条原则在视觉系统中的意义,还结合 SwiftUI 示例展示了如何在实际开发中落地这些理念,例如通过 .buttonStyle(.glass) 营造界面层级、使用 ConcentricRectangle 建立视觉节奏,以及借助 ViewThatFits 实现跨平台一致性。


空间计算为何必然崛起:下一代计算平台路线图 (Why VR, AR and Spatial Computing Will Inevitably Take Off: the Roadmap to the Next Computing Platform)

作为一位专注 VR/AR 领域的投资人,Wu Xu 在本文中系统回顾了计算平台从主机、PC、智能手机到可穿戴设备的演进路径,并结合硬件能力、产业节奏与产品形态,深入分析了 VR、空间计算与智能眼镜三条路径将如何并行演化并最终收敛。他将这一趋势称为下一代计算平台的“三线合围”。文中提出多个极具洞察力的判断,例如:“Vision Pro 是 iPhone 发布前的 Mac”;“真正改变日常的智能眼镜,关键不在硬件,而在 AI 是否能带来显著的主观优势感知”。


iOS 应用渲染架构深度解析 (iOS Application Rendering: A Deep Dive)

在这篇深入的技术文档中,Ethan Arbuckle 系统梳理了 iOS 应用渲染的完整架构流程——从 UIView 构建、CALayer 合成,到 CAContext 与系统渲染服务的协同,再到最终像素输出。文章详尽覆盖了 UIKit、QuartzCore、FrontBoardServices、BackBoardServices 与 Render Server 等核心组件的职责划分与协作机制,并深入剖析了多进程环境下如何通过 contextID 实现输入事件路由、动画同步与场景托管等关键能力。

工具

edge-agent: Swift 边缘计算运行时平台

虽然 Swift 支持 Linux,但在边缘设备(如 Raspberry Pi、Jetson Orin Nano)上部署和调试 Swift 应用程序一直缺乏成体系的解决方案。edge-agent 正是为了解决这一痛点而生——它是一个专为 Swift 开发者打造的边缘计算运行时平台,结合 Swift 静态 Linux SDK 与 Docker,提供从跨平台构建、容器化部署到远程调试的完整流程。通过预构建的 EdgeOS 镜像和 CLI 工具,开发者无需深入配置交叉编译环境,即可将 Swift 应用快速运行于边缘设备,并借助 LLDB 实现远程调试。

求贤

美团 iOS / Android 开发岗位招聘中!

美团客户端团队现招聘多个方向的开发工程师,偏向中级(含较丰富基础组件 /性能 /动态化架构经验者优先)。

卡片容器方向(Android / iOS / 鸿蒙)

  • 地点:北京望京
  • 职责包括负责公司级卡片运行时框架、DSL 与编译/发布流水线,参与容器化或动态化卡片的架构设计与优化。
  • 技术要求:熟悉 iOS 客户端架构,理解跨平台(Android / 鸿蒙)动态组件/容器化机制;性能调优能力;中级经验(3‑5 年或同等能力)。

首页业务 & 性能优化方向

  • 业务场景:美团首页,高频业务 & 大量用户访问;关注首页加载性能、滚动流畅性、内存占用与渲染效率等体验指标。
  • 技能要求:能用性能工具进行 Profiling/诊断;有 UI 渲染、动画/布局优化经历;有实战经历优先。

往期内容

THANK YOU

如果你觉得这份周报或者我的文章对你有所帮助,欢迎 点赞 并将其 转发 给更多的朋友。

weekly.fatbobman.com 订阅本周报的电子邮件版本。访问我的博客 肘子的 Swift 记事本 查看更多的文章。加入 Discord 社区,与 2000+ 中文开发者深入交流 Swift、SwiftUI 开发体验。

一图看懂 Sendable & @Sendable—— Swift 并发世界的「通行证」到底长什么样?

作者 unravel2025
2025年9月22日 15:22

速览思维导图(先收藏,再阅读)

数据竞争
├─ 定义:≥2 线程 + 同一块内存 + 至少一个写
└─ 后果:顺序不确定 / 崩溃 / 幽灵 Bug

Sendable(协议)
├─ 值类型:成员全部 Sendable ⇒ 自动符合
├─ 引用类型:final + 成员全部 Sendable + 无可变状态 ⇒ 手动符合
└─ 祖传代码:@unchecked Sendable + 人工保证线程安全

@Sendable(闭包注解)
├─ 要求:捕获的**所有**变量 必须 Sendable
└─ 场景:Task / TaskGroup / 自己写的并发 API

Swift 6 新宠:sending
├─ 只关心 **一次性过户**,不强制对象本身 Sendable
└─ 与 @Sendable 互补,不是替代

为什么要有 Sendable?

安全区域 危险区域
单线程、主线程 多任务、TaskGroup、actor 之间
值类型拷贝 引用类型共享

Sendable 就是编译器给你的“跨域通行证”:只要类型符合 Sendable,编译器就默认它可以安全地跨并发边界传递,无需额外同步。

值类型:天生 Sendable?

// ✅ 自动 Sendable:所有成员都是 Sendable
struct Movie {
    let title: String        // String 是 Sendable
    let year: Int            // Int   是 Sendable
}

class FormatterCache {
    var name: String = "unravel"
}

// ❌ 非 Sendable:成员包含非 Sendable 引用
struct Movie {
    let cache = FormatterCache()   // class 且非 Sendable
}

规则: 值类型递归成员必须全是 Sendable,否则整体就不是Sendable

可手动加 Sendable 让编译器再检查一遍:

struct Movie: Sendable {   // 再确认一次
    let title: String
}

引用类型:自己证明“终身安全”

final class Config: Sendable {        // ① 必须 final
    let apiKey: String = "123"        // ② 只有 let / Sendable 成员
    // ③ 无 mutable stored property
}

不符合? 编译器立刻打脸:

Stored property 'state' of Sendable-conforming class 'MyClass' is mutable

祖传代码:@unchecked Sendable —— 手动关保险箱

class FormatterCache: @unchecked Sendable {   // 你说了算
    private var formatters: [String: DateFormatter] = [:]
    private let queue = DispatchQueue(label: "cache.queue")

    func formatter(for format: String) -> DateFormatter {
        queue.sync {                      // 手动串行化
            if let f = formatters[format] { return f }
            let f = DateFormatter()
            f.dateFormat = format
            formatters[format] = f
            return f
        }
    }
}

使用守则

  • 100 % 确定已用锁/队列保护。
  • 写注释 + 单元测试(多线程压力)。
  • 计划迁移到 actor,逐步还债。

@Sendable 闭包:捕获列表大搜查

func performWork(_ operation: @escaping @Sendable () async -> Void)

要求:闭包里捕获的所有变量必须 Sendable

反例:

class FormatterCache {
    var name: String = "unravel"
    func formatter(for str: String) {
        print(str)
    }
}

func myTask1(operation:  @escaping @isolated(any) @Sendable () async throws -> Void) {
    
}

let cache = FormatterCache()
myTask1 {
    cache.formatter(for: "YYYY")
}

修复

  • cache 改成 actor 或 Sendable;
  • 或改用 sending

Swift 6 新关键字:sending —— 一次性通行证

class MyClass {
    var count = 0
}
func foo() async {
    let obj = MyClass()               // 非 Sendable
    Task {                       // Sending value of non-Sendable type '() async -> ()' risks causing data races
        obj.count += 1
    }
     print(obj.count)             
}
// Task 定义
// public init(name: String? = nil, priority: TaskPriority? = nil, operation: sending @escaping @isolated(any) () async -> Success)

原理:编译器只保证“过户”后不再使用,无需对象本身终身安全。

自定义 API:

func runLater(_ body: sending @escaping () async -> Void) {
    Task { await body() }
}

实战模板:把“全局锁”改成“零锁”

旧代码(锁) 新代码(actor + sending)
全局单例 + NSLock actor 单例 + sending 闭包
手动加锁/解锁 编译器保证串行
测试用例难写 await + 压力测试即可
actor ImageCache {
    static let shared = ImageCache()
    private var images: [String: Image] = [:]
    
    func insert(_ image: Image, for key: String) {
        images[key] = image
    }
}

func download() async -> Image {
    Image("any")
}

// 调用方
func load() async {
    let cache = ImageCache.shared          // actor 已 Sendable
    let img = await download()
    await cache.insert(img, for: "cat")
}

常见坑 & 速查表

场景 能否通过 修复姿势
class 里有 var 存储属性 let / 改 actor / @unchecked
struct 成员含非 Sendable class 把 class 改成 actor 或加锁后 @unchecked
闭包捕获非 Sendable 把对象改成 Sendable 或改用 sending
需要长期共享可变状态 actor
只需要一次性异步搬运 sending

一句话总结

  • Sendable 是“终身荣誉公民”——永远线程安全。
  • @Sendable 是“闭包安检门”——捕获链必须全公民。
  • sending 是“一次性签证”——过户后别再碰。

掌握这三张通行证,Swift 6 并发世界任你行。

❌
❌