普通视图

发现新文章,点击刷新页面。
今天 — 2025年4月8日iOS

切勿将辅助驾驶宣传成智能驾驶 | 肘子的 Swift 周报 #078

作者 东坡肘子
2025年4月8日 08:02

issue78.webp

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

切勿将辅助驾驶宣传成智能驾驶

不久前,某个造成三人死亡的交通事故因为涉及某新锐电动汽车品牌再度引发了人们对“智能驾驶”功能的质疑。在目前披露的有限资料中,至少可以确认的是,“智能驾驶”系统未能在相当长的一段行驶距离中判断出当前的路段正在施工(沿途有施工警示标志),只在撞击前2-3秒前给予了警示。这意味着,在系统报警后,驾驶者只有极短的反应时间。

从当前的法规角度来说,无论是否启用“智能驾驶”功能,对于事故所造成的后果在没有其他汽车机械结构问题的情况下,仍主要由驾驶人本人来承担。但随着越来越多事故与“智能驾驶”相关联,我们不得不质疑:这难道真的和这些汽车厂商对于“智能驾驶”的过分夸大宣传无关吗?

最近一两年出现了一个值得关注的现象:许多非科技行业的普通消费者了解 AI 概念和相关专业术语的渠道,很大程度上竟然来自于提供“智能驾驶”功能的汽车厂商的发布会和销售宣传。从“端到端”到 TOPS,似乎一夜之间,主要作为交通工具的车辆成为了高科技的最佳载体,“智能驾驶”功能成为了消费者购买车辆的重要参考因素。

事实上,无论是从当前的科技发展水平还是各个地区的法律准备来看,即使发展到了 L5 阶段(现阶段大多数车型仍处在 L2 至 L3 之间),所谓的“智能驾驶”仍应被定义为“辅助驾驶”。这不仅关乎系统能力的局限性,更涉及最终责任划分的法律问题。各个车厂很清楚这一点,因此你可以在它们提供的各种说明、手册、宣传资料(角落中,用小字标注)看到它们给出的警示。但在更广泛的宣传渠道上,“辅助驾驶”一词早已被“智能驾驶”所代替。

随着各种对“智驾”的持续宣传,消费者在不知不觉中就丧失了对驾驶安全的责任感,产生了“智驾”更好、更安全的错觉。即使某些“智能系统”确实有着不错的辅助功能,但它们远未达到可以完全替代人类驾驶员判断的程度,尤其是在复杂、非常规的交通环境中。

尤其值得警惕的是,许多厂商在宣传“智能驾驶”能力时,只展示高端配置下的最佳表现,却忽视了中低端车型在算力、摄像头、传感器等方面的减配情况。更严重的是,有些厂商并未针对不同配置对算法进行差异化适配,导致低配车型的“智能系统”极易出现计算失误和决策延迟的问题,这直接加剧了交通事故风险。

实际上,“智能”、“自动”本身就是极易产生误解的模糊概念。当这些模糊的营销词汇已明显误导消费者的驾驶安全认知时,必须尽快出台明确的法律规范,限制相关用语的宣传范围与频率,并对夸大宣传进行严厉处罚。只要法律明确最终责任人仍是驾驶者本人,那么所有驾驶系统无论如何宣传,都应被严格限定在“辅助驾驶”的范畴之内。同时,消费者也应明确认识到,现阶段没有车厂会为所谓“智能系统”的问题承担法律责任。更何况,普通消费者根本不具备就“智能系统”的缺陷进行有效举证的能力。

切勿将辅助驾驶宣传成智能驾驶!

前一期内容全部周报列表

原创

远离 dismiss,拥抱状态驱动

在 SwiftUI 开发中,环境值 dismiss 因其灵活、自适应的特性备受开发者青睐。它能够根据当前视图的上下文智能执行关闭操作,让许多开发者将它作为首选工具。然而,便捷的背后往往隐藏着风险。频繁使用 dismiss 可能在应用程序中埋下隐患,引发测试难题乃至难以追踪的稳定性问题。本文将分析我们为何应谨慎对待 dismiss,并介绍更加健壮可靠的状态管理方案。

近期推荐

Swift 6.1 发布

Swift 6.1 正式发布!Holly Borla 在本文中介绍了多个令人期待的新特性。本次更新在语言层面带来了更广泛的 nonisolated 支持、更智能的 withTaskGroup 类型推导,以及更便捷的 @objc @implementation,进一步强化与 Objective-C 的互操作能力。在语法便利性方面,尾随逗号现已支持用于函数参数、泛型定义、闭包捕获列表等结构,简化了代码生成与维护流程。

SwiftPM 引入 Package Traits,支持根据运行环境(如 Embedded 或 WebAssembly)配置不同的 API 和依赖,进一步增强跨平台能力。同时,SourceKit-LSP 现已默认开启后台索引,显著提升开发期间的响应速度与智能感知表现。

在测试方面,Swift Testing 推出了 Test Scoping Traits,使测试前后的上下文共享更加灵活易控;Swift-DocC 也改进了重载函数链接的歧义处理机制,提升文档的可维护性与可读性。

Swift 6.1 可通过 Xcode 16.3 获取,也可使用 swiftly 工具在 macOS 与 Linux 上独立安装。

Swift 的跨平台编译 (Cross Compiling Swift)

提升跨平台编译能力,是 Swift 向全平台生态迈进的重要一步。Khan Winter 在开发 Discord Bot 和 Bluesky Bot 的过程中,深入探索了将 Swift 应用从 macOS 编译部署至 Gentoo Linux 的两种路径:一是借助 Static Linux SDK 与 Swiftly 工具实现的本地交叉编译,二是使用 Docker 容器完成传统的跨平台构建。文章不仅展示了详细的工具链配置与部署脚本,也指出了当前 toolchain 与 SDK 配置上的不一致和易混淆问题。

Swift 中的现代 URL 构造方式 (Modern URL Construction in Swift)

在 Swift 中构造 URL 时避免处理 Optional 一直是开发者关注的问题。John Sundell 在这篇久违的回归文章中,分享了静态与动态 URL 构造的现代实践:对于静态 URL,可通过扩展 URL.init(staticString:) 简化强制解包流程,结合 Swift 5.9 引入的宏功能,还可实现编译期校验的 #staticURL(...);对于动态 URL,则推荐使用 iOS 16 起提供的 appending(component:)appending(queryItems:) 等 API,替代传统字符串拼接方式,提升代码的安全性与可读性。这些改进让我们能以更简洁、可靠的方式构造 URL,适用于网络请求与文件路径等场景。

在 SwiftUI 中应用 Inspector 组件 (Presenting an Inspector with SwiftUI)

Inspector 是 iOS 17、iPadOS 17 与 macOS 14 中引入的 SwiftUI 组件,常用于展示与选中内容相关的详细信息。它会根据平台与上下文以不同形式呈现,这是其灵活性的体现,也为开发者带来了额外的理解成本。在本文中,Antonella Giugliano 通过多个代码示例,详细演示了 Inspector 在各种使用场景下的表现,并介绍了如何通过 InspectorCommands 实现快捷键控制其显示。

将地图视图保存成图片 (Creating an Image from an MKMapView)

虽然 SwiftUI 提供了 ImageRenderer API 用于将视图转换为图片,但在某些场景下并不能满足需求。Patrick McConnell 在尝试导出 MapView 时就遇到了这个问题。本文分享了他在 macOS 项目中,通过 NSViewRepresentable 嵌入 MKMapView,并借助视图层级探索,找到一种可行方案,最终成功生成包含路径、图层与标注的完整地图图像。文章不仅解释了为何标注无法直接渲染成图像,还提供了对 NSViewMKMapView 的扩展代码,实现了当前地图状态的稳定导出。

支持 MCP 的七个 AI 框架 (The Top 7 MCP-Supported AI Frameworks)

Model Context Protocol(MCP)正逐步成为连接 LLM 与外部工具的行业标准,提供了一种统一、高效的上下文注入方式。在本文中,Amos Gyamfi 系统梳理了七个支持 MCP 的主流 AI 框架,包括 OpenAI Agents SDK、LangChain、Chainlit、Agno、Upsonic、Mastra 等,并通过详实的示例代码演示了如何将 MCP 工具集成进代理式(agentic)工作流,显著提升 AI 应用的扩展性与可维护性。尽管示例代码基于 Python 与 TypeScript,仍为苹果生态开发者提供了参考思路。

SwiftUI 状态管理 (SwiftUI Craftsmanship: State Management)

在 SwiftUI 中,一切皆由状态驱动。本文中,Danny Bolella 以“最小状态”作为指导原则,探讨了如何在复杂界面中实现状态管理的简化与分层:通过识别状态间的依赖关系,将状态分离到对应的子视图中,不仅提升了代码可读性,也让 UI 保持响应性与可维护性。

AdventureX 2025

AdventureX-2025

AdventureX 2025 将于 2025 年 7 月 23 日至 27 日在中国杭州举行。本届活动将创造中国黑客松规模的新纪录,提供 800 个参赛名额,并为参赛者提供免费食宿与出行补助。

往期内容

THANK YOU

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

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

昨天 — 2025年4月7日iOS

Issar 搜索

2025年4月7日 16:32

全文检索

全文检索是一种从数据库中搜索文本的强大功能。你现在应该已经熟悉索引的工作原理了,但还是让我们先了解一些基本知识。

索引就像一张查询表,允许快速地根据给定值查找数据。例如,如果你的对象含有一个 title 字段,你可以以该字段创建一张索引表,以此根据给定的标题来快速查询。

为什么全文检索很有用?

你本可以轻松通过 Filter 来搜索文本。Isar 为你提供了许多字符串查询方法,例如 .startsWith().contains().matches()。但问题在于 Filter 的复杂度是 O(n),其中 n 是 Collection 中对象的个数,像 .matches() 这样的字符串操作就格外消耗性能。

:::tip 全文检索比 Filter 快多了,但是索引也有局限的地方。在本专题中,我们将探寻如何解决这些局限性。 :::

基本示例

想法依然不变:我们对文本中的单词进行索引,而不是对整个文本索引,这样我们可以对单个单词进行搜索。

让我们先创建一个基本的全文检索索引:

class Message {
  Id? id;

  late String content;

  @Index()
  List<String> get contentWords => content.split(' ');
}

现在我们可以通过内容中某些指定词汇来搜索讯息:

final posts = await isar.messages
  .where()
  .contentWordsAnyEqualTo('hello')
  .findAll();

这条查询非常快,但是有几个问题:

  1. 我们只能搜索整个词汇
  2. 我们没考虑标点符号
  3. 我们不支持其他空白字符

正确分割文本

让我们完善上述例子。我们可以用一个复杂的正则来正确分割文本,但是在某些少数情况下它很可能会出错且导致查询变得很慢。

Unicode Annex #29 为几乎所有人类语言定义了如何正确分割文本。它很复杂,但是幸运的是,Isar 内部已经帮我们实现了:

Isar.splitWords('hello world'); // -> ['hello', 'world']

Isar.splitWords('The quick (“brown”) fox can’t jump 32.3 feet, right?');
// -> ['The', 'quick', 'brown', 'fox', 'can’t', 'jump', '32.3', 'feet', 'right']

我想要更多控制

很简单!我们可以修改索引配置,让它支持前缀匹配和大小写匹配:

class Post {
  Id? id;

  late String title;

  @Index(type: IndexType.value, caseSensitive: false)
  List<String> get titleWords => title.split(' ');
}

默认情况下,Isar 会将单词散列化,这么做性能很快且节省存储空间。但是这样就无法使用前缀匹配查询。我们改变了索引类型,使用 IndexType.value 而不是 IndexType.hash,来直接使用那些单词。借此我们就可以使用 .titleWordsAnyStartsWith() 的 Where 子句:

final posts = await isar.posts
  .where()
  .titleWordsAnyStartsWith('hel')
  .or()
  .titleWordsAnyStartsWith('welco')
  .or()
  .titleWordsAnyStartsWith('howd')
  .findAll();

我也需要 .endsWith() 方法

没问题!我们会用一个小技巧来实现 .endsWith() 匹配:

class Post {
    Id? id;

    late String title;

    @Index(type: IndexType.value, caseSensitive: false)
    List<String> get revTitleWords {
        return Isar.splitWords(title).map(
          (word) => word.reversed).toList()
        );
    }
}

不要忘记倒序排列查询的结果:

final posts = await isar.posts
  .where()
  .revTitleWordsAnyStartsWith('lcome'.reversed)
  .findAll();

词干提取算法

不幸的是,索引不支持 .contains() 匹配(其他数据库也如此)。但是还有几个备选方案值得我们研究一番。选择何种方式完全取决于你的使用场景。举个例子,你可以对词干进行索引,而不是对整个单词索引。

词干提取算法指的是自然语言处理领域里去除词缀得到词根的过程,即得到单词最一般的写法:

connection
connections
connective          --->   connect
connected
connecting

常见的算法有 Porter 词干提取算法Snowball 词干提取算法

还有将单词复杂形态转变成最基础形态的词形还原

语音算法

语音算法 是指根据发音来检索单词的算法。也就是说,它可以根据发音接近程度来帮你查询结果。

:::warning 大部分语音算法通常只支持单一语言,一般是英语。 :::

Soundex

Soundex 是一种语音算法,它通过英文发音来检索名字。它的目的是将同音词用同一编码表示,虽然发音略有差异,但可达到模糊匹配的效果。这是个非常直接明了的算法,也有若干改进版本。

若是用这个算法,那么单词 "Robert""Rupert" 都会返回编码 "R163",而单词 "Rubin" 则返回 "R150"。 同音词 "Ashcraft""Ashcroft" 则都会返回 "A261"

Double Metaphone

Double Metaphone 也是一种语音算法,是 Metaphone 的二代版本。它在前代基础上改进了不少基本设计。

Double Metaphone 加入了对大量来自外来语如斯拉夫语、德语、凯尔特语、希腊语、法语、意大利语、西班牙语、中文等的不规则英文单词发音的支持。

iOS 蓝牙开发基础知识梳理

作者 90后晨仔
2025年4月7日 15:36

1. 蓝牙基础

1.1 蓝牙类型

  • BLE(Bluetooth Low Energy)
    • 低功耗,适用于间歇性数据传输(如健康设备、传感器)。
    • iOS 主要支持 BLE(4.0+)。
  • 经典蓝牙(Bluetooth Classic)
    • 高带宽,持续传输(如音频设备)。
    • iOS 不支持经典蓝牙开发(仅支持连接配对设备,如耳机)。

1.2 角色划分

角色 描述 核心类
Central(中心设备) 主动扫描并连接外围设备(如 iPhone 作为中心设备连接手环)。 CBCentralManager, CBPeripheral
Peripheral(外围设备) 提供服务和数据(如手环作为外围设备广播心率数据)。 CBPeripheralManager, CBMutableService

2. Core Bluetooth 核心类

