阅读视图

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

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

在这里插入图片描述

概述

何曾几时,小伙伴们在 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 周报 #340 | 2025-06-30

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

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

文章

🌟 🐢 btrace 3.0 对外开源:重磅新增 iOS 支持!免插桩原理大揭秘!

@JonyFang: btrace 是字节开源的一款高性能 Android/iOS 端性能追踪(Tracing)工具,基于 Perfetto 进行数据展示。它能够详细记录方法的调用过程,精准分析耗时,并归因性能瓶颈,兼具高采样精度和低性能损耗。与 Apple 的 Time Profiler 等传统工具相比,btrace 更加灵活、可自定义,并支持系统方法追踪、有丰富的数据归因和可视化能力,能帮助开发者深入理解和优化 App 性能。

btrace 3.0 相比 2.0 的优化(iOS 视角):

  1. 采集方案升级。
    3.0 由单一编译期插桩,升级为“同步抓栈 + 异步抓栈”的混合采样方案。同步抓栈通过 hook 高频系统方法和关键节点,实时采集 Trace 数据;异步抓栈则通过独立采样线程定时回溯线程调用栈,保证采集的时间连续性。相比 2.0,3.0 大幅降低了接入和维护成本,采集更全面、对系统方法也支持更好。

  2. 数据存储与压缩优化。
    3.0 针对 Trace 数据量大、存储压力大等问题,设计了高效的调用栈去重与压缩结构。通过空间相似性(调用栈公共前缀合并)、时间相似性(连续相同栈合并)等手段,进一步减少内存和磁盘占用,提升了大体量数据下的可用性。

  3. 多线程与性能再提升。
    3.0 优化了多线程数据写入的并发安全性与性能,采用 CAS 等无锁 / 低锁技术,兼顾高性能与数据一致性,在复杂多线程场景下依然保持低开销。

  4. 死锁规避与线程采样精细化。
    异步采样时规避了 Time Profiler 可能导致的死锁风险,通过黑名单和信号安全 API 控制,提升了工具的稳定性。同时,仅采集活跃线程,有效降低了对 App 性能的影响。

  5. 丰富的性能归因与可视化。
    除了基本的方法调用追踪,3.0 进一步支持 CPU 时间、对象分配、缺页 / 上下文切换、线程阻塞等多维度的耗时归因,配合 Perfetto 可视化,帮助开发者一站式定位性能瓶颈。

  6. 易用性和生态提升。
    3.0 极大简化了接入流程,无需业务侧代码大改,无侵入式支持线上场景,支持性能自动诊断和多端(Android/iOS/ 鸿蒙 /Web)扩展,生态愈发完善。

整体来看,btrace 3.0 对 iOS 开发者而言,是一款集高性能、易用性、灵活性于一体的专业 Trace 工具。相比 2.0,3.0 大幅优化了采集方式、性能、安全性和数据分析能力,适合需要深入性能调优、线上问题定位和日常性能治理使用,推荐纳入工程实践!

🐕 避免在 Swift 中使用 self.

@AidenRao:这篇文章探讨了在 Swift 开发中避免不必要的 self. 前缀使用,利用编译器检查减少循环引用风险。它基于 Swift 5.3(SE-0269)和 5.8(SE-0365)的演进,在闭包中省略 self. 能让编译器强制捕获语义(如使用 [weak self]),从而暴露潜在内存泄漏问题。

🐕 Why I ’ ve Filed Over 1,000 Apple Feedbacks — And Why You Should Too

@阿权:文章分享了作者关于 Apple Feedback 的心得体会与收益:

  1. 反馈的价值:
    1. 作者自 2014 年起提交超 1000 条反馈,推动漏洞修复、文档更新及新功能开发(如 HealthKit、MetricKit 等框架的改进)。
    2. 对开发者而言,可以理清技术思路、记录测试用例,形成可复用的技术文档;提升沟通能力,强化对平台的理解,甚至在撰写反馈过程中解决问题。
  2. 有效提交反馈的策略:
    1. 内容结构化:
      1. 标题:包含框架名和关键词(如 “生产问题”“测试版”),例:“ HealthKit: 锻炼会话中 paddleSports 的 totalDistance 弃用导致数据保存失败”。
      2. 内容:遵循 “问题描述→预期→复现步骤→重要性→示例项目” 结构,附日志、截图或 sysdiagnose 文件。
    2. 高优场景:
      1. 生产环境漏洞、重大回归、影响广泛的 API 问题;
      2. beta 版本期间,及时提交问题,利用 WWDC 与工程师面对面跟进。
  3. 作者的反馈实践案例。

开发者可以积极参与反馈提交,尤其在 WWDC 和测试版周期中,通过结构化报告和社区分享推动平台改进。反馈不仅是对 Apple 的贡献,更是优化自身开发流程的重要手段。

🐢 Reverse-Engineering Xcode's Coding Intelligence prompt

@zhangferry:Xcode 26 提供了 Coding Intelligence 功能,并且支持自定义模型。但当前自定义模型支持的 URL 格式 是 ChatGPT 风格的,非这类格式例如 Gemini 还需要依赖 Proxyman 这类网络代理工具做一层转换。(感觉算是 Bug,希望后续能修复)
文中以 Xcode 中使用 Gemini 为例,抓取和分析了 AI 相关的代码解释、文档生成、代码生成这几个功能所涉及的 Prompt,每一个功能都对应一组封装好的 Prompt,可以了解到 Apple 是如何使用 PE 的:

  • 行为约束:完整理解用户意图及代码再回答问题
  • 前置知识:注意 Apple 平台的 API 选型;代码生成优先使用 Swift、OC;优先 Concurrency 而不是 Combine
  • 代码理解:使用 SEARCH 工具(函数调用)获取代码相关上下文,再丢给 LLM
  • 代码生成:除了代码本身,还会查找项目依赖,学习其 API

🐕 Flutter 又双叒叕可以在 iOS 26 的真机上 hotload 运行了,来看看又是什么黑科技

@Damien:由于 iOS 26 beta1 禁止了 Debug 时 mprotect 的 RX 权限,导致 Flutte 在 iOS 26 真机上 Debug 运行时出现了问题。为了解决这一问题,Flutter 团队采用了一种临时方案,即创建了 NOTIFY_DEBUGGER_ABOUT_RX_PAGES 函数。当 Flutter 应用需要执行新代码时,该函数会暂停应用并通知调试器,调试器随后利用其特权,通过 debugserver 修改内存权限,实现“双地址映射”,其中一个地址用于写入代码,另一个地址用于执行代码。这一方案虽然解决了当前的运行问题,但存在一定的延迟和较高的环境要求,未来仍需开发高性能的 Debug 解释器来提供更完善的解决方案。

代码

🐕 EFQRCode

@BarneyEFQRCode 是一个轻量级纯 Swift 二维码库,支持生成带水印 / 图标的风格化二维码和图片识别功能。基于 CoreGraphicsCoreImageImageIO,全平台支持 iOS/macOS/watchOS/tvOS/visionOS 。最新 7.0.0 版本重构了 API,引入 EFQRCodeGeneratorEFQRCodeRecognizer 类,支持链式配置,改进 Objective-C 兼容性。可通过 CocoaPodsCarthageSPM 集成。

音视频

🐢 SwiftData versus SQL Query Builder

@Kyle-Ye: Point-Free 团队在 WWDC 2025 期间免费放送了一期重磅视频,深入对比 SwiftData 与他们自家 SQL Query Builder(Structured Queries)在实际开发中的表现。视频以还原 Apple Reminders 复杂查询为例,展示了两种方案在代码简洁性、可组合性和类型安全等方面的差异。

Structured Queries 方案只需 23 行代码即可线性表达复杂查询逻辑,支持类型安全、可读性强;而 SwiftData 不仅写法更繁琐(32 行),还存在布尔和枚举类型无法直接排序 / 筛选、可选字段排序不灵活等问题,甚至有些写法在运行时会直接崩溃。

