普通视图

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

让 Claude Code 在你睡觉时持续运行:完整实战指南

作者 唐巧
2026年4月15日 13:44

让 Claude Code 在你睡觉时持续运行:完整实战指南

Claude Code 可以通过 -p 标志、权限绕过、循环模式和终端持久化的组合,实现数小时甚至整夜的无人值守运行。 开发者社区已经形成了一套可靠的操作手册:容器化运行环境、使用 “Ralph Wiggum” 循环模式、安装四个关键 Hook 防止卡死、保持 CLAUDE.md 精简。有开发者记录了 27 小时连续自主会话完成 84 个任务;另一位在睡觉时让 Claude 构建了一个 15,000 行的游戏。但社区也反馈,大约 25% 的过夜产出会被丢弃,而且如果没有适当的防护措施,Claude 曾在至少一位开发者的机器上执行过 rm -rf /。以下是你今晚就能用上的完整设置方案。


一、消除人工干预的三种模式

Claude Code 提供三个级别的自主运行模式,每个级别都在安全性和速度之间做取舍。理解它们是所有过夜方案的基础。

模式 1:-p(print/pipe)标志 —— 所有自动化的核心。 这是非交互式运行模式。接收 prompt,执行到完成,输出到 stdout,然后退出。无需 TTY,512MB 内存的服务器也能跑。

1
claude -p "查找并修复 auth.py 中的 bug" --allowedTools "Read,Edit,Bash"

模式 2:--permission-mode auto —— 更安全的折中方案。 2026 年初推出,使用 Sonnet 4.6 分类器自动批准安全操作,同时阻止高风险操作。分类器分两阶段运作:快速判定(8.5% 误报率),对标记项目进行思维链推理(0.4% 误报率)。如果连续 3 次操作被拒绝或单次会话累计 20 次被拒,系统会升级到人工介入——或者在 headless 模式下直接终止。

1
claude --permission-mode auto -p "重构认证模块"

模式 3:--dangerously-skip-permissions —— 完全绕过权限。 所有操作无需确认直接执行。Anthropic 自己的安全研究员 Nicholas Carlini 也使用这个模式,但有一个关键前提:*”在容器里跑,不要在你的真实机器上。”* 一项调查发现 32% 的开发者使用这个标志时遭遇了意外的文件修改,9% 报告了数据丢失

1
2
# 仅限 Docker/VM —— 绝对不要在宿主机上运行
claude --dangerously-skip-permissions -p "构建这个功能"

推荐的过夜运行方式是将 -p 与细粒度工具白名单 --allowedTools 结合使用,允许特定命令而非授予全面访问权限:

1
2
3
4
claude -p "修复所有 lint 错误并运行测试" \
--allowedTools "Read" "Edit" "Bash(npm run lint:*)" "Bash(npm test)" "Bash(git *)" \
--max-turns 50 \
--max-budget-usd 10.00

--max-turns--max-budget-usd 是无人值守会话的必备成本控制手段。没有它们,一个失控的循环可以在几分钟内烧光你的 API 预算。


二、Ralph Wiggum 循环:开发者的实际过夜方案

最经过实战验证的长时间自主工作模式是 Ralph Wiggum 循环——以《辛普森一家》中的角色命名,现已成为 Anthropic 官方插件。概念非常简单:一个 bash while 循环持续向 Claude 喂相同的 prompt。每次迭代中,Claude 查看当前文件状态和 git 历史,选择下一个未完成的任务,实现它,然后提交。

1
2
3
4
5
while true; do
claude --dangerously-skip-permissions \
-p "$(cat PROMPT.md)"
sleep 1
done

那位记录了 27 小时会话 的开发者使用了这个模式,配合一个详细的 prompt 文件,包含架构说明、目标、约束条件和明确的”完成”标准。他的核心发现:*”一句话 prompt 在一两个小时后就没劲了。27 小时的会话能持续下去,是因为 prompt 文件有足够多的上下文。”*

Prompt 文件比循环本身更重要。 一个有效的过夜 PROMPT.md 示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 任务:测试并加固认证系统

## 上下文
- 后端:Express + TypeScript,位于 src/api/
- 数据库:PostgreSQL,schema 在 prisma/schema.prisma
- 认证流程:JWT 中间件在 src/middleware/auth.ts

## 目标
- 查看 docs/plan.md,选择下一个未完成的任务
- 实现它,包含完善的错误处理
- 运行测试,修复失败,确认没有回归
- 做通用修复,不要打临时补丁
- 每完成一个任务后用描述性消息提交

## 成功标准
- 每次修改后所有测试通过
- 不会引入之前修复的回归
- 当 plan.md 中所有任务完成后输出 DONE

社区有几个工具扩展了这个基础循环。Ralph CLI 增加了速率限制(100次调用/小时)、熔断器、会话过期(默认24小时)和实时监控仪表板。Nonstop 增加了飞行前风险评估和阻塞决策框架——走之前输入 /nonstop 即可。Continuous-claude 自动化完整 PR 生命周期:创建分支、推送、创建 PR、等待 CI、合并。


三、防止过夜灾难的四个 Hook

开发者 yurukusa 记录了 108 小时无人值守运行,识别出七类过夜事故——包括 Claude 执行 rm -rf ./src/、进入无限错误循环、直接推送到 main 分支,以及产生每小时 8 美元的 API 费用。解决方案:四个关键 Hook,共同预防最常见的故障模式。

10 秒快速安装:

1
npx cc-safe-setup

Hook 1:No-Ask-Human 阻止 AskUserQuestion 工具调用,强制 Claude 自主做出决定,而不是坐在那里等几小时等人回复。这个 Hook 决定了 Claude 是整夜工作还是在晚上 11:15 卡住。在你坐在电脑前时,用 CC_ALLOW_QUESTIONS=1 覆盖。

Hook 2:Context Monitor 将工具调用次数作为上下文使用量的代理指标,在四个阈值(剩余 40%、25%、20%、15%)发出分级警告。在临界水平时,配套的空闲推送脚本会自动向终端注入 /compact 命令——两个进程,共 472 行代码,零人工干预

Hook 3:Syntax Check 在任何文件编辑后立即运行 python -m py_compilenode --checkbash -n,在错误级联成 50 次调试之前就捕获它们。

Hook 4:Decision Warn 在执行前标记破坏性命令(rm -rfgit reset --hardDROP TABLEgit push --force)。通过 CC_PROTECT_BRANCHES="main:master:production" 配置受保护分支。

.claude/settings.json 中配置:

1
2
3
4
5
6
{
"permissions": {
"allow": ["Bash(npm run lint:*)", "WebSearch", "Read"],
"deny": ["Read(.env)", "Bash(rm -rf *)", "Bash(git push * main)"]
}
}

四、tmux 设置与保持机器不休眠

Claude Code 的交互模式需要 TTY —— 不能用 nohup 或将其作为 systemd 服务运行(大约 15-20 秒后会因 stdin 错误崩溃)。tmux 是会话持久化的必备工具

1
2
3
4
5
6
7
8
9
10
11
12
13
# 启动命名会话
tmux new -s claude-work

# 在其中启动 Claude
claude --permission-mode auto

# 分离(Claude 继续运行):Ctrl+B,然后按 D

# 从任何地方重新连接(SSH、手机 Termius 等)
tmux attach -t claude-work

# 不连接就查看进度
tmux capture-pane -t claude-work -p -S -50

对于真正的 7×24 运行,社区推荐 VPS + Tailscale + tmux 方案:便宜的 VPS(Hetzner、Vultr、DigitalOcean)提供永不关机的算力,Tailscale 提供私有网络,mosh 在不稳定网络上保持连接持久性。给 Claude 一个任务,分离,合上笔记本,明天再回来。

macOS 防止休眠:

1
2
3
4
5
# 绑定到 Claude 进程
caffeinate -i -w $(pgrep -f claude) &

# 或者在接通电源时全局禁用休眠
sudo pmset -c sleep 0

管理多个并行会话方面,Amux 是一个约 12,000 行的 Python 文件,提供 Web 仪表板、手机 PWA 监控、自愈看门狗(自动重启崩溃会话)、按会话 token 追踪和 git 冲突检测。Codeman 提供类似的 Web UI,带 xterm.js 终端,支持最多 20 个并行会话。

一个强大的过夜 agent tmux 配置:

1
2
3
4
5
6
7
8
9
#!/bin/bash
tmux new-session -d -s claude-dev
tmux rename-window -t claude-dev:0 'Claude'
tmux new-window -t claude-dev:1 -n 'Tests'
tmux new-window -t claude-dev:2 -n 'Logs'
tmux send-keys -t claude-dev:0 'claude --permission-mode auto' Enter
tmux send-keys -t claude-dev:1 'npm run test:watch' Enter
tmux send-keys -t claude-dev:2 'tail -f logs/app.log' Enter
tmux attach-session -t claude-dev

五、CLAUDE.md 与长时间运行的上下文管理

过夜失败的最大原因是上下文窗口耗尽。Claude Code 的上下文窗口大约 200K token,使用率超过 70% 时性能开始下降。自动压缩在接近阈值时触发,但会丢失信息——仅保留 20-30% 的细节。有开发者报告 Claude 压缩后遗忘了所有内容,重新开始同一个任务,浪费了三个小时。

解决方案是检查点/交接模式,能够在上下文重置后存活:

1
2
3
4
5
6
# 在 CLAUDE.md 中
当上下文变大时,将当前状态写入 tasks/mission.md。
包括:已完成的、下一步的、被阻塞的、未解决的问题。
错误处理:最多重试 3 次。如果没有进展,记录到
pending_for_human.md 然后转到下一个任务。
压缩前,务必保存完整的已修改文件列表。

将 CLAUDE.md 控制在 200 行以内——每个词在每个会话中都消耗 token。从 800 行切换到 100 行的开发者达成社区共识:更短的配置实际上表现更好,因为 Claude 不会忽略被噪音淹没的指令。使用”仅在不可逆时才提问”规则,将提问频率降低约 80%:

1
2
3
4
5
6
# 自主运行的决策规则
- 技术方案不确定 → 选择传统方案
- 两种可行实现 → 选择更简单的那个
- 尝试 3 次后仍有错误 → 记录到 blocked.md,切换任务
- 需求模糊 → 应用最合理的理解,记录假设
- 永远不要提问。做出最佳判断然后继续。

CLAUDE.md 文件是分层的:~/.claude/CLAUDE.md(全局)、./CLAUDE.md(项目级,git 追踪)、.claude/CLAUDE.local.md(个人覆盖,gitignore)。自主运行时,全局文件保持最小,把运行特定指令放在项目文件中。

关键 token 节省技巧:在里程碑后主动使用 /compact,而非等待自动压缩;对独立任务使用子 agent(每个有自己的上下文窗口);不相关的工作启动新会话;积极使用 .claudeignore 排除无关文件。


六、过夜运行的速率限制处理

速率限制作为三个独立的、重叠的约束运作:每分钟请求数、每分钟输入 token 数、每分钟输出 token 数。一个可见的命令在内部可能产生 8-12 个 API 调用(lint、修复、测试、修复循环)。15 次迭代后,单个请求可能发送 20 万+ 输入 token

过夜运行速率限制生存策略:

在非高峰时段运行。 Anthropic 确认工作日太平洋时间早 5 点到 11 点限制更严格。过夜运行和周末会话完全避开高峰期限流——恰好就是你在睡觉的时候。

利用 Ralph 循环的内置重试。 运行 while 循环时,速率限制错误只会导致当前迭代失败,但循环不在乎——它在速率限制窗口重置后的下一次迭代中重试。有开发者警告:*”不要在 API/按用量计费模式下运行——重试会烧光你的预算。”*

运行中切换模型。 Sonnet 能处理 60-70% 的常规任务,每 token 成本比 Opus 低约 1.7 倍。过夜工作设置 --model sonnet,将 Opus 留给复杂推理。也可以设置 --fallback-model sonnet,让 Claude 在主模型过载时自动降级。

Token 消耗的真实数据:20 条消息会话消耗约 105,000 token;30 条消息会话跳到 232,000 token。大约 98.5% 的 token 花在重新读取对话历史——只有 1.5% 用于实际输出。这就是为什么全新会话和积极压缩如此重要。

成本估算:持续运行 Sonnet 大约 $10.42/小时。基于 cron 每 15 分钟运行一次的 agent,预计约 $48/天。使用 --max-budget-usd 作为硬上限。


七、CI/CD 流水线与 Cron 任务集成

对于计划性的自动化工作,Claude Code 可直接与 CI/CD 系统集成。官方 GitHub Action 是 anthropics/claude-code-action@v1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
name: Claude Code Review
on:
pull_request:
types: [opened, synchronize]
jobs:
review:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
prompt: "审查这个 PR 的安全和代码质量问题。"
claude_args: "--max-turns 5 --model claude-sonnet-4-6"

对于基于 cron 的自主 agent,Boucle 模式通过 state.md 文件在运行之间维持状态:

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash
# run-agent.sh —— 由 cron 调用
STATE="$HOME/agent/state.md"
LOG="$HOME/agent/logs/$(date +%Y-%m-%d_%H-%M-%S).log"

claude -p "你是一个自主 agent。读取你的状态,决定做什么,
然后用你学到的内容更新 state.md。
$(cat $STATE)" \
--allowedTools Read,Write,Edit,Bash \
--max-turns 20 \
--max-budget-usd 1.00 \
--bare 2>&1 | tee "$LOG"
1
2
# crontab -e
0 * * * * /path/to/run-agent.sh

200 次迭代后的关键教训:state.md 必须保持在 4KB 以下(它会被注入每个 prompt),使用结构化键值对而非散文,并添加文件锁防止重叠运行。每次迭代后 git commit——git log 就是你最好的调试工具。

CI 环境使用 --bare 模式(跳过 hook、MCP 服务器、OAuth 和 CLAUDE.md 加载,最快最可复现的执行方式)和 --permission-mode dontAsk(拒绝所有未显式允许的操作——自动化环境中最安全的模式)。


八、已知陷阱与可能出错的地方

社区已广泛记录了以下故障模式:

故障模式 后果 预防方法
破坏性命令 Claude 运行 rm -rfgit reset --hard 或覆盖生产数据 PreToolUse hook 阻止危险命令;Docker 配合 --network none
无限错误循环 修复 → 测试 → 同样错误 → 修复 → 重复 20+ 次 CLAUDE.md 规则:”最多重试 3 次,然后记录到 blocked.md 继续下一个”
压缩后上下文丢失 Claude 遗忘一切,重新开始同一任务 压缩前将状态写入 mission.md;使用 Ralph 循环获得全新上下文迭代
权限提示阻塞 会话无限期挂起等待人工输入 No-Ask-Human hook;--dangerously-skip-permissions--permission-mode auto
直接推送到 main 未测试的代码部署到生产环境 分支保护规则;PreToolUse hook 阻止 git push 到受保护分支
API 成本失控 子 agent 进入循环调用外部 API($8/小时) --max-budget-usd;速率限制 hook;熔断器
OAuth token 过期 中途打断自主工作流 所有自动化使用 ANTHROPIC_API_KEY 环境变量而非 OAuth
订阅 ToS 违规 用 Pro/Max 订阅(非 API key)的 headless 模式可能违反消费者条款 自动化/脚本使用务必用 ANTHROPIC_API_KEY

最重要的单一安全措施是容器化。多位经验丰富的开发者独立推荐使用带网络隔离的 Docker:

1
2
3
4
5
docker run -it --rm \
-v $(pwd):/workspace -w /workspace \
--network none \
-e ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" \
claude-code:latest --dangerously-skip-permissions -p "$(cat PROMPT.md)"

正如一位开发者所说:*”用 --dangerously-skip-permissions 运行 Claude Code 就像不做防护措施。所以用个套… 我是说容器。”*


九、今晚的快速启动清单

15 分钟设置过夜自主运行:

  1. 创建 git 检查点git add -A && git commit -m "pre-autonomous checkpoint"
  2. 安装四个关键 Hooknpx cc-safe-setup
  3. 编写 PROMPT.md,包含架构上下文、任务列表、成功标准,以及每完成一个任务就提交的指令
  4. 启动 tmux 会话tmux new -s overnight
  5. 防止休眠(macOS):caffeinate -s &
  6. 启动循环
1
2
3
4
5
6
7
8
while true; do
claude -p "$(cat PROMPT.md)" \
--allowedTools "Read" "Edit" "Bash(npm run *)" "Bash(git *)" \
--max-turns 30 \
--max-budget-usd 5.00 \
--permission-mode acceptEdits
sleep 2
done
  1. 分离 tmuxCtrl+B,然后按 D
  2. 去睡觉

早上起来:tmux attach -t overnight,然后查看 git log(git log --oneline)看 Claude 完成了什么。预计保留大约 75% 的产出,丢弃 25%。这很正常——正如一位开发者说的,*”不是完美,甚至不是最终版,但是在前进。”*

Xcode 26.4 下老项目与 Pod 兼容性修复指南

作者 Ox8BADFOOD
2026年4月14日 10:20

Xcode 26.4 下老项目与 Pod 兼容性修复指南

救命!Xcode 26.4 把我的老项目按在地上摩擦😭

谁懂啊家人们!更新完 Xcode 26.4 本想体验新功能,结果一编译直接红一片,全是老旧 Pod 库搞的鬼!AFNetworking、ZFPlayer、YYText 这些曾经的 iOS 开发神器,如今没人维护,遇上新版 Xcode 直接摆烂,留我们打工人疯狂擦屁股!

第一波暴击:私有头文件硬闯,直接被锁死

老旧 Pod 里还在无脑引用 <netinet6/in6.h> 这个 Darwin 私有头,Xcode 26.4 直接重拳出击,毫不留情甩报错:

Use of private header from outside its module: 'netinet6/in6.h'

说白了就是苹果把私有头锁门了,老库还硬要闯,纯纯作死!关键这头文件完全多余,<netinet6/in6.h> 早就把所有需要的内容都包含了,纯纯无用代码坑后人!

第二波暴击:YYText 逆天语法,数学式比较被制裁

YYText 里的链式比较 X < Y < Z 真的离大谱!照着数学公式写代码,以前 Xcode 睁一只眼闭一只眼,现在 26.4 直接较真报错:

Chained comparison 'X < Y < Z' does not behave the same as a mathematical expression

OC 根本不支持这种写法,老库不维护,bug 全留给我们,新版 Xcode 一严格,直接编译失败,血压瞬间拉满!

终极救星!一键脚本全自动修复

别手动改 Pod 源码了!直接把这段脚本塞进 Podfile,pod install 一键搞定所有报错,懒人狂喜,专治各种老库适配不服!

