普通视图

发现新文章,点击刷新页面。
今天 — 2025年7月3日首页

Swift 单元测试突破口:如何优雅测试私有方法?

作者 iOS新知
2025年7月3日 10:55

这里每天分享一个 iOS 的新知识,快来关注我吧

image.png

前言

在编写单元测试时,我们通常希望测试代码的业务逻辑。常用的测试编写格式是 GWT(Given-When-Then),其中在 "When" 步骤中调用我们想要测试的代码。然而,如果你曾写过测试用例,你可能遇到过无法测试私有方法的问题。

这是因为私有属性或方法的作用域仅限于其所在的类,因此我们无法在测试类中访问这些属性或方法。

例如,如果我们有一个类,其中包含一个私有方法,如下所示:

class SomeClass {
    private func somePrivateMethod() {
        // 一些逻辑代码
    }
}

尝试在单元测试中调用 somePrivateMethod,会发现无法访问,并会产生编译错误:“'somePrivateMethod' 由于 'private' 保护级别而无法访问”,这很容易理解。

class SomeClassTests: XCTestCase {
    func testSomePrivateMethod() {
        let testTarget = SomeClass()
        testTarget.somePrivateMethod() // 错误:无法访问私有方法
    }
}

那么我们该如何解决这个问题呢?如果 somePrivateMethod 包含一些业务逻辑,单元测试一定要能够覆盖,那么我们就必须找到其他可行的方法。

方法一:改变访问级别

一种方法是将这些方法的访问级别改为非私有,但这会将这些方法暴露给其他代码,显然这不是一个理想的方案。

public class SomeClass {
    public func somePrivateMethod() {
        // 一些逻辑代码
    }
}

方法二:使用 TestHooks

TestHooks 可以派上用场了。我们可以创建测试钩子并利用它们来访问私有方法和属性,以进行单元测试。TestHooks 只是我们的类持有的一个钩子集,通过这些钩子可以提供对私有方法和属性的访问。

创建 TestHooks 时需要注意的几点:

  1. TestHooks 是在我们希望访问其私有方法或属性的类的扩展中创建的(在我们的例子中是 SomeClass),因为只有这样钩子才能访问那些属性或方法。

  2. 我们想访问的每个属性或方法都需要一个钩子。

  3. 建议将钩子扩展放在 DEBUG 宏中,以避免误用。

实现 TestHook:

以下是 SomeClass 的 TestHook 实现示例:

#if DEBUG    // 在 debug 宏下添加以避免误用,并避免在发布环境中暴露私有方法
extension SomeClass {    // 在类的扩展中编写,我们希望访问其私有方法
    var testHooks: TestHooks {      // testHooks 的实例,通过它我们将在单元测试中访问私有方法
        TestHooks(target: self)      // 使用 self 初始化以访问 self 的私有方法
    }
    struct TestHooks {    // TestHooks 结构体,其中包含我们希望访问的所有属性和方法的钩子
        var target: SomeClass    // 需要访问其私有方法的目标
        func somePrivateMethod() {    // 暴露方法的钩子
            target.somePrivateMethod()    // 暴露该方法
        }
    }
}
#endif

这样一来,我们可以在单元测试文件中通过 testHooks 访问 somePrivateMethod

class SomeClassTests: XCTestCase {
    func testSomePrivateMethod() {
        let testTarget = SomeClass()
        testTarget.testHooks.somePrivateMethod() // 通过 testHooks 访问私有方法
    }
}

结尾

通过这种方式,我们可以在不改变代码结构的情况下,合理地测试私有方法。

TestHooks 提供了一种在测试环境中访问私有方法的途径,同时在发布环境中保持代码的封装性。这是一种在不破坏类封装原则的情况下进行单元测试的有效方法。

希望这能帮助到大家更好地进行 Swift 的单元测试,你对这种方式有什么看法呢?是否还有更好的方案分享,欢迎在评论区留言讨论。

这里每天分享一个 iOS 的新知识,快来关注我吧

本文同步自微信公众号 “iOS新知”,每天准时分享一个新知识,这里只是同步,想要及时学到就来关注我吧!

Swift组件:使用第三方静态库

作者 我唔知啊
2025年7月3日 10:23

在 iOS 项目开发中,我们常常需要引入第三方静态库(.a 文件)来使用特定功能,比如某些闭源 SDK、游戏手柄驱动、音频编解码模块等。本文结合实际问题,介绍如何在 Swift 组件中通过 CocoaPods 集成第三方静态库,并解决架构不匹配、模块导入失败等常见问题。


一、问题背景

我们拿到的 SDK 内容包括:

  • libWechatOpenSDK.a:静态库
  • .h 头文件
  • modulemap 文件
  • 仅支持 arm64(真机架构)

目标是通过 CocoaPods 本地集成该库,并暴露给 Swift 使用。


二、创建 Podspec 文件

创建 WechatOpenSDK.podspec,内容如下:

Pod::Spec.new do |s|
  s.name         = 'WechatOpenSDK'
  s.version      = '2.0.4'
  s.summary      = '本地集成的微信 OpenSDK'
  s.homepage     = 'https://open.weixin.qq.com'
  s.license      = { :type => 'Commercial' }
  s.author       = { 'WeChat' => 'wechat@tencent.com' }
  s.source       = { :path => '.' }

  s.ios.deployment_target = '9.0'
  s.swift_version = '5.0'
  s.requires_arc = true
  s.static_framework = true
  
  # 真机
  #s.source_files         = 'WechatOpenSDK/ios-arm64_armv7/*'
  #s.vendored_libraries   = 'WechatOpenSDK/ios-arm64_armv7/*.a'
  
  # 模拟器
  s.source_files         = 'WechatOpenSDK/ios-arm64_i386_x86_64-simulator/*'
  s.vendored_libraries   = 'WechatOpenSDK/ios-arm64_i386_x86_64-simulator/*.a'
  
  # SDK依赖
  s.libraries = 'c++'
  
end
昨天 — 2025年7月2日首页

与 AI 共舞:我的 Claude Code 一月谈

作者 Fatbobman
2025年7月2日 22:12

转眼间,我使用 Claude Code 已经整整一个月了。这段时间里,它迅速成为了开发者们的新宠,关于 Claude Code 的讨论充斥着我的社交媒体时间线。恰好有网友在 Discord 上让我聊聊 Claude Code,借此机会,我想梳理一下这段时间的使用感受,以及过去两年中 AI 为我的开发工作带来的便利与思考。

掌握生死时速:苹果应用加急审核全攻略!

作者 iOS新知
2025年7月2日 14:50

这里每天分享一个 iOS 的新知识,快来关注我吧

前言

image.png

在应用开发的过程中,总会遇到一些突发情况,比如应用版本在上线后出现重大 bug 或崩溃,严重影响了大部分用户的使用体验。这种情况下,及时响应和修复问题至关重要,不仅可以减少用户的不良体验,还能防止应用评分的下降。

然而,即便你已经找到了问题的根源,并准备好了解决方案,想要快速发布更新版本,也并非完全由你决定。

因为首先苹果是不允许热更新的,每个版本发布都必须经过苹果的审核,其次审核是否通过完全取决于苹果,因此加快审核流程就显得尤为重要。

但很多 iOS 开发者可能不知道,其实可以通过“加急审核”来告知 App Review 团队有紧急更新需要审核,从而加快审核速度。

本文将详细介绍如何通过 Apple 开发者门户请求加急审核。

如何请求加急审核

要请求加急审核,你需要前往 developer.apple.com 并登录到你的开发者账号。登录的账号必须具备管理需要请求加急审核的应用的权限。

image.png

  1. 滚动到主页底部,点击“联系我们”链接。

image.png 2. 从列表中选择“应用审核”类别。

Image

  1. 选择“请求加急应用审核”选项。

Image

  1. 点击“联系应用审核团队”。

Image

  1. 填写表单,告知审核团队你希望加快处理的应用信息。

Image

需要在表单中提供的信息包括:

  • 你希望向应用审核团队提出的请求类型:在我们这种情况下,选择“请求加急审核”选项。

  • 请求加急审核的人员姓名。

  • 请求加急审核的人员邮箱。

  • 拥有该应用的组织名称。

  • 应用名称。

  • 需要加急审核的版本平台。

完成以上步骤后,点击最后的“Send”按钮,将请求提交给应用审核团队。

正常情况下,几个小时后会收到苹果审核的结果。

注意事项

虽然加急审核是开发者工具箱中的一个强大工具,是在紧急情况下将应用快速交到用户手中的最佳方式,但它应仅在特殊情况下使用。正如 Apple 在审核表单中提到的:

如果你面临紧急情况,比如修复关键 bug 或发布应用以配合某个事件,可以通过填写此表单请求加急审核。

同时需要注意,太多次的加急审核请求可能会导致 Apple 对你的反感,然后可能会导致对你之后的加急请求不予理会,因此要谨慎使用。

这里每天分享一个 iOS 的新知识,快来关注我吧

本文同步自微信公众号 “iOS新知”,每天准时分享一个新知识,这里只是同步,想要及时学到就来关注我吧!

Swift 6.2 中的 `@concurrent`

作者 unravel2025
2025年7月2日 10:01

核心概念

@concurrent 是 Swift 6.2 引入的新特性,用于明确标记需要卸载到全局执行器(后台线程)的函数。它与 nonisolated(nonsending) 共同构成 Swift 并发模型的改进,旨在解决以下问题:

  1. 行为统一
    消除异步/同步函数在隔离行为上的不一致性
  2. 显式意图
    明确标识需要并发执行的代码
  3. 简化复杂度
    减少不必要的并发隔离域

关键机制解析

1. nonisolated(nonsending)(统一行为)

// 始终在调用者的执行器上运行
nonisolated(nonsending) func decode<T: Decodable>(_ data: Data) async throws -> T
版本 行为差异
Swift 6.1 异步函数 → 全局执行器
Swift 6.1 同步函数 → 调用者执行器
Swift 6.2 统一在调用者执行器运行

2. @concurrent(显式卸载)

// 明确卸载到全局执行器
@concurrent func decode<T: Decodable>(_ data: Data) async throws -> T
特性 说明
自动标记 nonisolated 无需额外声明
创建新隔离域 要求状态实现 Sendable
使用限制 不能与显式隔离声明(如 @MainActor)共存

何时使用 @concurrent

适用场景

class Networking {
    // 主线程安全的网络请求
    func loadData(from url: URL) async throws -> Data { ... }
    
    // 耗时解码 → 适合 @concurrent
    @concurrent func decode<T: Decodable>(_ data: Data) async throws -> T {
        let decoder = JSONDecoder()
        return try decoder.decode(T.self, from: data)
    }
    
    func getFeed() async throws -> Feed {
        let data = try await loadData(from: Feed.endpoint)
        // 避免阻塞调用者线程
        let feed: Feed = try await decode(data)
        return feed
    }
}

使用原则

  1. 精准定位
    仅标记实际需要并发的函数(如 CPU 密集型任务)
  2. 避免过度使用
    减少不必要的隔离域和 Sendable 约束
  3. 性能优化
    解决特定性能瓶颈(如大数据量解码)

总结对比表

特性 nonisolated (旧) nonisolated(nonsending) @concurrent
执行位置 异步→全局/同步→调用者 始终在调用者执行器 全局执行器
隔离域 可能创建新隔离域 不创建新隔离域 创建新隔离域
状态要求 潜在需要 Sendable 无特殊要求 必须 Sendable
使用场景 兼容旧版 默认推荐 显式并发需求
代码可读性 意图模糊 行为明确 意图明确
昨天以前首页

在Swift中运行Silero VAD

作者 ZHANGYU
2025年7月1日 10:26

最近又开始学习Swift了,前段时间在AI的帮助下做了一个可以和大模型聊天的软件,当时VAD的功能很头痛,搜了下有一个付费的Cobra VAD,另外就只有靠音频能量判断了,这种方式不准。

最近做的东西又有VAD需求了,研究了很久后可以在Swift里跑Silero VAD了,直接把代码丢出来。

由于我不知道如何把ONNX模型转成Core ML的,官方ONNX Runtime只有Pods的包,我用的是另一个Swift Packags版本的ONNX Runtime,用Pods的包要把import OnnxRuntimeBindings换一下。

//
//  SileroVAD.swift
//  Real-time Captions
//
//  Created by yu on 2025/6/30.
//