如果你关心 Swift 持久化方案、数据层架构,或在 SwiftData 和 SQL 之间犹豫,强烈建议观看本期视频。

内推

重新开始更新「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)

做了一个小游戏:笔画猜成语

前几天在看跑男,有一个游戏环节是这样的:嘉宾通过努力得到一个笔画,目标文字中如果包含该笔画就会出现。感觉挺有意思的,对它稍加改造,于是就有了这个小游戏:笔画猜成语

游戏机制是这样的:

  • 初始显示 20% 的笔画作为提示
  • 每隔一分钟会多 10% 的笔画,最多显示 50% 的笔画
  • 总共有 5 次猜测机会
  • 新题每日更新

和周围的人玩了下,还是挺有乐趣的。如果你也感兴趣的话,可以在这里体验哦。

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

背景

引入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休闲益智连连看【进阶】

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

在移动应用开发中,跨平台技术始终是开发者追求的圣杯。借助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【进阶】

通过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休闲益智儿童拼图【进阶】

【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 自定文字颜色

最近在做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 托管类型(下)

在这里插入图片描述

概述

相信各位似秃非秃小码农们都同意,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 托管类型(上)

在这里插入图片描述

概述

相信各位似秃非秃小码农们都同意,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 实战

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


目录

  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

Xcode 14.3 和 iOS 16.4 为 SwiftUI 带来了哪些新功能?

在这里插入图片描述

0. 概览

今年年初,Apple 推出了最新的 Xcode 14.3 以及对应的 iOS 16.4 。

与此同时,它们对目前最新的 SwiftUI 4.0 也添加了一些新功能:

  • sheet 弹窗后部视图(Interact with a view Behind a sheet)可交互;
  • sheet 弹窗背景透明化;
  • 调整 sheet 弹窗顶角弧度;
  • 控制弹窗内滚动手势优先级;
  • 定制紧密(compact-size )尺寸下 sheet 弹窗大小;
  • Xcode 预览(Preview)模式下对调试输出的支持;

让我们依次来了解一下它们吧。

Let‘s go!!!;)


1. sheet 后部视图可交互

在 iOS 16.4 之前,SwiftUI 中 sheet 弹窗后,如果点击其后部的视图会导致弹窗立即被关闭,从而无法与弹窗后部的视图进行交互。

从 iOS 16.4 开始,我们可以为 sheet 弹窗应用 presentationBackgroundInteraction() 方法,以达到不关闭弹窗而与后部视图交互之目的:

@available(iOS 16.4, *)
struct ContentView: View {
    @State private var isPresented = false
    @State private var number = 0
    
    var body: some View {
        ZStack(alignment: .top) {
            Rectangle()
                .fill(Gradient(colors: [.red,.green]).opacity(0.66))
                .ignoresSafeArea()
            Button("Sheet") {
                isPresented = true
            }
            .buttonStyle(.borderedProminent)
            .padding()
            
            VStack {
                Button("产生随机数: \(number)"){
                    number = Int.random(in: 0..<10000000)
                }
                .foregroundColor(.white)
                .font(.title.weight(.black))
            }.padding(.top, 200)
        }
        .sheet(isPresented: $isPresented) {
            Text("大熊猫侯佩 @ csdn")
                .font(.headline)
                .presentationDetents([.height(120), .medium, .large])
                // 开启后部视图交互
                .presentationBackgroundInteraction(.enabled)
        }
    }
}

在这里插入图片描述

2. sheet 背景透明化

从 iOS 16.4 开始,我们可以为 sheet 弹窗选择透明样式,更好的美化弹出窗口的显示效果。

如下代码所示,我们在 sheet 弹窗上应用了 presentationBackground(_: ) 修改器以实现透明磨砂效果:

@available(iOS 16.4, *)
struct ContentView: View {
    @State private var isSheet = false
    @State private var isSheetTransparency = false
    
    var body: some View {
        ZStack(alignment: .top) {
            Rectangle()
                .fill(Gradient(colors: [.red,.green]).opacity(0.66))
                .ignoresSafeArea()
            
            HStack {
                Button("弹出") {
                    isSheet = true
                }
                .sheet(isPresented: $isSheet) {
                    Text("大熊猫侯佩 @ csdn")
                        .font(.headline)
                        .presentationDetents([.height(120), .medium, .large])
                        // 或使用 .background 调用 presentationBackground() 方法效果相同
                        //.presentationBackground(.background)
                }
                
                Spacer()
                
                Button("透明弹出") {
                    isSheetTransparency = true
                }
                .sheet(isPresented: $isSheetTransparency) {
                    Text("大熊猫侯佩 @ csdn")
                        .font(.headline)
                        .presentationDetents([.height(120), .medium, .large])
                        .presentationBackground(.ultraThinMaterial)
                }
            }
            .font(.headline)
            .buttonStyle(.borderedProminent)
            .padding(.top, 200)
            .padding(.horizontal, 50)
        }
    }
}

在这里插入图片描述

3. sheet 顶部弧度调整

感觉 sheet 弹窗顶角生硬无弧度的小伙伴们有福了,从 iOS 16.4 开始,SwiftUI 开始支持调整 sheet 弹出窗口顶角的弧度了。

我们可以使用 .presentationCornerRadius() 修改器来实现这一功能:

@available(iOS 16.4, *)
struct ContentView: View {
    @State private var isSheet = false
    @State private var isSheetRadius = false
    
    var body: some View {
        ZStack(alignment: .top) {
            Rectangle()
                .fill(Gradient(colors: [.red,.green]).opacity(0.66))
                .ignoresSafeArea()
            
            HStack {
                Button("弹出") {
                    isSheet = true
                }
                .sheet(isPresented: $isSheet) {
                    Text("大熊猫侯佩 @ csdn")
                        .font(.headline)
                        .presentationDetents(.height(120), .medium, .large])
                }
                
                Spacer()
                
                Button("顶角圆润弧度弹出") {
                    isSheetRadius = true
                }
                .sheet(isPresented: $isSheetRadius) {
                    Text("大熊猫侯佩 @ csdn")
                        .font(.headline)
                        .presentationDetents([.height(120), .medium, .large])
                        .presentationCornerRadius(30.0)
                }
            }
            .font(.headline)
            .buttonStyle(.borderedProminent)
            .padding(.top, 200)
            .padding(.horizontal, 50)
        }
    }
}

在这里插入图片描述

4. sheet 滚动手势优先级调整

在 iOS 16.4 之前,如果我们 sheet 尺寸可变弹窗中包含滚动视图(比如 List,ScrollView 等),当用户在弹窗中滚动将会首先引起弹窗尺寸的改变,而不是其滚动内容的改变。

在 iOS 16.4 之后,我们可以调整 sheet 弹窗滚动手势优先级,以确保首先滚动其内容而不是改变弹窗尺寸。

这是通过 .presentationContentInteraction(.scrolls) 方法来实现的:

@available(iOS 16.4, *)
struct ContentView: View {
    @State private var isSheet = false
    @State private var isSheetScrollable = false
    
    var body: some View {
        
        ZStack(alignment: .top) {
            Rectangle()
                .fill(Gradient(colors: [.red,.green]).opacity(0.66))
                .ignoresSafeArea()
            
            
            HStack {
                Button("弹出") {
                    isSheet = true
                }
                .sheet(isPresented: $isSheet) {
                    VStack(spacing: 16) {
                        Text("大熊猫侯佩 @ csdn")
                            .font(.headline)
                        List(0..<50, id: \.self){ i in
                            Text("Item \(i)")
                                .font(.subheadline)
                        }
                        .listStyle(.plain)
                    }
                    .padding()
                    .presentationDetents([.height(120), .medium, .large])
                }
                
                Spacer()
                
                Button("滚动高优先级弹出") {
                    isSheetScrollable = true
                }
                .sheet(isPresented: $isSheetScrollable) {
                    VStack(spacing: 16) {
                        Text("大熊猫侯佩 @ csdn")
                            .font(.headline)
                        List(0..<50, id: \.self){ i in
                            Text("Item \(i)")
                                .font(.subheadline)
                        }
                        .listStyle(.plain)
                    }
                    .padding()
                    .presentationDetents([.height(120), .medium, .large])
                    .presentationContentInteraction(.scrolls)
                }
            }
            .font(.headline)
            .buttonStyle(.borderedProminent)
            .padding(.top, 200)
            .padding(.horizontal, 50)
        }
    }
}

