普通视图

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

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 ,避免不必要的问题。

参考资料

昨天 — 2025年4月6日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 直接访问子组件。根据应用的复杂性和需求,可以选择适合的方式来实现组件间的通信。在设计组件时,尽量保持组件的独立性和可复用性,避免过度耦合。

昨天以前iOS

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名

Flutter 运行新建项目也报错?

作者 yidahis
2025年4月3日 16:55

先看报错提示信息:

Error (Xcode): Target aot_assembly_release failed: Exception: release/profile builds are only supported for physical devices. attempted to
build for simulator.

环境

Mac 中,通过 flutter create daily_note新建一个项目,在VS Code 中添加 launch.json文件,内容如下:

{
    "configurations": [
    {
        "name": "daily_note (debug mode)",
        "request": "launch",
        "type": "dart"
    }
    ]
}

开始运行,结果出现开头的错误,非常的莫名其妙。

解决过程

首先运行 flutter doctor

[✓] Flutter (Channel stable, 3.29.0, on macOS 15.4 24E5228e darwin-arm64, locale zh-Hans-CN)
[!] Android toolchain - develop for Android devices (Android SDK version 35.0.1)
    ! Some Android licenses not accepted. To resolve this, run: flutter doctor --android-licenses
[✓] Xcode - develop for iOS and macOS (Xcode 16.0)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2024.1)
[✓] VS Code (version 1.98.2)
[!] Proxy Configuration
    ! NO_PROXY is not set
[✓] Connected device (4 available)
[✓] Network resources

! Doctor found issues in 2 categories.

iOS 上运行,所以 Xcode 没问题。

接着问 Trae,直接把报错信息和配置文件发给它。然后回复结果是让我增加一行,用于指定 flutterMode

{
    "configurations": [
    {
        "name": "daily_note (debug mode)",
        "request": "launch",
        "type": "dart",
        "flutterMode": "debug"
    }
    ]
}

运行还是同样的报错。于是继续把launch.json发给Trae,反复尝试了还是不行,有点快崩溃了。

回想之前遇到真机运行拔线就退出程序的问题,当时是通过设置FLUTTER_BUILD_MODE解决的,于是查看现在的新工程有没有这个设置。结果,真的没有。然后在 Xcode-Build Setting-User Defined中添加 FLUTTER_BUILD_MODE=Debug,重新运行后可以了。

总结

Trae很好,但不是万能的,有时候还得靠自己。想要掌握好新编程模式下的工具,还需要努力。

苹果 iOS 19 曝光,你的iPhone 还能再战一年?

作者 iOS阿玮
2025年4月3日 15:44

❝ App Store审核问题或3.2f咨询,请公众号留言。不免费,不闲聊。iOS研究院

前言

近期,关于苹果iOS 19的升级机型及其功能特性备受关注。据多方爆料和开发者信息透露,iOS 19将继续支持多款老款设备,但由于硬件性能的限制,部分重磅功能可能会受到限制。以下是iOS 19升级机型及核心功能的详细解析:

ae51f3deb48f8c5454e64d31cc890dfae0fe7f23.webp

一、支持机型

根据最新曝光的信息,iOS 19的升级机型列表与iOS 18基本保持一致,支持的设备范围,具体包括:

289f5637c5176d3512d754c7005c936b.jpg

二、iOS19

iOS 19的UI界面和图标、菜单等将全面向VisionOS靠拢。具体来说,原本棱角分明的APP图标将变成圆角设计,同时还会加入半透明效果。

fd039245d688d43f5605837440ea48140cf43b8f.webp

三、WWDC 25

苹果正式宣布,年度开发者大会将于2025年6月10日至14日(北京时间)举行。此次大会将延续自2020年以来的线上举办模式。苹果表示,WWDC 25将聚焦于Apple软件的最新发展,面向全球开发者免费开放,并提供与Apple专家交流的机会,让更多开发者深入了解新的框架、工具和功能等内容。

c75c10385343fbf22f387de68e8a508f67388fe7.webp

SwiftUI 字体系统详解

作者 Lexiaoyao20
2025年4月3日 12:31

在 iOS 应用开发中,文本排版是用户界面设计的关键元素之一。SwiftUI 提供了一套丰富而灵活的字体系统,让开发者能够轻松创建既美观又符合系统设计规范的文本样式。本文将深入探讨 SwiftUI 的字体系统,以及如何有效地使用它们来提升应用的用户体验。

SwiftUI 字体系统概述

SwiftUI 的字体系统主要分为三类:系统文本样式、自定义系统字体和自定义字体。这种分层设计使开发者既可以快速应用预设样式,又能在需要时进行精细的自定义。

系统文本样式(Text Style)

系统文本样式是 SwiftUI 提供的预定义字体样式,它们会根据用户在系统设置中的偏好自动调整大小,支持动态文字大小功能(Dynamic Type)。

Text("大标题").font(.largeTitle)
Text("标题").font(.title)
Text("正文内容").font(.body)
Text("图片说明").font(.caption)

以下是主要的系统文本样式及其用途:

样式 用途
.largeTitle 最大的标题样式,通常用于页面的主标题
.title 标准标题,用于重要区域的标题
.title2 二级标题,比 title 稍小
.title3 三级标题,比 title2 稍小
.headline 段落标题,通常使用粗体
.subheadline 副标题,比 headline 小
.body 正文样式,是默认的字体大小
.callout 标注文本,稍小于正文
.caption 说明文字,小字体,常用于图片说明或次要信息
.caption2 更小的说明文字
.footnote 脚注样式,比正文小
.preformatted 等宽字体,适用于代码显示(iOS 17+)

自定义系统字体

当预设样式无法满足需求时,可以使用 .system() 方法创建自定义系统字体:

// 基本自定义
Text("自定义大小").font(.system(size: 16))

// 自定义大小、粗细和设计风格
Text("圆角粗体").font(.system(size: 16, weight: .bold, design: .rounded))

字体权重选项

  • .ultraLight - 超细
  • .thin - 细体
  • .light - 轻体
  • .regular - 常规
  • .medium - 中等
  • .semibold - 半粗体
  • .bold - 粗体
  • .heavy - 重体
  • .black - 黑体

设计风格选项

  • .default - 默认系统字体
  • .serif - 衬线字体
  • .rounded - 圆角字体
  • .monospaced - 等宽字体
  • .italic - 斜体(iOS 16+)

自定义字体

对于品牌专属字体或特殊设计需求,SwiftUI 支持使用自定义字体:

// 基本自定义字体
Text("品牌专属字体").font(.custom("Brand Font", size: 14))

// 支持动态文字大小的自定义字体
Text("可调整大小的自定义字体").font(.custom("Brand Font", size: 14, relativeTo: .body))

使用 relativeTo 参数可以确保自定义字体也能响应系统的动态文字大小设置。

字体修饰符

除了设置整体字体样式,SwiftUI 还提供了单独修改字体特定属性的修饰符:

Text("粗体文本").fontWeight(.bold)
Text("斜体文本").italic()
Text("小型大写字母").textCase(.uppercase)
Text("下划线文本").underline()
Text("删除线文本").strikethrough()

最佳实践

  1. 优先使用系统文本样式:自动适应用户偏好设置,提升无障碍性
  2. 保持层次结构:使用不同字体样式建立清晰视觉层次
  3. 慎用自定义字体:确保支持动态文字大小
  4. 考虑本地化:不同语言可能需要不同的文本空间
  5. 测试极端情况:在最大和最小文字大小设置下测试界面

Swift 6.1 新特性

作者 YungFan
2025年4月3日 11:50

Swift 6.1 内置于 Xcode 16.3,这是 Swift 6 之后的首个小版本更新,新特性很少。

尾随逗号

元组、函数的参数、闭包捕获列表以及字符串插值等都可以像数组一样,在最后一个元素的后面添加,,以便轻松地追加、删除、重新排列或者注释最后一个元素。

// 元组
(404, "Not Found",)
// 函数的参数
func sum(num1: Int, num2: Int,) -> Int {
    num1 + num2
}
var vehicle = "Car"
// 闭包捕获列表
let closure = { [vehicle,] in
    print("Vehicle:", vehicle)
}
// 字符串插值
"This is a \(vehicle,)"

混合开发

介绍

  • 增加新的关键字@implementation,配合@objc可以为 Objective-C 导入的.h声明提供实现。
  • 实现方式:在 Swift 中扩展 Objective-C 类,然后通过@objc @implementation实现属性与方法以替换 Objective-C 的@implementation

实现

  1. 新建一个基于 Swift 语言的 iOS 项目。
  2. 创建一个 Objective-C 类,此时会弹出一个提醒对话框(添加这个文件会创建一个 Objective-C 与 Swift 的混合项目,你是否希望 Xcode 自动配置一个桥接头文件来让 2 种语言的类文件相互可见?),点击Create Bridging Header
  3. 项目中多出 3 个文件,分别为创建的 Objective-C 类文件(.h.m)与 Bridging Header 文件,修改 Objective-C 类文件如下。
// .h文件
@interface Person: NSObject

@property(nonatomic, copy) NSString *name; 
-(void)eat;

@end


// .m文件
// @implementation Person
// @end
  1. 在 Bridging Header 文件中通过#import "类名.h"导入所有需要使用的 Objective-C 类的头文件。
  2. 在 Swift 中实现并且调用 Objective-C。
import UIKit

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        // 调用
        let person = Person()
        person.name = "zhangsan"
        person.eat() // zhangsan吃饭了
    }
}

// MARK: - @objc @implementation extension Objective-C类
@objc @implementation extension Person {
    // 实现属性
    var name: String?

    // 实现方法
    func eat() {
        print("\(name ?? "")吃饭了")
    }
}

注意:一旦在 Swift 进行了实现,Objective-C 中的@implementation不需要再实现,否则会报2 duplicate symbols的编译错误。

并发编程

  • actor 允许在属性与函数上使用nonisolated,表示该 API 可以在任何并发上下文中安全调用。Swift 6.1 将nonisolated支持到了类型与扩展,这样其内的所有属性与方法不需要再单独添加nonisolated
// 结构体
nonisolated struct Station {
}

class Train {
}
// 扩展
nonisolated extension Train {
}
  • withTaskGroup()withThrowingTaskGroup()的闭包可以推断出子任务结果类型。
// Swift6.1之前
await withTaskGroup(of: Int.self) { group in
    ...
}
await withThrowingTaskGroup(of: String.self) { group in
    ...
}
// Swift6.1之后
await withTaskGroup { group in
    ...
}
await withThrowingTaskGroup { group in
    ...
}

Combine知识点switchToLatest

2025年4月3日 09:58

使用场景

  1. 搜索框自动补全(避免发送多个请求,只保留最后一个)
  2. 异步任务切换(如下载任务,视频播放切换,当用户连续点击了多个视频时,只播放最后一个,取消前几个的请求)
  3. 网络请求取消(当用户发起新的请求时,自动取消旧的请求)
  4. 用户交互事件 (点击按钮触发任务,只执行最新的点击任务)

switchToLatest解决的核心问题是:

  1. 自动取消旧的Publisher,只保留最新的Publisher
  2. 适用于“每次订阅都会创建新的Publisher”的情况

switchToLatest的定义

func switchToLatest()-> Publishers.SwitchToLatest<Upstream.Output, Upstream>

这里的Upstream指的是Publisher内部产生的Publisher,switchToLatest会从中只取最新的Publisher。

核心特点

  1. 适合于Publisher<Publisher<Output, Failure>, Failure>,即一个Publisher产生多个Publisher的情况
  2. 当新的Publiser出现时,会自动取消之前的Publisher,只执行最新的publisher。
  3. 只会订阅最新的Publiser并将其结果发送给下游

例1,防止重复的网络请求

场景

用户在搜索框中输入关键词时,每次输入都会触发新的API请求,我门希望只进行最新的一次请求,之前的取消。

mport Combine
import Foundation
// 模拟搜索 API
func searchAPI(query: String) -> AnyPublisher<String, Never> {
    Just("搜索结果: \(query)")
        .delay(for: .seconds(1), scheduler: DispatchQueue.global())  // 模拟网络请求延迟
        .eraseToAnyPublisher()
}

// 用户输入的搜索关键词
let searchText = PassthroughSubject<String, Never>()
// 处理搜索请求
let cancellable = searchText
    .map { query in searchAPI(query: query) }  // 每次输入都会生成一个新的 Publisher
    .switchToLatest()  // 只保留最新的 Publisher,取消旧的请求
    .sink { result in
        print(result)  // 只会输出最新搜索的结果
    }