import AVFoundation
import Foundation
import OnnxRuntimeBindings

/// 说话起止事件回调
protocol SileroVADDelegate: AnyObject {
    /// 检测到"开始说话"
    /// - Parameter probability: 触发时那一帧的 VAD 概率
    func vadDidStartSpeech(probability: Float)

    /// 检测到"结束说话"
    /// - Parameter probability: 触发时那一帧的 VAD 概率
    func vadDidEndSpeech(probability: Float)
}

final class SileroVAD {
    // MARK: - 可调参数

    public struct Config {
        /// 进入说话的高阈值
        public var threshold: Float = 0.5
        /// 退出说话的低阈值(自动与 threshold 保持 0.15 差值)
        public var negThreshold: Float { max(threshold - 0.15, 0.01) }
        /// 连续多长时间高于 threshold 才算"开始说话"(秒)
        public var startSecs: Float = 0.20
        /// 连续多长时间低于 negThreshold 才算"结束说话"(秒)
        public var stopSecs: Float = 0.80
        /// 采样率,仅支持 8 kHz / 16 kHz
        public var sampleRate: Int = 16000

        public init() {}
    }

    // MARK: - 内部状态

    private enum VADState {
        case silence // 静音状态
        case speechCandidate // 可能开始说话
        case speech // 正在说话
        case silenceCandidate // 可能结束说话
    }

    private enum VADError: Error {
        case modelLoadFailed(String)
        case invalidAudioFormat(String)
        case inferenceError(String)
        case tensorCreationFailed(String)
    }

    // MARK: - 核心属性

    private let session: ORTSession
    private var state: ORTValue
    private let config: Config
    public weak var delegate: SileroVADDelegate?

    // 状态机相关
    private var vadState: VADState = .silence
    private var speechFrameCount = 0
    private var silenceFrameCount = 0
    private var lastProbability: Float = 0.0

    // 阈值(基于配置计算的帧数)
    private let speechFrameThreshold: Int
    private let silenceFrameThreshold: Int

    // 音频缓冲
    private var sampleBuffer: [Float] = []
    private let bufferSize = 512

    // MARK: - 公有方法

    public init(config: Config = Config(), delegate: SileroVADDelegate? = nil) {
        self.config = config
        self.delegate = delegate

        // 计算帧数阈值(基于配置动态计算窗口时长)
        let windowDurationSecs = Float(bufferSize) / Float(config.sampleRate)
        speechFrameThreshold = Int(config.startSecs / windowDurationSecs)
        silenceFrameThreshold = Int(config.stopSecs / windowDurationSecs)

        guard let modelPath = Bundle.main.path(forResource: "silero_vad", ofType: "onnx") else {
            fatalError("SileroVAD: Model file not found in bundle")
        }

        do {
            let env = try ORTEnv(loggingLevel: .warning)
            let sessionOptions = try ORTSessionOptions()

            // 性能优化配置
            try sessionOptions.setGraphOptimizationLevel(.all)
            try sessionOptions.setIntraOpNumThreads(Int32(ProcessInfo.processInfo.processorCount))

            // 尝试启用Core ML硬件加速
            do {
                let coreMLOptions = ORTCoreMLExecutionProviderOptions()
                try sessionOptions.appendCoreMLExecutionProvider(with: coreMLOptions)
                print("SileroVAD: Using Core ML Execution Provider (Neural Engine/NPU)")
            } catch {
                print("SileroVAD: Using optimized CPU execution with \(ProcessInfo.processInfo.processorCount) cores")
            }

            session = try ORTSession(env: env, modelPath: modelPath, sessionOptions: sessionOptions)

        } catch {
            fatalError("SileroVAD: Failed to create ONNX session: \(error)")
        }

        // 初始化RNN状态 (shape: 2, 1, 128)
        let stateData = Array(repeating: Float(0.0), count: 2 * 1 * 128)
        do {
            state = try ORTValue(tensorData: NSMutableData(data: Data(bytes: stateData, count: stateData.count * 4)),
                                 elementType: .float,
                                 shape: [2, 1, 128])
        } catch {
            fatalError("SileroVAD: Failed to create initial state tensor: \(error)")
        }
    }

    /// 输入音频样本,自动处理状态检测
    public func feed(_ samples: [Float]) {
        sampleBuffer.append(contentsOf: samples)

        // 当有足够样本时自动检测
        while sampleBuffer.count >= bufferSize {
            if let probability = performDetection() {
                updateVADState(probability: probability)
            }
        }
    }

    /// 重置内部状态机 & RNN 隐状态
    public func reset() {
        // 重置状态机
        vadState = .silence
        speechFrameCount = 0
        silenceFrameCount = 0
        lastProbability = 0.0

        // 清空缓冲区
        sampleBuffer.removeAll()

        // 重置RNN状态
        let stateData = Array(repeating: Float(0.0), count: 2 * 1 * 128)
        do {
            state = try ORTValue(tensorData: NSMutableData(data: Data(bytes: stateData, count: stateData.count * 4)),
                                 elementType: .float,
                                 shape: [2, 1, 128])
        } catch {
            print("SileroVAD: Failed to reset state tensor: \(error)")
        }
    }

    // MARK: - 私有方法

    private func performDetection() -> Float? {
        guard sampleBuffer.count >= bufferSize else {
            return nil
        }

        // 取出一个窗口的样本
        let vadInput = Array(sampleBuffer.prefix(bufferSize))
        sampleBuffer.removeFirst(bufferSize)

        do {
            let probability = try runInference(audioData: vadInput)
            lastProbability = probability
            return probability
        } catch {
            print("SileroVAD: Detection error: \(error)")
            return nil
        }
    }

    private func runInference(audioData: [Float]) throws -> Float {
        guard audioData.count == 512 else {
            throw VADError.invalidAudioFormat("Audio data must be exactly 512 samples")
        }

        // 创建输入张量
        let inputTensor = try ORTValue(
            tensorData: NSMutableData(data: Data(bytes: audioData, count: audioData.count * 4)),
            elementType: .float,
            shape: [1, 512]
        )

        // 创建采样率张量
        var srData = Int64(config.sampleRate)
        let srTensor = try ORTValue(
            tensorData: NSMutableData(data: Data(bytes: &srData, count: 8)),
            elementType: .int64,
            shape: [1]
        )

        // 准备输入
        let inputs: [String: ORTValue] = [
            "input": inputTensor,
            "state": state,
            "sr": srTensor,
        ]

        // 执行推理
        let allOutputNames = try session.outputNames()
        let outputs = try session.run(withInputs: inputs, outputNames: Set(allOutputNames), runOptions: nil)

        // 提取结果
        guard let outputTensor = outputs["output"] else {
            throw VADError.inferenceError("Missing 'output' tensor")
        }

        guard let newStateTensor = outputs["stateN"] else {
            throw VADError.inferenceError("Missing 'stateN' tensor")
        }

        // 更新状态
        state = newStateTensor

        // 提取概率值
        let tensorData = try outputTensor.tensorData() as Data
        let probability = tensorData.withUnsafeBytes { bytes in
            bytes.load(as: Float.self)
        }

        return probability
    }

    private func updateVADState(probability: Float) {
        let isHighProbability = probability >= config.threshold
        let isLowProbability = probability <= config.negThreshold

        switch vadState {
        case .silence:
            if isHighProbability {
                vadState = .speechCandidate
                speechFrameCount = 1
                silenceFrameCount = 0
            }

        case .speechCandidate:
            if isHighProbability {
                speechFrameCount += 1
                if speechFrameCount >= speechFrameThreshold {
                    vadState = .speech
                    delegate?.vadDidStartSpeech(probability: probability)
                }
            } else {
                vadState = .silence
                speechFrameCount = 0
            }

        case .speech:
            if isLowProbability {
                vadState = .silenceCandidate
                silenceFrameCount = 1
                speechFrameCount = 0
            } else if isHighProbability {
                // 继续说话,重置静音计数
                silenceFrameCount = 0
            }

        case .silenceCandidate:
            if isLowProbability {
                silenceFrameCount += 1
                if silenceFrameCount >= silenceFrameThreshold {
                    vadState = .silence
                    delegate?.vadDidEndSpeech(probability: probability)
                }
            } else if isHighProbability {
                vadState = .speech
                silenceFrameCount = 0
            }
        }
    }
}

要下载模型silero_vad.onnx丢进项目。

当然这个代码也是Claude帮我写的。

Swift 的多平台策略,需要我们大家一起来建设 | 肘子的 Swift 周报 #091

作者 东坡肘子
2025年7月1日 08:00

issue91.webp

weekly.fatbobman.com 订阅本周报的电子邮件版本。访问我的博客 肘子的 Swift 记事本 查看更多的文章。加入 Discord 社区,与 2000+ 中文开发者深入交流 Swift、SwiftUI 开发体验。

Swift 的多平台策略,需要我们大家一起来建设

继 2025 年 2 月 Swift 社区论坛发布关于启动 Android Community Workgroup 的消息数月后,Swift.org 于上周正式宣布成立官方 Android 工作组。这标志着由官方主导的 Swift 安卓平台支持正式启动,未来 Swift 开发者有望获得更完善的安卓适配工具链与开发体验。

不过,在欣喜之余,我们也应正视一个现实:对于绝大多数 Swift 开发者来说,长期以来的开发工作深度依赖苹果生态,日常所用 API 多与系统框架强耦合。尽管 Swift 社区和苹果已着手推进 Foundation 的纯 Swift 化改造,并陆续提供更多跨平台基础库,但这距离满足实际跨平台开发的需求仍有相当差距。

不久前,Swift Package Index 在原有对苹果平台和 Linux 的兼容性标识基础上,新增了对 Android 与 Wasm 平台的支持,侧面反映出社区对多平台适配的重视。我也借此机会让自己的两个库完成了对 Linux 的兼容。不过在适配过程中也深刻体会到,目前还缺乏一个便捷、统一的跨平台开发环境。虽然这两个库的适配较为简单,仅通过 GitHub Actions 就完成了编译测试和修复,但若将来需要支持更多平台,社区能否构建一个便利、安全的适配机制将变得至关重要。

近年来,Swift 在多平台战略上的推进明显提速,但若想真正成为跨平台开发者的主流选择,仅靠官方与苹果的努力还远远不够。我们每一位 Swift 开发者的参与同样不可或缺。Swift 越强大,Swift 开发者越受益。Swift 的多平台生态,需要我们共同建设!

前一期内容全部周报列表

原创

NotificationCenter.Message:Swift 6.2 并发安全通知的全新体验

NotificationCenter 作为 iOS 开发中的经典组件,为开发者提供了灵活的广播——订阅机制。然而,随着 Swift 并发模型的不断演进,传统基于字符串标识和 userInfo 字典的通知方式暴露出了诸多问题。为了彻底解决这些痛点,Swift 6.2 在 Foundation 中引入了全新的并发安全通知协议:NotificationCenter.MainActorMessageNotificationCenter.AsyncMessage。它们充分利用 Swift 的类型系统和并发隔离特性,让消息的发布与订阅在编译期就能得到验证,从根本上杜绝了“线程冲突”和“数据类型错误”等常见问题。

近期推荐