在这里插入图片描述

5. 定制 sheet 在紧密尺寸下的大小

在 iOS 16.4 之前,如果在 iPhone 横屏时 sheet 弹窗,则弹出窗口将会铺满整个屏幕。

从 iOS 16.4 开始,我们可以为弹窗应用新的 .presentationCompactAdaptation(_: ) 修改器来改变横屏时弹窗的大小:

struct SheetView: View {
    @Environment(\.dismiss) var dismiss
    
    var body: some View {
        VStack(spacing: 16) {
            Text("大熊猫侯佩 @ csdn")
            Button("关闭"){
                dismiss()
            }
        }
    }
}

@available(iOS 16.4, *)
struct ContentView: View {
    @State private var isSheet = false
    @State private var isSheetCompactSizeCustom = false
    
    var body: some View {
                
        ZStack(alignment: .top) {
            Rectangle()
                .fill(Gradient(colors: [.red,.green]).opacity(0.66))
                .ignoresSafeArea()
            
            HStack {
                Button("弹出") {
                    isSheet = true
                }
                .sheet(isPresented: $isSheet) {
                    SheetView()
                        .padding()
                        .frame(width: 200)
                        .presentationDetents([.height(200), .medium, .large])
                }
                
                Spacer()
                
                Button("自定义尺寸弹出") {
                    isSheetCompactSizeCustom = true
                }
                .sheet(isPresented: $isSheetCompactSizeCustom) {
                    VStack(spacing: 16) {
                        Text("大熊猫侯佩 @ csdn")
                    }
                    .padding()
                    .frame(width: 350)
                    .presentationDetents([.height(200), .medium, .large])
                    .presentationCompactAdaptation(.sheet)
                }
            }
            .font(.headline)
            .buttonStyle(.borderedProminent)
            .padding(.top, 200)
            .padding(.horizontal, 50)
        }
    }
}

在这里插入图片描述

6. Xcode 预览模式对调试输出的支持

Xcode 14.3 之前,我们在预览(Preview)模式中测试 SwiftUI 界面功能时无法观察调试语句( print 等方法)的输出结果,必须在模拟器或真机中运行才可以在 Xcode 底部调试小窗口中看到 print() 等方法的输出。

从 Xcode 14.3 开始,以预览模式运行 App 时也可以在调试窗口中看到调试语句的输出了,真是太方便了:

@available(iOS 16.4, *)
struct ContentView: View {
    
    var body: some View {
                
        ZStack(alignment: .center) {
            Rectangle()
                .fill(Gradient(colors: [.red,.green]).opacity(0.66))
                .ignoresSafeArea()
            
            Button("显示 debug 输出") {
                print("显示随机数: \(Int.random(in: 0..<10000000))")
            }
        }
    }
}

在这里插入图片描述

7. 总结

在本篇博文中,我们介绍了在 Xcode 14.3 和 iOS 16.4 中 SwiftUI 为开发者带来的新方法和新功能,解决了诸多燃眉之急的问题,小伙伴们不想赶快尝试一下吗?🚀

感谢观赏,再会!8-)

音视频学习笔记 02 读取数据

在笔记 01 的代码里读取数据可能会出错。

av_read_frame() 返回 -35(即 AVERROR(EAGAIN) 或 AVERROR_EOF)表示当前无法读取到数据包(packet),但未来可能可以。这通常发生在以下几种情况:

1. 非阻塞模式下数据未就绪

  • 如果你将 AVFormatContext 设置为 非阻塞模式(通过 AVFMT_FLAG_NONBLOCK),av_read_frame() 可能会立即返回 EAGAIN(即 -35),表示当前没有可读的数据包,但稍后重试可能会成功。

  • 解决方法

    • 如果是故意使用非阻塞模式,需要循环重试(或其他逻辑处理)。
    • 如果不需要非阻塞模式,确保未设置 AVFMT_FLAG_NONBLOCK

2. 流结束(EOF)

  • 如果已经读取完所有数据包(如文件末尾或直播流中断),av_read_frame() 可能返回 EAGAIN 或 EOF

  • 解决方法

    • 检查 fmt_ctx->pb->eof_reached 或 pkt.flags 是否包含 AV_PKT_FLAG_EOF
    • 如果是正常结束,可以关闭上下文或重新初始化。

3. 输入流格式问题

  • 某些特殊格式(如实时流、网络流)可能需要更多初始化时间,或者数据未及时到达。

  • 解决方法

    • 确保输入源正常(如文件路径正确、网络流可访问)。
    • 检查 fmt_ctx 是否成功打开(avformat_open_input() 返回 0)。

4. 编码器/复用器未正确初始化

  • 如果 fmt_ctx 是输出上下文(例如用于编码或复用),可能需要先写入头信息(avformat_write_header())。

  • 解决方法

    • 确保正确初始化输出上下文。

调试步骤

  1. 检查错误码的具体含义

    char errbuf[AV_ERROR_MAX_STRING_SIZE];
    av_strerror(ret, errbuf, sizeof(errbuf));
    fprintf(stderr, "av_read_frame failed: %s\n", errbuf);
    

    输出错误详情(如 "Resource temporarily unavailable" 或 "End of file")。

  2. 验证输入源

    • 确认 fmt_ctx 已通过 avformat_open_input() 成功打开。
    • 检查 fmt_ctx->streams 是否包含有效的流(如 fmt_ctx->nb_streams > 0)。
  3. 重试逻辑

    • 如果是非阻塞模式或实时流,可能需要循环调用 av_read_frame() 直到返回 0

示例代码(处理非阻塞情况)

AVPacket pkt;
av_init_packet(&pkt);
while ((ret = av_read_frame(fmt_ctx, &pkt)) == AVERROR(EAGAIN)) {
    // 等待或处理其他任务(如非阻塞模式)
    usleep(1000); // 避免忙等待
}
if (ret < 0 && ret != AVERROR_EOF) {
    // 真实错误
    fprintf(stderr, "Error reading packet: %s\n", av_err2str(ret));
} else if (ret == AVERROR_EOF) {
    // 正常结束
    printf("Reached end of file.\n");
} else {
    // 成功读取到数据包
    // 处理 pkt...
    av_packet_unref(&pkt);
}

如果问题仍存在,请提供更多上下文代码(如 fmt_ctx 的初始化部分)。

上面的代码 和 解释 由 deepseek 给出。

修改后的代码:

ViewController

//
//  ViewController.swift
//  myapp
//
//  Created by mac on 2025/6/23.
//

import Cocoa

class ViewController: NSViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        self.view.setFrameSize(NSSize(width: 320, height: 240))
        let btn = NSButton.init(title: "Button", target: nil, action: nil)
        btn.title = "Hello"
        btn.frame = NSRect(
            x: 320 / 2 - 40,
            y: 240 / 2 - 15,
            width: 80,
            height: 30
        )
        btn.bezelStyle = .rounded
        btn.setButtonType(.pushOnPushOff)

        // callback
        btn.target = self
        btn.action = #selector(myFunc)

        self.view.addSubview(btn)
    }

    @objc
    func myFunc() {
        record_audio()
    }

    override var representedObject: Any? {
        didSet {
            // Update the view, if already loaded.
        }
    }
}

bridge 文件 myapp/myapp/myapp-Bridging-Header.h

//
//  Use this file to import your target's public headers that you would like to
//  expose to Swift.
//

#import "testc.h"

c 头文件

//
//  testc.h
//  myapp
//
//  Created by mac on 2025/6/23.
//