// 模拟用户输入
searchText.send("Swift")    // 请求 1
DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) {
    searchText.send("SwiftUI"// 请求 2,取消 "Swift"
}
DispatchQueue.global().asyncAfter(deadline: .now() + 1.5) {
    searchText.send("Combine"// 请求 3,取消 "SwiftUI"
}

// 输出结果
“搜索结果: Combine

代码解析

  1. searchText是一个PassthroughSubject,用于模拟用户输入的关键词
  2. map { query in searchAPI(query: query)} 每次输入都会调用searchAPI生成新的Publisher(异步网络请求)
  3. swiftToLatest() 只保留最新的Publisher,如果新的请求到来,会取消旧的请求
  4. slink订阅最终的结果,只会收到最新的搜索返回的值
例2, 点击按钮触发异步任务
场景:

用户点击下载按钮时,每次点击都睡触发新的下载任务,我们希望只有最后一次的任务执行,之前的任务自动取消。

import Combine
import Foundation  

// 模拟下载任务
func downloadTask(_ id: Int) -> AnyPublisher<String, Never> {
    Just("任务 \(id) 下载完成")
        .delay(for: .seconds(2), scheduler: DispatchQueue.global())  // 模拟下载延迟
        .eraseToAnyPublisher()
}
// 用户点击按钮的触发器
let downloadButtonTap = PassthroughSubject<Int, Never>()
// 处理下载任务
let cancellable = downloadButtonTap
    .map { id in downloadTask(id) }  // 每次点击都会生成新的下载任务
    .switchToLatest()  // 只执行最新的任务,旧任务会被取消
    .sink { result in
        print(result)  // 只会输出最新任务的完成结果
    }
// 模拟用户点击
downloadButtonTap.send(1// 任务 1
DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
    downloadButtonTap.send(2// 任务 2,取消任务 1
}
DispatchQueue.global().asyncAfter(deadline: .now() + 3) {
    downloadButtonTap.send(3// 任务 3,取消任务 2
}

// 输出
// 任务3 下载完成

任务1 和 2 在完成之前被取消,所以他们的slink不会接收到任何输出

得物 iOS 启动优化之 Building Closure

作者 得物技术
2025年4月3日 09:50

得物一直重视用户体验,尤其是启动时长这一重要指标。在近期的启动时长跟进中,我们发现了在BuildingClosure 阶段的一个优化方式,成功的帮助我们降低了 1/5 的 BuildingClosure 阶段的启动耗时。Building Closure 并非工程的编译阶段(虽然它有一个building),Building Closure 是应用初次启动时会经历的阶段,因此它会影响应用的启动时长。

单就BuildingClosure阶段而言,我们观察到该阶段其中一个函数从 480ms 暴增到 1200ms 左右(PC 电脑端运行 dyld 调试统计耗时数据),我们通过优化,将耗时从1200ms降低到110ms。即使相比最开始的情况,也相当于从480ms降低到了110ms,由此可见Building Closure 优化是应用进行启动优化必不可少的一个重要手段。因此在这里我们也和各位读者进行分享,期望能够对各自项目有所帮助。

一、神秘的 BuildingClosure

启动优化的技术、实现方案业界有不少的文章可以参考学习,这里不再额外赘述。我们来探索下启动过程中非常神秘的 BuildingClosure。

BuildingClosure 是在 System Interface Initialization 阶段 dyld 生成的,并且我们也无法做任何的干预,另外相关的剖析文章相对较少,所以说 BuildingClosure 较为神秘,也是实至名归。

BuildingClosure 是由 dyld 在应用启动阶段执行的,所以想要了解 BuildingClosure 还是要从 dyld 开始了解。

1.1 dyld && BuildingClosure

Dyld 源码可以在 Apple GitHub 上查阅 github.com/apple-oss-d…

相信大家都应该了解过,BuildingClosure 是在 iOS 13 引入进来的,对应的 dyld 为 dyld3,目的是为了减少启动环节符号查找、Rebase、Bind 的耗时。

核心技术逻辑是将重复的启动工作只做一次,在 App 首次启动、版本更新、手机重启之后的这次启动过程中,将相关信息缓存到 Library/Caches/com.app.dyld/xx.dyld 文件中,App 在下次启动时直接使用缓存好的信息,进而优化二次启动的速度。

在 iOS 15 Dyld4 中更是引入了 SwiftConformance,进一步解决了运行时 Swift 中的类型、协议检查的耗时。

图片

以上优化,我们都无需做任何工作即可享受 dyld 带来的启动速度的优化,可以感受到 Apple 的开发人员也在关心启动速度并为之做了大量的工作。

1.2 BuildingClosure 非常耗时

我们通过 instrument 观测到 BuildingClosure 的耗时占据了启动耗时将近 1/3 的时间。

虽然说,BuildingClosure 只会在首次启动、版本更新、手机重启的第一次启动生成和耗时,但是对用户的体验影响是非常之大的。

图片

1.3 BuildingClosure 文件解析

我们通过对 dyld 的编译和搭建模拟手机环境,成功模拟器了 dyld 加载可执行文件的过程,也就成功解析了 BuildingClosure 文件。BuildingClosure 文件数据格式如下(数据格式、注释仅供参考,并非全部的数据格式):

图片

BuildingClosure 文件内部结构(数据格式、注释仅供参考)

其中占用比较大的部分主要为 Loader-selectorReferencesFixupsSize SwiftTypeConformance  objcSelector objcClass

二、离奇的启动耗时暴增事件

如上,我们已经对 BuildingClosure 有了基本的了解和对 dyld 的执行过程有了一定的了解。但是这份宁静在某一天突然被打破。

2.1 启动耗时暴增 200ms

在我们一个新版本开发过程中,例行对启动耗时进行跟踪测试,但是发现新版本启动耗时暴增 200ms,可以说是灾难级别的事情。

我们开始对最近的出包做了基本的耗时统计,方式为基于 instrument,统计出来启动各个阶段的耗时数据。经过对比,可以明显观测到,200ms 耗时的增加表现在 BuildingClosure 这个环节。

但是 BuildingClosure 耗时的增加既不是阶梯式增加,也不是线性增加,并且只在新版本有增加。在排除相关因素(动态库、工程配置、打包脚本、编译环境)之后,仍然没有定位明确的原因。

在以上定位工作之后,最终确定耗时确实在 dyld 的 BuildingClosure 阶段耗时,并且怀疑可能是某些代码触发了 Dyld 的隐藏彩蛋。所以我们开始了对 BuildingClosure 更深一步的研究。

2.2 BuildingClosure 耗时异常变化定位

通过使用 Instrument 对 System Interface Initialization 阶段进行堆栈分析,最终发现了耗时最高的函数:dyld4::PrebuiltObjC::generateHashTables(dyld4::RuntimeState&)

在对比了新老版本数据,耗时变化差异的函数也是此函数,我们简称为 generateHashTables。这样使得我们更加确定耗时为 dyld 过程中的 BuildingClosure 阶段。

图片

使用 Instrument 分析 BuildingClosure 阶段耗时

三、启动优化新秘境

在发现 BuildingClosure 生成过程中耗时占比非常大,并且有异常时,起初并没有意识到有什么问题,因为这是 dyld 内的代码,并未感觉会有什么问题。但是一切都指向了该函数,于是开始撸起袖子看代码。

从代码中可以看到,此处是为了生成 BuildingClosure 中 objcSelector objcClass objcProtocol 这三个部分的 HashTable(可以参考上面的 【BuildingClosure 文件解析】部分)。

拿起 dyld 开始对耗时异常版本的可执行文件进行调试,通过对该函数和内部实现的代码逻辑阅读,以及增加耗时信息打印。最终确定,耗时的代码在 make_perfect 这个函数中,这个函数是对【输入的字符串列表】生成一个【完美 Hash 表】。

void PrebuiltObjC::generateHashTables(RuntimeState& state)
{
    // Write out the class table
    writeObjCDataStructHashTable(state, PrebuiltObjC::ObjCStructKind::classes, objcImages, classesHashTable, duplicateSharedCacheClassMap, classMap);
    // Write out the protocol table
    writeObjCDataStructHashTable(state, PrebuiltObjC::ObjCStructKind::protocols, objcImages, protocolsHashTable, duplicateSharedCacheClassMap, protocolMap);
    // If we have closure selectors, we need to make a hash table for them.
    if ( !closureSelectorStrings.empty() ) {
        objc::PerfectHash phash;
        objc::PerfectHash::make_perfect(closureSelectorStrings, phash);
        size_t size = ObjCStringTable::size(phash);
        selectorsHashTable.resize(size);
        //printf("Selector table size: %lld\n", size);
        selectorStringTable = (ObjCStringTable*)selectorsHashTable.begin();
        selectorStringTable->write(phash, closureSelectorMap.array());
    }
}

继续深入了解 make_perfect 这个函数的实现。

3.1 Perfect Hash

通过对研读代码逻辑和耗时分析,最终定位到耗时代码部分为PerfectHash.cpp 中 findhash 函数,这个函数也是 完美散列函数 的核心逻辑。

这里涉及到了一个概念PerfectHash,PerfectHash 的核心是完美散列函数,我们看下维基百科的解释:

zh.wikipedia.org/wiki/%E5%AE…

对集合S的完美散列函数是一个将S的每个元素映射到一系列无冲突的整数的哈希函数

简单来讲 完美散列函数 是【对输入的字符串列表】【为每个字符串生成一个唯一整数】。

for (si=1; ; ++si)
    {
        ub4 rslinit;
        /* Try to find distinct (A,B) for all keys */
        *salt = si * 0x9e3779b97f4a7c13LL; /* golden ratio (arbitrary value) */
        initnorm(keys, *alen, blen, smax, *salt);
        rslinit = inittab(tabb, keys, FALSE);
        if (rslinit == 0)
        {
            /* didn't find distinct (a,b) */
            if (++bad_initkey >= RETRY_INITKEY)
            {
                /* Try to put more bits in (A,B) to make distinct (A,B) more likely */
                if (*alen < maxalen)
                {
                    *alen *= 2;
                }
                else if (blen < smax)
                {
                    blen *= 2;
                    tabb.resize(blen);
                    tabq.resize(blen+1);
                }
                bad_initkey0;
                bad_perfect0;
            }
            continue;                             /* two keys have same (a,b) pair */
        }
        /* Given distinct (A,B) for all keys, build a perfect hash */
        if (!perfect(tabb, tabh, tabq, smax, scramble, (ub4)keys.count()))
        {
            if (++bad_perfect >= RETRY_PERFECT)
            {
                if (blen < smax)
                {
                    blen *= 2;
                    tabb.resize(blen);
                    tabq.resize(blen+1);
                    --si;               /* we know this salt got distinct (A,B) */
                }
                else
                {
                    return false;
                }
                bad_perfect0;
            }
            continue;
        }
        break;
    }

此时通过对比新老版本的数据(使用 dyld 分别运行新老版本的可执行文件对比打印的日志),发现:

  • 老版本循环了 31 次成功生成 HashTable

  • 新版本循环了 92 次成功生成 HashTable

至此,我们距离成功已经非常接近了,于是进一步研读 dyld 源码和增加了更多打印信息代码,最终找到了相互冲突的函数字符串名称。

/*
 * put keys in tabb according to key->b_k
 * check if the initial hash might work
 */
static int inittab_ts(dyld3::OverflowSafeArray<bstuff>& tabb, dyld3::OverflowSafeArray<key>& keys, int complete, int si)
// bstuff   *tabb;                     /* output, list of keys with b for (a,b) */
// ub4       blen;                                            /* length of tabb */
// key      *keys;                               /* list of keys already hashed */
// int       complete;        /* TRUE means to complete init despite collisions */
{
  int  nocollision = TRUE;
  ub4 i;
  memset((void *)tabb.begin(), 0, (size_t)(sizeof(bstuff)*tabb.maxCount()));
  /* Two keys with the same (a,b) guarantees a collision */
  for (i0; i < keys.count(); i++) {
    key *mykey = &keys[i];
    key *otherkey;
    for (otherkey=tabb[mykey->b_k].list_b;
     otherkey;
     otherkey=otherkey->nextb_k)
    {
      if (mykey->a_k == otherkey->a_k)
      {
          // 打印冲突的字符串
        std::cout << mykey->name_k << " and " << otherkey->name_k << " has the same ak " << otherkey->a_k << " si is " << si << std::endl;
        nocollision = FALSE;
          /* 屏蔽此处代码,有冲突的情况下,继续执行,便于打印所有的冲突
    if (!complete)
      return FALSE;
           */
      }
    }
    ++tabb[mykey->b_k].listlen_b;
    mykey->nextb_k = tabb[mykey->b_k].list_b;
    tabb[mykey->b_k].list_b = mykey;
  }
  /* no two keys have the same (a,b) pair */
  return nocollision;
}

根据以上信息,我们已经了解到在Building Closure阶段中,可能存在字符串的 Hash 碰撞 引发循环次数大幅增加,进而引发了启动耗时暴增。

在经过 dyld 调试的耗时数据、构建出包后验证的数据验证后,通过避免 Hash 碰撞,我们完成了启动时长的优化。

3.2 向前一步

其实从打印的冲突函数名称来看,历史代码中已经存在了 Hash 碰撞 的现象。

猜想,如果我们解决了所有的字符串的 Hash 碰撞,岂不是不仅可以修复启动耗时异常上升的问题,还可以进一步降低启动耗时,提高启动速度?

于是我们对每个有碰撞的函数名称进行修改,经过出包验证,结果与我们猜测的一致,启动耗时有明显的下降。

图片

数据为 PC 电脑端运行 dyld 生成 BuildingClosure 的耗时数据,非手机端数据

四、总结

我们探索了 BuildingClosure 的生成过程,发现在Building Closure阶段中,可能存在字符串的 Hash 碰撞 引发循环次数大幅增加,进而引发了启动耗时暴增,进而导致启动耗时的大幅增加。

我们也发现,Building Closure Hash碰撞相关的启动耗时,其实与项目配置、编译环境、打包脚本等均无任何关系,就只是存在了字符串的Hash 碰撞 ,才引发循环次数大幅增加,进而导致启动时长增加。

往期回顾

1.分布式数据一致性场景与方案处理分析|得物技术

2.从对话到自主行动:AI应用如何从 Chat 进化为 Agent?开源项目源码深度揭秘|得物技术

3.得物技术部算法项目管理实践分享

4.商家域稳定性建设之原理探索|得物技术

5.得物 Android Crash 治理实践

文 / 道隐

关注得物技术,每周一、三更新技术干货

要是觉得文章对你有帮助的话,欢迎评论转发点赞~

未经得物技术许可严禁转载,否则依法追究法律责任。

Tagged Pointer:苹果工程师的内存优化艺术

作者 布多
2025年4月2日 20:54

Tagged Pointer 的前世今生

在 iOS 开发的早期阶段,所有对象都采用传统的堆内存存储方式。每个对象指针都指向堆内存中的一块区域,这块区域不仅存储着对象的实际数据,还包含了类型信息、引用计数等元数据。这种存储方式虽然通用且灵活,但对于一些简单的数据类型(如 NSNumber 存储的小整数 10、200 等),却显得有些"大材小用"。试想,仅仅为了存储一个小整数,就需要在堆上分配内存、维护引用计数、进行内存回收,这不仅造成了内存空间的浪费,还会因为频繁的内存操作而影响系统性能。

为了解决这个问题,苹果的工程师们提出了 Tagged Pointer 技术。这项技术的核心思想是:对于一些小型数据,将其直接编码到指针中,而不是在堆上分配内存。这种巧妙的设计不仅节省了宝贵的内存空间,还通过减少内存分配和释放操作显著提升了性能。

这种创新的效果是立竿见影的。根据 WWDC2013 苹果官方发布的数据显示,采用 Tagged Pointer 技术后,相关操作获得了显著的性能提升:内存占用直接减少了 50%,数据访问速度提升了 3 倍,而对象的创建和销毁速度更是实现了惊人的 100 倍提升。这些数据充分证明了 Tagged Pointer 技术在内存优化和性能提升方面的巨大价值。

技术原理深度解析

虽然 Tagged Pointer 技术在 iOS 中被广泛应用于 NSNumber、NSDate、NSString 等多个对象类型,但 NSNumber 是最具代表性的例子。因此,本文将以 NSNumber 为例深入讲解其实现原理。其它对象的实现机制与 NSNumber 类似,读者可以举一反三。

值得注意的是,在现代 Xcode 版本中(具体从哪个版本开始不太确定),苹果为了增强数据安全性,对 Tagged Pointer 进行了数据混淆处理。这种混淆机制使得开发者无法通过直接打印指针来判断一个对象是否为 Tagged Pointer,也无法解析其中存储的具体数据。

为了便于调试和观察 Tagged Pointer 的底层实现,我们需要关闭这个数据混淆功能。只需要将环境变量 OBJC_DISABLE_TAG_OBFUSCATION 设置为 YES 即可。

operator.png

由于 NSNumber 的源码未开源,所以我们通过分析结果来了解 Tagged Pointer 的实现原理。以下是我的调试结果:

result1.png

通过分析打印结果,我们可以看到 Tagged Pointer 的精妙设计:它将数据直接编码在指针中,而不是在堆内存中分配空间。具体来说:

  1. 指针的第 6 位到第 60 位(从左往右数)用于存储实际数据。这意味着一个 NSNumber 对象最多可以存储 55 位的数据,即 0x7FFFFFFFFFFFFF,对应的十进制为 36028797018963967。

  2. 指针的其余位则用于存储元数据:

    • 第 1 位(最高位)作为 Tag 标记,用于标识这是一个 Tagged Pointer;
    • 第 2-5 位用于存储对象类型(如 NSNumber 为 3,NSString 为 2);
    • 最后 4 位用于存储数据类型(如 int 为 2,long 为 3)。

Tagged Pointer 的内存分布图如下所示:

TaggedPointer.png

需要注意的是,不同平台和系统版本下,Tagged Pointer 的实现细节可能有所不同。例如,在 iOS 模拟器和真机环境中,Tag 位和数据位的存储位置就存在差异:模拟器中 Tag 位在最低位,而真机中 Tag 位在最高位。这种差异主要是由于不同平台的内存对齐要求和处理器架构特性导致的。因此,在调试过程中如果发现结果与预期不符,可以看看是不是平台或系统版本的问题。

这种设计巧妙地利用了 64 位系统的特性。在现代操作系统中,由于虚拟内存地址空间的限制,64 位指针实际上只使用了最低的 47 位来寻址,这意味着在正常情况下,合法的内存地址最高位一定是 0。Tagged Pointer 正是巧妙利用了这一特点,通过将最高位设为 1 来标识这是一个特殊的指针。Tagged Pointer 还将这些 "空闲" 的位进行了重新规划利用:一部分用于存储类型标记,另一部分用于直接存储数据。这样,对于小型数据(如小整数、短字符串等)就不再需要额外分配堆内存,而是直接将数据编码在指针中。这种设计不仅完美地保证了数据的完整性和访问效率,还通过消除堆内存分配、引用计数管理等开销,实现了内存使用的极致优化。

虽然 NSNumber 的源码未开源,但我通过深入分析 objc_runtime 源码,还是找到了一些关键的入口函数。这些函数揭示了 Tagged Pointer 的底层运作机制,包括数据混淆(其实就是启动的时候生成一个随机数,然后进行异或 ^ 运算)以及 Tag 和 Data 的存储位置等核心实现细节。以下是几个比较重要的函数,碍于篇幅这里就不展开了,感兴趣的同学可以下载 runtime 自行研究,它们共同构成了 Tagged Pointer 的基础框架:

isTaggedPointer => 判断是否是 Tagged Pointer
_objc_makeTaggedPointer => 生成 Tagged Pointer
_objc_getTaggedPointerTag => 获取 Tag
_objc_getTaggedPointerValue => 获取 Value
initializeTaggedPointerObfuscator => runtime 启动时初始化 Tagged Pointer 混淆器

Tagged Pointer 的类型编码

在 Tagged Pointer 中,系统使用特定的位来编码对象类型和数据类型。以下是详细的编码对照表:

对象类型编码

对象类型编码存储在指针的第 2-5 位。

对象类型
2 NSString
3 NSNumber
4 NSIndexPath
5 NSManagedObjectID
6 NSDate

数据类型编码

数据类型编码存储在指针的最后4位。

数据类型
0 char
1 short
2 int
3 long
4 float
5 double
6 long long

判断 Tagged Pointer 的原理

系统是如何判断一个指针是否是 Tagged Pointer 的呢?其实,在 objc4-906 源码中,我们可以找到 isTaggedPointer 的实现细节。

bool isTaggedPointer() {
    return _objc_isTaggedPointer(this);
}

#define _OBJC_TAG_MASK (1UL<<63)
static bool _objc_isTaggedPointer(void *ptr) {
    return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}

从源码中可以看到,系统通过位运算 & 操作获取指针的最高位,并与 _OBJC_TAG_MASK(1UL<<63,即最高位为1的掩码)进行比较。如果结果等于 _OBJC_TAG_MASK,就表示这个指针是一个 Tagged Pointer(简单的说就是判断指针的第一位是否为1)。这种判断方式简单高效,只需要一次位运算就能完成判断。

注意事项

虽然我们一般不需要关心 Tagged Pointer 的具体实现细节,但在使用过程中还是有一些需要注意的地方:

  • Tagged Pointer 不是传统意义上的 ObjC 对象,它没有常规对象所具有的 isa 指针。因此,不要尝试直接访问或操作其 isa 指针,应该始终通过系统提供的公开接口来操作这些对象。

  • 出于安全考虑,在现代 Xcode 版本中,苹果对 Tagged Pointer 实现了数据混淆机制。这意味着即使你通过某种方式获取到了指针的原始值,也无法直接解析出其中存储的数据。正确的做法是使用框架提供的标准方法来访问数据。

  • Tagged Pointer 的具体实现细节可能会随着系统版本的更新而改变。因此,不要在代码中依赖其当前的实现方式(如位编码规则),这可能会导致你的应用在未来的系统版本中出现兼容性问题。

  • 在进行内存调试或性能分析时,要注意 Tagged Pointer 对象的特殊性。它们不会出现在常规的内存分配统计中,因为它们实际上并不占用堆内存。

关于 Tagged Pointer 有一个面试题,碍于篇幅,这里就不展开了,感兴趣的可以看一下 老生常谈内存管理 中的 “相关题目” 章节。

参考资料

WASM I/O 2025 | MoonBit获Kotlin核心开发,Golem Cloud CEO高度评价

作者 MoonBit
2025年4月2日 18:48

图片

图片

2025 年 3 月 27 日至 28 日,为期 2 日的 WASM I/O 2025 大会在巴塞罗那圆满落幕。期间,众多 WASM 社区领袖和技术专家,共同带来了 30 余场精彩的演讲,共同探讨云原生和开源领域的前沿洞察、核心技术与最佳实践。作为一门专为 WebAssembly 优化的新兴编程语言,MoonBit 作为首个中国开发者平台受邀在本次大会上进行技术分享。

主题演讲

在本次活动中,MoonBit 负责人张宏波带来了《MoonBit & WebAssembly》主题演讲。作为一门专为 WebAssembly(Wasm)设计的编程语言,MoonBit 凭借其极致的性能优化、创新的语法设计及开发者友好的工具链,展示了在浏览器与服务器端的全栈潜力。以下是本次演讲的核心技术回顾。图片

张宏波开宗明义地指出,现有语言在 Wasm 生态中存在三大痛点:二进制体积臃肿、编译速度缓慢以及运行时性能不足。MoonBit从语言设计之初便围绕Wasm优化,实现了极简二进制体积、闪电编译速度、高性能运行时的突破。

图片

MoonBit在设计时非常重视开发者体验,在工具链设计上体现了 “开发者优先” 的理念。其云 IDE 无需本地安装,集成并行类型检查、实时错误提示及基于 ChatGPT 的代码补全功能。调试支持亦是一大亮点,开发者可直接通过 Chrome DevTools 对 Wasm 模块进行断点调试和性能分析,与传统 JavaScript 工作流无缝衔接。MoonBit 与 WasmGC 的深度整合,使其在浏览器端表现尤为亮眼。通过零成本 JS 互操作Unicode 安全算法,在浏览器端实现无缝集成与性能突破。图片

针对服务器端开发,MoonBit 借助 Wasm 组件模型(Component Model)实现了跨语言协作的模块化架构。通过标准化接口定义语言(WIT),开发者可生成跨语言绑定。在编写 HTTP 服务器的实践中,MoonBit 生成的二进制大小仅为 27KB(Rust 为 100KB,Python 为 17MB)。

在未来,MoonBit 将深度整合组件模型,将 wasm-tools 直接嵌入工具链,支持 WASI 异步流(Phase 5提案)等前沿特性。

图片

MoonBit 的潜力不仅限于 Wasm,其支持多后端编译的特性为开发者提供了灵活的选择:编译为 C 代码时可运行于嵌入式设备(MCU),而实验性 JavaScript 后端则支持 Elm 架构,用于构建全栈应用。

对于一门语言来说,生态系统和开发者体验是其获得广大社区认可的必备条件。在 2025 年,MoonBit 将达到 Beta 版本,并进一步完善语言工具链与生态,随着 Wasm 在边缘计算、微服务等领域的深化,MoonBit 或将成为开发者拥抱下一代 Web 标准的关键选择。

社区反馈

在本次演讲后,Kotlin/Wasm 作者 Zalim 在社交媒体上表示:"MoonBit 在 WebAssembly 平台实现了精彩的成果",对 MoonBit 在 WASM 方向的技术成果给予高度认可。

图片

ZivergeTech & Golem Cloud 公司 CEO John A De Goes 表示:“与张宏波在 WASM I/O 见面后,我对 MoonBit 未来更加充满期待!MoonBit 融合了 Rust 语言的精华特性,还增加了垃圾回收(GC)机制,并优先关注工具链、性能和稳定性。欢迎大家加入我,一起参加在 LambdaConf 举办的 GolemCloud 黑客松活动,届时我会使用MoonBit!” 

图片

MoonBit 在本次 WASM I/O 上的精彩亮相吸引了海外社区诸多关注,我们期待未来 MoonBit 在全球开发者社区的无限潜力!

远离 dismiss,拥抱状态驱动

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

在 SwiftUI 开发中,环境值 dismiss 因其灵活、自适应的特性备受开发者青睐。它能够根据当前视图的上下文智能执行关闭操作:在模态视图中关闭窗口、在导航堆栈中弹出视图,甚至在多列导航容器中自动关闭边栏。正是这种看似“万能”的便捷性,让许多开发者将它作为首选工具。然而,便捷的背后往往隐藏着风险。频繁使用 dismiss 可能在应用程序中埋下隐患,引发测试难题乃至难以追踪的稳定性问题。本文将分析我们为何应谨慎对待 dismiss,并介绍更加健壮可靠的状态管理方案。通过重新审视视图呈现与消失的逻辑,我们能够打造出更稳定、易维护且可预测的 SwiftUI 应用。

通过 llms.txt 引导 AI 高效使用网站内容

2025年4月1日 19:00
作为示例,本站也开始提供 llms.txt 和 llms-full.txt 的支持,可以参看下面的链接获取相关文件。 llms.txt llms-full.txt 什么是 llms.txt 大型语言模型(LLMs)是截止至训练日期时的人类知识的总集。而如果想要精确地解决更加实时的问题(比如在进行代码生成、研究辅助等任务中),我们可以通过搜索最新知识,依赖网络信息,来极大提升模型的准确性。然而,标准的 HTML 内容通常包含导航元素、JavaScript、CSS 和其他对于 LLMs 而言非必要的信息。这些冗余信息会在对话中占据 LLMs 有限的上下文窗口,也会干扰和降低处理效率。此外,LLMs 直接抓取和解析完整的 HTML 页面效率也很低下。为了应对这些挑战,llms.txt 应运而生,它是一个正在讨论的标准(参见 llms-txt.org),旨在为 LLMs 提供一个简洁、专业的网站内容概述,以单一且易于访问的 Markdown 文件格式呈现。llms.txt 就像一个网站的“指南”,引导 AI 系统找到站点上的关键信息,并以易于阅读和分析的结构化格式...

iOS启动优化 - 1分钟让你的启动时间降低 1s

作者 yidahis
2025年4月1日 16:01

先回答标题

原理是通过减少启动时加载的动态库数量达到目的。那一分钟就可以?是的,因为 CocoaPods 默认是会加载所有的依赖库,可以在一分钟内通过删除 Other Linker Flags 中的链接标记,取消启动时自动加载的动态库。那 1s 是不是夸张了?没有一点点夸张,不信大家往下看。(文章结尾有惊喜!)

准备工作

如何测量启动时间?

咱们首选还是 instruments,它的 App Launch 工具 可将启动过程拆解为关键阶段(如 dyld 加载、Runtime 初始化、首帧渲染等),并量化各阶段的耗时。

image.png

如图,这里精确的量化了本次程序运行总的时长、加载可执行文件时长(包含主程序、动态连接器、动态库、各种初始化等)、加载三个动态库的运行时长。这里重点关注加载的动态库,它们的特点是描述以 Map Image + <动态库路径>

如何知道加载了哪些动态库?

如上图,其中以 Map Image 开头的描述表明这就是一个动态库。dlopen() 函数调用后也会生成一个同样的任务。 除了 instruments 还有其他手段。这里就给出一个笔者常用的一个工具。 新建一个类 LazyLoader,将以下代码拷贝到全局调用区。

// 打印动态库名称(兼容 M1/Mac 和 iOS)
void filterThirdPartyLibs(void) {
    uint32_t count = _dyld_image_count();
    for (uint32_t i = 0; i < count; i++) {
        const char* path = _dyld_get_image_name(i);
        if (!strstr(path, "/usr/lib/") &&
            !strstr(path, "/System/Library/")) {
            NSString* pathStr = [NSString stringWithCString:path encoding:NSUTF8StringEncoding];
            NSLog(@"Third-party: %@\n", pathStr.lastPathComponent);
        }
    }
}

// 在程序启动时自动调用
__attribute__((constructor)) static void runtime_init(void) {
    filterThirdPartyLibs();
}

见证奇迹的时刻即将来到,开始发车了。

首先通过 pod 新建一个私有 LoadAFDemo

pod lib create LoadAFDemo

接下来在 podfile 中添加以下依赖,并执行pod install

pod 'AFNetworking'

然后将工具 LazyLoader 拖入工程中并进行桥接。现在开始运行项目,可以得到如下结果:

image.png 可以看到 AFNetworking 在其中,这里过滤了系统的库,其他两个是主工程相关,因为开发环境被拆分为两个。 这个时候可以看到主工程的 Other Linker Flags 很简单,只有 AFNetworking

image.png

接下来打开App Launch 工具查看总的时长 242.06ms, 这个数字很重要,它代表了 pre-main 阶段的启动耗时。

image.png 如图,动态库的加载描述都是以 Map image 开头,其中第一个是笔者添加的依赖库 AFNetworking ,下面三个都是系统库的。

  1. 在 podfile 中添加一些依赖库。
pod 'SDWebImage'
pod 'Realm'
pod 'RxTheme','4.1.1'
pod 'RxGesture','3.0.2'
pod 'NSObject+Rx','5.1.0'
pod 'Moya','~> 14.0.0'
pod 'Alamofire', '~> 5.2.1'
pod 'SwiftyJSON','5.0.0'
pod 'R.swift','5.2.2'
pod 'CryptoSwift','1.4.2'
pod 'SnapKit','5.6.0'
pod 'FlexLayout','1.3.20'
pod 'PinLayout','1.9.2'
pod 'SwifterSwift/Foundation','5.2.0'
pod 'SwifterSwift/UIKit','5.2.0'
pod 'WMPageController','2.5.2'
pod 'CYLTabBarController'
pod 'KMNavigationBarTransition'
pod 'Firebase/AnalyticsWithoutAdIdSupport','9.6.0'
pod 'Firebase/Crashlytics','9.6.0'
pod 'FirebaseCoreDiagnostics','9.6.0'
pod 'FirebaseInstallations','9.6.0'
pod 'FirebasePerformance','9.6.0'
pod 'AppsFlyerFramework','6.5.2'
pod 'MoEngage-iOS-SDK','7.1.4'
pod 'MoEngageInApp', '2.1.2'
pod 'MORichNotification','5.2.0'
pod 'ZLPhotoBrowser', '4.2.5'
pod 'FCUUID','1.3.1'

此时运行pod install再观察,发现刚才添加的依赖库都被添加进来。

image.png

接下来运行项目,查看控制台输出:

image.png

总计44个三方库加载了,这里只截取部分。

接下来再次打开App Launch 工具查看总的时长为 1.47s, 比原来多出了近 1.2s。是不是很惊讶,这里的三方库加载耗时基本在 1ms 左右,共 44 个那也最多 100ms 才对。这里先卖个关子,请看接下来的表演。 image.png

接下来打开主工程,删除 Other Linker Flags 中的所有内容,再次运行项目。

image.png 发现三方库一个也没有加载。再次打开App Launch 工具查看总的时长为 197.07ms

image.png 看到这里的你,是不是惊讶,从 1.47s197.07ms,启动效能完全是飞升了。那这个做法可以直接运用到项目中去吗?接下来通过一些问题来解开谜题。

一些疑点

  1. 清空 Other Linker Flags 中的依赖后,代码能运行吗?
    答:能运行,不过相关的三方库会在 pre-main 阶段加载。请看下图, image.png 上图中首先 import AFNetworking 库,然后使用 AFHTTPSessionManager 获取其实例,且 callAF 方法没有任何地方调用,只是定义了。运行项目可以看到,控制台中输出了 AFNetworking,没有编译和运行报错。(如果只是 import 是不会触发链接操作的)。这里暂且把这种行为定义为 Real Import

  2. 那为什么会这样呢?
    其实答案很明显,获取 AFHTTPSessionManager 实例是编译期就确定的行为。而编译没有报错,那英爱是系统在 pre-main 阶段通过 LC_LOAD_DYLIB 指令加载 AFNetworking

  3. 在私有podspec中添加了 AFNetworking依赖, 还需要清除私有podspec中的 Other Linker Flags 吗?
    答:在私有库没有 Real Import 的前提下,不需要清除 Other Linker Flags 也可以实现依赖的三方库不被加载。就是说,私有podspec断开了和主工程链接关系,那么这个 AFNetworking 也就没有断开了和主工程的联系,所以即使 Other Linker Flags 添加了链接标记,也只是 AFNetworking 和当前私有库的。

  4. AFNetworking 换成 Alamofire 也是一样的吗?
    答:是的

  5. 没有加载AFNetworking的情况下,通过反射(NSClassFromString(@"AFHTTPSessionManager"))可以拿到实例吗? 答:当然不可以,需要时先动态加载 AFNetworking

  6. 说了这么多那 dyld 的加载动态库的原则是什么
    答:这个问题主要与 Mach-O 的文件结构有关。可以使用 otool -l <动态库路径> 查看文件中所以依赖的动态库。主要有两个命令LC_LOAD_DYLIBLC_LOAD_WEAK_DYLIB, 其实上面的问题中提到的Real Import实际上是执行 LC_LOAD_DYLIB 命令将动态库加载到强依赖库列表中,也就对应在Other Linker Flags中标记为-framework的动态库。LC_LOAD_WEAK_DYLIB命令将动态库加载到弱依赖库列表中,对应于Other Linker Flags中的-weak_framework标记的动态库。而无论是强依赖还是弱依赖,正常情况情况下都会加载。

  7. 你搞这么麻烦,直接用系统支持的 -weak_framework标记不就行了? 答:这个笔者经过实测,在目前的系统版本(iOS18)是不行的。经过查阅官方文档发现最后更新日期是2013年,应该是很久没有更新了。而且现在大部分使用的是dyld3

惊喜

为了更加直观的观察依赖关系,笔者专门开发了一个脚本工具。优点是:非常直观和方便。下面以咸鱼 iOS 为例。传送门

xianyu-outputs.html.png

都2025年,你竟然还敢买iOS的源码?

作者 iOS阿玮
2025年4月1日 10:22

App Store审核问题或3.2f咨询,请公众号留言。不免费,不闲聊。iOS研究院

序言

买源码这件事情在互联网公司初创是最常见的行为,除了节约了开发成本,更为重要的是节约了开发的时间

对于想做某方面业务,但是完全不懂技术的初创者来说,是再好不过的选择了。

尤其是在2015~2020年,口罩前期不少外包公司也是雨后春笋般。那个时候没有这么多较为知名的头部产品,也是人人都是产品经理的时代

做微商的想做自己的商城App,做化妆品、做珠宝、做鞋子的,等等...

转折点

在步入2022年之后,苹果先后提高了开发者的门槛。首先就是大陆区账号注册的难度提升,增加了人脸拍摄。提高了代码查重的算法。之前矩阵的打法也就是马甲包,不断沦为苹果重点照顾的重灾区

衍生的条款

在算法一步步提升的背景下,Guideline 4.3(a) - Design - Spam中拓展了3点内容:

  • We noticed your app still shares a similar binary, metadata, and/or concept as apps previously submitted by a terminated Apple Developer Program account.

译:我们注意到你的应用程序仍然与之前被终止的苹果开发者计划帐户提交的应用程序共享类似的二进制文件、元数据和/或概念。

  • Submitting an app with the same source code or assets as other apps already submitted to the App Store

译:提交与其他已提交到app Store的应用程序具有相同源代码或资产的应用程序

  • Purchasing an app template with problematic code from a third party

译:从第三方购买有问题代码的应用模板

加强版

最近有很多同行在咨询3.2f,其中最值得关注的是

Evidence of Dishonest or Fraudulent Activity


Your account is associated with terminated developer accounts and/or accounts 
engaged in fraudulent activities. These associations may include, but are not limited to, shared account information, 
submissions with similar or identical App Store metadata, app binaries with s
hared code, resources, or assets, or the transfer of apps engaged in fraudulent activities between accounts.

包括但不限于共享账户信息、具有相似或相同App Store元数据的提交、具有共享代码、资源或资产的应用程序二进制文件

也就是说在今年之前,仅仅局限于上架被拒4.3a。苹果在最近开始了新一轮的二进制比对,在未进行迭代,上新的账户中,触发了代码关联性的查重。

所以都2025年了,你还敢买iOS的源码?我建议是算了吧,你把握不住的~

遵守规则,方得长治久安,最后祝大家大吉大利,今晚过审!

Flutter 性能优化:实战指南

2025年4月1日 09:46

主要的优化方面,如首屏、列表、渲染、内存、数据加载、工具等。

一、首屏渲染优化

1. 骨架屏与占位符

  • 问题:首屏加载时白屏或长时间等待数据导致用户体验差。

  • 解决方案

    • 骨架屏:使用 SkeletonLoader 等组件模拟页面结构,替代空白屏4。
    • 占位符:数据未加载时返回轻量组件如 SizedBox.shrink(),降低首帧渲染时间(如减少 200ms)。
  • 代码示例

    Widget build(BuildContext context) {
      if (_isLoading) return const SizedBox.shrink(); // 轻量占位
      // 真实内容
    }
    

2. 资源预加载

  • 问题:首屏图片加载延迟。

  • 解决方案:在页面初始化前预加载关键资源(如轮播图、Banner)。

  • 代码示例

    Future<void> _precacheImages() async {
      await Future.wait([
        precacheImage(AssetImage('assets/banner.png'), context),
      ]);
    }
    

3. 数据预取与FFI优化

  • 问题:Flutter 通过 Channel 请求数据时线程切换和编解码耗时。

  • 解决方案

    • Native侧发起请求:在页面路由阶段由 Native 提前请求数据,避免 Flutter 线程切换。
    • FFI(Foreign Function Interface) :通过 FFI 直接读取 Native 缓存数据,减少序列化开销。
  • 效果:详情页启动时间优化 100ms 以上9。


二、长列表与滚动性能优化

1. 懒加载与按需构建

  • 问题:一次性渲染大量列表项导致卡顿。

  • 解决方案

    • ListView.builder/GridView.builder:仅构建可见项,避免内存溢出。
    • 分帧渲染(分帧加载) :将复杂子组件分帧渲染,避免单帧耗时过长。
  • 代码示例

    ListView.builder(
      itemCount: 1000,
      itemBuilder: (context, index) => ListItem(data[index]),
    )
    

2. 复用与缓存

  • 问题:频繁切换 Tab 导致重复加载数据。

  • 解决方案

    • 状态缓存:使用 PageStorageKey 或 AutomaticKeepAliveClientMixin 缓存页面状态。
    • 数据分级管理:通过 RxMap 维护不同分类的数据状态,减少重复请求。

三、UI渲染优化

1. 减少Widget重建

  • 问题:频繁调用 setState 导致不必要的 Widget 重建。

  • 解决方案

    • const Widget:使用 const 构造函数创建静态组件,避免重复构建。
    • Provider 选择性刷新:通过 Selector 或 Consumer 局部刷新,而非全量更新。
  • 代码示例

    const MyWidget(); // 编译时确定,避免重建
    

2. 动画优化

  • 问题:复杂动画导致帧率下降。

  • 解决方案

    • AnimatedBuilder:分离动画逻辑与静态组件,仅重绘动画部分。
    • 复用Child:将不变部分作为 child 参数传入,避免重复构建。
  • 代码示例

    AnimatedBuilder(
      animation: _controller,
      child: const StaticWidget(),
      builder: (context, child) => Transform.rotate(
        angle: _controller.value,
        child: child,
      ),
    )
    

四、内存与GPU优化

1. 内存泄漏检测

  • 问题:未释放资源导致内存持续增长。

  • 解决方案

    • 及时销毁监听:在 dispose 方法中取消订阅和释放资源。
    • 工具检测:使用 DevTools 的 Memory 视图分析内存泄漏。

2. GPU渲染优化

  • 问题:多图层叠加(如半透明蒙层)导致 GPU 负载高。

  • 解决方案

    • 避免 saveLayer:使用 ClipRRect 替代复杂蒙层,减少离屏渲染。
    • 缓存静态图像:通过 checkerboardRasterCacheImages 检测未缓存的图像。

五、工具与工程化实践

1. 性能分析工具

  • DevTools:通过 Timeline 视图分析帧耗时,定位构建、布局、绘制阶段的瓶颈。
  • Profile模式:打包 Profile 版本,获取接近真实环境的性能数据。

2. 模块级混合开发

  • 问题:全 Flutter 页面启动性能差(如动态库加载耗时)。

  • 解决方案

    • Native与Flutter混合:核心模块用 Native 实现,非核心模块保留 Flutter,降低启动时间。
    • FlutterBoost 扩展:支持模块级混合容器,解决生命周期和布局问题。

六、其他高级优化

1. Isolate 并行计算

  • 问题:Dart 单线程阻塞 UI。

  • 解决方案:将耗时计算(如 JSON 解析)放入 Isolate 执行。

  • 代码示例

    final result = await compute(heavyTask, data);
    

2. 资源压缩与预置

  • 问题:资源加载慢(如图片、字体)。

  • 解决方案

    • 图片压缩:使用 flutter_image_compress 优化图片体积。
    • 本地预置:关键资源(如 JSON 模板)预置到本地,减少网络依赖。

总结

  • 核心原则:减少 Widget 重建、按需加载、复用资源、分离耗时操作。

  • 实战案例

    • 携程酒店通过分帧渲染优化;
    • 淘宝特价版使用 FFI 和 Native 数据预取提升首屏速度;
    • 长列表懒加载和状态缓存避免卡顿。

iOS性能优化:OC和Swift实战指南

2025年4月1日 09:29

常见的iOS性能优化点,比如内存管理、UI优化、多线程处理、网络请求优化、启动优化、I/O操作、图像处理、算法优化、工具使用等。

一、内存优化

1. 循环引用处理

  • 原理:对象之间的强引用导致无法释放,内存泄漏。

  • Objective-C

    • 使用 __weak 或 __unsafe_unretained(需谨慎)打破循环:

      __weak typeof(self) weakSelf = self;
      self.block = ^{
          // 弱引用避免循环
          [weakSelf doSomething];
      };
      
    • 对 NSTimer 使用 NSProxy 或 weak 委托模式。

  • Swift

    • 使用 [weak self] 或 [unowned self] 捕获列表:

      self.block = { [weak self] in
          guard let self = self else { return }
          self.doSomething()
      }
      
    • unowned 适用于生命周期相同或更短的场景(如父子关系)。


2. 自动释放池(Autorelease Pool)

  • 原理:批量创建临时对象时,及时释放内存。

  • Objective-C

    @autoreleasepool {
        for (int i = 0; i < 100000; i++) {
            NSString *temp = [NSString stringWithFormat:@"%d", i];
            // 临时对象会被及时释放
        }
    }
    
  • Swift

    autoreleasepool {
        for i in 0..<100000 {
            let temp = "(i)"
            // 临时对象在块结束时释放
        }
    }
    
  • 适用场景:大量临时对象生成(如解析 JSON 数组、图像处理)。


二、UI 性能优化

1. 避免离屏渲染(Offscreen Rendering)

  • 原理:离屏渲染(如圆角、阴影)触发 GPU 额外绘制,导致卡顿。

  • 优化方法

    • 预渲染圆角

      • Objective-C

        // 使用 Core Graphics 提前绘制圆角
        UIGraphicsBeginImageContextWithOptions(view.bounds.size, NO, 0);
        [[UIBezierPath bezierPathWithRoundedRect:view.bounds cornerRadius:10] addClip];
        [image drawInRect:view.bounds];
        UIImage *roundedImage = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
        view.layer.contents = (id)roundedImage.CGImage;
        
      • Swift

        let renderer = UIGraphicsImageRenderer(size: view.bounds.size)
        let roundedImage = renderer.image { context in
            UIBezierPath(roundedRect: view.bounds, cornerRadius: 10).addClip()
            image.draw(in: view.bounds)
        }
        view.layer.contents = roundedImage.cgImage
        
    • 避免 shouldRasterize:除非复用图层,否则会触发离屏渲染。


2. Cell 复用与轻量化

  • 原理:避免频繁创建/销毁 Cell,减少 CPU 和内存压力。

  • Objective-C

    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
        // 复用 Cell
        UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"CellID"];
        if (!cell) {
            cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"CellID"];
        }
        // 轻量配置(避免复杂计算)
        cell.textLabel.text = [self.dataArray[indexPath.row] title];
        return cell;
    }
    
  • Swift

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "CellID", for: indexPath)
        cell.textLabel?.text = dataArray[indexPath.row].title
        return cell
    }
    
  • 优化点

    • 避免在 cellForRow 中执行耗时操作(如网络请求)。
    • 使用 prepareForReuse 清理旧数据。