Xcode Coding Intelligence 逆向解析简报 (Reverse-Engineering Xcode's Coding Intelligence Prompt)

在 Xcode 26 中,苹果正式推出了备受期待的 AI 编码助手 —— Coding Intelligence。相较于市面上已有的 AI 编程工具,苹果在系统提示词(system prompt)的设计上是否有自己的哲学?Peter Friese 借助 Proxyman 对其进行了深入逆向分析。通过这些解析出的提示词内容,我们不仅可以了解 Coding Intelligence 的工作机制,也能窥见苹果对现代开发实践的倾向性,比如:强烈推荐使用 Swift Concurrency(async/await、actor)而非 Combine,测试建议使用 Swift Testing 框架与宏。这些设计细节,是苹果开发范式的重要指标。


SwiftUI 设计系统中的语义颜色设计 (SwiftUI Design System Considerations: Semantic Colors)

在构建 SwiftUI 设计系统 API 时,如何优雅地处理 语义颜色(Semantic Colors) 始终是一个令人头疼的问题。Magnus Jensen 在本文中系统梳理了常见方案的优缺点,并提出了一种基于宏(macro)的解决路径,力求实现 可读性强、类型安全、上下文感知 的色彩系统。如果你正打算为自己的 SwiftUI 项目设计一套结构清晰、可维护的风格体系,这篇文章值得一读。


iOS 内存效率指南系列 (Memory Efficiency in iOS)

随着项目复杂度的提升,开发者终将面对内存相关的问题:内存泄漏、系统警告,甚至因资源占用过高被系统强制终止。在这种情况下,如何诊断问题、控制内存占用,是对开发者经验与体系理解的深度考验。Anton Gubarenko 在两篇文章(内存优化篇)中,系统梳理了 iOS 应用内存使用的评估方式、诊断工具以及优化手段,构建出一套完整、实用的内存管理知识体系。


What is @concurrent in Swift 6.2?

从 Swift 最近的几个版本更新和 Xcode 26 的表现可以看出,Swift 团队正有意识地优化并发编程的开发体验。通过启用新的默认行为,开发者无需在一开始就理解所有细节,便能写出更安全的并发代码。@concurrent 的引入,正是这一策略下的产物之一。在 Donny Wals 的这篇文章中,他详细介绍了 @concurrent 的背景与用途。简单来说,@concurrent 是 Swift 6.2 引入的显式并发标记,主要用于在启用 NonIsolatedNonSendingByDefault 特性时,明确指定函数运行在全局执行器上,从而在需要时将工作负载转移到后台线程,避免阻塞调用者所在的 actor(如主线程)。

或许有人会质疑 Swift 是否又在“用新关键字补旧洞”,但从语言设计趋势来看,随着并发模型逐步完善,许多旧关键字的使用将逐渐被默认机制吸收、简化甚至隐藏。


Swift 与 Java 互操作 (Swift 6.2 Java interoperability in Practice)

Swift 与 Java 的互操作并非新鲜事物,但过往的解决方案往往过程复杂且容易出错。Swift 6.2 引入的 swift-java 包具有划时代意义——这是首次提供官方支持、与工具链深度集成、开发体验接近一等公民的互操作方案,标志着 Swift 和 Java 之间真正意义上的“无缝互通”正式到来。Artur Gruchała 通过一个完整的示例项目,详细演示了如何从 Swift 端调用 Java 方法、构建双语言协作的 CLI 应用,并深入分析了实际开发中容易踩坑的关键细节——特别是 classpath 配置等看似简单却至关重要的环节。


Kodeco 教程:迁移到 Swift 6 (Migrating to Swift 6 Tutorial)

Swift 6 引入了更严格的并发规则与更加结构化的编程范式。在迁移过程中,理解隔离域、Sendable 类型、默认行为,以及 @concurrent 的使用变得尤为重要。Audrey Tam 通过一个完整的 SwiftUI 示例项目(附项目源码),系统演示了从 Swift 5 迁移至 Swift 6.2 的全过程,涵盖 Xcode 设置、并发语义调整与数据隔离等核心环节,是一篇很具实用价值的迁移教程。


Modern Concurrency - Swift 6.2 Suite of Examples

如何在 async/await 中实现类似 Combine 的 throttle 操作?如何持续追踪 @Observable 属性的变化?如何构建支持多消费者的异步流?Lucas van Dongen 在这个开源项目中给出了系统性的实践示例。他汇集了 Swift 6.2 并发模型下的多种模式,演示了如何在实际项目中逐步替代 Combine,迁移到更现代、类型安全的并发范式。


是否升级应用的最低支持版本?(Considerations for New iOS Versions)

WWDC 25 中 Liquid Glass 的登场令人惊艳,但要同时支持两种视觉风格,对开发资源是一大考验。这也让很多开发者开始思考是否应放弃对旧系统的支持。David Smith 建议从两个角度判断:现有用户影响新用户流失。以他的 Widgetsmith 应用为例,当前仍有约 9% 的新增用户来自旧系统,一旦抬高最低支持版本将直接失去这部分潜在用户。他认为,只有当旧系统用户占比降至个位数时,再做版本升级才更合理——简化技术负担,不应以牺牲业务增长为代价

活动

AdventureX 25 游客指南

AdventureX 25 将于 2025 年 7 月 23 日至 27 日在杭州市湖畔创研中心与未来科技城学术交流中心举行。本指南包含活动行程介绍、参与方式、群聊福利、出行与住宿建议及注意事项等内容。不论你是来逛展、互动,还是寻找志同道合的伙伴,这份指南都将帮助你轻松规划行程~

往期内容

THANK YOU

如果你觉得这份周报或者我的文章对你有所帮助,欢迎 点赞 并将其 转发 给更多的朋友。

weekly.fatbobman.com 订阅本周报的电子邮件版本。访问我的博客 肘子的 Swift 记事本 查看更多的文章。加入 Discord 社区,与 2000+ 中文开发者深入交流 Swift、SwiftUI 开发体验。

Swift底层原理学习笔记

作者 逍遥归来
2025年6月30日 16:02

笔记主要记录Swift和OC底层原理差异的地方,OC的底层原理之前的笔记有详细记录。

课程是逻辑教育的,视频基本只看了总结部分,然后结合网上已有笔记进行的重点梳理。

Swift 进阶一:类,对象,属性

类、对象

  • Swift对象的内存结构HeapObject,有两个属性:一个是Metadata,一个是Refcount,默认占用16字节大小,就是对象中没有任何东西也是16字节。
  • OC中实例对象的本质是结构体,是以objc_object为模板继承的,其中有一个isa指针,占8字节。
  • Swift比OC中多了一个refCounted引用计数大小,也就是多了8字节。

  • getClassObject函数是根据kind获取object类型
  • 如果kind(理解为isa指针)是Class类型,则将当前的Metadata强转成ClassMetadata,而ClassMetadataTargetClassMetadata根据类型的别名,其中TargetClassMetadata结构: image.png
  • TargetClassMetadata继承自TargetAnyClassMetadata image.png
  • 结构与OC中的objc_class的结构一样,有isa,有父类,有cacheDataData类似于objc_class中的bits
  • 根据上面分析我们可以得到结论:当metadatakindClass时,有如下的继承关系: image.png

  • 总结,swift的类内存结构可以理解为:
struct Metadata {
    void *kind;               // 类型标识(如类、结构体、枚举)
    void *superClass;         // 父类的 Metadata 指针
    void *cacheData;          // 方法缓存(类似 OC 的 cache_t)
    void *data;               // 指向额外数据的指针
    
    // 实例布局信息
    uint32_t flags;                   // 类型标志位
    uint32_t instanceAddressOffset;   // 实例变量的起始偏移量
    uint32_t instanceSize;            // 实例对象占用的内存大小
    uint16_t instanceAlignMask;       // 实例的对齐掩码
    uint16_t reserved;                // 保留字段
    
    // 类布局信息
    uint32_t classSize;               // 类对象占用的内存大小
    uint32_t classAddressOffset;      // 类变量的起始偏移量
    void *description;                // 类型描述信息
    
    
    // 类对象与元类对象的关键区别
    uint32_t flags;       // 包含类型标志位(如是否为元类)
    void *vtable;         // 类对象的 vtable 指向实例方法表
    void *classVtable;    // 元类对象的 vtable 指向类方法表
    
    // 方法签名表(所有方法),存放在类对象中
    MethodDescriptor* methodDescriptors;
    uint32_t methodCount;
}

// 例子:
class MyClass { 
    func instanceMethod() {} // 实例方法 
    static func classMethod() {} // 类方法 
} 
// 实例方法调用(通过类对象的 vtable) 
let obj = MyClass() obj.instanceMethod() // 类对象 → Metadata → vtable → 方法实现 

// 类方法调用(通过元类对象的 classVtable) 
MyClass.classMethod() // 类对象 → 元类对象 → Metadata → classVtable → 方法实现


属性

    1. 存储属性:有常量存储属性和变量存储属性两种,都占用内存
    1. 计算属性:不占用内存,本质为函数。
    1. 属性观察者
      1. 属性观察可以添加在类的存储属性继承的存储属性继承的计算属性
      1. 父类在调用init中改变属性值不会触发属性观察,子类调用父类的init触发属性观察
      1. 统一属性在父类和子类都添加观察,在触发观察时:
      • willSet方法,先子类后父类
      • didSet方法,先父类后子类
    1. 延迟属性(lazy) :延迟属性必须有初始(可以为nil),只有在访问后内存中才有值,延迟属性对内存有影响,不能保证线程安全
    1. 类型属性:类型属性必须有初始值,内存只分配一次,通过swift_once函数创建,类似dispatch_once,是线程安全的。

    • 可以用于单例:
    class XXX {
      static let share: XXX = XXX()
      private init(){}
    }
    

Swift 进阶二:值类型、引用类型、结构体

结构体,值类型

struct WSPerson { 
    var age: Int = 18 
} 
    
struct WSTeacher { 
    var age: Int 
}

image.png

  • 结构体会自动创建为所有参数赋值的构造函数。
  • 结构体开辟的内存在栈区
  • 结构体的赋值是深拷贝,并且有写时复制的机制。

结构体的属性修改问题

  • 结构体对象self类型为let,即不可以被修改。
  • 结构体中函数修改属性, 需要在函数前添加mutating关键字,本质是给函数的默认参数self添加了inout关键字,将selflet常量改成了var变量。
  • mutating方法修改结构体属性时,采用的是 "in-place" 的方式,也就是直接在当前实例的内存空间里修改属性值,并没有重新创建一个新的实例来替换原来的实例。这一特性和赋值操作有着本质的区别。

结构体的函数调用

  • 值类型对象的函数的调用方式是静态调用,即直接地址调用,调用函数指针,这个函数指针在编译、链接完成后就已经确定了,存放在代码段,而结构体内部并不存放方法。因此可以直接通过地址直接调用 image.png 这个符号哪里来的?

  • 是从Mach-O文件中的符号表Symbol Tables,但是符号表中并不存储字符串,字符串存储在String Table(字符串表,存放了所有的变量名和函数名,以字符串形式存储),然后根据符号表中的偏移值到字符串中查找对应的字符,然后进行命名重整:工程名+类名+函数名

方法重载问题

  • Objective-C里,方法重载是不被支持的,不过Swift却支持,这主要是由它们不同的函数签名机制和语言设计理念造成的。
  1. 函数签名机制
  • Objective - C:它的函数签名只依据方法名,和参数类型没有关系。 比如下面这两个方法,在OC看来是一样的,所以无法共存:
- (void)doSomethingWithInt:(int)value; 
- (void)doSomethingWithInt:(NSString *)value; 
  • Swift:它的函数签名是由方法名和参数类型共同组成的。 下面这样的重载在Swift中是被允许的:
func doSomething(value: Int) 
func doSomething(value: String) 

2. 消息传递机制

  • OC:采用的是运行时消息传递机制,方法调用是通过字符串(SEL)来实现的。 像[obj doSomethingWithInt:1]这样的调用,在运行时会被解析为SEL @selector(doSomethingWithInt:),要是有多个同名方法,就会引发冲突。
  • Swift:使用的是静态 dispatch 机制,在编译时就会确定具体要调用哪个方法。
  1. 补充说明
  • Swift 的重载:除了参数类型不同可以重载外,参数数量不同或者参数标签不同也能实现重载。
  • OC 的替代方案:在OC中,如果要实现类似功能,通常会采用命名约定,例如doSomethingWithInt:doSomethingWithString:

总结来说,Swift支持方法重载是其类型系统和编译时检查机制的自然结果,而OC不支持则是受限于其动态特性和历史设计。

Swift 进阶三:内存分区、方法调度、指针

方法调度

  • Swift 类的方法(非final、非static、非@objc修饰的)会被存放在一个名为 vtable 的表中。
  • 只有类能够使用 vtable,结构体和枚举由于不支持继承,所以没有 vtable
内存布局示例:
[实例对象内存]
  ├ isa 指针 ───→ [类对象]
                ├ Metadata 指针 ───→ [Metadata]
                │                  └ vtable 指针 ───→ [vtable 内存区域]
                │                                      ├ 0: init()
                │                                      ├ 1: method1()
                │                                      └ 2: method2()
                └ 其他类数据...
  • 方法调用时的流程,当调用一个类的实例方法时,Swift 运行时会:
    1. 通过实例的 isa 指针找到类对象。
    2. 从类对象中获取 Metadata 指针。
    3. 从 Metadata 中读取 vtable 指针。
    4. 根据方法在 vtable 中的索引,调用对应的函数实现。
  • vtable 仅存储可重写的方法,而类的所有方法(包括不可重写的)仍通过元数据(Metadata)管理。
  • 元类对象(Metaclass Object)的 Metadata 主要存储类方法(static/class 方法)的实现信息。
  • 协议方法的签名和实现由 Witness Table 管理,与类对象 / 元类对象的 Metadata 是分离的。
方法调用总结
  • struct值类型,它的函数调度是直接调用,即静态调度
    • 值类型在函数中如果要修改实例变量的值,则函数前面需要添加Mutating修饰
  • class引用类型,它的函数调度是通过vtable函数,即动态调度
  • extension中的函数是直接调用,即静态调度
  • final修饰的函数是直接调用,即静态调度
  • @objc修饰的函数是methodList函数表调度,如果方法需要在OC中使用,则类需要继承NSObject
  • dynamic修饰的函数调度方式是methodList函数表调度,它是动态可以修改的,可以进行method-swizzling
    • @objc+dynami修饰的函数是通过objc_msgSend来调用的
  • 如果函数中的参数想要被更改,则需要在参数的类型前面增加inout关键字,调用时需要传入参数的地址

Swift 进阶四:弱引用、闭包、元类型

Swift 内存管理

  • swift实例对象的内存中,存在一个Metadata,一个Refcount。后者记录引用计数。
  • Refcount最终可以获得64位整型数组bits,其结构: image.png
// 简化的 Refcount 结构(实际实现可能更复杂)
struct Refcount {
    // 64 位中的高 32 位:强引用计数
    uint32_t strongRefCount: 32;
    
    // 64 位中的低 32 位:
    uint32_t hasWeakRefs: 1;      // 是否有弱引用
    uint32_t hasUnownedRefs: 1;   // 是否有 unowned 引用
    uint32_t isDeiniting: 1;      // 是否正在析构
    uint32_t sideTableMask: 1;    // 是否使用 Side Table
    uint32_t weakRefCount: 28;    // 弱引用计数
};
  • 当引用计数超出直接存储范围时,通过 sideTableMask 标志切换到全局 Side Table 存储。
  • Swift在创建实例对象时的默认引用计数是1,而OCalloc创建对象时是没有引用计数的。

弱引用

  • 为对象增加弱引用时,实际是调用refCounts.formWeakReference,即去操作sideTable表,添加对象的弱引用关系,这里和OC处理是一致的。

swift中的runtime

  • 对于纯swift类来说,没有动态特性dynamic(因为swift是静态语言),方法和属性不加任何修饰符的情况下,已经不具备runtime特性,此时的方法调度,依旧是函数表调度即V_Table调度。
  • 对于纯swift类,方法和属性添加@objc标识的情况下,可以通过runtime API获取到,但是在OC中是无法进行调度的,原因是因为swift.h文件中没有swift类的声明。
  • 对于继承自NSObject类来说,如果想要动态的获取当前属性+方法,必须在其声明前添加@objc关键字,如果想要使用方法交换,还必须在属性+方法前添加dynamic关键字,否则当前属性+方法只是暴露给OC使用,而不具备任何动态特性。

补充

  • Any:任意类型,包括function类型、optional类型
  • AnyObject:任意类的instance、类的类型、仅类遵守的协议,可以看作是Any的子类
  • AnyClass:任意实例类型,类型是AnyObject.Type
  • T.self:如果T是实例对象,则表示它本身,如果是类,则表示metadata.T.self的类型是T.Type

深度解析!Apple App Site Association 文件背后的秘密和配置攻略

作者 iOS新知
2025年6月30日 14:22

这里每天分享一个 iOS 的新知识,快来关注我吧

前言

image.png

在 iOS 生态系统中,Apple App Site Association(AASA)文件扮演着至关重要的角色。它通过在你的 iOS 应用和网络域之间建立安全且经过验证的链接,实现了诸如通用链接(Universal Links)、共享网络凭证、Handoff 和 App Clips 等功能。

不知道你有没有注意过,当你在手机浏览器上访问知乎、小红书或 YouTube 时,有些链接会让你继续留在浏览器中,而另一些则会直接跳转到对应的应用中?这背后的驱动力正是 AASA 文件。

AASA 文件的创建

Apple App Site Association 文件是一个配置文件,它定义了 URL 的处理方式,并指定它们是打开在浏览器中还是直接链接到应用程序内的内容。

以下是一个基本示例:

{
  "applinks": {
    "apps": [],
    "details": [
      {
        "appID": "ABCDE12345.com.example.app",
        "paths": [
          "/",
          "/about-us/",
          "/products/*",
          "/services/detail/?id=*",
          "/news/article/*",
          "/promo/*",
          "NOT /private/",
          "NOT /settings/*",
          "*.html"
        ]
      }
    ]
  },
  "webcredentials": {
    "apps": ["ABCDE12345.com.example.app"]
  },
  "appclips": {
    "apps": ["ABCDE12345.com.example.app"]
  }
}

访问下边这些路径的请求都会将用户路由到应用程序中:

"/",
"/about-us/",
"/products/*",
"/services/detail/?id=*",
"/news/article/*",
...

而对于那些以 NOT 前缀的路径,系统将不会重新路由用户,允许体验继续在浏览器中进行:

"NOT /private/",
"NOT /settings/*",
...

部署 AASA 文件

当你准备好部署 AASA 文件时,需要将其托管在网站的根目录或 .well-known 目录下:

  • /apple-app-site-association

  • /.well-known/apple-app-site-association

注意:AASA 文件不应有文件扩展名。为了让内容分发网络(CDN)成功缓存此文件,它必须托管在可供所有 IP 地址和范围访问的域上,通过 HTTPS 提供文件,不重定向,并且不被访问策略阻止。

你还应该确保服务器以 Content-Type: application/json 的形式提供文件,并且文件大小不超过 128KB。

当你首次托管 AASA 文件时,苹果的 CDN 将在 24 小时内获取它,这意味着它们将在文件发布的第一天内从你的服务器请求并缓存一份副本。

根据苹果文档的说法,这个 24 小时的窗口仅与首次将文件放入苹果的 CDN 有关。随后的更新会在不同的时间间隔发生。

其实我们可以通过下边的链接看下其他公司的 AASA 文件是如何写的:

测试与验证

随着应用程序的迭代,你将需要修订 AASA 文件,以便为新的用户流程整合通用链接(Universal Link)。

AASA 文件中的错误很常见,因此验证此文件的行为和语法成为流程中不可或缺的一部分。一旦被苹果的 CDN 确认,此文件中的任何错误都可能影响所有用户的通用链接行为——无论是新用户还是老用户。

幸运的是,在更改到达苹果之前,有几种方法可以验证 AASA 文件的行为。

绕过 CDN

关联域支持一种备用模式,允许开发者通过绕过 CDN 直接从服务器获取文件,以验证修改后的 AASA 文件的行为。

要在 Xcode 项目中激活此模式:

  • 前往 Signing & Capabilities -> Associated Domains

  • 在域条目的末尾添加 ?mode=developer(例如:applinks:yourdomain.com?mode=developer

现在,当你构建并运行应用程序时,它将直接从你的服务器检索更新的 AASA 文件。

在开发期间只能使用备用模式,你必须在将应用程序提交到 App Store 之前从关联域中删除后面这段字符串。

验证 AASA 文件配置

你可以使用以下工具来验证 AASA 文件的语法和配置:

  • Universal Link & Apple App Site Association Testing Tool[1]

这是一个免费的工具,用于验证和测试 Apple App Site Association(AASA)文件。确保你的通用链接配置正确,通过简单的链接创建、实时测试和团队协作功能简化 AASA 文件的故障排除。

官方查询方式

你可以通过访问以下网址检查苹果的 CDN 是否已获取文件的最新版本:

  • https://app-site-association.cdn-apple.com/a/v1/{YOUR_DOMAIN_HERE}

如果 CDN 确实具有文件的最新版本,那么任何新应用安装也将获得此最新版本。但是对于老用户,他们的设备每周只会检查一次更新的副本。

重新安装应用程序将从 CDN 获取最新版本。

实际上,AASA 文件的任何更改的推出周期是 CDN 刷新其缓存所需的时间与现有用户每周检查的时间相结合。

缓存更新时间

为了更好地理解缓存版本更新前剩余的时间,我们可以查看来自 CDN 的响应头中的 Cache-Control 字段。

Cache-Control 头由网络服务器使用,以决定浏览器和中间缓存(如 CDN)应该如何以及在多长时间内缓存文件的提供版本。

在 Facebook 和 Yelp 上,我们可以通过以下链接查看:

  • https://app-site-association.cdn-apple.com/a/v1/facebook.com

  • https://app-site-association.cdn-apple.com/a/v1/yelp.com

然后我们可以通过从 max-age 中减去 age 来确定缓存何时更新:

image.png

image.png

  • max-age:6 小时,缓存刷新时间为 17,760 秒 / 约 5 小时。

  • max-age:1 小时,缓存刷新时间为 3,112 秒 / 约 50 分钟。

在实际情况中,最短的 max-age 是 3,600 秒,即 1 小时,最长的是 21,600 秒,即 6 小时。

值得注意的是,苹果的 CDN 会覆盖原始网站指定的 Cache-Control 设置。例如,直接从 Yelp 访问 AASA 文件的 max-age 为 1200,但从苹果的 CDN 检索时,其 max-age 为 3600。

通过更长时间的缓存文件,苹果能够最大限度地减少对原始服务器的请求频率,从而减少网络和原始服务器的流量和负载。

总结

AASA 文件是 iOS 生态系统中一个非常重要的组件,它通过在 iOS 应用和网络域之间建立安全且经过验证的链接,实现了诸如通用链接(Universal Links)、共享网络凭证、Handoff 和 App Clips 等功能。

通过了解 AASA 文件的创建、部署、测试与验证,开发者可以更好地优化其应用的链接配置,提升用户体验,并确保应用的稳定性和可靠性。

参考资料

[1]

Universal Link & Apple App Site Association Testing Tool: getuniversal.link/?ref=digita…

这里每天分享一个 iOS 的新知识,快来关注我吧

本文同步自微信公众号 “iOS新知”,每天准时分享一个新知识,这里只是同步,想要及时学到就来关注我吧!

Swift 的多平台策略,需要我们大家一起来建设 - 肘子的 Swift 周报 #91

作者 Fatbobman
2025年6月30日 22:00

继 2025 年 2 月 Swift 社区论坛发布关于启动 Android Community Workgroup 的消息数月后,Swift.org 于上周正式宣布成立官方 Android 工作组。这标志着由官方主导的 Swift 安卓平台支持正式启动,未来 Swift 开发者有望获得更完善的安卓适配工具链与开发体验。

Android生态震荡:Swift官方支持落地,跨平台开发迎来“原生级”方案

2025年6月30日 11:02

前言

Swift.org 在 2025年6-26日宣布成立 Android Workgroup,目标是:将Android确立为Swift的官方支持平台并持续维护

Establish and maintain Android as an officially supported platform for Swift.


为什么要做出这个决定?

原因1:跨平台需求高

  • 目前开发者的跨平台开发需求很高;
  • iOS 团队希望一套 Swift 业务逻辑可复用到 Android;
  • Server-Side Swift & Embedded 已验证多平台可行性。

原因2:竞品压力

Google的Kotlin Multiplatform 正快速发展,Flutter也逐渐成熟,成为跨端主流开发方案,官方希望能够提升 Swift 的竞争力。

下面将主要讲解:

  1. Swift支持的Android内容
  2. 关键技术实现
  3. 具体使用
  4. 对比主流跨平台方案
  5. 对开发者的影响

1. Swift支持的Android内容

“官方支持”绝非简单的“Swift代码能在Android编译”就算完成,包括:

1.1 编译器与工具链

  • 为Android的AArch64、x86-64和armv7架构生成预编译SDK
  • 与 NDK 链接脚本、libswiftCore、libdispatch 等系统库适配

1.2 持续集成系统

所有Swift的Pull Request将自动运行Android目标测试,防止平台兼容性退化

1.3 核心库适配

Foundation和Concurrency等核心库将针对Android的文件系统、线程模型差异进行专门优化

1.4 互操作桥接

设计Swift与Java/Kotlin之间的双向调用机制,打破语言壁垒

1.5 开发体验提升

  • 将提供Gradle/Bazel/SwiftPM插件支持
  • VS Code扩展也在开发路线图中

2. 关键技术实现:LLVM

Swift对Android的支持并非从零开始构建,Swift编译器从诞生之初就基于LLVM架构,而Android NDK从R13版本开始就完全转向基于LLVM的Clang编译器。

这种同源架构使得Swift编译器能够被“重定向”,为Android支持的CPU架构生成原生机器码。在具体实现上:

  • Swift直接链接Android的native日志系统(logcat),而非创建自己的日志机制
  • 通过Android NDK提供的LLVM工具链完成交叉编译
  • 运行时库适配Android的bionic libc和线程模型

3. 实战体验:Swift on Android初探

虽然完整支持还在路上,但开发者已经可以尝试Swift开发Android应用。以下是一个简单的互操作示例:

// Swift
@_cdecl("sayHello")
public func sayHello() -> UnsafePointer<CChar> {
    return strdup("Hello from Swift 🐦")
}

// kotlin
class HelloBridge {
    companion object {
        init { System.loadLibrary("hello") }
    }
    external fun sayHello(): String
}

通过JNI将Swift函数挂载到Kotlin,编译的.so文件打包进APK即可运行。

社区已有Tokamak UI框架等尝试支持Android,预示着更完整的解决方案即将到来。


4. 对比主流跨平台方案

对比主流的KMP、Flutter、RN方案


5. 对开发者的意义

对于iOS开发者:跨平台新机遇

  • 多端拓展:轻松将iOS应用扩展到Android平台,覆盖更广泛的用户群体
  • 业务逻辑100%复用:Model层、网络模块和核心算法可以直接共享,无需重写

对于Android开发者:新语言选项

  • 人才流动加速:熟悉Swift的iOS开发者将更容易参与Android项目开发
  • 性能敏感模块的新选择:可将Swift作为“更安全的C++”使用于高性能计算模块

社区与生态影响

  • 学习曲线降低:新人学一门Swift即可开发iOS、Android、服务端等多平台应用
  • 跨平台工具链成熟:Swift Package Manager将成为真正的跨平台依赖管理工具
  • 社区协作加强:Android工作组采用开放协作模式,任何开发者都可参与贡献

结语

这次不仅是Swift语言的一次跨界尝试,更是移动开发的一次生态震荡:苹果主导语言首次系统性拥抱 Google 生态,同时也是苹果对于跨端研发模式与 Kotlin / KMP 生态 的全新博弈。

参考链接: github.com/swiftlang/s… github.com/swiftlang/s…

Objective-C 和 Swift 闭包内部实现详解

2025年6月30日 09:45

底层结构

c

// Block 基础结构体
struct Block_layout {
    void *isa;              // 指向类信息:_NSConcreteGlobalBlock/_NSConcreteStackBlock/_NSConcreteMallocBlock
    int flags;              // 状态标志位
    int reserved;           // 保留字段
    void (*invoke)(void *, ...); // 函数指针
    struct Block_descriptor *descriptor; // 描述信息
    // 捕获的变量跟随在此后
};

// Block 描述符
struct Block_descriptor {
    unsigned long reserved;
    unsigned long size;     // Block 总大小
    void (*copy)(void *dst, const void *src);     // 拷贝辅助函数
    void (*dispose)(const void *); // 析构辅助函数
};

// __block 变量包装结构
struct __Block_byref {
    void *isa;
    struct __Block_byref *forwarding;  // 指向真实地址
    int flags;
    int size;
    // 原始变量存储在此
};

完整实现示例

objectivec

// 原始代码
void example() {
    __block int counter = 0;
    void (^block)(void) = ^{ counter++; };
}

// 编译器转换后的伪代码
struct __block_impl_counter {
    void *isa = &_NSConcreteStackBlock;
    int flags = 0;
    int reserved;
    void (*invoke)(struct __block_impl *);
    struct __Block_descriptor *descriptor;
    struct __Block_byref_counter *counter_ref; // 捕获的__block变量
};

struct __Block_byref_counter {
    void *isa;
    __Block_byref_counter *forwarding;
    int flags;
    int size;
    int counter;  // 原始变量
};

static void __example_block_invoke(struct __block_impl *__cself) {
    struct __Block_byref_counter *counter_ref = __cself->counter_ref;
    counter_ref->forwarding->counter++; // 通过forwarding指针修改
}

static void __example_block_copy(struct __block_impl *dst, struct __block_impl *src) {
    _Block_object_assign(&dst->counter_ref, src->counter_ref, BLOCK_FIELD_IS_BYREF);
}

static void __example_block_dispose(struct __block_impl *src) {
    _Block_object_dispose(src->counter_ref, BLOCK_FIELD_IS_BYREF);
}

static struct __Block_descriptor {
    unsigned long reserved;
    unsigned long size;
    void (*copy)(struct __block_impl *, struct __block_impl *);
    void (*dispose)(struct __block_impl *);
} __block_descriptor = { 0, sizeof(struct __block_impl), __example_block_copy, __example_block_dispose };

void example() {
    // 创建__block变量
    struct __Block_byref_counter counter_ref = {
        NULL,
        &counter_ref,  // forwarding指向自己
        0,
        sizeof(struct __Block_byref_counter),
        0 // counter初始值
    };
    
    // 创建Block
    struct __block_impl block_impl = {
        &_NSConcreteStackBlock,
        BLOCK_HAS_COPY_DISPOSE,
        0,
        __example_block_invoke,
        &__block_descriptor
    };
    block_impl.counter_ref = &counter_ref;
    
    // 当Block被复制到堆时
    struct __block_impl *heap_block = malloc(sizeof(struct __block_impl));
    memcpy(heap_block, &block_impl, sizeof(struct __block_impl));
    __example_block_copy(heap_block, &block_impl);  // 处理捕获变量
    
    // 堆上Block的isa指针更新
    heap_block->isa = &_NSConcreteMallocBlock;
    
    // __block变量复制到堆
    struct __Block_byref_counter *heap_counter = malloc(sizeof(struct __Block_byref_counter));
    memcpy(heap_counter, &counter_ref, sizeof(struct __Block_byref_counter));
    heap_counter->forwarding = heap_counter;  // 指向堆上副本
    counter_ref.forwarding = heap_counter;    // 栈变量指向堆副本
    
    // 调用Block
    heap_block->invoke(heap_block);
}

关键机制:

  1. 变量捕获

    • 基本类型:值捕获
    • 对象类型:强引用捕获
    • __block变量:包装为__Block_byref结构体
  2. 内存管理

    objectivec

    // 栈Block -> 堆Block转换
    void (^heapBlock)(void) = [^{} copy];
    [heapBlock release];
    
  3. 循环引用解决方案

    objectivec

    __weak typeof(self) weakSelf = self;
    self.block = ^{ 
        __strong typeof(weakSelf) strongSelf = weakSelf;
        [strongSelf doSomething];
    };
    

Swift Closure 内部实现原理及代码

底层结构

c

// Swift闭包在SIL(Swift中间语言)中的表示
struct Closure {
    void *metadata;                     // 类型元数据
    struct HeapObject *object;          // 引用计数头部
    void (*function)(void *context);    // 函数指针
    struct Context *context;            // 捕获上下文
};

// 捕获上下文结构
struct Context {
    void *metadata;
    struct HeapObject *object;
    // 捕获的变量按顺序排列
    // 对于值类型:直接存储值
    // 对于引用类型:存储指针
};

完整实现示例

swift

// 原始Swift代码
func createClosure() -> () -> Void {
    var counter = 0
    return { counter += 1 }
}

// 编译器生成的伪代码
struct Context {
    // 引用计数头
    struct HeapObject {
        void *metadata;
        atomic_int refCount;
    } header;
    
    // 捕获的变量
    struct Box {
        int value;  // counter的存储
    } *counterBox;
};

// 闭包函数实现
void closureImp(struct Context *context) {
    context->counterBox->value += 1; // 修改捕获变量
}

// 创建闭包
Closure createClosure() {
    // 在堆上分配上下文
    struct Context *context = swift_allocObject(sizeof(struct Context));
    
    // 在堆上分配Box存储counter
    context->counterBox = swift_allocObject(sizeof(struct Box));
    context->counterBox->value = 0;
    
    // 创建闭包实例
    return (Closure){
        .metadata = ClosureTypeMetadata,
        .object = &context->header,
        .function = closureImp,
        .context = context
    };
}

// 调用闭包
let closure = createClosure()
closure.function(closure.context) // counter = 1

关键机制:

  1. 变量捕获

    swift

    var a = 10
    let closure = { 
        a += 1  // a被捕获到Box中
        print(a)
    }
    
  2. 捕获列表

    swift

    [unowned self, b = a + 1] in // 显式控制捕获
    
  3. 内存管理

    • 非逃逸闭包:栈分配
    • 逃逸闭包:堆分配(自动引用计数)
  4. 循环引用解决方案

    swift

    class MyClass {
        var closure: (() -> Void)?
        
        func setup() {
            closure = { [weak self] in
                self?.doSomething()
            }
        }
    }
    

对比总结

特性 Objective-C Block Swift Closure
内存分配 手动管理(栈/堆转换需copy 自动管理(ARC)
值类型捕获 默认值捕获,__block修饰可修改 直接可修改(自动Boxing)
引用类型 强引用捕获 强引用捕获
生命周期控制 __weak/__strong [weak]/[unowned]/捕获列表
逃逸行为 无显式标记 @escaping显式标记
优化 全局Block不捕获变量 非逃逸闭包可栈分配
调用开销 通过函数指针调用 通过上下文+函数指针调用

性能关键点:

  1. Objective-C

    objectivec

    // 优先使用栈Block
    void (^stackBlock)(void) = ^{ /* ... */ };
    
    // 需要长期持有时才复制到堆
    void (^heapBlock)(void) = [stackBlock copy];
    
  2. Swift

    swift

    // 非逃逸闭包优化(编译器自动识别)
    func optimize(@noescape _ closure: () -> Void) {
        closure()
    }
    
    // 避免不必要的捕获
    let safeClosure = { [weak self] in
        guard let self = self else { return }
        self.process()
    }
    

特殊场景处理:

Swift 自动闭包

swift

// @autoclosure 实现
func assert(_ condition: @autoclosure () -> Bool) {
    if !condition() { /* 处理 */ }
}

// 编译器转换:
func assert(_ condition: () -> Bool) {
    if !condition() { /* ... */ }
}
assert({ 2 > 1 }())  // 自动包装

Objective-C Block 与 C 交互

objectivec

// 作为C函数参数
void acceptBlock(void (^block)(void));

// 桥接调用
void (^ocBlock)(void) = ^{ NSLog(@"Called"); };
acceptBlock((__bridge void (^)(void))ocBlock);

通过深入分析底层实现,可以更好地理解闭包的内存行为、性能特征和最佳实践,避免常见陷阱如循环引用和性能损耗。

新版 Xcode 中 CoreData 模型编辑器显示拓扑图功能取消的替代方案

2025年6月29日 20:03

在这里插入图片描述

概述

何曾几时,小伙伴们在 Xcode 的 CoreData 模型编辑器里可以肆无忌惮的浏览数据库表结构的拓扑图,造福了我们这些秃头码农们,可惜这一功能现在已不复存在!

在这里插入图片描述

那么,还有没有什么替代方案呢?本文由此应运而生了。

在本篇博文中,您将学到如下内容:

    1. Xcode 中 CoreData 模型编辑器的现状
    1. 替代方案与工具
    • 方案一:使用第三方工具生成拓扑图
    • 方案二:通过代码或调试工具查看
    • 方案三:手动检查模型文件
    1. 未来可能的改进

众所周知,Core Data 模型编辑器在 Xcode 早期版本中确实提供了可视化的关系拓扑图(如实体间的关联关系视图)显示功能,但在 Xcode 14 及之后的版本中,这一功能已被取消。

以下是当前可用的替代方案和注意事项:


1. Xcode 中 CoreData 模型编辑器的现状

在这里插入图片描述

  • 功能移除:从 Xcode 14 开始,CoreData 模型编辑器中的可视化关系图(即拓扑图)功能被移除。虽然数据模型文件(.xcdatamodeld)中仍包含 elements 部分的 XML 数据,但这些信息已不再用于显示实体间的布局关系了。
  • 模型编辑方式:开发者只能通过文本或列表形式编辑实体、属性和关系,无法直接通过图形界面查看实体间的拓扑结构。

2. 替代方案与工具

方案一:使用第三方工具生成拓扑图

  • CoreDataPro:这是一款专门用于查看和管理 Core Data 数据库的工具,支持可视化数据模型的结构和关系。用户可以通过它加载 .momd.xcdatamodeld 文件,生成实体关系图。
  • 生成 ER 图:将 Core Data 的模型文件导出为其他格式(如 XML 或 SQL),再使用数据库设计工具(如 DBDiagramMySQL Workbench)生成实体关系图。

方案二:通过代码或调试工具查看

  • 逆向 SQLite 文件:Core Data 默认使用 SQLite 作为存储格式。开发者可以通过 SQLite 浏览器(如 SQLite ManagerDB Browser for SQLite)直接查看数据库表结构,包括实体对应的表和关系字段。
  • NSManagedObject 子类生成:通过 Xcode 自动生成的 NSManagedObject 子类代码,可以间接查看实体间的关联关系。例如,若实体 BookAuthor 存在一对多关系,生成的代码中会包含 @NSManaged 修饰的关联属性。

方案三:手动检查模型文件

  • 查看 XML 内容:Core Data 的模型文件(.xcdatamodeld)本质是 XML 格式。开发者可以直接查看其内容,解析实体间的关联关系(通过 <relationship> 标签)。
  • 示例代码片段
    <entity name="Book" representedClassName="Book">
        <relationship name="author" destinationEntity="Author" inverseName="books"/>
    </entity>
    

3. 未来可能的改进

  • SwiftData 的替代方案:不知道苹果在 WWDC 2023 推出的 SwiftData 框架(基于 Core Data 优化)是否会在未来提供更现代化的数据模型管理工具,更难预料其是否支持显示可视化拓扑图。因为 SwiftData 的本意是纯描述型数据库,所以这一事件的概率估计不是很高。☺
  • 社区工具开发:开发者社区可能继续推出更强大的第三方工具,弥补 Xcode 功能缺失的不足。

总结

如果依赖可视化拓扑图进行开发,推荐以下步骤:

  1. 使用 CoreDataPro 或 SQLite 浏览器:直接查看数据库结构和关系。
  2. 结合代码生成与 XML 分析:通过生成的 NSManagedObject 子类和模型文件 XML 内容,手动验证关系逻辑。
  3. 关注苹果更新:留意 Xcode 后续版本是否重新引入相关功能,或转向 SwiftData 等新框架。

若需进一步调试数据库内容,可参考如何通过 SQLite 工具查看 Core Data 存储文件。

感谢观赏,再会啦!8-)

“一人得道,雨燕升天”:Swift 协议扩展助力 CoreData 托管类型(下)

2025年6月28日 14:49

在这里插入图片描述

概述

相信各位似秃非秃小码农们都同意,Swift 是一门现代化、安全且表现力足够丰富的语言。不过,它毕竟还是一种偏静态的语言,灵活性无法和 Python、ruby 之类的动态语言相提并论。

在这里插入图片描述

不过话虽如此,通过巧妙的一步步重构源代码,我们也可以用 Swift 完成之前貌似不可能完成的任务,所需的只是那么一丢丢耐心和执着而已。

在本篇博文中,您将学到如下内容:

  1. 一种很“硬”的解决方案
  2. 不想回到最初的样子
  3. 让编译器乖乖听话

希望在亲眼目睹本系列文章中 Swift 代码那循序渐进的重构和升华之后,小伙伴们倘若再遇到与此类似的语言设计问题,必能胸有成竹、胜券在握!

无需等待,Let‘s go!!!;)


5. 一种很“硬”的解决方案

对于前文中的问题,一种简单粗暴的解决方法是:强行让两种类型“蛮来生作”。

extension AchievementEvaluator {
    static func queryAll(context: NSManagedObjectContext) throws -> [Evaluator] {
        let request = Evaluator.fetchRequest() as! NSFetchRequest<Evaluator> // ⚠️ 强制转换
        return try context.fetch(request)
    }
}

如您所见,我们通过 Swift 强制类型转换语法,将 Evaluator.fetchRequest 实际的类型与 Evaluator 类型强行匹配。

虽然,这可以让编译器暂时闭嘴,但是也同时置我们自己于“刀山火海”之上!

上述代码的风险是:我们需要自行确保类型转换的安全性,若 Evaluator.fetchRequest() 实际返回的请求类型与 Evaluator 不匹配,将立即导致运行时发生崩溃。

6. 不想回到最初的样子

除了强行转换以外,我们还可以采用迂回战术:创建约束协议从而绕过编译器的“桎梏”。

首先,新建一个约束协议 Fetchable:

// 定义核心约束协议
protocol Fetchable: NSManagedObject {
    static func fetchRequest() -> NSFetchRequest<Self>
}

接着,对原来的 AchievementEvaluator 协议定义稍作调整,让其关联类型遵守我们上面创建的约束协议:

// 原协议调整
protocol AchievementEvaluator {
    associatedtype Evaluator: Fetchable & AchievementEvaluator // 新增 Fetchable 约束
    
    static func queryAll(context: NSManagedObjectContext) throws -> [Evaluator]
}

随后,在 AchievementEvaluator 协议扩展中利用约束关系重新打造我们的 queryAll() 方法:

extension AchievementEvaluator where Evaluator: Fetchable {
    static func queryAll(context: NSManagedObjectContext) throws -> [Evaluator] {
        let request = Evaluator.fetchRequest() // ✅ 类型已明确为 NSFetchRequest<Evaluator>
        return try context.fetch(request)
    }
}

最后,让 Achv_NoBreakVictory 成就实体类遵守 Fetchable 约束协议即可:

extension Achv_NoBreakVictory: Fetchable, AchievementEvaluator {
    typealias Evaluator = Achv_NoBreakVictory
}

虽然这种思路本身没什么问题,但可惜的是编译器还是会义无反顾的再次大声说“我恨你!”:

在这里插入图片描述

Protocol 'Fetchable' requirement 'fetchRequest()' cannot be satisfied by a non-final class ('Achv_NoBreakVictory') because it uses 'Self' in a non-parameter, non-result type position

通过上面的错误信息不难发现:大家貌似又回到了之前的“故步自封”—— 我们仍然需要让 Achv_NoBreakVictory 类加上 final 成为“孤家寡人”才能得偿所愿,这是我们不希望看到的。

所以,我们又该如何随遇而安呢?

7. 让编译器乖乖听话

其实,解决之道并没有想象的那么复杂,我们只需重新设计 Fetchable 协议即可。

我们的核心思想是:

机制 作用
entityName 属性 动态获取实体名称,避免依赖自动生成的 fetchRequest()
手动构建 NSFetchRequest 通过 NSFetchRequest<Self>(entityName:) 确保类型匹配
子类覆盖 entityName 允许继承体系中的子类指定自己的实体名称

首先,通过 实体名称动态构建请求,绕过自动生成的 fetchRequest() 方法的限制:

protocol Fetchable: NSManagedObject {
    static var entityName: String { get } // 要求实体提供名称
}

extension Fetchable {
    static func fetchRequest() -> NSFetchRequest<Self> {
        // 手动构建请求,确保类型安全
        return NSFetchRequest<Self>(entityName: entityName)
    }
}

接下来,我们只要让 Achv_NoBreakVictory 类乖巧的提供 entityName 名称即可:

extension Achv_NoBreakVictory: Fetchable, AchievementEvaluator {
    static var entityName: String {
        "Achv_NoBreakVictory"
    }
    
    typealias Evaluator = Achv_NoBreakVictory
}

现在,编译源代码将如您所愿,一切都毫无问题,整个世界清净了!

通过 动态实体名称 + 手动构建请求,既能保持类的可继承性,又能满足 Core Data 类型安全要求。其关键点在于:

  1. 通过 entityName 属性解耦实体名称与类型推断。
  2. 子类必须显式覆盖 entityName 以正确映射数据库实体。

然而,我们还可以更进一步。

观察上面 Achv_NoBreakVictory 类中对应 entityName 属性的代码可以发现:每个成就实体类的 entityName 就是它们自己类的名称。既然如此,为什么不把 entityName 也直接放到协议扩展中去呢?

extension AchievementEvaluator where Evaluator: Fetchable {
    
    static var entityName: String {
        "\(Self.self)"
    }
    
    static func queryAll(context: NSManagedObjectContext) throws -> [Evaluator] {
        let request = Evaluator.fetchRequest() // ✅ 类型已明确为 NSFetchRequest<Evaluator>
        return try context.fetch(request)
    }
}

如上代码所示,我们将原本需要每个 AchievementEvaluator 实体类实现的 entityName 属性放到了 AchievementEvaluator 协议扩展中,大大减少了重复代码,这样的 DRY 和 KISS 谁能不爱呢?棒棒哒!

或者我们干脆彻底摆脱 entityName 属性的限制,直接将其嵌入到 Fetchable 协议扩展的 fetchRequest() 方法中,让实现百尺竿头、更入佳境:

extension Fetchable {
    static func fetchRequest() -> NSFetchRequest<Self> {
        // 手动构建请求,确保类型安全
        return NSFetchRequest<Self>(entityName: "\(Self.self)")
    }
}

至此,我们通过不断迭代重构,彻底摆脱了最初文章开头 CoreData 成就托管类实现的恼人纠缠,小伙伴们还不赶快给自己一个大大的赞吧!❤️

总结

在本篇博文中,我们借助于精心设计的 Fetchable 约束协议成功的摆脱了 Swift 协议扩展中的“磨搅讹绷”,小伙伴们值得拥有!

感谢观赏,再会啦!8-)