2.1 Central 角色

  • CBCentralManager:管理中心设备,扫描、连接外围设备。
  • CBPeripheral:表示连接的外围设备,包含服务、特征等数据。
  • CBService:外围设备提供的服务(如心率服务)。
  • CBCharacteristic:服务中的特征,用于读写数据或订阅通知。

2.2 Peripheral 角色

  • CBPeripheralManager:管理外围设备,广播服务。
  • CBMutableService:可修改的服务(定义 UUID 和特征)。
  • CBMutableCharacteristic:可修改的特征(定义权限和值)。

3. BLE 开发流程(Central 模式)

3.1 初始化 Central Manager

import CoreBluetooth

class BluetoothManager: NSObject {
    var centralManager: CBCentralManager!
    
    override init() {
        super.init()
        centralManager = CBCentralManager(delegate: self, queue: nil)
    }
}

extension BluetoothManager: CBCentralManagerDelegate {
    // 检测蓝牙状态
    func centralManagerDidUpdateState(_ central: CBCentralManager) {
        switch central.state {
        case .poweredOn:
            print("蓝牙已开启,开始扫描设备")
            central.scanForPeripherals(withServices: nil, options: nil)
        case .poweredOff:
            print("蓝牙未开启")
        default: break
        }
    }
}

3.2 扫描设备

// 扫描所有设备(指定 serviceUUID 可过滤目标设备)
centralManager.scanForPeripherals(withServices: nil, options: [CBCentralManagerScanOptionAllowDuplicatesKey: false])

// 发现设备回调
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) {
    print("发现设备: \(peripheral.name ?? "未知设备"), RSSI: \(RSSI)")
    // 根据设备名或 UUID 筛选目标设备
    if peripheral.name?.contains("MyDevice") == true {
        centralManager.connect(peripheral, options: nil)
    }
}

3.3 连接设备

// 发起连接
centralManager.connect(peripheral, options: nil)

// 连接成功回调
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
    print("连接成功: \(peripheral.name ?? "")")
    peripheral.delegate = self
    peripheral.discoverServices(nil) // 发现所有服务
}

// 连接失败回调
func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
    print("连接失败: \(error?.localizedDescription ?? "")")
}

3.4 发现服务与特征

extension BluetoothManager: CBPeripheralDelegate {
    // 发现服务
    func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
        guard let services = peripheral.services else { return }
        for service in services {
            print("发现服务: \(service.uuid)")
            // 发现服务的特征
            peripheral.discoverCharacteristics(nil, for: service)
        }
    }
    
    // 发现特征
    func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
        guard let characteristics = service.characteristics else { return }
        for characteristic in characteristics {
            print("特征 UUID: \(characteristic.uuid)")
            // 订阅通知(如需接收数据)
            if characteristic.properties.contains(.notify) {
                peripheral.setNotifyValue(true, for: characteristic)
            }
            // 读取特征值
            peripheral.readValue(for: characteristic)
        }
    }
}

3.5 读写数据

// 读取特征值
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
    if let data = characteristic.value {
        let value = String(data: data, encoding: .utf8) ?? "无法解析"
        print("收到数据: \(value)")
    }
}

// 写入数据(需特征支持写入)
func writeData(to characteristic: CBCharacteristic, peripheral: CBPeripheral) {
    let data = "Hello BLE".data(using: .utf8)!
    peripheral.writeValue(data, for: characteristic, type: .withResponse)
}

4. 关键注意事项

4.1 权限与配置

  • Info.plist 配置
    <key>NSBluetoothAlwaysUsageDescription</key>
    <string>需要蓝牙权限以连接设备</string>
    <key>NSBluetoothPeripheralUsageDescription</key>
    <string>需要蓝牙权限以连接设备</string> <!-- 兼容旧版本 -->
    
  • 后台模式(保持连接):
    <key>UIBackgroundModes</key>
    <array>
        <string>bluetooth-central</string>
        <string>bluetooth-peripheral</string>
    </array>
    

4.2 UUID 规范

  • 标准 UUID:16 位或 128 位(如心率服务 UUID:0x180D0000180D-0000-1000-8000-00805F9B34FB)。
  • 自定义 UUID:生成唯一 UUID(如使用 uuidgen 命令)。

4.3 连接优化

  • 自动重连:监听断开事件并重新连接。
    func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
        print("设备断开,尝试重连...")
        central.connect(peripheral, options: nil)
    }
    
  • 超时处理:使用 Timer 限制连接时间。

5. 常见问题与解决方案

问题 解决方案
扫描不到设备 确认设备处于广播模式,检查蓝牙权限,确保 UUID 匹配。
连接不稳定 优化信号环境,实现自动重连逻辑,检查设备电量。
读写数据失败 确认特征具有正确的权限(.read, .write),使用正确的写入类型(.withResponse)。
后台模式断开连接 启用 bluetooth-central 后台模式,使用 CBCentralManagerOptionRestoreIdentifierKey 恢复连接。
数据传输不完整 分包发送大数据(每包 ≤ 20 字节),接收端拼接数据。

6. 第三方库推荐

  • Bluejay:简化 BLE 操作,支持链式调用。
  • SwiftBluetooth:封装 Core Bluetooth,提供更简洁的 API。
  • BabyBluetooth(OC):简化流程,适合快速开发。

总结

  • 核心步骤:初始化 → 扫描 → 连接 → 发现服务/特征 → 读写数据。
  • 关键代理CBCentralManagerDelegate, CBPeripheralDelegate
  • 注意事项:权限配置、UUID 管理、后台模式、数据分片。
  • 适用场景:健康设备、智能家居、传感器数据采集等低功耗实时通信。

什么是WebSocket ?ios 中如何使用?

作者 90后晨仔
2025年4月7日 14:41

1. WebSocket 基础

1.1 什么是 WebSocket?

  • 全双工通信协议:基于 TCP,允许客户端与服务器建立持久连接,双方可主动发送数据。
  • 实时性:替代 HTTP 轮询,适用于实时聊天、股票行情、在线游戏等场景。
  • 协议升级:通过 HTTP 握手(Upgrade: websocket)建立连接,后续通过 WebSocket 帧通信。

1.2 与 HTTP 对比

特性 HTTP WebSocket
连接方式 短连接(请求-响应后断开) 长连接(持久化,双向通信)
通信方向 单向(客户端主动请求) 双向(客户端/服务器均可主动发送)
头部开销 每次请求携带完整 HTTP 头 初始握手后,数据帧头部极小
适用场景 静态资源获取、REST API 实时数据传输(如聊天、推送)

2. iOS 中 WebSocket 的实现方式

2.1 官方方案:URLSessionWebSocketTask(iOS 13+)

  • 核心类URLSessionWebSocketTask(属于 URLSession 框架)
  • 特点
    • 原生支持,无需第三方库。
    • 支持 iOS 13+,兼容性有限。
    • 提供基本连接管理、消息发送/接收功能。
基本使用流程
import Foundation

// 使用 actor 来确保线程安全
actor WebSocketManager {
    // 创建一个 URLSession 实例,用于管理 WebSocket 连接
    private let session = URLSession(configuration: .default)
    
    // 声明一个可选的 URLSessionWebSocketTask,用于处理 WebSocket 通信
    private var webSocketTask: URLSessionWebSocketTask?
    
    // 连接到 WebSocket 服务器
    func connect() {
        // 检查 URL 是否有效
        guard let url = URL(string: "wss://your-server.com/socket") else {
            print("无效的 URL") // 如果 URL 无效,打印错误信息并退出
            return
        }
        
        // 创建 WebSocketTask 并将其赋值给 webSocketTask 属性
        webSocketTask = session.webSocketTask(with: url)
        
        // 启动 WebSocket 连接
        webSocketTask?.resume()
        
        // 开始接收消息
        receiveMessage()
    }
    
    // 接收消息的方法
    private func receiveMessage() {
        // 调用 WebSocketTask 的 receive 方法,监听服务器发来的消息
        webSocketTask?.receive { [weak self] result in
            // 在一个新的 Task 中处理接收到的消息(确保线程安全)
            Task {
                await self?.handleReceive(result)
            }
        }
    }
    
    // 处理接收到的消息
    private func handleReceive(_ result: Result<URLSessionWebSocketTask.Message, Error>) {
        switch result {
        case .success(let message):
            // 根据消息类型进行处理
            switch message {
            case .data(let data):
                // 如果是二进制数据,打印数据内容
                print("收到二进制数据: (data)")
            case .string(let text):
                // 如果是文本消息,打印文本内容
                print("收到文本消息: (text)")
            @unknown default:
                // 处理未知的消息类型(防止未来扩展导致崩溃)
                fatalError()
            }
            
            // 继续监听下一条消息
            self.receiveMessage()
        case .failure(let error):
            // 如果接收消息失败,打印错误信息
            print("接收失败: (error)")
        }
    }
    
    // 发送消息到 WebSocket 服务器
    func sendMessage(_ message: String) {
        // 将消息以字符串形式发送到服务器
        webSocketTask?.send(.string(message)) { error in
            if let error = error {
                // 如果发送失败,打印错误信息
                print("发送失败: (error)")
            } else {
                // 如果发送成功,打印成功信息
                print("消息发送成功")
            }
        }
    }
    
    // 关闭 WebSocket 连接
    func disconnect() {
        // 取消 WebSocketTask,并指定关闭原因
        webSocketTask?.cancel(with: .goingAway, reason: nil)
        print("连接已关闭")
    }
}

2.2 第三方库:Starscream

  • 适用场景:支持 iOS 11+,功能更丰富(如 SSL 验证、自定义头、自动重连)。
  • 集成方式:通过 CocoaPods 或 SPM 安装。
基本使用
import Starscream

// 1. 创建 WebSocket 实例
var request = URLRequest(url: URL(string: "wss://your-server.com/socket")!)
request.setValue("Bearer token", forHTTPHeaderField: "Authorization")
let socket = WebSocket(request: request)

// 2. 设置代理
socket.delegate = self

// 3. 连接服务器
socket.connect()

// 4. 发送消息
socket.write(string: "Hello Server")

// 5. 关闭连接
socket.disconnect()

// 实现 WebSocketDelegate
extension ViewController: WebSocketDelegate {
    func didReceive(event: WebSocketEvent, client: WebSocket) {
        switch event {
        case .connected(let headers):
            print("连接成功: \(headers)")
        case .disconnected(let reason, let code):
            print("断开连接: \(reason), code: \(code)")
        case .text(let text):
            print("收到文本: \(text)")
        case .binary(let data):
            print("收到二进制数据: \(data)")
        case .error(let error):
            print("错误: \(error?.localizedDescription ?? "")")
        case .cancelled:
            print("连接取消")
        default:
            break
        }
    }
}

3. 核心功能与最佳实践

3.1 连接管理

  • 心跳机制(Ping/Pong):定期发送 Ping 帧检测连接活性。
    // URLSessionWebSocketTask
    webSocketTask.sendPing { error in
        if let error = error {
            print("Ping 失败: \(error)")
        }
    }
    
    // Starscream
    socket.write(ping: Data()) // 发送 Ping
    
  • 自动重连:网络中断后自动尝试重新连接(需第三方库支持或自定义逻辑)。

3.2 消息处理

  • 数据类型:支持文本(String)、二进制(Data)。
  • 序列化:常用 JSON 格式传输结构化数据。
    // 发送 JSON
    let message = ["type": "chat", "content": "Hello"]
    if let data = try? JSONEncoder().encode(message) {
        webSocketTask.send(.data(data)) { /* 处理错误 */ }
    }
    

3.3 线程安全

  • 主线程更新 UI:在接收消息的回调中切换至主线程更新界面。
    DispatchQueue.main.async {
        self.label.text = receivedText
    }
    

3.4 错误处理与断线重连

  • 监听错误事件:捕获连接错误、消息发送失败等。
  • 重连策略:指数退避重试(如 1s、2s、4s...)。

4. 安全性

  • 使用 wss://:WebSocket Secure(基于 TLS 加密)。
  • 证书验证:在 URLSessionDelegate 中处理 SSL 证书校验(如需自定义)。
  • 身份验证:通过 HTTP 头(如 Authorization: Bearer token)或握手阶段传递 Token。

5. 性能优化

  • 消息压缩:服务器启用 WebSocket 扩展(如 permessage-deflate)。
  • 连接复用:避免频繁创建/销毁连接。
  • 数据量控制:避免单次发送过大消息(分片传输)。

6. 常见问题与解决方案

问题 解决方案
连接不稳定 实现自动重连逻辑,监听网络状态变化(如 Network.framework)。
内存泄漏 使用 [weak self] 避免循环引用,及时释放不再使用的 WebSocket 实例。
后台连接断开 启用后台模式(需配置 Capabilities > Background Modes > Remote notifications)。
消息顺序混乱 在消息中添加序列号,客户端按序处理。

7. 适用场景示例

  • 实时聊天应用:消息即时推送、已读回执。
  • 金融行情推送:实时股票价格、K线数据。
  • 多人在线游戏:玩家位置同步、状态更新。
  • 物联网控制:设备状态监控、指令下发。

总结

  • 核心价值:WebSocket 为 iOS 应用提供高效、实时的双向通信能力。
  • 选择方案:优先使用 URLSessionWebSocketTask(iOS 13+),低版本或需高级功能时选择 Starscream。
  • 关键实践:心跳保活、错误重连、数据序列化、线程安全。

Metal 进阶:读取可绘制对象的像素

作者 一牛
2025年4月7日 12:20

引言

Hi,大家好,我是一牛。同学们已经了解过,如何绘制三角形到Metal 的可绘制对象中。Metal 的可绘制对象其实就是一幅当前帧的纹理。同学们可能会猜想,既然我们能绘制三角形,那我们是否可以读取这幅纹理的像素,或者将该纹理保存为图片? 答案是可以的。今天我将带着大家一起学习下如何读取可绘制对象(Drawable)的像素。

关键配置

metalView.framebufferOnly = false

首先我们需要将这个属性设置成false。这个属性的默认值是true,表示可绘制对象的纹理只能用来渲染,而我们需要读取该纹理,所以我们需要将这个属性设置成false。值得注意的是,这样做可能会影响性能,我们在做性能优化时,需要考虑到这点。

metalView.colorPixelFormat = MTLPixelFormat.rgba8Unorm

为了方便演示,我们将可绘制对象的纹理像素格式设置成rgba8Unorm,这代表像素有四个通道,每个通道是8个比特,并且按照 RGBA 的顺序排列。

创建渲染管线描述符

let pipelineDescriptor = MTLRenderPipelineDescriptor()
pipelineDescriptor.vertexFunction = vertextFunc
pipelineDescriptor.fragmentFunction = fragmentFunc
pipelineDescriptor.colorAttachments[0].pixelFormat = mtkView.colorPixelFormat;