三、多线程优化

1. 主线程任务最小化

  • 原理:主线程阻塞导致 UI 卡顿(16ms 内未完成一帧绘制)。

  • Objective-C

    // 将耗时操作放到后台线程
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSData *data = [NSData dataWithContentsOfURL:url];
        dispatch_async(dispatch_get_main_queue(), ^{
            self.imageView.image = [UIImage imageWithData:data];
        });
    });
    
  • Swift

    DispatchQueue.global(qos: .userInitiated).async {
        let data = try? Data(contentsOf: url)
        DispatchQueue.main.async {
            self.imageView.image = UIImage(data: data!)
        }
    }
    

2. 线程安全与锁优化

  • Objective-C

    • @synchronized 实现简单但性能较低:

      objc

      @synchronized(self) {
          // 临界区
      }
      
    • 高性能场景使用 os_unfair_lock(替代已废弃的 OSSpinLock):

      #include <os/lock.h>
      os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;
      os_unfair_lock_lock(&lock);
      // 临界区
      os_unfair_lock_unlock(&lock);
      
  • Swift

    • 使用 NSLock 或 DispatchQueue 屏障:

      let queue = DispatchQueue(label: "com.example.threadSafe", attributes: .concurrent)
      queue.async(flags: .barrier) {
          // 写操作(独占访问)
      }
      