“一人得道,雨燕升天”:Swift 协议扩展助力 CoreData 托管类型(上)

2025年6月28日 14:43

在这里插入图片描述

概述

相信各位似秃非秃小码农们都同意,Swift 是一门现代化、安全且表现力足够丰富的语言。不过,它毕竟还是一种偏静态的语言,灵活性无法和 Python、ruby 之类的动态语言相提并论。

在这里插入图片描述

不过话虽如此,通过巧妙的一步步重构源代码,我们也可以用 Swift 完成之前貌似不可能完成的任务,所需的只是那么一丢丢耐心和执着而已。

在本篇博文中,您将学到如下内容:

  1. 背景故事
  2. 想法不错,无奈编译器不允许!
  3. “不情愿”的 final
  4. DRY 制胜法宝:协议扩展(Protocol Extension)

希望在亲眼目睹本系列文章中 Swift 代码那循序渐进的重构和升华之后,小伙伴们倘若再遇到与此类似的语言设计问题,必能胸有成竹、胜券在握!

无需等待,Let‘s go!!!;)


1. 背景故事

我们的项目基于 SwiftUI + CoreData 构建,在数据库中我们需要为用户创建各种各样的成就(Achievements),因为每种成就本身有很大的不同(字段、获取手段等),所以考虑在 CoreData 数据库中使用抽象基类 + 实体类的组成方法:

  • Achievement 类是成就的抽象基类,其中包含所有成就都共有的字段和方法;
  • Achv_NoBreakVictory 类和其它实体类都“派生”于 Achievement 基类,对应于每一种具体的成就,它们包含自己独有的字段和方法;

