普通视图

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

在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 开发体验。

Re: 0x01. 从零开始的光线追踪实现-光线、相机及背景

作者 壕壕
2025年6月30日 22:24

目标

书接上文,之前已经实现一个铺满整个窗口的红色填充,这趟来实现光线、相机及背景。

本节最终效果

image.png

计算物体在窗口坐标的位置

其实这个光追的思维模式很简单,就是从相机处开始发射一束射线,射线撞到哪些“物体”,就计算跟该“物体”相交的颜色。如图所示,从相机处发射射线,以左上角开始逐像素扫一遍,计算对应像素的颜色

fig-04-camera-view-space.svg

我们再看看 viewport 的坐标,假设一个窗口大小是 480272480 * 272(没错,PSP 的分辨率😁)的宽高,那么 xx 的区间就是 [0,479)[0, 479)yy 的区间就是 [0,271)[0, 271)

fig-03-viewport-coords.svg

现在我们要来处理一个标准化的像素坐标,处理像素在屏幕中的 2D 位置

struct Vertex {
  float4 position [[position]];
};

fragment float4 fragmentFn(Vertex in [[stage_in]]) {
  auto uv = in.position.xy / float2(float(480 - 1), float(272 - 1));
  // ...
}

上面这一步的作用是把像素级的屏幕坐标转成区间 [0,1][0, 1] 的归一化坐标。
假设现在有一个物体,它的坐标是 (240,135)(240, 135),通过上面的计算式子可以得出
uv=(240/479,135/271)(0.5,0.5)uv = (240 / 479, 135 / 271) ≈ (0.5, 0.5),说明它在屏幕的中间

接着我们假定相机的位置是原点 (0,0,0)(0, 0, 0),相机距离 viewport 11。我们计算出宽高的比例再套进这个计算 (2 * uv - float2(1)),等于讲把 [0,1][0, 1] 映射成 [1,1][-1, 1] 的范围,其实就是

原始 uv 变换后
(0, 0) (-1, -1) 左下角
(1, 0) (1, -1) 右下角
(0.5, 0.5) (0, 0) 居中
(1, 1) (1, 1) 右上角

再把 (2 * uv - float2(1))float2(aspect_ratio, -1) 相乘等于讲横向乘以 aspect_ratio 用来做等比例变换

至于纵向乘以 -1,那是因为在 Metal 中,yy 轴是向下为正,乘一下 -1 就可以把 yy 轴翻转变成向上为正,接下来计算方向就简单多了,因为 zz 轴面向相机,其实就是相机距离取反,上面假定相机距离为 1,所以取反再跟 uvuv 放一块就是方向,同时我们又假定相机的位置是原点 (0,0,0)(0, 0, 0),那么求光线就很容易了

struct Ray {
  float3 origin;
  float3 direction;
};

fragment float4 fragmentFn(Vertex in [[stage_in]]) {
  // ...
  const auto focus_distance = 1.0;
  // ...
  const auto direction = float3(uv, -focus_distance);
  Ray ray = { origin, direction };
}

现在既然有了光线,再就是要计算一下光线的颜色,因为目前场景中没有物体,所以就默认计算背景色,我们先把光线从 [1,1][-1, 1] 映射回 [0,1][0, 1],然后再线性插值计算渐变天空颜色,所以先要让光线经过归一化操作到 [1,1][-1, 1]

// [-1, 1]
normalize(ray.direction)

然后再给该向量加 11

// [-1, 1] + 1 = [0, 2]
normalize(ray.direction) + 1

然后把 [0,2][0, 2] 乘以 0.50.5 就转成 [0,1][0, 1] 了,之后再代入线性插值公式计算结果,具体渐变色值可以根据自己的需求调整,我这里直接使用 Ray Tracing in One Weekend 的色值 float3(0.5, 0.7, 1)

blendedValue=(1a)startValue+aendValueblendedValue = (1 − a) \cdot startValue + a \cdot endValue

float3 sky_color(Ray ray) {
  const auto a = 0.5 * (normalize(ray.direction).y + 1);
  return (1 - a) * float3(1) + a * float3(0.5, 0.7, 1);
}

最后总结一下代码

struct Ray {
  float3 origin;
  float3 direction;
};

float3 sky_color(Ray ray) {
  const auto a = 0.5 * (normalize(ray.direction).y + 1);
  return (1 - a) * float3(1) + a * float3(0.5, 0.7, 1);
}

fragment float4 fragmentFn(Vertex in [[stage_in]]) {
  const auto origin = float3(0);
  const auto focus_distance = 1.0;
  const auto aspect_ratio = 480 / 272;
  auto uv = in.position.xy / float2(float(480 - 1), float(272 - 1));
  uv = (2 * uv - float2(1)) * float2(aspect_ratio, -1);
  const auto direction = float3(uv, -focus_distance);
  Ray ray = { origin, direction };
  return float4(sky_color(ray), 1);
}

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新知”,每天准时分享一个新知识,这里只是同步,想要及时学到就来关注我吧!

昨天以前掘金 iOS

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…

零一开源|前沿技术周报 #5

作者 kymjs张涛
2025年6月30日 10:49

前沿技术周刊 是一份专注于技术生态的周刊,每周更新。本周刊深入挖掘高质量技术内容,为开发者提供持续的知识更新与技术洞察。

订阅渠道:[零一开源] | [掘金] | [RSS]