#ifndef testc_h
#define testc_h

#include "libavcodec/avcodec.h"
#include "libavdevice/avdevice.h"
#include "libavformat/avformat.h"
#include "libavutil/avutil.h"
#include <stdio.h>
#include <unistd.h>

void record_audio(void);

#endif /* testc_h */

c 代码体

//
//  testc.c
//  myapp
//
//  Created by mac on 2025/6/23.
//

#include "testc.h"

void record_audio(void) {
    int ret = 0;
    char errors[1024] = {
        0,
    };

    AVFormatContext *fmt_ctx = NULL;

    // 读出来的数据存储到这个 packet 里面
    AVPacket pkt;
    int count = 0;

    // [[video device]:[audio device]]
    //    char *devicename = ":2";
    char *devicename = ":1";
    //    char *devicename = ":0";

    // 设置日志级别
    av_log_set_level(AV_LOG_DEBUG);

    // 1 register audio device
    avdevice_register_all();

    // 2 get format
    // const AVInputFormat *iformat = av_find_input_format("avfoundation");
    const AVInputFormat *input_format = av_find_input_format("avfoundation");

    AVDictionary *options = NULL;
    av_dict_set(&options, "sample_rate", "44100", 0);
    av_dict_set(&options, "channels", "2", 0);

    // 3 open device
    ret = avformat_open_input(&fmt_ctx, devicename, input_format, &options);

    if (ret < 0) {
        av_strerror(ret, errors, 1024);
        printf(stderr, "Failed to open audio device [%d] %s\n", ret, errors);

        return;
    }

    // AVPacket 使用之前要初始化
    //    av_init_packet(&pkt);
    av_new_packet(&pkt, 512);
    //    ret = av_read_frame(fmt_ctx, &pkt);

    while (count++ < 500) {
        // read data from device
        while ((ret = av_read_frame(fmt_ctx, &pkt)) == AVERROR(EAGAIN)) {
            // 等待或处理其他任务(如非阻塞模式)
            usleep(1000); // 避免忙等待
        }

        if (ret < 0 && ret != AVERROR_EOF) {
            // 真实错误
            fprintf(stderr, "Error reading packet: %s\n", av_err2str(ret));
        } else if (ret == AVERROR_EOF) {
            // 正常结束
            printf("Reached end of file.\n");
        } else {
            // 成功读取到数据包
            // 处理 pkt...

            av_log(NULL, AV_LOG_INFO, "count = %d, ret = %d, pkt.size = %d\n",
                   count, ret, pkt.size);

            // 每次使用完释放包
            av_packet_unref(&pkt);
        }
    }

    // close device and 释放上下文
    avformat_close_input(&fmt_ctx);

    printf("this is a c function\n");

    av_log(NULL, AV_LOG_DEBUG, "hello world from av_log \n ");

    return;
}


其中的 usleep 可以通过 man usleep 查看用法, 和引用的 c 头文件 #include <unistd.h>

仓库: gitee.com/dbafu/imooc…

记录App切后台时AppIcon变成默认雪花icon问题

xcode做新项目时,设置了app图标。发现点击app进入前台时,App Icon是正常的,但是回到桌面时App Icon又变成了默认的雪花图标。

之前也遇到过,但是不求甚解,在此列出解决方案。

问题1: AppIcon的设置

随便设置了个图片为app图标,编译报错xxx/Assets.xcassets: The stickers icon set or app icon set named "AppIcon" did not have any applicable content. 同时appIcon可视化窗口显示黄色⚠️图标。

Xcode 提示你在 Assets.xcassets 中名为 "AppIcon" 的 App 图标集合里没有提供任何有效的图片资源。

iOS 应用要求必须有完整的 AppIcon 集合,并且要包含适用于各种设备和分辨率的图标尺寸。如果没有正确设置这些图标,App 就无法通过 App Store 审核,甚至可能在某些模拟器或真机上运行异常。

我使用了makeappicon.com/ 生成appIcon图标。 网站生成的结果包含AppIcon.appiconset,直接把AppIcon.appiconset替换原项目中Assets中的appIcon即可。 image.png 结果如下 image.png

问题2: 切后台appIcon变成默认雪花icon

现在成功设置appIcon后,切后台时发现appIcon变成了默认的雪花icon。

原因是系统缓存了旧图标,iOS 系统有时会缓存应用的图标缩略图,尤其是多任务界面中的预览图。即使你更新了图标,也可能不会立即刷新。

解决办法: 卸载重装

现在能正常显示了

image.png

Swift 的 `@resultBuilder`;构建自己的HTML DSL解析器,实现简单JSX功能

🧠 1. 什么是 @resultBuilder

@resultBuilder 是 Swift 提供的一种 自定义 DSL(领域特定语言) 支持工具,允许你用类似“代码块”的语法构造复杂的值结构。
其广泛应用于 SwiftUI(如 @ViewBuilder)、字符串构建器、HTML DSL 等场景。

✅ 你可以将它理解为:

“让多个表达式拼接或组合为一个最终值的机制”。


🧱 2. 基本结构

swift
复制编辑
@resultBuilder
struct MyBuilder {
    static func buildBlock(_ components: T...) -> T
}

其中 T 是你最终组合出的类型,比如 StringView、数组等。


🧩 3. 常用构建方法(方法签名)

一个完整的 resultBuilder 可以实现以下静态方法,支持各种控制流结构:

方法 用途
buildBlock(_:) 拼接多个表达式的核心方法(必须)
buildExpression(_:) 将单个表达式转为构建器的中间值
buildOptional(_:) 支持 if letif 条件语句
buildEither(first:) / buildEither(second:) 支持 if-else 的两个分支
buildArray(_:) 支持 for-in 结构
buildLimitedAvailability(_:) 支持 #available 编译条件
buildFinalResult(_:) 可选:对最终返回值进行最后处理(Swift 5.9+)

🧪 4. 简单例子:拼接字符串

swift
复制编辑
@resultBuilder
struct StringBuilder {
    static func buildBlock(_ components: String...) -> String {
        components.joined()
    }
}

func makeString(@StringBuilder _ content: () -> String) -> String {
    content()
}

let result = makeString {
    "Hello, "
    "world!"
}
// 输出: "Hello, world!"

🪜 5. 进阶支持:if、for、可选等控制流

swift
复制编辑
@resultBuilder
struct StringBuilder {
    static func buildBlock(_ components: String...) -> String {
        components.joined()
    }

    static func buildOptional(_ component: String?) -> String {
        component ?? ""
    }

    static func buildEither(first: String) -> String {
        first
    }

    static func buildEither(second: String) -> String {
        second
    }

    static func buildArray(_ components: [String]) -> String {
        components.joined(separator: ", ")
    }
}

然后:

swift
复制编辑
makeString {
    "Hello"
    if Bool.random() {
        "🌞"
    } else {
        "🌧️"
    }
    for item in ["A", "B", "C"] {
        item
    }
}

🧮 6. 在 SwiftUI 中的应用:@ViewBuilder

swift
复制编辑
struct MyView: View {
    var body: some View {
        VStack {
            Text("Hello")
            if Bool.random() {
                Text("SwiftUI")
            } else {
                Text("Rocks!")
            }
        }
    }
}

SwiftUI 中的 VStack 使用了 @ViewBuilder,这使得你能用 if、for 等结构直接组合多个视图,而不是手动拼接。


🧑‍💻 7. 自定义 @resultBuilder 应用场景示例

使用场景 描述
@ViewBuilder SwiftUI 视图组合
@StringBuilder 构建字符串 DSL
@HTMLBuilder 构建 HTML DSL
@CommandBuilder 构建命令行工具链
@SQLBuilder 构建 SQL 语句(如:Swift ORM 框架)

📌 8. Swift 5.9 中的新功能(可选)

buildFinalResult(_:)

这个方法可以对构建器的结果做“最后一轮包装或转换”处理。适用于 Swift 5.9+。