四、网络优化

1. 请求合并与缓存

  • Objective-C

    // 使用 NSURLSession 的缓存策略
    NSURLRequest *request = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestReturnCacheDataElseLoad timeoutInterval:10];
    
  • Swift

    let request = URLRequest(url: url, cachePolicy: .returnCacheDataElseLoad, timeoutInterval: 10)
    
  • 优化点

    • 减少重复请求(如短时间内多次刷新)。
    • 使用 HTTP/2 多路复用降低连接开销。

五、启动时间优化

1. 冷启动阶段优化

  • 原理:减少 main() 函数之前的加载时间(T1)和首帧渲染时间(T2)。

  • Objective-C

    • 减少动态库数量,合并 Category。
    • 避免在 +load 方法中执行代码。
  • Swift

    • 使用 @UIApplicationMain 减少启动代码。

    • 延迟非必要初始化:

      DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
          // 延迟初始化第三方 SDK
      }
      

六、I/O 优化

1. 文件读写异步化

  • Objective-C

    dispatch_io_t channel = dispatch_io_create_with_path(DISPATCH_IO_STREAM, [path UTF8String], O_RDONLY, 0, queue, ^(int error) {});
    dispatch_io_read(channel, 0, SIZE_MAX, queue, ^(bool done, dispatch_data_t data, int error) {});
    
  • Swift

    let queue = DispatchQueue.global(qos: .background)
    DispatchIO.read(fromFileDescriptor: fd, queue: queue) { data, _ in
        // 处理数据
    }
    

七、图像处理优化

1. 异步解码与降采样

  • Objective-C

    // 使用 ImageIO 进行降采样
    CGImageSourceRef source = CGImageSourceCreateWithURL((__bridge CFURLRef)url, NULL);
    CGImageRef imageRef = CGImageSourceCreateImageAtIndex(source, 0, (__bridge CFDictionaryRef)@{(id)kCGImageSourceThumbnailMaxPixelSize: @(300)});
    UIImage *image = [UIImage imageWithCGImage:imageRef];
    
  • Swift

    let source = CGImageSourceCreateWithURL(url as CFURL, nil)!
    let options: [CFString: Any] = [kCGImageSourceThumbnailMaxPixelSize: 300]
    let imageRef = CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary)!
    let image = UIImage(cgImage: imageRef)
    

八、算法与数据结构

1. 高效遍历与查询

  • Objective-C

    • 使用 NSDictionary 替代 NSArray 快速查找(O(1) vs O(n))。
  • Swift

    • 使用 lazy 延迟计算集合:

      let filteredData = data.lazy.filter { $0.isValid }.map { $0.value }
      
    • 使用 ContiguousArray 提升性能(连续内存布局)。


九、工具使用

1. Instruments 分析

  • Time Profiler:定位 CPU 热点函数。
  • Allocations:分析内存分配类型和泄漏。
  • Core Animation:检测离屏渲染和帧率。

十、Swift 特有优化

1. 减少动态派发

  • 使用 final 和 private 修饰类或方法:

    final class NetworkManager {
        private func fetchData() { ... }
    }
    

2. 值类型优先

  • 使用结构体(struct)替代类(class)减少引用计数开销:

    struct Point {
        var x: Double
        var y: Double
    }
    

总结

  • Objective-C 优化重点:手动内存管理、@autoreleasepool、避免 performSelector 潜在泄漏。
  • Swift 优化重点:值类型、协议扩展、DispatchQueue 和现代语法(如 Result 类型)。
  • 通用原则:减少主线程阻塞、复用资源、异步化耗时操作、利用工具分析瓶颈。

为什么 Swift 的反射像个“玩具”?聊聊 Mirror 背后的设计哲学

作者 JQShan
2025年3月31日 21:03

引子:当程序员想“窥探”一个对象

如果你用过 Java 或 C#,大概对「反射」这个词不陌生——它能让你在运行时像变魔术一样动态调用方法、修改属性,甚至篡改私有字段。但在 Swift 中,如果你想“窥探”一个对象的内部,苹果只给了你一个看似简陋的工具:Mirror。它不能修改属性,无法获取方法,连私有变量都藏着掖着。这不禁让人想问:反射这么重要的能力,苹果为何只做了个“玩具”出来?

今天,我们就来聊聊 Mirror 的设计逻辑,以及它背后隐藏的 Swift 语言哲学。

一、Mirror 的“克制”:安全与性能的双重防线

想象一下,你正在造一辆车。Java 的反射像是给你一套万能扳手,能拆发动机、改刹车片,甚至把车门卸下来。而 Swift 的 Mirror 更像是一把车钥匙——它能让你打开车门,看看内饰,但别想乱改零件。这种“克制”背后,是苹果对 Swift 的两大核心坚持:

  1. 安全第一 Swift 从诞生起就带着「杜绝未定义行为」的执念。反射能绕过编译检查,就像给代码开了个后门。比如,Java 中你可以用反射强行修改 final 字段,但这可能导致不可预知的崩溃。而 Mirror 的只读设计,本质上是在说: “你可以看看,但别乱动。”
  2. 速度不能妥协 Swift 被用于 iOS 系统内核、高性能游戏引擎等场景。反射的动态类型检查(as?Any)会带来运行时开销,而 Mirror 的轻量化设计,让它在需要时足够快,甚至能被编译器优化掉部分成本。

二、为什么不需要“万能扳手”?Swift 的编译时魔法

苹果似乎对动态反射兴趣缺缺,但其实他们找到了一种更“Swift 风格”的解决方案:把问题消灭在编译时。举个例子:

  • 场景:JSON 解析 在 Java 中,你可能用反射遍历字段,匹配 JSON 的 key。而在 Swift 中,Codable 协议通过编译器自动生成代码,直接映射属性——无需运行时反射,且类型安全零开销。
// 编译器自动生成编解码逻辑!
struct User: Codable {
    var name: String
    var age: Int
}
  • 场景:依赖注入 Java 的 Spring 框架依赖反射创建对象,而 Swift 可以通过泛型 + 协议,在编译时完成类型绑定:
// 编译时就知道 Container 里存了什么类型
container.register(UserService()) 
let service: UserService = container.resolve()

苹果的逻辑很明确:能通过类型系统解决的问题,绝不留到运行时。

三、Mirror 的生存空间:优雅的妥协

当然,总有些场景需要运行时信息。比如调试工具、动态生成日志,或是教学demo中展示对象结构。这时 Mirror 就派上用场了:

// 打印对象的所有属性
func debugPrint(_ value: Any) {
    let mirror = Mirror(reflecting: value)
    for child in mirror.children {
        print("(child.label ?? "?"): (child.value)")
    }
}

但你会发现,Mirror 的设计处处透着“小心翼翼”:

  • 不支持方法反射(避免动态派发)
  • 不暴露内存布局(防止不安全访问)
  • 对枚举和结构体的支持有限(鼓励模式匹配)

它更像是一个“安全气囊”,只在必要时弹出,而非让开发者随时飙车。

四、从 Mirror 看 Swift 的“价值观”

  1. 开发者不是敌人 Java 的反射默认允许访问私有字段,而 Swift 的 Mirror 对私有属性的可见性取决于模块边界——它假设开发者是理性的,但依然用访问控制保护代码的封装性。
  2. 工具链即力量 Swift 更倾向于通过编译器(如自动生成 Codable 代码)、Xcode 工具链(如 LLDB 调试器)来辅助开发,而非依赖运行时动态能力。
  3. 生态的统一性 在 SwiftUI 中,属性包装器(@State)、函数式编程等特性,让开发者无需反射也能实现动态 UI 和数据绑定。反射不再是必需品,而是备胎。

五、如果你真的需要“万能扳手”……

虽然苹果不鼓励,但总有极客想突破限制。比如:

  • 用 @dynamic 修饰符兼容 Objective-C 的运行时
  • 通过指针黑魔法直接操作内存布局(危险!但刺激)
  • 第三方库如 Runtime 提供元编程能力

但当你走这条路时,苹果的设计师可能会在背后叹气: “何必呢?明明有更安全的方式啊。”

结语:Mirror 是一面镜子,照出 Swift 的灵魂

Mirror 的简陋,恰恰反映了 Swift 的野心——它不想成为另一个“什么都能做,但处处是坑”的动态语言,而是试图用类型安全、编译时优化和清晰的API,重新定义现代编程的边界。就像 Swift 之父 Chris Lattner 所说: “我们希望开发者写出明显正确的代码,而非依赖运行时的小聪明。”

所以,下次当你嫌弃 Mirror 功能弱时,不妨换个角度想:或许不是苹果吝啬,而是他们相信,最好的魔法,应该发生在编译时 ✨。

MCP 崛起与苹果的 AI 框架设想 - 肘子的 Swift 周报 #77

作者 Fatbobman
2025年3月31日 22:00

在最近一段时间,在社交网络上,越来越多的 Model Context Protocol(MCP)使用者展示了各种丰富多彩的应用场景,从操控 Blender 创建精美场景,到利用最新的 GPT-4o 图片构建完整的漫画故事。MCP 巧妙地打开了以文本为主要互动手段的大模型,与现实世界之间的大门。

Swift运行时以及与OC混编

作者 jz_study
2025年3月30日 20:44

Swift运行时

  • 在swift中如果我们要定义一个表示错误的类型非常简单,只要遵循Error协议就可以了,我们通常用枚举或结构体来表示错误类型,枚举可能用的多些,因为它能更直观的表达当前错误类型的每种错误细节。

image.png

如何抛出错误

  • 函数、方法和初始化器都可以抛出错误。需要在参数列表后面,返回值前面加throws关键字

image.png

image.png

image.png

使用Do-Catch做错误处理

  • 在Swift中我们使用do-catch块对错误进行捕获,当我们调用一个throws声明的函数或方法时,我们必须把调用语句放在do语句块中,同时do语句块后面紧接着使用catch语句块。

image.png

try?

  • try?会将错误转换为可选值,当调用try?+函数或方法语句时候,如果函数或方法抛出错误,程序不会发生崩溃,而返回一个nil,如果没有抛出错误则返回可选值

image.png