码圈新闻

  1. 2025 Google I/O:Android这次真的AI化了
  2. 华为云发布盘古大模型5.5,现场揭秘底层技术
  3. 小米多模态大模型MiMo-VL开源,官方称多方面领先 Qwen2.5-VL-7B
  4. 代码革命的先锋:aiXcoder-7B模型
  5. React Native 0.80 开始支持 iOS 预构建

新技术介绍

  1. MNN LLM:让DeepSeek-R1 跑在手机上,还能支持多模态
  2. 苹果向开发者开放本地AI能力,推出全新Foundation Models框架

大厂在做什么

  1. 节省前端1000+pd人力成本!快手快聘「伏羲工作台」技术实践全解析
  2. B站在KMP跨平台的业务实践之路

深度技术

  1. 万字长文总结多模态大模型最新进展(Modality Bridging篇)
  2. 全网最强ViT (Vision Transformer)原理及代码解析
  3. 与微软“代码女王”关于GitHub、智能编程的一小时对谈
  4. 零基础解码Transformer与大模型核心原理
  5. 万字长文深入浅出教你优雅开发复杂AI Agent

博客推荐

  1. Android: 2025年,Android 16 都快来了,你知道各个版本 Android的特点吗?
    随着 Android 的发展,每个新版本的 Android 都带来了新的 API 和 改进。这篇文章将介绍从 Android 5 开始到最新版 Android 的API及其行为的变更。
  2. 后端: 转转上门履约服务拆分库表迁移实践
    随着业务不断发展,一个服务中部分功能模块适合沉淀下来作为通用的基础能力。作为通用的基础能力,对提供的服务可用性和稳定性有较高的要求,因此把该部分功能模块拆分出来,单独一个服务是比较好的选择。为了更好的与业务服务物理隔离,不仅需要从代码层面拆分,数据库层面也需要拆分。在做技术方案设计时面临着以下几个问题:
  3. 其他: 纪念陈皓(左耳朵耗子)老师
    突然才意识到,原来陈皓老师(网名:左耳朵耗子)已经离开两整年了。 两年前的5月份,这位年仅47岁的技术前辈因病离开,这也让世间从此少了一位倔强又浪漫的技术人。 相信不少同学了解陈皓老师都是从他的个人博客酷壳CoolShell开始的。
  4. iOS: iOS开发:关于路由
    在iOS开发中引入路由框架一直是一个有争议的话题。 因为即使不使用路由框架,似乎也不会有太大的影响。那么我们先来回顾一下几个典型的跳转场景:
  5. iOS: iOS 实现自定义对象深拷贝(OC/Swift)
    在 OC 中,对象的赋值操作是浅拷贝(指针复制),创建一个指针,旧对象引用对象计数加 1。在 Swift 中,类是引用类型,引用类型在赋值和传参时都是操作的引用,这些引用指向同一个对象。如下,对 classA 的修改也会影响到 classB:
  6. Android: 一句话说透Android里面的Activity、Window、DecorView、View之间的关系
    一句话总结: Activity 是老板,Window 是装修队长,DecorView 是毛坯房,View 是你买的家具。老板喊队长来装修毛坯房,最后把你的家具摆进去!

关于我们

零一开源 是我自己做的一个文章和开源项目的分享站,有写博客开源项目的也欢迎来提供投递。
每周会搜集、整理当前的新技术、新文章,欢迎大家订阅

[奸笑]

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);

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

Flutter性能优化Tips

2025年6月30日 09:14

前言

Flutter作为跨平台移动应用开发框架,一直广受欢迎,在开发过程中,确保应用的性能是至关重要的。以下是一些优化Flutter应用性能的工具和方法

一、渲染优化

1. 减少Widget重建范围 使用 const 构造函数

 使用`const`关键字创建不会改变的Widget,这样可以避免不必要的重建。
 使用`const`构造函数创建常量Widget。

flutter 中 widget 都是 inmutable 的,使用const构造函数后,Flutter可以在编译期创建Widget实例,而不是在每次重建时都创建新实例。由于相同的const对象在内存中只存在一份,这减少了内存消耗和对象创建的开销。当父Widget重建时,Flutter会复用这些constWidget而不需要重新创建,从而显著提升渲染性能。

Widget build(BuildContext context) {
  return Container(
    padding: EdgeInsets.all(8.0),
    child: Text('Hello World'),
  );
}
// 优化后
Widget build(BuildContext context) {
  return const Container(
    padding: EdgeInsets.all(8.0),
    child: Text('Hello World'),
  );
}

2. 合理使用StatefulWidget

Flutter的重建机制会从setState()被调用的StatefulWidget开始,重建整个子树。通过将状态隔离在更小的组件中,可以显著减少重建范围。这样当状态变化时,只有包含该状态的小组件会重建,而不是整个页面,从而降低CPU使用率并提高渲染效率。

class MyPage extends StatefulWidget { // 优化前 - 整个页面重建
  @override
  _MyPageState createState() => _MyPageState();
}
class _MyPageState extends State<MyPage> {
  bool isLoading = false;
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('标题永远不变'),
        if (isLoading) CircularProgressIndicator() else DataList(),
      ],
    );
  }
}
// 优化后 - 只重建变化部分
class MyPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        const Text('标题永远不变'),
        LoadingStateWidget(),
      ],
    );
  }
}
// 示例代码
class LoadingStateWidget extends StatefulWidget {
  Widget build(BuildContext context) {
    return isLoading ? CircularProgressIndicator() : DataList();
  }
}