举例,目标:构建 HTML DSL

swift
复制编辑
let html = makeHTML {
    HTML("html") {
        HTML("body") {
            HTML("h1") { "Welcome" }
            HTML("p") { "This is a custom DSL!" }
        }
    }
}

print(html.render())

🧱 第一步:定义 HTML 节点结构

swift
复制编辑
protocol HTMLComponent {
    func render() -> String
}

struct HTMLText: HTMLComponent {
    let text: String
    func render() -> String {
        text
    }
}

struct HTML: HTMLComponent {
    let tag: String
    let children: [HTMLComponent]

    init(_ tag: String, @HTMLBuilder _ content: () -> [HTMLComponent]) {
        self.tag = tag
        self.children = content()
    }

    func render() -> String {
        let inner = children.map { $0.render() }.joined()
        return "<(tag)>(inner)</(tag)>"
    }
}

🧙 第二步:定义 @HTMLBuilder

swift
复制编辑
@resultBuilder
struct HTMLBuilder {
    static func buildBlock(_ components: HTMLComponent...) -> [HTMLComponent] {
        components
    }

    static func buildOptional(_ component: [HTMLComponent]?) -> [HTMLComponent] {
        component ?? []
    }

    static func buildEither(first component: [HTMLComponent]) -> [HTMLComponent] {
        component
    }

    static func buildEither(second component: [HTMLComponent]) -> [HTMLComponent] {
        component
    }

    static func buildArray(_ components: [[HTMLComponent]]) -> [HTMLComponent] {
        components.flatMap { $0 }
    }

    static func buildExpression(_ expression: String) -> [HTMLComponent] {
        [HTMLText(text: expression)]
    }

    static func buildExpression(_ expression: HTMLComponent) -> [HTMLComponent] {
        [expression]
    }
}

🧪 第三步:封装顶层构造器

swift
复制编辑
func makeHTML(@HTMLBuilder content: () -> [HTMLComponent]) -> HTMLComponent {
    let result = content()
    return HTMLText(text: result.map { $0.render() }.joined(separator: "\n"))
}

📦 最终效果

swift
复制编辑
let html = makeHTML {
    HTML("html") {
        HTML("body") {
            HTML("h1") { "Hello Swift!" }
            HTML("p") {
                if Bool.random() {
                    "Dynamic paragraph!"
                } else {
                    "Static paragraph!"
                }
            }
        }
    }
}

print(html.render())

输出可能为:

html
复制编辑
<html><body><h1>Hello Swift!</h1><p>Static paragraph!</p></body></html>

📚 小结

特性 说明
灵活组合 像 DSL 一样用多个表达式组合出结构
控制流 支持 if/else、for、可选绑定等
编译期支持 编译器会在语法上理解这些结构
SwiftUI SwiftUI 的构建基石
可扩展性强 适用于构建任意复杂层级的数据

详解 JSExport:JavaScript 与 Objective-C 的通信桥梁

前言

在 iOS 开发中,JavaScriptCore 框架提供了强大的 JS 引擎,可以让我们在应用中运行 JavaScript 代码。而 JSExport 是这个框架中最重要的机制之一,它可以让我们将 Objective-C 的对象暴露给 JavaScript 调用,以使 JavaScript 可以像使用普通 JS 对象一样访问 Objective-C 的方法和属性。

我们先来了解下什么是 JSExport。

JSExport 是什么?

简单来说,JSExport 是一个协议,只要你定义的 Objective-C 协议继承自它,并让一个类遵循这个协议,就可以将该类的方法和属性导出为 JavaScript 可访问的接口

@protocol JSExport

了解完概念,来看下它在代码中是如何使用的。

基本使用步骤

使用 JSExport 通常需要以下几步:

  • 创建一个继承自 JSExport 的协议;
  • 将希望暴露的方法或属性声明在协议中;
  • 创建一个遵循该协议的 Objective-C 类;
  • 将该类的实例注入到 JSContext 中;
  • 在 JavaScript 中调用暴露出来的方法。

比如我们想把下面的代码暴露给 JS 调用,官方文档的示例代码如下:

@protocol MyPointExports <JSExport>
@property double x;
@property double y;
- (NSString *)description;
- (instancetype)initWithX:(double)x y:(double)y;
+ (MyPoint *)makePointWithX:(double)x y:(double)y;
@end
 
@interface MyPoint : NSObject <MyPointExports>
- (void)myPrivateMethod;  // This isn't in the MyPointExports protocol, so it isn't visible to JavaScript code.
@end
 
@implementation MyPoint

@end

我们来分类看一下如何将 OC 的代码暴露给 JS 调用:

暴露属性

示例代码如下:

// 在协议中声明
@protocol MyPointExports <JSExport>
@property double x;
@property double y;
@end

// 类中合成属性的 set get 方法
#import "MyPoint.h"

@implementation MyPoint

@synthesize x;
@synthesize y;

@end

// 在 JS 中调用暴露的属性
JSContext *context = [[JSContext alloc] init];

  
Calculator *calc = [[Calculator alloc] init];
context[@"calc"] = calc;

MyPoint *point = [MyPoint new];
point.x = 10.2;
point.y = 12.1;


context[@"point"] = point;

JSValue *xResult = [context evaluateScript:@"point.x"];
JSValue *yResult = [context evaluateScript:@"point.y"];
NSLog(@"X:%f, Y:%f", [xResult toDouble], [yResult toDouble]);

打印结果:X:10.200000, Y:12.100000。

暴露实例方法

示例代码如下:

// 在协议中声明
@protocol MyPointExports <JSExport>

- (NSString *)description;
- (instancetype)initWithX:(double)x y:(double)y;

@end
// 在类中实现
- (NSString *)description {
    return @"MyPoint";
}

- (instancetype)initWithX:(double)x y:(double)y {
    if (self = [super init]) {
        self.x = x;
        self.y = y;
    }
    return self;
}
// 在 JS 中调用暴露的方法
JSContext *context = [[JSContext alloc] init];
context[@"MyPoint"] = [MyPoint class];
JSValue *result = [context evaluateScript:@"new MyPoint(1, 2)"];
NSLog(@"X:%f, Y:%f", [result[@"x"] toDouble], [result[@"y"] toDouble]);

打印结果:X:1.000000, Y:2.000000。

暴露类方法

// 在协议中声明
@protocol MyPointExports <JSExport>

@property double x;
@property double y;

- (instancetype)initWithX:(double)x y:(double)y;

+ (MyPoint *)makePointWithX:(double)x y:(double)y;

@end
// 在类中实现

@implementation MyPoint

@synthesize x;
@synthesize y;

+ (MyPoint *)makePointWithX:(double)x y:(double)y {
    return [[MyPoint alloc] initWithX:x y:y];
}

- (instancetype)initWithX:(double)x y:(double)y {
    if (self = [super init]) {
        self.x = x;
        self.y = y;
    }
    return self;
}
@end
// 在 JS 中调用暴露的类方法
JSContext *context = [[JSContext alloc] init];
context[@"MyPoint"] = [MyPoint class];
JSValue *result = [context evaluateScript:@"MyPoint.makePointWithXY(3, 4)"];
NSLog(@"X:%f, Y:%f", [result[@"x"] toDouble], [result[@"y"] toDouble]);

打印结果:X:3.000000, Y:4.000000。

总结

JSExport 是 JavaScriptCore 框架中连接 Objective-C 与 JavaScript 的核心机制,正确的使用可以让你灵活地在原生与脚本之间切换逻辑,适用场景如下:

  • 配置型逻辑引擎;
  • 脚本化功能扩展;
  • 小程序平台;
  • 混合框架。

使用 JSExport 的关键步骤:

  • 明确协议继承 JSExport
  • 方法命名符合规则;
  • 类遵循协议;
  • 注入实例到 JSContext

通过这套机制,你可以让 JavaScript 像本地对象一样调用 Objective-C 类中的方法,极大提升了扩展性和灵活性。