try!

  • 如果你确信一个函数或者方法不会抛出错误,可以使用try!来中断错误的传播。但是如果错误真的发生了,你会得到一个运行时错误。
let photo = try! loadImage(atPath:"./Resources/John Appleseed.jpg")

指定退出的清理动作

  • defer关键字:defer block里的代码会在函数return之前执行,无论函数是从哪个分支return的,还是有throw,还是自动而然走到最后一行(类似于ts的finally)

image.png

权限控制(模块和源文件)

  • 模块指的是独立的代码分发单元,框架或应用程序会作为一个独立的模块来构建和发布。在Swift中,一个模块可以使用import关键字导入另外一个模块
  • 源文件就是Swift中的源代码文件,它通常属于一个模块,即一个应用程序或者框架。尽管我们一般会将不同的类型分别定义在不同的源文件中,但是同一个源文件也可以包含多个类型、函数之类的定义。

访问级别

image.png

潜规则1
  • 如果一个类的访问级别是fileprivate或private那么该类的所有成员都是fileprivate或private(此时成员无法修改访问级别),如果一个类的访问级别是open、internal或者public那么它的所有成员都是internal,类成员的访问级别不能高于类的访问级别(注意:嵌套类型的访问级别也符合此条规则)
潜规则2
  • 常量、变量、属性、下标脚本访问级别低于其所声明的类型级别,并且如果不是默认访问级别(internal)要明确声明访问级别(例如一个常量是一个private类型的类类型,那么此常量必须声明为private或fileprivate)
潜规则3
  • 在不违反1、2两条潜规则的情况下,setter的访问级别可以低于getter的访问级别(例如一个属性访问级别是internal),那么可以添加private(set)修饰符将setter权限设置为private,在当前模块中只有此源文件可以访问,对外部是只读的)
潜规则4
  • 必要构造方法(required修改)的访问级别必须和类访问级别相同,结构体的默认逐一构造函数的访问级别不高于其成员的访问级别(例如一个成员时private那么这个构造函数就是private,但是可以通过自定义来声明一个public的构造函数),其他方法(包括其他构造方法和普通方法)的访问级别遵循潜规则1
不透明类型(why)

image.png

  • 代码是可以编译通过的,但是makeTrapezoid的返回类型有凑有偿,被暴露了出去

image.png

  • 不能将其Container用作函数的返回类型,因为该协议具有关联类型。也不能将它用作返回类型的泛型约束,因为函数体外没有足够的信息来推断泛型类型需要什么

image.png

解决问题

image.png

返回不透明类型vs返回协议类型
  • 返回opaque类型看起来非常类似于使用协议类型作为函数的返回类型,但这两种返回类型的不同之处在于它们是否保留了类型标识。opaque类型是指一种特定类型,尽管函数的调用者不能看到是哪种类型;协议类型可以指代符合协议的任何类型。一般来说,协议类型为存储值的基础类型提供了更大的灵活性,而不透明类型可以对这些基础类型做出更强有力的保证。

image.png

ARC

  • Swift使用自动引用计数(ARC)来跟踪并管理应用使用的内存。大部分情况下,这意味着在Swift语言中,内存管理"仍然工作",不需要自己去考虑内存管理的事情。当实例不再被使用时,ARC会自动释放这些类的实例所占用的内存。
  • 引用计数只应用在类的实例。结构体(Structure)和枚举类型是值类型,并非引用类型,不是以引用的方式来存储和传递的

ARC如何工作

image.png

循环引用

  • 在两个类实例彼此保持对方的强引用,使得每个实例都使对方保持有效时会发生这种情况。我们称之为强引用环。
  • 通过用弱引用或者无主引用来取代强引用,我们可以解决强引用环问题

image.png

image.png

解决循环引用
  • 弱引用和无主引用允许引用环中的一个实例引用另外一个实例,但不是强引用。因此实例可以互相引用但是不会产生强引用环。
  • 对于生命周期中引用会变为nil的实例,使用弱引用;对于初始化时赋值之后引用再也不会赋值为nil的实例,使用无主引用

弱引用

  • 弱引用不会增加实例的引用计数,因此不会阻止ARC销毁引用的实例。这种特性使得引用不会变成强引用环。声明属性或者变量的时候,关键字weak表明引用弱引用。
  • 弱引用只能声明为变量类型,因为运行时它的值可能改变。弱引用绝对不能声明为常量。
  • 因为弱引用可以没有值,所以声明弱引用的时候必须是可选类型的。在Swift语言中,推荐用可选类型来作为可能没有值的引用的类型。

image.png

image.png

无主引用

  • 和弱引用相似,无主引用也不强持有实例。但是和弱引用不同的是,无主引用默认始终有值。因此,无主引用定义为非可选类型(non-optional-type).在属性、变量前添加unowned关键字,可以声明一个无主引用。
  • 因为是非可选类型,因此当使用无助引用的时候,不需要展开,可以直接访问。不过非可选类型变量不能赋值为nil,因此当实例被销毁的时候,ARC无法将引用赋值为nil。
  • 当实例被销毁后,试图访问该实例的无主引用会触发运行时错误。使用无主引用时请确保引用始终指向一个未销毁的实例。

image.png

闭包引用循环

  • 将一个闭包赋值给类实例的某个属性,并且这个闭包使用了实例,这样也会产生强引用环。这个闭包可能访问了实例的某个属性,例如self.someProperty,或者调用了实例的某个方法,例如self.someMethod。这两种情况导致了闭包使用self,从而产生了循环引用。
闭包引用循环解决
  • 定义占有列表-占有列表中的每个元素都是由weak或者unowned关键字和实例的引用(如self或someInstance)组成。每一对都在中括号中,通过逗号分开。
  • 当闭包和占有的实例总是互相引用时并且总是同时销毁时,将闭包内的占有定义为无主引用。
  • 相反的,当占有引用有时可能会是nil时,将闭包内的占有定义为弱引用。

image.png

内存安全

  • 默认情况下,Swift会克服代码层面上的一些不安全的行为,如:确保一个变量被初始化完后才能被访问、确保变量在销毁后不会被访问等等安全操作。
  • Swift也会确保在多路访问内存中同一区域时不会冲突(独占访问该区域)。通常情况下,我们完全无需考虑内存访问冲突的问题,因为Swift是自动管理内存的。然而,在码代码的时候,了解那些地方可能发生内存访问冲突是非常重要的。通常情况喜爱,如果你的代码有内存访问冲突,那么Xcode会提示编译错误或者运行时错误。
  • 内存访问分为两种
  1. 即时访问:即在访问开始至结束前都不可能有其他代码来访问同一区域
  2. 长期访问:即在访问开始至结束前可能有其他代码来访问同一区域。长期访问可能和其他即时访问或者长期访问重叠
inout参数访问冲突

image.png

image.png

image.png

解决访问冲突问题:

image.png

self访问冲突

image.png

image.png

有问题

image.png

三方库

简介

使用cocoapods

image.png

  • 网络请求:Alamofire
  • JSON解析:SwiftJSON
  • 资源管理:R.swift
  • 社交分享:MonkeyKing
  • 图片缓存与加载:Kingfisher
  • 自动布局:SnapKit
  • 标准款扩展:Dollar
网络请求:Alamofire
  • Alamofire是在平果URL Loading System基础上封装的网络库,简单易用并且可扩展
  • Alamofire
基本用法
  • AF命名空间,链式调用
AF.request("https://time.geekbang.org").response { response in
 debugPrint(response)
}
request方法详解

image.png

SwiftJSON

image.png

Dollar(是对标准库的扩展,类似于js的loadash)

image.png

image.png

image.png

Snapkit

image.png

  • make初始化约束还调用
  • update更新约束的使用
  • remark有冲突约束的时候使用
图片加载和缓存-Kingfisher(类似SDWebImage)

image.png

R.Swift

image.png

image.png

image.png

R.Swift之前

image.png

R.Swift的使用

image.png

MonkeyKing

image.png

OC和Swift运行时简介

Objective-C运行时

  • 动态类型(dynamic typing)
  • 动态绑定(dynamic binding)
  • 动态加载(dynamic loading)

image.png

image.png

派发方式

  • 直接派发(Direct Dispatch)
  • 函数表派发(Table Dispatch)
  • 消息机制派发(Message Dispatch)
直接派发
  • 直接派发是最快的,不止是因为需要调用的指令集会更少,并且编译器还能够有很大的优化空间,例如函数内联等,直接派发也有人称为静态调用
  • 然而,对于编程来说直接调用也是最大的局限,而且因为缺乏动态性所以没办法支持继承和多继承
函数表派发

image.png

image.png

  • 查表是一种简单,易实现,而且性能可预知的方式.然而,这种派发方式比起直接派发还是慢一点,从字节码角度来看,多了两次读和一次跳转,由此带来了性能的损耗,另一个慢的原因在于编译器可能会由于函数内执行的任务导致无法优化(如果函数带有副作用的话)
  • 这种基于数组的实现,缺陷在于函数表无法拓展.子类会在虚数函数表的最后插入新的函数,没有位置可以让extesion安全地插入函数
消息机制派发
  • 消息机制是调用函数最动态的方式.也是Cocoa的基石,这样的机制催生了KVO,UIAppearence和CoreData等功能.这种运作方式的关键在于开发者可以在运行时改变函数的行为.不止可以通过swizzling来改变,甚至可以用isa-swizzling修改对象的继承关系,可以在面向对象的基础上实现自定义派发

Swift运行时

  • 纯Swift类的函数调用已经不再是Objective-c的运行时发消息,而是类似C++的vtable,在编译时就确定了调用哪个函数,所以没法通过runtime获取方法、属性。
  • 而Swift为了兼容Objective-C,凡是继承自NSObject的类都会保留其动态性,所以我们能通过runtime拿到他的方法。这里有一点说明:老版本的Swift(如2.2)是编译期隐式的自动帮我加上了@objc,而4.0以后版本的Swift去掉了隐式特性,必须使用显式添加
  • 不管是纯Swift类还是继承自NSObject的类只要在属性和方法前面添加@objc关键字就可以使用runtime

image.png

  • 值类型总是会使用直接派发,简单易懂
  • 而协议和类的extension都会使用直接派发
  • NSObject的extension会使用消息机制进行派发
  • NSObject声明作用域里的函数都会使用函数表进行派发
  • 协议里声明的,并且带有默认实现的函数会使用函数表进行派发

image.png

Swift运行时-final @objc

  • 可以在标记为final的同时,也是用@objc来让函数可以使用消息机制派发。这么做的结果就是,调用函数的时候会使用直接派发,但也会在Objective-C的运行时里注册响应的selector.函数可以响应perfrom(selector:)以及别的Objective-C特性,但在直接调用时又可以有直接派发的性能
Swift运行时

image.png

image.png

image.png

Swift与OC的桥接

Swift调用OC

image.png

OC调用Swift

image.png

image.png

NS_SWIFT_NAME
  • 在Objective-C中,重新命名在swift中的名称
NS_SWIFT_UNAVAILABLE
  • 在Swift中不可见,不能使用
  • OC中可以调用Swift方法,要保证swift的类是继承自NSObject的

Subclass

  • 对于自定义的类而言,Objective-C的类,不能继承自Swift的类,即要混编的OC类不能是Swift类的子类。反过来,需要混编的Swift类可以继承自OC的类。

  • 定义一个常量值,后面可以方便使用;如#define TOOLBAR_HEIGHT 44;
  • 定义一个不变化的常用值,或者一个较长的对象属性;如#define SCREEN_WIDTH ([[UIScreen mainScreen] bounds].size.width)
  • 定义一个会变化的常用变量值,或者一个较长的对象属性;如: #define STATUS_BAR_HEIGHT ([UIApplication sharedApplication].statusBarFrame.size.height)
  • 定义一个带参数的宏,类似于一个函数;如#define RGB_COLOR(r,g,b) [UIColor colorWithRed:r/255.f gree:g/255.f blue:b/255.f alpha:1.0]

Swift独有特性

  • Swift中有许多OC没有的特性,比如,Swift元组、为一等公民的函数、还有特有的枚举类型。所以,要使用的混编文件要注意Swift独有属性问题。

NS_REFINED_FOR_SWIFT

  • Objective-C的API和Swift的风格相差比较大,Swift调用Objective-C的API时可能由于数据类型等不一致导致无法达到预期(比如,Objective-C里的方法采用了C语言风格的多参数类型;或者Objective-C方法返回NSNotFound,在Swift中期望返回nil)。这时候就要NS_REFINED_FOR_SWIFT

image.png

SwiftUI-国际化

作者 YungFan
2025年3月30日 20:13

介绍

  • 如果 App 需要提供给不同国家的用户使用,则需要进行国际化处理。
  • SwiftUI 项目的国际化主要包括:Info.plist 文件国际化、文本国际化等。

配置国际化语言

在进行国际化之前,必须要添加需要的国际化语言,选中国际化的项目 —> PROJECT —> Info —> Localizations,点击+添加需要的国际化语言(默认已经存在英文)。

Info.plist文件国际化

  1. 新建一个Strings File,必须命名为InfoPlist.strings
  2. 选中InfoPlist.strings,在 Xcode 的右侧文件检查器中找到Localization,点击Localize...,然后勾选配置的国际化语言。
  3. InfoPlist.strings左侧多了一个箭头,点击箭头展开后可以看见不同语言的Strings File,里面存放的是形如Key = Value的键值对。
  4. 在不同语言的Strings File中设置需要国际化的内容,如 App 名称等。
// 英文App名
"CFBundleName" = "I18N";
// 中文App名
"CFBundleName" = "国际化";

文本国际化

  1. 新建一个Strings File,必须命名为Localizable.strings
  2. 选中InfoPlist.strings,在 Xcode 的右侧文件检查器中找到Localization,点击Localize...,然后勾选配置的国际化语言。
  3. Localizable.strings左侧多了一个箭头,点击箭头展开后可以看见不同语言的Strings File
  4. 在不同语言的Strings File中设置需要国际化的文本键值对。
// 英文
"title" = "Reminder";
"message" = "Weather Information";
// 插值
"Weather is %@" = "Today is %@";
"Temperature is %lld" = "The temperature is %lld";
// 中文
"title" = "提示";
"message" = "今日天气";
// 插值
"Weather is %@" = "今天 %@";
"Temperature is %lld" = "气温 %lld 度";
  1. SwiftUI 文本国际化非常简单,开箱即用,因为大多数 View 与 Modifier 的构造方法中都将LocalizedStringKey作为参数类型,该参数的值为文本键值对中的键。
import SwiftUI

struct ContentView: View {
    let weather = "Sunny"
    let temperature = 10

    var body: some View {
        VStack {
            // 纯文本,有3种方式
            Text(title)
            
            Text(LocalizedStringKey("title"))

            Text("title", comment: "The title of the dialog.")
            
            // 自定义View
            MessageView("message")
            
            // 插值
            Text("Weather is \(weather)")
            
            Text("Temperature is \(temperature)")   
        }
    }
}

struct MessageView: View {
    var messaege: LocalizedStringKey

    init(_ messaege: LocalizedStringKey) {
        self.messaege = messaege
    }

    var body: some View {
        Text(messaege)
    }
}

注意:插值格式参考 String Format Specifiers

测试

默认情况下,App 的语言随着系统语言的变化而变化。但在开发阶段,如果才能快速测试 App 的国际化效果?主要有以下几种方式。

  1. 运行 App 之后在设备/模拟器通过设置(Settings)—> 通用(General)—> 语言与地区(Languages & Region) 切换系统语言以查看 App 的国际化效果。
  2. 通过 Xcode 菜单 —> Product —> Scheme —> Edit Scheme... —> Run —> Options —> App Language,选择需要测试的国际化语言之后再运行 App。
  3. 通过 Xcode 菜单 —> Product —> Scheme —> Manage Scheme... —> 选择需要复制的 Scheme —> 点击下方的圆形...图标 —> Duplicate —> 重命名 Scheme,然后将复制的 Scheme 按照方式 2 将 App Language 设置为需要测试国际化语言,最后运行时选择对应国际化语言的 Scheme。

效果

  • 英文。

英文.png

  • 中文。

中文.png

老司机 iOS 周报 #329 | 2025-03-31

作者 ChengzhiHuang
2025年3月30日 21:50

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

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

新手推荐

🐎 Understanding structural identity in SwiftUI

@阿权:SwiftUI 使用结构体描述视图,由 SwiftUI 内部完成视图的绘制与更新。而结构体标识则是视图层级中的唯一 id,用于识别状态变化前后的视图结构体是否是同一视图,进而进行更新和重绘。

文章介绍了 SwiftUI 结构体标识的基本原理和应用、SwiftUI 重绘机制以及 SwiftUI 结构体标识的具体实践。

文章

🐢 Profiling apps using Instruments

@Smallfly:这是苹果官方出品的 Instruments 教程。Instruments 主要用来分析应用程序的性能、资源使用和行为,了解如何提高响应性,减少内存使用,以及运行时的复杂的行为。该教程包含对卡顿的基础知识介绍,卡顿的分析和检测,并提供了卡顿的优化建议。

🐢 充分理解 C/C++ 重要概念:运行时库

@极速男孩:本文系统解析了 C/C++ 运行时库的核心概念与实现机制。运行时库作为程序运行的基础支撑,封装了操作系统 API(如内存管理 malloc/ 文件操作 fopen)、提供标准库函数(字符串 / 数学运算),并管理程序生命周期(全局变量初始化 / 资源清理)。跨平台差异显著:Linux 依赖 glibc/libstdc++,Windows 通过 MSVC 运行时库实现,Android/iOS 则采用轻量化 Bionic/libc++。开发中需警惕多实例问题(静态链接导致内存堆冲突)和多版本兼容性问题(GLIBC/C++ABI 不匹配),解决方案包括统一动态链接、规范跨模块接口设计(避免传递 C++ 对象)及固化工具链版本。