在这里我们需要将颜色附件的像素格式设置成和可绘制对象的纹理像素格式一致。在 Metal 中颜色附件用于渲染管线中的渲染目标。颜色附件通常与帧缓冲区相关联,最终渲染的结果会被写入到颜色附件中,所以二者的像素格式必须一致。

渲染渐变的长方形

func drawQuad(in view: MTKView, with commandBuffer: MTLCommandBuffer?) {
    guard let renderPassDesriptor = view.currentRenderPassDescriptor else {
        return
    }
    let commanderEncoder = commandBuffer?.makeRenderCommandEncoder(descriptor: renderPassDesriptor)
    commanderEncoder?.setRenderPipelineState(pipelineState)
    let vertices = [
        //top-left
        Vertext(position: [-1.0,  1.0, 1.0, 1.0], color: [1.0, 0.0, 1.0, 1.0]),
        //bottom-left
        Vertext(position: [-1.0, -1.0, 1.0, 1.0], color: [1.0, 0.0, 0.0, 1.0]),
        //bottom-right
        Vertext(position: [ 1.0, -1.0, 1.0, 1.0], color: [1.0, 1.0, 0.0, 1.0]),
        //top-left
        Vertext(position: [-1.0,  1.0, 1.0, 1.0], color: [1.0, 0.0, 1.0, 1.0]),
        //bottom-right
        Vertext(position: [ 1.0, -1.0, 1.0, 1.0], color: [1.0, 1.0, 0.0, 1.0]),
        //top-right
        Vertext(position: [ 1.0,  1.0, 1.0, 1.0], color: [0.0, 1.0, 0.0, 1.0]),
    ]
    let vertexBuffer = device.makeBuffer(bytes: vertices, length: MemoryLayout<Vertext>.stride * vertices.count)
    commanderEncoder?.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
    commanderEncoder?.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: vertices.count)
    commanderEncoder?.endEncoding()
}

在这里,我们绘制了两个三角形,并且给三角形的顶点设置了颜色,并且我们在片元着色器中直接返回了三角形顶点的颜色,Metal 会使用插值算法,将三角形内部做像素填充,实现渐变效果。

读取像素值

func readPixels(commandBuffer: MTLCommandBuffer?, from texture: MTLTexture, at region: CGRect) -> MTLBuffer? {
    let sourceOrigin = MTLOrigin(x: Int(region.origin.x), y: Int(region.origin.y), z: 0)
    let sourceSize = MTLSize(width: Int(region.size.width), height: Int(region.size.height), depth: 1)
    let bytesPerPixel = 4
    let bytesPerRow = sourceSize.width * bytesPerPixel
    let bytesPerImage = sourceSize.height * bytesPerRow
    guard let readBuffer = texture.device.makeBuffer(length: bytesPerImage, options: .storageModeShared) else {
        return nil
    }
    let blitEncoder = commandBuffer?.makeBlitCommandEncoder()
    blitEncoder?.copy(from: texture, sourceSlice: 0, sourceLevel: 0, sourceOrigin: sourceOrigin, sourceSize: sourceSize, to: readBuffer, destinationOffset: 0, destinationBytesPerRow: bytesPerRow, destinationBytesPerImage: bytesPerImage)
    blitEncoder?.endEncoding()
    commandBuffer?.commit()
    commandBuffer?.waitUntilCompleted()
    return readBuffer
}

为了读取纹理的像素值,我们需要使用Blit命令编码器,用于将图像数据从一个缓冲区拷贝到另一个缓冲区。

在这里我们将输出的像素缓冲设置成统一内存(GPUCPU都可以访问),这样Blit命令编码器可以将GPU中的图像数据拷贝到该输出缓冲中。

// 设置输出缓存为统一内存
guard let readBuffer = texture.device.makeBuffer(length: bytesPerImage, options: .storageModeShared) else {
return nil
}

之前我们将Metal的可绘制对象的纹理像素格式设置成rgba8Unorm,所以在这里我们需要设置输出缓冲匹配该像素格式。

// 每个像素4个字节
let bytesPerPixel = 4
// 每一行的字节数
let bytesPerRow = sourceSize.width * bytesPerPixel
// 图片的字节数
let bytesPerImage = sourceSize.height * bytesPerRow

提交命令缓冲时,我们还需要在GPU执行命令缓冲区前,阻塞当前线程,我们才能读取正确的像素。

commandBuffer?.waitUntilCompleted()

点击屏幕获取像素

由于纹理左上角坐标是 (0, 0) , 而当前视图左下角的坐标是 (0,0) , 我们需要将点击点转换到纹理坐标

let bottomUpPixelPosition = view.convertToBacking(event.locationInWindow)
let bottomDownPixelPosition = CGPoint(x: bottomUpPixelPosition.x, y: view.frame.size.height - bottomUpPixelPosition.y)

点击时我们需要先编码渲染命令,然后编码读取纹理命令,这两个操作需要在一帧内提交给GPU。由于此时手动渲染了一帧图片,因此在MTKViewDelegate中不需要重复渲染。

let commandBuffer = commandQueue.makeCommandBuffer()
// 渲染纹理
drawQuad(in: theView, with: commandBuffer)
isDrawForReadThisFrame = false
guard let readTexture = theView.currentDrawable?.texture else {
    return nil
}
// We only want to get the pixel of the click point.
let region = CGRect(x: pixelPosition.x, y: pixelPosition.y, width: 1, height: 1)
// 读取纹理
guard let pixelBuffer = readPixels(commandBuffer: commandBuffer, from: readTexture, at: region) else {
    return nil
}

将输出缓存转像素

let pixelPointer = pixelBuffer.contents().bindMemory(to: UInt8.self, capacity: 4)
let r = pixelPointer[0]
let g = pixelPointer[1]
let b = pixelPointer[2]
let a = pixelPointer[3]
return Pixel(r: r, g: g, b: b, a: a)

结语

通过Blit 命令编码器,我们将帧缓冲的纹理拷贝到统一内存中,实现了读取屏幕像素的目的。

本实例已开源

iOS开发,runtime实现切片编程原理以及实战用例

2025年4月7日 09:05

在 iOS 开发中,利用 Objective-C Runtime 实现切片编程(AOP,Aspect-Oriented Programming)的核心原理是 Method Swizzling。通过动态交换方法的实现(IMP),可以在不修改原始代码的情况下插入自定义逻辑。以下是详细原理和代码示例:


一、核心原理

  1. Method Swizzling

    • 通过 class_getInstanceMethod 和 method_exchangeImplementations 交换两个方法的实现。
    • 将原始方法替换为自定义方法,在自定义方法中插入切片逻辑后调用原始实现。
  2. 动态消息转发

    • 使用 class_addMethod 动态添加方法实现,避免因原方法未实现导致的 Crash。
  3. 关联对象(可选)

    • 通过 objc_setAssociatedObject 存储切片逻辑的 Block,实现更灵活的 AOP。

二、完整代码示例

1. 创建 AOP 工具类 AspectUtility

#import <objc/runtime.h>

@implementation AspectUtility

+ (void)hookClass:(Class)targetClass
 originalSelector:(SEL)originalSelector
 swizzledSelector:(SEL)swizzledSelector {
    
    Method originalMethod = class_getInstanceMethod(targetClass, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(targetClass, swizzledSelector);
    
    // 尝试给原方法添加实现(避免原方法未实现)
    BOOL didAddMethod = class_addMethod(targetClass,
                                        originalSelector,
                                        method_getImplementation(swizzledMethod),
                                        method_getTypeEncoding(swizzledMethod));
    
    if (didAddMethod) {
        // 添加成功:替换新方法的实现为原始实现
        class_replaceMethod(targetClass,
                            swizzledSelector,
                            method_getImplementation(originalMethod),
                            method_getTypeEncoding(originalMethod));
    } else {
        // 添加失败:直接交换两个方法的实现
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}

@end

2. 实现具体切片逻辑

以 Hook UIViewController 的 viewDidLoad 方法为例:

// UIViewController+Aspect.h
#import <UIKit/UIKit.h>

@interface UIViewController (Aspect)
@end

// UIViewController+Aspect.m
#import "UIViewController+Aspect.h"
#import "AspectUtility.h"

@implementation UIViewController (Aspect)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [AspectUtility hookClass:[self class]
                 originalSelector:@selector(viewDidLoad)
                 swizzledSelector:@selector(aspect_viewDidLoad)];
    });
}

- (void)aspect_viewDidLoad {
    // 插入前置逻辑
    NSLog(@"Before viewDidLoad: %@", NSStringFromClass([self class]));
    
    // 调用原始实现(实际已交换为 aspect_viewDidLoad)
    [self aspect_viewDidLoad];
    
    // 插入后置逻辑
    NSLog(@"After viewDidLoad: %@", NSStringFromClass([self class]));
}

@end

三、代码解释

  1. +load 方法

    • 类加载时自动调用,确保方法交换在程序启动时完成。
    • 使用 dispatch_once 保证线程安全,避免重复交换。
  2. 动态添加方法

    • 通过 class_addMethod 处理原方法未实现的情况(如父类方法未被子类实现)。
  3. 方法交换流程

    • 调用 aspect_viewDidLoad 时,实际执行的是原始 viewDidLoad 的实现。
    • 在自定义方法中插入日志代码后,通过 [self aspect_viewDidLoad] 调用原始实现。

四、高级用法:Block 动态切片

通过关联对象存储 Block,实现更灵活的切片:

#import <objc/runtime.h>

typedef void (^AspectBlock)(id target);

@implementation AspectUtility

+ (void)hookClass:(Class)targetClass
       selector:(SEL)selector
        preBlock:(AspectBlock)preBlock
       postBlock:(AspectBlock)postBlock {
    
    Method originalMethod = class_getInstanceMethod(targetClass, selector);
    IMP originalIMP = method_getImplementation(originalMethod);
    
    IMP newIMP = imp_implementationWithBlock(^(id self) {
        if (preBlock) preBlock(self);
        ((void (*)(id, SEL))originalIMP)(self, selector);
        if (postBlock) postBlock(self);
    });
    
    method_setImplementation(originalMethod, newIMP);
}

@end

// 调用示例
[AspectUtility hookClass:[UIViewController class]
               selector:@selector(viewDidLoad)
              preBlock:^(id self) {
                  NSLog(@"Before viewDidLoad");
              }
             postBlock:^(id self) {
                 NSLog(@"After viewDidLoad");
              }];

五、注意事项

  1. 避免重复交换

    • 使用 dispatch_once 确保每个方法只交换一次。
  2. 命名冲突

    • 为交换方法添加前缀(如 aspect_),防止与系统方法冲突。
  3. 子类未实现父类方法

    • 优先使用 class_addMethod 确保原方法存在。
  4. 性能影响

    • 避免对高频调用的方法(如 dealloc)进行 Hook。

以下是 6 个深入用例及其技术实现细节,结合底层原理和代码示例,帮助你彻底掌握这一技术。


用例 1:监控所有按钮点击事件(埋点统计)

需求

  • 无侵入式统计所有 UIButton 的点击事件,记录点击的类名和方法名。

实现方案

通过 Hook UIControl 的 sendAction:to:forEvent: 方法,插入埋点逻辑:

// UIControl+AOP.m
#import <objc/runtime.h>

@implementation UIControl (AOP)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self swizzleSendAction];
    });
}

+ (void)swizzleSendAction {
    Class cls = [UIControl class];
    SEL originalSel = @selector(sendAction:to:forEvent:);
    SEL swizzledSel = @selector(aop_sendAction:to:forEvent:);
    
    Method originalMethod = class_getInstanceMethod(cls, originalSel);
    Method swizzledMethod = class_getInstanceMethod(cls, swizzledSel);
    
    method_exchangeImplementations(originalMethod, swizzledMethod);
}

- (void)aop_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
    // 插入埋点逻辑
    NSString *className = NSStringFromClass([target class]);
    NSString *methodName = NSStringFromSelector(action);
    NSLog(@"埋点: %@ - %@", className, methodName);
    
    // 调用原始实现
    [self aop_sendAction:action to:target forEvent:event];
}

@end

核心原理

  • Hook 的是 UIControl 的事件派发核心方法 sendAction:to:forEvent:
  • 所有按钮、开关等继承自 UIControl 的组件点击都会被捕获。

用例 2:全局页面生命周期监控

需求

  • 监控所有 UIViewController 的 viewDidAppear: 和 viewDidDisappear: 方法。
  • 统计页面停留时长。

实现代码

// UIViewController+AOP.m
#import <objc/runtime.h>

@implementation UIViewController (AOP)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self swizzleMethod:@selector(viewDidAppear:)
                 withMethod:@selector(aop_viewDidAppear:)];
        [self swizzleMethod:@selector(viewDidDisappear:)
                 withMethod:@selector(aop_viewDidDisappear:)];
    });
}

+ (void)swizzleMethod:(SEL)originalSel withMethod:(SEL)swizzledSel {
    Method originalMethod = class_getInstanceMethod(self, originalSel);
    Method swizzledMethod = class_getInstanceMethod(self, swizzledSel);
    
    BOOL didAdd = class_addMethod(self,
                                  originalSel,
                                  method_getImplementation(swizzledMethod),
                                  method_getTypeEncoding(swizzledMethod));
    if (didAdd) {
        class_replaceMethod(self,
                            swizzledSel,
                            method_getImplementation(originalMethod),
                            method_getTypeEncoding(originalMethod));
    } else {
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}

- (void)aop_viewDidAppear:(BOOL)animated {
    [self aop_viewDidAppear:animated];
    NSLog(@"进入页面: %@", NSStringFromClass([self class]));
    self.enterTime = [NSDate date]; // 通过关联对象存储时间
}

- (void)aop_viewDidDisappear:(BOOL)animated {
    [self aop_viewDidDisappear:animated];
    NSTimeInterval duration = [[NSDate date] timeIntervalSinceDate:self.enterTime];
    NSLog(@"离开页面: %@, 停留时长: %.2fs", NSStringFromClass([self class]), duration);
}

// 关联对象存储 enterTime
- (void)setEnterTime:(NSDate *)enterTime {
    objc_setAssociatedObject(self, @selector(enterTime), enterTime, OBJC_ASSOCIATION_RETAIN);
}

- (NSDate *)enterTime {
    return objc_getAssociatedObject(self, @selector(enterTime));
}
@end

关键技术点

  1. 关联对象(Associated Object) :用于存储页面进入时间。
  2. 精准时长统计:在 viewDidAppear 记录时间点,在 viewDidDisappear 计算差值。

用例 3:防止数组越界崩溃

需求

  • Hook NSArray 的 objectAtIndex: 方法,在越界时返回 nil 而非崩溃。

实现代码

// NSArray+AOP.m
#import <objc/runtime.h>

@implementation NSArray (AOP)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class cls = NSClassFromString(@"__NSArrayI"); // 不可变数组类簇
        [self swizzleMethod:cls
              originalSelector:@selector(objectAtIndex:)
              swizzledSelector:@selector(aop_objectAtIndex:)];
    });
}