post_install do |installer|
 # Xcode 26 Explicit Modules:不允许直接 #import Darwin 私有头 <netinet6/in6.h>,
  # 相关类型已由 <netinet/in.h> 间接提供,移除即可(AFNetworking、ZFPlayer 等旧版 Pod 常见)。
  pods_dir = File.join(installer.sandbox.root, 'Pods')
  Dir.glob(File.join(pods_dir, '**', '*.{h,m,mm,c}')).each do |file|
    begin
      content = File.read(file)
      next unless content.include?('#import <netinet6/in6.h>')

      File.chmod(0644, file)
      new_content = content.gsub(/^\s*#import\s+<netinet6\/in6\.h>\s*\n/, '')
      File.write(file, new_content)
      puts "[Podfile] Removed #import <netinet6/in6.h> from #{file}"
    rescue => e
      puts "[Podfile] Warning: failed to patch #{file}: #{e.message}"
    end
  end

# Xcode 26:修复 YYText 中 YYTextLayout.m 文件的非法链式比较问题
  # 问题:Objective-C 中的链式比较语法 `a < b < c` 在 Xcode 较新版本中会产生编译错误
  # 修复方法:将链式比较转换为使用三元运算符的明确比较 `(a < b) ? x : y`
  yytext_layout_file = File.join(pods_dir, 'YYText', 'YYText', 'Component', 'YYTextLayout.m')
  if File.exist?(yytext_layout_file)
    begin
      content = File.read(yytext_layout_file)
      new_content = content
        # 修复垂直方向的链式比较
        .gsub(
          # gsub:"global substitute"(全局替换)
          # 基本语法 string.gsub(pattern, replacement)
          'position = fabs(left - point.y) < fabs(right - point.y) < (right ? prev : next);',
          'position = (fabs(left - point.y) < fabs(right - point.y)) ? prev : next;'
        )
        # 修复水平方向的链式比较
        .gsub(
          'position = fabs(left - point.x) < fabs(right - point.x) < (right ? prev : next);',
          'position = (fabs(left - point.x) < fabs(right - point.x)) ? prev : next;'
        )

      if new_content != content
        File.chmod(0644, yytext_layout_file)  # 添加写权限
        File.write(yytext_layout_file, new_content)  # 写入修复后的内容
        puts "[Podfile] Fixed chained comparison in #{yytext_layout_file}"
      end
    rescue => e
      puts "[Podfile] Warning: failed to patch #{yytext_layout_file}: #{e.message}"
    end
  end
end

最后疯狂吐槽

苹果更新 Xcode 爽歪歪,我们适配老项目苦哈哈! 经典三方库集体停更,技术栈无人维护,Xcode 规则一收紧,全是编译坑。打工人的命也是命啊!

还好有这个一键脚本,复制粘贴就能解决所有报错,赶紧给你的老项目续命吧!


总结

  1. Xcode 26.4 严格校验私有头和语法,老旧未维护 Pod 库必报错

  2. 核心问题:netinet6/in6\.h 私有头引用、YYText 链式比较语法错误

  3. 粘贴脚本 → pod install → 编译成功,三步搞定,无需手动修改库源码

iOS逆向工程:详细解析ptrace反调试机制的破解方法与实战步骤

2026年4月15日 11:44

Ptrace 提供了一种父进程可以控制子进程运行的机制,并可以检查和改变它的核心image。

  • 它主要用于实现断点调试。

1、一个被跟踪的进程运行中,直到发生一个信号,则进程被中止,并且通知其父进程。 2、在进程中止的状态下,进程的内存空间可以被读写。父进程还可以使子进程继续执行,并选择是否是否忽略引起中止的信号。

  • 本文采用tweak 的方式进行MSHookFunction

在iOS应用安全中,除了反调试机制,代码混淆也是一种重要的保护手段。IpaGuard是一款强大的iOS IPA文件混淆工具,无需源码即可对代码和资源进行混淆加密,支持多种开发平台,有效增加反编译难度。它提供代码混淆、资源文件混淆、调试信息清理等功能,可以帮助开发者保护应用免受逆向工程攻击。

软件环境:Xcode 硬件环境:iPhone5越狱手机、Mac 开发工具:Cycript、LLDB、logos Tweak、hopper、MonkeyDev、AFLEXLoader、dumpdecrypted、debugserver、ssh、class_dump、hook

  • 破解方案

1、运行时期,断点ptrace,直接返回 2、分析如何调用的ptrace,hook ptrace 3、通过tweak,替换disable_gdb函数 4、修改 PT_DENY_ATTACH

I、运行时期,断点ptrace,直接返回

初始化应用程序,而不是运行中附着

iPhone:~ root#  debugserver -x posix *:12345  /var/mobile/Containers/Bundle/Application/A612F542-81EF-456A-A6A0-B23046EF57BA/AlipayWallet.app/AlipayWallet

初始化程序,目的是从程序入口就开始进行附着,这样我们就可以在一些安全防护代码执行之前,进行破解。

跳过ptrace:过命令thread return直接返回,以跳过函数的逻辑。

 (lldb) br set -n ptrace
 Breakpoint 2: where = libsystem_kernel.dylib`__ptrace, address = 0x00000001966af2d4
 (lldb) br command add 2
 Enter your debugger command(s).  Type 'DONE' to end.
 > thread return
 > c
 > DONE

II、分析如何调用的ptrace,hook ptrace

去掉ptrace的思路

  • 当程序运行后,使用 debugserver *:1234 -a BinaryName 附加进程出现 segmentfault 11 时,一般说明程序内部调用了ptrace 。
  • 为验证是否调用了ptrace 可以 debugserver -x backboard *:1234 /BinaryPath(这里是完整路径),然后下符号断点 b ptrace,c 之后看ptrace第一行代码的位置,然后 p $lr 找到函数返回地址,再根据 image list -o -f 的ASLR偏移,计算出原始地址。最后在 IDA 中找到调用ptrace的代码,分析如何调用的ptrace。
  • 开始hook ptrace。

2.0 准备工作:砸壳

  • 砸壳

blog.csdn.net/z929118967/…

  • 查找app路径
iPhone:~ root# ps -e |grep AlipayWallet
  714 ??         0:26.44 /var/mobile/Containers/Bundle/Application/A612F542-81EF-456A-A6A0-B23046EF57BA/AlipayWallet.app/AlipayWallet
  736 ttys000    0:00.01 grep AlipayWallet

  • 获取NSDocumentDirectory
iPhone:~ root# cycript -p AlipayWallet
cy# [[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask][0]
#"file:///var/mobile/Containers/Data/Application/89313E1C-76C2-41E3-8ECD-F4BDC1A78524/Documents/"

  • scp 拷贝文件
devzkndeMacBook-Pro:decrypted devzkn$ scp /Users/devzkn/Downloads/kevin-software/ios-Reverse_Engineering/dumpdecrypted-master/dumpdecrypted.dylib iphone150:/var/mobile/Containers/Data/Application/89313E1C-76C2-41E3-8ECD-F4BDC1A78524/Documents/

devzkndeMacBook-Pro:decrypted devzkn$ scp iphone150:/var/mobile/Containers/Data/Application/89313E1C-76C2-41E3-8ECD-F4BDC1A78524/Documents/AlipayWallet.decrypted /Users/devzkn/decrypted/AlipayWallet

  • class-dump
devzkndeMacBook-Pro:bin devzkn$ class-dump --arch armv7 /Users/devzkn/decrypted/AlipayWallet10.1.8/AlipayWallet.decrypted -H -o /Users/devzkn/decrypted/AlipayWallet10.1.8/head

-查看 bundleIdentifier

iPhone:~ root# cycript -p AlipayWallet
cy# [[NSBundle mainBundle] bundleIdentifier]
@"com.alipay.iphoneclient"

2.1 编写 tweak 分析

%hook DFClientDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

    %log();

    // 打印某个类的所有方法的,查看所有方法的执行顺序

     [KNHook hookClass:@"H5WebViewController"];//aluLoginViewController
     [KNHook hookClass:@"TBSDKServer"];//getUaPageName    aluMTopService _tokenLoginInvoker

     [KNHook hookClass:@"TBSDKMTOPServer"];//getUaPageName    aluMTopService _tokenLoginInvoker

    return %orig;
}
%end

2.2 具体步骤

2.2.1 debugserver

iPhone:~ root#  debugserver *:12345 -a AlipayWallet
debugserver-@(#)PROGRAM:debugserver  PROJECT:debugserver-320.2.89
 for armv7.
Attaching to process AlipayWallet...
Segmentation fault: 11

当程序运行后,使用 debugserver *:1234 -a BinaryName 附加进程出现 segmentfault 11 时,一般说明程序内部调用了ptrace 。

iPhone:~ root#  debugserver *:12345 -x backboard /var/mobile/Containers/Bundle/Application/A612F542-81EF-456A-A6A0-B23046EF57BA/AlipayWallet.app/AlipayWallet
debugserver-@(#)PROGRAM:debugserver  PROJECT:debugserver-320.2.89
 for armv7.
Segmentation fault: 11

2.2.2 分析如何调用的ptrace

  • debugserver -x
iPhone:~ root# debugserver -x *:12345 /var/mobile/Containers/Bundle/Application/A612F542-81EF-456A-A6A0-B23046EF57BA/AlipayWallet.app/AlipayWallet
error: invalid TYPE for the --launch=TYPE (-x TYPE) option: '*:12345'
Valid values TYPE are:
  auto       Auto-detect the best launch method to use.
  posix      Launch the executable using posix_spawn.
  fork       Launch the executable using fork and exec.
  backboard  Launch the executable through BackBoard Services.

总共有四种类型

debugserver -x backboard *:1234 /var/mobile/...... 把这个backboard改成posix试试

iPhone:~ root#  debugserver -x posix *:12345  /var/mobile/Containers/Bundle/Application/A612F542-81EF-456A-A6A0-B23046EF57BA/AlipayWallet.app/AlipayWallet
debugserver-@(#)PROGRAM:debugserver  PROJECT:debugserver-320.2.89
 for armv7.
Listening to port 12345 for a connection from *...

  • 在ptrace上下断点,找到调用ptrace的地方
(lldb)  b ptrace
Breakpoint 1: no locations (pending).
WARNING:  Unable to resolve breakpoint to any actual locations.

(lldb) c
Process 1657 resuming

  • 关闭Target,重新启动Target
1 location added to breakpoint 1
Process 1657 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
    frame #0: 0x37a13e64 libsystem_kernel.dylib`__ptrace
libsystem_kernel.dylib`__ptrace:
->  0x37a13e64 <+0>:  ldr    r12, [pc, #0x4]           ; <+12>
    0x37a13e68 <+4>:  ldr    r12, [pc, r12]
    0x37a13e6c <+8>:  b      0x37a13e74                ; <+16>
    0x37a13e70 <+12>: rsbeq  r9, r11, #192, #2
Target 0: (AlipayWallet) stopped.
(lldb)  p/x $lr
(unsigned int) $0 = 0x0000bfbb

由此可见ptrace函数在libsystem_kernel.dylib这个动态库中,使用时才进行加载,不是静态放在本地的,所以我们不能简单地去tweak ptrace函数。

(lldb) image list -o -f |grep AlipayWallet
[  0] 0x00000000 /private/var/mobile/Containers/Bundle/Application/A612F542-81EF-456A-A6A0-B23046EF57BA/AlipayWallet.app/AlipayWallet(0x0000000000004000)

所以ptrace的调用者位于0x0000bfbb - 0x00000000 = 0x0000bfbb处,如图所示:

  • 在hopper使用go to add ,快捷键G
昨天 — 2026年4月14日iOS

Flutter应用代码混淆完整指南:Android与iOS平台配置详解

2026年4月14日 14:37

Flutter中的代码混淆

代码混淆可以隐藏你的Dart代码中的函数和类名,让 反编译 App变得困难。对于更全面的混淆需求,特别是针对iOS IPA文件,可以使用专业工具如IpaGuard,它支持无需源码的代码和资源混淆,兼容Flutter等多种开发平台,有效增加反编译难度。

注:Dart的混淆还没有经过完全的测试,如果发现问题请到GitHub上提 issue 。关于混淆的问题,还可以参考 Stack Overflow 上的这个问题。

Flutter中的混淆配置其实是在Android和iOS端分别配置的。

Android

<ProjectRoot>/android/gradle.properties 文件中添加如下代码:

extra-gen-snapshot-options=--obfuscate

默认情况下,Flutter不会混淆或者缩减Android host,如果你使用了第三方的Java或者Android库,那么你可能需要减小APK体积,或者防止你的App被反编译。

  • Step 1:配置Proguard文件

新建 /android/app/proguard-rules.pro 文件,然后添加如下配置:


#Flutter Wrapper
-keep class io.flutter.app.** { *; }
-keep class io.flutter.plugin.**  { *; }
-keep class io.flutter.util.**  { *; }
-keep class io.flutter.view.**  { *; }
-keep class io.flutter.**  { *; }
-keep class io.flutter.plugins.**  { *; }

上面的配置只保护Flutter库,其他额外的库(比如Firebase)需要你自己添加配置。

  • Step 2:

打开 /android/app/build.gradle 文件,定位到 buildTypes 处,在 release 配置中将 minifiyEnableduseProguard 标志设为true,同时还需要指向Step1中创建的ProGuard文件:


android {
    ...
    buildTypes {
        release {
            signingConfig signingConfigs.debug
            minifyEnabled true
            useProguard true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

注意混淆和缩减无用代码会加长App的编译时间。

iOS

  • Step 1:修改 "build aot"

<ProjectRoot>/packages/flutter_tools/bin/xcode_backend.sh 文件中添加 build aot flag:

${extra_gen_snapshot_options_or_none}

然后定义这个flag:


local extra_gen_snapshot_options_or_none=""
if [[ -n "$EXTRA_GEN_SNAPSHOT_OPTIONS" ]]; then
  extra_gen_snapshot_options_or_none="--extra-gen-snapshot-options=$EXTRA_GEN_SNAPSHOT_OPTIONS"
fi
  • Step 2:应用你的修改

在你的App的根目录下运行以下两条命令:


git commit -am "Enable obfuscation on iOS"
flutter
  • Step 3:更改release配置

<ProjectRoot>/ios/Flutter/Release.xcconfig 中添加下面这行:

EXTRA_GEN_SNAPSHOT_OPTIONS=--obfuscate

对于iOS平台,如果需要更强大的混淆保护,可以考虑使用IpaGuard这样的工具,它可以直接对IPA文件进行混淆加密,支持代码和资源文件的全面混淆,无需源码即可操作,并兼容Flutter应用,提供即时测试功能。

AI没我们想的那么聪明!复盘我的Vibe Coding翻车案例

作者 吴就业
2026年4月14日 13:17

我已经失去了编写代码的能力,变成只会写提示词的提示词工程师。但也没有完全丧失,我还会Review代码,偶尔手动改几行代码。

比如用Swift写UI,我在完全不复制粘贴的情况下,已经不会从0行代码写出一个页面,只会在AI生成的代码微调布局。

准确的说,不是丧失了能力,而是丧失了记忆!

回顾最近在开发ApiCatcher这款iOS端HTTPS抓包和调试工具的过程中,除了用AI开发核心功能遇到困难,其实在开发UI过程中也遇到一些困难。

今天分享我用AI开发ApiCatcher UI遇到的两个问题。解决这两个问题,我从Gemini 3.1 Pro模型,升级到Claude Opus 4.6模型,又从Claude Opus 4.6模型降到Gemini 3 Flash,最新只用Gemini 3 Flash就解决了问题。

第一个案例,我让AI帮我开发ApiCatcher的“AI对话生成脚本”功能,这里遇到的问题就是Markdown渲染。AI响应的消息里面包含代码。

关于UI,我的提示词大概是这样的:

页面做成聊天对话界面,底部是输入框和发送按钮,其余空间是显示聊天记录,用户只能发送文本消息,AI响应的消息是Markdown格式,AI响应的消息包含代码块,需要能高亮渲染代码块,代码不自动换行,而是代码块可以左右滚动。

这里的难点就是要支持代码块的高亮和代码块可以水平滚动。

无论是Gemini还是Claude,他们的思路都是先用开源的Swift语言编写的Markdown渲染开源库,怎么调都调不出令我满意的效果,渲染结果太丑了。在尝试完几个开源库后,Gemini和Claude会走向自己实现,写得非常复杂,然后实现的效果更加糟糕!

在无数次挫败后,我开始自己思考解决方案。我告诉Claude,SwiftUI的Markdown渲染开源库不好用,我们可以用Web技术栈,找JS开源库,然后用WebView来渲染Markdown消息。

这次Claude终于开窍了,但也没完全开窍。他把每个Markdown消息,都用一个WebView来渲染....

我再告诉他,为什么不是只用一个WebView来渲染所有消息呢?这时候Claude限制额度了......

我降回了Gemini 3 Flash。终于在几次调整后,Gemini做出了我想要的效果。

最终用到的技术栈:WebView、CodeMirror、Highlight.js。

第二个案例,JSON可折叠可搜索。

这次一上来就用Claude Sonnet 4.6模型,提示词:帮我实现JSON全屏预览页面,要求:JSON高亮显示,路径可以折叠和展开,可以搜索,搜索结果高亮,可以点击“下一个”按钮跳转到下一个搜索结果的位置。

Claude还是一样的思路,先找SwiftUI的开源库,但折腾半天卡在了搜索功能。还是一样折腾到耗尽Claude额度。

SwiftUI没有好用的开源库,那就换个思路,于是我又想到了WebView。最后选择WebView+CodeMirror+Highlight.js,加上CodeMirror的一个行号插件、搜索插件,用Gemini 3 Flash开发,完美实现!

在这两个案例中,无论是Claude还是Gemini,他们的思考方式都是先找SwiftUI可以使用的开源库,如果达不到目的,就改变策略,自己实现,但这种复杂的任务,他们根本没能力实现好。他们无法变通到通过WebView去乔接,从使用Swift技术栈,转变为使用Web开发技术栈。

我们只要告诉AI方法,他们就能实现,所以,目前AI的能力,其实受限于使用者的能力、认知。初级程序员+AI 无法超越 高级程序员+AI!

未来提示词会替代编程语言,程序员从写代码变成写提示词,但我们依然需要学习提升写提示词的能力。编程能力从来不是指代码写得多漂亮,而是对底层原理的认知深度,同样写提示词的能力也并非是指表达能力,同样是对底层原理的认知深度和广度。

以上面两个案例为例,我们需要认识到Swift原生开发并不是只能使用Swift,SwiftUI开发的原生应用也可以使用WebView来渲染网页,可以Swift生成网页代码传递给WebView渲染,并且可以生成JS代码让WebView执行。我们可以不懂怎么去写代码实现,只需要知道可以这样做,这极大降低了学习难度!

AI放大了我们个人的能力:从需要学会如何编码实现,了解具体的底层原理,到只需要知道可以这样实现。

但只是放大个人能力,AI遇强则强,遇弱则弱,我们依然需要保持学习。

【AFNetworking】OC 时代网络请求事实标准,Alamofire 的前身

作者 探索者dx
2026年4月14日 00:39

【AFNetworking】OC 时代网络请求事实标准,Alamofire 的前身

iOS三方库精读 · 第 9 期


一、一句话介绍

AFNetworking 是 iOS / macOS 平台上最早也是最成功的 Objective-C HTTP 网络库,它让网络请求从繁琐的 NSURLConnection / NSURLSession API 变得简洁优雅,成为 OC 时代的事实标准。

属性
GitHub Stars 33k+
最新版本 4.0.0
License MIT
支持平台 iOS 9+ / macOS 10.10+ / watchOS / tvOS
维护状态 维护模式(Maintenance Mode)

二、为什么选择它

原生痛点(2011 年的 iOS 世界)

在没有 AFNetworking 之前,iOS 开发者不得不面对:

  • NSURLConnection 样板代码:每次请求都要实现 delegate 回调,代码分散难以维护
  • JSON 解析手动处理:iOS 5 之前没有 NSJSONSerialization,需要第三方库
  • 图片加载无缓存:网络图片每次都要重新下载,没有内存/磁盘缓存
  • 网络状态监听复杂:Reachability API 繁琐,代码量大
  • HTTPS 配置困难:自签名证书、SSL Pinning 需要深入了解安全 API

AFNetworking 核心优势

  1. 一行代码发起请求:GET / POST / PUT / DELETE 统一 API,无需 delegate
  2. 自动序列化:JSON / XML / Plist / Image 响应自动解析
  3. UIImageView CategorysetImageWithURL: 一行搞定网络图片加载
  4. Reachability 内建:实时监听网络状态,断网自动提示
  5. SSL Pinning 支持:证书校验一行配置,安全合规
  6. Block 回调:告别分散的 delegate,代码逻辑更清晰

原生 API vs AFNetworking

场景 原生 NSURLSession AFNetworking
GET 请求 10+ 行代码 + delegate [manager GET:success:failure:]
JSON 解析 NSJSONSerialization 手动调用 自动解析为 NSDictionary / NSArray
图片加载 需自行实现缓存 setImageWithURL: 一行
网络监听 Reachability 复杂 API setReachabilityStatusChangeBlock:
文件上传 构造 multipart 请求繁琐 POST:constructingBodyWithBlock:

三、核心功能速览

基础层 概念解释、环境配置、基础用法

环境配置

CocoaPods(OC 项目首选)

pod 'AFNetworking', '~> 4.0'

Swift Package Manager

// Package.swift
dependencies: [
    .package(url: "https://github.com/AFNetworking/AFNetworking.git", from: "4.0.0")
]

Swift 项目桥接

// {ProjectName}-Bridging-Header.h
#import <AFNetworking/AFNetworking.h>
#import "UIImageView+AFNetworking.h"

基础 GET 请求

// AFNetworking 4.x (OC)
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];

[manager GET:@"https://jsonplaceholder.typicode.com/users"
  parameters:nil
    progress:nil
     success:^(NSURLSessionDataTask *task, id responseObject) {
         NSLog(@"成功: %@", responseObject);
     }
     failure:^(NSURLSessionDataTask *task, NSError *error) {
         NSLog(@"失败: %@", error.localizedDescription);
     }];

进阶层 最佳实践、性能优化、线程安全

POST 请求带参数

NSDictionary *params = @{
    @"name": @"张三",
    @"email": @"zhangsan@example.com",
    @"age": @28
};

[manager POST:@"https://api.example.com/users"
   parameters:params
     progress:nil
      success:^(NSURLSessionDataTask *task, id responseObject) {
          NSLog(@"创建成功: %@", responseObject);
      }
      failure:^(NSURLSessionDataTask *task, NSError *error) {
          NSLog(@"创建失败: %@", error);
      }];

文件上传

NSData *imageData = UIImageJPEGRepresentation(image, 0.8);

[manager POST:@"https://api.example.com/upload"
   parameters:nil
    constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
        [formData appendPartWithFileData:imageData
                                    name:@"file"
                                fileName:@"photo.jpg"
                               mimeType:@"image/jpeg"];
    }
     progress:^(NSProgress *progress) {
         NSLog(@"上传进度: %.0f%%", progress.fractionCompleted * 100);
     }
      success:^(NSURLSessionDataTask *task, id responseObject) {
          NSLog(@"上传成功");
      }
      failure:^(NSURLSessionDataTask *task, NSError *error) {
          NSLog(@"上传失败: %@", error);
      }];

图片加载(UIImageView Category)

#import "UIImageView+AFNetworking.h"

// 基础用法
[imageView setImageWithURL:[NSURL URLWithString:@"https://example.com/avatar.jpg"]];

// 带占位图
[imageView setImageWithURL:[NSURL URLWithString:@"https://example.com/avatar.jpg"]
         placeholderImage:[UIImage imageNamed:@"default_avatar"]];

// 取消加载(cell 复用场景)
[imageView cancelImageDownloadTask];

网络状态监听

[[AFNetworkReachabilityManager sharedManager] setReachabilityStatusChangeBlock:
    ^(AFNetworkReachabilityStatus status) {
        switch (status) {
            case AFNetworkReachabilityStatusNotReachable:
                NSLog(@"无网络连接");
                break;
            case AFNetworkReachabilityStatusReachableViaWiFi:
                NSLog(@"WiFi 连接");
                break;
            case AFNetworkReachabilityStatusReachableViaWWAN:
                NSLog(@"蜂窝网络");
                break;
            default:
                break;
        }
    }];

[[AFNetworkReachabilityManager sharedManager] startMonitoring];

HTTPS 证书校验

manager.securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate];
manager.securityPolicy.allowInvalidCertificates = YES;  // 允许自签名
manager.securityPolicy.validatesDomainName = YES;

深入层 源码解析、设计思想、扩展定制

核心类关系

AFURLSessionManager (核心)
    ├── AFHTTPSessionManager (HTTP 封装)
    │       ├── requestSerializer (请求序列化)
    │       └── responseSerializer (响应序列化)
    ├── AFSecurityPolicy (安全策略)
    └── AFNetworkReachabilityManager (网络监听)

Category 扩展:
    UIImageView+AFNetworking (图片加载)
    UIProgressView+AFNetworking (进度条)
    AFNetworkActivityIndicatorManager (状态栏网络指示器)

Session Manager 核心设计

// AFHTTPSessionManager 继承 AFURLSessionManager
// 职责分离:网络层、请求构建、响应解析各自独立

@interface AFHTTPSessionManager : AFURLSessionManager
@property (nonatomic, strong) AFHTTPRequestSerializer <AFURLRequestSerialization> *requestSerializer;
@property (nonatomic, strong) AFHTTPResponseSerializer <AFURLResponseSerialization> *responseSerializer;
@end

// 序列化协议设计
@protocol AFURLRequestSerialization <NSObject>
- (NSURLRequest *)requestBySerializingRequest:(NSURLRequest *)request
                                withParameters:(id)parameters
                                         error:(NSError * __autoreleasing *)error;
@end

四、实战演示

场景:完整的网络请求封装

// APIClient.h - 统一网络请求封装
#import <AFNetworking/AFNetworking.h>

@interface APIClient : AFHTTPSessionManager
+ (instancetype)sharedClient;

// 业务方法
- (void)fetchUsers:(void(^)(NSArray *users, NSError *error))completion;
- (void)createUser:(NSDictionary *)params
        completion:(void(^)(NSDictionary *user, NSError *error))completion;
- (void)uploadAvatar:(UIImage *)image
           completion:(void(^)(NSString *url, NSError *error))completion;
@end

// APIClient.m
@implementation APIClient

+ (instancetype)sharedClient {
    static APIClient *instance;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        NSURL *baseURL = [NSURL URLWithString:@"https://api.example.com/"];
        instance = [[self alloc] initWithBaseURL:baseURL];
        
        // 配置请求/响应序列化
        instance.requestSerializer = [AFJSONRequestSerializer serializer];
        instance.responseSerializer = [AFJSONResponseSerializer serializer];
        instance.requestSerializer.timeoutInterval = 30;
        
        // 添加通用请求头
        [instance.requestSerializer setValue:@"application/json"
                             forHTTPHeaderField:@"Content-Type"];
        
        // HTTPS 配置
        instance.securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeNone];
    });
    return instance;
}

- (void)fetchUsers:(void(^)(NSArray *users, NSError *error))completion {
    [self GET:@"users"
      parameters:nil
        progress:nil
         success:^(NSURLSessionDataTask *task, id responseObject) {
             completion(responseObject, nil);
         }
         failure:^(NSURLSessionDataTask *task, NSError *error) {
             completion(nil, error);
         }];
}

- (void)createUser:(NSDictionary *)params
        completion:(void(^)(NSDictionary *user, NSError *error))completion {
    [self POST:@"users"
   parameters:params
     progress:nil
      success:^(NSURLSessionDataTask *task, id responseObject) {
          completion(responseObject, nil);
      }
      failure:^(NSURLSessionDataTask *task, NSError *error) {
          completion(nil, error);
      }];
}

- (void)uploadAvatar:(UIImage *)image
           completion:(void(^)(NSString *url, NSError *error))completion {
    NSData *data = UIImageJPEGRepresentation(image, 0.8);
    
    [self POST:@"upload"
   parameters:nil
    constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
        [formData appendPartWithFileData:data
                                    name:@"avatar"
                                fileName:@"avatar.jpg"
                               mimeType:@"image/jpeg"];
    }
     progress:nil
      success:^(NSURLSessionDataTask *task, id responseObject) {
          NSString *url = responseObject[@"url"];
          completion(url, nil);
      }
      failure:^(NSURLSessionDataTask *task, NSError *error) {
          completion(nil, error);
      }];
}

@end

五、源码亮点

进阶层 值得借鉴的用法

序列化器组合模式

// 请求序列化器可插拔
manager.requestSerializer = [AFJSONRequestSerializer serializer];   // JSON
manager.requestSerializer = [AFPropertyListRequestSerializer serializer];  // Plist

// 响应序列化器可插拔
manager.responseSerializer = [AFJSONResponseSerializer serializer];  // JSON
manager.responseSerializer = [AFXMLParserResponseSerializer serializer];  // XML
manager.responseSerializer = [AFImageResponseSerializer serializer];  // Image

Block 回调替代 Delegate

// 传统 delegate 分散在多个方法
// AFNetworking 用 block 聚合逻辑
[manager GET:url
  parameters:params
    progress:^(NSProgress *progress) {
        // 进度回调
    }
     success:^(NSURLSessionDataTask *task, id responseObject) {
        // 成功回调
    }
     failure:^(NSURLSessionDataTask *task, NSError *error) {
        // 失败回调
    }];

深入层 设计思想解析

NSURLSession 封装架构

// AFURLSessionManager 核心方法
- (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request
                            completionHandler:(void (^)(NSURLResponse *, id, NSError *))handler {
    // 1. 创建 task
    NSURLSessionDataTask *task = [self.session dataTaskWithRequest:request];
    
    // 2. 存储 task delegate(用于回调)
    self.mutableTaskDelegatesKeyedByTaskIdentifier[@(task.taskIdentifier)] = delegate;
    
    // 3. 返回 task
    return task;
}

// task delegate 处理各种回调
- (void)URLSession:(NSURLSession *)session
          dataTask:(NSURLSessionDataTask *)dataTask
    didReceiveData:(NSData *)data {
    // 累积数据,最终一次性回调
    [mutableData appendData:data];
}

UIImageView Category 的缓存设计

// UIImageView+AFNetworking 内部使用 AFImageDownloader
// 自动实现内存缓存 + 磁盘缓存

@interface AFImageDownloader : NSObject
@property (nonatomic, strong) AFImageCache *imageCache;  // 内存缓存
@property (nonatomic, strong) NSURLCache *urlCache;       // 磁盘缓存
@end

// 缓存策略
- (void)downloadImageForURLRequest:(NSURLRequest *)request
                        completion:(void (^)(UIImage *, NSError *))completion {
    // 1. 先查内存缓存
    // 2. 再查磁盘缓存
    // 3. 最后发起网络请求
}

六、踩坑记录

问题 1:iOS 9+ ATS 限制

问题:iOS 9 默认要求 HTTPS,HTTP 请求被阻止。

原因:App Transport Security (ATS) 默认禁止明文 HTTP。

解决

<!-- Info.plist 添加例外 -->
<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsArbitraryLoads</key>
    <true/>
</dict>

问题 2:Response Serializer 编码问题

问题:返回中文乱码或解析失败。

原因:服务器返回非 UTF-8 编码,AFJSONResponseSerializer 默认 UTF-8。

解决

AFHTTPResponseSerializer *serializer = [AFHTTPResponseSerializer serializer];
serializer.stringEncoding = NSUTF8StringEncoding;  // 或其他编码
manager.responseSerializer = serializer;

问题 3:Cell 复用图片错乱

问题:UITableView 快速滚动时,图片显示错误。

原因:Cell 复用时未取消之前的下载任务。

解决

- (void)prepareForReuse {
    [super prepareForReuse];
    [self.imageView cancelImageDownloadTask];  // 取消之前的下载
    self.imageView.image = nil;
}

问题 4:后台任务未处理

问题:App 进入后台后,下载任务中断。

原因:NSURLSession 默认不支持后台。

解决

// 使用后台 session configuration
NSURLSessionConfiguration *config = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"com.example.background"];
AFHTTPSessionManager *manager = [[AFHTTPSessionManager alloc] initWithSessionConfiguration:config];

问题 5:Swift 混编桥接头文件缺失

问题:Swift 项目中使用 AFNetworking 报找不到模块。

原因:未配置 Objective-C Bridging Header。

解决

  1. 创建 {ProjectName}-Bridging-Header.h 文件
  2. 添加 #import <AFNetworking/AFNetworking.h>
  3. Build Settings → Swift Compiler - General → Objective-C Bridging Header 设置路径

七、延伸思考

AFNetworking vs Alamofire 横向对比

维度 AFNetworking Alamofire
语言 Objective-C Swift
首发年份 2011 2014
GitHub Stars 33k+ 41k+
最低版本 iOS 9.0 iOS 12.0
包体积 ~500KB ~300KB
维护状态 维护模式 活跃开发
Combine 集成 ❌ 不支持 ✅ DataRequest.publisher
async/await ❌ 不支持 ✅ async(...)
SwiftUI 支持 ❌ 不支持 需自行封装
学习曲线 中等 低(链式 API)
OC 兼容性 原生 需桥接

API 对照迁移表

AFNetworking (OC) Alamofire (Swift)
[manager GET:success:failure:] AF.request(url).response()
[manager POST:parameters:success:failure:] AF.request(url, method: .post, parameters: params)
[manager downloadTaskWithRequest:progress:destination:completion:] AF.download(url).downloadProgress()
[manager uploadTaskWithRequest:from:progress:completion:] AF.upload(multipartFormData:)
AFNetworkReachabilityManager NWPathMonitor (系统 API)
AFSecurityPolicy ServerTrustManager + PinnedCertificatesTrustEvaluator
UIImageView+AFNetworking AlamofireImage / Kingfisher

选型建议

选 AFNetworking:

  • 维护遗留 OC 项目,迁移成本高
  • 项目需要支持 iOS 9 ~ iOS 11
  • 团队 OC 技术栈成熟,Swift 经验少

选 Alamofire:

  • 新项目,Swift 为主要语言
  • 需要 Combine / async-await 集成
  • 想要更活跃的社区和持续更新
  • 最低支持 iOS 12+

渐进式迁移策略

  1. 共存阶段:OC 模块继续用 AFNetworking,新 Swift 模块用 Alamofire
  2. 抽象层:统一封装 NetworkService 协议,底层可替换
  3. 按模块迁移:优先迁移独立的业务模块,而非逐个请求修改
  4. 测试覆盖:迁移前后确保集成测试通过

八、参考资源

官方资源

相关文章

系列 Demo 仓库


本期互动

小作业

查看你现有的 OC 项目中 AFNetworking 的使用方式,尝试将一个简单的 GET 请求改写为 Alamofire 版本,对比代码量和可读性变化。

思考题

AFNetworking 从 1.x 到 4.x 经历了 NSURLConnection → NSURLSession 的底层迁移,如果你是核心开发者,你会如何设计 API 以保持向后兼容?

读者征集

你在从 AFNetworking 迁移到 Alamofire 的过程中遇到过哪些坑?欢迎评论区分享,优质回答会收录进下一期《踩坑记录》。


📅 本系列每周五晚更新 · 已学习:[✓ Alamofire] [✓ Kingfisher] [✓ Lottie] [✓ MarkdownUI] [✓ SDWebImage] [✓ SnapKit] [✓ ListDiff] [✓ RxSwift] [→ AFNetworking] [○ Charts]

被 Vibe 摧毁的版权壁垒,与开发者的新护城河 -- 肘子的 Swift 周报 #131

作者 东坡肘子
2026年4月14日 07:56

issue131.webp

被 Vibe 摧毁的版权壁垒,与开发者的新护城河

Anthropic 不久前宣布,由于其最新模型 Mythos 在网络安全与代码漏洞挖掘方面的能力“过于强大”,已达到令人不安的程度,因此采取了极为罕见的克制措施:仅向 Project Glasswing 内的少数关键基础设施企业开放,不面向公众发布,普通开发者也无法通过 API 调用(当然,也有分析者指出,这一安排同样有助于防止模型蒸馏,并锁定企业级客户)。但即便这头“猛兽”被暂时按住,当前主流 AI 模型的代码能力,已经足以让复制一款产品变得轻而易举。

上周,Reddit 上一位开发者宣称,自己花了一年时间“逆向 SwiftUI API”,打造了一个全新的 Swift Web 框架。帖子行文流畅、术语考究,一度吸引了不少关注。但 Paul Hudson 很快现身评论区打假:所谓“独立研发”,实际上只是将他的 MIT 开源项目 Ignite 做了简单的字符串替换,甚至连原作者带有个人风格的代码注释都原封不动保留,随后将整个仓库压缩为一次提交以抹除历史,并违规改为具有传染性的 GPL 协议。社区中不少开发者都怀疑,这套“逆向 SwiftUI”的叙事本身也是由 AI 生成。更耐人寻味的是,该作者本就是 Ignite 的主要贡献者之一——当 Vibe Coding 将“重新打包一个项目”的成本降至极低时,“我参与过”本身也可能成为一种模糊责任边界的话术。

几乎在同一时间,macOS 上精致的 AI 工作状态监控应用 Vibe Island 在发布后不久便遭遇了像素级仿制。尽管仿制者打着“开源替代品”的旗号公开了代码,这依然对原作者的商业销售与创作热情造成了明显冲击。然而,即便作者希望采取法律手段,也将面临一个新的时代难题:在确权与维权过程中,他可能需要证明其作品具备足够的人类独创性,并说明 AI 生成内容的参与程度,否则将面临更高的不确定性。

事实上,代码的法律壁垒正从“确权端”开始松动。上个月,中国版权保护中心正式启用新版《计算机软件著作权登记申请表》及相关审查新规,明确要求经办人实名承诺“未使用 AI 开发编写代码、撰写文档或生成登记材料”,并在审查中重点评估人类智力投入是否达到著作权法所要求的“独创性”门槛;缺乏实质人类参与的内容,将难以获得确权。违规者还可能被纳入失信名单,并与个人征信挂钩。

这一趋势也与欧美近期的判例方向趋于一致:如果一段代码主要由 AI 根据提示词快速“改写或重组”生成,其获得著作权保护的可能性将显著降低。

我们必须承认一个残酷的事实:“我有一个绝妙的 Idea,并把它 Vibe 成代码”,已经不足以构成商业壁垒。这种名为 Vibe Coding 的新范式,不仅改变了开发流程、显著提升了效率,也从三个方向同时动摇了软件版权体系的基础逻辑:确权门槛提高、侵权举证变难、功能复刻被默许。

令人遗憾的是,即便这些克隆项目饱受争议,它们依然在 GitHub 上收获了不菲的 Star。这说明,在获取成本极低的前提下,仅靠道德呼吁,已经很难阻止人们对“免费平替”的追逐。

或许,正如我们在 第 120 期周报中讨论 Skip 的开源举措 时所提到的那样——在代码实现成本趋近于零、应用随时可能被 AI 一键克隆的当下,闭门造车“卖工具”将变得愈发困难。与用户建立真实连接,将“出品方的信誉与社区的信任”转化为不可复制的品牌资产,或许才是未来开发者真正的核心竞争力与护城河。

本期内容 | 前一期内容 | 全部周报列表

近期推荐

Swift Blog Carnival: Tiny Languages

Swift 社区正在发起第一届 Blog Carnival,四月的主题是 Tiny LanguagesChristian Tietze 邀请开发者围绕这一主题撰写博客:自定义 DSL、Result Builder、脚本解析器、路由规则……任何与“微型语言”相关的思考都可以作为切入点,截止日期为 5 月 1 日。

目前已有三篇投稿:

  • Matt Massicotte 回顾了他 从 Rake 到 Make,再到各类 Swift 任务运行器的探索历程,坦言至今仍未找到理想替代品
  • Chris Liscio 分享了 Capo 应用内置 DSL 的设计:用于描述键盘与 MIDI 绑定,基于 Point-Free 的 swift-parsing 库构建
  • Nicolas Zinovieff 则展示了一个 符号数学 DSL 的实验:通过协议与运算符重载,让 (1 + 2 * "X") * (3 - "Y")成为合法的 Swift 表达式,并在提供具体值时惰性求值,核心实现不超过 300 行

在 macOS 菜单中清晰显示当前选中状态 (Indicating Selection in macOS Menus Using SwiftUI)

SwiftUI 提供了不少用于表达选择的组件,例如 PickerToggleMenu,但如何清晰地引导用户进行选择,并准确标识当前选中项,并没有想象中那样理所当然。Gabriel Theodoropoulos 从最基础的 Button 出发,一步步演进到 PickerToggle,系统梳理了几种常见方案及其各自的局限。文章的价值不在于给出“唯一正确解”,而在于提醒开发者:SwiftUI 提供的标准组件,并不会自动带来最佳的用户呈现效果。很多时候,你仍需在“系统一致性”与“实现自由度”之间做出权衡。


构建 List 的替代组件 (Building List replacement in SwiftUI)

如何在 ListScrollView + LazyStack 之间做出选择,一直是困扰不少 SwiftUI 开发者的问题。在本文中,Majid Jabrayilov 在重构其 CardioBot 应用时,选择基于 SwiftUI 的 Container View API(iOS 17+)构建了三个可复用的 UI 组件:ScrollingSurface、DividedCard、SectionedSurface,以替代 List。这些组件在使用方式上与 List + Section 高度相似,但彻底摆脱了 listRowBackgroundlistItemTint 等仅在 List 中生效的限制。

List 并非只是“有默认样式的 LazyVStack”——两者在底层架构、滚动控制、与导航容器的协同、以及大数据集下的性能表现上均有本质差异。如何在两者之间做出综合判断,可以参考这篇旧文。


AppIntents meet MCP

当大家还在将 AppIntents 视为 Siri 与快捷指令的“配套工具”时,Florian Schweizer 给出了一个更值得关注的方向:将 AppIntents 直接暴露为 MCP(Model Context Protocol)工具,从而让 LLM 能够调用你的应用能力。Florian 基于 SwiftMCP,通过宏构建 MCP Server,并将 AppIntents 无缝映射为 MCP Tools,使 AI Agent 能够直接调用应用中的 Intent,实现跨应用自动化。

去年便有传闻,苹果正在为其生态系统引入 MCP 支持。或许再过两个月,在 WWDC 26 上答案便会揭晓。


创建交互式小组件 (Ride the Lightning Air: Building Interactive WidgetKit Widgets)

很多开发者都会被 WidgetKit 的文档误导,错将 AppIntentTimelineProvider 视为实现交互按钮的关键——实则不然。它是为“用户可配置 Widget”(如长按后编辑设置项)准备的,与交互行为并无直接关系。而真正用于实现交互(按钮点击触发行为)的,依然是最基础的 TimelineProviderWesley Matlock 通过一个虚构航空公司的登机状态 Widget,完整演示了正确路径:使用 TimelineProvider + Button(intent:) + AppGroup 共享存储来构建交互式 Widget。

整个数据流形成一个清晰的闭环:用户操作 → Intent 执行 → 状态写入 → Widget 刷新 → UI 更新。


文件存储与 iCloud:从本地到云端的完整认知

在 iOS / macOS 开发和使用中,文件存储往往被当作“基础能力”,但它实际上直接决定了数据的生命周期与系统行为。

Working with files and directories in iOS 一文中,Natascha Fadeeva 系统梳理了 App Sandbox 的结构,以及 DocumentsLibraryCaches 等目录的职责划分,帮助开发者建立“什么数据该放在哪里”的基本认知,并说明如何避免无关文件被 iCloud 备份。

Howard Oakley 撰写的 Understanding and Testing iCloud 则从系统层面揭示了这些数据的“后续命运”。iCloud 并非单一服务,而是由 CloudKit、iCloud Drive、系统更新等多个子系统组成,不同数据类型对应不同的同步与备份路径。

文件的存储位置,并不仅仅是组织问题,更是在定义它是否会被备份、同步,以及在设备之间如何流转。

因此,iCloud 问题往往不是“是否开启同步”这么简单,而是涉及客户端、网络、缓存以及服务端限流等多个环节。

工具

Bad Dock: 让你的 Dock 图标动起来

这是一个“离谱但严肃”的 macOS 实验项目。Eric Martz 利用公开的 NSDockTile / NSDockTilePlugin API,绕过 Big Sur 之后 Dock 图标的 squircle 限制,将视频流直接渲染进 Dock 图标。实现思路并不复杂,但非常完整:使用 AVAssetReader 解码视频、主动降帧至约 12fps、通过 ring buffer 控制内存占用,最终将一个“整活想法”打磨成了结构清晰的技术验证。

这类项目的价值不在功能本身,而在于展示:系统 API 的边界,往往比文档写得更远。

补充说明:该项目实现的是运行时动态 Dock 图标(应用运行时持续绘制);应用退出后,仅能通过 NSDockTilePlugin 保留静态自定义图标。

往期内容

💝 支持与反馈

如果本期周报对你有帮助,请:

  • 👍 点赞 - 让更多开发者看到
  • 💬 评论 - 分享你的看法或问题
  • 🔄 转发 - 帮助同行共同成长

🚀 拓展 Swift 视野

昨天以前iOS

被 Vibe 摧毁的版权壁垒,与开发者的新护城河 - 肘子的 Swift 周报 #131

作者 Fatbobman
2026年4月13日 22:00

Anthropic 不久前宣布,由于其最新模型 Mythos 在网络安全与代码漏洞挖掘方面的能力“过于强大”,已达到令人不安的程度,因此采取了极为罕见的克制措施:仅向 Project Glasswing 内的少数关键基础设施企业开放,不面向公众发布,普通开发者也无法通过 API 调用

iOS 自定义 UICollectionView 拼图布局 + 布局切换动画实践

作者 汉秋
2026年4月13日 14:51

🧩 iOS 自定义 UICollectionView 拼图布局 + 布局切换动画实践

本文只讲两件事: 👉 如何自定义 UICollectionView 布局 👉 如何优雅地切换布局并处理动画问题


✨ 一、为什么要自定义布局?

系统的 UICollectionViewFlowLayout 有一个明显限制:

👉 只能做规则网格(grid)布局

但很多场景需要:

  • 拼图布局(不规则)
  • 瀑布流变种
  • 卡片混排

例如👇

┌───────────────┐
│       A       │
├───────┬───────┤
│   B   │   C   │
└───────┴───────┘

👉 这种布局 FlowLayout 是做不了的


🧠 二、自定义布局核心原理

自定义布局本质只做三件事:

1️⃣ 计算每个 cell 的 frame

UICollectionViewLayoutAttributes

2️⃣ 返回可见区域的 attributes

override func layoutAttributesForElements(in rect: CGRect)

3️⃣ 告诉 collectionView 内容尺寸

override var collectionViewContentSize: CGSize

🔥 三、实现一个拼图布局(MosaicLayout)

核心代码

class MosaicLayout: UICollectionViewFlowLayout {

    var layoutAttributes: [UICollectionViewLayoutAttributes] = []
    var contentSize: CGSize = .zero

    override func prepare() {
        super.prepare()

        guard let collectionView = collectionView else { return }

        layoutAttributes.removeAll()

        let count = collectionView.numberOfItems(inSection: 0)
        guard count > 0 else { return }

        let width = collectionView.bounds.width
        let height = width

        let frames: [CGRect] = [
            CGRect(x: 0, y: 0, width: 1, height: 0.6),
            CGRect(x: 0, y: 0.6, width: 0.5, height: 0.4),
            CGRect(x: 0.5, y: 0.6, width: 0.5, height: 0.4)
        ]

        for (index, relativeFrame) in frames.enumerated() {

            let frame = CGRect(
                x: relativeFrame.origin.x * width,
                y: relativeFrame.origin.y * height,
                width: relativeFrame.width * width,
                height: relativeFrame.height * height
            )

            let attr = UICollectionViewLayoutAttributes(forCellWith: IndexPath(item: index, section: 0))
            attr.frame = frame
            layoutAttributes.append(attr)
        }

        contentSize = CGSize(width: width, height: height)
    }

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        return layoutAttributes.filter { $0.frame.intersects(rect) }
    }

    override var collectionViewContentSize: CGSize {
        return contentSize
    }
}

🎬 五、布局切换动画

collectionView.setCollectionViewLayout(newLayout, animated: true)

⚠️ 六、崩溃问题

The invalidation context is not an instance of UICollectionViewFlowLayoutInvalidationContext

解决方案

collectionView.setCollectionViewLayout(newLayout, animated: false)
collectionView.reloadData()

🎯 七、总结

👉 自定义布局 = 自己控制 frame 👉 动画关键 = 避免同时更新数据源


如果你觉得有用,欢迎点赞 👍

Flutter iOS应用混淆与安全配置详细文档指南

2026年4月13日 12:03

Flutter iOS应用混淆与安全配置文档

概述

本文档详细描述了iOS应用的混淆与安全配置过程。这些配置旨在保护应用代码、API密钥和敏感数据,防止逆向工程和恶意攻击。配置包括 Dart 代码混淆、原生代码混淆、运行时安全检查和数据安全措施。

混淆与安全措施

Dart代码混淆

Flutter提供了内置的代码混淆功能,通过以下参数启用:

--obfuscate --split-debug-info=./symbols
1

这将:

  • 重命名代码中的标识符,使反编译后的代码难以理解
  • 将调试信息分离到单独的文件中,减少发布版本中的可读信息
  • 保留符号信息用于崩溃分析,但不包含在发布版本中

此外,使用像IpaGuard这样的专业混淆工具可以进一步增强应用安全性。IpaGuard是一款强大的iOS IPA文件混淆工具,无需源码即可对代码和资源进行混淆加密,支持Flutter等多种开发平台,有效增加反编译难度。

原生代码混淆与安全

在iOS上,我们通过以下配置增强安全性:

  1. BuildSettings.xcconfig 配置:
// 启用代码混淆和优化
GCC_OPTIMIZATION_LEVEL = s
SWIFT_OPTIMIZATION_LEVEL = -O
SWIFT_COMPILATION_MODE = wholemodule
DEAD_CODE_STRIPPING = YES

// 安全设置
ENABLE_STRICT_OBJC_MSGSEND = YES
CLANG_WARN_SUSPICIOUS_MOVE = YES
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES
GCC_NO_COMMON_BLOCKS = YES
STRIP_STYLE = all
STRIP_INSTALLED_PRODUCT = YES
COPY_PHASE_STRIP = YES
DEBUG_INFORMATION_FORMAT = dwarf-with-dsym

// 启用应用传输安全
PRODUCT_SETTINGS_URL_SCHEMES = "$(inherit)"
PRODUCT_SETTINGS_APP_TRANSPORT_SECURITY_ALLOWS_ARBITRARY_LOADS = NO

// 添加其他安全属性
OTHER_LDFLAGS = $(inherited) -Wl,-no_pie
12345678910111213141516171819202122
  1. Xcode构建参数
xcodebuild -workspace Runner.xcworkspace -scheme Runner -configuration Release clean build \
  ENABLE_BITCODE=YES STRIP_INSTALLED_PRODUCT=YES DEPLOYMENT_POSTPROCESSING=YES \
  -sdk iphoneos -allowProvisioningUpdates
123

这些参数确保:

  • 启用Bitcode,允许App Store进一步优化代码
  • 移除不必要的符号和调试信息
  • 进行部署后处理操作,应用额外的优化

运行时安全检查

通过以下Swift代码实现运行时安全检查:

// 检查设备是否已越狱
func isJailbroken() -> Bool {


    #if targetEnvironment(simulator)
    return false
    #else
    // 检查常见的越狱文件路径
    let jailbreakPaths = [
        "/Applications/Cydia.app",
        "/Library/MobileSubstrate/MobileSubstrate.dylib",
        "/bin/bash",
        "/usr/sbin/sshd",
        "/etc/apt",
        "/private/var/lib/apt/",
        "/usr/bin/ssh"
    ]

    for path in jailbreakPaths {


        if FileManager.default.fileExists(atPath: path) {


            return true
        }
    }

    // 检查是否可以写入私有目录
    let stringToWrite = "Jailbreak Test"
    do {


        try stringToWrite.write(toFile: "/private/jailbreak.txt", atomically: true, encoding: .utf8)
        try FileManager.default.removeItem(atPath: "/private/jailbreak.txt")
        return true
    } catch {


        // 无法写入,说明没有越狱
    }

    return false
    #endif
}

// 检查是否连接调试器
func isDebuggerAttached() -> Bool {


    #if DEBUG
    return false
    #else
    var info = kinfo_proc()
    var mib: [Int32] = [CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()]
    var size = MemoryLayout<kinfo_proc>.stride
    let status = sysctl(&mib, UInt32(mib.1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556

苹果iOS应用开发上架与推广完整教程

2026年4月13日 10:38

苹果应用上架与推广详细指南

作为苹果个人开发者,确保用户能够顺畅地下载并安装自己精心开发的应用,是不可或缺的一环。接下来,我们将深入探讨实现这一目标的一系列核心步骤。

01应用上架准备

> 注册开发者账号

注册开发者账号是发布应用的第一步,必须在苹果开发者官网完成信息填写和费用支付。你需要前往苹果开发者官网,填写包括姓名、联系方式在内的个人真实信息,并支付相应费用。完成注册与身份验证后,你将获得发布应用的权限。

> 确保应用符合准则

在用户能够顺畅地下载并安装应用之前,开发者需要完成一系列的准备工作。这些准备工作的目标是确保应用的下载和安装过程尽可能顺畅,从而提升用户体验。

应用需符合苹果的质量与安全标准,不得包含恶意代码,需具备良好的UI设计和正常功能。苹果对应用的质量、功能及安全性都设有严格标准。应用不得包含恶意代码,必须具备良好的用户界面设计,且功能正常、无显著漏洞。此外,应用还需遵守法律法规,如不得侵犯他人知识产权,不得诱导用户进行不合理付费等。只有满足这些准则的应用,才有可能通过审核并上架供用户下载。

> 选用开发工具与测试

接下来,我们就将详细介绍这些准备工作。在着手构建和测试应用之前,有几个关键步骤需要完成。这些步骤旨在为应用的顺畅下载和安装铺平道路,进而优化用户体验。

使用Xcode进行开发,选择合适的编程语言并在多设备和系统版本上进行全面测试,确保功能完备、兼容性和性能。通常,Xcode被视为开发首选,它提供了从代码编写到编译、调试的一站式服务。依据应用特性,选择如Swift或Objective-C等编程语言。以一个简易笔记应用为例,我们可以运用Swift来构建用户界面,并实现笔记的增删改查功能。务必在不同型号的iOS设备和多种系统版本上进行详尽测试。这包括验证功能的完备性,例如确认笔记应用能否顺利存储和读取数据;确保兼容性,以保证应用在iPhone、iPad等设备上的一致性;以及评估性能,如测试应用的启动速度和响应时间。例如,在iPhone 13与iPhone 8上分别运行笔记应用,观察是否存在布局混乱或功能失效的问题。此外,开发者也可以使用AppUploader等工具来简化iOS证书的申请和管理,支持在Windows、Linux或Mac系统中操作,无需依赖Mac电脑。

02向App Store提交与推广

> 准备元数据与分类选择

接下来,就可以将你的应用提交到App Store了。 提交应用前需准备应用名称、描述、截图及视频,并选择合适的分类以提高用户搜索的精准性

为应用起一个简洁且能体现核心功能的名称,如“速记笔记”,同时撰写详细且吸引人的描述,突出应用特点、功能和优势。此外,还需提供展示关键界面和操作流程的截图及视频预览,使用户能直观了解应用。依此选择合适的分类,如笔记应用可归于“效率”类别。这样能帮助用户在App Store中更精准地搜索到应用。

> 提交审核与推广策略

通过App Store Connect提交审核,积极进行应用推广,包括社交媒体、博主合作及应用内推广,提高用户下载量。

使用Xcode完成应用打包后,通过App Store Connect提交应用进行审核。或者,使用AppUploader工具上传IPA文件到App Store,它支持多平台,比Application Loader更高效,且不携带设备信息。审核过程通常需数日,期间苹果团队将检查应用是否符合各项标准。若审核过程中发现任何问题,将收到反馈通知,需根据反馈进行相应修改后重新提交。同时,积极推广应用也是提高下载量的关键。

  1. 社交媒体推广:借助Twitter、Facebook、Instagram等社交媒体平台,广泛宣传您的应用。您可以分享应用截图、详细的功能介绍以及使用教程等,以此吸引更多潜在用户的关注。例如,在Twitter上发布一系列关于您的笔记应用使用技巧的推文,并附上应用的下载链接。

  2. 与博主和媒体合作:积极联系相关领域的博主、自媒体或科技媒体,向他们介绍您的应用,并努力争取他们的推荐或报道。例如,与专注于效率类应用评测的博主取得联系,邀请他们体验您的应用并分享他们的使用感受。

  3. 应用内推广:如果您还有其他已发布的应用,可以在这些应用内部推广您的新应用,从而引导现有用户下载并体验。

通过上述推广策略,苹果个人开发者可以成功地推动应用的下载安装,并通过广泛的推广活动提高应用的下载量和用户活跃度,从而让您的开发成果得到更多用户的认可和喜爱。

【DGCharts】iOS 图表渲染事实标准——8 种图表类型、高度可定制,3 行代码画出一条折线

作者 探索者dx
2026年4月13日 10:25

【DGCharts】iOS 图表渲染事实标准——8 种图表类型、高度可定制,3 行代码画出一条折线

iOS三方库精读 · 第 11 期


一、一句话介绍

DGCharts(原名 Charts)是一个用于 iOS/macOS/tvOS 的图表渲染库,它是 Android 端 MPAndroidChart 的 Swift 移植版,让在 UIKit 与 SwiftUI 中绘制专业级折线图、柱状图、饼图等 8 种图表类型变得像配置数据一样简单。

属性 信息
⭐ GitHub Stars 27.5k+
最新稳定版 5.1.0
License Apache 2.0
支持平台 iOS 13+ / macOS 10.14+ / tvOS 13+
语言 Swift(纯 Swift,无 OC 接口)

二、为什么选择它

原生痛点

苹果官方没有提供专用图表框架(Swift Charts 直到 iOS 16 才随 SwiftUI 一起发布),在此之前,iOS 开发者要想画一条像样的折线图,要么:

  • 用 CoreGraphics 手撸贝塞尔曲线 → 代码量大,动画难实现
  • 用 CALayer + CAAnimation → 正确实现坐标系变换极其麻烦
  • 引入 WebView 渲染 ECharts → 性能差、交互延迟

DGCharts 解决的核心痛点:

  1. 零 CoreGraphics 基础可上手:数据 → DataSet → Chart,三行完成
  2. 内置 8 种图表类型:折线、柱状、饼、雷达、散点、K 线、气泡、组合图
  3. 原生手势支持:缩放、拖拽、高亮点击,全部开箱即用
  4. 可定制到像素级别:颜色、字体、轴线、动画均可覆盖
  5. iOS 13+,Swift Charts 的前辈:大量存量 App 仍在使用,理解它有助于迁移评估

与 Swift Charts 的核心区别(一眼对比)

维度 DGCharts Swift Charts(苹果官方)
平台要求 iOS 13+ iOS 16+
UI 框架 UIKit + SwiftUI SwiftUI only
图表类型 8 种(含 K 线/雷达) 折/柱/面积/点(4 种)
动画控制 精细帧级控制 声明式,较难定制
手势交互 内置缩放/平移 需自己实现
维护状态 社区维护,活跃 Apple 官方,稳定更新

三、核心功能速览

基础层(新手必读)

环境集成

SPM(推荐):

// Package.swift 或 Xcode Add Package Dependency
// URL: https://github.com/danielgindi/Charts.git
// from: "5.1.0"

CocoaPods:

pod 'DGCharts', '~> 5.1.0'
最简用法:3 行画折线图
import DGCharts

let chartView = LineChartView(frame: view.bounds)

// 准备数据
let entries = (0..<10).map { ChartDataEntry(x: Double($0), y: Double.random(in: 10...100)) }
let dataSet = LineChartDataSet(entries: entries, label: "销售额")
chartView.data = LineChartData(dataSet: dataSet)

view.addSubview(chartView)

基础层 概念:ChartDataEntry 是最小数据单元(x, y),XxxChartDataSet 是同类数据的集合(样式配置在这里),XxxChartData 是图表的数据容器,XxxChartView 是最终渲染视图。


进阶层(最佳实践)

8 种图表类型一览
类名 用途
LineChartView 折线图(支持曲线插值)
BarChartView 柱状图(支持堆叠/分组)
PieChartView 饼图 / 甜甜圈图
RadarChartView 雷达图(蜘蛛网图)
ScatterChartView 散点图
CandleStickChartView K 线图(OHLC)
BubbleChartView 气泡图
CombinedChartView 组合图(折+柱叠加)
常用样式定制
// 折线数据集样式
let dataSet = LineChartDataSet(entries: entries, label: "趋势")
dataSet.colors = [.systemBlue]          // 线条颜色
dataSet.lineWidth = 2.0                  // 线宽
dataSet.circleColors = [.systemBlue]     // 数据点颜色
dataSet.circleRadius = 4.0               // 数据点半径
dataSet.drawFilledEnabled = true         // 开启渐变填充
dataSet.fillColor = .systemBlue
dataSet.fillAlpha = 0.3
dataSet.mode = .cubicBezier              // 贝塞尔平滑曲线

// 图表视图样式
lineChartView.rightAxis.enabled = false  // 隐藏右轴
lineChartView.xAxis.labelPosition = .bottom
lineChartView.animate(xAxisDuration: 1.0, easingOption: .easeInOutQuart)
交互设置
chartView.scaleXEnabled = true           // 允许 X 轴缩放
chartView.scaleYEnabled = false          // 禁止 Y 轴缩放
chartView.dragEnabled = true             // 允许拖拽
chartView.pinchZoomEnabled = true        // 双指缩放
chartView.doubleTapToZoomEnabled = false // 关闭双击缩放
X 轴自定义 Formatter
// 将数字索引转为日期字符串
class DateAxisValueFormatter: AxisValueFormatter {
    private let dates = ["1月", "2月", "3月", "4月", "5月", "6月"]
    func stringForValue(_ value: Double, axis: AxisBase?) -> String {
        let index = Int(value)
        return index < dates.count ? dates[index] : ""
    }
}
chartView.xAxis.valueFormatter = DateAxisValueFormatter()

深入层(源码视角)

DGCharts 的核心渲染架构遵循 Renderer 模式

  • ChartView → 入口,持有 ChartData 和多个 DataRenderer
  • DataRenderer(如 LineChartRenderer)→ 负责具体图形绘制,使用 CGContext
  • ChartViewBase → 提供手势识别、坐标变换 Transformer
  • Transformer → 负责将数据坐标系映射到屏幕坐标系(核心矩阵变换)

每次 data 被 set,触发 notifyDataSetChanged()calcMinMax()setNeedsDisplay(),最终回调 draw(_ rect:) 由各 Renderer 完成绘制。


四、实战演示

场景:股票日 K 线 + 成交量组合图

// Swift UIKit,iOS 17+
import UIKit
import DGCharts

class StockChartVC: UIViewController {

    private lazy var combinedChart: CombinedChartView = {
        let chart = CombinedChartView()
        chart.legend.enabled = true
        chart.rightAxis.enabled = false
        chart.xAxis.labelPosition = .bottom
        chart.animate(xAxisDuration: 0.8)
        return chart
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemBackground
        view.addSubview(combinedChart)
        combinedChart.frame = CGRect(x: 16, y: 100,
                                      width: view.bounds.width - 32,
                                      height: 320)
        setupChart()
    }

    private func setupChart() {
        // ---- K 线数据 ----
        let candleEntries: [CandleChartDataEntry] = [
            CandleChartDataEntry(x: 0, shadowH: 185, shadowL: 162, open: 165, close: 178),
            CandleChartDataEntry(x: 1, shadowH: 190, shadowL: 170, open: 178, close: 188),
            CandleChartDataEntry(x: 2, shadowH: 195, shadowL: 175, open: 188, close: 177),
            CandleChartDataEntry(x: 3, shadowH: 182, shadowL: 165, open: 177, close: 169),
            CandleChartDataEntry(x: 4, shadowH: 175, shadowL: 160, open: 169, close: 172),
        ]
        let candleSet = CandleChartDataSet(entries: candleEntries, label: "K 线")
        candleSet.shadowColor = .darkGray
        candleSet.shadowWidth = 1.5
        candleSet.decreasingColor = .systemRed     // 阴线颜色
        candleSet.decreasingFilled = true
        candleSet.increasingColor = .systemGreen   // 阳线颜色
        candleSet.increasingFilled = true
        candleSet.neutralColor = .systemGray

        // ---- 成交量柱状图 ----
        let barEntries = [
            BarChartDataEntry(x: 0, y: 3200),
            BarChartDataEntry(x: 1, y: 5400),
            BarChartDataEntry(x: 2, y: 4100),
            BarChartDataEntry(x: 3, y: 6800),
            BarChartDataEntry(x: 4, y: 3900),
        ]
        let barSet = BarChartDataSet(entries: barEntries, label: "成交量(手)")
        barSet.colors = [.systemBlue.withAlphaComponent(0.5)]
        barSet.axisDependency = .right  // 绑定右轴(虽然禁用了,仅用于数据缩放)

        // ---- 组合 ----
        let combined = CombinedChartData()
        combined.candleData = CandleChartData(dataSet: candleSet)
        combined.barData = BarChartData(dataSet: barSet)

        // X 轴 Formatter
        combinedChart.xAxis.valueFormatter = IndexAxisValueFormatter(
            values: ["Mon", "Tue", "Wed", "Thu", "Fri"]
        )
        combinedChart.xAxis.granularity = 1
        combinedChart.data = combined
    }
}

五、源码亮点

进阶层:值得借鉴的用法

MarkerView——点击数据点显示气泡
// 自定义 Marker,继承 MarkerView
class BalloonMarker: MarkerView {
    private var label: String = ""

    override func refreshContent(entry: ChartDataEntry, highlight: Highlight) {
        label = String(format: "%.1f", entry.y)
        super.refreshContent(entry: entry, highlight: highlight)
    }

    override func draw(context: CGContext, point: CGPoint) {
        // 用 CoreGraphics 绘制气泡背景 + 文字
        let attrs: [NSAttributedString.Key: Any] = [
            .font: UIFont.systemFont(ofSize: 12),
            .foregroundColor: UIColor.white
        ]
        let attrStr = NSAttributedString(string: label, attributes: attrs)
        let size = attrStr.size()
        let rect = CGRect(x: point.x - size.width / 2 - 8,
                          y: point.y - size.height - 12,
                          width: size.width + 16, height: size.height + 8)
        context.setFillColor(UIColor.systemBlue.cgColor)
        UIBezierPath(roundedRect: rect, cornerRadius: 6).fill()
        attrStr.draw(in: rect.insetBy(dx: 8, dy: 4))
    }
}

// 绑定
chartView.marker = BalloonMarker()
chartView.drawMarkers = true

深入层:设计思想解析

1. Transformer 矩阵变换

DGCharts 用一个 CGAffineTransform 维护数据坐标 → 屏幕坐标的映射:

屏幕坐标 = 数据坐标 × scale + offset + chart padding

每次缩放/平移,只更新 transform 矩阵然后 setNeedsDisplay,避免重新计算所有点,是渲染性能的核心保障。

2. Protocol-Oriented Renderer
// 每种图表有独立 Renderer,统一协议
public protocol DataRenderer: AnyObject {
    func drawData(context: CGContext)
    func drawValues(context: CGContext)
    func drawExtras(context: CGContext)
    func drawHighlighted(context: CGContext, indices: [Highlight])
}

遵循开闭原则,新增图表类型只需实现此协议,不修改任何现有代码。

3. ChartHighlighter — 触摸高亮的寻值逻辑

触摸事件 → getHighlight(x:y:) → 二分查找最近 x → 在该 x 的所有 DataSet 中找最近 y → 返回 Highlight(x:y:dataSetIndex:) → 触发 highlightValue。整套流程在主线程同步完成,保证交互响应 < 16ms。


六、踩坑记录

问题 1:数据更新后图表没有刷新

  • 原因:修改了 entries 数组但没有通知图表
  • 解决
    chartView.data?.notifyDataChanged()
    chartView.notifyDataSetChanged()
    

问题 2:X 轴标签显示 0, 1, 2 而不是自定义文字

  • 原因:没有设置 xAxis.valueFormatter 或者 granularity 不正确
  • 解决
    xAxis.valueFormatter = IndexAxisValueFormatter(values: yourLabels)
    xAxis.granularity = 1.0   // 必须设置,否则可能跳步
    

问题 3:饼图中间的 "洞" 大小无法控制

  • 原因holeRadiusPercent 默认 0.5(即 50%)
  • 解决
    pieChart.holeRadiusPercent = 0.3  // 调整洞的比例,0 = 实心饼图
    

问题 4:动画结束后视图突然闪烁

  • 原因animateXanimateY 同时调用,两个动画结束时各触发一次 setNeedsDisplay
  • 解决:使用 animate(xAxisDuration:yAxisDuration:) 合并为一次调用

问题 5:在 SwiftUI 中使用时手势冲突

  • 原因:DGCharts 的 UIPanGestureRecognizer 与 SwiftUI ScrollView 的手势抢夺
  • 解决
    chartView.dragEnabled = false       // 在 ScrollView 内关闭拖拽
    chartView.pinchZoomEnabled = false  // 关闭缩放
    

问题 6:Swift Package Manager 拉取超时

  • 原因:Charts 仓库体积较大(含所有 Demo)
  • 解决:在 package.json 中指定 .upToNextMinor(from: "5.1.0"),避免每次重新解析

七、延伸思考

同类库对比

维度 DGCharts Swift Charts (Apple) AAChartKit Charts.js (WebView)
平台要求 iOS 13+ iOS 16+ iOS 11+ iOS (via WKWebView)
图表类型数 8 4 10+ 20+
性能 原生,高 原生,高 原生,良好 WebView,差
K 线图
SwiftUI 支持 UIViewRepresentable 原生 UIViewRepresentable WKWebViewRepresentable
维护状态 社区活跃 Apple 官方 活跃 Web 项目
学习曲线 中等 低(SwiftUI 声明式) 需了解 JS

推荐使用场景

  • ✅ iOS 13~15 的存量 App,无法使用 Swift Charts
  • ✅ 需要 K 线图、雷达图等特殊类型
  • ✅ 需要精细控制动画和交互手势
  • ✅ 同时支持 iOS 和 macOS 的多平台 App

不推荐场景

  • ❌ 全新 App,最低支持 iOS 16+,且只用 SwiftUI → 直接用 Swift Charts
  • ❌ 只需要一个简单静态饼图/柱图 → 手动 CoreGraphics 更轻量
  • ❌ 需要超复杂的动态可视化(如力导向图)→ 考虑 WebView + D3.js

八、参考资源


九、本期互动

小作业

使用 LineChartView 实现一个实时动态折线图:每秒追加一个随机数据点,并保持最多显示最近 20 个点(超出则移除最旧的点),同时保证图表 X 轴自动滚动跟随最新数据。完成后在评论区贴出你的核心更新逻辑。

思考题

DGCharts 的 Transformer 用矩阵变换把数据坐标映射到屏幕坐标,这种设计和 CALayertransform 有何本质区别?如果让你自己实现一套坐标系映射,你会选择哪种方案?为什么?

读者征集

下一期我们将深入 Hero(转场动画库)。你在项目中用过哪些自定义转场方案(Hero / UIViewControllerTransitioningDelegate / NavigationController Push 动画)?欢迎评论区分享你的实践经验,优质回答会收录进下一期《踩坑记录》。


📅 本系列每周五晚更新 ✅ 第1期:Alamofire · ✅ 第2期:SDWebImage · ✅ 第3期:Kingfisher · ✅ 第4期:SnapKit · ✅ 第5期:ListDiff · ✅ 第6期:RxSwift · ✅ 第7期:Lottie · ✅ 第8期:MarkdownUI · ✅ 第9期:AFNetworking · ➡️ 第11期:DGCharts · ○ 第12期:Hero

Objective-C Runtime 完整机制:objc_class /cache/bits 源码解析

作者 MonkeyKing
2026年4月13日 08:08

Objective-C(以下简称 OC)的灵活性、动态性,核心源于其底层的 Runtime 机制。而 Runtime 所有动态行为(消息发送

objc_class 的核心字段中,superclass(父类指针)、cache(方法缓存)、bits(类数据指针+标志位)三者缺一不可。其中,cache 决定了方法调用的效率,bits 存储了类的核心数据(方法、属性、协议等),二者是理解 Runtime 动态机制的关键。

很多开发者使用 OC 多年,却只停留在“会用”层面,对objc_class 的底层结构、cache 的缓存机制、bits 的数据存储逻辑一知半解。本文将基于 Apple 开源的 objc4 源码(最新稳定版),逐行解析 objc_classcachebits 的底层实现,结合 Runtime 核心流程,让你彻底吃透 OC 类的底层逻辑。

一、前置基础:Runtime 与 objc_class 的核心关联

在解析具体源码前,先明确两个核心前提,避免陷入细节误区:

  1. OC 是“动态语言”,其类和对象的行为并非编译期确定,而是由 Runtime 动态解析——比如方法调用、属性访问,最终都会被 Runtime 转化为底层函数调用(如 objc_msgSend)。

先看最基础的 objc_object 结构体(所有对象的祖宗),它是理解 objc_class 的前提:

// 所有OC对象的底层结构体(精简版,保留核心字段)
struct objc_object {
    isa_t isa; // 64位联合体,存储类指针、引用计数、标志位等信息
};

// isa_t 的核心结构(ARM64架构,iOS真机环境)
union isa_t {
    uintptr_t bits; // 原始64位数值,承载所有信息
    // 位域展开(64位按位分配)
    struct {
        uintptr_t nonpointer : 1;        // bit 0:是否是优化后的isa(0=纯指针,1=包含额外信息)
        uintptr_t has_assoc : 1;         // bit 1:是否有关联对象
        uintptr_t has_cxx_dtor : 1;      // bit 2:是否有C++析构函数
        uintptr_t shiftcls : 33;         // bit 3-35:类指针(右移3位存储,节省空间)
        uintptr_t magic : 6;             // bit 36-41:固定值0x1a,用于调试校验
        uintptr_t weakly_referenced : 1; // bit 42:是否被弱引用
        uintptr_t unused : 1;            // bit 43:未使用
        uintptr_t has_sidetable_rc : 1;  // bit 44:引用计数是否溢出到SideTable
        uintptr_t extra_rc : 19;         // bit 45-63:引用计数-1(存储额外引用计数)
    };
};

简单来说,isa 的核心作用是“标识对象的类型”——通过shiftcls 字段,对象能找到自己对应的类(objc_class),而类的 isa 则指向元类(Meta Class),这是 OC 实现方法调用的基础。

二、核心解析:objc_class 结构体源码拆解

OC 中的“类”(如 NSObject、自定义类),底层本质是 objc_class 结构体的实例。以下是从 objc4 源码中提取的精简版 objc_class 结构体(保留核心字段,省略辅助方法),也是本文的核心分析对象:

// 类的底层结构体(继承自objc_object,因此包含isa字段)
struct objc_class : objc_object {
    // 1. 父类指针:指向当前类的父类(如NSObject的父类是nil)
    Class superclass;
    // 2. 方法缓存:哈希表结构,缓存最近调用的方法,提升调用效率
    cache_t cache;
    // 3. 类数据指针+标志位:存储类的核心数据(方法、属性、协议等)
    class_data_bits_t bits;
    
    // 核心方法:从bits中取出类的可读写数据(class_rw_t)
    class_rw_t *data() const {
        return bits.data();
    }
};

从源码可以看出,objc_class 继承自 objc_object,因此它本身也有 isa 字段(继承而来),同时新增了三个核心字段:superclasscachebits

三者的核心关系的是:superclass 负责继承链的构建,cache 负责方法调用的缓存优化,bits 负责存储类的核心业务数据,三者协同支撑起 OC 类的所有动态行为。

补充:Class 类型的本质

我们日常使用的 Class 类型,本质是 objc_class 的指针别名,源码定义如下:

typedef struct objc_class *Class;

这就是为什么我们可以用 Class cls = [NSObject class]; 获取类对象——本质是获取 objc_class 结构体的指针。

三、深度解析:cache_t(方法缓存)的底层实现

在 OC 中,方法调用是高频操作(如 [self method]),如果每次调用都遍历类的方法列表查找,会严重影响性能。cache_t 的核心作用就是“缓存最近调用的方法”,下次调用时直接从缓存中取出,无需重复查找,这是 Runtime 优化方法调用效率的关键。

1. cache_t 结构体源码(精简版)

// 方法缓存结构体(哈希表实现)
struct cache_t {
    // 缓存存储的数组(数组元素是cache_entry_t类型,存储方法名和函数指针)
    bucket_t *_buckets;
    // 缓存的容量(总是2的幂,如4、8、16,方便哈希计算)
    mask_t _mask;
    // 已缓存的方法数量(当count > mask * 3/4时,会触发缓存扩容)
    mask_t _occupied;
    
    // 核心方法:插入方法缓存
    void insert(SEL sel, IMP imp, id receiver);
    // 核心方法:查找方法缓存
    IMP lookup(SEL sel);
};

其中,bucket_t 是缓存的“桶”,存储单个方法的缓存信息,源码如下:

// 单个缓存项(存储一个方法的信息)
struct bucket_t {
    SEL _sel; // 方法名(选择子,本质是const char*,如@selector(method))
    IMP _imp; // 函数指针(指向方法的具体实现代码地址)
    
    // 辅助方法:获取方法名和函数指针
    SEL sel() const { return _sel; }
    IMP imp() const { return (IMP)((uintptr_t)_imp ^ (uintptr_t)this); }
};

2. cache_t 的核心特性与工作流程

理解 cache_t,关键要掌握“哈希表存储”“缓存插入”“缓存查找”“缓存扩容”四个核心流程,结合源码逻辑逐一拆解:

(1)哈希表存储逻辑

cache_t 采用“开放寻址法”实现哈希表:

  • 用方法名 SEL 的哈希值,对_mask(缓存容量-1)取模,得到当前方法在 _buckets 数组中的索引;
  • 如果该索引对应的桶为空,直接存入当前方法的 SELIMP
  • 如果该索引已被占用(哈希冲突),则顺次查找下一个空桶,直到找到空桶存入。

这里 _mask = 容量 - 1(如容量为8,_mask=7),取模操作可简化为 hash & _mask,效率远高于传统取模运算,这也是缓存容量必须是2的幂的原因。

(2)缓存插入流程(insert 方法核心逻辑)

当我们第一次调用某个方法时,Runtime 会先查找方法列表,找到后将其插入 cache_t,核心步骤如下(结合源码逻辑简化):

  1. 计算方法 SEL 的哈希值 hash = sel_hash(sel)

注意:IMP 存储时会进行“异或加密”(_imp = (IMP)((uintptr_t)imp ^ (uintptr_t)this)),读取时再解密,这是苹果的安全优化,防止恶意篡改方法实现。

(3)缓存查找流程(lookup 方法核心逻辑)

当我们再次调用该方法时,Runtime 会先从 cache_t 中查找,核心步骤如下:

  1. 计算 SEL 的哈希值,得到索引 index = hash & _mask

(4)缓存扩容机制

_occupied(已缓存数量)超过 _mask * 3/4(缓存容量的75%)时,会触发缓存扩容,核心逻辑:

  • 新容量 = 旧容量 * 2(始终保持2的幂);
  • 创建新的 _buckets 数组(容量为新容量);
  • 将旧缓存中的所有方法,重新哈希后插入新数组;
  • 更新 _mask(新容量-1)和 _occupied(重置为旧的数量),释放旧数组内存。

3. cache_t 的实战意义

理解 cache_t 的缓存机制,能帮我们解释很多实际开发中的现象:

  • 为什么“首次调用方法比后续调用慢”?—— 首次调用需要查找方法列表,后续调用直接从缓存中获取,效率更高;
  • 为什么分类(Category)的方法会覆盖原类方法?—— 分类方法会在 Runtime 加载时,插入到类的方法列表头部,首次调用时会优先被缓存,后续调用会直接使用分类的方法;
  • 为什么频繁调用不同方法,会导致缓存命中率下降?—— 缓存容量有限,频繁切换方法会导致缓存被覆盖,需要重新查找方法列表。

四、深度解析:class_data_bits_t(bits)的底层实现

如果说 cache_t 是“方法调用的加速器”,那么 bits 就是“类的核心数据仓库”——它存储了类的所有核心信息,包括方法列表、属性列表、协议列表、成员变量列表等,是 Runtime 实现动态特性的核心载体。

bits 的类型是 class_data_bits_t,它本身是一个“64位整数”,低位存储标志位,高位存储指向 class_rw_t 的指针(类的可读写数据),这种设计既能节省内存,又能高效访问数据。

1. class_data_bits_t 结构体源码(精简版)

// bits的类型:存储类数据指针+标志位
struct class_data_bits_t {
private:
    uintptr_t bits; // 64位整数,核心存储载体
    
public:
    // 核心方法:从bits中取出class_rw_t指针(核心数据)
    class_rw_t *data() const {
        // FAST_DATA_MASK:掩码,用于过滤标志位,取出高位的指针地址
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
    
    // 标志位操作方法(示例)
    bool isSwiftLegacy() const { return getBit(FAST_IS_SWIFT_LEGACY); }
    bool isSwiftStable() const { return getBit(FAST_IS_SWIFT_STABLE); }
    
private:
    // 读取指定位置的标志位
    bool getBit(uintptr_t bit) const {
        return (bits & bit) != 0;
    }
};

其中,FAST_DATA_MASK 是关键掩码(ARM64架构下),源码定义如下:

#define FAST_DATA_MASK 0x00007ffffffffff8UL

该掩码的作用是“过滤低位的标志位,保留高位的指针地址”——ARM64架构下,bits 的 bit 346 存储 class_rw_t 指针,bit 02 存储标志位,通过 bits & FAST_DATA_MASK 可快速取出指针。

2. 核心标志位解析(bit 0~2)

bits 的低位(bit 0~2)存储了3个核心标志位,用于标识类的类型和特性,源码定义如下:

  • FAST_IS_SWIFT_LEGACY = 1 << 0(bit 0):是否是旧版 Swift 类(OC 类该标志位为0);
  • FAST_IS_SWIFT_STABLE = 1 << 1(bit 1):是否是新版 Swift 类(OC 类该标志位为0);
  • FAST_HAS_DEFAULT_RR = 1 << 2(bit 2):是否有默认的 retain/release 方法(ARC 环境下,OC 类默认有)。

这些标志位的作用是“快速区分类的类型”,Runtime 在处理方法调用、内存管理时,会根据这些标志位执行不同的逻辑。

3. class_rw_t:bits 指向的核心数据

bits.data() 会返回 class_rw_t 指针,class_rw_t 是“类的可读写数据”结构体,存储了类的方法、属性、协议等核心信息,源码精简如下:

// 类的可读写数据(runtime运行时可修改)
struct class_rw_t {
    // 版本号(用于兼容不同的Runtime版本)
    uint32_t version;
    // 类的flags(标志位,如是否是元类、是否有分类等)
    uint32_t flags;
    
    // 方法列表(存储类的所有方法,包括实例方法和类方法)
    method_array_t methods;
    // 属性列表(存储类的所有属性)
    property_array_t properties;
    // 协议列表(存储类遵循的所有协议)
    protocol_array_t protocols;
    
    // 成员变量列表(存储类的所有成员变量)
    ivar_array_t ivars;
};

其中,method_array_tproperty_array_t 等都是“动态数组”(本质是指针数组),支持 Runtime 运行时动态添加(比如分类添加方法、属性),这也是 OC 支持“动态扩展”的核心原因。

4. bits 的核心工作流程

bits 的工作流程非常简单,核心是“通过掩码取出数据指针,访问类的核心信息”,结合 Runtime 方法查找流程,可总结为:

  1. 当 Runtime 需要查找类的方法时,先通过 objc_class->bits.data() 取出 class_rw_t 指针;

五、三者协同:objc_class / cache / bits 完整工作流程

结合前面的解析,我们用一个“方法调用”的完整流程,串联起 objc_classcachebits 的协同工作,让你彻底理解三者的关联:

  1. 调用 [obj method],OC 编译器将其转化为 Runtime 函数调用 objc_msgSend(obj, @selector(method))

从这个流程可以看出:cache 负责“加速查找”,bits 负责“存储数据”,objc_class 负责“组织关联”,三者协同,构成了 OC 方法调用的底层逻辑,也是 Runtime 动态机制的核心。

六、实战延伸:源码解析的实际应用

很多开发者会问:“搞懂这些源码,对实际开发有什么用?” 其实,Runtime 源码解析的价值,在于“解决底层问题、实现高级特性”,以下是3个常见的实战场景:

1. 解决“方法未实现”崩溃问题

当调用未实现的方法时,会触发 unrecognized selector sent to instance 崩溃。通过理解 cachebits 的查找流程,我们可以通过 Runtime 钩子(如 resolveInstanceMethod),动态添加方法实现,避免崩溃:

// 动态添加未实现的方法
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(unimplementedMethod)) {
        // 动态添加方法实现
        class_addMethod([self class], sel, (IMP)dynamicMethod, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

// 动态方法实现
void dynamicMethod(id self, SEL _cmd) {
    NSLog(@"动态添加的方法实现");
};

2. 实现“方法交换”(Method Swizzling)

方法交换是 OC 开发中常用的高级技巧,其底层依赖 bits 中的方法列表。通过修改 class_rw_t->methods 中方法的 IMP,可以实现方法交换:

// 方法交换
+ (void)swizzleMethod {
    Class cls = [self class];
    // 获取两个方法的SEL
    SEL originalSel = @selector(originalMethod);
    SEL swizzledSel = @selector(swizzledMethod);
    
    // 获取方法实例
    Method originalMethod = class_getInstanceMethod(cls, originalSel);
    Method swizzledMethod = class_getInstanceMethod(cls, swizzledSel);
    
    // 交换方法实现
    method_exchangeImplementations(originalMethod, swizzledMethod);
}

3. 动态添加属性(关联对象)

OC 中不能直接给分类添加属性,但可以通过 Runtime 的关联对象机制实现,其底层依赖 objc_objecthas_assoc 标志位(存储在 isa 中)和 bits 中的相关逻辑:

// 给分类添加关联属性
@interface NSObject (Associated)
@property (nonatomic, copy) NSString *associatedStr;
@end

@implementation NSObject (Associated)
- (NSString *)associatedStr {
    return objc_getAssociatedObject(self, _cmd);
}

- (void)setAssociatedStr:(NSString *)associatedStr {
    objc_setAssociatedObject(self, @selector(associatedStr), associatedStr, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
@end

七、总结:Runtime 核心机制的本质

通过对 objc_classcachebits 的源码解析,我们可以发现:OC Runtime 的核心本质,是“用结构体存储类和对象的信息,用哈希表优化查找效率,用动态数组支持扩展”。

总结三个核心要点,帮你快速掌握本文重点:

  1. objc_class 是类的底层载体,继承自 objc_object,包含 superclasscachebits 三个核心字段,负责组织类的继承关系和核心数据;

理解这些底层源码,不仅能帮你解决实际开发中的底层问题,更能让你从根源上理解 OC 的动态性,为后续学习更高级的 Runtime 特性(如元类、消息转发、分类加载)打下基础。毕竟,只有看透底层,才能真正掌控 OC 开发。

Recent lld/ELF performance improvements

作者 MaskRay
2026年4月12日 15:00

Since the LLVM 22 branch was cut, I've landed patches thatparallelize more link phases and cut task-runtime overhead. This postcompares current main against lld 22.1, mold, and wild.

Headline: a Release+Asserts clang --gc-sections link is1.37x as fast as lld 22.1; Chromium debug with --gdb-indexis 1.07x as fast. mold and wild are still ahead — the last sectionexplains why.

Benchmark

lld-0201 is main at 2026-02-01 (6a1803929817);lld-load is main plus the new[ELF] Parallelize input file loading. mold andwild run with --no-fork so the wall-clocknumbers include the linker process itself.

Three reproduce tarballs, --threads=8,hyperfine -w 1 -r 10, pinned to CPU cores withnumactl -C.

Workload lld-0201 lld-load mold wild
clang-23 Release+Asserts, --gc-sections 1.255 s 917.8 ms 552.6 ms 367.2 ms
clang-23 Debug (no --gdb-index) 4.582 s 4.306 s 2.464 s 1.565 s
clang-23 Debug (--gdb-index) 6.291 s 5.915 s 4.001 s N/A
Chromium Debug (no --gdb-index) 6.140 s 5.904 s 2.665 s 2.010 s
Chromium Debug (--gdb-index) 7.857 s 7.322 s 3.786 s N/A

Note that llvm/lib/Support/Parallel.cpp design keeps themain thread idle during parallelFor, so--threads=N really utilizes N+1 threads.

wild does not yet implement --gdb-index — it silentlywarns and skips, producing an output about 477 MB smaller on Chromium.For fair 4-way comparisons I also strip --gdb-index fromthe response file; the no --gdb-index rows above use thatsetup.

A few observations before diving in:

  • The --gdb-index surcharge on the Chromium link is+1.42 s for lld (5.90 s → 7.32 s) versus+1.12 s for mold (2.67 s → 3.79 s). This is currently oneof the biggest remaining gaps.
  • Excluding --gdb-index, mold is 1.66x–2.22x as fast andwild 2.5x–2.94x as fast on this machine. There is plenty of roomleft.
  • clang-23 Release+Asserts --gc-sections (workload 1) hascollapsed from 1.255 s to 918 ms, a 1.37x speedup over 10 weeks. Most ofthat came from the parallel --gc-sections mark, parallelinput loading, and the task-runtime cleanup below — each contributing amultiplicative factor.

macOS (Apple M4) notes

The same clang-23 Release+Asserts link, --threads=8, onan Apple M4 (macOS 15, system allocator for all four linkers):

Linker Wall User Sys (User+Sys)/Wall
lld-0201 324.4 ± 1.5 ms 502.1 ms 171.7 ms 2.08x
lld-load 221.5 ± 1.8 ms 476.5 ms 368.8 ms 3.82x
mold 201.2 ± 1.7 ms 875.1 ms 220.5 ms 5.44x
wild 107.1 ± 0.5 ms 456.8 ms 284.6 ms 6.92x

Parallelize--gc-sections mark

Garbage collection had been a single-threaded BFS overInputSection graph. On a Release+Asserts clang link,markLive was ~315 ms of the 1562 ms wall time (20%).

commit6f9646a598f2 adds markParallel, a level-synchronizedBFS. Each BFS level is processed with parallelFor; newlydiscovered sections land in per-thread queues, which are merged beforethe next level. The parallel path activates when!TrackWhyLive && partitions.size() == 1.Implementation details that turned out to matter:

  • Depth-limited inline recursion (depth < 3) beforepushing to the next-level queue. Shallow reference chains stay hot incache and avoid queue overhead.
  • Optimistic "load then compare-exchange" section-flag dedup insteadof atomic fetch-or. The vast majority of sections are visited once, sothe load almost always wins.

On the Release+Asserts clang link, markLive dropped from315 ms to 82 ms at --threads=8 (from 199 ms to 50 ms at--threads=16); total wall time 1.16x–1.18x.

Two prerequisite cleanups were needed for correctness:

  • commit6a874161621e moved Symbol::used into the existingstd::atomic<uint16_t> flags. The bitfield waspreviously racing with other mark threads.
  • commit2118499a898b decoupled SharedFile::isNeeded from themark walk. --as-needed used to flip isNeededinside resolveReloc, which would have required coordinatedwrites across threads; it is now a post-GC scan of global symbols.

Parallelize input fileloading

Historically, LinkerDriver::createFiles walked thecommand line and called addFile serially.addFile maps the file (MemoryBuffer::getFile),sniffs the magic, and constructs an ObjFile,SharedFile, BitcodeFile, orArchiveFile. For thin archives it also materializes eachmember. On workloads with hundreds of archives and thousands of objects,this serial walk dominates the early part of the link.

The pending patch will rewrite addFile to record aLoadJob for each non-script input together with a snapshotof the driver's state machine (inWholeArchive,inLib, asNeeded, withLOption,groupId). After createFiles finishes,loadFiles fans the jobs out to worker threads. Linkerscripts stay on the main thread because INPUT() andGROUP() recursively call back intoaddFile.

A few subtleties made this harder than it sounds:

  • BitcodeFile and fatLTO construction callctx.saver / ctx.uniqueSaver, both of which arenon-thread-safe StringSaver /UniqueStringSaver. I serialized those constructors behind amutex; pure-ELF links hit it zero times.
  • Thin-archive member buffers used to be appended toctx.memoryBuffers directly. To keep the outputdeterministic across --threads values, each job nowaccumulates into a per-job SmallVector which is merged intoctx.memoryBuffers in command-line order.
  • InputFile::groupId used to be assigned inside theInputFile constructor from a global counter. With parallelconstruction the assignment race would have been unobservable but stillugly; b6c8cba516daabced0105114a7bcc745bc52faaehoists ++nextGroupId into the serial driver loop and storesthe value into each file after construction.

The output is byte-identical to the old lld and deterministic across--threads values, which I verified with diffacross --threads={1,2,4,8} on Chromium.

A --time-trace breakdown is useful to set expectations.On Chromium, the serial portion of createFiles accounts foronly ~81 ms of the 5.9 s wall, and loadFiles (after thispatch) runs in ~103 ms in parallel. Serial readFile/mmap isnot the bottleneck. What moves the needle is overlapping the per-fileconstructor work — magic sniffing, archive member materialization,bitcode initialization — with everything else that now kicks off on themain thread while workers chew through the job list.

Extending parallelrelocation scanning

Relocation scanning has been parallel since LLVM 17, but three caseshad opted out via bool serial:

  1. -z nocombreloc, because .rela.dyn mergedrelative and non-relative relocations and needed deterministicordering.
  2. MIPS, because MipsGotSection is mutated duringscanning.
  3. PPC64, because ctx.ppc64noTocRelax (aDenseSet of (Symbol*, offset) pairs) waswritten without a lock.

commit076226f378df and commitdc4df5da886e separate relative and non-relative dynamic relocationsunconditionally and always build .rela.dyn withcombreloc=true; the only remaining effect of-z nocombreloc is suppressing DT_RELACOUNT. commit2f7bd4fa9723 then protects ctx.ppc64noTocRelax with thealready-existing ctx.relocMutex, which is only taken onrare slow paths. After these changes, only MIPS still runs scanningserially.

Target-specific relocationscanning

Relocation scanning used to go through a generic loop inRelocations.cpp that calledTarget->getRelExpr through a virtual for everyrelocation — once to classify the expression kind (PC-relative, PLT,TLS, etc.) and again from the TLS-optimization dispatch. On anyrealistic link that is a hot inner loop running over tens of millions ofrelocations, and the virtual call plus its post-dispatch switch are areal fraction of the cost.

The fix is to move the whole per-section scan loop intotarget-specific code, so each Target::scanSection /scanSectionImpl pair can inline its owngetRelExpr, handle TLS optimization in-place, andspecialize for the two or three relocation kinds that dominate on thatarchitecture. Rolled out across most backends in early 2026:

  • 4b887533389cx86 (i386 / x86-64). On lld's own object files,R_X86_64_PC32 and R_X86_64_PLT32 make up ~95%of relocations and now hit an inlined hot path.
  • 371e0e2082e9AArch64, 4ea72c1e8cbdRISC-V, cd01e6526af6LoongArch, c04b00de7508ARM, 6d9169553029Hexagon, aec1c984266cSystemZ, 5e87f8147d68PPC32, aecc4997bf12PPC64.

Besides devirtualization, inlining TLS relocation handling intoscanSectionImpl let the TLS-optimization-specificexpression kinds be replaced with general ones:R_RELAX_TLS_GD_TO_LE / R_RELAX_TLS_LD_TO_LE /R_RELAX_TLS_IE_TO_LE fold into R_TPREL,R_RELAX_TLS_GD_TO_IE folds into R_GOT_PC, andgetTlsGdRelaxSkip goes away. What remains in the shareddispatch path — getRelExpr called fromrelocateNonAlloc and relocateEH — is a muchsmaller set.

Average Scan relocations wall time on a clang-14 link(--threads=8, x86-64, 50 runs, measured via--time-trace) drops from 110 ms to 102 ms, ~8% from the x86commit alone.

Faster getSectionPiece

Merge sections (SHF_MERGE) split their input into"pieces". Every reference into a merge section needs to map an offset toa piece. The old implementation was always a binary search inMergeInputSection::pieces, called fromMarkLive, includeInSymtab, andgetRelocTargetVA.

commit42cc45477727 changes this in two ways:

  1. For non-string fixed-size merge sections,getSectionPiece uses offset / entsizedirectly.
  2. For non-section Defined symbols pointing into mergesections, the piece index is pre-resolved duringsplitSections and packed into Defined::valueas ((pieceIdx + 1) << 32) | intraPieceOffset.

The binary search is now limited to references via section symbols(addend-based), which is common on AArch64 but rare on x86-64 where theassembler emits local labels for .L references intomergeable strings. The clang-relassert link with--gc-sections is 1.05x as fast.

Optimizingthe underlying llvm/lib/Support/Parallel.cpp

All of the wins above rely onllvm/lib/Support/Parallel.cpp, the tiny work-stealing-ishtask runtime shared by lld, dsymutil, and a handful of debug-info tools.Four changes in that file mattered:

  • commitc7b5f7c635e2 — parallelFor used to pre-split work intoup to MaxTasksPerGroup (1024) tasks and spawn each throughthe executor's mutex + condvar. It now spawns onlyThreadCount workers; each grabs the next chunk via anatomic fetch_add. On a clang-14 link(--threads=8), futex calls dropped from ~31K to ~1.4K(glibc release+asserts); wall time 927 ms → 879 ms. This is the reasonthe parallel mark and parallel scan numbers are worth quoting at all —on the old runtime, spawn overhead was a real fraction of the work beingparallelized.
  • commit9085f74018a4 — TaskGroup::spawn() replaced themutex-based Latch::inc() with an atomicfetch_add and passes the Latch& throughExecutor::add() so the worker calls dec()directly. Eliminates one std::function construction perspawn.
  • commit5b1be759295c — removed the Executor abstract baseclass. ThreadPoolExecutor was always the onlyimplementation; add() and getThreadCount() arenow direct calls instead of virtual dispatches.
  • commit8daaa26efdda — enables nested parallel TaskGroup viawork-stealing. Historically, nested groups ran serially to avoiddeadlock (the thread that was supposed to run a nested task might beblocked in the outer group's sync()). Worker threads nowactively execute tasks from the queue while waiting, instead of justblocking. Root-level groups on the main thread keep the efficientblocking Latch::sync(), so the common non-nested case paysnothing. In lld this lets SyntheticSection::writeTo callswith internal parallelism (GdbIndexSection,MergeNoTailSection) parallelize automatically when calledfrom inside OutputSection::writeTo, instead of degeneratingto serial execution on a worker thread — which was the exact situationD131247 had worked aroundby threading a root TaskGroup all the way down.

Small wins worth mentioning

  • 036b755daedbparallelizes demoteAndCopyLocalSymbols. Each file collectslocal Symbol* pointers in a per-file vector viaparallelFor, which are merged into the symbol tableserially. Linking clang-14 (--no-gc-sections) with its 208K.symtab entries is 1.04x as fast.

Where lld still loses time

To locate the gap I ran lld --time-trace,mold --perf, and wild --time on the Chromium--gdb-index link (--threads=8). Grouped intocomparable phases:

Work scope lld-0201 lld-load mold wild
mmap + parse sections + merge strings + symbol resolve 376 ms 292 ms 230 ms 113 ms
--gc-sections mark 268 ms 79 ms 30 ms — *
Scan relocations 106 ms 97 ms 60 ms — *
Assign / finalize / symtab 76 ms 100 ms 27 ms 84 ms
Write sections 87 ms 87 ms 90 ms 110 ms
Wall (hyperfine) 1255 ms 918 ms 553 ms 367 ms

* wild fuses --gc-sections marking and relocation-drivenlive-section propagation into one Find required sectionspass (60 ms), so these two rows are effectively merged.

A subtlety on wild's parse number: wild'sLoad inputs into symbol DB phase by itself is only 23 ms,but it does only mmap + .symtab scan +global-name hash bucketing. Section-header parsing, mergeable-stringsplitting, COMDAT handling, and symbol resolution are deferred to laterwild phases. The 113 ms row above sums those(Load inputs into symbol DB 23 +Resolve symbols 12 + Section resolution 21 +Merge strings 57) so it covers the same work lld callsParse input files.

Meaningful gaps, in order of absolute impact:

Parse: lld-load 292 ms vs wild 113 ms ≈ 2.6x. Thebiggest remaining cross-linker gap on this workload, and the samepattern holds on the larger workloads below. The phase is alreadyparallel; the gap is constant factor in the per-object parse path(reading section headers, interning strings, splitting CIEs/FDEs,merging globals into the symbol table). On clang-relassert the 179 msparse gap alone accounts for ~33% of the 551 ms wall-clock gap betweenlld-load and wild.

Assign / finalize / symtab: 100 ms vs mold 27 ms ≈3.7x. finalizeAddressDependentContent,assignAddresses, finalizeSynthetic,Add symbols to symtabs, and Finalize .eh_frametogether cost ~100 ms on this workload; mold's equivalents(compute_section_sizes, compute_symtab_size,create_output_sections, set_osec_offsets)total 27 ms. This gap grows linearly with the number of.symtab entries — on clang-debug it's 127 ms lld vs 27 msmold, on Chromium 570 ms vs ~80 ms. I have a local branch that turnsSymbolTableBaseSection::finalizeContents into aprefix-sum-driven parallel fill and replaces thestable_partition + MapVector shuffle withper-file lateLocals buffers. 1640 ELF tests pass; notposted yet.

markLive: 79 ms, 3.4x faster than the Feb 1baseline (268 ms). This is apples-to-oranges comparison: lldsupports __start_/__stop_ edges,SHF_LINK_ORDER dependencies, linker scriptsKEEP, and others features. lld correctly handles--gc-sections --as-needed with Symbol::used(tests gc-sections-shared.s, weak-shared-gc.s,as-needed-not-in-regular.s):

  • mold over-approximates DT_NEEDED on twoaxes: it emits DT_NEEDED for DSOs referenced onlyvia weak relocs, and for DSOs referenced only from GC'd sections. Italso retains undefined symbols that are only reachable from deadsections in .dynsym.
  • wild handles weak refs correctly but not dead-sectionrefs: weak-only references do not force DT_NEEDED(matching lld), but DSOs referenced only from GC'd sections still getDT_NEEDED entries. wild does drop the correspondingundefined symbols from .dynsym, so itsDT_NEEDED decision and its symtab-inclusion decisiondiverge slightly.
  • lld is strictest on all three axes

Scan relocations: 97 ms vs 60 ms. Clean 1.6x ratio,small absolute. Target-specific scanning (theAdd target-specific relocation scanning for …) removed somedispatch overhead; what remains isInputSectionBase::relocations overhead. wild foldsrelocation-driven liveness into Find required sections,which is why there's no separate wild row.

Interestingly, writing section content is not a gap(87–110 ms across all four). The earlier assumption that.debug_* section writes were a lld weakness didn't survivemeasurement.

One cost that only shows up on debug-info-heavy workloads is--gdb-index construction, which lld does in ~1.3 s vsmold's ~0.9 s on Chromium. The work is embarrassingly parallel perinput, but lld funnels string interning through a shardedDenseMap; mold uses a lock-free ConcurrentMapsized by HyperLogLog. wild does not yet implement--gdb-index.

wild is worth calling out separately: its user time is comparable tolld's but its system time is roughly half, and its parse phase is 4-8xfaster than either of the C++ linkers across all three workloads. moldis at the other extreme — the highest user time on every workload,bought back by aggressive parallelism.

Claude Code 从 AWS Bedrock 切换到 Team 订阅指南

作者 唐巧
2026年4月12日 22:44

背景

Claude Code 支持多种认证方式,包括 AWS Bedrock、Google Vertex AI、Anthropic API Key 和 Claude 订阅(Pro/Max/Team/Enterprise)。当你从 Bedrock 切换到 Team 订阅时,需要清除 Bedrock 的配置,否则 Claude Code 会一直走 Bedrock 通道。

核心问题

使用 Bedrock 认证时,/login/logout 命令是被禁用的(官方设计如此)。因此你无法在 Bedrock 模式下直接切换登录方式。

Bedrock 配置的来源有两种:

  1. 环境变量 — 通过 export 或写在 ~/.zshrc / ~/.bashrc
  2. settings.json — 写在 ~/.claude/settings.jsonenv 字段中

很多用户(尤其是通过 setup wizard 配置的)的 Bedrock 设置是写在 settings.json 里的,单纯 unset 环境变量并不能解决问题。

切换步骤

第一步:检查 Bedrock 配置来源

1
2
3
4
5
# 检查环境变量
env | grep -i -E "claude_code_use|anthropic|bedrock|aws"

# 检查 settings.json
cat ~/.claude/settings.json

如果在 settings.json 中看到类似以下内容,说明 Bedrock 配置在这里:

1
2
3
4
5
6
7
8
{
"env": {
"CLAUDE_CODE_USE_BEDROCK": "1",
"AWS_REGION": "us-west-2",
"ANTHROPIC_MODEL": "arn:aws:bedrock:...",
"CLAUDE_CODE_AWS_PROFILE": "default"
}
}

第二步:清除 Bedrock 配置

如果配置在 settings.json 中,编辑 ~/.claude/settings.json,删除 env 中所有 Bedrock 相关的键值对:

  • CLAUDE_CODE_USE_BEDROCK
  • AWS_REGION
  • ANTHROPIC_MODEL
  • CLAUDE_CODE_AWS_PROFILE
  • CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS(Bedrock 专用)
  • CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC(Bedrock 专用)

保留你仍需要的配置(如代理、权限设置等)。清理后的文件示例:

1
2
3
4
5
6
7
8
9
10
11
12
{
"env": {
"HTTP_PROXY": "http://your-proxy:8118",
"HTTPS_PROXY": "http://your-proxy:8118"
},
"permissions": {
"allow": [
"Bash(*)"
],
"defaultMode": "dontAsk"
}
}

如果配置在环境变量中,清除相关变量:

1
2
3
4
5
unset CLAUDE_CODE_USE_BEDROCK
unset ANTHROPIC_MODEL
unset ANTHROPIC_API_KEY
unset AWS_REGION
unset CLAUDE_CODE_AWS_PROFILE

同时检查并清理 shell 配置文件:

1
2
grep -r "CLAUDE_CODE_USE_BEDROCK\|ANTHROPIC_MODEL\|ANTHROPIC_API_KEY" \
~/.zshrc ~/.bashrc ~/.zprofile ~/.bash_profile 2>/dev/null

第三步:重新启动 Claude Code

1
claude

此时应该会弹出登录方式选择界面,选择 「Claude account with subscription」,然后在浏览器中授权你的 Team 计划。

第四步:确认切换成功

启动后,欢迎界面底部应显示类似:

1
Sonnet 4.6 · Claude Pro(或 Team)

而不是之前的 arn:aws:bedrock:...

也可以在交互界面中输入 /status 确认当前认证方式。

第五步:切换模型(可选)

如果需要使用 Opus 模型,在交互界面中输入:

1
/model

用方向键选择 Opus 即可。

认证优先级

Claude Code 的认证优先级从高到低为:

  1. 云提供商凭据(CLAUDE_CODE_USE_BEDROCK / CLAUDE_CODE_USE_VERTEX / CLAUDE_CODE_USE_FOUNDRY
  2. ANTHROPIC_AUTH_TOKEN 环境变量
  3. ANTHROPIC_API_KEY 环境变量
  4. apiKeyHelper 脚本
  5. 订阅 OAuth 凭据(/login

只要高优先级的认证方式存在,低优先级的就不会生效。所以必须彻底清除 Bedrock 配置,订阅认证才能生效。

注意事项

  • 代理地址:Bedrock 用的代理可能无法访问 api.anthropic.com,切换后可能需要更换代理或去掉代理配置。
  • Premium 席位:Team 计划需要 Premium 席位才能使用 Claude Code,确认管理员已分配。
  • 用量共享:Team 计划的用量限额在 Claude 网页端和 Claude Code 之间是共享的。
  • Memory 延续CLAUDE.md 等本地文件不受认证方式影响,切换后照常保留。对话历史不会跨会话保存,这点两种方式一样。

iOS 26 模拟器启动卡死:Method Swizzling 在系统回调时触发 nil 崩溃

作者 inxx
2026年4月11日 19:56

一、现象

在 Xcode 26.4 + iOS 26.4 模拟器上运行项目,app 卡在 Launching 界面,始终无法进入主界面。控制台有大量 objc 类重复实现的警告(AuthKitUI / AuthKit 框架重复),但这些是系统 bug,与本次崩溃无关。

使用 LLDB 暂停进程,thread list 看到主线程异常:

thread #1: tid = 0xb124db, 0x0000000118fc9c10 CoreFoundation`-[__NSArrayM insertObject:atIndex:] + 251, queue = 'com.apple.main-thread'

二、定位过程

在 LLDB 中执行 thread select 1 + bt,得到完整调用栈:

frame #0: CoreFoundation`-[__NSArrayM insertObject:atIndex:] + 251
frame #1: FNCategory`-[NSMutableArray safe_insertObject:atIndex:] at NSMutableArray+FN.m:68
frame #2: FNCategory`-[NSMutableArray safe_addObject:] at NSMutableArray+FN.m:51
frame #3: CoreFoundation`-[NSEnumerator allObjects] + 189
frame #4: AXCoreUtilities`-[AXBinaryMonitor _frameworkNameForImage:]
frame #5: AXCoreUtilities`-[AXBinaryMonitor _handleLoadedImagePath:]
frame #6: AXCoreUtilities`___axmonitor_dyld_image_callback_block_invoke

关键结论:系统无障碍框架 AXCoreUtilities 在动态加载镜像(dyld image load)时,触发了一个回调,该回调内部调用了 NSEnumerator allObjects,而这个 allObjects 底层最终调用了 NSMutableArray addObject:

由于项目通过 Method Swizzling 将系统的 addObject: 替换成了自定义的 safe_addObject:,这个系统内部调用被"劫持"进了我们的代码。

safe_addObject: 内部调用了 safe_insertObject:atIndex:,这里对 NSMutableArray 插入对象时发生了崩溃。

三、根本原因

这是一个经典的 Method Swizzling 副作用问题,iOS 26 改变了 AXCoreUtilities 的内部实现,触发了长期潜伏的 bug。

完整调用链如下:

  1. AXCoreUtilities(系统无障碍框架)在 dyld 加载镜像时触发内部回调
  2. 回调内部操作了一个系统私有数组对象,调用了 insertObject:atIndex:
  3. 由于 Method Swizzling,insertObject:atIndex: 已被替换成 safe_insertObject:atIndex:,系统内部调用被"劫持"进了我们的代码
  4. safe_insertObject:atIndex: 内部再调用 [self safe_insertObject:anObject atIndex:index](即原始方法),但此时 self 是系统内部的私有数组类型,不是普通的 __NSArrayM,导致无限递归或调用到了错误的 IMP,最终崩溃

问题的本质是:Swizzling 作用在父类(NSMutableArray)上,但系统传入的是私有子类对象,Swizzling 后的方法实现与私有类的内存布局不兼容,在 iOS 26 收紧了 AXCoreUtilities 的调用时序之后,这个潜在冲突被激活。

正规的修复思路是在 SwizzlingMethod 里加类型保护,确保只 swizzle __NSArrayM 本身而不影响其私有子类。但由于 FNCategory 是 Pod,还有 AFNetworking、DoraemonKit 等我们无法直接修改源码的三方库存在同样问题,所以统一在 Podfile post_install 里做全局兼容处理。

四、踩过的坑

坑 1:以为是 objc 类重复警告导致的

启动时控制台打印了大量 Class AKAlertImageURLProvider is implemented in both AuthKitUI and AuthKit 的警告,误以为是这些重复类导致崩溃。实际上这是 iOS 26.4 模拟器运行时自身的打包问题,与启动卡死无关。

坑 2:只修复了 FNCategory,没有扩大范围

最初只在 FNCategory 的 NSMutableArray+FN.m 里加了 nil 保护,但 AFNetworking 和 DoraemonKit 也有同样模式的 Swizzling,同样存在风险。

五、修复方案

思路

不针对单个文件做字符串替换,而是在 Podfile 的 post_install 阶段,全局扫描所有 Pod 源文件,找到所有 method_exchangeImplementations( 调用,在其前面统一注入 nil 保护。

实现(Podfile post_install)

post_install do |installer|
  # ... 其他 post_install 逻辑 ...

  # 全局修复:为所有 Pod 的 method_exchangeImplementations 调用注入 nil 保护
  # 防止 iOS 26 系统框架在 dyld 镜像加载回调中触发 Swizzled 方法时崩溃
  fixed_count = 0
  Dir.glob('Pods/**/*.{m,mm}').each do |file|
    content = File.read(file)
    next unless content.include?('method_exchangeImplementations(')

    new_content = content.gsub(
      /^(\s*)(method_exchangeImplementations\((\w+)\s*,\s*(\w+)\s*\)\s*;)/
    ) do
      indent = $1
      full_call = $2
      arg1 = $3
      arg2 = $4
      "#{indent}if (#{arg1} && #{arg2}) #{full_call}"
    end

    if new_content != content
      File.chmod(0644, file)
      File.write(file, new_content)
      puts "✅ 已修复 #{file} 的 method_exchangeImplementations nil 保护"
      fixed_count += 1
    end
  end
  puts "共修复 #{fixed_count} 处 method_exchangeImplementations nil 保护" if fixed_count > 0
end

修复效果

执行 pod install 后的输出:

✅ 已修复 Pods/AFNetworking/AFNetworking/AFNetworking/AFURLSessionManager.m 的 method_exchangeImplementations nil 保护
✅ 已修复 Pods/DoraemonKit/iOS/DoraemonKit/Src/Core/Category/NSObject+Doraemon.m 的 method_exchangeImplementations nil 保护
✅ 已修复 Pods/DoraemonKit/iOS/DoraemonKit/Src/Core/Plugin/Performance/StartTime/DoraemonStartTimeViewController.m 的 method_exchangeImplementations nil 保护
✅ 已修复 Pods/FNCategory/FNCategory/Classes/NSMutableArray+FN.m 的 method_exchangeImplementations nil 保护
✅ 已修复 Pods/FNCategory/FNCategory/Classes/NSObject+FNSwizzle.m 的 method_exchangeImplementations nil 保护
✅ 已修复 Pods/FNCategory/FNCategory/Classes/UIViewController+FNFullScreen.m 的 method_exchangeImplementations nil 保护
Integrating client project
Pod installation complete! There are 32 dependencies from the Podfile and 35 total pods installed.

共修复 6 处,涉及 AFNetworking、DoraemonKit、FNCategory 三个 Pod。

六、总结

项目 说明
问题类型 Method Swizzling 缺少 nil 保护,被系统内部回调触发
触发条件 iOS 26 改变了 dyld 镜像加载回调时序,在类注册完成前触发 Swizzle
崩溃位置 NSMutableArray insertObject:atIndex:safe_insertObject:atIndex:
修复方式 Podfile post_install 全局注入 if (A && B) nil 保护
优点 一次修复,覆盖所有 Pod,无需逐个修改,pod update 后自动重新修复
注意 这是 Swizzling 的通用最佳实践,不局限于 iOS 26,建议所有项目都加上

除法的意义

作者 云风
2026年4月12日 20:52

可可已经在三年级下学期了,数学似乎还是有点问题。这个阶段考试成绩其实都不会太差,但一旦作业或考卷上的错题并非粗心大意就值得警惕。乘除法是二年级学的,三年级已经在学两位数除一位数的除法。但会计算并不难,计算只是一项机械性技能,难的是理解乘除法的意义。理解乘除比理解加减法困难的多。

我翻出几个月前的一篇 blog,发现过了 4 个月,她的问题依旧:乘除法作为一项计算技能和其背后的意义是割裂的。这导致了很多问题到底如何解决一筹莫展。固然多作练习就能开悟,毕竟几乎没有成人回头看小学数学会觉得难以理解的。但我还是想尽力搞清楚她的小脑袋里到底是哪打结了。

今晚讲一道相当简单的数学题:

有 96 个鸡蛋,8 个一盒装,可以装多少盒?

可可不知道如何解决这个问题,我一开始是很诧异的。我先反复确认她理解了题目的文本,并非语言理解的问题。真的是无法联想到应该使用除法这个工具,而 96 这个数字过大,即使不使用除法,也不知道该如何处理。我默不作声,让她仔细想想,她愣在那里不知所措,都急得掉眼泪了。

我决定一步步推演这个问题。

先问一个简单的版本:有 12 个鸡蛋,10 个一盒装,最多可以装满几盒?

我本以为她能一口答出,但可能是前面的问题受挫,她还是不知道如何下手。我想想,从桌游盒中找了一堆 token 和若干小碗,说你自己装碗试试吧。装完 12 个后,又把问题改成了 30 个,她重新摆弄了一次,这下明白了。

我说,现在要把道具收起来了,换成草稿纸,你该如何解决这个问题呢?

我教她用减法:用 30 - 10 = 20 , 20 - 10 = 10 ,10 - 10 = 0 ;数一下一共减了 3 次。可可说,我知道了,其实不用数,只要看数字是几十,那么就是几盒了。

那么,回到一开始的问题,不是 10 个一盒而是 8 个一盒就不能直接看出来了,该怎么办呢?可可说那我也会:她从 96 - 8 = 88 开始一步步的做减法计算,很耐心的减到了 0 ,数了一下是 12 ,中间居然没有算错。

我说,96 / 8 = 12 ,并不真的要花这么多时间做减法。你其实会算除法,只是不知道除法有什么用。除法就是连续计算减法的次数,就好比乘法就是连续做多次加法一样。你需要把 token 一个个放进碗里的过程抽象化成数字写到草稿纸上,打草稿就是把脑子里想的东西具象化出来。这个过程借助数学符号可以更简单。数字是符号,加减乘除也是符号,符号能帮助你思考,但先要明白这些符号代表的道理。

我再换个问题:

有 80 个鸡蛋,8 个一盒装,可以装多少盒?

可可没犹豫,马上告诉我是 8 盒。我说你别着急,拿草稿纸仔细算一下。她算完不好意思的告诉我是 10 盒。我画了张矩形图,给她讲解了一下 8 x 10 = 10 x 8 的道理:10 行 8 个与 8 行 10 个其实只是图形旋转了一下,总数是一样的。

那么,从 96 个鸡蛋里先拿出 80 个装满 10 盒后,剩下的还可以装多少盒呢?她计算了一下 96 - 80 = 16 ,16 / 8 = 2 ;然后 10 盒与 2 盒合在一起也是 12 。

再看除法的竖式草稿,其实是一样的。

今天花了一个小时讲这道数学题(她的考卷上的错题),这次似乎真的懂了。

老司机 iOS 周报 #368 | 2026-04-13

作者 ChengzhiHuang
2026年4月12日 20:16

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

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

新闻

🐕 Swift 6.3 Released

@Kyle-Ye: Swift 6.3 正式发布,带来了多项语言和工具链层面的重要更新。语言特性方面,新增 @c attribute 允许将 Swift 函数和枚举直接暴露给 C 代码并自动生成头文件,新增 :: 模块名选择器语法解决多模块同名 API 的歧义问题,同时为库作者提供了 @specialize@inline(always)@export(implementation) 等性能控制属性。构建工具方面,Swift Package Manager 预览集成了统一的 Swift Build 引擎,并新增预编译 Swift Syntax 支持和 swift package show-traits 命令。平台扩展方面,Embedded Swift 在 C 互操作和调试能力上有显著改进,同时本版本也是 Swift SDK for Android 的首个正式发布版本。此外 Swift Testing 新增了 warning 级别的 issue severity 和测试取消支持,DocC 也增加了 Markdown 输出和代码块标注等实验性功能。建议所有 Swift 开发者关注并评估升级。

新手推荐

🐎 Xcode 26.4 Simulator Paste Is Broken: Here's the Workaround

@Barney:这篇文章记录了 Xcode 26.4 的一个很影响调试体验的回归问题:Mac 到 iOS Simulator 的剪贴板同步失效,Cmd + V 没反应,长按输入框也看不到 Paste。作者尝试了重启 Simulator、切换 Automatically Sync Pasteboard、killall pboard 和重置权限等常见手段都无效,最后给出一个可立即落地的 workaround:直接用 xcrun simctl pbcopy booted 把宿主机剪贴板内容写入当前启动中的模拟器。文末还补了一个更顺手的版本 pbpaste | xcrun simctl pbcopy booted,基本可以当作临时替代方案。适合最近升级到 Xcode 26.4、正好被这个问题卡住的同学收藏。

文章

🐕 Tracking token usage in Foundation Models

@Cooper Chen:这篇文章介绍了如何在 Apple Foundation Models 框架中追踪 token 使用情况,并将其作为优化大模型应用的关键指标。作者通过示例展示了如何统计指令、prompt 和完整对话的 token 消耗,并结合上下文窗口评估占用比例,判断是否接近限制。文章还总结了多种优化方法,如精简 prompt、减少冗余内容和拆分长对话,以提升性能和降低成本。同时提供可视化工具帮助开发者直观分析 token 分布。整体而言,这篇文章强调了以 token 为核心的工程优化思路,对构建高效 LLM 应用具有实用价值。

🐕 Beta Preview: ComposableArchitecture 2.0

@AidenRao:Point-Free 在这篇 Beta Preview 里预告了 Composable Architecture 2.0(Composable Architecture 是 Point ‑ Free 团队开源的一套 Swift 应用架构 / 框架,用来“以一致且可理解的方式”组织业务逻辑,并把 组合(composition) 和 可测试性(testing) 当作一等公民。它既可以用于 SwiftUI,也能用于 UIKit 等场景。):这是一次从底层模型到日常写法都“重新推倒重来”的大版本更新。它把 API 词汇刻意对齐 SwiftUI(例如 onChange、preferences、生命周期回调等),让你用熟悉的视图心智模型去写业务逻辑:View 负责“渲染什么”,而新的 Feature 负责“要做什么”。

🐕 Xcode Build Optimization using 6 Agent Skills

@阿权:作者介绍了自己的一套 AI Agent Skill,可以自动分析并优化 Xcode 项目的编译速度。原理是同城修改 Xcode 项目配置来优化编译流程。处理了影响编译速度的几个因素:代码复杂度、build phases、Swift Package 依赖、增量构建等(具体分析过程可参考 Build performance analysis for speeding up Xcode builds)。这套 skill 工作流程如下:

  1. Orchestrator skill(重点关注)优化编译流水线,分析项目目录,为后续 skill 做好准备。
  2. Benchmark skill 执行 3 次干净构建和增量构建,构建信息存到本地 JSON,供后续继续分析。
  3. 分析阶段,Compilation Analyzer、Project Analyzer、SPM Analyzer 3 个 skill 做具体的分析,检查 Build Settings、Project Configuration、源码和依赖。
  4. 展示优化改进计划。
  5. 人工 review 并应用优化选项。
  6. Build Fixer skill 应用优化计划。
  7. 再次 benchmark 并展示最终优化结果。

文章提供了 AI Agent 提升 iOS 研效的另一种思路,希望对你有所启发。

🐎 Why Your @Observable Class init() Runs Multiple Times in SwiftUI

@DylanYang:本文作者主要讲解了 SwiftUI 中被 @observable 修饰的类初始化方法多次执行的问题,核心原因是使用 @State 存储 ViewModel 时,会随 View 频繁重建重复执行初始化逻辑,搭配 NavigationStack 导航场景会进一步加剧该问题。作者同时给出了.task 延迟赋值、将 ViewModel 托管至上层视图等解决方案,并提醒开发者不要在 init 中编写耗时操作与副作用逻辑。

内推

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

写给设计师:如何设计一份 AI 友好的设计规范

作者 唐巧
2026年4月11日 23:07

你有没有这种体验:让 AI 帮你写个页面,它生成的代码颜色全是瞎编的、间距全靠猜、按钮样式跟你们产品完全不搭?

然后你甩给它一份设计规范的 PDF,指望它能“学会”你们的设计体系。

结果呢?AI 看 PDF 基本等于盲人摸象——它看到的是一堆碎片化的文字和完全无法理解的截图。那些精心排版的视觉示例,在 AI 眼里跟噪音差不多。

问题不是 AI 不行,而是我们给 AI 的“学习资料”不对。

传统设计规范的问题

传统设计规范长这样:一份精美的 PDF,里面有品牌色卡、组件截图、do/don’t 的对比图、各种排版示例。

这东西给人看,完美。给 AI 看,灾难。

原因很简单:

第一,PDF 是视觉媒介,AI 是文本动物。 PDF 里那些色卡截图,AI 根本“看”不出来里面的色值是什么。它需要的是 #1A73E8 这个字符串,不是一个蓝色方块的图片。

第二,设计规范的“规则”通常是散文式的。 比如“不要在一个页面里放太多主按钮”——这句话人类一看就懂,但 AI 很难把它转化成一个可执行的判断。太多是多少?什么算主按钮?什么算一个页面?

第三,知识是碎片化的。 颜色写在第 3 页,间距写在第 7 页,按钮的规范在第 12 页,而按钮用到的颜色和间距需要 AI 自己去关联。这种跨页面的信息拼装,AI 做起来很吃力。

核心思路:把设计决策变成结构化数据

一句话总结:视觉示例给人看,结构化数据给 AI 读。

具体来说,就是把传统设计规范里的每一个设计决策,都翻译成 AI 能精确解析的格式。

那用什么格式呢?我让 Claude Opus 帮我调研了一下,它推荐的方案是:Markdown + JSON + YAML 的组合。其中:

  • Markdown 负责描述性的内容:设计原则、使用场景、什么时候该用什么不该用
  • JSON 负责精确的数值定义:颜色值、字号、间距、阴影
  • YAML 负责组件级的结构化规范:组件的变体、状态、约束规则

为什么不统一用一种格式?因为各有所长。JSON 适合定义纯数据(Design Token),YAML 适合描述有层次的组件规范(因为可读性更好),Markdown 适合写需要段落和叙事的内容(设计原则、模式指引)。

具体分五步来做

1. Design Token 化:把所有“魔法数字”抽出来

传统规范里,设计师说“主色调是品牌蓝”,然后在 PDF 里放一个色块。

AI 友好的方式是把它变成一个 Token:

1
2
3
4
5
6
7
8
9
10
11
12
{
"color": {
"brand": {
"primary": {
"value": "#1A73E8",
"usage": "主操作按钮、重要链接、选中态",
"contrast_on_white": "4.6:1",
"wcag_aa": true
}
}
}
}

注意这里不只有色值,还有 usage(什么场景用)和 wcag_aa(是否满足无障碍标准)。这些上下文信息对 AI 来说极其重要——它不只要知道“是什么颜色”,还要知道“什么时候用”和“为什么选这个颜色”。

同理,字号、间距、圆角、阴影、动画时长……所有数值类的设计决策,都应该 Token 化。

2. 组件规范用结构化 Schema 描述

传统规范里,一个按钮组件的描述可能是一页截图加几段说明文字。

AI 友好的方式是用 YAML 写一个完整的结构化定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
component: Button

variants:
- name: primary
description: "主操作按钮,页面中最重要的行动号召"
styles:
background: "{color.brand.primary}"
text_color: "#FFFFFF"
border_radius: "{border_radius.md}"

sizes:
- name: md
height: "40px"
padding: "0 {spacing.md}"
font_size: "{typography.scale.body-md.size}"

states: [default, hover, active, focus, disabled, loading]

这里面有几个关键设计:

用花括号引用 Token,比如 {color.brand.primary}。这样 AI 在生成代码时,会自动去 Token 文件里查对应的值,而不是硬编码一个色值。整个系统是关联的。

明确列出所有状态。人类设计师可能觉得“hover 状态不用说大家都知道”,但 AI 需要你把它列出来。缺什么它就不做什么。

有变体(variants)和尺寸(sizes)的穷举。 AI 最擅长在有限集合里做选择,而不是在模糊描述里做推断。

3. 把 do/don’t 改写成可执行的规则

这是最关键的一步。

传统规范里的“Don’t”通常配一张错误示例截图,AI 完全看不懂。

AI 友好的方式是把它写成带 ID、有严重等级、能机器检查的规则:

1
2
3
4
5
6
7
8
9
10
11
12
rules:
- id: btn-001
rule: "同一视图中最多一个 primary 按钮"
severity: error
rationale: "多个 primary 按钮导致用户无法识别主操作"

- id: btn-003
rule: "按钮文案不超过 6 个中文字符"
severity: warning
examples:
correct: ["提交订单", "确认删除", "开始学习"]
incorrect: ["好的", "订单信息", "下一步操作确认提交"]

这种格式有几个好处:

  • 有唯一 ID:AI 审查代码时可以引用“违反了规则 btn-001”
  • 有严重等级:error 是必须修的,warning 是建议修的,AI 可以区分优先级
  • 有原因:rationale 告诉 AI“为什么”,当遇到边缘情况需要取舍时,AI 能做更合理的判断
  • 有正反例:而且是文字形式的,不是截图

4. 提供“AI 入口文件”

你的设计规范可能有几十个文件,AI 不知道该先看哪个。你需要一个 README.md 作为入口,就像给 AI 画一张地图:

1
2
3
4
5
6
7
8
9
10
11
12
## AI 使用指引

### 生成 UI 代码时
1. 先读取 tokens/ 中的变量,禁止硬编码颜色/字号/间距值
2. 查找对应 components/*.yaml 获取组件结构和约束规则
3. 查阅 patterns/*.md 确认页面级布局要求
4. 检查 accessibility.md 确保符合无障碍标准

### 审查 UI 代码时
1. 逐条检查组件 YAML 中的 rules 字段
2. 验证 Token 引用是否正确
3. 检查 severity: error 的规则是否被违反

这个入口文件告诉 AI 三件事:有哪些文件、每个文件是干嘛的、不同任务应该按什么顺序查阅哪些文件。

5. 设计原则要可操作化

传统规范里的设计原则通常很抽象:“我们追求简洁”。

AI 友好的方式是让原则可操作——不只说“是什么”,还说“怎么用”和“冲突时怎么办”:

1
2
3
4
### 清晰优先于美观
- **含义**: 用户能否在 3 秒内理解界面意图,比视觉精致更重要
- **实践**: 信息层次分明,操作路径可预期,文案直白无歧义
- **权衡**: 当装饰性元素影响信息传达时,移除装饰性元素

特别是要提供一个 原则冲突解决矩阵。比如“清晰”和“包容性”冲突时谁优先?“性能”和“一致性”冲突时呢?人类设计师靠直觉判断,AI 需要明确的规则。

推荐的文件结构

说了这么多,最终的目录结构长这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
design-system/
├── README.md ← AI 入口,索引整个规范
├── principles.md ← 设计原则 + 冲突解决矩阵
├── accessibility.md ← 无障碍要求 + AI 审查清单
├── tokens/
│ ├── colors.json ← 品牌色、功能色、中性色
│ ├── typography.json ← 字体、字号阶梯、行高
│ ├── spacing.json ← 间距、栅格、断点
│ ├── elevation.json ← 阴影、层级
│ └── motion.json ← 动画时长、缓动函数
├── components/
│ ├── button.yaml ← 按钮规范
│ ├── input.yaml ← 输入框规范
│ ├── modal.yaml ← 弹窗规范
│ └── card.yaml ← 卡片规范
└── patterns/
├── form-layout.md ← 表单布局模式
├── error-handling.md ← 错误处理策略
├── responsive.md ← 响应式断点规则
└── dark-mode.md ← 深色模式适配

每层的分工很清晰:

  • tokens/ 是最底层的原子变量,纯数据,JSON 格式
  • components/ 是组件级规范,结构化描述,YAML 格式
  • patterns/ 是页面级模式,需要叙事和流程说明,Markdown 格式

一些实操建议

不要一步到位。 你不需要一次把整个设计规范都改造完。可以先从 Design Token 开始——把颜色和字号从 PDF 里抽出来做成 JSON 文件,这一步投入产出比最高。

保持两个版本同源。 理想情况下,JSON/YAML 是“源文件”,PDF 版本从源文件自动生成。这样改一处,两边都更新。如果做不到自动生成,至少保证人工同步。

给每个决策加上“为什么”。 这是很多人最容易忽略的。AI 在遇到边缘情况时,rationale 字段就是它做判断的依据。没有 rationale,它只会机械执行规则;有了 rationale,它能理解意图,做出更灵活的判断。

把规范放到代码仓库里。 设计规范不应该是一个飞书文档或者 Figma 链接,而是一个 Git 仓库里的文件夹。这样 AI 工具可以直接读取,开发者可以在 CI/CD 里做自动检查,版本变更有迹可循。

实际测试。 改造完之后,拿你的 AI 工具(Claude、Cursor、Copilot 等)实际跑一遍:让它基于你的设计规范生成一个页面,看看它是不是真的引用了 Token、遵守了规则。不好使就迭代。

最后

AI 时代的设计规范,本质上是一个 API——它不再只是给人“阅读”的文档,而是给机器“调用”的接口。

格式变了,但设计的本质没变。你仍然需要好的设计判断来决定什么颜色、什么间距、什么交互模式。只是表达方式要变一变:从“让人看懂”升级为“人机双读”。

如果你的设计师不知道如何输出上面的文件,没关系,把这篇文章发给你的 AI Agent(推荐使用 Claude Opus 4.6),然后说:我需要按照文章中的方案来产生一套面向 AI 的设计规范,你来帮我完成,现在你告诉我需要哪些文件和资料,我来负责提供。

放心,AI 会一步一步带着你完成这份规范。

希望对你有用。

❌
❌