🐕 Detecting body poses in a live video feed

@AidenRao:这篇文章介绍了如何利用苹果的 Vision 框架在 SwiftUI 应用中实时检测视频流中的人体姿态。主要步骤包括:

  1. 通过 AVCaptureSession 管理视频采集,处理权限申请,配置视频输入 / 输出格式,并调整画面方向以适应界面显示
  2. 使用 UIViewRepresentable 协议将 AVCaptureVideoPreviewLayer 集成到 SwiftUI 视图中,实现实时画面展示
  3. 创建 PoseEstimationViewModel 类处理视频帧,利用 Vision 的 VNHumanBodyPoseRequest 检测 19 个关键身体节点(如头、肩、肘、腕等),并建立关节连接关系(如肩到肘、膝到踝等)。
  4. 通过检测到的身体坐标点动态绘制骨骼连线,最终实现在视频流上叠加姿态骨架图的效果

文章提供了完整的代码实现思路,适用于健身追踪、AR 等场景开发。

🐎 Why I Avoid Group

@DylanYang:作者通过一系列的 Demo 向我们展示了 Group 目前存在的一些问题。官方文档上写着作用于 Group 的 modifier 会作用在 Group 内的每一个元素上,但实际在模拟器上的表现并非如此。作者猜测可能和渲染 SwiftUI 的根结点实质是一个 UIKit 的 view 有关。如果使用一个 VStack 包裹 Group,则它的表现又符合预期了。

🐎 这篇 GPU 学习笔记,详细整理了其工作原理、编程模型和架构设计

@老驴:最近 AI 大火也让 nvidia 再次在风口浪尖烧,你有没有想过 GPU 到底是怎么工作的?它为什么在特定应用下这么快?和我们日常写的代码、性能优化有什么关系?

这篇文章用清晰的逻辑和大量实例,从 GPU 架构原理、并行执行模型到实际的 CUDA 编程,带你系统了解 GPU 背后的技术基础。即使你平时不写 CUDA,这些知识也能帮你更好地理解 Metal、CoreML 这些 Apple 平台下的 GPU 特性。

如果你是一名希望拓展技术广度的开发者,值得一读。

代码

FlowNavigation

Announcing FlowNavigation 本文介绍了作者开发的一个名为 FlowNavigation 的 SwiftUI 框架。该框架旨在简化线性流程的定义和管理 , 如注册流程。作者提供了一个示例代码 , 展示如何使用该框架来定义一个包含电子邮件、密码和提交屏幕的流程。该框架目前还在测试阶段,作者希望能够获得社区反馈并继续完善。

内推

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

[上海] Soul App - 终端技术团队 - 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)

iOS 中的 RunLoop 详解

作者 90后晨仔
2025年3月30日 20:03

1. 什么是 RunLoop?

RunLoop(运行循环) 是 iOS/macOS 开发中用于管理线程事件和消息的核心机制。它通过一个循环不断监听和处理输入事件(如触摸、定时器、网络数据),并在没有任务时让线程休眠以节省资源。

  • 本质:基于 mach port 的事件循环机制。
  • 核心作用:让线程在有任务时执行任务,无任务时休眠,避免资源浪费。

2. RunLoop 与线程的关系

  • 主线程:默认自动创建并启动 RunLoop(UIApplicationMain 函数中启动)。
  • 子线程:默认不开启 RunLoop,需手动创建和启动。
  • 生命周期:线程与 RunLoop 一一对应,存储在全局字典中(线程销毁时 RunLoop 也释放)。

3. RunLoop 的核心组成

组件 作用
Modes(模式) 定义 RunLoop 在不同场景下监听的事件源(Source)和观察者(Observer)。
Sources(事件源) - Source0:处理 App 内部事件(如触摸、PerformSelector)。
- Source1:基于 mach port 的系统事件(如硬件事件)。
Timers(定时器) 基于 RunLoop 的定时器(如 NSTimer),受 RunLoop Mode 影响。
Observers(观察者) 监听 RunLoop 的状态变化(如即将处理事件、即将休眠等),用于性能监控。

4. RunLoop 的 Mode(模式)

模式 描述
Default(NSDefaultRunLoopMode 默认模式,处理大多数事件(如普通触摸、网络请求)。
Tracking(UITrackingRunLoopMode 界面滚动时的模式(如 UIScrollView 滑动),优先保证滑动流畅。
Common(NSRunLoopCommonModes 一个虚拟模式,包含多个模式的集合(Default + Tracking),用于通用事件监听。

5. RunLoop 的典型应用场景

(1) 保持线程存活

通过 RunLoop 让子线程持续处理任务而不退出:

class BackgroundThread {
    private var thread: Thread?
    private var shouldKeepRunning = true
    
    func start() {
        thread = Thread { [weak self] in
            // 创建并启动 RunLoop
            let runLoop = RunLoop.current
            runLoop.add(NSMachPort(), forMode: .default)
            while self?.shouldKeepRunning == true {
                runLoop.run(mode: .default, before: .distantFuture)
            }
        }
        thread?.start()
    }
    
    func stop() {
        shouldKeepRunning = false
        thread?.cancel()
    }
}

(2) 优化滚动性能

将耗时任务放在 Default 模式,避免滚动时执行(如 UIScrollView 滑动):

DispatchQueue.main.async {
    // 在 Default 模式下执行任务
    RunLoop.current.perform(inMode: .default) {
        // 处理耗时任务(如数据解析)
    }
}

(3) 监听 RunLoop 状态

通过 CFRunLoopObserver 监控主线程卡顿:

let observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, CFRunLoopActivity.allActivities.rawValue, true, 0) { observer, activity in
    switch activity {
    case .beforeWaiting:
        print("RunLoop 即将休眠")
    case .afterWaiting:
        print("RunLoop 被唤醒")
    default: break
    }
}
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, .commonModes)

(4) 解决定时器失效问题

在滚动模式下(如 UIScrollView 滑动时),将 NSTimer 添加到 CommonModes

let timer = Timer(timeInterval: 1.0, repeats: true) { _ in
    print("Timer fired")
}
RunLoop.current.add(timer, forMode: .common)

6. RunLoop 的工作流程

  1. 接收事件:通过 mach port 接收系统事件(Source1)或 App 内部事件(Source0)。
  2. 通知 Observers:即将处理事件(kCFRunLoopBeforeTimerskCFRunLoopBeforeSources)。
  3. 处理事件:执行触发的 Source0、Source1 和 Timer 事件。
  4. 进入休眠:若没有事件,调用 mach_msg 让线程休眠,等待新事件唤醒。
  5. 唤醒处理:被唤醒后处理新到达的事件,循环往复。

7. 注意事项

  • 避免阻塞主线程:主线程 RunLoop 负责 UI 更新,长时间阻塞会导致卡顿。
  • 子线程 RunLoop 管理:手动创建的 RunLoop 需自行控制生命周期(如添加保活 Source)。
  • 性能优化:合理使用 Mode 隔离不同任务(如滑动时暂停后台任务)。

总结

场景 RunLoop 的作用 关键技术点
线程保活 让子线程持续处理任务 添加保活 Source(如 NSMachPort
滑动性能优化 隔离耗时任务与滚动事件 使用 CommonModes 或模式切换
定时器管理 确保定时器在滚动时仍有效 注册到 CommonModes
卡顿监控 监听主线程 RunLoop 状态变化 CFRunLoopObserver

RunLoop 是 iOS 系统高效管理线程和事件的核心机制,合理利用可优化性能、实现复杂任务调度。在大多数情况下,开发者无需直接操作 RunLoop,但理解其原理对解决卡顿、线程管理等问题至关重要。

CSPJ 教学思考:模拟

作者 唐巧
2025年3月29日 23:06

模拟是最有效的练习编程熟练度的基础算法,也是有效的掌握各种编程技巧的练习方式。

本文将把各种模拟技巧与题目结合,用题目带着学生掌握这些模拟技巧。

二维数组包边

有些时候,我们在处理二维数组的时候,需要处理 x,y 坐标的边界。这样写起来会比较麻烦,但是,如果我们将数据从下标 1 开始保存,那么就人为在数据外面留了一圈缓冲带。这个时候,在处理 x,y 周围坐标的时候,就不会出现数据下标越界的情况了。

例题:P2670 NOIP 2015 普及组 扫雷游戏

该题如果正常写,需要判断每个格子周围 8 个格子的状态。如果我们把数据从 1 开始读入,在判断的时候就容易很多。以下是参考代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/**
* P2670 [NOIP 2015 普及组] 扫雷游戏
*
* Author: Tang Qiao
*/
#include <bits/stdc++.h>
using namespace std;

int n, m;
char tu[110][110];
int movex[] = {-1, -1, -1, 0, 0, 1, 1, 1};
int movey[] = {-1, 0, 1, -1, 1, -1, 0, 1};

int main() {
cin >> n >> m;
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
cin >> tu[i][j];
}
}

for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
if (tu[i][j] == '*') continue;
int cnt = 0;
for (int k = 0; k < 8; ++k) {
int x = i + movex[k];
int y = j + movey[k];
if (tu[x][y] == '*') cnt++;
}
tu[i][j] = cnt + '0';
}
}

for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
cout << tu[i][j];
}
cout << endl;
}
return 0;
}

练习:B4248 语言月赛 202503 数字棋盘

本题也可以用包边的技巧,保证数据在检查的时候不会越界。参考代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <bits/stdc++.h>
using namespace std;

int n, m;
int a[1001][1001];
int x, y;

bool check(int i, int j) {
// 检查上方格子
if (i > 1 && a[i-1][j] == y) return true;
// 检查下方格子
if (i < n && a[i+1][j] == y) return true;
// 检查左侧格子
if (j > 1 && a[i][j-1] == y) return true;
// 检查右侧格子
if (j < m && a[i][j+1] == y) return true;
return false;
}

int main() {
ios::sync_with_stdio(false);
cin >> n >> m;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
cin >> a[i][j];
}
}
cin >> x >> y;
int count = 0;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (a[i][j] == x && check(i, j)) {
count++;
}
}
}
cout << count << endl;
return 0;
}

围圈数数

有一种模拟题,要求我们把人围成一个圈,在圈上数数,然后问你数到的是谁。类似于小时候玩的“点兵点将”游戏,可能是顺时针数,也可能是逆时针数。

对于这种数数题目,最简单的做法是:直接用加减来进行目标的选择。加减之后,下标可能变负数或者超过总数,这个时候进行简单的取模调整,就可以将下标调整正常。

例题一:P1563 NOIP 2016 提高组 玩具谜题

此题我们:

  • idx = (idx + b) % n; 来完成顺时针数
  • idx = (idx - b + n) % n; 来完成逆时针数

通过这样的简单的加减和取模,保证能够快速跳到目标位置,完成模拟操作。完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* Author: Tang Qiao
*/
#include <bits/stdc++.h>
using namespace std;
#define MAXN int(1e5 + 10)

int n, m;
int face[MAXN];
string name[MAXN];

int main() {
cin >> n >> m;
for (int i = 0; i < n; ++i) {
cin >> face[i] >> name[i] ;
}

int idx = 0;
for (int i = 0; i < m; ++i) {
int a, b;
cin >> a >> b;
// 圈内向左 == 圈外向右
if ((face[idx] == 0 && a == 0)
|| (face[idx] == 1 && a == 1)) {
idx = (idx - b + n) % n;
} else {
idx = (idx + b) % n;
}
}
cout << name[idx] << endl;
return 0;
}

例题二:B4246 语言月赛 202503 环形游走

此题有个技巧:就是走的时候可能绕多圈,这个时候我们先把要走的步数模 n: step % n, 这样就把前面的多圈跳过了,也不会把坐标减成特别特别小的负数。

参考代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* Author: Tang Qiao
*/
#include <bits/stdc++.h>
using namespace std;

int main() {
int n, m;
cin >> n >> m;
vector<int> a(n);
for (int i = 0; i < n; i++) {
cin >> a[i];
}
int current = 0;
for (int i = 0; i < m; i++) {
int step = a[current] % n;
current = (current - step + n) % n;
}
cout << current + 1 << endl;
return 0;
}

矩阵变换

矩阵变换这类模拟题,会要求我们在一个二维的数组上进行各种操作,包括填充,旋转,查找,合并等。需要我们熟悉各种矩阵变换的技巧。

例题:P5725【深基4.习8】求三角形

此题是一道基础的填充题。

  • 对于第一种要求,我们用在二维数组上填充实现。
  • 对于第二种要求,我们直接输出结果,在合适的位置加上一些空格。

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
* Author: Tang Qiao
*/
#include <bits/stdc++.h>
using namespace std;

int tu[11][11];
int n;
int main() {
cin >> n;

// 处理第一种要求
int cnt = 1;
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= n; ++j) {
tu[i][j] = cnt++;
}
}
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= n; ++j) {
printf("%02d", tu[i][j]);
}
printf("\n");
}
printf("\n");
// 处理第二种要求
cnt = 1;
int bk = n-1;
for (int i = 1; i <= n; ++i, bk--) {
for (int j = 1; j <= bk; ++j) printf(" ");
for (int j = 1; j <= i; ++j) {
printf("%02d", cnt++);
}
printf("\n");
}

return 0;
}

例题:P5731 蛇形方阵

蛇形方阵是一道基础题,用于练习二维数组上的操作。我使用的模拟技巧是:

  • 定义一个 order 变量,表示当前方向
  • 与 order 变量配合,定义一个 movex 和 movey 数组,表示当前方向的移动

相关代码是:

1
2
3
int order;
int movex[] = {0, 1, 0, -1};
int movey[] = {1, 0, -1, 0};

每次移动,先判断是否越界或者已经填充过值:

  • 如果越界或已经填充过值,则改变方向再移动
  • 如果没越界,则移动

关键代码如下:

1
2
3
4
5
if (nx < 1 || nx > n || ny < 1 || ny > n || tu[nx][ny] != 0) {
order = (order + 1) % 4;
nx = x + movex[order];
ny = y + movey[order];
}

因为要填充 n*n 个数,所以循环一共执行 n*n 次。

完整的参考代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
* Author: Tang Qiao
*/
#include <bits/stdc++.h>
using namespace std;

int n, x, y, order;
int tu[15][15];
int movex[] = {0, 1, 0, -1};
int movey[] = {1, 0, -1, 0};
int main() {
cin >> n;
memset(tu, 0, sizeof(tu));
x = 1;
y = 0;
order = 0;
for (int i = 1; i <= n*n; i++) {
int nx = x + movex[order];
int ny = y + movey[order];
if (nx < 1 || nx > n || ny < 1 || ny > n || tu[nx][ny] != 0) {
order = (order + 1) % 4;
nx = x + movex[order];
ny = y + movey[order];
}
x = nx;
y = ny;
tu[x][y] = i;
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
printf("%3d", tu[i][j]);
}
printf("\n");
}
return 0;
}

例题:P4924 1007 魔法少女小Scarlet

本题涉及矩阵的旋转,实际操作起来还是有点麻烦。这里我们按旋转的中心来重建坐标系的话,可以观察到如下规律:

  • 顺时针旋转:(a, b) -> (b, -a)
  • 逆时针旋转:(a, b) -> (-b, a)

这样,我们就可以构建关键的旋转代码了,假如我们是基于中心点 (x, y) 半径是 r 的顺时针旋转的话,那么,对于坐标 (a, b),我们:

  • 首先:把它移动到以 (x, y) 为中心:(a-x, b-y)
  • 然后:我们把坐标按上面的规则变换成 (b-y, x-a)
  • 最后:我们把坐标加上 (x, y) 的偏移,还原成原始坐标:(b-y+x, x-a+y)

以上逻辑写成代码是:g[b-y+x][x-a+y]=f[a][b]

同理,如果是逆时针旋转:

  • 首先:把它移动到以 (x, y) 为中心:(a-x, b-y)
  • 然后:我们把坐标按上面的规则变换成 (y-b, a-x)
  • 最后:我们把坐标加上 (x, y) 的偏移,还原成原始坐标:(y-b+x, a-x+y)

以上逻辑写成代码是:g[y-b+x][a-x+y]=f[a][b]

本题保证了数据不会在旋转时越界,整体的参考代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/**
* Author: Tang Qiao
*/
#include <bits/stdc++.h>
using namespace std;

#define MAXN 510
int f[MAXN][MAXN], g[MAXN][MAXN];
int n, m;
int main() {
cin >> n >> m;
int cnt = 1;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
f[i][j] = cnt++;
}
}
for (int i = 1; i <=m; ++i) {
int x, y, r, z;
cin >> x >> y >> r >> z;
if (z == 0) {
for (int a = x-r; a <= x+r; ++a)
for (int b = y-r; b <= y+r; ++b)
g[b-y+x][x-a+y]=f[a][b];

} else {
for (int a = x-r; a <= x+r; ++a)
for (int b = y-r; b <= y+r; ++b)
g[y-b+x][a-x+y]=f[a][b];
}

for (int a = x-r; a <= x+r; ++a)
for (int b = y-r; b <= y+r; ++b)
f[a][b] = g[a][b];
} // end of m
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
cout << f[i][j] << " ";
}
cout << endl;
}
return 0;
}

游戏模拟

游戏模拟类的题目通常会告诉你一个相对复杂一点的游戏规则,然后让你用程序将这个游戏规律实现,最终将游戏的结果输出出来。

这种题目一方面考查了读题能力,需要对游戏规则的理解清楚,另一方面则是要对游戏规则进行建模,用合适的数据结构实现游戏中的模拟。

以下是一些相关的题目。