+ (void)swizzleMethod:(Class)class originalSelector:(SEL)originalSel swizzledSelector:(SEL)swizzledSel {
    Method originalMethod = class_getInstanceMethod(class, originalSel);
    Method swizzledMethod = class_getInstanceMethod(class, swizzledSel);
    
    method_exchangeImplementations(originalMethod, swizzledMethod);
}

- (id)aop_objectAtIndex:(NSUInteger)index {
    if (index < self.count) {
        return [self aop_objectAtIndex:index]; // 调用原始实现
    } else {
        NSLog(@"⚠️ 数组越界: index=%lu, count=%lu", (unsigned long)index, (unsigned long)self.count);
        return nil;
    }
}

@end

关键细节

  1. 类簇处理NSArray 实际类为 __NSArrayI,需通过 NSClassFromString 获取。
  2. 防御式编程:在调用原始方法前进行越界判断。

用例 4:动态替换方法实现(Block 高级用法)

需求

  • 通过 Block 动态替换任意类的方法实现,支持前置(before)和后置(after)逻辑。

完整实现

// AspectManager.h
typedef void (^AspectBlock)(id target, NSInvocation *invocation);

@interface AspectManager : NSObject

+ (void)hookInstanceMethod:(Class)targetClass
                 selector:(SEL)selector
               beforeBlock:(AspectBlock)beforeBlock
                afterBlock:(AspectBlock)afterBlock;

@end

// AspectManager.m
#import <objc/runtime.h>
#import <objc/message.h>

@implementation AspectManager