Vibe Coding 一段时间后的感受

TL;DR

  • 工欲善其事,必先利其器:选对工具至关重要,目前 Claude Code (CC) 在代码理解和生成质量上相比 Cursor 表现更优。
  • 人机协同新范式:Vibe Coding 能极大提升开发效率,但需要掌握新的协作技巧,否则可能事倍功半。
  • 能力圈的重塑:AI Agent 的崛起正在改变传统的编程模式,迫使程序员更新自己的核心能力圈,将重心从具体实现转移到更高层次的架构设计和战略规划上。

最近,我通过 Vibe Coding 的方式完成了三个个人项目:

  • 五子棋:一个功能完善的网页五子棋游戏。
  • 笔画猜成语:一个根据汉字笔画顺序猜成语的互动游戏。
  • 歌曲转视频脚本:一个能自动为日文歌曲的歌词和单词注音并翻译,最终生成视频的学习工具。

在开发「五子棋」时,由于初次尝试 Vibe Coding,经验不足,踩了不少坑,但最终坚持了下来,算是摸到了一些门路。有了此前的经验,做「笔画猜成语」时更加得心应手,效率也大幅提升,大约只花了 3 天时间,就将一个模糊的想法变成了一个功能完整的 Web App。而「歌曲转视频」项目,更是在 2 个多小时内就产出了可运行的核心脚本。

Vibe Coding 工具的选择

我最初使用 Cursor,但在看到越来越多的人推荐 Claude Code(以下简称 CC)后,决定一试,结果确实令人惊喜。我的习惯是在 Cursor 的终端里打开 CC,这样 CC 可以直接访问当前项目的文件上下文(当然,在独立的终端里使用也完全可行)。

通过两个简单的例子就能看出 CC 与 Cursor 的能力差异:

  1. 重构组件属性:我让 Cursor 移除一个 React 组件中未使用的属性。它的做法是给这个属性一个别名,在前面加上下划线 _,以此「欺骗」编译器,绕过检查。当我追问该属性是否无用时,它才承认并将其删除,但却忽略了检查调用该组件的其他地方,工作只完成了一半。我撤销修改后,将同样的指令交给 CC,它一次性就完美地完成了任务,干净利落。

  2. 代码结构优化:在开发五子棋的禁手功能(黑棋不能走出三三、四四或长连)时,我有一个核心的协调文件 gomoku-ai.ts。Cursor 的做法是直接将禁手判断的复杂逻辑堆砌到这个协调文件中。而 CC 则展现了更优秀的设计能力,它创建了一个独立的 forbidden-move.ts 文件,专门用于封装所有禁手检测的逻辑,再由 gomoku-ai.ts 进行统一调度,使得代码结构更清晰,职责更分明。

类似的例子还有很多,这让我越来越依赖 CC。Cursor 更像一个初级程序员,优先考虑完成当前任务,对代码质量、复杂度和可维护性的考量不足,倾向于「能加代码就不改代码」。而 CC 在理解意图、分析代码结构以及构建更优代码方面表现更强。当然,它也并非完美,有时也会在「奋战」十分钟后报告任务完成,但却没有解决问题。

Vibe Coding 实用技巧

我喜欢将 Coding Agent 比作一匹马,这能强化我们对其「驾驭」和「驯服」的过程。不同的马有不同的脾性与能力(所以摸清每个 Agent 的能力圈很重要),选择一匹既强大又易于驾驭的马,才能事半功倍。以下是我总结的一些通用技巧:

1. 精准的目标分解

在开发五子棋 Web App 时,我犯了一个错误。在 v0.dev 上设计好页面后,就把整个实现任务直接丢给了 Agent,以为五子棋算法早已成熟,AI 肯定了然于胸,加上最终的目标界面,应该很快就能做完,结果却不尽人意。

我耐着性子,以产品经理和 QA 的身份不断提出修改意见。噩梦从此开始:AI 修复一个 Bug 的同时,往往会引入新的 Bug,或是让之前修复过的问题重现,陷入「按下葫芦起了瓢」的循环。

检查代码后我发现,AI(当时使用的还是 Cursor) 写的算法冗长复杂、职责不清,上百个测试用例中充斥着大量敷衍了事的断言(比如只检查返回值是不是布尔类型,而不关心是 true 还是 false)。基于这样的代码迭代无异于在沙地上盖楼。

最终,我决定放弃原有算法,从一个成熟的开源项目中剥离出经过验证的五子棋算法,然后让 CC 将其整合进现有项目——相当于给汽车换了个引擎,问题迎刃而解。

吸取教训后,在做「笔画猜成语」时,我首先对目标进行了细致的拆解。

如图所示,页面的核心是成语笔画展示区。这意味着我需要一个组件,它能接受一个汉字和对应的笔画序号,并使用 SVG 数据将其绘制出来。我的第一个子任务就是寻找一个合适的汉字笔画库,并让 Agent 基于这个库构建出核心组件。

任务分解还能带来意想不到的好处:掌控感、大局观和节奏感

  • 掌控感:每个任务都足够小,Review 起来更轻松,可以始终保持对代码的掌控。
  • 大局观:你清楚每个子任务在整个项目中的位置,对整体进度了如指掌。
  • 节奏感:一个接一个完成任务会带来持续的正反馈,推动项目稳步前进。

2. 提供关键输入(Input)和认真审查(Review)

Agent 并不总能选择最佳实现路径,如果不加干预,可能会在错误的方向上浪费大量时间和 Token。例如,在生成带注音和翻译的歌词图片时,我希望实现这样的效果:

CC 最初的方案是使用 sharp 配合 canvas 绘制,这不仅实现复杂,效果也未必理想。此时就需要人为干预,我给出的策略是:利用无头浏览器渲染一个包含 ruby 标签(HTML 原生支持注音)的页面,然后对该页面进行截图,这个方案更简单、效果也更好。

Review 同样至关重要,尤其是在商业项目中。在做「笔画猜成语」时,我要求每天提供一个固定的新成语。Agent 实现时用了一个简单的哈希算法,导致每天的成语都非常相近(如昨天是「坐享其成」,今天是「坐井观天」)。如果没 Review,这个问题可能只有上线后才能发现。我让它改进,它又手动实现了一个伪随机算法。我再次给出 Input,让它使用 seedrandom 这个成熟的库来确保高质量的伪随机性。

3. 及时干预

如果 CC 尝试了 5 次左右仍未完成任务,或者在执行一个明确的任务时消耗了大量时间却最终失败,就需要立即干预。这通常意味着任务粒度过粗、描述不清、AI 的策略有问题或者当前项目复杂度太高。 如果任务分解地很好,这种情况发生的概率不高,但一旦发生,很有可能会把之前节省下来的时间都还回去···。

4. 善用计划模式(Plan Mode)

CC 支持一个非常实用的「计划模式」。在执行一个有难度的任务前,可以让它先拟定一份行动计划。只有在你确认计划可行后,它才会开始实施,这极大地提高了复杂任务的成功率。

5. 拥抱软件开发的最佳实践

过去,我们常因耗时费力而忽略一些重要的开发实践,如编写测试或使用 Storybook。但现在,Agent 以其不知疲倦的特性,让这些「最佳实践」的落地变得轻而易举。测试驱动开发(TDD)等过去只存在于书本中的理念,现在完全可以在 Agent 的协助下融入日常开发流程。

6. Agent 的一些通病

  • 需求理解偏差:不太会主动确认需求,往往按自己的理解埋头就做。
  • 有「偷懒」倾向:为了完成任务可能会走捷径,例如引入 Magic Number、硬编码等。
  • 代码审美有限:对复杂度和代码整洁度的认知不足,尽管 CC 已比多数同类工具要好。

Vibe Coding 带来了什么

Someday -> Today