题号 描述
P1042 NOIP 2003 普及组 乒乓球
P1328 NOIP 2014 提高组 生活大爆炸版石头剪刀布
P1518 USACO2.4 两只塔姆沃斯牛 The Tamworth Two

其它模拟题目

题号 描述

Swift基础知识(三)

作者 90后晨仔
2025年3月29日 19:21

Swift 与 Objective-C 中的自省(Introspection)对比


1. 核心概念

  • 自省(Introspection):在运行时检查对象的类型或是否符合特定协议的能力。
  • Objective-C:基于 NSObject 的类体系,主要用于检查类继承关系。
  • Swift:支持所有类型(类、结构体、枚举),不依赖 NSObject,且涵盖协议检查。

2. 类型检查语法与行为

特性 Objective-C Swift
检查类继承关系 isKindOfClass:(判断类或子类)
isMemberOfClass:(仅判断自身类)
is 操作符(判断类型或子类型)
支持类型 NSObject 子类 所有类型(类、结构体、枚举)
协议一致性检查 conformsToProtocol: is(检查协议类型)
as?(转换并检查协议)
值类型支持 不支持(仅类) 支持(结构体、枚举)
泛型支持 支持(结合泛型类型检查)

3. 具体用法与示例

Objective-C
// 类继承检查
BOOL isView = [obj isKindOfClass:[UIView class]];       // YES(obj 是 UIView 或其子类实例)
BOOL isExactView = [obj isMemberOfClass:[UIView class]]; // NO(obj 必须是 UIView 实例)

// 协议检查
BOOL conforms = [obj conformsToProtocol:@protocol(NSCopying)];
Swift
// 类型检查
let obj: Any = "Hello"
if obj is String { // true
    print("是 String 类型")
}

// 协议检查
protocol Drawable { func draw() }
struct Circle: Drawable { func draw() { } }

let shape: Any = Circle()
if shape is Drawable { // true
    print("符合 Drawable 协议")
}

// 值类型检查
let value: Any = 100
if value is Int { // true
    print("是 Int 类型")
}

// 类型转换 + 检查
if let drawableShape = shape as? Drawable {
    drawableShape.draw()
}

4. 核心区别总结

维度 Objective-C Swift
类型体系依赖 必须继承自 NSObject 不依赖任何基类,支持所有类型
检查范围 仅类(包括子类) 类、结构体、枚举、协议
协议检查 需显式调用 conformsToProtocol: 直接通过 isas? 检查
语法简洁性 方法调用(冗长) 操作符(简洁直观)
泛型支持 支持泛型类型检查

5. 高级特性(Swift 独有)

  • 模式匹配:结合 switch 语句进行类型和协议匹配。
    func checkType(_ value: Any) {
        switch value {
        case is String: print("字符串类型")
        case let num as Int where num > 0: print("正整数")
        case is Drawable: print("可绘制对象")
        default: break
        }
    }
    
  • 元类型检查:通过 .Type 获取类型元信息。
    let type: Int.Type = Int.self
    let instance = type.init(10) // 创建 Int 实例
    

6. 使用建议

  • Objective-C:在维护旧项目或与 Cocoa 框架交互时使用,注意仅适用于 NSObject 子类。
  • Swift:在新项目中优先使用 isas?,充分利用其对值类型和协议的支持,提升代码灵活性和安全性。

三、Swift 闭包(Closures)与 Objective-C Block 的对比

1. 内存分配与结构

特性 Swift 闭包 Objective-C Block
底层数据结构 闭包是 捕获上下文的函数,本质是结构体 Block 是 封装函数指针的结构体对象
内存位置 默认栈分配,逃逸闭包自动提升到堆 默认栈分配,需显式 copy 到堆
结构体布局 闭包结构体包含函数指针和捕获的上下文数据 Block 结构体包含 isa 指针、函数指针、捕获变量等
生命周期管理 通过 引用计数(ARC) 管理堆内存 手动 copy/release 或 ARC 管理堆内存

2. 变量捕获机制

特性 Swift 闭包 Objective-C Block
值类型捕获 捕获值类型的副本(深拷贝) 默认捕获值类型变量的 原始值(需 __block 修饰允许修改)
引用类型捕获 捕获引用类型的强引用(需通过捕获列表弱化) 捕获对象的强引用(需 __weak 弱化)
可变性支持 通过 varinout 参数捕获可变变量 __block 修饰符实现变量可变性

3. 函数执行与上下文

特性 Swift 闭包 Objective-C Block
函数指针 闭包的函数指针直接指向编译生成的函数代码 Block 的函数指针通过 invoke 成员指向代码
上下文管理 闭包通过结构体存储捕获的变量(值或引用) Block 通过结构体的 descriptor 管理捕获变量引用计数
逃逸性处理 编译器自动检测逃逸闭包,并生成堆分配逻辑 需手动调用 copy 将 Block 从栈复制到堆

4. 底层实现细节

Swift 闭包
  1. 结构体表示

    // 伪代码表示闭包结构体
    struct Closure<T> {
        var function: (T) -> Void  // 函数指针
        var capturedValues: [Any]  // 捕获的上下文数据
    }
    
  2. 内存管理

    • 非逃逸闭包:栈分配,函数返回后销毁。
    • 逃逸闭包:自动复制到堆,由 ARC 管理生命周期。
  3. 捕获列表优化

    • 显式声明 [weak self][unowned self],避免隐式强引用。
    • 编译器生成代码时,捕获列表直接修改闭包结构体的成员引用类型。
Objective-C Block
  1. 结构体表示

    // 伪代码表示 Block 结构体
    struct Block_layout {
        void *isa;                  // 指向 Block 类型(栈/堆/全局)
        int flags;                  // 状态标记(是否被拷贝等)
        int reserved;               // 保留字段
        void (*invoke)(void *, ...); // 函数指针
        struct Block_descriptor *descriptor; // 描述符(引用计数、捕获变量等)
        // 捕获的变量数据...
    };
    
  2. 内存管理

    • 栈 Block:默认创建在栈上,函数返回后失效。
    • 堆 Block:通过 copy 操作复制到堆,由引用计数管理。
  3. 变量捕获

    • __block 修饰的变量会被包装为 Block_byref 结构体,允许跨 Block 修改。
    • 对象类型变量通过 retain/release 管理引用计数(ARC 下自动处理)。

5. 性能对比

维度 Swift 闭包 Objective-C Block
内存开销 更小(结构体直接存储捕获数据) 较大(包含 isaflags 等元数据)
执行速度 更快(直接函数指针调用) 稍慢(需通过 invoke 间接调用)
捕获变量修改 灵活(值类型副本独立修改) __block 包装,性能开销较大

6. 总结

  • Swift 闭包

    • 更轻量:基于结构体和值语义,减少内存开销。
    • 更安全:编译时检查捕获列表,避免循环引用。
    • 更高效:栈分配优化 + 直接函数调用。
  • Objective-C Block

    • 更底层:直接操作结构体和引用计数,灵活性高。
    • 兼容性:与 Cocoa 框架深度集成,适合传统代码维护。
    • 显式控制:需手动管理 copy__block 修饰符。

底层核心差异

  • Swift 闭包是 值类型结构体,通过编译优化实现高效捕获和内存管理。
  • Objective-C Block 是 对象,依赖运行时元数据(isadescriptor)管理生命周期和捕获变量。

Swift基础知识(二)

作者 90后晨仔
2025年3月29日 18:53

一、Swift 属性观察器(Property Observers)

1. 核心概念

属性观察器用于监听存储属性值的变化,在值被修改前后执行自定义逻辑。包含两种观察器:

  • willSet:在值即将被设置前调用,默认提供 newValue 参数(新值)。
  • didSet:在值已被设置后调用,默认提供 oldValue 参数(旧值)。

2. 使用条件

  • 适用对象:仅用于 lazyvar 存储属性
    var score: Int = 0 {  // 正确:非 lazy 的存储属性
        willSet { print("新值:\(newValue)") }
        didSet { print("旧值:\(oldValue)") }
    }
    
    // lazy var data: [Int] = loadData() { willSet { } } // 错误:lazy 属性不支持
    
  • 不适用场景
    • 常量(let 属性)。
    • 计算属性(需通过 set 方法监听)。
    • 延迟存储属性(lazy var)。

3. 注意事项

  • 初始化不触发:属性在初始化赋值时不会调用观察器。
    struct User {
        var name: String = "Guest" { // 初始化赋值不会触发观察器
            didSet { print("名字被修改") }
        }
    }
    
  • 自定义参数名:可显式命名参数(如 willSet(newScore))。
  • 避免循环修改:在 didSet 中修改属性本身会再次触发观察器。

4. 使用场景

  • 数据验证:确保值在合法范围内。
    var age: Int = 0 {
        didSet {
            age = min(max(age, 0), 120)  // 限制年龄在 0~120
        }
    }
    
  • UI 同步更新:值变化时刷新界面。
    var isLoggedIn: Bool = false {
        didSet {
            updateLoginUI()
        }
    }
    
  • 日志记录:跟踪属性变化历史。
    var temperature: Double = 25.0 {
        willSet { print("温度将从 \(temperature) 变为 \(newValue)") }
        didSet { print("温度已从 \(oldValue) 更新为 \(temperature)") }
    }
    

5. 与计算属性的区别