3. 使用RepaintBoundary切分重绘区域

当Widget重绘时,Flutter默认会重新绘制该Widget及其所有子Widget。RepaintBoundary会创建一个新的图层,将其子Widget的重绘行为隔离。这意味着当子Widget需要重绘时,Flutter无需重绘边界外的内容。防止不必要的GPU渲染工作,提高复杂界面的性能。可以开启 Inspector 查看重绘

class MyComplexUI extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        // 频繁更新的区域
        RepaintBoundary(
          child: AnimatedProgressIndicator(),
        ),
        ComplexStaticContent(), // 静态内容
      ],
    );
  }
}

二、列表优化

1. 使用ListView.builder代替Column表优化

Column会一次性构建所有子Widget,无论它们是否在视口内可见。这会导致大量内存使用和初始渲染延迟。相比之下,ListView.builder实现了"视口渲染"技术,只构建和渲染当前可见的项目,其他项会在滚动进入视口时才被构建。这大大减少了内存占用和初始渲染时间,对于长列表尤其重要。

// 优化前 - 使用Column一次性构建所有项
Widget build(BuildContext context) {
  return SingleChildScrollView(
    child: Column(
      children: List.generate(1000, (index) => 
        ListTile(title: Text('Item $index'))
      ),
    ),
  );
}
// 优化后 - 使用ListView.builder按需构建
Widget build(BuildContext context) {
  return ListView.builder(
    itemCount: 1000,
    itemBuilder: (context, index) => ListTile(
      title: Text('Item $index'),
    ),
  );
}

2. ListView key 的使用

当列表涉及到 增删重排以及列表动效的情况下,需要使用 key,避免重建以后丢失状态,只更新/创建/销毁发生变动的那一项,极大减少无谓的 build、layout、paint 和 State 重建,提升渲染效率。

ListView(
  children: items.map((item) => ListTile(
    key: ValueKey(item.id),
    title: Text(item.title),
  )).toList(),
)

3. ListView itemExtent 使用

  • 不设置 itemExtent 时,ListView 会对子项逐一进行布局测量(layout),每次滚动或构建都需遍历和计算高度,开销大。
  • 设置 itemExtent 后,ListView 可以直接通过数学公式(比如滚动偏移/高度)精确定位和渲染可见Item,避免了对子项的反复测量
  • 这使得列表滚动更加流畅,内存和CPU消耗更低,尤其在长列表、复杂子项情况下优势明显。
  • ListView 的 每个 Title 都固定高度
ListView.builder(
  itemCount: 1000,
  itemExtent: 60.0, // 每项高度固定为 60
  itemBuilder: (context, index) {
    return ListTile(
      title: Text('Item $index'),
    );
  },
)

三、图片优化

1. 图片缓存和预加载

网络图片加载是耗时操作,如果图片不缓存,同一图片会被重复下载多次,导致网络资源浪费和UI闪烁。使用CachedNetworkImage加载图片,实现了多级缓存机制:内存和持久缓存,可以有效降低内存的占用

import 'package:cached_network_image/cached_network_image.dart';
CachedNetworkImage(
  imageUrl: 'https://example.com/image.jpg',
  placeholder: (context, url) => CircularProgressIndicator(),
  errorWidget: (context, url, error) => Icon(Icons.error),
)

2. 图片加载使用合理尺寸

加载远超实际显示尺寸的大图是常见的性能问题。例如,下载一张5MB的4K图片,却只在200x200像素的区域显示,会导致更多的带宽消耗、更大的内存占用,并且图片的解码和缩放会消耗性能,可以使用 Inspector 检测哪些图片过大

// 优化前 - 加载原始大小图片
Image.network('https://example.com/large_image.jpg')
// 优化后 - 指定适当尺寸
Image.network(
  'https://example.com/large_image.jpg?w=300&h=200',  // 服务端支持动态调整图片尺寸
  width: 300,
  height: 200,
  fit: BoxFit.cover,
)

四、代码层面优化

1. 避免不必要的计算

在Flutter中,build方法可能会非常频繁地调用,每次UI状态变化都可能触发重建。如果在build方法中执行复杂计算,会导致: UI卡顿、电池耗电增加、设备发热


  int _calculate(int n) {
    // 检查缓存
    if (_cache.containsKey(n)) {
      return _cache[n]!;
    }
    、、、、计算逻辑
  }
  Widget build(BuildContext context) {
    final result = _calculate(input);
    return Text('Result: $result');
  }

2. Isolate多线程操作

虽然 flutter 有多个线程,但是Dart是单线程执行,dart 一次只能执行一个任务,任务按照顺序一个接一个的执行,具体查看 详解 Flutter engine多线程、Dart isolate和异步普罗哈基米

当执行耗时操作时:UI线程被阻塞 动画会卡顿,帧率下降 可能触发ANR(Android)或页面frezen(iOS)

通过使用compute函数或Isolate,可以将计算密集型任务移至后台线程执行,保持UI线程流畅响应。Flutter的compute函数封装了Isolate创建和通信的复杂性,适合大多数场景。

3. GIF重复解码问题

  • 在Flutter中,直接用Image.assetImage.network加载GIF图片时,如果该GIF在界面上多处被使用,或在滚动列表中多次出现,每次都会重新解码一次。这会导致CPU资源浪费、卡顿、甚至内存暴涨。
  • Flutter的默认图片解码不会自动全局缓存GIF的每一帧,导致每次触达界面都要重新解码(尤其是大GIF或大量列表时),加重主线程负担
  • 使用三方库加载 gif,解析并缓存每一帧图片以及间隔,采用序列帧的方式加载图片