Achievement 和 Achv_NoBreakVictory 类的定义如下所示:

@objc(Achievement)
public class Achievement: NSManagedObject {

}

@objc(Achv_NoBreakVictory)
public class Achv_NoBreakVictory: Achievement {

}

对于 Achv_NoBreakVictory 这一成就实体托管类来说,我们往往需要查询它的所有实例,所以有必要写一个方法来达成此目的:

static func queryAll(context: NSManagedObjectContext) throws -> [Achv_NoBreakVictory] {
        let req: NSFetchRequest<Achv_NoBreakVictory> = fetchRequest()
        return try context.fetch(req)
    }

但是问题来了:如果我们有一大堆这样的实体类,难道要不厌其烦的在每个类中实现上面的方法吗?

答案当然是大大的 NO!

2. 想法不错,无奈编译器不允许!

因为 Achievement 会派生出很多不同的成就实体子类,这些子类同样需要上面的 queryAll 方法来查询它们各自的所有实例,为了规范它们共同的“言行”,我们决定创建一个协议让它们来遵守:

protocol AchievementEvaluator {
    static func queryAll(context: NSManagedObjectContext) throws -> [Self]
}

接下来,我们需要让 Achv_NoBreakVictory 实体类遵守 AchievementEvaluator 协议:

extension Achv_NoBreakVictory: AchievementEvaluator {
    static func queryAll(context: NSManagedObjectContext) throws -> [Achv_NoBreakVictory] {
        let req: NSFetchRequest<Achv_NoBreakVictory> = fetchRequest()
        return try context.fetch(req)
    }
}