特性 属性观察器 计算属性
用途 监听存储属性变化 通过计算动态获取值
触发时机 属性值被修改前后 每次访问时计算
存储能力 需有存储空间(必须初始化) 无存储空间(依赖其他属性)
语法 willSet/didSet get + set(或只读 get

6. 总结

  • 核心作用:在属性值变化前后注入逻辑,提升代码灵活性和可维护性。
  • 适用场景:数据验证、日志记录、UI 同步等。
  • 限制:仅适用于非 lazyvar 存储属性,初始化不触发。

二、Swift 中的异常捕获

Swift 通过 Error 协议和 throws 关键字实现异常处理,提供多种方式捕获和处理错误。以下是主要的异常捕获方法:

1. do-catch 语句(详细错误处理)

  • 用法:捕获特定错误类型并处理。
  • 适用场景:需要根据不同错误类型执行不同逻辑。
  • 示例
    enum NetworkError: Error {
        case invalidURL
        case timeout(seconds: Int)
    }
    
    func fetchData() throws {
        throw NetworkError.timeout(seconds: 30)
    }
    
    do {
        try fetchData()
    } catch NetworkError.invalidURL {
        print("URL 无效")
    } catch NetworkError.timeout(let seconds) {
        print("请求超时:\(seconds) 秒")
    } catch {
        print("未知错误:\(error)")
    }
    

2. try?(转换为可选类型)

  • 用法:忽略具体错误,返回 nil
  • 适用场景:不关心错误细节,只需判断是否成功。
  • 示例
    let result = try? someThrowingFunction()
    if let data = result {
        // 成功
    } else {
        // 失败
    }
    

3. try!(强制解包,慎用)

  • 用法:假设函数不会抛出错误,若出错则触发运行时崩溃。
  • 适用场景:确信代码不会失败(如本地静态数据加载)。
  • 示例
    let data = try! loadLocalConfig() // 确定本地文件存在时使用
    

4. 向上传递错误(throws 关键字)

  • 用法:函数声明 throws,错误由调用者处理。
  • 适用场景:将错误传递给上层调用链。
  • 示例
    func processFile() throws {
        let content = try String(contentsOfFile: "path/to/file")
        // 处理内容
    }
    
    // 调用处需处理或继续传递
    do {
        try processFile()
    } catch {
        print("文件处理失败:\(error)")
    }
    

5. rethrows(传递闭包中的错误)

  • 用法:函数本身不产生错误,但可能传递闭包的异常。
  • 适用场景:高阶函数(如 mapfilter)中处理闭包可能抛出的错误。
  • 示例
    func customMap<T>(_ array: [T], _ transform: (T) throws -> T) rethrows -> [T] {
        var result = [T]()
        for item in array {
            try result.append(transform(item))
        }
        return result
    }
    
    let numbers = [1, 2, 3]
    let doubled = try? customMap(numbers) { num in
        if num == 2 { throw NetworkError.invalidURL }
        return num * 2
    }
    

6. 异步错误处理(async/await

  • 用法:在异步函数中使用 throwstry
  • 适用场景:异步操作中的错误处理。
  • 示例
    func downloadData() async throws -> Data {
        let url = URL(string: "https://example.com")!
        let (data, _) = try await URLSession.shared.data(from: url)
        return data
    }
    
    Task {
        do {
            let data = try await downloadData()
        } catch {
            print("下载失败:\(error)")
        }
    }
    

总结

方法 关键字 特点 适用场景
详细错误处理 do-catch 精准捕获错误类型 需要区分不同错误逻辑
可选值简化 try? 忽略错误,返回 nil 不关心错误细节
强制解包(高危) try! 假设成功,崩溃风险高 确定不会失败的场景
向上传递错误 throws 将错误传递给调用者 多层调用链中的错误处理
闭包错误传递 rethrows 传递闭包中的错误 高阶函数中的闭包操作
异步错误处理 async throws 结合 async/await 处理异步错误 网络请求、文件读写等异步操作

最佳实践

  • 优先使用 do-catch 处理可预见的错误。
  • 谨慎使用 try!,确保代码绝对安全。
  • 异步操作中结合 async/await 提升可读性。

三、Swift 中 defer 关键字的详解

1. 基本概念

defer 用于定义一个代码块(延迟执行块),该代码块会在 当前作用域结束前 执行,无论作用域是通过正常返回、抛出错误还是其他控制流(如 returnbreak)结束的。
核心作用:确保清理或收尾逻辑(如资源释放)一定会执行,避免资源泄漏。


2. 使用场景

  • 资源管理:文件操作、网络请求、锁的获取与释放等。
  • 错误处理:在抛出错误前确保清理逻辑执行。
  • 代码可读性:将清理代码紧跟在初始化代码后,提高可维护性。

3. 基本用法

func readFile() {
    let file = openFile()
    defer {
        closeFile(file) // 作用域结束前执行
    }
    // 处理文件...
    if someCondition {
        return // 触发 defer
    }
    // 更多操作...
} // 函数结束,触发 defer

4. 执行规则

  • 逆序执行:同一作用域内的多个 defer定义顺序的逆序 执行(类似栈结构)。
    defer { print("1") }
    defer { print("2") }
    // 输出:2 → 1
    
  • 作用域限制defer 仅在 当前作用域 有效(如函数、循环、条件语句)。
    func example() {
        if condition {
            defer { print("if 块结束") }
            // ...
        } // 此处执行 defer
        // 函数后续代码...
    }
    

5. 常见应用示例

(1) 文件操作
func readFile(path: String) throws -> String {
    let file = try openFile(path)
    defer {
        closeFile(file) // 确保文件关闭
    }
    return try parseFile(file)
}
(2) 加锁与解锁
let lock = NSLock()
func criticalSection() {
    lock.lock()
    defer { lock.unlock() } // 确保锁释放
    // 执行关键代码...
}
(3) 循环中的资源清理
for url in urls {
    let data = try downloadData(from: url)
    defer { 
        cleanupTemporaryFiles() // 每次循环结束清理
    }
    process(data)
}

6. 注意事项

  • 禁止控制流操作defer 块中不能使用 breakreturn 或抛出错误。
  • 避免副作用:修改外部变量可能导致逻辑混乱。
    var value = 0
    func example() {
        defer { value += 1 } // 不推荐
        // ...
    }
    
  • 性能影响:频繁使用 defer 可能影响性能(极少数情况)。

7. 错误处理中的 defer

即使函数抛出错误,defer 仍会执行:

func riskyOperation() throws {
    let resource = allocateResource()
    defer { releaseResource(resource) } // 错误抛出前执行
    try mayThrowError()
}

8. 总结

场景 示例 作用
文件/网络资源释放 defer { file.close() } 防止资源泄漏
锁的获取与释放 defer { lock.unlock() } 避免死锁
错误处理中的清理 defer { cleanup() } 确保异常时资源释放
临时数据清理 defer { removeTempFiles() } 提升代码健壮性

最佳实践

  • defer 紧跟在资源获取代码后,提高可读性。
  • 避免在 defer 中执行耗时操作或修改外部状态。
  • 优先用于必须执行的清理逻辑,而非复杂业务代码。

通过合理使用 defer,可以显著提升代码的健壮性和可维护性,确保资源安全和逻辑清晰。


四、Swift 与 Objective-C 中的 Protocol 区别

1. 基本概念

  • Objective-C:Protocol 是一种定义方法列表的方式,用于声明接口,实现类必须遵守这些接口(除非标记为 @optional)。
  • Swift:Protocol 不仅定义接口,还支持属性、方法、关联类型、默认实现等,是面向协议编程(POP)的核心工具。

2. 核心区别

特性 Objective-C Protocol Swift Protocol
可选方法 使用 @optional 标记可选方法 默认无可选方法,可通过 @objc optional 兼容 OC,或通过协议扩展提供默认实现
默认实现 不支持 支持通过 协议扩展 提供默认实现
关联类型 不支持 支持 associatedtype,实现泛型协议
值类型支持 仅适用于类(Class) 适用于类、结构体(Struct)、枚举(Enum)
协议继承与组合 单继承(只能继承一个协议) 支持多继承(ProtocolA & ProtocolB
泛型支持 支持泛型约束(where 子句)
属性定义 只能定义方法 可定义属性(需指定 { get }{ get set }
类型检查 运行时检查(conformsToProtocol: 编译时检查 + 运行时检查(isas?

3. 使用场景与示例

Objective-C Protocol
@protocol DataSource <NSObject>
@required
- (NSInteger)numberOfItems;
@optional
- (NSString *)titleForItemAtIndex:(NSInteger)index;
@end

// 类遵循协议
@interface ViewController : UIViewController <DataSource>
@property(nonatomic,weak)id <DataSource> delegate;
@end
  • 特点:主要用于委托模式(Delegate)、数据源模式(DataSource)。
Swift Protocol
protocol Drawable {
    func draw()
}

// 协议扩展提供默认实现
extension Drawable {
    func draw() { print("默认绘制") }
}

struct Circle: Drawable {} // 结构体遵循协议
class Square: Drawable {}  // 类遵循协议

// 关联类型
protocol Container {
    associatedtype Item
    var items: [Item] { get }
    mutating func add(_ item: Item)
}

// 泛型约束
func process<T: Container>(container: T) where T.Item: Equatable {
    // ...
}
  • 特点:支持面向协议编程,适用于值类型和引用类型,灵活扩展功能。

4. 协议扩展(Swift 独有)

Swift 允许通过扩展为协议添加默认实现,而 Objective-C 无法实现:

protocol Loggable {
    func log()
}

extension Loggable {
    func log() { print("日志记录") }
}

struct User: Loggable {} // 自动获得默认 log() 实现

5. 可选方法的实现方式

  • Objective-C:显式标记 @optional,调用时需检查 respondsToSelector:

    @protocol NetworkDelegate <NSObject>
    @optional
    - (void)didReceiveData:(NSData *)data;
    @end
    
    // 调用前检查
    if ([delegate respondsToSelector:@selector(didReceiveData:)]) {
        [delegate didReceiveData:data];
    }
    
  • Swift:通过协议扩展或 @objc optional(仅限类)实现可选方法。

    @objc protocol NetworkDelegate {
        @objc optional func didReceiveData(_ data: Data)
    }
    
    class Handler: NetworkDelegate {} // 可选实现方法
    
    // 调用时检查
    delegate?.didReceiveData?(data)
    

6. 总结

场景 Objective-C Swift
接口定义 委托、数据源等简单场景 面向协议编程、泛型抽象、功能扩展
类型支持 仅类 类、结构体、枚举
灵活性 基础功能,依赖运行时检查 编译时安全,支持默认实现和关联类型
跨平台设计 主要用于 iOS/macOS 开发 跨平台(iOS/macOS/服务器/开源项目)

选择建议

  • Objective-C:在传统项目或需要与 OC 代码交互时使用,适合简单接口定义。
  • Swift:在新项目或需要高度抽象、复用和类型安全时使用,充分发挥面向协议编程的优势。

五、Swift 与 Objective-C 初始化方法(init)有什么不一样?

1. 核心设计理念

  • Swift:强调 安全性严格性,通过两段式初始化编译时检查确保对象完整初始化。
  • Objective-C灵活性优先,依赖开发者自觉管理初始化过程,缺少编译时强制约束。

2. 主要区别

特性 Swift Objective-C
初始化阶段 两段式初始化(属性初始化 → 自定义操作) 无明确阶段划分
安全检查 强制所有非可选(non-optional)存储属性初始化 无强制检查,未初始化的属性可能为 nil 或默认值
可选属性 必须显式初始化或声明为 Optional 默认允许 nil(引用类型)
初始化器类型 支持 指定初始化器(Designated)和 便利初始化器(Convenience) 无明确分类,但可通过 NS_DESIGNATED_INITIALIZER 标记指定初始化器
修饰符 convenience(便利初始化器)、required(强制子类实现)、override(重写父类初始化器) 无类似关键字
可失败初始化器 支持 init?init! 返回 nil 表示失败
继承与重写规则 子类必须重写父类的指定初始化器或自动继承,规则严格 子类可自由重写初始化器,无强制要求
值类型支持 结构体(struct)、枚举(enum)可定义初始化器 仅类(class)支持初始化器

3. 关键机制详解

(1) 两段式初始化(Swift 独有)
  • 阶段 1:确保所有存储属性被初始化。
  • 阶段 2:在属性初始化完成后,进一步自定义实例(如调用方法、访问 self)。
  • 优势:避免属性未初始化就被使用,提升安全性。
  • 示例
    class Person {
        var name: String
        var age: Int
        
        // 指定初始化器
        init(name: String, age: Int) {
            self.name = name // 阶段1:初始化属性
            self.age = age
            // 阶段2:可调用方法
            setup()
        }
        
        convenience init() {
            self.init(name: "Unknown", age: 0) // 必须调用指定初始化器
        }
        
        private func setup() { /*...*/ }
    }
    
(2) 强制属性初始化(Swift)
  • 所有非可选存储属性必须在初始化完成前赋值,否则编译器报错。
  • 示例
    class User {
        var id: Int    // 错误:未初始化
        var name: String?
        
        init(id: Int) {
            self.id = 1234 // 正确:非可选属性必须赋值
        }
    }
    
(3) 初始化器修饰符(Swift)
  • convenience:定义便利初始化器,必须调用同类中的指定初始化器。
    class Rectangle {
        var width: Double
        var height: Double
        
        init(width: Double, height: Double) {
            self.width = width
            self.height = height
        }
        
        convenience init(side: Double) {
            self.init(width: side, height: side) // 调用指定初始化器
        }
    }
    
  • required:强制子类实现该初始化器。
    class Vehicle {
        required init() { /*...*/ }
    }
    
    class Car: Vehicle {
        required init() { // 必须实现
            super.init()
        }
    }
    
(4) 可失败初始化器
  • Swift:通过 init? 返回可选实例,init! 返回隐式解包实例。
    struct Temperature {
        let celsius: Double
        init?(celsius: Double) {
            guard celsius >= -273.15 else { return nil }
            self.celsius = celsius
        }
    }
    
  • Objective-C:返回 nil 表示失败。
    @interface MyClass : NSObject
    - (instancetype)initWithValue:(NSInteger)value;
    @end
    
    @implementation MyClass
    - (instancetype)initWithValue:(NSInteger)value {
        if (value < 0) return nil;
        self = [super init];
        return self;
    }
    @end
    

4. 初始化器继承规则

  • Swift
    • 子类默认不继承父类初始化器。
    • 若子类未定义任何指定初始化器,则自动继承父类所有指定初始化器。
    • 若子类实现了父类所有指定初始化器,则自动继承父类便利初始化器。
  • Objective-C:子类自动继承父类所有初始化器,除非显式重写。

5. 总结

场景 Swift Objective-C
安全性 高(编译时强制检查) 低(依赖开发者自觉)
灵活性 较低(严格规则限制) 高(自由定义初始化逻辑)
代码复杂度 高(需遵循两段式、修饰符规则) 低(简单直接)
适用类型 类、结构体、枚举 仅类
错误处理 可失败初始化器、异常抛出 返回 nilNSError

选择建议

  • Swift:在需要高安全性、复杂类型设计的场景下使用,遵循严格初始化规则。
  • Objective-C:在维护旧项目或需要快速灵活初始化时使用,但需注意潜在风险。

iOS 中的 `@autoreleasepool` 详细解析

作者 90后晨仔
2025年3月29日 16:10

1. 什么是 @autoreleasepool

@autoreleasepoolObjective-CSwift 中用于管理 自动释放对象(Autoreleased Objects) 的机制。它的核心作用是 延迟对象的释放时机,避免内存峰值过高,尤其在需要频繁创建临时对象的场景下优化内存使用。


2. 为什么需要 @autoreleasepool

在 iOS 内存管理中:

  • MRC(手动引用计数):开发者需手动调用 retain/release 管理对象生命周期。
  • ARC(自动引用计数):编译器自动插入内存管理代码,但 自动释放池 依然用于管理某些场景下的临时对象。

核心问题
当一个对象被标记为 autorelease(如通过工厂方法 [NSString stringWithFormat:] 创建的对象),它会被添加到当前的 autoreleasepool 中,直到池子销毁时才释放。若未及时释放池子,大量临时对象可能导致内存峰值。


3. @autoreleasepool 的工作原理

  • 创建与销毁
    • 进入 @autoreleasepool 作用域时,系统会创建一个池子。
    • 退出作用域时,池子中的所有对象会收到 release 消息,若引用计数为 0 则释放。
  • 延迟释放
    对象不会立即释放,而是延迟到池子销毁时统一处理。
@autoreleasepool {
    // 临时对象会被添加到当前池子
    NSString *tempStr = [NSString stringWithFormat:@"Hello, %@", @"World"];
    // 池子销毁时,tempStr 会被释放(若引用计数为 0)
}

4. 使用场景

(1) 循环中创建大量临时对象

在循环中频繁生成临时对象时,使用 @autoreleasepool 及时释放内存:

for (int i = 0; i < 100000; i++) {
    @autoreleasepool {
        NSString *temp = [NSString stringWithFormat:@"Index: %d", i];
        // 每次循环结束后,temp 会被释放
    }
}
(2) 主线程 RunLoop 的自动释放池
  • 主线程 的 RunLoop 在每次事件循环(Event Loop)中会自动创建和销毁 autoreleasepool
  • 子线程 默认不自动创建池子,需手动添加:
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        @autoreleasepool {
            // 子线程中的代码
        }
    });
    
(3) 优化内存敏感操作
  • 解析大型 JSON/XML 文件。
  • 处理图像或视频数据时生成临时缓冲对象。

5. 底层机制

(1) NSAutoreleasePool@autoreleasepool
  • MRC 时代:通过 NSAutoreleasePool 类手动管理池子:
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    // 创建对象
    [pool drain]; // 释放池子
    
  • ARC 时代:使用 @autoreleasepool 语法糖,编译器会将其转换为高效的底层代码。
(2) 自动释放池的栈结构
  • 自动释放池以 形式管理,后进先出(LIFO)。
  • 嵌套使用 @autoreleasepool 时,内层池子优先释放:
    @autoreleasepool {
        // 外层池子
        @autoreleasepool {
            // 内层池子(优先释放)
        }
    }
    
(3) 对象如何被添加到池子?
  • 通过 autorelease 方法标记对象(ARC 下由编译器自动插入):
    // MRC 下显式调用
    NSString *str = [[[NSString alloc] initWithFormat:@"Test"] autorelease];
    

6. ARC 下的注意事项

  • 无需手动调用 autorelease:ARC 会自动插入内存管理代码。
  • 推荐使用 @autoreleasepool 语法:比 NSAutoreleasePool 更高效,且可读性更好。
  • 避免过度嵌套:仅在必要时使用,频繁创建池子可能影响性能。

7. 性能优化建议

  • 减少临时对象数量:尽量复用对象,避免循环内频繁创建。
  • 合理控制池子作用域:在内存敏感代码段精确包裹 @autoreleasepool
  • 避免主线程阻塞:耗时操作放在子线程,并为其添加池子。

8. 总结

场景 解决方案
循环中创建大量临时对象 在循环内部使用 @autoreleasepool
子线程中操作 手动添加 @autoreleasepool
解析大型数据文件 分段处理数据并包裹池子
优化内存敏感代码 精确控制池子作用域,及时释放临时对象

通过合理使用 @autoreleasepool,开发者可以有效管理内存峰值,提升应用性能和稳定性。

__block 与 __weak的区别是什么?

作者 90后晨仔
2025年3月29日 15:57

两者在功能上的区别?

__block会持有该对象,即使超出了该对象的作用域,该对象还是会存在的,直到block对象从堆上销毁;

__weak仅仅是将该对象赋值给weak对象,当该对象销毁时,weak对象将指向nil;

__block可以让block修改局部变量,而__weak不能。
MRC中__block是不会引起retain;但在ARC中__block则会引起retain。所以ARC中应该使用__weak。

循环引用的问题?

block下循环引用的问题

__block本身并不能避免循环引用,避免循环引用需要在block内部把__block修饰的obj置为nil

__weak可以避免循环引用,但是其会导致外部对象释放了之后,block 内部也访问不到这个对象的问题,我们可以通过在 block 内部声明一个 __strong的变量来指向 weakObj,使外部对象既能在 block 内部保持住,又能避免循环引用的问题。

KVC(Key-Value Coding)的底层实现分析

作者 90后晨仔
2025年3月29日 15:37

一、KVC(Key-Value Coding)的底层行为

1. KVC 赋值流程(setValue:forKey:

graph TD
A[调用 setValue:forKey:] --> B{是否存在 setKey: 或 _setKey: 方法?}
B -->|是| C[调用对应方法]
B -->|否| D[检查 +accessInstanceVariablesDirectly]
D -->|YES| E[按顺序查找成员变量 _key -> _isKey -> key -> isKey]
E --> F[找到则赋值]
D -->|NO| G[抛出 NSUnknownKeyException]

2. KVC 取值流程(valueForKey:

graph TD
A[调用 valueForKey:] --> B{是否存在 getKey/key/isKey/_key 方法?}
B -->|是| C[调用对应方法]
B -->|否| D[检查 +accessInstanceVariablesDirectly]
D -->|YES| E[按顺序查找成员变量 _key -> _isKey -> key -> isKey]
E --> F[找到则返回值]
D -->|NO| G[抛出 NSUnknownKeyException]

二、KVC 的底层行为与实现细节

1. setValue:forKey: 赋值流程详解

当调用 setValue:forKey: 时,KVC 按以下顺序查找并执行赋值操作:

方法优先查找

  1. set<Key>:
    查找与属性名匹配的 setter 方法(如属性 age 对应 setAge:)。
  2. _set<Key>:
    若未找到 set<Key>:,则查找带下划线的 setter 方法(如 _setAge:)。

成员变量次之

若未找到上述方法,检查 +accessInstanceVariablesDirectly 方法的返回值:

  • 返回 YES:按顺序查找成员变量:
    • _<key>_is<Key><key>is<Key>
      (例如 _age_isAgeageisAge)。
  • 返回 NO:直接抛出 NSUnknownKeyException 异常。

赋值结果处理

  • 找到成员变量:直接赋值。
  • 未找到:抛出异常。
// 示例:通过 KVC 设置私有成员变量
@interface Person : NSObject {
    @private
    NSString *_nickname;
}
@end

Person *p = [[Person alloc] init];
[p setValue:@"Jack" forKey:@"nickname"]; // 成功赋值 _nickname

2. valueForKey: 取值流程详解

当调用 valueForKey: 时,KVC 按以下顺序查找并返回值:

方法优先查找

  1. get<Key>
    查找与属性名匹配的 getter 方法(如 getName)。
  2. <key>
    直接查找与属性名相同的方法(如 name)。
  3. is<Key>
    查找布尔类型的 getter(如 isActive 对应属性 active)。
  4. _<key>
    查找带下划线的 getter(如 _name)。

成员变量次之

若未找到上述方法,检查 +accessInstanceVariablesDirectly 方法的返回值:

  • 返回 YES:按顺序查找成员变量:
    • _<key>_is<Key><key>is<Key>
      (例如 _age_isAgeageisAge)。
  • 返回 NO:抛出 NSUnknownKeyException 异常。

取值结果处理

  • 找到成员变量:返回其值。
  • 未找到:抛出异常。
// 示例:通过 KVC 获取私有成员变量
NSString *nickname = [p valueForKey:@"nickname"]; // 返回 _nickname 的值

3. KVC 方法查找与成员变量访问对比

操作类型 方法查找顺序 成员变量查找顺序 权限控制方法
赋值 setKey:_setKey: _key_isKeykeyisKey +accessInstanceVariablesDirectly
取值 getKeykeyisKey_key _key_isKeykeyisKey +accessInstanceVariablesDirectly

4. 常见场景与陷阱

场景 1:布尔类型属性的特殊处理

若属性为布尔类型(如 isActive),valueForKey:@"active" 会优先调用 isActive 方法:

@interface User : NSObject
@property (nonatomic, assign) BOOL isActive;
@end

User *user = [[User alloc] init];
user.isActive = YES;
NSNumber *isActive = [user valueForKey:@"active"]; // 调用 isActive 方法

场景 2:集合类型属性的 KVC 访问

若属性为集合(如 NSArray),valueForKey: 可能触发以下方法:

  • countOf<Key>:返回集合元素数量。
  • objectIn<Key>AtIndex::根据索引获取元素。
@interface Team : NSObject
- (NSUInteger)countOfMembers;
- (id)objectInMembersAtIndex:(NSUInteger)index;
@end

Team *team = [[Team alloc] init];
NSArray *members = [team valueForKey:@"members"]; // 触发上述方法

陷阱:直接访问私有成员变量

若未实现 getter/setter 且 accessInstanceVariablesDirectly 返回 NO,直接访问成员变量会崩溃:

@interface SecretData : NSObject {
    @private
    NSString *_password;
}
@end

@implementation SecretData
+ (BOOL)accessInstanceVariablesDirectly {
    return NO; // 禁止 KVC 访问成员变量
}
@end

SecretData *data = [[SecretData alloc] init];
[data setValue:@"123456" forKey:@"password"]; // 抛出 NSUnknownKeyException

5. 性能优化建议

  • 避免频繁使用 KVC:方法查找和消息转发会带来额外开销,性能敏感场景优先使用直接方法调用。
  • 缓存 Key Paths:若需多次访问同一属性,可将 Key Path 转换为指针或常量减少字符串解析开销。
  • 合理控制访问权限:通过重写 +accessInstanceVariablesDirectly 限制不必要的成员变量暴露。

三、面试题扩展

Q:valueForKey:objectForKey: 有何区别?

  • valueForKey:
    属于 KVC 方法,通过属性名或键路径查找值,支持方法调用和成员变量访问,可触发 KVO。
  • objectForKey:
    NSDictionary 的方法,直接从字典中根据键取值,不涉及 KVC/KVO 机制。

Q:如何阻止 KVC 访问私有成员变量?

重写 +accessInstanceVariablesDirectly 返回 NO

+ (BOOL)accessInstanceVariablesDirectly {
    return NO; // 禁止 KVC 直接访问成员变量
}
❌
❌