4.减少使用圆角

  • ClipRRectClipOval等圆角裁剪组件,或Containerdecoration: BoxDecoration(borderRadius: ...),在Flutter内部会创建新的图层并触发离屏渲染(offscreen rendering)。
  • 离屏渲染会将裁剪区域单独绘制到一块缓存(内存消耗),然后再合成到主画布。大量使用会极大增加GPU负担,尤其在滑动列表、动画场景中,明显降低性能
  • 能用圆角图片替代裁剪就用图片
  • 只在必要时裁剪(如界面核心部分)
  • 避免嵌套多重ClipRRect。 通过设置checkerboardOffscreenLayers 检测离屏渲染
MaterialApp(
  showPerformanceOverlay: true,
  checkerboardOffscreenLayers: true,
)

5.避免过度使用透明度(Opacity)

  • Opacity widget 会导致其子widget单独绘制到一个新的图层,再整体设置透明度。频繁使用会带来离屏渲染,尤其在动画、列表中影响大。
  • 离屏渲染导致额外的内存消耗和GPU合成压力,严重时会出现掉帧
  • 能用颜色透明值直接设置就不用Opacity(比如Container(color: Colors.black.withOpacity(0.2)))。
  • 动画透明度优先用FadeTransition(配合AnimationController),避免整个子树离屏渲染。
  • 避免在大区域、复杂子树上用Opacity

6. 减少图层嵌套(Widget嵌套过多)

Flutter的Widget嵌套层级过深会导致:

  • 构建(build)树复杂,重建耗时增加
  • Layout、Paint 阶段递归遍历层数增多,性能下降
  • DevTools调试难度增大 每多一层Widget,Flutter的构建、布局、绘制流程都要多一层递归遍历。大量无意义的嵌套严重拖慢渲染效率
  • 合并能合并的Widget(如用Container替代嵌套的Padding+DecoratedBox+Align)。
  • 用自定义Widget封装常用结构,避免重复堆叠
  • 合理拆分大布局,避免过深的子树

7. 动效使用 child 参数,减少重建

动画每一帧都会触发 build。如果子树内容不变却每帧都重建,浪费性能。把不变的内容放到 child 参数,build 只处理“变”的部分,大大减少无意义的构建。

  • AnimatedBuilder
  • AnimatedWidget
  • FadeTransitionScaleTransitionRotationTransition 等 以上组件适用 child 参数
AnimatedBuilder(
  animation: controller,
  child: const Text('静态内容'), // 只 build 一次
  builder: (context, child) {
    return Transform.rotate(
      angle: controller.value * 2 * pi,
      child: child, // 每帧只变 transform
    );
  },
)

8. 优先用 Transform/Opacity 动画,而非重建布局

TransformOpacity 这类属性动画,底层是 GPU 合成变换,不涉及重新布局和绘制,性能极高。相比之下,若动画导致 Widget 结构/布局频繁变动,每帧都要 layout 和 paint,性能很低。

// 推荐
FadeTransition(
  opacity: animation,
  child: Image.asset('xxx.png'),
)

// 不推荐(每帧都重建图片)
AnimatedBuilder(
  animation: controller,
  builder: (context, child) {
    return Opacity(
      opacity: controller.value,
      child: Image.asset('xxx.png'),
    );
  },
)

五、状态管理优化

1. 局部状态更新

  • 全局状态变更导致的过度重建是Flutter性能问题的主要来源之一。当使用setState()或整体状态更新时,会导致整个子树重建,包括许多实际上不依赖变化状态的组件。
  • 使用Provider、GetX、Riverpod和Bloc等细粒度重建机制,都有助于提高应用性能。可以选择性地只通知特定状态变化,而不是刷新整个状态,状态逻辑和UI逻辑分离,提高代码可维护性

2. 内存优化与防止内存泄漏

内存泄漏是Flutter应用中常见的性能问题,特别是在长时间运行的应用中。主要原因包括:

  • 未释放的流订阅(Streams)
  • 动画控制器未dispose
  • 尚未完成的异步任务持有上下文引用
  • 全局单例持有对已销毁组件的引用

六、资源优化

1. 减小应用体积

应用体积直接影响用户下载意愿和存储空间占用。Flutter应用体积较大的主要原因包括:

  • Flutter引擎本身的体积
  • 未优化的资源文件(图片、音视频等)
  • 大量第三方依赖
  • 未配置代码压缩和混淆

通过以下策略可以减小应用体积:

  • 使用--split-per-abi生成特定架构的APK,避免包含所有架构的原生库
  • 启用R8/ProGuard代码压缩和混淆
  • 压缩图片资源,使用适当的格式(WebP优于PNG),也可以使用 tinypng 压缩图片
  • 删除未使用的资源和代码
  • 按需加载功能模块

七、检测工具

1. 使用Flutter DevTools进行性能分析

性能调优的第一步是准确测量和定位问题。Flutter提供了强大的性能分析工具:

  • Inspector 在 flutter 的 debug 和 profile 模式下使用,开启 DevTools 的 Inspector Page
image.png

Highlight Repaint(重绘高亮) Highlight Oversize Images(图片尺寸高亮)

image.pngimage.png