不幸的是,这样做的话编译器会立即大声抱怨:

在这里插入图片描述

Protocol 'AchievementEvaluator' requirement 'queryAll(context:)' cannot be satisfied by a non-final class ('Achv_NoBreakVictory') because it uses 'Self' in a non-parameter, non-result type position

这个编译错误是由于 Swift 协议中 Self 类型与类继承体系之间的冲突引起的。要解决这个问题,需要理解以下核心机制:

  1. 协议中 Self 的严格性
    Swift 协议中的 Self 代表「实现该协议的具体类型」。当协议方法返回 [Self] 时,要求实现该方法的类型必须在编译时明确自身类型
  2. final 类的继承风险
    如果 Achv_NoBreakVictory 是非 final 类,它可以被继承(如 class SubAchv: Achv_NoBreakVictory)。此时子类 SubAchv 必须实现 spawnAll() -> [Self],但继承自父类的 spawnAll() 实际返回的是 [Achv_NoBreakVictory] 而非 [SubAchv],所以这会导致类型不匹配,违背协议要求。

那我们该如何解决呢?

3. “不情愿”的 final

经过查看上面的错误提示,我们可以幡然醒悟,一种简单的解决方案应运而生,即将 Achv_NoBreakVictory 类变为 final 类,可以让编译器“敢怒不敢言”:

public final class Achv_NoBreakVictory: Achievement {}

不过,或许我们的 Achv_NoBreakVictory 类是“委托” CoreData 模型编辑器自动生成的,这样的话每次更新 Achv_NoBreakVictory 类的内容都需要费劲手动再添加 final 关键字,不烦吗?

除了强制让 Achv_NoBreakVictory 类“后继无人”以外,另一种颇为 Nice 的解决方法是为 AchievementEvaluator 协议添加关联类型:

protocol AchievementEvaluator {
    associatedtype Evaluator
    static func queryAll(context: NSManagedObjectContext) throws -> [Evaluator]
}

通过上面一番操作之后,我们 Achv_NoBreakVictory 类扩展中 queryAll() 方法的代码已经可以顺利通过编译了,厉害了我的秃们!

4. DRY 制胜法宝:协议扩展(Protocol Extension)

通过仔细观察上面 Achv_NoBreakVictory 类扩展中的 queryAll() 方法,聪明的小伙伴们不难发现:每个 Achievement 实体类 queryAll() 方法的代码实际上都大同小异,我们实在没必要“痴鼠拖姜”的一一重复实现它们。

侵淫苹果撸码多年的秃头小码农们都知道,Swift 协议有一种机制专注于解决此事,它就是协议扩展(Protocol Extension)

简单来说,我们可以将 queryAll() 方法直接放在 AchievementEvaluator 协议扩展里,而不是在遵守它的每个类里:

extension AchievementEvaluator {
    static func queryAll(context: NSManagedObjectContext) throws -> [Evaluator] {
        let req: NSFetchRequest<Evaluator> = Evaluator.fetchRequest()
        return try context.fetch(req)
    }
}

extension Achv_NoBreakVictory: AchievementEvaluator {
    typealias Evaluator = Achv_NoBreakVictory
    
    /*
    static func queryAll(context: NSManagedObjectContext) throws -> [Achv_NoBreakVictory] {
        let req: NSFetchRequest<Achv_NoBreakVictory> = fetchRequest()
        return try context.fetch(req)
    }*/
}

在上面的代码中,我们将原本位于实体类 Achv_NoBreakVictory 中的 queryAll 方法调皮地瞬移到了 AchievementEvaluator 协议扩展里面。

不过这样一来,编译器的抱怨也会再次“卷土重来”:

在这里插入图片描述

Cannot assign value of type 'NSFetchRequest<any NSFetchRequestResult>' to type 'NSFetchRequest<Self.Evaluator>'

造成这种错误的根本原因是:在 Swift 中处理 Core Data 的 NSFetchRequest 泛型类型时,没有确保类型系统的严格匹配。

  • NSFetchRequest<Evaluator> 的泛型要求:Core Data 的 fetchRequest() 默认返回 NSFetchRequest<NSFetchRequestResult>,而协议中定义的 Evaluator 关联类型要求返回具体的 Evaluator 类型,导致类型不匹配。
  • 协议扩展的泛型约束不足:编译器无法确认 Evaluator.fetchRequest() 返回的请求类型是否与 Evaluator 类型一致。

那么,此时我们又该何去何从呢?

在下一篇博文中,我们将继续 AchievementEvaluator 协议扩展的进化之旅,敬请期待吧!

总结

在本篇博文中,我们讨论了在用 Swift 协议扩展优化和重构 CoreData 托管类型功能遇到的问题,并初步提供了一些“不尽如人意”的解决方法。

感谢观赏,我们下一篇再会!8-)

深入剖析 RxSwift 中的 Queue:环形队列的 Swift 实战

作者 Daniel02
2025年6月28日 14:34

高性能事件调度背后的数据结构机密


目录

  1. 背景
  2. 为什么选择环形队列?
  3. 环形队列数据结构概览
  4. 关键实现细节
    1. 数据布局与索引计算
    2. 入队 enqueue
    3. 出队 dequeue
    4. 动态扩容与缩容 resize
  5. 性能分析
  6. 与其他数据结构对比
  7. 在 RxSwift 中的实际应用场景
  8. 示例:如何在项目中复用该实现
  9. 总结
  10. 参考链接

背景

RxSwift 的内部实现里,无论是事件传递还是调度缓冲,环形队列(circular queue) 都扮演着基石般的角色。

  • 事件先进先出 (FIFO):当前事件必须处理完才能继续下一个。
  • 背压/缓存:当上游过快、下游较慢时,诸如 observeOnflatMapconcatMap 等操作符会把事件暂存于队列,待消费端就绪后再逐一处理。

为什么选择环形队列?

特性 优势 适用场景
O(1) 入队/出队 仅索引递增,无需移动元素 高频、短生命周期任务
内存连续 优秀的缓存命中率 调度器、事件缓冲
动态扩缩容简单 比链表更高效 流量激增或骤减
低额外开销 仅两个索引变量 移动端资源受限

相比链表,环形数组几乎在所有维度都更贴合 RxSwift “大量、快速、短命事件”的特征。

  • 链表 vs 数组:链表虽不需扩容,但每个节点单独的创建和销毁,在高并发写入场景中频繁的创建和销毁节点反而更慢,且容易造成内存碎片。
  • 缓存命中(CPU cache locality):数据被访问的方式与缓存(cache)的效率之间的关系。它体现了程序在访问内存时是否能有效地利用 CPU 的高速缓存,以提高性能

环形队列数据结构概览

环形队列(Circular Queue)是什么?
环形队列是一种使用 固定大小或按需调整的顺序存储空间 来实现队列 (FIFO) 语义的数据结构。它将底层数组的首尾视为相连,通过“回环”或取模运算,让 最后一个槽位的下一个位置 重新映射到索引 0。这样无需搬移元素即可完成出队操作,同时保持内存连续性。

概念要点

关键词 说明
固定容量 & 动态容量 最简单实现容量固定;RxSwift 通过“两倍扩张 + 四分之一缩容”策略自动伸缩。
双指针/双索引 pushNextIndex 指向待写入槽位;dequeueIndex 指向待读取槽位。差值即为队列长度。
逻辑连续 vs 物理分裂 队列元素在逻辑上按时间顺序连续,但物理存储可能被分成“数组尾段 + 数组头段”两段。

下图演示了典型的环形队列状态变化(绿色为已占用单元):

image.png

image.png

  • pushNextIndex:下一个写入位置
  • dequeueIndex:通过计算得到的队头位置
  • 当写指针越过数组尾部,会从索引 0 重新开始形成“环”。

环形队列向外公开的 API

方法 / 属性 作用 时间复杂度
enqueue(_ element: T) 入队,将元素追加至尾部 O(1) 均摊
dequeue() -> T? 出队,弹出队头元素,队列为空返回 nil O(1)
peek() -> T 只读队头但不移除 O(1)
isEmpty: Bool 队列是否为空 O(1)
count: Int 当前元素数量 O(1)
makeIterator() Swift Sequence 迭代支持 O(n) 整体,但单步 O(1)

这些接口与标准队列抽象保持一致,开发者无需关心内部的环形实现即可完成常见操作。数组带来的 Cache 友好和一次性拷贝优化,使得这些 API 在实际运行中维持极低的延迟。


关键实现细节

数据布局与索引计算

private var storage: ContiguousArray<T?>
private var pushNextIndex = 0   // **写指针** —— 指向下一次写入的位置
private var innerCount    = 0   // 当前元素数量

private var dequeueIndex: Int { // **读指针** —— 通过计算得到
    let index = pushNextIndex - count
    return idx < 0 ? index + storage.count : index
}

为什么保存 pushNextIndex,而 计算 dequeueIndex

在 RxSwift 场景里,入队(enqueue)操作通常比出队(dequeue)更高频,在响应式流中,数据通常由外部事件(如网络响应、用户输入、定时器)快速产生,然后再异步地被消费者(观察者、订阅者)慢慢处理。因此,为了简化 入队(enqueue)操作,并保证 扩容后的数据迁移逻辑更加高效和清晰,因此选择保存和队尾元素相关索引。

计算公式拆解

  • 队头理论推导dequeueIndex = (pushNextIndex - innerCount + capacity) % capacity
  • 实际代码实现:先做减法再按需加 capacity

在编码过程中,尽量避免使用乘*、除/、模%、浮点数,效率低下,CPU做这些操作比较耗时;因此,RxSwift队头实际的计算转换成了加和减

这样既可保持常量级计算,又能在无需硬件除法指令的情况下完成取模。

入队 enqueue enqueue

mutating func enqueue(_ element: T) {
    if count == storage.count {        // 存满 → 扩容
        resizeTo(max(storage.count, 1) * resizeFactor)
    }
    storage[pushNextIndex] = element   // 写入
    pushNextIndex += 1
    innerCount += 1
    if pushNextIndex >= storage.count { // 环回
        pushNextIndex -= storage.count
    }
}
  1. 空间不足 → 扩容
  2. 数据写入
  3. 写索引自增,如越界则回环

出队 dequeue

mutating func dequeue() -> T? {
    if self.count == 0 {
        return nil
    }

    defer {
        let downsizeLimit = storage.count / (resizeFactor * resizeFactor)
        if count < downsizeLimit, downsizeLimit >= initialCapacity {
            resizeTo(storage.count / resizeFactor)  // 缩容
        }
    }
    return dequeueElementOnly()
}

 private mutating func dequeueElementOnly() -> T {
    precondition(count > 0)
    
    let index = dequeueIndex

    defer {
        storage[index] = nil
        innerCount -= 1
    }

    return storage[index]!
}

  • 先读后清 ,缩容操作之所以放在 defer 中,是为了确保在元素真正出队(dequeueElementOnly)之后再进行判断和可能的缩容操作;基于更新后的真实元素数量的“延后清理或操作”的模式。

动态扩容与缩容 resize

环形队列的数组在写满或低于一定数量时会触发容量调整,核心目标是:

  1. 保证逻辑顺序不变 —— 队列是 FIFO,调整后索引必须保持原先的出队顺序;
  2. 一次性线性拷贝 —— 尽可能利用批量拷贝,避免逐元素迁移;
  3. 简化后续索引运算 —— 调整后让队头落到 0pushNextIndex落到 count
mutating private func resizeTo(_ size: Int) {
    // 申请新数组
    var newStorage = ContiguousArray<T?>(repeating: nil, count: size)
    // 保存现有元素总数
    let count = self.count
    // 旧队头位置 & 尾段剩余空间
    let dequeueIndex = self.dequeueIndex
    let spaceToEndOfQueue = storage.count - dequeueIndex
    // **** 分段拷贝 ****
    //第一次拷贝的原来的尾段
    let countElementsInFirstBatch = Swift.min(count, spaceToEndOfQueue)
    // 第二次拷贝的原来的头段
    let numberOfElementsInSecondBatch = count - countElementsInFirstBatch
    // 原来的尾段放到新的数组开始的位置
    newStorage[0 ..< countElementsInFirstBatch] = storage[dequeueIndex ..< (dequeueIndex + countElementsInFirstBatch)]
    // 原来的头段追加到新数组中,此时队头就是新数组索引0的位置,pushNextIndex即为新数组的数量
    newStorage[countElementsInFirstBatch ..< (countElementsInFirstBatch + numberOfElementsInSecondBatch)] = storage[0 ..< numberOfElementsInSecondBatch]
    // 更新索引与存储
    self.innerCount = count
    pushNextIndex = count
    storage = newStorage

}