「日文歌曲转成带翻译的视频」这个 Project 我其实酝酿了蛮久的,如何实现也有大概的思路,但一想到要手动去实现里面的代码,要去熟悉 ffmpeg 的调用,还是会下意识地去拖延。但 CC 大大降低了我创建这个项目的阻力,虽然还是需要花一点时间描述清楚这个项目,对其进行分解,但相比自己去实现已经方便了很多。

我还记得当时是 10 点左右,我想测试下「笔画猜成语」在 0 点时能否正常切换到新一天的成语,既然还有两个小时,不如就来做这个项目吧,结果真的在 12 点多,就把它做出来了。

这便是 AI Agent 的魅力,它将那些看似遥不可及的「Someday」,变成了触手可及的「Today」。 那些曾经因为技术门槛、因为预想中的繁琐而搁置的想法,如今有了一个快速验证的通道。从一个模糊的概念到看得见、摸得着、能分享给朋友的作品。

程序员能力圈的重塑

这个访谈视频中,极限编程(XP)之父、《测试驱动开发》作者 Kent Beck,一位拥有 52 年编程经验的传奇程序员,分享了他对 AI 时代程序员能力转变的看法。他将 AI 编程工具比作一个「难以预测的精灵」 [06:44],它能实现你的愿望,但结果未必如你所想。

他的一段话精辟地总结了 Vibe Coding 「迫使」程序员做出的改变[01:12:42]:

I had this tweet two years ago where I said “90% of my skills just went to zero dollars and 10% of my skills just went up 1000x”. And this is exactly what I’m talking about - having a vision, being able to set milestones towards that vision, keeping track of a design to maintain or control the levels of complexity as you go forward. Those are hugely leveraged skills now compared to knowing where to put the ampersands and the stars and the brackets in Rust.

我两年前的一条推里说「我的 90% 的技能价值归零,而 10% 的技能价值增长了 1000 倍」。这正是我所说的——拥有一个愿景,能够为这个愿景设定里程碑,并在前进过程中跟踪设计以控制复杂性。与知道在 Rust 中如何放置符号和括号相比,这些才是现在被极度放大的核心技能。

这里的「迫使」是积极的。Agent 的出现,将程序员从繁琐的具体代码实现中解放出来,让我们能像 Kent Beck 所说的那样,去思考「真正宏大的想法」  [01:13:36]。我们的核心竞争力不再是写代码的速度或对语言语法的熟练度,而是产品感、架构能力、任务拆解能力和战略眼光

编程的本质正在回归——将复杂的问题,分解成计算机可以理解并执行的简单步骤。只不过这一次,我们操作的「计算机」拥有了更强的理解力和执行力。这要求我们站得更高,看得更远,专注于创造真正的价值。

Swift学习总结——常见数据类型

1 常量

swift中,可以使用关键字let来声明常量。我们通过以下几个关键点来认识常量。

1.1 只能赋值一次

常量只能被赋值一次,如果重复赋值,会报错。以下图为例: image.png

1.2 常量在初始化之前,不能使用

如下面的示例,对常量a进行了声明,并且是Int型,但是没有初始化,直接使用则会报错。同样,变量age也没有初始化,直接使用也会报错。 image.png

声明即赋值,或者先声明使用前再赋值。如下面的两个示例:

        // 声明即赋值
        let age = 28
        print(age)
        
        // 先声明,使用前赋值
        let height : Int
        height = 175
        print(height)

1.3 不要求在编译时期确定,但使用之前必须赋值一次

示例中age3是一个常量,并通过一个函数赋值,所以在编译期间并没有确定值,而是在执行期间确定。 image.png

1.4 常量需要确定数据类型

常量在声明后,如果没有确定数据类型,也没有赋值,会报错。 image.png

2 标识符

  1. 比如常量、变量、函数,几乎可以使用任何字符

    下面的示例中,使用一些特殊的图案,来对常量、变量、函数进行命名: image.png

  2. 标识符不能以数字开头,不能包含制表符、空白字符、箭头等特殊字符

    标识符依然会有一些限制要求,比如不能以数字开头,不能使用制表符、空格等特殊字符。同时,在编码过程中,为了更加易于理解、扩展和维护,我们还是要遵循一些编码规范:

    • 清晰性:命名清晰,功能清晰
    • 可读性:便于快速理解方法作用
    • 可维护性:减少因命名歧义引发的 Bug,后期代码微调简单
    • 一致性:团队协作风格统一

3 常见数据类型

3.1 Objective-C数据类型

首先回顾一下Objective-C的数据类型。

  1. 基本数据类型 Objective-C的基本数据类型与C语言类似,主要包括:
    • 整型int(通常占用4字节,表示范围为-2,147,483,648至2,147,483,647)、short(通常占用2字节,表示范围为-32,768至32,767)、long(通常占用4或8字节,取决于平台)、long long(至少占用8字节,表示范围更大)。
    • 浮点型float(单精度浮点数,通常占用4字节)、double(双精度浮点数,通常占用8字节)、long double(高精度浮点数,具体大小依赖于编译器实现)。
    • 字符型char(通常占用1字节,用于存储单个字符)。
    • 布尔型BOOL(用于表示真假值,通常占用1字节)。
  2. 对象数据类型 Objective-C的核心特性之一是面向对象的数据类型,比如Foundation框架中提供的一些复合型对象:
    • 字符串‌:如NSString(不可变字符串)和NSMutableString(可变字符串)。
    • 数组‌:如NSArray(不可变数组)和NSMutableArray(可变数组)。
    • 字典‌:如NSDictionary(不可变字典)和NSMutableDictionary(可变字典)。
    • 数字‌:如NSNumber
    • 数据‌:如NSData
  3. 扩展数据类型 Objective-C还支持一些扩展的数据类型,包括:
    • 指针类型‌:如int*float*NSObject*等。
    • 结构体类型‌:如CGRectCGSizeCGPoint等。
    • 枚举类型‌:如NSComparisonResultUITableViewStyle等。
    • 类型定义‌:通过typedef关键字定义自定义数据类型,如typedef enumtypedef struct等。
    • 其他类型‌:如NSUIntegerNSIntegerSEL等。

3.2 Swift数据类型