通过 flutter run --profile 开启 profile 模式,使用性能视图(Performance view) 通过火焰图查看哪些方法耗时,修改对应的方法提高性能

  1. Performance Overlay - 显示GPU和UI线程的实时性能图表,帮助识别帧丢失
  2. DevTools Timeline - 详细记录渲染、布局和构建事件,帮助定位性能瓶颈
  3. Widget构建检查器 - 分析Widget树,查找不必要的重建
  4. 内存分析器 - 监控内存使用和潜在泄漏

image.png

总结

Flutter 性能优化的本质是:减少不必要的重建和重绘、减少内存/CPU/GPU压力、让静态内容尽量复用,动态内容最小化刷新,按需加载和渲染,让 UI 始终流畅响应用户。

新版 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-)

iOS引入Masonry库编译报错libarclite_iphonesimulator.a

2025年6月28日 16:47

背景

引入Masonry编译报错如下:

File not found: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/arc/libarclite_iphonesimulator.a

Linker command failed with exit code 1 (use -v to see invocation)

这是非常常见的问题,自我第一次使用Masonry库就有这个错误。但是觉得很奇怪,Masonry作为一个广泛使用的成熟库,为什么引入还会报错,这也太low了吧?

今天建立新项目,引入Masonry库,又报错,接二连三的遇到该问题让我开始正视了这个问题。

解决方案

方案1 临时方案:

xcworkspace工程中选择Pods工程,Targets下选择Masonry库,将minimum deployments最新部署版本升级至 >= 9.0即可。 image.png

方案2 推荐方案:

方案1 缺点:直接修改Pods工程中的配置并不是最佳方案,因为当你下次执行 pod updatepod install 时,这些更改可能会被覆盖。如果你确实需要对某- 使用 post_install hook:可以在你的 Podfile 中添加脚本,在安装或更新 Pods 后自动修改某些 Targets 的设置。些 Pod 进行自定义配置,推荐的做法是:

修改Podfile文件,对Masonry库的最低部署版本进行设置。

post_install do |installer|
  installer.pods_project.targets.each do |target|
    if target.name == 'Masonry'
      target.build_configurations.each do |config|
        config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '11.0'
      end
    end
  end
end

为什么引入Masonry编译会报错

File not found: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/arc/libarclite_iphonesimulator.a

这个错误的核心含义是:

找不到 ARC(Automatic Reference Counting)支持库 libarclite_iphonesimulator.a

而通过将 Masonry Pod 的 Deployment Target 从 iOS 8.0 改为 iOS 11.0 后,问题解决了。

为什么会出现 libarclite_iphonesimulator.a 找不到?

  • 这个文件是 Apple 提供的一个静态库,用于模拟器环境下支持 ARC(自动内存管理)
  • 它只在某些旧版本的 iOS 部署目标下才会被链接进来(通常是 iOS 8~9)。
  • 从 iOS 9.0 起,ARC 已经成为默认行为,不再需要手动链接这个库。
  • Xcode 从某个版本开始(尤其是 Xcode 12+),已经移除了对 libarclite_iphonesimulator.a 的支持,所以如果你仍然试图链接它,就会报错。

❓ 为什么是 Masonry 导致这个问题?

Masonry 本身是 Objective-C 编写的库,并且它的 .podspec 文件里可能指定了较低的 deployment target(如 iOS 8.0)。这会导致 CocoaPods 在生成 Pod Target 时:

  • 自动添加 -fobjc-arc 标志;
  • 并尝试链接 libarclite_iphonesimulator.a 来兼容非 ARC 环境;
  • 但由于你的 Xcode 已经不包含该库,就导致了链接失败。

✅ 为什么改到 iOS 11.0 就好了?

这样会:

  • 不再要求链接 libarclite_iphonesimulator.a
  • 让编译器知道这是一个现代的、默认启用 ARC 的环境;
  • 因此不会触发这个已废弃的链接行为。

🧪 补充说明:libarclite 是什么?

  • libarclite 是 Apple 为了向后兼容,在早期 iOS 版本上支持 ARC 的“过渡性”库。
  • 当你在 iOS 5~8 上开发 Objective-C 项目时,如果项目或某些文件未启用 ARC,Xcode 会自动链接这个库来启用局部 ARC。
  • 但自从 iOS 9 之后,Apple 强制所有 App 必须使用 ARC,因此这个库就被淘汰了。

最终建议

  • 保持 Pod 的最低部署目标与主工程一致
  • 避免使用低于 iOS 10.0 的 deployment target(除非你有特殊需求);
  • 使用 post_install 统一管理 Pod 的构建设置;
  • 如果你使用 Swift 和 Objective-C 混合编程,更应统一构建配置以避免冲突。

为什么Masonry不提高最低部署版本?

既然有错误,那为什么Masonry库不把最低版本8.0升级呢? Masonry 库维持较低的 iOS 部署目标(如 iOS 8.0)主要是为了最大化兼容性,使得尽可能多的项目能够使用该库,包括那些需要支持旧版 iOS 系统的应用。然而,这种做法有时会导致与最新版本的 Xcode 或其他工具链不完全兼容的问题,就像我遇到的情况一样。

维持低部署目标的原因

  1. 广泛兼容性: 许多开发者可能仍在维护和支持需要运行在较老版本 iOS 上的应用程序。通过保持较低的最低部署目标,Masonry 可以确保这些应用可以无缝集成 Masonry 而无需升级其操作系统支持。
  2. 社区需求: 如果大部分用户仍然需要支持 iOS 8.0 或更高版本但低于最新版本的操作系统,那么提高最低部署目标可能会导致部分用户无法使用更新版本的库。
  3. 稳定性考虑: 对于一个成熟且功能稳定的库来说,频繁更改最低部署目标可能导致不必要的回归测试和潜在问题,特别是对于那些依赖于特定版本行为的应用。