为什么要“两段复制”?

  • 可能的分裂:当队列发生环回后,逻辑上连续的元素会被物理地分成“尾段 + 头段”两段。若直接拷贝整个旧数组,元素顺序将错乱。
  • 维持 FIFO 顺序:先复制尾段(从 dequeueIndex 到旧数组末尾),再复制头段(索引 0 开始)的剩余部分,可在新数组 [0..<count) 重新拼接出正确的时间顺序。
  • 线性内存访问:每一段都是线性区域,且可被系统优化为块拷贝。

队头元素位置的变化

操作前 操作后
队头索引 = dequeueIndex (可能 > 0) 固定为 0
pushNextIndex (任意位置) 固定为 count

缩容与扩容共用同一逻辑,只是 size 参数不同,因为尾段和头段均是连续存储。当元素数量小于 capacity / 4 且不低于初始容量时触发缩容,确保内存占用与业务峰谷相匹配。

性能分析

  • 时间复杂度
    • 入队/出队:O(1)
    • 扩/缩容:均摊 O(1)(几何倍数增长,摊销成本极低)
  • 对比链表
    • 链表 enqueue 需分配节点;数组仅更新索引
    • 链表缺乏 缓存命中

虽然扩容是一次性 O(n),但每次扩容都是在上一次扩容后进行了很多次 O(1) 的插入之后才发生,触发扩容的代价会被之前的多次 O(1) 插入“摊销”掉。


与其他数据结构对比

特性 环形数组 单向链表 双端队列 Deque
内存布局 连续 离散 连续
入/出队时间 O(1) O(1) (指针操作) O(1)
扩容开销 copy (偶发) copy (偶发)
缓存命中
适合场景 高频、小对象 大对象、频繁插入删除中间节点 双端操作

在 RxSwift 中的实际应用场景

场景 描述 优势体现
调度器 (SerialDispatchQueueScheduler) 将任务缓存至队列,串行执行 线程安全 + 低延迟
操作符 observeOn 上游高速 → 队列缓存 → 下游消费 背压管理
合并流 (flatMap, concatMap) 子流事件临时缓冲 减少锁/条件变量

背压管理(Backpressure Management) : 是一种控制数据流速的机制,目的是防止生产者(数据发送方)发送数据过快,导致消费者(数据接收方)来不及处理,最终引发资源耗尽、缓冲区溢出或系统崩溃等问题。


示例:如何在项目中复用该实现

var q = Queue<Int>(capacity: 4)

// 写入
(1...10).forEach { q.enqueue($0) }

// 消费
while !q.isEmpty {
    print(q.dequeue()!)
}

总结

  • 环形队列 通过常数级操作与良好缓存局部性,完美契合 RxSwift 的事件驱动模型。
  • 精巧的 写索引 + 计算读索引 方案,简化了状态管理。
  • 扩缩容逻辑在数组层面“一劳永逸”,保持 API 简洁。

掌握并善用这一数据结构,不仅能帮助你更深入理解 RxSwift 的内部机理,也能在自己的高性能队列、调度器甚至网络层缓存中大显身手。


参考链接

  • RxSwift 源码 Source/Schedulers/Queue.swift

Swift 6 新特性(一):count(where:) 方法带来的从复杂到简洁变化

作者 iOS新知
2025年6月27日 12:47

这里每天分享一个 iOS 的新知识,快来关注我吧

前言

Swift 语言一直在不断演进,推出的新特性不仅提升了性能,还提高了代码的可读性。

其中一个值得关注的新功能就是在 SE-0220 中引入的 count(where:) 方法。这个方法让我们可以更高效、更具表现力地统计序列中满足特定条件的元素,避免了之前需要结合 filter()count 的复杂操作。

count(where:) 方法将过滤(filter)和计数(count)两个步骤合并为一个,省去了创建和丢弃中间数组的麻烦,从而提升了性能,同时代码也变得更加简洁。

示例1:统计高于冰点的温度

假设我们有一个以摄氏度为单位的温度数组,我们想统计其中有多少温度高于冰点(0°C):

在之前,我们可能需要这样写:

let temperatures = [-510, -22025, -1]
let aboveFreezingCount = temperatures.filter { $0 > 0 }.count

现在,我们可以使用 count(where:) 方法来简化代码:

let temperatures = [-510, -22025, -1]
let aboveFreezingCount = temperatures.count { $0 > 0 }

// 输出 `3`
print(aboveFreezingCount)

在这个例子中,aboveFreezingCount 的值为 3,因为有三个温度(10, 20, 25)符合条件。

示例2:统计具有特定前缀的元素

让我们再看一个示例,假如我们有一组产品名称,想统计以 "Apple" 开头的产品数量:

let products = [
    "Apple", 
    "Banana", 
    "Apple Pie",
    "Cherry", 
    "Apple Juice", 
    "Blueberry"
]
let appleCount = products.count { $0.hasPrefix("Apple") }

// 输出 `3`
print(appleCount)

在这个例子中,appleCount 的值为 3,因为 "Apple"、"Apple Pie" 和 "Apple Juice" 都以 "Apple" 开头。

示例3:根据长度统计元素

一个常见的应用场景是根据元素的长度进行统计。例如,我们可能想知道数组中有多少名字的长度少于六个字符:

let names = ["Natalia""Liam""Emma""Olivia""Noah""Ava"]
let shortNameCount = names.count { $0.count < 6 }

// 输出 `4`
print(shortNameCount)

在这个例子中,shortNameCount 的值为 4,因为 "Liam"、"Emma"、"Noah" 和 "Ava" 的长度都少于六个字符。

示例4:统计特定元素

如果需要统计序列中某个特定元素出现的次数,可以在闭包中使用等于运算符 (==)。例如:

let animals = ["cat""dog""cat""bird""cat""dog"]
let catCount = animals.count { $0 == "cat" }

// 输出 `3`
print(catCount)

在这个例子中,catCount 的值为 3,因为 "cat" 在数组中出现了三次。

适用范围和平台支持

count(where:) 方法适用于所有遵循 Sequence 协议的类型,这意味着我们不仅可以在数组中使用,还可以在集合、字典和其他序列类型中使用。

但需要注意的是,序列必须是有限的,以确保方法能够在合理的时间内完成。

count(where:) 方法在 Swift 6 中引入,因此需要 Xcode 16 才能使用这个特性。它支持多种平台和操作系统版本,包括 iOS 8.0+、macOS 10.10+、visionOS 1.0+ 等。

总结

count(where:) 方法是一个非常实用的功能,它不仅简化了代码,还提高了性能。如果你是一名经验丰富的 Swift 开发者,想要学习高级技巧,可以关注我的公众号,我会持续分享 Swift 相关的技巧和知识。

你对这个新特性有什么看法呢?欢迎在评论区与我们分享你的想法。

这里每天分享一个 iOS 的新知识,快来关注我吧

本文同步自微信公众号 “iOS新知”,每天准时分享一个新知识,这里只是同步,想要及时学到就来关注我吧!

swift的get和set,newValue和oldValue

2025年6月27日 10:38

计算属性和存储属性都长什么样子,一定要记忆深刻

存储属性

var name: String
var name = "a"
var property: Int = {
return 1
}()

计算属性

class sample {
    var no1 = 0.0, no2 = 0.0
    var length = 300.0, breadth = 150.0

    var middle: (Double, Double) {
        get{
            return (length / 2, breadth / 2)
        }
        set(axis){ //注意这里的axis只是给newValue显示指定了参数名
            no1 = axis.0 - (length / 2)
            no2 = axis.1 - (breadth / 2)
        }
    }
}
或者
var computedValue: Int {
    get { _backingValue }
    set {
        // 使用隐式 newValue 在计算属性的setter中,如果不显式指定参数名,则默认使用`newValue`作为参数名。但是,计算属性没有内置的旧值(oldValue)访问,因为计算属性本身不存储值
        print("新值: \(newValue)")
        _backingValue = newValue
    }
}

在计算属性的setter中,如果不显式指定参数名,则默认使用`newValue`作为参数名。但是,计算属性没有内置的旧值(oldValue)访问,因为计算属性本身不存储值那么如果我们需要一个旧的值呢?需要手动存储旧值

private var _storage = 0
private var _oldValue = 0 // 额外存储旧值
var computedWithOldValue: Int {
    get { _storage }
    set {
        _oldValue = _storage  // 保存当前值为旧值
        _storage = newValue   // 更新为新值

        print("旧值: \(_oldValue), 新值: \(newValue)")
    }
}

只读的计算属性

var metaInfo: [String:String] {
        return [
            "head": self.head,
            "duration":"\(self.duration)"
        ]
    }
或者
var name: String {
       get {
           return ""
       }
   }

注意只读的计算属性并不是我们之前认识的readonly:因为只读计算属性在本类/结构体中也不能赋值图片那么如何实现readonly呢?

private(setvar name: String

这个也很好理解,是有set方法是private的,所以在类/结构体外还是可以get的

struct test1{
    private(set) var members :[String] = []
}

注意一个private(set)的集合,是不能添加和删除元素的

var t = test1()
t.members.append("a") //Cannot use mutating member on immutable value: 'members' setter is inaccessible

也可以结合计算属性使用

private var rawValue: Double = 0

private(set) var calibratedValue: Double {
    get { rawValue * 1.25 }
    set { rawValue = newValue / 1.25 }
}

关于计算属性的几个点

  • 存储属性我们可以定义常量或者是变量,但是对于计算属性,必须定义为变量,并且计算属性在定义时必须包含类型
  • 对于计算属性来说,set方法是可选的,而get方法必不可少
  • let的存储属性没有set方法,只读的计算属性area也没有set方法;所以我们不能简单的通过有没有set方法来区分属性是计算属性还是存储属性

接下来继续看一下协议

  • 协议可以要求遵循协议的类型提供特定名称和类型的实例属性或类型属性。
  • 协议不指定属性是存储属性还是计算属性,它只指定属性的名称和类型
  • 在协议中,实例属性总是使用var声明为变量属性
  • 在协议中,始终使用static关键字作为类属性声明的前缀, 在类中实现时,可以使用classstatic关键字作类属性声明前缀(Class properties are only allowed within classes)
  • 协议还指定属性是可读的还是可读可写的

可读可写的属性在类型声明后通过写入{get set}表示可读属性通过写入{get}表示

protocol SomeProtocol {
    var mustBeSettable: Int { get set }
    var doesNotNeedToBeSettable: Int { get }
}

如果协议要求属性是只读的,这意味着遵循协议的类型必须提供该属性,并且至少允许外部读取(get)。但是,在实现时,该属性可以是存储属性或计算属性,甚至可以是可读可写的(即可写的),因为协议只要求至少可读,不禁止可写。如下图,mustBeSettable至少要满足SomeProtocol中的“可读可写”图片属性观察者属性观察器(didSet 和 willSet)是在属性的值被修改时触发的。这种修改必须通过显式的赋值语法完成

self.property = newValue

这种是观察不到的:

@State private var blurAmount = 0.0 {
    didSet {
        print("New value is \(blurAmount)")
    }
}
Slider(value: $blurAmount, in: 0...20)
    .onChange(of: blurAmount) { newValue in
      print("New value is \(newValue)")
    }

除了在声明语句中对属性赋值,其他对属性做赋值操作,必会触发观察器属性观察器(Property Observers)提供了两个特殊的关键字 newValue 和oldValue,用于在属性值变化时访问新值和旧值。它们分别用于 willSet 和 didSet 观察器中

var property: DataType = initialValue {
    willSet {
        // 使用 newValue 访问即将设置的值
        // 当前属性值仍是旧值
    }
    didSet {
        // 使用 oldValue 访问被覆盖的值
        // 当前属性值已是新值
    }
}

willSet中自带一个newValue的属性,oldValue用property自身即可访问,相同的didSet自带一个oldValue的属性,newValue用property自身即可访问

❌
❌