Swift 数据类型主要包括基本数据类型(如整数、浮点数、布尔值、字符和字符串)和复合数据类型(如数组、元组、可选类型等)。

  1. 基本数据类型‌ 如整数、浮点数、布尔值、字符和字符串:

    • 整数类型‌: Int:平台相关长度(32位系统为32位,64位系统为64位),建议默认使用以提高代码一致性。‌‌变体:Int8 Int16 Int32 Int64(有符号)和 UInt8 UInt16等(无符号),需显式指定。‌‌
    • 浮点数类型Float:32位单精度浮点数(约6位小数精度)。‌‌Double:64位双精度浮点数(约15位小数精度,推荐默认使用)。‌‌
    • 布尔类型Bool:仅包含 truefalse,用于逻辑判断。‌‌
    • 字符与字符串Character:单个Unicode字符(如 "A")。String:文本数据,支持插值和多行语法(如 "Hello")。‌‌
  2. 复合数据类型‌ 数组、元组、可选类型等:

    • 数组与字典Array:有序同类型集合。‌‌ Dictionary:键值对集合(如 ["key": "value"])。‌‌
    • 元组与可选类型Tuple:异构值组合(如 (1, "error"))。‌‌ Optional:表示值可能存在为 nil(如 Int?)。‌‌
  3. 引用类型 类(class

3.3 Swift数据类型的底层实现

我们知道Objective-C对象数据类型class类底层实现是结构体‌,这个结构体的定义在runtime源码中。比如Foundation库中提供的类型,如NSStringNSArray等,以及我们自定义的继承自NSObject的类,这些底层都是结构体。在iOS底层学习——OC对象的本质与isa中也有过探索。

Swift中,引用类型类(class)底层并不是结构体来实现的,但是一些基本数据类型如IntFloatDoubleCharacterString以及ArrayDictionary却是定义为结构体主要是因为它们作为值类型在性能和内存管理上具有优势。

下图对swift数据类型进行了归类: image.png

我们在开发工具上,也能够看到,Int是一个结构体: image.png

快速跳转到定义(Jump to Definition),Command + 点击鼠标左键快捷键,也能看到,源码中Int被定义成为一个public型的结构体。 image.png

我们知道结构体占用的内存空间,取决于结构体内部所有变量占用空间之和,这里提出一个疑问:将这些基本数据类型定义成结构体,难道不会增加复杂度,增加内存使用吗?其实swift内部做了优化,后面我们再深入探索。

4 部分数据类型的使用

4.1 整型类型

  • Swift提供了多种整型类型,如Int8Int16Int32Int64UInt8UInt16UInt32UInt64
  • Int8表示占8位,1个字节;UInt8U表示无符号。
  • 32bit平台,Int等价于Int3264bit平台,Int等价于Int64
  • 一般情况下,我们使用Int即可,除非对内存占用空间有强制性要求。

可以通过maxmin属性,了解数据类型对应的最大值和最小值。(注意这里的maxmin是属性,不是函数方法) image.png

4.2 浮点型

  • Float 32位,精度只有6位
  • Double 64位,精度至少15位
  • 初始化一个浮点型时,如果没有声明类型,默认是Double

image.png

4.3 不同进制表示方式

  • let intDecimal = 17 // 十进制
  • let intBinary = 0b10001 // 二进制
  • let intOctal = 0o21 // 八进制
  • let intHexadecimal = 0x11 // 十六进制

4.4 字符型

  • 字符型和字符串一样,使用双引号“”
  • 初始化一个字符型时,如果没有声明类型,默认是String
  • 字符可以存储ASCII字符Unicode字符

image.png

4.5 数组、字典

  • let array = ["a", "b", "c"]
  • let dic = ["a" : 12, "b" : 13, "c" : 21]
  • 字典类型也是使用[]

在原生的容器类型中,他们都是泛型的,也就是我们在一个集合中,只能放同一种类型的元素。 image.png

如果我们要把不相关的类型,放到同一个容器类型中的话,比较容易想到的是使用 AnyAnyObject,如下面的示例,再或者使用NSArrayNSDictionary

  • let array : [Any] = ["a", "b", "c", 1]
  • let dic : [String : Any] = ["a" : 12, "b" : "13"]

image.png

4.6 类型转换

  1. 整数转换

    如下图的示例中,age1age2虽然都是整型,但是其占用的存储空间是不同的,所以是不能直接相加的。 image.png

    因为Int8占用8位一个字节,而Int16占用两个字节,所以可以将age1转换为Int6,再相加,而age3也自动变成Int16型。 image.png

  2. 整数、浮点数转换

    如下图所示,整型和浮点型类型不批配,是不可以直接相加的: image.png

    可以将Int型转为浮点型,此时intp也为Double型: image.png

    但是下面这种方式是可以的,字面量可以直接相加,因为数字字面量本身没有明确的类型: image.png

4.7 元祖tuple

将多个不同数据类型组合赋予一个变量,可以通过序号来访问对应位置上的值image.png

可以将定义的元祖赋予一个元祖,对应位置上元素会自动赋值: image.png

如果元素不需要赋值,可以直接用_来代替: image.png

在定义元祖时,还可以给每个元素设置一个key: image.png

JavaScriptCore 入门

背景

在现在大前端的概念越来越重要的背景下,在开发 iOS 应用时,我们常常需要在应用中执行 JavaScript 代码,或者在原生代码和 JS 之间进行交互。Apple 提供的 JavaScriptCore 框架,可以让我们在不依赖 WebView 的前提下,直接在 Objective-C 或 Swift 中嵌入 JavaScript 引擎,执行 JS 代码、传值、调用函数,从而实现双向通信。

在本篇文章中,会主要介绍 JavaScriptCore 的基本使用方式、核心类、双侧之间的互相调用以及异常处理。

首先,我们先来了解下什么是 JavaScriptCore。

JavaScriptCore 是什么?

JavaScriptCore 是 Apple 提供的一个框架,它封装了 WebKit 中的 JavaScript 引擎。通过它我们可以实现下面的功能:

  • 在 Native 应用中直接执行 JavaScript 代码;
  • 将 Native 对象或者方法暴露给 JavaScript 使用;
  • 调用 JS 函数并获取返回值;
  • 捕获处理 JS 代码中触发的异常;

了解完什么是 JavaScriptCore 以及它的使用场景,下面来看下它的核心类。

JavaScriptCore 核心类介绍

JavaScriptCore 中最常用的几个类包括以下四个:

  • JSContext:表示一个 JS 执行上下文环境(沙箱)
  • JSValue:JS 中的值在 Objective-C 中的包装
  • JSExport:通过协议导出原生方法和属性给 JS 使用
  • JSVirtualMachine:表示一个虚拟机,可用于多个 JSContext

通过这些类,我们就可以实现 Native 代码和 JS 代码之间的互相调用。

了解完概念,下面就开始写代码了。

Objective-C 调用 JavaScript 方法

示例代码如下:

JSContext *context = [[JSContext alloc] init];

[context evaluateScript:
 @"function add(x, y) { return x + y; }"];

JSValue *addFunc = context[@"add"];
JSValue *result = [addFunc callWithArguments:@[@5, @7]];

NSLog(@"add(5, 7) = %@", [result toNumber]); // 输出 12

首先,我们创建一个 JSContext 类型的实例对象 context 用来表示 JS 执行上下文。接着调用 evaluateScript 方法传递进去一个字符串,该字符串的内容是一个 JS 函数 add,用来计算两个参数之和。然后创建一个 JSValue 类型的对象用来接收 context 中的 add 方法。最后调用 callWithArguments 方法将需要计算的数字传递进去并将结果返回给 JSValue 类型的实例对象 result。

这就是在 Objective-C 调用 JavaScript 方法的流程。

打印结果如下:

add(5, 7) = 12

接着,我们再来看下如何在 JavaScript 调用 Objective-C 方法。

JavaScript 调用 Objective-C 方法

在 JavaScript 调用 Objective-C 方法要比在 Objective-C 调用 JavaScript 方法稍微复杂一点。需要下面三步:

  • 定义一个协议继承自 JSExport,将需要 JS 调用的方法放在协议里;
  • 声明类并实现这个协议;
  • 将类的实例对象注册给 JSContext;

示例代码如下:

// 第一步:声明协议
#import <JavaScriptCore/JavaScriptCore.h>

@protocol CalculatorExport <JSExport>

- (NSInteger)addWithNum1:(NSInteger)num1 num2:(NSInteger)num2;

@end

// 第二步:实现协议
@interface Calculator : NSObject <CalculatorExport>
@end

@implementation Calculator

- (NSInteger)addWithNum1:(NSInteger)num1 num2:(NSInteger)num2 {
    return num1 + num2;
}

@end

//将实例对象注册给 JSContext

JSContext *context = [[JSContext alloc] init];

Calculator *calc = [[Calculator alloc] init];
context[@"calc"] = calc;

[context evaluateScript:@"var result = calc.addWithNum1Num2(3, 4);"];

NSLog(@"结果:%@", [context[@"result"] toNumber]);

输出结果如下:

结果:7

异常处理

在两侧联调开发时,不可避免的会出现代码方面的问题,这时候我们需要通过给 exceptionHandler 赋值,在回调中处理异常的场景。

示例代码如下:

JSContext *context = [[JSContext alloc] init];

context.exceptionHandler = ^(JSContext *ctx, JSValue *exception) {
    NSLog(@"JS 异常:%@", exception);
};

Calculator *calc = [[Calculator alloc] init];
context[@"calc"] = calc;

[context evaluateScript:@"var result = calc.sub(3, 4);"]; // 在 Native 侧,并没有导出 sub 方法

NSLog(@"结果:%@", [context[@"result"] toNumber]);

输出结果如下:

JS 异常:TypeError: calc.sub is not a function. (In 'calc.sub(3, 4)', 'calc.sub' is undefined)
结果:nan
❌