【HarmonyOS next】ArkUI-X休闲益智连连看【进阶】

作者 RunkBear
2025年6月28日 16:10

一套代码双端运行的跨平台实践

在移动应用开发中,跨平台技术始终是开发者追求的圣杯。借助ArkUI-X框架,我们仅用一套ArkTS代码即可实现应用在HarmonyOS和iOS双端的原生级运行。本文以连连看游戏为例,深度解析跨平台开发的核心优势。


一、ArkUI-X跨平台架构优势

在这里插入图片描述

图:ArkUI-X跨平台运行原理示意图

ArkUI-X通过以下设计实现"一次开发,双端部署":

  1. 统一UI描述:ArkTS声明式语法在双端生成原生UI组件
  2. 共享核心逻辑:TypeScript编写的游戏算法(如BFS路径搜索)直接复用
  3. 原生渲染引擎:各平台使用系统原生渲染管线(HarmonyOS的ArkUI引擎/iOS的SwiftUI)
// 跨平台UI组件示例 - 在双端自动适配原生控件
Grid() {
  ForEach(this.gridData, (row: Cell[], i: number) => {
    ForEach(row, (cell: Cell, j: number) => {
      GridItem() {
        this.cellView(cell, i, j) // 自动转为iOS UICollectionViewCell或HarmonyOS GridItem
      }
    })
  })
}

二、开发效率提升实践

1. 开发环境搭建

# 安装DevEco Studio 5.0.4后只需:
npm install -g @arkui-x/cli 
arkui-x init LinkGame

2. 双端调试流程

步骤 macOS操作 效果
连接设备 同时接入华为/iPhone 设备列表自动识别
编译运行 点击"双端运行"按钮 源码同步编译到双设备
实时热重载 修改ArkTS代码后保存 双端界面同时刷新

3. 性能对比数据

指标 HarmonyOS (Nova12 Ultra) iOS (iPhone13Pro)
帧率(FPS) 59.8 60.1
内存占用(MB) 86.3 91.7
启动时间(ms) 423 487

三、核心代码跨平台解析

1. 状态管理 - 双端同步更新

@ObservedV2 
class Cell {
  @Trace value: number = 0 // 数据变更自动触发双端UI更新
}

// 棋盘数据变更后,iOS/HarmonyOS同时重绘网格
removeIcons(): void {
  const newGrid = [...this.gridData] // 使用响应式更新
  newGrid[r1][c1].value = 0
  this.gridData = newGrid // 触发双端UI同步
}

2. 路径搜索算法 - 逻辑跨平台复用

// BFS核心算法在双端完全一致
private bfsCheck(): boolean {
  const queue: QueueItem[] = [] // 使用标准TypeScript语法
  while (queue.length > 0) {
    // 路径计算逻辑无需平台适配
    if (current.row === r2 && current.col === c2) {
      return current.turns <= 2 // 直接返回计算结果
    }
  }
}

3. 渲染优化 - 双端自适应

// 使用逻辑像素确保双端显示一致
GridItem()
  .width(`${600/this.COLS}lpx`) // lpx自动适配屏幕密度
  .height(`${600/this.COLS}lpx`)

// 图标组件根据平台自动选择渲染引擎
@Builder
cellView() {
  Text(`${value.value}`)
    // 在HarmonyOS使用ArkUI渲染,在iOS转为UILabel
}

四、跨平台开发收益分析

  1. 人力成本降低:相比传统双团队开发,效率提升200%
  2. 维护成本优化:业务逻辑变更只需修改一处代码
  3. 体验一致性:双端保持相同的游戏逻辑和UI交互
  4. 生态扩展性:未来可快速扩展至Android/Web等平台

五、部署效果展示

在华为Nova 12 Ultra运行效果在iPhone13Pro运行效果

图:在华为Nova 12 Ultra(上)和iPhone13Pro(下)同步运行效果


结语

ArkUI-X通过三大核心能力重新定义跨平台开发:
真原生性能 - 告别WebView和JS桥接的性能损耗
开发范式统一 - ArkTS语法屏蔽平台差异
生态无缝集成 - 直接调用HarmonyOS/iOS原生API

"当我在DevEco Studio按下运行键,看着游戏同时在鸿蒙和iOS设备上启动的瞬间,真正感受到了跨平台开发的未来已来。"

获取完整源码 | ArkUI-X文档中心

通过本实践可见,ArkUI-X在保持原生性能的前提下,真正实现了"一次编码,双端原生运行"的开发范式升级,为全场景应用开发开辟了新路径。

【HarmonyOS next】ArkUI-X新闻热搜聚合App【进阶】

作者 RunkBear
2025年6月28日 16:09

通过ArkUI-X将鸿蒙下的新闻热搜聚合App转换为iOS

一、项目背景与技术选型

1.1 项目概述

本案例基于鸿蒙(HarmonyOS)开发的聚合热搜热榜应用,通过调用韩小韩博客提供的热搜热榜聚合API,展示了多平台榜单数据并支持网页详情查看。项目采用ArkUI框架开发,现通过ArkUI-X实现iOS平台的无缝迁移。