+ (void)hookInstanceMethod:(Class)targetClass
                 selector:(SEL)selector
               beforeBlock:(AspectBlock)beforeBlock
                afterBlock:(AspectBlock)afterBlock {
    
    Method originalMethod = class_getInstanceMethod(targetClass, selector);
    IMP originalIMP = method_getImplementation(originalMethod);
    
    IMP newIMP = imp_implementationWithBlock(^(id self, ...) {
        // 创建 NSInvocation
        NSMethodSignature *signature = [targetClass instanceMethodSignatureForSelector:selector];
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
        [invocation setTarget:self];
        [invocation setSelector:selector];
        
        // 处理可变参数
        va_list args;
        va_start(args, self);
        for (int i = 2; i < signature.numberOfArguments; i++) { // self 和 _cmd 是前两个参数
            void *arg = va_arg(args, void *);
            [invocation setArgument:arg atIndex:i];
        }
        va_end(args);
        
        // 执行前置逻辑
        if (beforeBlock) beforeBlock(self, invocation);
        
        // 调用原始方法
        ((void (*)(id, SEL, ...))originalIMP)(self, selector, args);
        
        // 执行后置逻辑
        if (afterBlock) afterBlock(self, invocation);
    };
    
    method_setImplementation(originalMethod, newIMP);
}

@end

// 调用示例:Hook UIViewController 的 viewWillAppear:
[AspectManager hookInstanceMethod:[UIViewController class]
                         selector:@selector(viewWillAppear:)
                       beforeBlock:^(id target, NSInvocation *invocation) {
                           NSLog(@"Before viewWillAppear: %@", target);
                       }
                        afterBlock:^(id target, NSInvocation *invocation) {
                           NSLog(@"After viewWillAppear: %@", target);
                        }];

核心技术

  1. NSInvocation 封装:处理可变参数和复杂方法签名。
  2. va_list 可变参数解析:兼容不同参数个数的方法。
  3. IMP 与 Block 转换:通过 imp_implementationWithBlock 动态创建方法实现。

用例 5:检测 Dealloc 是否执行(内存泄漏监控)

需求

  • 监控指定对象的 dealloc 是否正常执行,用于排查内存泄漏。

实现代码

// NSObject+DeallocMonitor.m
#import <objc/runtime.h>

@implementation NSObject (DeallocMonitor)

- (void)monitorDealloc {
    @synchronized (self) {
        static const char kDeallocMonitorKey;
        if (objc_getAssociatedObject(self, &kDeallocMonitorKey)) return;
        
        // 创建虚拟对象,在其 dealloc 时触发回调
        __weak typeof(self) weakSelf = self;
        id monitor = [[DeallocMonitor alloc] initWithBlock:^{
            NSLog(@"✅ %@ 正常释放", NSStringFromClass([weakSelf class]));
        }];
        
        objc_setAssociatedObject(self, &kDeallocMonitorKey, monitor, OBJC_ASSOCIATION_RETAIN);
    }
}

@end

// 辅助类 DeallocMonitor
@interface DeallocMonitor : NSObject
@property (nonatomic, copy) void (^deallocBlock)(void);
@end

@implementation DeallocMonitor

- (instancetype)initWithBlock:(void (^)(void))block {
    if (self = [super init]) {
        _deallocBlock = [block copy];
    }
    return self;
}

- (void)dealloc {
    if (_deallocBlock) _deallocBlock();
}

@end

// 使用示例
UIViewController *vc = [[UIViewController alloc] init];
[vc monitorDealloc];

关键机制

  1. 关联对象生命周期绑定monitor 对象与目标对象生命周期同步。
  2. DeallocMonitor 辅助类:在其 dealloc 中触发回调,间接监控目标对象释放。

用例 6:方法替换的撤销(动态恢复原始方法)

需求

  • 在某些条件下(如测试环境)撤销 Method Swizzling,恢复原始方法。

实现代码

// AspectUtility+Rollback.m
@implementation AspectUtility (Rollback)

+ (void)rollbackHookForClass:(Class)targetClass
           originalSelector:(SEL)originalSelector
           swizzledSelector:(SEL)swizzledSelector {
    
    Method originalMethod = class_getInstanceMethod(targetClass, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(targetClass, swizzledSelector);
    
    if (!originalMethod || !swizzledMethod) return;
    
    // 检查当前 IMP 是否已被交换
    IMP originalIMP = method_getImplementation(originalMethod);
    IMP swizzledIMP = method_getImplementation(swizzledMethod);
    
    if (originalIMP == swizzledIMP) {
        // 恢复原始 IMP
        method_setImplementation(originalMethod, originalIMP);
    }
}

@end

注意事项

  • 需要记录原始 IMP 的指针,或通过其他方式确保能准确恢复。
  • 在多线程环境下需加锁保证原子性。

深入原理:动态方法解析与消息转发

  1. Method Swizzling 的本质

    • 修改类的 method_list,交换两个 Method 结构体的 IMP 指针。
    • 通过 objc_msgSend 的消息查找机制,所有方法调用都会走到新的 IMP。
  2. 为什么要在 +load 中执行?

    • +load 方法在类被加载到 Runtime 时调用,早于 main 函数执行。
    • 确保方法交换在程序启动时完成,避免多线程竞争。
  3. 类簇(Class Clusters)的特殊处理

    • 如 NSArrayNSString 等类属于类簇,实际类名是 __NSArrayI__NSCFString 等。
    • 需通过 NSClassFromString 或逆向工程获取真实类名。

总结:Runtime AOP 的最佳实践

场景 技术方案 注意事项
事件埋点 Hook UIControl 事件方法 注意类簇的真实类名
生命周期监控 交换 UIViewController 方法 使用关联对象存储额外数据
容器防崩溃 替换 NSArray/NSDictionary 方法 严格处理类簇
动态 Block 替换 使用 imp_implementationWithBlock 处理可变参数和复杂方法签名
内存泄漏检测 关联对象 + 辅助监控类 避免循环引用
方法替换撤销 记录原始 IMP 并恢复 多线程环境下需加锁

黄金法则

  • 始终在 +load 中使用 dispatch_once
  • 优先使用 class_addMethod 避免覆盖父类实现。
  • 为交换方法添加前缀(如 aop_)防止命名冲突。
  • 避免 Hook 高频方法(如 dealloc),可能引发性能问题。

通过灵活组合这些技术,可以实现无侵入式的日志、监控、安全校验等全局功能,极大提升代码可维护性。

SwiftUI-MLX本地大模型开发(二)

作者 YungFan
2025年4月7日 08:56

介绍

SwiftUI-MLX本地大模型开发一文中,我们已经详细讲了如何利用 MLX 进行本地大模型的开发。但是通过案例可以发现 2 个问题:

  1. MLX 内置的大模型数量有限。
  2. 每次大模型都需要从 HuggingFace 下载。

如何解决这 2 个问题,方案是:定制大模型与使用离线大模型。

定制大模型

// MARK: - 注册自定义模型,模型必须为MLX格式
extension MLXLLM.ModelRegistry {
    public static let llama3_2_3B_4bit = ModelConfiguration(
        id: "mlx-community/Llama-3.2-3B-Instruct-4bit", // Hugging Face上模型的仓库路径
        overrideTokenizer: "PreTrainedTokenizer" // 分词器
    )
}

使用离线大模型

  • 每次都从 HuggingFace 在线下载模型非常麻烦,如果已经离线下载了模型到本地,可以通过指定路径的方式加载模型。
  • 可以在 Model Scope 模型搜索地址 中搜索并下载需要的 MLX 大模型。
extension MLXLLM.ModelRegistry {
    public static let localModel = ModelConfiguration(
        directory: URL(fileURLWithPath: "/Users/yangfan/Documents/modelscope/Llama-3.2-3B-Instruct"),  // 本地模型的路径
        overrideTokenizer: "PreTrainedTokenizer"
    )
}

使用

struct ContentView: View {
    // 提示词
    @State private var prompt: String = "什么是SwiftUI?"
    // 输出结果
    @State private var response: String = ""
    @State private var isLoading: Bool = false

    var body: some View {
        VStack(spacing: 16) {
            // 顶部输入区域
            HStack {
                TextField("输入提示词...", text: $prompt)
                    .textFieldStyle(.roundedBorder)
                    .font(.system(size: 16))

                Button {
                    response = ""

                    Task {
                        do {
                            try await generate()
                        } catch {
                            debugPrint(error)
                        }
                    }
                } label: {
                    Text("生成")
                        .foregroundStyle(.white)
                        .padding(.horizontal, 16)
                        .padding(.vertical, 8)
                        .background(prompt.isEmpty ? Color.gray : Color.blue)
                        .cornerRadius(8)
                }
                .buttonStyle(.borderless)
                .disabled(prompt.isEmpty || isLoading)
            }
            .padding(.horizontal)
            .padding(.top)

            // 分隔线
            Rectangle()
                .fill(Color.gray.opacity(0.2))
                .frame(height: 1)

            // 响应展示区域
            if response != "" {
                ResponseBubble(text: response)
            }

            Spacer()
        }

        if isLoading {
            ProgressView()
                .progressViewStyle(.circular)
                .padding()
        }
    }
}

extension ContentView {
    // MARK: 文本生成
    func generate() async throws {
        isLoading = true
        // 加载模型
        //  let modelConfiguration = ModelRegistry.llama3_2_3B_4bit
        let modelConfiguration = ModelRegistry.localModel
        let modelContainer = try await LLMModelFactory.shared.loadContainer(configuration: modelConfiguration) { progress in
            print("正在下载 \(modelConfiguration.name),当前进度 \(Int(progress.fractionCompleted * 100))%")
        }
        // 生成结果
        let _ = try await modelContainer.perform { [prompt] context in
            let input = try await context.processor.prepare(input: .init(prompt: prompt))
            let result = try MLXLMCommon.generate(input: input, parameters: .init(), context: context) { tokens in
                let text = context.tokenizer.decode(tokens: tokens)
                Task { @MainActor in
                    self.response = text
                    self.isLoading = false
                }
                return .more
            }
            return result
        }
    }
}

struct ResponseBubble: View {
    let text: String

    var body: some View {
        ScrollView {
            VStack(alignment: .leading, spacing: 8) {
                Text("AI")
                    .font(.system(size: 16))
                    .foregroundColor(.gray)

                Text(text)
                    .font(.system(size: 16))
                    .lineSpacing(4)
                    .padding()
                    .background(Color.blue.opacity(0.1))
                    .cornerRadius(12)
            }
        }
        .padding(.horizontal)
    }
}

效果

  • 定制大模型。

定制大模型.gif

  • 使用离线大模型。

使用离线大模型.gif

Swift项目实战

作者 jz_study
2025年4月6日 21:35

CocoaPods

  • CocoaPods是非常好用的第三方依赖管理工具。它于2011年发布,经过几年的发展,已经非常完善。CocoaPods支持项目中采用Objective-C或swift语言。CocoaPods会将第三方库的源代码编译为静态库.a文件或者动态框架.framework文件的形式,并将它们添加到项目中,建立依赖关系

Carthage

  • Carthage是一个轻量级的项目依赖管理工具。Carthage主张"去中心化"和"非侵入性"。CocoaPods搭建了一个中心库,第三方库被收入到中心库,所以没有收录的第三方库是不能使用CocoaPods管理的,这就是所谓的"去中心化"思想。而Carthage没有这样的中心库,第三方基本上都是从Github或者私有git库中下载的,这就是"去中心化"。另外,CocoaPods下载第三方库后,会将其编译成静态链接库或者动态框架文件,这种做法会修改Xcode项目属性配置依赖关系,这就是所谓的"侵入性"。而Carthage下载成功后,会将第三方库编译为动态框架,由开发人员自己配置依赖关系,Carthage不会修改Xcode项目属性,这就是所谓的"非侵入性"。
  • 使用brew安装carthage

image.png

  • 创建cartfile文件(类似于podfile)
$ touch cartfile

image.png

cartfile - Dependency origin

  • Carthage支持两种类型的源,一个是github,另一个是git
  1. github表示依赖源,告诉Carthage去哪里下载文件。依赖源之后跟上要下载的库,格式为Username/ProjectName
  2. Git关键字后面跟的是资料库的地址,可以是远程的URL地址,使用git:// ,http:// ,ssh:// ,或者是本地资料库地址

cartfile - Dependency version

  • 告诉Carthage使用哪个版本,这是可选的,不写默认使用最新版本
  1. == 1.0表示使用1.0版本
  2. =1.0表示使用1.0或更高的版本

  3. ~>1.0表示使用版本1.0以上但是低于2.0的最新版本,如1.2,1.6
  4. branch名称/tag名称/commit名称,意思是使用特定的分支/标签/提交,比如可以是分支名master,也可以是提交5c8a74a
  • carthage update
$carthage update

image.png

  • Cartfile.resolved文件和Carthage目录

image.png

Cartfile.resolved文件
  • 这个文件是生成后的依赖关系以及各个库的版本号,不能修改
  • Cartfile.resolved文件确保提交的项目可以使用完全相同的配置与方式运行启用。跟踪项目当前所用的依赖版本号,保持多端开发一致,出于这个原因,建议提交这个文件到版本控制中

image.png

Carthage目录
  • Checkouts保存从git拉取的依赖库源文件
  • Build包含编译后的文件,包含Mac与iOS对应的.framework

image.png

项目配置
  • 项目Target->Build Setting->Search Paths->Framework Search Paths添加$(PROJECT_DIR)/Carthage/Build/iOS

image.png

  • 项目Target->Build Phase->‘+’->New Run Script Phase
  1. 添加脚本/usr/local/bin/Carthage copy-frameworks
  2. 添加"Input Files" $(SRCROOT)/Carthage/Build/iOS/Alamofire.framework

image.png

Swift Package Manager

  • Swift Package Manager是苹果推出的用于管理分发swift代码的工具,可以用于创建使用swift库和可执行程序
  • 能够通过命令快速创建library或者可执行的swift程序,能够跨平台使用,使开发出来的项目能够在不同平台上运行
  • Xcode集成

image.png

image.png

image.png

image.png

image.png

项目整体结构

  • 项目目录

image.png

image.png

主要页面

image.png

image.png

Model

image.png

R.swift在编译期起作用

image.png

image.png

UIColor Extension 和 便捷初始化

image.png

登录注册

  • 面向协议编程

image.png

BaseViewController

image.png

Protocol + Extension

image.png

show Toast

image.png

Color生成Image

image.png

首页 + UITabbarController

image.png

image.png

轮播图

image.png

swift的协议允许类类型遵循,加 :AnyObject

image.png

Swift轮播图

image.png

无限轮播

image.png

image.png

自动滚动

image.png

  • 手动拖动,定时器停止
Swift-UITableViewCell

image.png

image.png

详情页

image.png

简单富文本

image.png

个人中心页

image.png

商城订单列表页

image.png

  • swift泛型抽象list中的cell以及item

关于学习

  • 保持好学的心态,保持学习
  • 只有改变是不变的

关于坚持

  • 沉下心来研究

继续学习Swift

大前端

  • 简单来说,大前端就是所有前端的统称,比如Android、iOS、web、Watch等,最接近用户那一层就是UI层,然后将其统一起来,就是大前端。大前端最大的特点在于一次开发,同时适用于所有平台,开发者不用为一个APP需要做Android和iOS两种模式而担心。大前端是web统一的时代,利用web不仅能开发出网站,更可以开发出手机端web应用和移动端应用程序。

跨平台

  • H5+原生(Cordaova、Ionic、微信小程序)
  • JaveScript开发+原生渲染(React Native、Weex、快应用)
  • 自绘UI+原生(Flutter)
  • 增强版Web App(PWA)

可选链

可选值的缺点

  • 使用可选值有时会让人感到有点笨拙,所有的解包和检查会变得如此繁重,以至于会让你想要丢几个感叹号上去强制解包,好让你能继续工作下去。但是请小心:如果你强制解包一个没有值的可选值,你的代码就崩了。为了解决这个缺点,Swift引入两个特性,一是合并空值运算符,二是可选链

可选链

  • 可选链是一个调用和查询可选属性、方法和下标的过程,它可能为nil。如果可选项包含值,属性、方法或者下标的调用成功;如果可选项是nil,属性、方法或者下标的调用会返回nil。多个查询可以链接在一起,如果链中任何一个节点是nil,那么整个链就会得体地失败。
可选链代替强制展开
  • 你可以通过在你希望如果可选项为非nil就调用属性、方法或者脚本的可选值后边使用问号(?)来明确可选链。这和在可选值后放叹号(!)来强制展开它的值非常类似。主要的区别在于可选链会在可选项为nil时得体地失败,而强制展开则在可选项为nil时触发运行时错误
  • 为了显示可选链可以在nil值上调用,可选链调用的结果一定是一个可选值,就算你查询的属性、方法或者下标返回的是非可选值。你可以使用这个可选项返回值来检查可选链调用是成功(返回的可选项包含值),还是由于链中出现了nil而导致没有成功(返回的可选值是nil)
  • 另外,可选链调用的结果与期望的返回值类型相同,只是包装成了可选项。通常返回Int的属性通过可选链后会返回一个Int?.

image.png

image.png

为可选链定义模型类

image.png

通过可选链访问属性

image.png

通过可选链访问属性

image.png

通过可选链嗲用方法
  • 函数和方法没有返回类型就隐式地指明为Void类型。意思是说他们返回一个()的值或者是一个空的元组。

image.png

通过可选链调用方法
  • 如果你尝试通过可选链来设置属性也是一样的。上边通过可选链访问属性中的例子尝试设置address值给john.residence,就算是residence属性是nil也行。任何通过可选链设置属性的尝试都会返回一个Void?类型值,它允许你与nil比较来检查属性是否设置成功;

image.png

通过可选链访问下标
  • 通过可选链访问下标你可以使用可选链来给可选项下标取回或设置值,并且检查下标的调用是否成功。

image.png

链的多层连接
  • 你可以通过连接多个可选链来在模型中深入访问属性、方法以及下标。总之,多层可选链不会给返回的值添加多层的可选性

也就是说:

  • 如果你访问的值不是可选项,它会因为可选链而变成可选项
  • 如果你访问的值已经是可选的,它不会因为可选链而变得更加可选

因此:

  • 如果你尝试通过可选链取回一个Int值,就一定会返回Int?,不论通过了多少层的可选链
  • 类似地,如果你尝试通过可选链访问Int?值,Int?一定就是返回的类型,无论通过了多少层的可选链。
用可选返回值链接方法
  • 可以通过可选链来调用返回可选类型的方法,并且如果需要的话可以继续对方法的返回值进行链接

image.png

KVC

  • 从Swift4开始,类和struct都支持KVC
  • 继承自NSObject的类,标记为@objc的属性可以使用setValue(_:forKey)
  • 非继承自NSObject的类和结构体,使用索引+参数值

KVC-索引+参数名

image.png

image.png

KVO

  • 只有NSObject才能支持KVO
  • 要观察的属性必须使用@objc dynamic修饰

image.png

面试看什么

  • 你现在的能力
  • 你将来的潜力
  1. Array & Set
  • 区别
  • 怎么实现一个Array->存储->添加删除怎么做->怎么优化->Ring buffer
  • 怎么实现一个Set->Hash存储->Hash算法->Hash冲突解决
  • Array算法->排序/查找/乱序/LCS/.....
  • Set算法->子集/位运算/OrderedSet

react组件间的通信有哪些?

2025年4月6日 06:25

React 组件间的通信方式

在 React 中,组件间的通信是构建应用的核心部分。不同组件之间需要共享数据和状态,常见的通信方式包括以下几种:

1. 父子组件通信

父组件可以通过 props 向子组件传递数据。这是 React 中最基本的通信方式。子组件通过 props 接收父组件传递的数据。

// 父组件
function Parent() {
  const message = "Hello from Parent!";
  return <Child message={message} />;
}

// 子组件
function Child({ message }) {
  return <div>{message}</div>;
}

2. 子向父组件通信

子组件可以通过调用父组件传递的函数来向父组件传递数据。父组件将一个函数作为 props 传递给子组件,子组件在适当的时候调用这个函数。

// 父组件
function Parent() {
  const handleChildMessage = (message) => {
    console.log(message); // 输出子组件传来的信息
  };

  return <Child onMessage={handleChildMessage} />;
}

// 子组件
function Child({ onMessage }) {
  return (
    <button onClick={() => onMessage("Hello from Child!")}>
      Send Message to Parent
    </button>
  );
}

3. 兄弟组件通信

兄弟组件通常通过父组件进行通信。父组件可以将共享的状态和方法传递给兄弟组件。

// 父组件
function Parent() {
  const [message, setMessage] = useState("");

  return (
    <>
      <ChildA setMessage={setMessage} />
      <ChildB message={message} />
    </>
  );
}

// 兄弟组件 A
function ChildA({ setMessage }) {
  return (
    <button onClick={() => setMessage("Hello from Child A!")}>
      Send Message
    </button>
  );
}

// 兄弟组件 B
function ChildB({ message }) {
  return <div>{message}</div>;
}

4. 使用 Context API

Context API 允许您在组件树中共享数据,而不必通过每一层组件的 props。适用于深层嵌套组件之间的通信。

const MessageContext = createContext();

function Parent() {
  const [message, setMessage] = useState("Hello!");

  return (
    <MessageContext.Provider value={{ message, setMessage }}>
      <ChildA />
      <ChildB />
    </MessageContext.Provider>
  );
}

function ChildA() {
  const { setMessage } = useContext(MessageContext);
  return <button onClick={() => setMessage("Updated Message!")}>Update</button>;
}

function ChildB() {
  const { message } = useContext(MessageContext);
  return <div>{message}</div>;
}

5. 使用 Redux

对于大型应用,使用 Redux 可以更有效地管理组件间的状态。在 Redux 中,组件通过 connect 函数连接到 Redux store,从而获取共享的状态和分发 actions。

import { createStore } from 'redux';
import { Provider, useDispatch, useSelector } from 'react-redux';

// Redux reducer
const initialState = { message: "Hello from Redux!" };
function reducer(state = initialState, action) {
  switch (action.type) {
    case 'UPDATE_MESSAGE':
      return { ...state, message: action.payload };
    default:
      return state;
  }
}

// 创建 Redux store
const store = createStore(reducer);

// 组件 A
function ComponentA() {
  const dispatch = useDispatch();
  return (
    <button onClick={() => dispatch({ type: 'UPDATE_MESSAGE', payload: 'New Message!' })}>
      Update Message
    </button>
  );
}

// 组件 B
function ComponentB() {
  const message = useSelector(state => state.message);
  return <div>{message}</div>;
}

// 应用组件
function App() {
  return (
    <Provider store={store}>
      <ComponentA />
      <ComponentB />
    </Provider>
  );
}

6. 使用 EventEmitter

对于一些不想使用 Redux 或 Context 的场景,可以使用 EventEmitter 来实现组件间的通信。可以通过自定义事件来实现组件间的解耦通信。

import { EventEmitter } from 'events';

const eventEmitter = new EventEmitter();

// 组件 A
function ComponentA() {
  const sendMessage = () => {
    eventEmitter.emit('message', 'Hello from Component A!');
  };

  return <button onClick={sendMessage}>Send Message</button>;
}

// 组件 B
function ComponentB() {
  const [message, setMessage] = useState("");

  useEffect(() => {
    const handleMessage = (msg) => setMessage(msg);
    eventEmitter.on('message', handleMessage);

    return () => {
      eventEmitter.off('message', handleMessage);
    };
  }, []);

  return <div>{message}</div>;
}

总结

在 React 中,组件间的通信方式多种多样。选择合适的通信方法取决于应用的规模和复杂性。对于简单的父子组件通信,可以使用 props;对于更复杂的状态管理,可以考虑使用 Context API 或 Redux。了解这些通信方式将帮助您更有效地构建可维护的 React 应用。

Browser Use 原理解析-为一个小项目能融1700万美元

作者 bang
2025年4月7日 20:29

Browser Use 成为近期的明星项目,两个人的纯技术开源项目,核心代码 8000 行,融资 1700 万美元,让人好奇它具体做了什么,为什么这么值钱。

做了什么?

简单说 Browser Use 让大语言模型对网页的识别和操作的效率、准确度变高了,有利于 Agent 完成任务。

目前要让 AI Agent 完成任务,可以直接让 AI 浏览网页,像人一样去理解页面,执行操作,之前一般的做法主要靠截屏:

  1. 其他产品(Anthropic 的 Computer use、OpenAI 的 Operator 等)操作 GUI,主要靠 VLM 识别截屏,再输出要操作的坐标位置,Agent 执行操作。
  2. 在这过程中,web 的源码也可以加入上下文,让模型获得更多信息,但 web 源码内容太多,信息噪音太大,token 消耗也高。

而 Browser User 对 web 页面做了结构化处理,翻译成大模型友好的格式,再输入 LLM 识别。举例 Google 首页:

1.Browser use 会在页面上嵌入脚本,遍历 DOM 结构,找出页面上的元素,显式打上标记:

2. 转换为以下纯文本:

[Start of page]
[1]<a Gmail >Gmail/>
[2]<a 搜索图片 >图片/>
[3]<div />
[4]<a false;button;Google 应用/>
[5]<a 登录/>
[6]<img />
[7]<div />
[8]<textarea 搜索;false;q;combobox;Google 搜索/>
[9]<div />
[10]<div 按图搜索;button/>
[11]<input button;Google 搜索;btnK;submit/>
[12]<input btnI; 手气不错 ;submit/>
[13]<a English/>
[14]<a Bahasa Melayu/>
[15]<a தமிழ்/>
[16]<a 关于 Google/>
[17]<a 广告/>
[18]<a 商务/>
[19]<a Google 搜索的运作方式/>
[20]<a 隐私权/>
[21]<a 条款/>
[22]<div false;button/>
[23]<div 设置/>
[End of page]

内容格式极简,关键信息都有,提取了所有可交互元素,模型完全可以通过这些信息“看”和“操作”网页。

例如要执行搜索,模型很容易判断搜索框是索引为[8]的元素,Agent只需要把元素[8]对应的 XPath 拿出来,获取到页面上对应的元素,执行操作就可以。

所以 Browser Use 使用非多模态的模型例如 Deepseek 也可以跑起来,不依赖截图识别。但如果是多模态模型,截图也默认会一起输入模型,提升识别准确率。

Browser Use 核心就是做了这个点,剩下的就是怎样把流程串起来。

实现细节

核心代码包括四个部分:agent 负责决策和串流程,controller 负责转换决策为具体操作,dom 负责网页分析,browser 负责与实际浏览器交互。

  1. agent实现了个小型 AI Agent,负责串起流程,管理上下文信息,决策生成下一步指令,让 Browser Use 可以一步步完整执行一个任务(例如购买机票),这也让 Browser Use 变成易于集成为 Agent
    1. service.py 实现了典型的 Agent 的 ReAct 模式,推理 → 执行步骤 → 模型观察结果下一步。可单独配置 plan 模型。
    2. message_manager 管理消息历史,并做了一些类似敏感数据过滤、图片内容处理等。
    3. memory 实现记忆功能,基于mem0,但目前应该只实现了一半,只把每步存起来,没有调取使用。
  2. controller负责控制和执行浏览器操作的高级指令,是连接AI代理和浏览器操作的桥梁。
    1. registry/ 实现了 Action 的注册和管理能力
    2. service.py 定义和注册了所有可用的浏览器 Action,click/go_to_url/input_text等。
  3. dom对 web 页面的处理和分析,生成上述 AI 友好的文本结构。
    1. buildDomTree.js 是嵌入页面的 JS 脚本,遍历 dom 过滤出可交互元素,绘制高亮框等。
    2. service.py 操作 JS 注入、节点信息获取、跨域处理等能力,views.py 提供 DOM 节点在 python 的数据模型。
  4. browser对接 Playwright,在它上面封装了一些能力。
    1. context.py 管理浏览器上下文,以及一些细节功能处理,像标签页/导航管理、截图、定位和获取元素、URL白名单检测、文件下载处理等。
    2. browser.py 封装了浏览器实例的创建和配置。

它也用到了很多开源项目和服务:

  1. Playwright:微软开发的 web 自动化测试框架,核心是提供了用代码命令操作浏览器的能力,这能力刚好是 AI Agent 需要的,Browser Use 只需要基于它做上层开发。如果只需要浏览器的能力,官方也有封装的 MCP 服务(github.com/microsoft/playwright-mcp)
  2. LangChain:Agent 基于 LangChain 构造,主要用到模型调用和 message 管理。
  3. Laminar:trace / 评估 AI 产品的服务,Laminar 对 LangChain / OpenAISDK 等框架做好了适配,加一行代码就可以对 Browser Use 整个 Session 调用链路调用过程进行追踪和评估。Laminar 跟 Browser User 一样也是 YC 初创公司,开源→服务的打法。跟另一个项目 openllmetry 类似,都是基于 OpenTelemetry 做 AI 的监控分析工具,这个赛道也很卷。
  4. posthog:数据采集,让 Browser Use 的作者能更好知道项目被使用的情况,会收集一些使用数据上报到 posthog,Agent的执行过程都会上报,对数据敏感的可以关了。
  5. mem0:专为 LLM 提供的记忆层服务,分级存储用户信息、RAG 召回、易用的 API。也是开源+服务的模式。
  6. 浏览器服务:Browser Use 支持连接远程的 Browser 服务去执行任务(这也是 Playwright 支持的),官方文档里推荐的就有 browserbase.comanchorbrowser.comsteel.devbrowserless.io 这几个服务。

其他就是一些配套实现了,gif 动图、多种模型调用的 example、test case 等。

为什么这么值钱

一个并不复杂的开源项目,得到市场这么大的认可,事后分析,可能是因为:

  1. 是 Agent 的核心基础设施
    1. Agent 跟现实世界交互,最优方案是通过 API,而不是 GUI 界面,所以基于 MCP 统一协议封装 API 是当下一大热门。
    2. 但绝大多数服务没有 API,只有给人类提供的 GUI,现阶段要让 Agent 用处更广泛,还是得让它能理解、使用 GUI,而 Browser 是 GUI 的主要容器,在现阶段就是最核心的基础设施之一
  2. 有很高的上限
    1. Browser 足够复杂,需要持续迭代,优化识别率、上下文管理、新的评测机制、探索模型上限等,深耕能形成壁垒。
    2. Browser 一定有很强的云服务诉求,要各种上层 Agent 自己部署容器和 Browser 成本太高,商业化路径清晰
  3. 在这个领域做到了 SOTA
    1. 据 Browser Use 自己的评测,在 WebVoyager Benchmark 上获得业界最好的效果:
    2. 从近期声量、github 的活跃上看,稳居头部。

有需求,有商业化,有流量,在这个时间点让它很值钱。

想法

  1. 长期看,模型直接理解截屏是更自然更能 scale up 的做法,所有信息截屏都有,大模型应该像人一样能准确识别和操作,模型公司应该会一直在这条路上尝试。
  2. Browser Use 是在模型能力不足时期的中间优化方案,如果这个时期足够长,它就价值很大,如果模型很快突破,它就会失去价值。
  3. 可以用同样的思路复刻 Mobile Use,iOS / Android 都有现成的 accessibility 能力,能拿到当前界面结构化的数据,只是会有沙盒的各种限制,这事很适合系统厂商去做。桌面端应该也可以。
  4. Agent 上下游相关配套基建都处于起步阶段,小团队很有机会把其中某个点做出彩。

切勿将辅助驾驶宣传成智能驾驶 - 肘子的 Swift 周报 #78

作者 Fatbobman
2025年4月7日 22:00

不久前,某个造成三人死亡的交通事故因为涉及某新锐电动汽车品牌再度引发了人们对“智能驾驶”功能的质疑。在目前披露的有限资料中,至少可以确认的是,“智能驾驶”系统未能在相当长的一段行驶距离中判断出当前的路段正在施工(沿途有施工警示标志),只在撞击前2-3秒前给予了警示。这意味着,在系统报警后,驾驶者只有极短的反应时间。

LLVM integrated assembler: Improving MCExpr and MCValue

作者 MaskRay
2025年4月6日 15:00

In my previous post, RelocationGeneration in Assemblers, I explored some key concepts behindLLVM’s integrated assemblers. This post dives into recent improvementsI’ve made to refine that system.

The LLVM integrated assembler handles fixups and relocatableexpressions as distinct entities. Relocatable expressions, inparticular, are encoded using the MCValue class, whichoriginally looked like this:

1
2
3
4
5
class MCValue {
const MCSymbolRefExpr *SymA = nullptr, *SymB = nullptr;
int64_t Cst = 0;
uint32_t RefKind = 0;
};

In this structure:

  • RefKind acts as an optional relocation specifier,though only a handful of targets actually use it.
  • SymA represents an optional symbol reference (theaddend).
  • SymB represents another optional symbol reference (thesubtrahend).
  • Cst holds a constant value.

While functional, this design had its flaws. For one, the wayrelocation specifiers were encoded varied across architectures:

  • Targets like COFF, Mach-O, and ELF's PowerPC, SystemZ, and X86 embedthe relocation specifier within MCSymbolRefExpr *SymA aspart of SubclassData.
  • Conversely, ELF targets such as AArch64, MIPS, and RISC-V store itas a target-specific subclass of MCTargetExpr, and convertit to MCValue::RefKind duringMCValue::evaluateAsRelocatable.

Another issue was with SymB. Despite being typed asconst MCSymbolRefExpr *, itsMCSymbolRefExpr::VariantKind field went unused. This isbecause expressions like add - sub@got are notrelocatable.

Over the weekend, I tackled these inconsistencies and reworked therepresentation into something cleaner:

1
2
3
4
5
6
class MCValue {
const MCSymbol *SymA = nullptr, *SymB = nullptr;
int64_t Cst = 0;
uint32_t Specifier = 0;
};

This updated design not only aligns more closely with the concept ofrelocatable expressions but also shaves off some compiler time in LLVM.The ambiguous RefKind has been renamed toSpecifier for clarity. Additionally, targets thatpreviously encoded the relocation specifier withinMCSymbolRefExpr (rather than usingMCTargetExpr) can now access it directly viaMCValue::Specifier.

To support this change, I made a few adjustments:

  • IntroducedgetAddSym and getSubSym methods, returningconst MCSymbol *, as replacements for getSymAand getSymB.
  • Eliminated dependencies on the old accessors,MCValue::getSymA and MCValue::getSymB.
  • Reworkedthe expression folding code that handles + and -
  • Storedthe const MCSymbolRefExpr *SymA specifier atMCValue::Specifier
  • Some targets relied on PC-relative fixups with explicit specifiersforcing relocations. I have definedMCAsmBackend::shouldForceRelocation for SystemZ and cleanedup ARM and PowerPC
  • Changedthe type of SymA and SymB toconst MCSymbol *
  • Replacedthe temporary getSymSpecifier withgetSpecifier
  • Replacedthe legacy getAccessVariant withgetSpecifier

Streamlining Mach-O support

Mach-O assembler support in LLVM has accumulated significanttechnical debt, impacting both target-specific and generic code. Oneparticularly nagging issue was theconst SectionAddrMap *Addrs parameter inMCExpr::evaluateAs* functions. This parameter existed tohandle cross-section label differences, primarily for generating(compact) unwind information in Mach-O. A typical example of this can beseen in assembly like:

1
2
3
4
5
6
        .section        __TEXT,__text,regular,pure_instructions
Leh_func_begin0:
.section __TEXT,__eh_frame,coalesced,no_toc+strip_static_syms+live_support
Ltmp3:
Ltmp4 = Leh_func_begin0-Ltmp3
.long Ltmp4

The SectionAddrMap *Addrs parameter always felt like aclunky workaround to me. It wasn’t until I dug into the Mach-OAArch64 object writer that I realized this hack wasn't necessary forthat writer. This discovery prompted a cleanup effort to remove thedependency on SectionAddrMap for ARM and X86 and eliminatethe parameter:

  • [MC,MachO]Replace SectionAddrMap workaround with cleaner variablehandling
  • MCExpr:Remove unused SectionAddrMap workaround

While I was at it, I also tidied up MCSymbolRefExpr byremovingthe clunky HasSubsectionsViaSymbolsBit, furthersimplifying the codebase.

注意,暂时不要升级 MacOS ,Flutter/RN 等构建 ipa 可能会因 「ITMS-90048」This bundle is invalid 被拒绝

2025年4月7日 06:32

近期,不少使用构建 ipa 提交 App Store 的用户遇到 「ITMS-90048」This bundle is invalid 而拒绝的问题,这个 错误的核心原因是在提交给 App Store Connect 的归档文件 (.xcarchive) 里,包含了一个不允许存在的隐藏文件 ._Symbols

而用户在 ipa 存档里,确实也可以看到 .Symbols 这个隐藏文件的存在,可以看到这个目录是一个空文件夹:

这个问题目前在 Flutter#166367RN#50447 等平台都有相关 issue ,而出现这个的原因,主要在于这些平台都是从脚本构建出一个 ipa 包进行提交,而如果原生平台,一般更习惯在 Xcode 里通过 Prodict > Archive 这种方式来提交,目前这种方式并不会有这个问题

所以如果你遇到这个问题,也可以先实现 fluter build ios ,然后通过 Prodict > Archive 这种方式提交来绕靠问题。

目前这个问题推测来自新的 macOS 15.4 ,因为对于 macOS (尤其是 APFS 文件系统)在处理文件时,会为文件创建以 ._ 开头的隐藏文件,这些文件用于存储 Finder 信息、资源 fork 或其他元数据等。

而在 iOS 构建过程中,需要生成 Symbols 文件目录,用于存储调试符号 (dSYMs) 等信息,所以推测问题可能出在构建或归档过程中,系统对 Symbols 文件进行了某种操作(如 rsync),导致 macOS 生成了对应的 ._Symbols 元数据文件,并且这个隐藏文件被错误地打包进了 .xcarchive 文件。

目前看来,macOS 15.4 确实包括对内置 rsync 的重大修订:

image-20250406133119461

另外,用户在遇到该问题后,也尝试降级到 Xcode 和 Command Line Tools ,但是问题依然存在;也有用户未升级 Xcode ,但升级到 macOS 15.4,也同样触发该问题,所以问题看起来主要是 macOS 15.4 导致

而如果已经是 macOS 15.4 的用户,最简单的做法就是使用 Xcode 的 Prodict > Archive ,或者手动删除该文件:

unzip -q app.ipa -d x
rm -rf app.ipa x/._Symbols
cd x
zip -rq ../app.ipa .
cd ..
rm -rf x

或者 flutter build ipa --release 之后,执行一个 ./cleanup.sh

IPA_PATH="build/ios/ipa/your_app_name.ipa"
# export IPA_PATH="$(find "build/ios/ipa" -name '*.ipa' -type f -print -quit)"

if [ -f "$IPA_PATH" ]; then
  echo "Checking for unwanted files like ._Symbols in $IPA_PATH"
  unzip -l "$IPA_PATH" | grep ._Symbols && zip -d "$IPA_PATH" ._Symbols/ || echo "No ._Symbols found"
else
  echo "IPA not found at $IPA_PATH"
fi

目前看来问题并不在框架端,所以非必要还是暂时不要升级 macOS 15.4 ,避免不必要的问题。

参考资料

昨天以前iOS

老司机 iOS 周报 #330 | 2025-04-07

作者 ChengzhiHuang
2025年4月6日 20:09

ios-weekly
老司机 iOS 周报,只为你呈现有价值的信息。

你也可以为这个项目出一份力,如果发现有价值的信息、文章、工具等可以到 Issues 里提给我们,我们会尽快处理。记得写上推荐的理由哦。有建议和意见也欢迎到 Issues 提出。

新闻

🐕 Swift 6.1 Released

@AidenRao:Swift 6.1 正式推出,核心更新:

  1. 并发优化: nonisolated 支持类型和扩展,任务组子任务结果类型自动推断;
  2. OC 迁移: 新增 @implementation 支持,允许在 Swift 中实现 Objective-C 类型,便于逐步迁移;
  3. 开发体验:尾随逗号支持扩展至参数列表、元组等场景;
  4. 包管理:新增 package traits 机制,适配跨平台条件编译;
  5. 测试增强:支持自定义测试前后逻辑,异常处理更便捷;
  6. 文档工具:Swift-DocC 优化符号链接可读性。

推荐通过 Xcode 16.3 或 swiftly 工具链安装体验。

新手推荐

🐕 Modern URL construction in Swift

@阿权:本文介绍了 Swift 在 URL 构建方面的现代解决方案,通过类型扩展、宏和新 API 的结合,实现了更安全、简洁的 URL 处理方式。开发者可根据项目需求选择合适方案,提升代码质量。具体内容为:

  1. 静态字符串构建 URL 使用 Optional 方式显得冗余,应直接强制解包。
    1. 解法 1:添加 URL 扩展直接创建 URL 实例,内部解包为空时 fatallError,输出信息。
    2. 解法 2:通过自定义宏创建 URL 实例,实现解包并抛出错误的逻辑。
  2. 对于动态构建的 URL,应使用更结构的 URL、URLComponents 拼接、构建方法,甚至能够直接获取本地常见目录的 URL。

文章

🐕 Deploying a Swift Server App to Fly.io and Railway

@Kyle-Ye: 本文介绍了如何使用 Vapor 框架部署 Swift 服务端应用程序到 Fly.io 和 Railway 平台。文章涵盖了初始化 Vapor 项目、编写 Dockerfile、以及在两个平台上部署应用的具体步骤。此外 , 还提到了一些进阶主题 , 如自定义域名和添加数据库服务等。

🐢 AI 产品经理进阶:万字深析大模型的 MCP( &

@EyreFree:这篇文章深度剖析了大模型的 MCP 技术。MCP 是 Anthropic 于 2024 年底开源的开放标准协议,旨在统一 AI 与外部数据源和工具的连接方式,降低集成成本。它采用客户端 - 服务器架构,基于 JSON-RPC 2.0 通信,定义多种原语规范交互。该技术已在智能问答、编程辅助、办公自动化等场景崭露头角。文章还全面分析了 MCP 的优势与局限,如标准统一、开源灵活,但也存在生态不完善、远程支持不足等问题。此外,还展望了其未来在完善远程云支持、构建 “应用商店” 式分发机制、拓展多模态应用等方面的演进方向,为 AI 从业者或对之感兴趣的同学提供了极具价值的参考。

🐕 Fast & Fluid: Integrating Rust egui into SwiftUI

@david-clang:作者在开发实时预览 SwiftData 和 CoreData 数据库的 Mac App DataScout 时,发现 SwiftUI 的 Table 性能相当差,尝试用 AppKit 的 NSTableView 也无法满足需求,最后用 Rust 的 UI 框架 egui 去优化性能。作者把 SwiftUI 中嵌入 egui 渲染视图的 Demo 整理成文章,还用代码示例展示如何在 SwiftUI 的 NavigationSplitView 中嵌入 egui 渲染的视图。以下是 egui 和传统 UI 框架的对比:

  • 传统 UI 框架(如 SwiftUI、UIKit)多采用保留模式(Retained Mode),需显式管理 UI 组件状态(例如按钮状态、列表数据等),框架内部通过对比新旧状态差异来局部更新界面。
  • egui即时模式(Immediate Mode) 则相反:每帧完全丢弃旧 UI 状态,根据当前数据重新生成整个界面,通过高频重建实现“无状态化”。

虽然 Demo 中使用 egui_wgpu_backend 作为渲染后端,但它在 Metal 上渲染单帧需要 10 毫秒,作者在开发 DataScout 时,通过自定义渲染后端,把帧渲染时间缩短到仅 1-2 毫秒,最终才实现高性能需求,可见把 “ SwiftUI 中嵌入 egui 渲染视图” 封装成成熟框架会比较难,但本文优化 SwiftUI 性能的思路值得我们学习。

🐎 得物 iOS 启动优化之 Building Closure

@Smallfly:本文深入解析了 iOS 应用启动优化中常被忽视的 Building Closure 阶段(由 dyld 动态链接器负责),聚焦其耗时问题与优化实践。文章通过真实案例,揭示了某版本因 Building Closure 阶段耗时暴增 200ms 的根因定位过程,并最终通过 解决Perfect Hash 算法的哈希冲突,将关键函数耗时从 1200ms 降至 110ms。

文中详细剖析了 Building Closure 的工作原理(如首次启动生成缓存、Swift/ObjC 协议一致性处理),并提供了 文件结构解析、耗时定位方法(Instrument 工具)及优化方案,适合以下读者参考:

  1. iOS 开发工程师:需优化应用启动速度,尤其是冷启动场景;
  2. 性能调优团队:关注底层 dyld 机制,探索启动耗时优化新方向;
  3. 技术管理者:了解复杂问题排查流程与跨团队协作经验。

内推

重新开始更新「iOS 靠谱内推专题」,整理了最近明确在招人的岗位,供大家参考

具体信息请移步:https://www.yuque.com/iosalliance/article/bhutav 进行查看(如有招聘需求请联系 iTDriverr)

关注我们

我们是「老司机技术周报」,一个持续追求精品 iOS 内容的技术公众号,欢迎关注。

关注有礼,关注【老司机技术周报】,回复「2024」,领取 2024 及往年内参

同时也支持了 RSS 订阅:https://github.com/SwiftOldDriver/iOS-Weekly/releases.atom

说明

🚧 表示需某工具,🌟 表示编辑推荐

预计阅读时间:🐎 很快就能读完(1 - 10 mins);🐕 中等 (10 - 20 mins);🐢 慢(20+ mins)

Flutter - Xcode16 还原编译速度

作者 LinXunFeng
2025年4月6日 19:12

欢迎关注微信公众号:FSA全栈行动 👋

一、前言

在之前发布的【Flutter - iOS编译加速】一文中,我们提到升级至 Xcode16 之后,iOS 的编译速度慢到令人发指,随后探索发现是 xcrun cc snapshot_assembly.S snapshot_assembly.o 这一汇编耗时变长了。而就在几天前,有人在相关的 issue 中留言了他篡改使用 Xcode 15cc 来提升编译速度的步骤,详情可见 github.com/dart-lang/s…

我在他的基础上做了优化与封装,只需两句命令即可还原编译速度,在开始详细介绍之前,先展示一下两台构建机优化前后的编译时长记录。

构建机 优化前(min) Release + 二进制依赖(min) Release + 二进制依赖 + 还原编译速度(min)
i7 25+ 14+ 11+
M4 16+ 8+ 4+

M4 只要四分多钟,真香~

二、调整

以下是他提供的修改步骤

cp -r /Applications/Xcode-15.4.0.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain ~/Library/Developer/Toolchains
cd ~/Library/Developer/Toolchains
mv XcodeDefault.xctoolchain Xcode15.4.xctoolchain
/usr/libexec/PlistBuddy -c "Add CompatibilityVersion integer 2" Xcode15.4.xctoolchain/ToolchainInfo.plist
/usr/libexec/PlistBuddy -c "Set Identifier clang.Xcode15.4" Xcode15.4.xctoolchain/ToolchainInfo.plist
  1. Xcode 15.4 内部的默认工具链复制到 ~/Library/Developer/Toolchains 目录。
  2. 将当前工作目录切换到 ~/Library/Developer/Toolchains 目录。
  3. 将复制过来的 XcodeDefault.xctoolchain 重命名为 Xcode15.4.xctoolchain,方便区分。
  4. 修改 .xctoolchain/ToolchainInfo.plist 文件,添加 CompatibilityVersion,并将其值设置为整数类型 2,修改 Identifier 的值为 clang.Xcode15.4
- Future<RunResult> cc(List<String> args) => _run('cc', args);
+ Future<RunResult> cc(List<String> args) => _run('--toolchain', <String>[
+   'clang.Xcode15.4',
+   'cc',
+   ...args,
+ ]);

修改 flutter_tools 源码,将 cc 修改为 --toolchain 来使用 clang.Xcode15.4 下的 cc

三、详解

默认的工具链路径是 Xcode 中的 /Applications/Xcode.app/Contents/Developer/Toolchains,不过也可以将工具链放到 ~/Library/Developer/Toolchains 目录下,这样就可以在不修改 Xcode 应用本身的情况下,使用和管理不同的工具链版本。

接着是修改 .xctoolchain/ToolchainInfo.plist 文件,里面可以设置的一些字段如下:

字段 说明
CFBundleIdentifier 唯一标识
CompatibilityVersion 适配版本,适配 Xcode 时必为 2
DisplayName 【可选】显示名称
ShortDisplayName 【可选】简短的显示名称

注:在 DisplayNameShortDisplayName 都不设置时,名字会显示为 CFBundleIdentifier

关于 CompatibilityVersion 的说明,在网上基本是搜不到的,只有如下这个注释,Xcode 8 及以上,使用 2,否则使用 1

# Xcode 8 requires CompatibilityVersion 2
set(COMPAT_VERSION 2)
if(XCODE_VERSION VERSION_LESS 8.0.0)
  # Xcode 7.3 (the first version supporting external toolchains) requires
  # CompatibilityVersion 1
  set(COMPAT_VERSION 1)
endif()

摘自: github.com/llvm/llvm-p…

四、改进

直接修改 flutter_tools 源码并写死 clang.Xcode15.4 太过于粗暴,如果我们为了安全起见,只想打测试包的时候还原编译速度,而打上架包保持原样就不好调整了,所以这里我对他的修改进行了优化。

首先来介绍一下 TOOLCHAINS 这个环境变量,它可以影响 /usr/bin/ 下的命令调用,如 /usr/bin/xcrun

注:Developer Directory/Applications/Xcode.app/Contents/Developer 或者 /Library/Developer/CommandLineTools,可以通过 xcode-select --print-path 进行检查

如果我们没有设置 TOOLCHAINS,根据上述流程图,在调用 /usr/bin/xcrun 时,会根据 Developer Directory 搜索该命令,如果找到同名命令,则执行该命令。

xcrun --find cc

# /Applications/Xcode-16.2.0.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/cc

如果我们将 TOOLCHAINS 设置为 .xctoolchainIdentifier,如: clang.Xcode15.4

export TOOLCHAINS=clang.Xcode15.4

那么根据上述流程图,则是在 Xcode15.4.xctoolchain 中找到 cc

xcrun --find cc
/Users/lxf/Library/Developer/Toolchains/Xcode15.4.xctoolchain/usr/bin/cc

根据这一特性,我做了如下调整:

调整 cc 方法,当有配置 CONDOR_TOOLCHAINS 环境变量时,将值取出并赋值给 TOOLCHAINS

//   Future<RunResult> cc(List<String> args) => _run('cc', args);
  Future<RunResult> cc(List<String> args) {
    final String condorToolchains = platform.environment['CONDOR_TOOLCHAINS'] ?? '';
    final Map<String, String> environment = <String, String>{
      if (condorToolchains.isNotEmpty) "TOOLCHAINS": condorToolchains,
    };
    _run('--find', <String>['cc'], environment: environment).then((RunResult result) {
      printStatus(
        '\n[condor] find cc: ${result.stdout}\n',
      );
    });
    return _run('cc', args, environment: environment);
  }

_run 方法新增 environment 参数,用于设置环境变量。

//   Future<RunResult> _run(String command, List<String> args) {
//     return _processUtils.run(
//       <String>[...xcrunCommand(), command, ...args],
//       throwOnError: true,
//     );
//   }
  Future<RunResult> _run(String command, List<String> args, {Map<String, String>? environment}) {
    return _processUtils.run(
      <String>[...xcrunCommand(), command, ...args],
      throwOnError: true,
      environment: environment,
    );
  }

五、Condor

上述步骤还是比较繁琐的,所以这里我将其进行了封装,只需要执行两句命令即可。

1、安装与更新 condor

Homebrew

如果你是首次安装,则执行如下命令

brew tap LinXunFeng/tap && brew install condor

如果不是首次安装,则需要执行如下命令进行更新

brew update && brew reinstall condor

Pub Global

如果你习惯使用 Pub,或者你的电脑是 Intel 芯,则可以执行如下命令进行安装或更新

dart pub global activate condor_cli

2、拷贝 xctoolchain

condor optimize-build xctoolchain-copy --xcode Xcode-15.4.0

--xcode 参数请使用 Xcode 15/Applications/ 下的名字,如果你电脑上没有 Xcode 15,建议使用 github.com/XcodesOrg/X… 进行安装。

这一步会做如下几个操作

  1. /Applications/Xcode-15.4.0.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain 拷贝至 ~/Library/Developer/Toolchains/Xcode-15.4.0.xctoolchain
  2. Xcode-15.4.0.xctoolchain/ToolchainInfo.plist 中的 Identifier 设置为 Xcode-15.4.0
  3. 添加 CompatibilityVersion 并设置为 2

3、cc 重写向

该命令会对 flutter_tools 源码进行修改,使其具备重定向 cc 的能力而已,在有配置 CONDOR_TOOLCHAINS 环境变量时才会生效,否则则使用默认的 cc

# 使用默认 flutter,则不需要传 flutter 参数
condor optimize-build redirect-cc

# 如果你想指定 fvm 下的指定 Flutter 版本
condor optimize-build redirect-cc --flutter fvm spawn 3.24.5

4、应用

后续你只需要 export CONDOR_TOOLCHAINS=Xcode-15.4.0 就可以在 Xcode 16 上感受到 Xcode 15 的编译速度了 🥳

如打包前 export

export CONDOR_TOOLCHAINS=Xcode-15.4.0
flutter clean
flutter build ipa

如果你想验证,可以加上 --verbose,并将输出保存到 result.txt

flutter build ipa --verbose > result.txt

命令执行完毕后打开 result.txt,搜索 condor 即可。

或者如果你不需要按需配置,也可以直接在 Run Script 里设置 CONDOR_TOOLCHAINS 环境变量。

验证也很简单,如下图所示,选择当前的 Build 任务,搜索 condor 即可。

六、是否有影响

Xcode 的工具链中,ccclang 的替身

而不同版本的 clang 对同一份 .S 进行汇编,还是有可能生成内容不一样的 .o 的。不过我自己对比 Xcode 16Xcode 15 生成的 .o 并没有什么不同。

对比 .o 文件,我们可以使用系统自带的 cmp 命令,cmp 是一个用于比较文件的命令行工具,它可以逐字节比较二进制文件。如下所示

cmp /Users/lxf/cc15/snapshot_assembly.o /Users/lxf/cc16/snapshot_assembly.o

cmp 命令执行完成,退出代码为 0,并且没有输出。这表明 cmp 命令没有发现两个文件之间有任何不同之处。因此可以证明这两个 .o 文件的内容是相同的。

即,基于 Xcode 16 来说并没有影响,这种方式生成的 .o 可以用于上架包,如果还是不放心,可以在打上架包时,不设置 CONDOR_TOOLCHAINS 环境变量即可。

七、资料

如果文章对您有所帮助, 请不吝点击关注一下我的微信公众号:FSA全栈行动, 这将是对我最大的激励. 公众号不仅有 Flutter 技术,还有 AIAndroidiOSPython 等文章, 可能有你想要了解的技能知识点哦~

2.流程控制

作者 思考着亮
2025年4月6日 15:25

if-else 的特殊用法

  1. if后面的条件可以省略小括号
  2. 条件后面的大括号不可以省略
let age = 4
if age >= 22 {
   print("可以结婚了")
}

3.if后面的条件只能是Bool类型的

if age {
}

上面的语句会报错,也就是只能显式的转化是一个Bool类型 image.png

while 语句

var num = 5
while num >0 {
print("num is \(num)")
num -=1
}
var num = -1
repeat {
print("num" is \(num))
} while num >0 //打印了一次
  • repeat-while 相当于C语言中的do-while
  • 这里不用num--,是因为从swift3开始,去除掉了自增(++),自减(--)运算符

for

闭区间运算符:a...b, a<= 取值 <= b,也就是说 ... 和 <= 都可以表示闭区间

  1. 普通的写法
let names = ['Anna','Alex','Brain','Jack']
for i in 0...3 {
  print(names[i])
}
//Anna Alex Brian Jack

2.将区间写成一个变量

let range = 1...3
for i in range {
print(names[i])
}
// Alex Brian Jack

3.区间可以一部分用变量,一部分不用变量

let a = 1
var b = 2
for i in a..b {
print(names[i])
}
// Alex Brian
for i in a...3 {
print(names[i])
}

4.上面的循环的下标i默认是用let声明的,有需要时可以声明为var

for var i in 1...3 {
i += 5
print(i)
}// 6 7 8

5.如果下面的代码用不到循环下标i,可以用 _去省略

for _ in 1...3 {
print("for")
} // 打印3次

半开区间运算符:a..<b, a<= 取值 < b,也就是用 ..., <= 和 < 构成半开区间

for i in 1...<5 {
print(i)
}
// 1 2 3 4

单侧区间:让区间朝一个方向尽可能的远

let range = ...5
range.contains(7) // false
range.contains(4) // true
range.contains(-3)// true

区间运算符在数组上的运用

let names = ['Anna',"Alex","Brian","jack"]
for name in names[0...3] {
print(name)
}
for name in names[2...]{
 print(name)
} // Brain Jack
for name in names[...2]{
 print(name)
}// Anna Alex Brian
for name in names[..<2] {
 print(name)
} //Anna Alex

区间类型

let range1: ClosedRange<Int> = 1...3
let range2: Range<Int> = 1..<3
let range3: PartialRangeThrough<Int> = 。。。5

需要特别注意的是上面的PartialRangeThrough 不能循环

let range:ClosedRange<Int> = 3...5

for _ in range{
print("我打印了开区间")
}
let range2: Range<Int> = 1..<3

for _ in range2 {

    print("我是打印半开区间")

}
let range3:PartialRangeThrough = ...5
for _ in range3 {
}

上面的开区间循环报错

For-in loop requires 'PartialRangeThrough<Int>' to conform to 'Sequence'

字符,字符串也能使用区间运算符,但默认不能用在for-in中

let stringRange1 = "cc"..."ff"
stringRange1.contains("cb")
stringRange1.contains("dz")
stringRange1.contains("fg")

let stringRange2 = "a"..."f"
stringRange2.contains("d")
stringRange2.contains("h")
// \0 到 ~ 包括了所有可能要用到的ASCII字符
let characterRange: ClosedRange<Character> = "\0"..."~"
characterRange.contains("G")

带间隔的区的区间值

let hours = 11
let hourInterval = 2
// tickMark的取值:从4开始,累加2,不超过11
for tickMark in stride(from: 4, to: hours, by: hourInterval){
    print(tickMark)
}

switch 语句

var num = 1
// case, default 后面不能写大括号
switch num {
case 1:
    print("number is 1")
    // 默认可以不写break,并不会贯穿到后面的条件
    break
case 2:
    print("number is 2")
    break
case 3:
    print("number is 3")
    break
default:{
    print("number is other")
}()
    break
}

贯穿问题

下面case 1 是不会贯穿到下面的

var num = 1
// case, default 后面不能写大括号
switch num {
case 1:
    print("number is 1")
    // 默认可以不写break,并不会贯穿到后面的条件
case 2:
    print("number is 2")
case 3:
    print("number is 3")
default:
    print("number is other")
    break
}

如果需要贯穿使用fallthrough,这个的贯穿的意思就是不判断接下来的一个条件,而且只会判断接下来的一个条件,后面还是会判断条件的。

var num = 1
// case, default 后面不能写大括号
switch num {
case 1:
    print("number is 1")
    fallthrough
    // 默认可以不写break,并不会贯穿到后面的条件
case 2:
    print("number is 2")
case 3:
    print("number is 3")
default:
    print("number is other")
    break
}

switch 必须要保证能处理所有情况

image.png

case default 后面至少要有一个语句,如果不想做任何事,加break即可

image.png

如果能保证已处理所有情况,也可以不必使用deault

// 定义枚举的一种方式
enum Answer {case right, wrong}
let answer = Answer.right

switch answer {
case Answer.right:
    print("right")
case Answer.wrong:
    print("wrong")
}

// 下面是一种简化的方式
//switch answer {
//case .right:
//    print("right")
//case .wrong:
//    print("left")
//}

switch 也支持Character,String 类型

String类型

单一值

let string = "Jack"
switch string {
case "Jack":
    fallthrough
case "rose":
    print("Right person")
default:
    break
}

多个值

switch string {
case "Jack","Rose":
    print("Right Person")
default:
    break
}

Character类型

let character: Character = "a"
switch character {
case "a","A":
    print("the letter A")
default:
    print("Not the letter A")
}

匹配区间,元祖匹配

区间匹配

let count = 62
switch count {
case 0:
    print("none")
case 1..<5:
    print("a few")
case 5..<12:
    print("several")
case 12..<100:
    print("dozens of")
case 100..<1000:
    print("hundres of")
default:
    print("many")
}

元祖匹配

let point = (1,1)
switch point {
case(0,0):
    print("the origin")
case(_,0):
    print("on the x-axis")
case(0,_):
    print("on the y-axis")
case(-2...2,-2...2):
    print("inside the box")
default:print("outside of the box")
}
  • 可以使用下划线_忽略某个值
  • 关于case匹配的问题,属于模式匹配的范畴,以后会再次详细展开详解

值绑定

值绑定其实就是将case传过来的值绑定在一个变量上,这个变量可以用 let var,变量就可以在后面使用

// 值绑定
let point1 = (2,0)
switch point1 {
case (let x, 0):
    print("on the x-axis with an x value of \(x)")
case (0, let y):
    print("on the y-axis with a y value of \(y)")
case let (x,y):
    print("somewhere else at (\(x),\(y))")
}

和 where 结合起来使用,where 那里的,其实就是加限制条件,限制本次循环是否进入循环体,而不是整个循环是否结束。相当于continus

// 和 where配合使用
let point3 = (1,-1)
switch point3 {
case let (x,y) where x == y:
    print("on the line x==y")
case let (x,y) where x == -y:
    print("on the line x==-y")
case let (x,y):
    print("在别的线上")
}
var numbers = [10,20,-10,-20,30,-30]
var sum = 0
for num in numbers where num > 0 {
    sum = sum + num
}
print(sum)

标签语句

加标签是为了跳出外层循环 outer: for i in 1...4 {     for k in 1...4 {         if k == 3 {             continue outer         }         if i == 3 {             break outer         }         print("i==(i),k ==(k)")     } }

react父子组件如何通信?

2025年4月6日 06:25

React 父子组件如何通信?

在 React 中,组件之间的通信是一个重要的概念,尤其是父组件与子组件之间的通信。父子组件之间的通信主要有以下几种方式:

1. 通过 Props 传递数据

父组件可以通过 props 向子组件传递数据。props 是 React 中用于组件间传递数据的一种机制。子组件可以通过 this.props 访问父组件传递的数据。

// 父组件
function ParentComponent() {
  const message = "Hello from Parent!";
  return <ChildComponent message={message} />;
}

// 子组件
function ChildComponent(props) {
  return <div>{props.message}</div>; // 显示父组件传递的消息
}

2. 使用回调函数

父组件可以通过 props 向子组件传递一个回调函数,子组件通过调用这个函数来将数据传递回父组件。这种方式通常用于子组件向父组件传递事件或数据。

// 父组件
function ParentComponent() {
  const handleChildData = (data) => {
    console.log("Data from child:", data);
  };

  return <ChildComponent onSendData={handleChildData} />;
}

// 子组件
function ChildComponent(props) {
  const sendDataToParent = () => {
    props.onSendData("Data from Child!"); // 调用父组件的回调函数
  };

  return <button onClick={sendDataToParent}>Send Data to Parent</button>;
}

3. 使用 Context API

Context API 是 React 提供的一个功能,用于在组件树中共享数据,而不必通过 props 一层层传递。它适用于深层嵌套的组件需要访问相同的数据。

import React, { createContext, useContext } from "react";

// 创建一个 Context
const MyContext = createContext();

// 父组件
function ParentComponent() {
  const value = "This is context data";

  return (
    <MyContext.Provider value={value}>
      <ChildComponent />
    </MyContext.Provider>
  );
}

// 子组件
function ChildComponent() {
  const contextValue = useContext(MyContext); // 使用 useContext 钩子

  return <div>{contextValue}</div>; // 显示来自 Context 的数据
}

4. 使用 Redux 或 MobX 等状态管理库

如果应用程序比较复杂,父子组件之间需要频繁地传递数据,使用状态管理库(如 Redux 或 MobX)可能更合适。它们提供了全局状态管理,组件可以直接从全局状态中获取数据或更新数据。

// 使用 Redux 示例
import { createStore } from 'redux';
import { Provider, useSelector, useDispatch } from 'react-redux';

// 创建 Redux store
const store = createStore((state = { message: "Hello!" }) => state);

// 父组件
function ParentComponent() {
  return (
    <Provider store={store}>
      <ChildComponent />
    </Provider>
  );
}

// 子组件
function ChildComponent() {
  const message = useSelector(state => state.message); // 从全局状态获取数据
  const dispatch = useDispatch();

  const updateMessage = () => {
    // 更新全局状态
    dispatch({ type: 'UPDATE_MESSAGE', payload: 'New message!' });
  };

  return (
    <div>
      <div>{message}</div>
      <button onClick={updateMessage}>Update Message</button>
    </div>
  );
}

5. 使用 refs

在某些情况下,父组件可以通过 refs 直接访问子组件的方法和属性。这种方式适用于需要直接操作子组件的场景,但应谨慎使用,因为它可能会导致组件之间的耦合度增加。

// 父组件
class ParentComponent extends React.Component {
  constructor(props) {
    super(props);
    this.childRef = React.createRef();
  }

  callChildMethod = () => {
    this.childRef.current.childMethod(); // 调用子组件的方法
  };

  render() {
    return (
      <div>
        <button onClick={this.callChildMethod}>Call Child Method</button>
        <ChildComponent ref={this.childRef} />
      </div>
    );
  }
}

// 子组件
class ChildComponent extends React.Component {
  childMethod() {
    console.log("Child method called!");
  }

  render() {
    return <div>Child Component</div>;
  }
}

总结

父子组件之间的通信方式主要包括通过 props 传递数据、使用回调函数、Context API、状态管理库(如 Redux 或 MobX)以及通过 refs 直接访问子组件。根据应用的复杂性和需求,可以选择适合的方式来实现组件间的通信。在设计组件时,尽量保持组件的独立性和可复用性,避免过度耦合。

SwiftUI 入门指南:快速构建跨平台应用

2025年4月4日 12:12

SwiftUI 是苹果推出的一个强大的 UI 框架,允许开发者使用声明式语法快速构建跨平台应用。它支持 iOS、macOS、tvOS 和 watchOS 等多个平台,帮助开发者以更少的代码实现更多功能。以下是 SwiftUI 中一些常用的 API 和示例代码,帮助你快速上手。

视图和控件

1. Text

用于显示静态文本,可以设置字体、颜色、对齐方式等属性。

Text("Hello, SwiftUI!")
    .font(.title)
    .foregroundColor(.blue)

2. Image

用于显示图像。

Image("image-name")
    .resizable()
    .frame(width: 100, height: 100)

3. Button

用于创建按钮。

Button("点击我") {
    print("按钮被点击")
}

4. TextField

用于输入文本。

@State private var text = ""

TextField("输入文本", text: $text)

5. Toggle

用于开关控件。

@State private var isOn = false

Toggle("开关", isOn: $isOn)

布局容器

1. VStack

垂直堆叠视图。

VStack {
    Text("文本1")
    Text("文本2")
}

2. HStack

水平堆叠视图。

HStack {
    Text("文本1")
    Text("文本2")
}

3. ZStack

层叠视图。

ZStack {
    Image("背景")
    Text("文本")
}

4. List

列表视图。

struct Item: Identifiable {
    let id = UUID()
    var name: String
}

@State private var items: [Item] = [
    Item(name: "Item1"),
    Item(name: "Item2")
]

List {
    ForEach(items) { item in
        Text(item.name)
    }
}

动画和效果

1. withAnimation

用于添加动画。

@State private var opacity: Double = 1.0

Button("淡入淡出") {
    withAnimation {
        opacity = 0.5
    }
}

Text("文本")
    .opacity(opacity)

2. .animation

为视图添加动画。

@State private var isExpanded = false

Button("展开/折叠") {
    isExpanded.toggle()
}

Text("文本")
    .scaleEffect(isExpanded ? 1.5 : 1.0)
    .animation(.easeInOut(duration: 1.0))

其他

1. NavigationLink

用于导航。

struct DetailView: View {
    var body: some View {
        Text("详情页")
    }
}

NavigationLink(destination: DetailView()) {
    Text("前往详情页")
}

2. @State

用于状态变量。

@State private var counter = 0

Button("点击增加") {
    counter += 1
}

Text("计数器:\(counter)")

3. @Binding

用于绑定变量。

struct ChildView: View {
    @Binding var isOn: Bool

    var body: some View {
        Toggle("开关", isOn: $isOn)
    }
}

struct ParentView: View {
    @State private var isOn = false

    var body: some View {
        ChildView(isOn: $isOn)
    }
}

4. API 数据获取

SwiftUI 中获取 API 数据可以使用 URLSession 类。以下是一个简单的例子:

import SwiftUI

struct Post: Codable, Identifiable {
    let id = UUID()
    var title: String
    var body: String
}

struct ContentView: View {
    @State private var posts: [Post] = []

    var body: some View {
        List(posts, id: \.id) { post in
            VStack(alignment: .leading) {
                Text(post.title)
                Text(post.body)
                    .font(.subheadline)
                    .foregroundColor(.secondary)
            }
        }
        .onAppear {
            fetchData()
        }
    }

    func fetchData() {
        guard let url = URL(string: "https://jsonplaceholder.typicode.com/posts") else { return }

        URLSession.shared.dataTask(with: url) { data, response, error in
            if let data = data {
                do {
                    let posts = try JSONDecoder().decode([Post].self, from: data)
                    DispatchQueue.main.async {
                        self.posts = posts
                    }
                } catch {
                    print(error.localizedDescription)
                }
            }
        }.resume()
    }
}

通过这些示例,你可以快速掌握 SwiftUI 的基本用法,并开始构建自己的跨平台应用。SwiftUI 的声明式语法使得代码更加直观和易于维护。

Command SwiftCompile failed with a nonzero exit code Command SwiftGeneratePch em

2025年4月3日 20:04

Command SwiftCompile failed with a nonzero exit code Command SwiftGeneratePch emitted errors but did not return a nonzero exit code to indicate failure Command PrecompileSwiftBridgingHeader emitted errors but did not return a nonzero exit code to indicate failure

背景是xcode16.2 HandyJSON库不维护了 用了三步解决以上问题,

第一步 原文路径

升级Xcode15后 打包报错 xxx Command SwiftCompile failed with a nonzero exit code

解决办法: 选中pod 报错的库 Code Generation->Compilation Mode改成和debug一样的 Incremental。

image.png

image.png

第二步 原文路径

Xcode编译时报错“Command CompileSwiftSources failed with a nonzero exit code”。

应该是项目中的Socket.IO-Client-Swift这个pod导致的。

解决方案:

在Build Setting里添加一条user-defined

属性为SWIFT_ENABLE_BATCH_MODE,值为NO

image.png

Xcode编译时报错“Command CompileSwiftSources failed with a nonzero exit code”。

应该是项目中的Socket.IO-Client-Swift这个pod导致的。

解决方案:

在Build Setting里添加一条user-defined

属性为SWIFT_ENABLE_BATCH_MODE,值为NO

第三步

由于我自己维护HandyJSON库,见了一个abcHandyJSON.kit库,代码报错提示找不到 /Users/xx/Library/Developer/Xcode/DerivedData/项目名-bqphxvnehmbicihernwajnvfgqwr/Build/Products/Debug-iphoneos/HandyJSON/HandyJSON.modulemap

搜索other swift flag

image.png 中的HandyJSON替换成自己的abcHandyJSON名

❌
❌