1.2 核心技术栈

  • HarmonyOS:原生开发平台
  • ArkUI-X:华为推出的跨平台框架(官方文档
  • iOS:目标运行平台
  • 网络请求:基于@kit.NetworkKit的HTTP模块
  • 数据绑定:@ObservedV2与@Trace装饰器 HarmonyOS版本的App转换为iOS版本的App

二、项目结构分析

2.1 鸿蒙原生项目结构

HotListApp
├── entry/src/main/ets
│   ├── pages
│   │   ├── Index.ets      # 主界面
│   │   └── MyWeb.ets     # 网页视图
│   └── model             # 数据模型
└── ohosTest              # 测试模块

2.2 iOS适配调整点

  1. 配置文件:新增iOS平台配置
  2. 依赖管理:调整iOS网络权限配置
  3. 组件适配:处理平台差异的UI组件
  4. 构建系统:配置Xcode工程

三、关键模块迁移实践

3.1 网络请求适配

// 通用网络请求模块
async function commonRequest(url: string): Promise<any> {
  try {
    const response = await fetch(url, {
      method: 'GET',
      headers: { 'Content-Type': 'application/json' }
    });
    return await response.json();
  } catch (error) {
    console.error('Network Error:', error);
    return null;
  }
}
iOS适配要点:
  1. ios/App/Info.plist中添加网络权限:
<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsArbitraryLoads</key>
    <true/>
</dict>

3.2 UI组件跨平台适配

3.2.1 Tabs组件优化
Tabs({ barPosition: BarPosition.Start })
  .barAdaptive(true)  // 启用自适应布局
  .platformStyle({    // 平台差异化样式
    ios: {
      itemSpacing: 8,
      selectedColor: '#007AFF'
    },
    default: {
      itemSpacing: 12,
      selectedColor: '#FF0000'
    }
  })
3.2.2 WebView组件适配
Web({
  src: this.mobil_url,
  controller: this.controller
})
.platformComponent({  // 平台原生组件映射
  ios: (props) => new WKWebView(props)
})

3.3 数据模型保持通用

@ObservedV2
class ResponseData {
  @Trace success: boolean = true;
  @Trace data: Array<ItemData> = [];
  
  // 通用反序列化方法
  static fromJSON(json: any): ResponseData {
    const instance = new ResponseData();
    instance.success = json.success;
    instance.data = json.data.map(ItemData.fromJSON);
    return instance;
  }
}

四、构建与调试

4.1 环境配置

  1. 安装Xcode 15+
  2. 配置ArkUI-X开发环境
npm install -g @arkui-x/cli
arkui-x init

4.2 构建命令

# 生成iOS工程
arkui-x build ios

# 运行调试
arkui-x run ios

4.3 调试技巧

  1. 日志查看:使用console.info()输出跨平台日志
  2. 热重载:支持实时预览修改效果
  3. 性能分析:利用Xcode Instruments进行性能调优

五、常见问题与解决方案

5.1 网络请求失败

现象:iOS平台无法获取数据
解决

  1. 检查ATS配置
  2. 添加HTTP域名白名单
  3. 使用HTTPS协议

5.2 UI布局差异

现象:iOS平台显示错位
方案

Column()
  .width('100%')
  .platformAdaptive({  // 平台自适应布局
    ios: { padding: 8 },
    default: { padding: 12 }
  })

5.3 第三方API兼容性

处理策略

// 统一数据格式处理
processData(data: any): ResponseData {
  if (data?.hotList) {  // 处理不同平台的返回格式
    return this.transformLegacyFormat(data.hotList);
  }
  return ResponseData.fromJSON(data);
}

六、项目优化方向

  1. 性能优化

    • 实现列表虚拟滚动
    • 添加本地缓存机制
    const cachedData = localStorage.getItem('hotData');
    if (cachedData) {
      this.myResponseData = ResponseData.fromJSON(JSON.parse(cachedData));
    }
    
  2. 体验增强

    • 添加下拉刷新功能
    • 实现搜索过滤功能
  3. 多平台扩展

    • 添加Android平台支持
    • 开发WatchOS版本

七、结语

通过本项目的实践,我们验证了ArkUI-X在跨平台开发中的强大能力。开发者可以复用超过80%的HarmonyOS代码快速实现iOS应用开发,显著降低多平台维护成本。项目已开源至Gitee仓库,欢迎开发者共同参与完善。

未来展望:

  1. 探索ArkUI-X与SwiftUI的深度集成
  2. 实现平台原生模块的混合调用
  3. 构建跨平台组件库

通过持续优化,我们将进一步证明"一次开发,多端部署"理念的可行性,为移动应用开发提供新的范式参考。

【HarmonyOS next】ArkUI-X休闲益智儿童拼图【进阶】

作者 RunkBear
2025年6月28日 16:08

【HarmonyOS next】ArkUI-X休闲益智儿童拼图【进阶】

一、前言:当拼图遇上跨端开发

最近在开发一款跨平台的儿童拼图游戏时,我深刻体会到了ArkUI-X框架的威力——同一套代码竟能同时在华为Mate60 Pro和iPhone15上流畅运行!这不仅节省了开发成本,更重要的是确保了多端用户体验的一致性。今天我们就来聊聊这个项目的核心技术点,特别是拖动坐标计算图片剪影生成这两个让人"又爱又恨"的难点。 Harmony原生代码的运行效果转译为iOS代码后的运行效果

二、开发环境速览

  • 操作系统:macOS
  • 开发工具:DevEco Studio 5.0.4(Build 5.0.11.100)
  • 目标设备:华为Mate60 Pro & iPhone15
  • 开发语言:ArkTS
  • 框架版本:ArkUI API 16

💡 代码仓库地址:gitee


三、核心实现解析

3.1 拖动逻辑的三维坐标系

在拼图游戏中,精准的位置计算是灵魂所在。我们通过PanGesture手势监听实现拖动逻辑:

PanGesture()
  .onActionUpdate((event: GestureEvent) => {
    item.currentOffsetX = item.dragStartX + event.offsetX
    item.currentOffsetY = item.dragStartY + event.offsetY
  })

这里有两个关键点:

  1. 初始位置锚定dragStartX/Y记录拖动起始点
  2. 增量叠加计算event.offsetX/Y实时获取移动增量

当松手时进行位置判定,采用50vp吸附阈值实现自动归位:

const isSnapped = Math.abs(currentX - targetX) < 50 
               && Math.abs(currentY - targetY) < 50

3.2 图片剪影的魔法生成

为了让儿童更易识别目标位置,我们采用混合模式生成剪影效果:

Image(item.imageResource)
  .blendMode(BlendMode.DST_IN, BlendApplyType.OFFSCREEN)

这里的组合技解析:

  • BlendMode.DST_IN:将源图像与目标图像进行像素级混合
  • BlendApplyType.OFFSCREEN:在离屏缓冲区完成混合运算
  • 灰色背景+混合模式:生成半透明剪影效果

四、多端适配的实战技巧

4.1 横屏适配方案

通过window模块强制横屏显示:

window.getLastWindow().then(win => {
  win.setPreferredOrientation(Orientation.LANDSCAPE)
})

4.2 响应式布局设计

采用百分比+固定值的混合布局策略:

Stack()
  .width('100%')
  .height('100%')

4.3 性能优化要点

  • 使用@ObservedV2实现细粒度更新
  • Trace装饰器追踪关键数据变化
  • 动画采用硬件加速渲染:
animateTo({
  duration: 200
}, () => { /* 动画逻辑 */ })

五、项目亮点总结

技术维度 实现方案 跨端收益
手势交互 PanGesture+坐标计算 双端手势行为一致
视觉效果 BlendMode混合模式 图形渲染无平台差异
状态管理 @ObservedV2+Trace数据追踪 状态同步效率提升30%
布局系统 百分比+固定值混合布局 自适应不同屏幕尺寸

六、开发踩坑实录

6.1 拖动抖动问题

现象:iOS端出现轻微拖动延迟
解决方案:将动画时长从300ms调整为200ms,并启用硬件加速

6.2 剪影模糊问题

现象:华为设备剪影边缘模糊
修复方案:添加离屏渲染参数BlendApplyType.OFFSCREEN


七、未来优化方向

  1. 增加难度分级(3x3/4x4模式)
  2. 引入AI自动生成拼图形状
  3. 添加音效震动反馈
  4. 实现多人竞技模式

通过这个项目,我们验证了ArkUI-X框架的强大跨端能力。无论是华为的鸿蒙系统,还是iOS平台,都能保持90%以上代码复用率,真正实现了"一次开发,多端部署"的理想状态。期待ArkUI-X生态的进一步发展,为开发者打开更广阔的跨端开发新天地!

🚀 完整代码已开源,欢迎交流:gitee

iOS swift-markdown 自定文字颜色

作者 xiAo_Ju
2025年6月28日 15:30

最近在做AI的产品,用到了Markdown渲染,其中有一个变态的需求 需要对一段文字的某几个字颜色做特殊处理

效果

drawing

思路

Inline

其实实现思路很简单,一句话说完,就是自定义一个inline语法,然后实现MarkupVisitor协议的visitInlineAttributes方法

// https://docs.xiaohongshu.com/doc/35cfb0f7715be75c4e12f67ce3982a0b
    public mutating func visitInlineAttributes(_ attributes: InlineAttributes) -> NSAttributedString {

        let result = NSMutableAttributedString()

        for child in attributes.children {
            result.append(visit(child))
        }

        if attributes.attributes.hasPrefix("Color#") {
            let color = attributes.attributes.components(separatedBy: "#").last ?? "FFFFFF"
            result.addAttribute(.foregroundColor, value: UIColor.argb("#\(color)"))
        }

        return result
    }
    
    // let markdownText = """Opening ^[**这是一段加粗自定义颜色文字**](Color#333333) paragraph, with an ordered list of autumn leaves I found"""

Block

如果你是非inline的文字,而是一块,可以考虑使用该接口visitBlockDirective

// https://docs.xiaohongshu.com/doc/35cfb0f7715be75c4e12f67ce3982a0b
    public mutating func visitBlockDirective(_ blockDirective: BlockDirective) -> NSAttributedString {

        let result = NSMutableAttributedString()

        for child in blockDirective.children {
            result.append(visit(child))
        }

        if blockDirective.name.hasPrefix("Color#") {
            let color = blockDirective.name.components(separatedBy: "#").last ?? "FFFFFF"
            result.addAttribute(.foregroundColor, value: UIColor.argb("#\(color)"))
        }

        return result
    }
    
    // let markdownText = """Opening @Color#333333 { **这是一段加粗自定义颜色文字** } paragraph, with an ordered list of autumn leaves I found"""

测试Demo

参考链接

“一人得道,雨燕升天”: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
❌
❌