阅读视图

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

Claude Code 四大核心技能使用指南

本文详细介绍 Superpowers、Feature-Dev、UI/UX Pro Max 和 Ralph Wiggum 四个强大技能的使用方法,帮助开发者充分发挥 Claude Code 的潜力。


目录

  1. 技能系统概述
  2. Superpowers:完整开发工作流
  3. Feature-Dev:功能开发指南
  4. UI/UX Pro Max:设计智能系统
  5. Ralph Wiggum:迭代循环开发
  6. 技能选择指南
  7. 总结

技能系统概述

Claude Code 的技能系统是一套可组合的专业工作流,它们在特定场景下自动激活,帮助开发者以系统化的方式完成复杂任务。

核心原则

如果你认为某个技能有哪怕 1% 的可能适用于当前任务,
你就必须调用它。这不是建议,而是强制要求。

技能优先级

当多个技能可能适用时,按以下顺序使用:

  1. 流程技能优先(brainstorming、debugging)- 决定如何处理任务
  2. 实现技能其次(frontend-design、mcp-builder)- 指导具体执行

Superpowers:完整开发工作流

什么是 Superpowers?

Superpowers 是一套完整的软件开发工作流,基于可组合的"技能"构建。它从你启动编程代理的那一刻开始工作,不是直接跳入编码,而是先退一步理解你真正想要做什么。

安装方式

Claude Code 用户:

# 注册 marketplace
/plugin marketplace add obra/superpowers-marketplace

# 安装插件
/plugin install superpowers@superpowers-marketplace

# 验证安装
/help

核心工作流

Superpowers 包含七个阶段的完整开发流程:

1. Brainstorming(头脑风暴)

在编写代码之前激活,通过问答细化粗略想法,探索替代方案,分段展示设计供验证。

关键原则:

  • 一次只问一个问题
  • 优先使用选择题
  • 无情地应用 YAGNI(你不会需要它)
  • 总是提出 2-3 种方案后再定案

流程:

理解项目状态 → 逐个提问细化想法 → 提出2-3种方案
→ 分段展示设计(每段200-300字)→ 验证后保存设计文档

2. Git Worktrees(Git工作树)

设计批准后激活,在新分支上创建隔离的工作空间,运行项目设置,验证干净的测试基线。

3. Writing Plans(编写计划)

将工作分解为小任务(每个2-5分钟),每个任务都有:

  • 精确的文件路径
  • 完整的代码
  • 验证步骤

4. Subagent-Driven Development(子代理驱动开发)

这是 Superpowers 的核心机制:

每个任务分派新的子代理 → 两阶段审查:
  1. 规格合规审查(代码是否符合规格)
  2. 代码质量审查(代码是否写得好)

优势:

  • 每个任务有新鲜的上下文(无污染)
  • 自动审查检查点
  • 子代理可以在工作前后提问

示例流程:

读取计划 → 提取所有任务 → 创建TodoWrite

任务1[分派实现子代理] → 子代理实现、测试、提交
  [分派规格审查子代理] → 确认代码符合规格
  [分派代码质量审查子代理] → 批准代码质量
  [标记任务完成]

任务2: ...

所有任务完成后 → 最终代码审查 → 完成开发分支

5. Test-Driven Development(测试驱动开发)

Superpowers 强制执行 RED-GREEN-REFACTOR 循环:

写失败测试 → 观察失败 → 写最小代码 → 观察通过 → 提交

铁律:

没有先失败的测试,就没有生产代码

在测试之前写了代码?删除它。从头开始。

常见借口及真相:

借口 真相
"太简单不需要测试" 简单代码也会出错,测试只需30秒
"我之后会测试" 立即通过的测试什么也证明不了
"删除X小时的工作太浪费" 沉没成本谬误,保留未验证的代码才是技术债务
"TDD太教条" TDD才是务实的,"务实"的捷径=生产环境调试=更慢

6. Code Review(代码审查)

在任务之间激活,根据计划审查代码,按严重程度报告问题。关键问题会阻止进度。

7. Finishing Branch(完成分支)

任务完成时激活,验证测试,提供选项(合并/PR/保留/丢弃),清理工作树。

核心哲学

  • 测试驱动开发 - 始终先写测试
  • 系统化优于临时 - 流程优于猜测
  • 降低复杂性 - 简单是首要目标
  • 证据优于声明 - 在宣布成功前验证

Feature-Dev:功能开发指南

什么是 Feature-Dev?

Feature-Dev 是一个系统化的功能开发技能,帮助开发者深入理解代码库、识别并询问所有不明确的细节、设计优雅的架构,然后实现。

七个阶段

Phase 1: Discovery(发现)

目标: 理解需要构建什么

操作:

  1. 创建包含所有阶段的 todo 列表

  2. 如果功能不清晰,询问用户:

    • 要解决什么问题?
    • 功能应该做什么?
    • 有什么约束或要求?
  3. 总结理解并与用户确认

Phase 2: Codebase Exploration(代码库探索)

目标: 在高层和底层理解相关现有代码和模式

操作:

  1. 并行启动 2-3 个 code-explorer 代理,每个代理:

    • 全面追踪代码,专注于理解抽象、架构和控制流
    • 针对代码库的不同方面
    • 包含 5-10 个关键文件列表

示例代理提示:

  • "找到与 [功能] 类似的功能并全面追踪其实现"
  • "映射 [功能区域] 的架构和抽象"
  • "分析 [现有功能/区域] 的当前实现"
  1. 代理返回后,阅读所有识别的文件以建立深入理解
  2. 呈现发现和模式的综合摘要

Phase 3: Clarifying Questions(澄清问题)

目标: 在设计前填补空白并解决所有歧义

这是最重要的阶段之一,不能跳过。

操作:

  1. 审查代码库发现和原始功能请求
  2. 识别未明确的方面:边缘情况、错误处理、集成点、范围边界、设计偏好、向后兼容性、性能需求
  3. 以清晰、有组织的列表向用户呈现所有问题
  4. 等待答案后再进行架构设计

Phase 4: Architecture Design(架构设计)

目标: 设计具有不同权衡的多种实现方案

操作:

  1. 并行启动 2-3 个 code-architect 代理,关注不同方面:

    • 最小变更:最小变化,最大复用
    • 清洁架构:可维护性,优雅抽象
    • 务实平衡:速度 + 质量
  2. 审查所有方案,形成哪个最适合此特定任务的意见

  3. 向用户呈现:

    • 每种方案的简要摘要
    • 权衡比较
    • 你的推荐及理由
    • 具体实现差异
  4. 询问用户偏好哪种方案

Phase 5: Implementation(实现)

目标: 构建功能

未经用户批准不要开始

操作:

  1. 等待用户明确批准
  2. 阅读前几个阶段识别的所有相关文件
  3. 按选定架构实现
  4. 严格遵循代码库约定
  5. 编写干净、文档完善的代码
  6. 随着进展更新 todos

Phase 6: Quality Review(质量审查)

目标: 确保代码简单、DRY、优雅、易读且功能正确

操作:

  1. 并行启动 3 个 code-reviewer 代理,关注不同方面:

    • 简单性/DRY/优雅
    • 缺陷/功能正确性
    • 项目约定/抽象
  2. 整合发现,识别你建议修复的最高严重性问题

  3. 向用户呈现发现并询问他们想怎么做(现在修复、稍后修复或按原样进行)

  4. 根据用户决定处理问题

Phase 7: Summary(总结)

目标: 记录完成的工作

操作:

  1. 将所有 todos 标记为完成

  2. 总结:

    • 构建了什么
    • 做出的关键决策
    • 修改的文件
    • 建议的下一步

UI/UX Pro Max:设计智能系统

什么是 UI/UX Pro Max?

UI/UX Pro Max 是一个可搜索的 UI 设计数据库,包含 50+ 种 UI 风格、21 种调色板、50 种字体配对、20 种图表类型、8 种技术栈的最佳实践。

前提条件

确保已安装 Python:

python3 --version || python --version

使用方法

当用户请求 UI/UX 工作(设计、构建、创建、实现、审查、修复、改进)时,按以下工作流程:

步骤 1:分析用户需求

从用户请求中提取关键信息:

  • 产品类型:SaaS、电商、作品集、仪表盘、着陆页等
  • 风格关键词:极简、活泼、专业、优雅、深色模式等
  • 行业:医疗、金融科技、游戏、教育等
  • 技术栈:React、Vue、Next.js,或默认使用 html-tailwind

步骤 2:搜索相关领域

使用 search.py 多次搜索以收集全面信息:

python3 .claude/skills/ui-ux-pro-max/scripts/search.py "<关键词>" --domain <领域> [-n <最大结果数>]

推荐搜索顺序:

顺序 领域 用途
1 product 获取产品类型的风格推荐
2 style 获取详细风格指南(颜色、效果、框架)
3 typography 获取字体配对和 Google Fonts 导入
4 color 获取配色方案(主色、辅色、CTA、背景、文字、边框)
5 landing 获取页面结构(如果是着陆页)
6 chart 获取图表推荐(如果是仪表盘/分析)
7 ux 获取最佳实践和反模式
8 stack 获取技术栈特定指南

步骤 3:可用技术栈

关注点
html-tailwind Tailwind 工具类、响应式、无障碍(默认)
react 状态、hooks、性能、模式
nextjs SSR、路由、图片、API 路由
vue Composition API、Pinia、Vue Router
svelte Runes、stores、SvelteKit
swiftui 视图、状态、导航、动画
react-native 组件、导航、列表
flutter Widgets、状态、布局、主题

示例工作流

用户请求: "做一个专业护肤服务的着陆页"

# 1. 搜索产品类型
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "beauty spa wellness service" --domain product

# 2. 搜索风格
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "elegant minimal soft" --domain style

# 3. 搜索字体
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "elegant luxury" --domain typography

# 4. 搜索配色
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "beauty spa wellness" --domain color

# 5. 搜索着陆页结构
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "hero-centric social-proof" --domain landing

# 6. 搜索 UX 指南
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "animation" --domain ux
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "accessibility" --domain ux

# 7. 搜索技术栈指南
python3 .claude/skills/ui-ux-pro-max/scripts/search.py "layout responsive" --stack html-tailwind

专业 UI 通用规则

这些是经常被忽视但会让 UI 看起来不专业的问题:

图标和视觉元素

规则 正确做法 错误做法
不使用 emoji 图标 使用 SVG 图标(Heroicons、Lucide) 使用 emoji 作为 UI 图标
稳定的悬停状态 使用颜色/透明度过渡 使用会移动布局的缩放变换
一致的图标尺寸 使用固定 viewBox(24x24)配合 w-6 h-6 随意混合不同图标尺寸

交互和光标

规则 正确做法 错误做法
光标指针 给所有可点击元素添加 cursor-pointer 交互元素保持默认光标
悬停反馈 提供视觉反馈(颜色、阴影、边框) 元素交互时无任何指示
平滑过渡 使用 transition-colors duration-200 瞬间状态变化或太慢(>500ms)

亮/暗模式对比度

规则 正确做法 错误做法
亮模式玻璃卡片 使用 bg-white/80 或更高透明度 使用 bg-white/10(太透明)
亮模式文字对比 使用 #0F172A(slate-900)作为文字 使用 #94A3B8(slate-400)作为正文
边框可见性 亮模式使用 border-gray-200 使用 border-white/10(不可见)

交付前检查清单

视觉质量

  • 没有使用 emoji 作为图标
  • 所有图标来自一致的图标集
  • 品牌 logo 正确
  • 悬停状态不会导致布局偏移

交互

  • 所有可点击元素有 cursor-pointer
  • 悬停状态提供清晰的视觉反馈
  • 过渡平滑(150-300ms)
  • 键盘导航的焦点状态可见

亮/暗模式

  • 亮模式文字有足够对比度(最少 4.5:1)
  • 玻璃/透明元素在亮模式下可见
  • 边框在两种模式下都可见

布局

  • 浮动元素与边缘有适当间距
  • 内容不会隐藏在固定导航栏后面
  • 响应式适配 320px、768px、1024px、1440px
  • 移动端无水平滚动

Ralph Wiggum:迭代循环开发

什么是 Ralph Wiggum?

Ralph Wiggum 是一种基于持续 AI 代理循环的开发方法。正如 Geoffrey Huntley 所描述的: "Ralph 是一个 Bash 循环" - 一个简单的 while true,重复给 AI 代理喂入提示文件,让它迭代改进工作直到完成。

这个技术以《辛普森一家》中的 Ralph Wiggum 命名,体现了不顾挫折持续迭代的哲学。

核心概念

这个插件使用 Stop hook 实现 Ralph,拦截 Claude 的退出尝试:

# 你只运行一次:
/ralph-loop "你的任务描述" --completion-promise "DONE"

# 然后 Claude Code 自动:
# 1. 处理任务
# 2. 尝试退出
# 3. Stop hook 阻止退出
# 4. Stop hook 喂入相同的提示
# 5. 重复直到完成

循环发生在你当前会话内 - 你不需要外部 bash 循环。

这创建了一个自引用反馈循环

  • 迭代之间提示不变
  • Claude 之前的工作保留在文件中
  • 每次迭代看到修改的文件和 git 历史
  • Claude 通过读取文件中自己过去的工作自主改进

快速开始

/ralph-loop "构建一个 todos 的 REST API。要求:CRUD 操作、输入验证、测试。完成后输出 <promise>COMPLETE</promise>。" --max-iterations 50 --completion-promise "COMPLETE"

Claude 将:

  • 迭代实现 API
  • 运行测试并看到失败
  • 根据测试输出修复 bug
  • 迭代直到满足所有要求
  • 完成后输出完成承诺

命令

/ralph-loop

在当前会话中启动 Ralph 循环。

/ralph-loop "<提示>" --max-iterations <n> --completion-promise "<文本>"

选项:

  • --max-iterations <n> - N 次迭代后停止(默认:无限)
  • --completion-promise <text> - 表示完成的短语

/cancel-ralph

取消活动的 Ralph 循环。

/cancel-ralph

提示编写最佳实践

1. 清晰的完成标准

错误示例:

构建一个 todo API 并让它好用。

正确示例:

构建一个 todos 的 REST API。

完成条件:
- 所有 CRUD 端点工作正常
- 输入验证就位
- 测试通过(覆盖率 > 80%)
- 带 API 文档的 README
- 输出:<promise>COMPLETE</promise>

2. 增量目标

错误示例:

创建一个完整的电商平台。

正确示例:

阶段 1:用户认证(JWT、测试)
阶段 2:产品目录(列表/搜索、测试)
阶段 3:购物车(添加/删除、测试)

所有阶段完成后输出 <promise>COMPLETE</promise>。

3. 自我纠正

错误示例:

为功能 X 写代码。

正确示例:

使用 TDD 实现功能 X:
1. 写失败测试
2. 实现功能
3. 运行测试
4. 如果失败,调试并修复
5. 需要时重构
6. 重复直到全部通过
7. 输出:<promise>COMPLETE</promise>

4. 安全阀

始终使用 --max-iterations 作为安全网,防止在不可能的任务上无限循环:

# 推荐:始终设置合理的迭代限制
/ralph-loop "尝试实现功能 X" --max-iterations 20

哲学

Ralph 体现几个关键原则:

原则 说明
迭代 > 完美 不要追求第一次就完美。让循环细化工作。
失败是数据 "确定性的失败"意味着失败是可预测的和有信息量的。
操作员技能很重要 成功取决于写好提示,不只是有好模型。
坚持获胜 持续尝试直到成功。循环自动处理重试逻辑。

适用场景

适合:

  • 有明确成功标准的定义良好的任务
  • 需要迭代和细化的任务(如让测试通过)
  • 你可以走开的绿地项目
  • 有自动验证的任务(测试、linter)

不适合:

  • 需要人类判断或设计决策的任务
  • 一次性操作
  • 成功标准不清楚的任务
  • 生产环境调试(使用针对性调试代替)

真实世界成果

  • 在 Y Combinator 黑客马拉松测试中一夜成功生成 6 个仓库
  • 一份 50k合同以50k 合同以 297 API 成本完成
  • 用这种方法在 3 个月内创建了整个编程语言("cursed")

技能选择指南

场景对照表

场景 推荐技能
构建新功能 Feature-Dev → Superpowers
UI/UX 设计实现 UI/UX Pro Max
长时间自主任务 Ralph Wiggum
完整项目开发 Superpowers(完整工作流)
修复 bug Superpowers(systematic-debugging)
代码审查 Superpowers(code-reviewer)

组合使用

这些技能可以组合使用:

  1. Feature-Dev + UI/UX Pro Max:开发带 UI 的新功能
  2. Ralph Wiggum + Superpowers:自主完成带 TDD 的长任务
  3. Superpowers + Feature-Dev:完整的企业级功能开发

总结

技能 核心价值 最佳场景
Superpowers 完整的 TDD 工作流 项目开发全流程
Feature-Dev 系统化功能开发 新功能实现
UI/UX Pro Max 设计智能数据库 UI 设计和前端开发
Ralph Wiggum 自主迭代循环 长时间自动化任务

黄金法则

如果你认为某个技能有哪怕 1% 的可能适用,就必须使用它。
这不是可选的,这是强制的。你不能为绕过它找理由。

iOS实现 WKWebView 长截图的优雅方案

在 iOS 开发中,为 WKWebView 实现长截图功能是一个常见且棘手的需求。开发者通常会遇到以下几个痛点:

  • 网页内容高度不确定
  • 滚动区域难以完整截取
  • 截图过程中的界面闪烁影响用户体验

本文将介绍一种高效、稳定的解决方案,通过分段渲染图像拼接,完美捕获整个网页内容,并提供可直接集成的完整代码。


🎯 核心思路

我们的方案主要分为三个清晰的步骤:

  1. 布局调整:将 WebView 移至临时容器,为完整渲染做准备。
  2. 分段渲染:按屏幕高度分段捕获内容,生成多张切片图像。
  3. 图像拼接:将所有切片图像无缝拼接成一张完整的长图。

这种方法巧妙地绕过了直接截取 UIScrollView 的局限性,同时通过遮罩视图,保证了用户界面的视觉稳定性,避免闪烁。


💻 完整实现代码

WKWebView分类中添加长截图方法

  • WKWebView+Capture.h
#import <WebKit/WebKit.h>
#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface WKWebView (Capture)

/**
 * 捕获 WKWebView 的完整内容并生成长截图
 * @param completion 完成回调,返回拼接好的长图(失败则返回 nil)
 */
- (void)captureEntireWebViewWithCompletion:(void (^)(UIImage * _Nullable capturedImage))completion;

@end

NS_ASSUME_NONNULL_END


  • WKWebView+Capture.m
#import "WKWebView+Capture.h"

@implementation WKWebView (Capture)

/**
 * 捕获 WKWebView 的完整内容并生成长截图
 * @param completion 完成回调,返回拼接好的长图(失败则返回 nil)
 */
- (void)captureEntireWebViewWithCompletion:(void (^)(UIImage *capturedImage))completion {

    // ⚠️ 关键:确保在主线程执行
    if (![NSThread isMainThread]) {
        NSLog(@"错误:WebView 截图必须在主线程执行");
        if (completion) completion(nil);
        return;
    }

    // 步骤1: 检查父视图并保存原始状态
    UIView *parentView = self.superview;
    if (!parentView) {
        if (completion) completion(nil);
        return;
    }

    CGRect originalFrame = self.frame;
    CGPoint originalContentOffset = self.scrollView.contentOffset;

    // 步骤2: 创建遮罩视图,保持界面"静止"的视觉效果,可以额外添加loading
    UIView *snapshotCoverView = [self snapshotViewAfterScreenUpdates:NO];
    snapshotCoverView.frame = self.frame; // 确保遮罩视图位置与 WebView 完全一致
    [parentView insertSubview:snapshotCoverView aboveSubview:self];

    // 步骤3: 创建隐藏的临时窗口和容器
    UIWindow *temporaryWindow = [[UIWindow alloc] initWithFrame:self.bounds];
    temporaryWindow.windowLevel = UIWindowLevelNormal - 1000; // 置于底层
    temporaryWindow.hidden = NO;
    temporaryWindow.alpha = 0;
    temporaryWindow.userInteractionEnabled = NO;

    UIView *captureContainerView = [[UIView alloc] initWithFrame:self.bounds];
    captureContainerView.clipsToBounds = YES;

    // 将 WebView 移入临时容器
    [self removeFromSuperview];
    [captureContainerView addSubview:self];
    [temporaryWindow addSubview:captureContainerView];

    // 步骤4: 获取完整内容高度并调整布局
    CGFloat fullContentHeight = self.scrollView.contentSize.height;
    self.frame = CGRectMake(0, 0, originalFrame.size.width, fullContentHeight);
    self.scrollView.contentOffset = CGPointZero;

    __weak typeof(self) weakSelf = self;

    // ⭐ 延迟执行,确保 WebView 内容布局与渲染完成
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)),
                   dispatch_get_main_queue(), ^{
        __strong typeof(weakSelf) strongSelf = weakSelf;
        if (!strongSelf) {
            if (completion) completion(nil);
            return;
        }

        // 步骤5: 分段截图核心逻辑
        CGFloat pageHeight = captureContainerView.bounds.size.height; // 单屏高度
        CGFloat totalHeight = fullContentHeight; // 总内容高度

        NSMutableArray<UIImage *> *imageSlices = [NSMutableArray array];
        CGFloat offsetY = 0;

        while (offsetY < totalHeight) {
            CGFloat remainingHeight = totalHeight - offsetY;
            CGFloat sliceHeight = MIN(pageHeight, remainingHeight);

            // 处理最后一段高度不足一屏的情况
            if (remainingHeight < pageHeight) {
                CGRect containerFrame = captureContainerView.frame;
                containerFrame.size.height = remainingHeight;
                captureContainerView.frame = containerFrame;
            }

            // 移动 WebView,将当前要截取的区域"暴露"出来
            CGRect webViewFrame = strongSelf.frame;
            webViewFrame.origin.y = -offsetY;
            strongSelf.frame = webViewFrame;

            // 渲染当前分段到图像上下文
            UIGraphicsBeginImageContextWithOptions(
                CGSizeMake(originalFrame.size.width, sliceHeight),
                NO,
                [UIScreen mainScreen].scale
            );

            CGContextRef context = UIGraphicsGetCurrentContext();
            CGFloat scaleX = originalFrame.size.width / captureContainerView.bounds.size.width;
            CGFloat scaleY = sliceHeight / captureContainerView.bounds.size.height;
            CGContextScaleCTM(context, scaleX, scaleY);

            [captureContainerView.layer renderInContext:context];
            UIImage *sliceImage = UIGraphicsGetImageFromCurrentImageContext();
            UIGraphicsEndImageContext();

            if (sliceImage) {
                [imageSlices addObject:sliceImage];
            }

            offsetY += sliceHeight; // 移动到下一段
        }

        UIImage *finalImage = nil;

        // 步骤6: 图像拼接
        if (imageSlices.count == 1) {
            finalImage = imageSlices.firstObject;
        } else if (imageSlices.count > 1) {
            UIGraphicsBeginImageContextWithOptions(
                CGSizeMake(originalFrame.size.width, totalHeight),
                NO,
                [UIScreen mainScreen].scale
            );

            CGFloat drawOffsetY = 0;
            for (UIImage *slice in imageSlices) {
                [slice drawInRect:CGRectMake(0,
                                             drawOffsetY,
                                             slice.size.width,
                                             slice.size.height)];
                drawOffsetY += slice.size.height;
            }

            finalImage = UIGraphicsGetImageFromCurrentImageContext();
            UIGraphicsEndImageContext();
        }

        // 步骤7: 恢复原始状态
        [strongSelf removeFromSuperview];
        [captureContainerView removeFromSuperview];
        temporaryWindow.hidden = YES;

        strongSelf.frame = originalFrame;
        strongSelf.scrollView.contentOffset = originalContentOffset;
        [parentView insertSubview:strongSelf belowSubview:snapshotCoverView];
        [snapshotCoverView removeFromSuperview];

        // 步骤8: 在主线程回调最终结果
        if (completion) {
            completion(finalImage);
        }
    });
}

@end


📱 效果展示

长截图展示

🚀 使用方法

调用方式非常简单,只需一行代码。

// 在需要截图的地方调用
[webView captureEntireWebViewWithCompletion:^(UIImage *capturedImage) {
    if (capturedImage) {
        // ✅ 截图成功,处理结果
        // 例如:保存到相册
        UIImageWriteToSavedPhotosAlbum(capturedImage, nil, nil, nil);
        // 或:上传、分享、预览等
    } else {
        // ❌ 截图失败
        NSLog(@"截图失败");
    }
}];

📝 总结

本文提供的方案通过以下关键技术,优雅地解决了 WKWebView 长截图的难题:

  • 临时容器管理:隔离渲染环境,避免干扰主界面。
  • 分段渲染:将长内容分解为多个可管理的屏幕片段。
  • 状态恢复:完整保存并恢复 WebView 的原始状态,确保业务无感知。

如果你有更好的实现思路,或在实际应用中遇到了特殊场景,欢迎在评论区分享交流!

「共」型人才:AI 时代的个体进化论

当 AI 能够完美代劳记忆型事务、高效处理逻辑琐事时,一个焦虑也随之而来:作为个体,我们的核心竞争力究竟还剩什么?

传统的「T」型或「π」型人才理论,关注的是技能树的形状(深度与广度),在 AI 时代,这两个模型的达成路径和价值权重发生了根本性变化。于是我构想出了一个「共」型人才理论,这可能更符合 AI 时代对个体的要求。

什么是「共」型人才?

将「共」字拆解:

  • 下半部分(基石): 决定了一个人的底盘。只要基石稳固,即便行业被 AI 颠覆,也能迅速在新土壤中扎根。
  • 上半部分(建筑): 决定了一个人的高度。这是利用 AI 杠杆构建的双专业壁垒,以及独属于人类的整合创造力。

第一部分:基石(下半部)—— 内核的修炼

基石分为左右两点的「生命力」、「元能力」,以及承载它们的「职场通用力」。

一、左边的点:生命力(韧性)

这是个体的反脆弱系统。在快速变化的 AI 时代,比拼的往往不是谁跑得快,而是谁在逆境中不崩盘,并能从混乱中获益。

1. 情绪调节能力

即对他人的情绪有觉察,对自己的情绪有掌控。面对批评或压力,能迅速通过深呼吸、肌肉放松等技巧避免被情绪劫持。也能够穿透情绪的迷雾,看到对方发火背后的真实需求,将冲突转化为增进信任的契机。

2. 认知重构能力

决定我们情绪和行为的,往往不是发生的事情本身,而是我们对这件事情的看法(认知)。认知重构就是给大脑换个滤镜。这不是「阿Q精神」式的自欺欺人,而是用更具适应性的视角替代单一的消极视角。

比如朋友圈经常看到某某在外面玩,就很羡慕甚至有点嫉妒,这是下意识的反应,但不是完整的视角。更完善的思考可能是:

  • 经常在外面玩其实也挺累的,可能光顾着拍照了,没有很好的感受当地的风景和文化。
  • 辛苦劳作后的休憩最为舒适,经常玩,新鲜感和愉悦感就没那么强了。
  • 人家无论是通过家里的支持或自己的努力赢得了经常出去玩的机会,这也是应得的。

3. 课题分离能力

这是切断精神内耗的利刃,他的核心是:分清楚什么是你的事,什么是别人的事。专注解决自己的事,不过度干预别人的事,并接受「我无法控制别人,别人也无法控制我」这一事实。我能控制的是我的态度和行为,我不能控制的是别人的评价和结果。就像你可以把马带到河边(你的课题),但不能强按着马头喝水(马的课题)。

4. 求助能力

求助不是示弱,而是懂得利用外部资源扩展生存边界。通过向合适的人寻求支持,不仅解决了问题,更建立了一次潜在的高质量的社会连接,这是构建韧性网络的重要一环。

二、右边的点:元能力

元能力对应的是学习能力。用来构建知识网络,增强调用和处理知识的能力,以下是我觉得最为重要的 4 种元能力。

1. 认知性好奇心

这个我认为是最重要的,它不是单纯的想知道 What 的感知性/消遣性好奇心,而是对运行机制、底层原理的好奇,关注的是 How 和 Why, 追求的是填补认知空白和解决智力上的难题。

认知性好奇心产生于「我知道一点,但又不知道全部」的时候, 这个差距会带来一种类似「认知瘙痒」的不适感, 学习的过程,就是「止痒」的过程,所以最好的学习区,是在「已知」和「未知」的边缘。

2. 专注力

如果把学习比作「吃饭消化」,那么专注力就是「牙齿」和「食道」。它决定了你能把多少食物(信息)吃进嘴里,以及嚼得有多碎,但前提得先张开嘴巴,因为未被关注的信息,大脑不会存储。

如果注意力的强度不够,效果也不会好,就像在沙滩上写字,潮水一来就没了。只有在高强度的专注下,神经元才会高频放电,突触之间的连接才会变强,所以,专注力是一个很重要的能力。

3. 思维模型

思维模型就像是安装在大脑里的「应用程序」或「工具箱」。拥有一套多元化的模型组合(查理·芒格所谓的「格栅理论」),能在面对复杂问题时更有洞察力。以下是我认为最重要的一些思维模型。

第 0 类:元思维模型
  • 系统思维: 帮助理解「事物之间如何连接」的宏观框架,而不是割裂地看问题,主张看整体、看动态。核心元素: 反馈回路、存量与流量、瓶颈理论、滞后效应。
  • 结构化思维: 能够将复杂的信息、问题进行逻辑拆解、分类和整理的能力。 非结构化思维就像走进一个堆满杂物的仓库,书本、衣服、工具混在一起,你想找把锤子,可能要翻半天。 结构化思维就像走进一个管理完善的图书馆或药房。每一个区域都有分类,每一层架子都有标签,你能迅速定位并解决问题。
  • 抽象思维: 透过现象看本质的能力, 将我们感知到的具体事物,剥离掉细节,提取出其共同规律、本质特征或概念的思维过程。
第 1 类:提升决策质量(如何思考)
  • 第一性原理:打破一切既定的假设和类比,将问题拆解成最基本的事实(公理),然后从这些基本事实出发重新构建解决方案。
  • 逆向思维: 许多难题正向思考很难,反过来想就容易了。不仅要问“我如何获得成功?”,更要问“我如何才能避免失败?”。
  • 二阶思维: 吃甜食的直接后果是快乐(一阶),但二阶后果是血糖升高、长期可能导致肥胖。为了长期的健康,可能会需要放弃短期的一阶快乐。
第 2 类:提升效率与效能(如何行动)
  • 帕累托法则 (80/20 Rule): 在任何一组东西中,最重要的只占一小部分。约 80% 的产出源自 20% 的投入。
  • 复利效应: 只要坚持做正确的事,时间的加成会带来指数级的增长。这不仅适用于理财,更适用于知识积累、习惯养成和人际关系。
第 3 类:理解世界与自我(如何自处)
  • 地图不是疆域:地图只是对疆域的一种描绘,它永远无法包含疆域的所有细节。如果你看着地图以为这就是真实世界,你就会在现实中迷路。
  • 概率思维: 世界不是黑白分明的,而是由概率构成的灰色地带。不要追求 100% 的确定性,而要追求大概率正确的决策。
  • 汉隆剃刀: 能解释为愚蠢的,就不要解释为恶意。 同事没有回你邮件,不要觉得他是故意针对你(恶意),很可能只是他太忙漏看了或者系统出错了(疏忽/愚蠢)。这能帮你减少 90% 不必要的愤怒和冲突。

4. 认知偏误管理

认知偏误是大脑为了节省能量而采取的「思维捷径」。虽然它们在进化上曾帮助人类快速反应,但在现代复杂的决策环境中,它们往往会导致我们犯错。

第一维度:关于「自我认知」
  • 达克效应: 这是关于「无知者无畏」的经典解释。能力不足的人往往无法认识到自己的不足,因此会产生过度的自信;而真正的高手反而容易低估自己。
  • 确认偏误: 我们倾向于寻找、解释和记忆那些能够证实我们已有观点的信息,而自动过滤或忽略那些反驳我们的信息。
第二维度:关于「决策陷阱」
  • 沉没成本谬误: 我们会因为已经投入了时间、金钱或情感,而坚持继续做一件不理智的事情,即使它已经没有未来的价值。
  • 锚定效应: 我们在做判断时,极易受到获取的「第一条信息」(锚点)的影响,即使这个信息可能毫无关联。
第三维度:关于「看待世界」
  • 基本归因谬误: 就是我们在解释别人的行为时,倾向于高估其「内在性格」的影响,而低估「外部环境」的作用。 我们会想: “他做这件事,是因为他就是这种人。”。我们忽略:“他做这件事,可能是因为当时的情况迫使他这么做。”。
  • 幸存者偏差: 我们只看到了经过筛选后「活下来」的样本,而忽略了那些「死掉」的样本,从而得出错误的结论。

三、下面的一横:职场通用力

这是无论技术如何变迁,人与人协作都必须具备的接口协议。

1. 沟通能力

沟通能力是一个涵盖了输入、处理、输出、反馈四个维度的闭环系统,是一个高度复杂的复合能力。

  • 输入: 积极倾听,听懂话外音;敏锐观察,捕捉非语言信号。
  • 处理:同理心换位思考,用逻辑整理杂乱信息。
  • 输出: 精准表达,甚至用故事力包装枯燥逻辑。
  • 互动: 懂得即时反馈与冲突管理,将对抗转化为对话。

2. Sell 的能力

如果沟通能力是底层的基础设施(地基),那么 Sell 能力是在这个地基上盖起的、带有明确目的性的建筑。一个人可以沟通很好,但不会 Sell;但一个擅长 Sell 的人,一定是沟通的高手。

  • 认知引导: 沟通是基础,Sell 是目的。Sell 的本质是改变对方认知,促成决策。
  • 缔结结果: 不止于聊得开心,更在于能把对话推向一个确定的结论(Call to Action)。一个拥有 Sell 能力的人,具备更强的心理能量和目标导向。

3. 闭环思维

它不仅指把事情做完,更指把「事情做完」这个结果反馈给发起者,从而形成一个完整的圆环。也就是常说的: 凡事有交代,件件有着落,事事有回音。 如果没有「反馈」,这个环就是断裂的。在他人眼中,这就像把石头扔进深井里,听不到回声,不知道事情是成了、败了,还是被忘了。

4. Ownership

Ownership 精神的核心是:不给自己设限,着眼于全局目标,主动填补团队的「真空地带」。比如大家都在一条船上,船底漏了个洞。 打工心态:指着洞说“这不是我弄坏的,而且修船是维修工的事”,然后看着船沉。Ownership:哪怕不是我弄坏的,我也先想办法堵上,因为船沉了对谁都没好处。

有 Ownership 精神是好事,但需要很小心地处理好边界。

  • 是「负责结果」,不是「包揽一切」:Ownership 不代表你要亲自做每一件事,而是你要确保事情发生。如果资源不足,向老板争取资源、协调其他人来做,也是 Ownership 的体现。
  • 注意「越位」风险:当你插手别人负责的领域时,沟通方式很重要。不要直接替别人做决定,而是以「补位」和「协助」的姿态切入(例如:“我发现这里有个风险,需不需要我帮忙一起看一下?”)。
  • 自我保护:不要让 Ownership 成为别人甩锅给你的理由。在承担额外责任前,确保你的核心本职工作已经完成得很漂亮。 ⠀

第二部分:建筑(上半部)—— AI 时代的双核驱动

这部分是「共」型人才的核心差异点。在 AI 出现之前,成为「双专业人才」极难;但在 AI 时代,这变得触手可及。

一、两竖:AI 赋能的「双专业壁垒」

这两根柱子代表你在两个不同领域的专业深度。

1. 传统困境 vs. AI 破局

  • 过去(一万小时定律): 想要跨界(例如从营销跨到编程),需要耗费数年去记忆语法、熟悉框架 API、调试环境。极高的沉没成本让大多数人望而却步,只能停留在「T」型(一专)。
  • 现在(AI 杠杆): AI 极大地抹平了「硬知识」的门槛
    • 以编程为例: 你不再需要背诵复杂的 API 或纠结于标点符号的 bug。AI 是你的超级助手,你可以更专注在架构设计(Architecture)逻辑拆解Prompt 指令
    • 以设计为例: 你不需要精通每一笔的光影渲染,你更需要具备审美标准和创意构想,剩下的交给生成式 AI。

2. 新时代的「专业」定义

在 AI 的加持下,这两竖的构建不再依赖死记硬背,而是依赖:

  • 理解力与判断力: 你必须懂行,才能判断 AI 给出的结果是 60 分还是 90 分。
  • 逻辑互补性: 选择两个具备「中度相关性」的领域效果最佳。例如:心理学 + 算法内容创作 + 数据分析

AI 使得获取第二专业的成本指数级下降,为个体提供了前所未有的理论与工具支撑,让「共」型人才成为可能。

二、上面的一横:整合力

这是机器难以替代的人类高地。如果下面的一切是积木,那么这一横就是让积木变成摩天大楼的蓝图。它是 「1 + 1 > 2」 的化学反应。

1. 翻译器:降维打击沟通墙

在组织中,这种双语能力,可以让你在团队协作中成为了「节点型」人物,极大地降低了系统内的熵(混乱度)和沟通成本。

2. 迁移器:跨界降维打击

你拥有单领域专家不具备的独特视角。你可以拿着 A 领域的锤子(方法论),去解决 B 领域那颗顽固的钉子。这种跨界打击往往能产生奇效。

3. 孵化器:边缘创新的温床

当你打通了两根竖线,中间的空白地带就是创新的温床。

  • 懂代码 + 懂法律 智能合约专家 / 计算法学
  • 懂心理 + 懂产品 行为设计 / 增长黑客

结语

在「共」型人才模型中,AI 不再是我们的竞争对手,而是我们构建那「第二根竖线」的最强杠杆。

  • 下半部分(情绪、认知、沟通)让我们保持像人一样思考,拥有机器无法模拟的温度与韧性。
  • 上半部分(双专业整合)让我们像超级个体一样行动,利用 AI 快速拓展能力边界。

这不仅是职场竞争力的提升,更是一种更自由、更广阔的人生可能。

Swift 方法派发深度探究

引言:一个危险的实验

想象一下,你正在调试一个复杂的 iOS 应用,想要在不修改源码的情况下监控所有 UIViewControllerviewDidAppear 调用;还有如果要支持热修复,该如何?你可能会想到使用 Method Swizzling

extension UIViewController {
    @objc dynamic func swizzled_viewDidAppear(_ animated: Bool) {
        print("🎯 [AOP] \(type(of: self)) 显示")
        swizzled_viewDidAppear(animated) // 调用原始实现
    }
    
    static func swizzleViewDidAppear() {
        let original = #selector(viewDidAppear(_:))
        let swizzled = #selector(swizzled_viewDidAppear(_:))
        
        guard let originalMethod = class_getInstanceMethod(self, original),
              let swizzledMethod = class_getInstanceMethod(self, swizzled) else {
            return
        }
        
        method_exchangeImplementations(originalMethod, swizzledMethod)
    }
}

看起来完美,对吧?但这里隐藏着一个 Swift 的重要秘密:为什么必须使用 @objc dynamic 如果去掉 dynamic 会发生什么?

Part 1: 为什么 Swizzling 需要动态派发?

1.1 Swizzling 的工作原理

Method Swizzling 本质上是在运行时交换两个方法的实现。它依赖 Objective-C 运行时的消息派发机制:

// Objective-C 运行时的工作方式
objc_msgSend(object, selector, ...)

当调用 [object method] 时,运行时:

  1. 根据对象的类查找方法列表
  2. 找到对应 selector 的实现(IMP)
  3. 执行该实现

Swizzling 就是修改了第 2 步的映射关系。

1.2 Swift 与 Objective-C 的冲突

问题在于:Swift 默认不使用消息派发

class MyClass {
    func normalMethod() { }     // Swift 默认派发
    @objc func exposedMethod() { }  // 对 OC 可见,但仍不是消息派发
    @objc dynamic func dynamicMethod() { }  // 这才是消息派发
}

如果你尝试 Swizzle 一个非 dynamic 的方法:

class TestSwizzle: NSObject {
    @objc func original() { print("Original") }
    @objc func swizzled() { print("Swizzled") }
    
    static func attemptSwizzle() {
        let original = #selector(original)
        let swizzled = #selector(swizzled)
        
        guard let origMethod = class_getInstanceMethod(self, original),
              let swizMethod = class_getInstanceMethod(self, swizzled) else {
            return
        }
        
        print("交换前:")
        print("original IMP: \(method_getImplementation(origMethod))")
        print("swizzled IMP: \(method_getImplementation(swizMethod))")
        
        method_exchangeImplementations(origMethod, swizMethod)
        
        print("交换后:")
        print("original IMP: \(method_getImplementation(origMethod))")
        print("swizzled IMP: \(method_getImplementation(swizMethod))")
        
        let test = TestSwizzle()
        test.original()  // 输出什么?
    }
}

运行结果可能让你困惑:

交换前:
original IMP: 0x0000000102f7fbc0
swizzled IMP: 0x0000000102f7fcc0

交换后:
original IMP: 0x0000000102f7fcc0
swizzled IMP: 0x0000000102f7fbc0

Original   //❓ 调用结果还是 "Original"

为什么 IMP 发生交换后,但行为没变?

Part 2: Swift 的三种派发方式

2.1 派发方式对比

假设大家已有概念,为了方便快速浏览,我把这些汇总到了一个表格:

特性 直接派发 (Direct Dispatch) 表派发 (Table Dispatch) 消息派发 (Message Dispatch)
Swift 写法 final func
struct 的方法
extension 中的方法
private/fileprivate func
class func (默认)
@objc func (仅 Swift 内)
@objc dynamic func
@objc dynamic var
调用方式 编译时确定地址,直接跳转 通过在类对象虚函数表查找 Objective-C 运行时 objc_msgSend
性能 ⚡️ 最快 (几乎无开销) 较快 (一次指针查找) 🐌 最慢 (哈希查找+缓存)
灵活性 ❌ 最低 (无法重写) ✅ 中等 (支持继承重写) ✅✅ 最高 (支持运行时修改)
内存占用 无额外开销 每个类一个虚函数表 每个类方法列表 + 缓存
重写支持 ❌ 不支持 ✅ 支持 ✅ 支持
运行时修改 ❌ 不可能 ❌ 不可能 (Swift 5+) ✅ 可能 (Method Swizzling)
KVO 支持 ❌ 不支持 ❌ 不支持 ✅ 支持
典型应用 工具方法、性能关键代码 普通业务逻辑、可继承的类 需要动态特性的代码
二进制影响 最小 中等 最大 (生成 OC 元数据)
调试难度 简单 中等 困难 (调用栈复杂)

2.2 方法派发特别注意点

extension 中的方法特别说明:

extension 中的方法默认是静态派发,不能被子类重写,编译器可以在编译时确定具体实现。 这样设计的原因有下面几点:

  • 明确性: extension表示添加新功能,override表示修改现有功能,两者分离,避免混淆。
  • 安全性:不允许重写 → 保证 extension 方法的稳定性。
  • 模块化:不用担心用户重写了自己模块extension中方法导致异常。
  • 良好实践:使用 extension 分离关注点。
class BaseClass {
    // 进入类的虚函数表
    func original() { print("Base original") }
}

extension BaseClass {
    // 不在虚函数表中!相当于直接是函数地址
    // 编译后的伪代码: 是生成一个全局函数
    // void String_extension_customMethod(String *self) {
    //     // 函数体
    // }
    func extensionMethod() { print("extension method") }
    
    // ❌ 不能在 extension 中重写原类方法
    // override func original() { }  // 编译错误
}

class SubClass: BaseClass {
    // ✅ 可以重写原类方法
    override func original() { print("SubClass original") }
    
    // ❌ 不能重写 extension 中的方法
    // override func extensionMethod() { }  // 编译错误
}

对 Objective-C 类的 extension

// Objective-C 类(如 UIView)
extension UIView {
    // 仍然是直接派发(在 Swift 中调用时)
    func swiftExtensionMethod() { }
    
    // 但通过 @objc 暴露给 Objective-C 时
    @objc func objcExposedMethod() { }  
    // Swift 内:直接派发
    // Objective-C 内:通过桥接,底层是消息派发
}

extension NSObject {
    @objc dynamic func specialMethod() { }
    // 这会强制使用消息派发
    // 可以被重写(因为是消息派发)
    // 但这是特殊情况,利用了 Objective-C 运行时
}

协议扩展extension

protocol Drawable {
    func draw()  // 协议要求
}

extension Drawable {
    func draw() {  // 默认实现
        print("默认绘制")
    }
    // 这是直接派发,但可以通过协议类型动态派发
}

class Circle: Drawable { }

let circle = Circle()
circle.draw()  // 直接派发:调用默认实现

let drawable: Drawable = circle
drawable.draw()  // 协议派发:通过协议见证表PWT(Protocol Witness Table)
// 内存布局
Circle 实例:
┌──────────┐
 数据字段  
├──────────┤
 PWT 指针   指向 Circle 的协议见证表
└──────────┘

CirclePWT:
┌──────────┐
 draw()     索引 0
├──────────┤
 resize()   索引 1
└──────────┘

2.3 Swift 类的虚函数表

对象实例内存布局:
┌───────────────────┐
│    对象头 (16字节)  │ ← 包含指向类对象的指针
├───────────────────┤
│  引用计数 (8字节)   │
├───────────────────┤
│  属性 name (8字节) │
├───────────────────┤
│  属性 age (8字节)  │
└───────────────────┘

类对象内存布局:
┌───────────────────┐
│   类信息 (元数据)   │
├───────────────────┤
│  虚函数表指针      │ → 指向虚函数表数组
├───────────────────┤
│  其他元数据...     │
└───────────────────┘

虚函数表结构:
┌───────────────────┐
│    makeSound()    │ ← 函数指针 [0]
├───────────────────┤
│       eat()       │ ← 函数指针 [1]
├───────────────────┤
│      sleep()      │ ← 函数指针 [2]
└───────────────────┘

虚函数表(V-Table)工作原理:

Dog 类的虚函数表(编译时根据顺序确定索引):
[0]: Dog.makeSound() 地址
[1]: Dog.otherMethod() 地址
...

调用 animal.makeSound():
1. 获取 animal 的虚函数表指针
2. 根据索引得到 makeSound 在表中的地址(编译时确定)
3. 跳转到对应地址执行

2.4 Swift 类方法的派发

类元数据结构:

┌─────────────────────┐
│    类型描述符        │ ← Metadata header
├─────────────────────┤
│    父类指针          │
├─────────────────────┤
│    实例变量偏移      │
├─────────────────────┤
│  ↓ 实例方法表指针    │ → 指向实例方法的虚函数表
├─────────────────────┤
│  ↓ 类方法表指针      │ → 指向类方法的独立表
├─────────────────────┤
│    协议列表指针      │
├─────────────────────┤
│    泛型信息...       │
└─────────────────────┘

实例方法表(虚函数表):
┌─────────────────────┐
│    instanceMethod1   │ ← 索引 0
├─────────────────────┤
│    instanceMethod2   │ ← 索引 1
└─────────────────────┘

类方法表:
┌─────────────────────┐
│     classMethod1     │ ← 索引 0  
├─────────────────────┤
│     classMethod2     │ ← 索引 1
└─────────────────────┘

派发方式

class MyClass {
    
    // 默认的表派发类方法
    class func classMethod() {  // 通过类的元数据进行派发
        print("类方法 - 表派发")
    }
    
    // 直接派发,不能被重写
    static func staticMethod() {  
        print("静态方法 - 直接派发")
    }
    
    // final class func 等价于 static func
    final class func alsoCannotOverride() { }
    
    @objc dynamic class func dynamicClassMethod() {
        print("类方法 - 消息派发")
    }
}

class AppConfig {
    // 存储在全局数据段
    static let appName = "MyApp"        // __TEXT 段(只读)
    static var launchCount = 0          // __DATA 段(读写)
    static let shared = AppConfig()     // 引用存储在全局,对象在堆上
    
    // 惰性静态属性
    static lazy var heavyResource = createHeavyResource()
}

/*
内存位置:
- appName: 编译时常量 → 代码段
- launchCount: 全局变量 → 数据段  
- shared: 引用在数据段,对象在堆上
- heavyResource: 第一次访问时初始化
*/

Part 3: 混合派发的危险实验

3.1 当 Swift 遇到 Swizzling

让我们看一个更完整的例子:

class MixedClass {
    // 情况1:纯 Swift
    func swiftMethod() { print("Swift Method") }
    
    // 情况2:暴露给 OC,但 Swift 内使用表派发
    @objc func exposedMethod() { print("Exposed Method") }
    
    // 情况3:完全动态
    @objc dynamic func dynamicMethod() { print("Dynamic Method") }
}

// 尝试 Swizzle
extension MixedClass {
    @objc dynamic func swizzled_swiftMethod() {
        print("Swizzled Swift")
        swizzled_swiftMethod()
    }
    
    @objc func swizzled_exposedMethod() {
        print("Swizzled Exposed")
        swizzled_exposedMethod()
    }
    
    @objc dynamic func swizzled_dynamicMethod() {
        print("Swizzled Dynamic")
        swizzled_dynamicMethod()
    }
    
    static func testAll() {
        let instance = MixedClass()
        
        print("=== 原始调用 ===")
        instance.swiftMethod()      // Swift Method
        instance.exposedMethod()    // Exposed Method
        instance.dynamicMethod()    // Dynamic Method
        
        // 尝试 Swizzle swiftMethod(缺少 dynamic)
        if let orig = class_getInstanceMethod(self, #selector(swiftMethod)),
           let swiz = class_getInstanceMethod(self, #selector(swizzled_swiftMethod)) {
            method_exchangeImplementations(orig, swiz)
        }
        
        // 尝试 Swizzle exposedMethod(只有 @objc)
        if let orig = class_getInstanceMethod(self, #selector(exposedMethod)),
           let swiz = class_getInstanceMethod(self, #selector(swizzled_exposedMethod)) {
            method_exchangeImplementations(orig, swiz)
        }
        
        // Swizzle dynamicMethod(正确方式)
        if let orig = class_getInstanceMethod(self, #selector(dynamicMethod)),
           let swiz = class_getInstanceMethod(self, #selector(swizzled_dynamicMethod)) {
            method_exchangeImplementations(orig, swiz)
        }
        
        print("\n=== Swizzle 后调用 ===")
        instance.swiftMethod()      // 还是 Swift Method ❌
        instance.exposedMethod()    // 还是 Exposed Method ❌  
        instance.dynamicMethod()    // Swizzled Dynamic ✅
        
        print("\n=== 通过 OC 运行时调用 ===")
        // 通过 performSelector 调用
        instance.perform(#selector(swiftMethod))      // 可能崩溃 💥
        instance.perform(#selector(exposedMethod))    // Swizzled Exposed ✅
        instance.perform(#selector(dynamicMethod))    // Swizzled Dynamic ✅
    }
}

3.2 为什么会这样?

内存布局解释:

当 Swift 编译一个类时:

  • 纯 Swift 方法 → 放入虚函数表
  • @objc 方法 → 生成桥接方法,同时放入虚函数表和 OC 方法列表(在Swift中调用未交换,OC中调用时已交换)
  • @objc dynamic 方法 → 直接放入 OC 方法列表

Swizzling 只影响 OC 方法列表,不影响虚函数表!

Part 4: 属性的 @objc dynamic

4.1 Swift中使用KVO

class Observable: NSObject {
    // 普通属性,不支持 KVO
    var name: String = ""
    
    // @objc dynamic 属性,支持 KVO
    @objc dynamic var age: Int = 0
    
    // 只有 @objc,不支持 KVO
    @objc var height: Double = 0.0
}

let obj = Observable()

// 尝试观察
// 运行时错误**Fatal error: Could not extract a String from KeyPath Swift.ReferenceWritableKeyPath<XXX.Observable, Swift.String>**
obj.observe(\.name, options: .new) { _, change in
    print("name changed: \(change.newValue ?? "nil")")
}  

obj.observe(\.age, options: .new) { _, change in
    print("age changed: \(change.newValue ?? 0)")
}  // ✅ 正常工作

obj.observe(\.height, options: .new) { _, change in
    print("height changed: \(change.newValue ?? 0)")
}  // 无法观察到变化

4.2 属性访问的派发方式

class PropertyTest {
    // 直接派发(编译时展开),会被内联
    var directProperty: Int {
        get { return _storage }
        set { _storage = newValue }
    }
    
    // 表派发(通过方法)
    var tableProperty: Int {
        get {
            print("getter 调用")
            return _storage
        }
        set {
            print("setter 调用")
            _storage = newValue
        }
    }
    
    // 消息派发(支持 KVO)
    @objc dynamic var messageProperty: Int {
        get { return _storage }
        set { _storage = newValue }
    }
    
    private var _storage: Int = 0
}

// @objc dynamic 属性会生成:
// - (NSInteger)messageProperty;
// - (void)setMessageProperty:(NSInteger)value;
// 这些是真正的 Objective-C 方法

4.3 属性观察器的有趣现象

class Observed: NSObject {
    @objc dynamic var value: Int = 0 {
        didSet {
            print("value 从 \(oldValue) 变为 \(value)")
        }
    }
    
    // 测试 KVO 和 didSet 的交互
    func test() {
        self.value = 10  // 触发 didSet
        
        // 通过 KVC 设置
        self.setValue(20, forKey: "value")  // 也会触发 didSet ✅
    }
}

// 为什么能工作?
// @objc dynamic 属性生成的 setter 会:
// 1. 调用 willChangeValueForKey
// 2. 设置新值
// 3. 调用 didSet(Swift 注入的代码)
// 4. 调用 didChangeValueForKey(触发 KVO)

Part 5: 派发方式的确定规则

5.1 决策树

// Swift 编译器决定派发方式的逻辑:
func determineDispatch(for method: Method) -> DispatchType {
    if method.isFinal || type.isFinal || type.isStruct {
        return .direct      // 1. final 或 struct → 直接派发
    }
    
    if method.isDynamic {
        return .message     // 2. dynamic → 消息派发
    }
    
    if method.isObjC {
        // @objc 但不 dynamic:桥接方法
        return .table       // 3. 在 Swift 内使用表派发
    }
    
    if method.isInExtension && !type.isObjCClass {
        return .direct      // 4. 非 OC 类的扩展 → 直接派发
    }
    
    return .table           // 5. 默认 → 表派发
}

5.2 特殊情况

// 1. 协议要求
protocol MyProtocol {
    func requiredMethod()   // 表派发(通过协议见证表)
}

// 2. 泛型约束
func genericFunc<T: MyProtocol>(_ obj: T) {
    obj.requiredMethod()    // 静态派发(编译时特化)
}

// 3. @_dynamicReplacement
class Replaceable {
    dynamic func original() { print("Original") }
}

extension Replaceable {
    @_dynamicReplacement(for: original)
    func replacement() { print("Replacement") }
}
// Swift 5 引入的官方 "Swizzling"

Part 6: 性能影响与优化

优化建议

// ❌ 避免在性能关键路径使用 dynamic
class Cache {
    @objc dynamic var data: [String: Any] = [:]  // 每次访问都有消息派发开销
    
    func expensiveOperation() {
        for _ in 0..<10000 {
            _ = data["key"]  // 慢!
        }
    }
}

// ✅ 优化方案
class OptimizedCache {
    private var _data: [String: Any] = [:]
    
    var data: [String: Any] {
        get { return _data }
        set { _data = newValue }
    }
    
    @objc dynamic var observableData: [String: Any] {
        get { return _data }
        set { _data = newValue }
    }
    
    func expensiveOperation() {
        let localData = data  // 一次读取
        for _ in 0..<10000 {
            _ = localData["key"]  // 快!
        }
    }
}

Part 7: 实际应用指南

7.1 何时使用何种派发?

// 指南:
class MyClass {
    // ✅ 使用直接派发:
    final func utilityMethod() { }  // 工具方法,不重写
    private func helper() { }       // 私有方法
    
    // ✅ 使用表派发(默认):
    func businessLogic() { }        // 业务逻辑,可能被重写
    open func publicAPI() { }       // 公开 API
    
    // ⚠️ 谨慎使用消息派发:
    @objc dynamic func kvoProperty() { }  // 需要 KVO
    @objc dynamic func swizzleMe() { }    // 需要 Method Swizzling
    
    // ❌ 避免混用:
    @objc func confusingMethod() { }  // 既不是鱼也不是熊掌
    // 在 Swift 中是表派发,在 OC 中是消息派发
    // 可能导致不一致的行为
}

7.2 安全 Swizzling 的最佳实践

class SafeSwizzler {
    /// 安全的 Method Swizzling
    static func swizzle(_ type: AnyClass,
                       original: Selector,
                       swizzled: Selector,
                       isClassMethod: Bool = false) throws {
        
        // 1. 获取方法
        let getMethod = isClassMethod ? class_getClassMethod : class_getInstanceMethod
        guard let originalMethod = getMethod(type, original),
              let swizzledMethod = getMethod(type, swizzled) else {
            throw SwizzleError.methodNotFound
        }
        
        // 2. 检查是否已经是消息派发
        let originalEncoding = method_getTypeEncoding(originalMethod)
        if originalEncoding == nil {
            throw SwizzleError.notMessageDispatch
        }
        
        // 3. 检查是否已经 Swizzled
        if alreadySwizzled {
            throw SwizzleError.alreadySwizzled
        }
        
        // 4. 执行交换
        method_exchangeImplementations(originalMethod, swizzledMethod)
    }
    
    enum SwizzleError: Error {
        case methodNotFound
        case notMessageDispatch
        case alreadySwizzled
    }
}

// 使用
extension UIViewController {
    @objc dynamic func safe_viewDidLoad() {
        print("Safe tracking")
        safe_viewDidLoad()
    }
    
    static func enableSafeTracking() {
        do {
            try SafeSwizzler.swizzle(
                UIViewController.self,
                original: #selector(viewDidLoad),
                swizzled: #selector(safe_viewDidLoad)
            )
            print("✅ Safe swizzling 成功")
        } catch {
            print("❌ Swizzling 失败: \(error)")
        }
    }
}

总结

关键要点

  1. Swift 有三种派发方式

    • 直接派发:最快,用于 final、结构体等
    • 表派发:默认,通过虚函数表
    • 消息派发:最慢,但支持运行时特性
  2. @objc vs dynamic

    • @objc:让 Swift 方法对 OC 可见,但 Swift 内仍用表派发
    • dynamic:强制使用消息派发
    • @objc dynamic:OC 可见 + 消息派发
  3. Swizzling 的真相

    • 只能交换消息派发的方法
    • 交换表派发方法会导致 Swift 和 OC 行为不一致
    • 这是很多 Swizzling bug 的根源
  4. 性能影响

    • 消息派发比直接派发慢 4-5 倍
    • 避免在性能关键路径使用 dynamic
    • 合理使用 final 优化性能

哲学思考

Swift 的派发机制体现了语言设计的平衡艺术:

  • 安全 vs 灵活:表派发保证安全,消息派发提供灵活
  • 性能 vs 功能:直接派发优化性能,动态派发启用高级功能
  • Swift vs Objective-C:两种运行时模型的巧妙融合

理解这些机制,你就能:

  • 写出更高效的 Swift 代码
  • 安全地使用运行时特性
  • 避免诡异的 Swizzling bug
  • 更好地理解 Swift 的设计哲学

记住:强大的能力伴随着巨大的责任。动态派发给了你 hook 系统方法的能力,但也可能带来难以调试的问题。使用时务必谨慎!

“死了么”App荣登付费榜第一名!

背景 2026年初的App Store付费榜,突然杀出一匹“黑马”——一款名为 「“死了么 - 官方正版”」 的产品,以8元付费下载的模式,毫无征兆地登顶付费榜单榜首,成为今年首个现象级爆款。对于常年

[转载] 认知重建:Speckit用了三个月,我放弃了——走出工具很强但用不好的困境

[转载] 认知重建:Speckit用了三个月,我放弃了——走出工具很强但用不好的困境

原文地址

2025 年 AI 编程工具遍地开花,但一个尴尬的现实是:工具越来越强,预期越来越高,落地却越来越难——speckit 的规范流程在企业需求的”千层套路”、海量代码面前显得理想化,上下文窗口频繁爆满让复杂任务半途而废,每次做类似需求还是要花同样的时间因为知识全在人脑里。本文记录了我从踩坑规范驱动工具,到借鉴 Anthropic 多 Agent 协作架构、融合上下文工程与复合工程理念,最终实现边际成本递减、知识持续复利的完整历程。如果你也在”AI 工具明明很强但就是用不好”的困境中挣扎,或许能找到一些共鸣。附带还有新的工作流下人的工作模式转变思考~

起点:规范驱动开发的美好承诺

1.0 团队的 AI Coding 起点

先交代一下背景:我所在的是一个后端研发团队,日常工作以存量项目迭代为主,涉及多个微服务的协作开发。

2024 年中,团队开始尝试 AI 辅助编程。最初的体验是:

短上下文场景效果不错

  • 写一个独立函数、实现一个工具方法——AI 表现良好
  • 简单的代码补全、格式化、注释生成——确实提效

但规模化复用始终没起来

  • 当时只有三种触发类型的 rules(早期 rules 时代)
  • 虽然提出过”在基础 agent 之上封装 agent”的想法
  • 但几个月过去,仍然没有太多人真正动起来

原因分析

  • 规范没有形成共识——每个人对”怎么用好 AI”理解不同
  • 对 AI 工程化没有标准认识——不知道该往哪个方向努力
  • 提示词复用习惯没建立——好的 prompt 停留在个人经验,没有沉淀

这个困境促使我开始探索外部方案:有没有已经成熟的”AI 编程工程化”方法论?有没有可以直接借鉴的最佳实践?

带着这些问题,我遇到了 speckit 和 openspec。

遇见 speckit:AI 编程的”正确打开方式”?

2024 年开始,AI 编程助手如雨后春笋般涌现。Copilot、Cursor、Claude 让很多人第一次体验到了”AI 写代码”的魔力。但兴奋之后,问题也随之而来:

  • AI 生成的代码质量参差不齐
  • 需求理解经常偏离预期
  • 缺乏持续性,上下文丢失严重
  • 改一处坏十处,维护成本高

正当我被这些问题困扰时,遇到了 speckit——一个规范驱动开发(Spec-Driven Development, SDD)工具包。

speckit 的理念很吸引人:

1
2
3
规范即代码 → 规范直接生成实现,而非仅作为指导文档
权力倒置 → 代码服务于规范,而非规范服务于代码
测试优先 → 强制 TDD,不可协商地要求先写测试

它定义了一套清晰的 5 阶段流程:

1
2
Constitution → Specify → Plan → Tasks → Implement
(宪章) (规范) (计划) (任务) (实施)

每个阶段对应一个命令,依次执行:创建项目宪章和开发原则 → 定义需求和用户故事 → 创建技术实现计划 → 生成可执行的任务列表 → 执行所有任务构建功能。

再加上 9 条不可变的架构原则(库优先、CLI 接口、测试优先、简洁性、反抽象…),7 层 LLM 输出约束机制,防止过早实现、强制标记不确定性、结构化自检…

这不就是 AI 编程的”工程化正确答案”吗?

带着这样的期待,我开始在项目中尝试落地。

openspec:另一种优雅的尝试

除了 speckit,我还研究了 openspec——一个更轻量的规范驱动框架:

1
2
3
Specs as Source of Truth → specs/ 目录始终反映系统当前真实状态
Changes as Proposals → 所有修改先以提案形式存在,经确认后实施
Lock Intent → AI 编码前通过明确规范锁定意图

openspec 的 Delta 机制设计得很巧妙:不同于直接存储完整的”未来状态”,它只存储变更操作本身(ADDED/MODIFIED/REMOVED/RENAMED)。归档时通过语义名称匹配来定位需求,避免了 Git Merge 常见的位置冲突问题。同时采用 Fail-Fast 机制,在写入前做完整冲突检测,保证不会产生半完成状态。

两个工具,两种风格,但都指向同一个目标:让 AI 编程更可控、更规范。

碰壁:理想流程遭遇企业现实

一个真实需求的”千层套路”

让我用一个真实的 12 月活动需求来说明问题:

协作复杂度

  • 跨 BG、跨前后端、跨 FT、跨项目、跨小组、跨服务
  • 跨部门合作接口因合规要求变来变去,迟迟给不到位
  • 雅典娜平台上接近 20 种商品类型,全得人工一个个配
  • 活动流程必须按”玩法引擎”的方法论来拆解
  • 技术方案得按习惯写在 iWiki 里

并行任务流

1
2
3
4
5
6
同时处理:
├── 找产品确认商品细节
├── 找运营确认玩法逻辑
├── 找跨团队研发对齐接口
├── 找跨项目研发对齐交互
└── 内部技术方案评审

方案设计的”考古”需求

  • 某个商品创建、资产查看以前有什么坑?
  • 现在的玩法能力有哪些?能不能直接用?
  • 导航小结页到底是啥?怎么让它弹 Banner?

**写代码前的”九九八十一难”**:

1
2
3
4
5
6
前置任务链:
├── 玩法引擎:依赖数据、激励动作要在引擎仓库里实现
├── 外部依赖:关联的代码改动在其他服务里
├── 配置中心:要去阿波罗(Apollo)配配置
├── 雅典娜:商品场景得先配好(早期没数据还得 Mock)
└── 数据库:涉及表变更,得去测试环境操作

执行中的细节坑

  • 阿波罗配置有个坑,该怎么绕过去?
  • 规则引擎的语法到底怎么写?
  • 商品发放操作是重点,具体发到哪个钱包?

speckit 流程 vs 企业现实

把 speckit 的理想流程放到这个场景里:

1
2
3
4
5
6
7
8
9
speckit 假设的流程:
Constitution → Specify → Plan → Tasks → Implement
↓ ↓ ↓ ↓ ↓
一次性定义 一次性写清 线性规划 任务分解 按序实施

企业现实:
多方博弈 → 动态调整 → 并行推进 → 持续扯皮 → 边做边改
↓ ↓ ↓ ↓ ↓
需求会变 方案会改 依赖会卡 资源会抢 意外会来

核心矛盾:speckit 假设需求是清晰的、可一次性规划的,但企业真实需求是动态的、多方博弈的、持续变化的。

openspec 的 Delta 机制也救不了

openspec 的”提案→审查→归档”流程看起来更灵活,但:

  • **假设需求可以”提案化”**:实际上外部接口因合规变来变去,5 个维度同时推进相互依赖,评审中发现问题需要立即改方案

  • 人工介入成本高:Delta 与主 Spec 冲突时报错终止,复杂冲突需要人工解决,而人的认知窗口有限。具体来说,openspec archive 会在以下情况直接报错退出:

    • MODIFIED 引用的需求在主 Spec 中不存在(可能被别人删了或改名了)

    • ADDED 的需求在主 Spec 中已存在(别的分支先合入了同名需求)

    • RENAMED 的源名称不存在,或目标名称已被占用

    • 同一个需求同时出现在 MODIFIED 和 REMOVED 中(逻辑矛盾)

这些冲突没有自动解决策略,CLI 只会打印类似 MODIFIED failed for header "### Requirement: xxx" - not found 的错误信息,然后终止。你需要:手动打开两个文件对比、理解冲突原因、决定保留哪个版本、手工修改 Delta 文件、重新执行归档。整个过程要求你同时在脑中持有”主 Spec 当前状态”和”Delta 期望变更”两套信息——这对认知负担是很大的挑战

  • 强依赖命名的脆弱性:产品叫”用户激励”,运营叫”活动奖励”,研发叫”商品发放”——同一个需求在不同阶段有不同表述

最致命的问题:无法应对”考古”需求

speckit 和 openspec 都有一个共同盲区:流程从零开始

1
2
3
4
5
6
7
8
9
speckit 流程:
Constitution 定义原则 → Specify 定义需求 → Plan 设计方案 → ...

但真实需求必须"考古":
├── 这个商品创建以前有什么坑?
├── 现有玩法能力有哪些?
├── 导航小结页的 Banner 怎么弹?
├── Apollo 配置有什么特殊处理?
└── 雅典娜 20 种商品类型的配置方式各不同

缺失能力:没有”上下文检索”机制,无法自动关联历史经验、已有能力、已知陷阱。

AI 生成 spec 时能看到的:

  • ✅ 代码仓库
  • ✅ project.md/Constitution
  • ✅ 用户意图

AI 看不到(但需要知道)的:

  • ❌ 业务边界(涉及哪些服务?)
  • ❌ 历史经验(以前怎么做的?有什么坑?)
  • ❌ 配置规范(Apollo 特殊要求?)
  • ❌ 平台知识(雅典娜 20 种商品配置注意事项)
  • ❌ 协作约束(依赖其他团队接口?合规要求?)

结果:依赖人 review 时逐步想起来告诉 AI,45 分钟 + 持续的认知负担。

AI 工程化如何破局?(预告)

面对上述问题,AI 工程化的解决思路是什么?这里先做个预告,详细方案见第五节。

企业现实问题 speckit/openspec 的困境 AI 工程化的解法
需求动态变化 假设一次性规划,变更成本高 需求以”进行中”状态管理,支持随时调整,阶段性沉淀
多线并行博弈 线性流程,Delta 冲突报错终止 Agent 自主决策路由,Skill 独立执行,不强依赖顺序
考古需求 无上下文检索,AI 只能看到代码 context/ 分层管理历史经验,按阶段自动加载
配置/平台知识 需要人 review 时口述 沉淀为 context/tech/,AI 执行时主动提醒
冲突解决成本 人工对比、手工修改、认知负担重 不依赖”合并”,而是”覆盖+沉淀”,冲突时 AI 辅助决策
边际成本恒定 每次 45 分钟,无复利 首次建立 context,后续复用,边际成本递减

核心差异

1
2
3
4
5
6
7
8
9
speckit/openspec 的思路:
规范化流程 → 约束 AI 行为 → 期望产出质量

问题:流程本身不适配企业现实,约束越多越僵化

AI 工程化的思路:
上下文完整性 → AI 决策质量 → 自动沉淀经验 → 下次更好

解法:不是约束 AI,而是给 AI 完整信息 + 让知识复利

一个具体例子——同样是”商品发放”需求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
speckit 模式(第 3 次做):
1. Constitution → 写项目原则(已有,跳过)
2. Specify → 写需求规范(45 分钟,人逐步想起遗漏告诉 AI)
3. Plan → 写技术方案(人提醒:Apollo 有坑、钱包要区分)
4. Tasks → 生成任务(人补充:雅典娜配置注意事项)
5. Implement → 执行(遇到问题再排查)
耗时:45 分钟 + 排查时间,知识留在人脑

AI 工程化模式(第 3 次做):
1. /req-dev "商品发放需求"
2. Agent 识别意图 → 自动加载 context/experience/商品发放历史问题.md
3. Agent 提醒:"历史上有钱包选择、Apollo 配置、雅典娜商品类型三个坑点"
4. 人确认:"对,继续"
5. Skill 执行 → 自动校验 → 生成代码 → 沉淀新发现
耗时:10 分钟,知识沉淀到 context/

后续章节将详细展开这套方案的设计原理和落地实践。


反思:从第一性原理重新审视

人的认知局限是刚性约束

实话实说,我的脑容量有限:

  • 记性不好:只能记住关键的大方向,具体细节过脑就忘
  • 专注窗口小:同时关注的信息有限,必须采用”专注单任务+全局索引”策略

我的日常工作模式(经过各种场景检验的最优路径):

  • 任务管理(外挂大脑):Todo List 分优先级(红色紧急/黄色进行中/绿色完成/无色未开始)
  • 备忘录:记录死记硬背的内容(打包命令、数据库 IP 密码、文档散落信息)
  • 桌面即上下文:N 个桌面窗口,每个窗口对应一个垂直领域
  • 复杂任务 SOP 化:脑内计划 + 执行机器模式 + 文档跟踪
  • 简单任务 Fire and Forget:低频低思考成本事项秒回即忘

这套土办法是经过检验的最优路径。如果硬套 speckit/openspec 的范式,反而会丢掉这些 SOP,得不偿失。

执行过程的知识价值被忽视

speckit 和 openspec 都只关注”规范”(Spec)和”结果”(Code),忽视”过程”(Process)。

但真实价值恰恰在过程中:

1
2
3
4
5
执行 → 有问题 → 验证 → 排查 → 继续执行

排查信息往往没被记录

时间一久或换人,下次重新排查

这个循环中的排查信息,才是最宝贵的知识!

边际成本恒定是致命缺陷

1
2
3
4
5
6
7
Speckit 模式:
第 1 次商品发放需求:45 分钟(人逐步想起遗漏)
第 2 次商品发放需求:45 分钟(人 AGAIN 逐步想起遗漏)
第 n 次商品发放需求:45 分钟(还是要想,还是那么久)

边际成本恒定,无复利效应。
知识在哪里?在人脑里,每次都要重新想起来。

这与我期望的”越用越快”完全相反。

转折:遇见复合工程与上下文工程

复合式工程:让每一步都成为下一步的基石

在探索过程中,我接触到了”复合式工程”(Compounding Engineering)的理念。这个概念来自 Claude Code 团队与 Every 团队的实践交流,并在 Every 团队开源的 Compound Engineering Plugin 中得到了系统化实现——这是一个包含 27 个 Agent、19 个 Command、13 个 Skill 的完整 AI 辅助开发工具包。

定义”复合式工程”

“复合式工程”的核心目标非常明确:让每一单元的工程工作使后续工作变得更容易,而非更难。

1
2
传统开发:累积技术债务 → 每个功能增加复杂性 → 代码库越来越难维护
复合工程:每个功能产出文档模式 → 创建可复用组件 → 建立减少决策疲劳的约定 → 知识在团队中复合增长

与传统工程中每增加一个功能都会增加系统复杂度和维护成本不同,”复合式工程”追求的是一种”复利”效应,让系统的能力随着时间推移指数级增长。

核心工作流循环:Plan → Work → Review → Compound

Compound Engineering Plugin 设计了一个闭环的工作流循环:

1
2
3
4
5
Plan ──────→ Work ──────→ Review ──────→ Compound
详细规划 执行工作 质量检查 知识沉淀
↑ │
└───────────────────────────────────────┘
知识复合:下次规划更精准
  • Plan:多代理并行研究仓库模式、最佳实践、框架文档,输出结构化计划
  • Work:系统性执行计划,边做边测,质量内建
  • Review:多代理并行审查(安全、性能、架构等),输出分级 Todo
  • Compound:这是复合工程的核心——将解决的问题结构化记录,形成团队知识资产

完整实现参见:Compound Engineering Plugin

为什么叫”Compound”?

1
2
3
4
5
6
第一次解决 "N+1 query in brief generation" → Research (30 min)
文档化 → docs/solutions/performance-issues/n-plus-one-briefs.md (5 min)
下次类似问题 → Quick lookup (2 min)
知识复合 → Team gets smarter

Each unit of engineering work should make subsequent units of work easier—not harder.

实现机制:知识复合的典型场景

实现复合工程的关键,在于建立系统化的知识沉淀机制。以下是几个典型场景:

场景 1:Agent 重复犯同类错误

1
2
3
触发:发现 Agent 在某类问题上反复出错
沉淀:将教训写入 AGENTS.md / CLAUDE.md / 系统提示词
效果:该类错误不再发生,无需人工提醒

场景 2:某类问题需要频繁人工检查

1
2
3
触发:Code Review 时反复指出同类问题
沉淀:创建 Lint 规则 / Pre-commit Hook / CI 检查
效果:问题在提交前自动拦截,减少人工负担

场景 3:复杂流程被多次执行

1
2
3
触发:某个多步骤操作被团队重复执行
沉淀:封装为 Skill / Command / Agent
效果:一键触发标准化流程,新人也能执行专家级操作

场景 4:解决了一个有价值的问题

1
2
3
触发:花了较长时间解决某个棘手问题
沉淀:结构化记录到 context/experience/ 目录
效果:下次遇到类似问题,Agent 自动加载相关经验

这些场景的共同特点是:在问题解决的当下立即沉淀,而不是事后补文档。

Claude 团队的复合工程应用案例

以下是 Every 团队和 Anthropic 内部使用复合工程的真实案例:

案例 1:”@claude,把这个加到 claude.md 里”

当有人在 PR 里犯错,团队会说:”@claude,把这个加到 claude.md 里,下次就不会再犯了。”或者:”@claude,给这个写个测试,确保不会回归。”通过这种方式,错误转化为系统的免疫能力。

案例 2:100% AI 生成的测试和 Lint 规则

Claude Code 内部几乎 100% 的测试都是 Claude 写的。坏的测试不会被提交,好的测试留下来。Lint 规则也是 100% Claude 写的,每次有新规则需要,直接在 PR 里说一句:”@claude,写个 lint 规则。”

案例 3:十年未写代码的经理

经理 Fiona 十年没写代码了,加入团队第一天就开始提交 PR。不是因为她重新学会了编程,而是因为 Claude Code 里积累了所有团队的实践经验——系统”记得”怎么写代码。

案例 4:内置记忆系统

把每次实现功能的过程——计划怎么制定的、哪些部分需要修改、测试时发现了什么问题、哪些地方容易遗漏——全部记录下来,编码回所有的 prompts、sub-agents、slash commands。这样下次别人做类似功能时,系统会自动提醒:”注意,上次这里有个坑。”

成果:一个自我进化的开发伙伴

这一范式带来的最终效果是惊人的。它将 AI 从一个被动执行命令的工具,转变为一个能够从经验中持续学习、并让整个开发流程效率不断”复利”增长的开发伙伴。

为什么这解决了古老的知识管理问题

传统的知识管理困境:

1
2
3
4
5
6
7
8
方式 1:写文档
问题:没人看。写完就过时。维护成本高。

方式 2:靠人传授
问题:老人离职知识断层。新人上手慢。传授效率低。

方式 3:代码注释
问题:注释会过时。只能解释"是什么",难以解释"为什么这么做"和"以前踩过什么坑"。

复合工程的答案:把知识编码进工具,让工具在正确的时刻主动提醒你

1
2
3
4
5
6
7
8
9
不是:写一份"商品发放注意事项"文档,期望大家会看
而是:在 context/experience/商品发放历史问题.md 里记录,
Agent 在执行商品发放需求时自动加载,主动提醒

不是:靠老人口头传授"Apollo 配置有个坑"
而是:把坑编码到 skill 里,执行时自动校验

不是:在代码里写注释"这里要注意 XX"
而是:让 AI 在生成代码前就已经知道要注意 XX

关键设计模式

从 Compound Engineering Plugin 中可以提炼出三个核心设计模式:

模式 核心思想 价值
并行代理 多角度分析时启动多个专业代理,合并结果后继续 提高分析覆盖度和效率
意图路由 入口统一,根据意图自动路由到具体工作流 降低用户认知负担
知识复合 问题解决 → 文档化 → 未来查找 → 团队变聪明 边际成本递减

我的实践:基于工具架构的知识复合

基于复合工程理念,我设计了一套 AI 工程工具架构来实现知识的持续沉淀与复用:

工具架构

1
2
3
4
5
6
7
用户输入 → Command(入口)→ Agent(决策层)→ Skill(执行层)

意图识别、流程路由

调用具体 Skill 执行

experience-index(经验检索)
  • Command:用户交互入口,如 /req-dev/optimize-flow
  • Agent:自主决策,智能判断意图,可调用多个 Skill
  • Skill:固化流程,执行具体操作步骤

知识复合的两条路径

1
2
3
4
5
6
7
8
9
10
路径 1:经验沉淀(/optimize-flow)
用户发现规律 → experience-depositor Agent → 识别规则类型 → 写入规则文件

context-rules.md(上下文映射)
risk-rules.md(风险识别)
service-rules.md(服务补全)
pattern-rules.md(代码模式)

路径 2:经验检索(experience-index Skill)
需求分析/方案设计/代码编写前 → 自动检索匹配规则 → 加载相关 Context、提示风险、建议服务

复利效应示例

1
2
3
4
5
6
7
第 1 次做支付需求:45 分钟(边做边踩坑)
↓ 沉淀规则:/optimize-flow "支付需求要加载 payment-service.md 并提示资金安全"

第 2 次做支付需求:15 分钟(experience-index 自动加载背景、提示风险)
↓ 沉淀更多规则:错误处理模式、服务依赖关系

第 N 次做支付需求:5 分钟(系统已积累完整的支付领域知识)

与传统文档的本质区别

1
2
传统文档:写完没人看,看了也找不到对的时机
AI 工程化:experience-index 在正确的时刻自动检索,主动推送给 Agent

这就是为什么”知识应该沉淀到工具”不是一句口号,而是有实际 ROI 的工程决策。

对长期任务工程设计的启示

Compound Engineering Plugin 为 AI 工程化提供了极好的参考蓝图:

维度 启示
任务分解 阶段化执行(Plan → Work → Review → Compound),并行化处理,状态持久化
质量保障 多角度并行审查,分级处理(P1/P2/P3),持续验证(边做边测)
知识管理 即时文档化(趁上下文新鲜),分类存储(按问题类型),交叉引用(关联 Issue、PR)
工具设计 工具提供能力而非行为,Prompt 定义意图和流程,让代理决定如何达成目标

极简主义:设计理念如何影响我的实践

Claude Code 团队的实践给了我另一个启发:

“最好的工具,就是没有工具。”

他们的做法:

  • 只给模型一样东西:bash
  • 每周都在删工具,因为新模型不需要了
  • 减少模型的选择,就是增加模型的能力
  • “模型吞噬脚手架”——曾经的外部辅助,逐渐被模型吸收

产品极简主义:不是”越来越丰富”,而是”越来越纯粹”。每一代模型发布,工具都会变得更简单,因为复杂性转移到了模型内部。

这个理念深刻影响了我做 AI 工程化的设计思路

  1. 入口极简化:整个系统只有两个命令入口——/req-dev/optimize-flow。不是因为功能少,而是把复杂性藏到了 Agent 的智能路由里。用户不需要记住十几个命令,只需要表达意图,Agent 会判断该调用哪个 Skill。
  2. Skill 而非工具堆叠:speckit/openspec 倾向于提供更多工具、更多模板、更多约束。我选择相反的方向——把能力编码为 Skill,让 Agent 在需要时自动调用,而不是让用户手动选择”现在该用哪个工具”。
  3. 上下文自动加载:Claude Code 团队说”人类和 AI 看同样的输出,说同样的语言,共享同一个现实”。我把这个原则应用到上下文管理——不是让用户手动指定”加载哪些背景资料”,而是让 Agent 根据当前阶段自动加载相关的 context/。用户感受不到”上下文加载”这个动作,但 AI 已经具备了完整的信息。
  4. 删除优先于添加:每次迭代时,我会问自己”有哪些东西可以删掉?”而不是”还能加什么功能?”。AGENTS.md 从最初的长篇大论,精简到现在只放通用规范和目录指针,具体流程全部下沉到 Skill 里。
  5. 双重用户设计:Claude Code 为工程师和模型同时设计界面。AI 工程化也是——/req-dev 命令人可以手动调用,Agent 也可以在流程中自动调用子 Skill。同一套能力,两种调用方式,没有冗余。

当前实践的目标:让工具尽可能”隐形”——用户只需要说”我要做一个商品发放需求”,系统自动加载上下文、自动识别阶段、自动调用对应 Skill、自动沉淀经验。用户感受不到在”使用工具”,只是在”完成工作”。

注:关于工具消失的行业发展趋势,详见第九节”未来展望”。

上下文工程:AI 能力的前提是信息完整性

参考:Anthropic - Effective Context Engineering for AI Agents

什么是上下文工程?

上下文(Context) 指的是在从大语言模型(LLM)采样时包含的一组 token——不仅仅是提示词,还包括系统提示、工具定义、对话历史、检索到的文档等所有进入模型的信息。

上下文工程 是指在 LLM 推理过程中,策划和维护最优 token 集合的策略集合。它代表了 LLM 应用构建方式的根本转变:

提示词工程(旧范式) 上下文工程(新范式)
关注如何编写有效的提示词 管理整个上下文状态
主要针对一次性分类或文本生成任务 针对多轮推理和长时间运行的智能体
“找到正确的词语和短语” “什么样的上下文配置最可能产生期望行为?”

核心指导原则

找到最小可能的高信号 token 集合,最大化期望结果的可能性

为什么不重视上下文工程会导致严重问题?

很多团队把 AI 辅助编程的失败归咎于”模型不够强”或”提示词没写好”,但真正的根因往往是上下文工程的缺失。Anthropic 的研究揭示了几个关键问题:

问题 1:上下文腐蚀(Context Rot)

研究发现:随着上下文窗口中 token 数量增加,模型准确回忆信息的能力会下降

1
2
3
4
上下文腐蚀的恶性循环:
加载更多信息 → 窗口膨胀 → 信息检索精度下降 → 行为异常

人发现问题 → 加更多上下文纠正 → 窗口更膨胀 → 更差

这不是断崖式下降,而是梯度下降——模型在长上下文中仍然能力强大,但信息检索和长程推理的精度会持续降低。

问题 2:注意力预算耗尽(Attention Budget Exhaustion)

LLM 就像人类有限的工作记忆一样,拥有”注意力预算”:

1
2
3
4
5
6
7
8
9
Transformer 架构的约束:
├── 每个 token 都要关注所有其他 token,产生 n² 个成对关系
├── 训练数据中短序列比长序列更常见,模型对长上下文依赖的经验较少
└── 位置编码插值虽允许处理更长序列,但会降低 token 位置理解的精度

结果:
├── 每引入一个新 token 都会消耗注意力预算
├── 低质量的 token 会"稀释"高质量信息
└── 关键信息可能被噪声淹没

问题 3:speckit/openspec 的上下文盲区

回顾第二节的 speckit 困境,从上下文工程角度重新审视:

问题现象 上下文工程视角的根因
人 review 时逐步想起遗漏告诉 AI 历史经验没有编码为可检索的上下文
45 分钟完成需求,边际成本恒定 每次都是”冷启动”,没有上下文复用
上下文窗口频繁爆满 没有分层加载策略,一次性塞入过多信息
AI 行为异常,半途而废 上下文腐蚀导致关键信息被”遗忘”

问题 4:工具设计不当导致上下文污染

Anthropic 指出一个常见失败模式:

“臃肿的工具集,覆盖过多功能或导致使用哪个工具的决策点模糊不清”

判断标准:如果人类工程师无法明确说出在给定情况下应该使用哪个工具,AI 智能体也不能做得更好。

1
2
3
4
5
工具设计不当的后果:
├── 工具描述冗长 → 消耗上下文预算
├── 工具边界模糊 → AI 决策困难,产生更多试错对话
├── 工具返回冗余信息 → 上下文快速膨胀
└── 最终:窗口爆满,任务失败

有效上下文工程的核心原则

基于 Anthropic 的实践和我们的落地经验,总结以下原则:

原则 1:分层式信息组织

1
2
3
4
5
6
7
8
context/
├── business/
│ └── 活动业务边界.md ← 概要层(意图识别时加载)
├── tech/
│ └── Apollo配置规范.md ← 技术层(方案设计时加载)
└── experience/
├── 商品发放历史问题.md ← 经验层(实施前加载)
└── 雅典娜配置注意事项.md ← 详细层(配置时加载)

原则 2:”即时”上下文策略(Just-in-Time Context)

不是预先加载所有可能相关的信息,而是维护轻量级索引,在运行时动态加载:

1
2
3
4
5
6
7
传统方式(预加载):
启动 → 加载所有相关文档(20000 tokens)→ 开始工作 → 窗口已满一半

即时策略:
启动 → 加载索引文件(500 tokens)→ 识别当前阶段 → 按需加载(3000 tokens)

窗口保持精简,信息高度相关

Claude Code 的实践:使用 globgrep 等原语允许即时导航和检索文件,而不是预先加载完整数据对象到上下文中。

原则 3:上下文压缩与笔记系统

对于长时间运行的任务:

1
2
3
4
5
6
7
8
9
10
压缩(Compaction):
├── 将接近上下文窗口限制的对话内容总结
├── 保留:架构决策、未解决的 bug、实现细节
├── 丢弃:冗余的工具输出或消息
└── 用摘要重新初始化新的上下文窗口

结构化笔记(Structured Note-taking):
├── 智能体定期将笔记写入上下文窗口外的持久化存储
├── 稍后根据需要拉回上下文窗口
└── 实现跨压缩步骤的连贯性

原则 4:工具设计的上下文效率

1
2
3
4
5
6
7
8
9
10
11
好的工具设计:
├── 自包含:不依赖"记住"之前的对话
├── 返回精简:只返回 token 高效的必要信息
├── 边界清晰:用途明确,减少决策成本
└── 发挥模型优势:利用模型擅长的能力

坏的工具设计:
├── 返回完整数据库查询结果(可能数千行)
├── 工具描述长达数百 token
├── 多个工具功能重叠,边界模糊
└── 强迫模型做它不擅长的事情

上下文工程与 AI 工程化的关系

理解了上下文工程,就能理解 AI 工程化架构设计的”为什么”:

AI 工程化设计 上下文工程原理
context/ 分层目录 分层式信息组织,按阶段按需加载
Skill 封装固定流程 稳定执行过程,避免提示词遗漏导致的上下文不完整
Subagent 架构 主 Agent 保持精简,子任务独立窗口
状态文件传递 不依赖”记忆”,依赖结构化状态
经验沉淀机制 将知识编码为可检索上下文,而非依赖人脑

本质规律

1
2
3
AI 的决策质量 ∝ 可用信息的完整性 × 信息的信噪比
↑ ↑
不是越多越好 高信号、低噪声才有效

这意味着:

  • 与其让人在 review 时逐步想起遗漏告诉 AI
  • 不如建立系统化的上下文管理,让 AI 自动获取精简且高信号的信息

实践:AI 工程化的设计与落地

AI 工程化是什么

经过反复思考和实践,我提炼出了 AI 工程化的定义:

智能化管理工作信息,以上下文工程的理解管理整个工作场景,借助AI的能力,降低人对已识别问题的处理成本

组成部分

1. 脚手架(Git 仓库形式)

  • 把规范转为基础的目录结构
  • 附带基础的初始化命令
  • 存放业务线的上下文信息(业务背景、技术背景等)
  • 随项目独立迭代的资源文件

2. 工具包(插件形式)

  • 提供 AI 工程需要的 cmd、skill、mcp、agent、hook 等
  • 在插件市场迭代,分版本管理
  • update 即可升级最新的规范、能力集成

为什么分脚手架和工具包?

  • 插件市场内容会迭代、分版本,需要灵活升级
  • 脚手架项目初始化后,随项目迭代,是独立的 git 仓库
  • 脚手架适合存放基础资源文件和业务上下文信息
  • 工具包适合封装通用能力和规范

Image 1

核心架构:Agent + Skill 分层设计

1
2
3
4
5
用户输入 → Command → Agent(决策层)→ Skill(执行层)

意图识别、流程路由

调用具体 Skill 执行
  • Agent:自主决策层,负责意图识别、流程路由、上下文管理
  • Skill:过程执行层,负责固定流程任务的具体执行
  • Command:用户交互入口,通过 Agent 路由到具体执行

当前系统设计

  • 5 个 Agents:phase-router、requirement-manager、design-manager、implementation-executor、experience-depositor
  • 12 个 Skills:req-create、req-change、experience-index、design-create、design-change、workspace-setup、design-implementation、code-commit、requirement-completer、requirement-archiver、meta-maintainer、index-manager
  • 2 个 Commands/req-dev(需求研发统一入口)、/optimize-flow(流程优化沉淀)

目录结构:位置即语义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
your-project/
├── AGENTS.md # 项目记忆入口(每次会话自动加载)
├── .codebuddy/ # AI 自动化配置
│ ├── agents/ # Agent 定义(决策层)
│ ├── commands/ # 命令入口
│ └── skills/ # Skill 定义(执行层)
├── context/ # 项目知识库(长期记忆)
│ ├── business/ # 业务领域知识
│ ├── tech/ # 技术背景
│ │ └── services/ # 服务分析文档
│ └── experience/ # 历史经验
├── requirements/ # 需求管理
│ ├── INDEX.md # 需求索引
│ ├── in-progress/ # 进行中需求
│ └── completed/ # 已完成需求
└── workspace/ # 代码工作区(Git 忽略)

三个核心约束

  1. 入口短小:AGENTS.md 只放通用规范 + 目录指针,不写具体流程步骤
  2. 位置即语义:requirements/ 放需求产物,context/ 放可复用上下文,workspace/ 放代码
  3. 复利沉淀:每次执行命令,除了产出当前结果,还要让”下一次更快、更稳”

经验沉淀的技术实现

前面 4.1 节讲了复合工程的理念和三层沉淀机制,这里聚焦具体怎么实现

触发时机:什么时候沉淀?

1
2
3
4
5
6
7
8
不是:做完需求后专门花时间"写总结"
而是:在流程关键节点自动触发沉淀

具体触发点:
├── 需求完成时 → requirement-completer skill 自动提取可复用经验
├── 遇到问题解决后 → 用户说"记住这个坑" → experience-depositor agent 记录
├── 代码提交时 → code-commit skill 检查是否有值得记录的模式
└── 流程优化时 → /optimize-flow 命令专门用于沉淀和优化

沉淀格式:记录什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# context/experience/商品发放-钱包选择问题.md

## 问题描述
商品发放时选错钱包类型,导致用户领取失败

## 触发条件
- 需求涉及商品发放
- 商品类型为虚拟商品

## 解决方案
虚拟商品必须发到虚拟钱包,实物商品发到实物钱包
具体判断逻辑见 Apollo 配置:xxx.wallet.type

## 校验方式
检查 goods_type 与 wallet_type 的匹配关系

## 关联文档
- context/tech/Apollo配置规范.md
- context/tech/services/商品服务技术总结.md

检索机制:怎么在对的时候加载?

检索由 experience-index Skill 统一负责,在需求分析、方案设计、代码编写前自动调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Agent 的上下文加载逻辑:

1. 意图识别阶段
phase-router 识别意图,路由到对应 Agent

2. 经验检索阶段
Agent 调用 experience-index Skill,传入场景描述
Skill 检索四类规则文件:
├── context-rules.md → 匹配需加载的背景文档
├── risk-rules.md → 匹配风险提示
├── service-rules.md → 匹配服务依赖建议
└── pattern-rules.md → 匹配代码规范

3. 返回结构化结果
{
"context": { "files": ["商品发放历史问题.md"] },
"risk": { "alerts": [{"level": "high", "message": "注意钱包类型"}] },
"service": { "suggestions": ["商品服务", "钱包服务"] },
"pattern": { "files": ["error-handling.md"] }
}

4. Agent 主动提醒
"注意:历史上商品发放有钱包选择问题,请确认..."

规则沉淀入口:通过 /optimize-flow 命令,调用 experience-depositor Agent 将新规则写入对应规则文件。

演进路径:从文档到 Skill 到 Command

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
阶段 1:纯文档(被动)
context/experience/xxx.md
→ AI 读取后提醒,但需要人确认

阶段 2:校验 Skill(半自动)
skill/product-distribution-validator
→ 自动校验配置,发现问题直接报错

阶段 3:完整 Command(全自动)
cmd/implement-product-distribution
→ 一个命令:加载背景 + 校验 + 生成 + 提醒 + 沉淀新经验

演进判断标准:
- 同类需求做了 5 次以上 → 考虑封装 Skill
- Skill 被调用 10 次以上 → 考虑封装 Command
- 不要过早抽象,让实践驱动演进

与 speckit 的本质区别

1
2
3
4
5
6
7
8
9
speckit 的知识流向:
人脑 → Spec 文档 → 代码
↑__________|
下次还要从人脑开始

AI 工程化的知识流向:
人脑 → context/ → Skill → Command
↑_________|________|
知识留在工具链里,下次直接复用

时间成本的量化对比

前面 2.5 节从”问题-方案”角度做了概念对比,这里从时间成本角度量化差异:

执行次数 speckit/openspec AI 工程化 累计节省
第 1 次 45 分钟 45 分钟(建立 context/) 0
第 2 次 45 分钟(人重新想) 15 分钟(部分复用) 30 分钟
第 5 次 45 分钟(还是要想) 5 分钟(大量复用) 130 分钟
第 10 次 45 分钟(…) 3 分钟(高度自动化) 315 分钟

关键差异

  • 知识位置:speckit 在人脑(每次想),AI 工程化在 context/+skill/
  • 新人上手:speckit 依赖老人传授,AI 工程化第一天就能用
  • 边际成本:speckit 恒定,AI 工程化递减

深度对比:为什么传统 SDD 工具不够用

前面 2.5 节从”问题-方案”角度概述了 AI 工程化的优势,本节深入分析 speckit 和 openspec 的技术设计缺陷,帮助理解为什么需要新的解决方案。

speckit 的核心缺陷

问题 1:流程过于理想化

speckit 的 Constitution → Specify → Plan → Tasks → Implement 流程假设:

  • 需求是清晰的
  • 可以一次性规划
  • 按阶段线性推进

但企业真实场景是:

  • 需求动态变化
  • 多方并行博弈
  • 持续扯皮调整

问题 2:无法处理”考古”需求

speckit 从零开始定义,但真实开发必须”考古”:

  • 历史坑点在哪?
  • 现有能力有哪些?
  • 配置规范是什么?

问题 3:知识不会沉淀

1
2
3
每次执行:Constitution → Specify → Plan → Tasks → Implement

每次从头开始

缺失机制:

  • ❌ 实施过程中发现的坑不会被记录
  • ❌ 排查信息丢失
  • ❌ 下次遇到类似问题还得重新排查

问题 4:宪章系统的僵化

9 条不可变原则固然保证质量,但:

  • ✅ 适合标准化项目(Demo、开源库)
  • ❌ 不适合企业定制场景(历史债务、框架限制、合规要求)

openspec 的核心缺陷

问题 1:Delta 机制的理论美好与现实骨感

假设需求可以”提案化”,但企业真实场景是多线并行、动态调整、持续扯皮。

问题 2:Fail-Fast 的代价

理论上保证一致性,实际上成为阻塞点。人的认知窗口有限,很难手动解决复杂冲突。

问题 3:强依赖命名的脆弱性

产品、运营、研发对同一个需求有不同表述,命名不一致导致归档失败。

问题 4:Archive 只是”合并”,不是”学习”

1
2
3
4
5
6
F(CurrentSpec, DeltaSpec) → NewSpec

缺失的维度:
F(CurrentSpec, DeltaSpec, Context, Lessons) → NewSpec + Knowledge
↑ ↑
实施上下文 经验教训

共性问题:忽视人的现实工作模式

问题 1:忽视认知负担管理

两个工具都假设人能理解并遵循复杂流程、维护大量结构化文档、记住所有规范和约束。

但现实是:土办法最管用。工具应该适配人的工作模式,而不是强行改变它。

问题 2:忽视”执行过程”的价值

只关注”规范”和”结果”,忽视”过程”中的知识价值。

问题 3:忽视复利效应的关键性

1
2
3
4
5
传统工具:帮你"做事"
复合工程:帮你"越做越快"

传统工具:每次都是新的开始
AI 工程化:每次都站在上次的肩膀上

问题 4:Spec 详细程度的悖论

规范驱动开发有一个根本性的矛盾:

1
2
Spec 越详细 → 越接近代码本身 → 维护两份"代码"
Spec 越简略 → 越难指导 AI → 失去规范的意义

详细 Spec 的问题

  • 当 Spec 详细到可以精确指导 AI 生成代码时,它本身就变成了另一种形式的”代码”
  • 你需要同时维护 Spec 和 Code 两套产物,且要保持同步
  • 代码改了 Spec 要改,Spec 改了代码要改——双倍维护成本

AI 工程化的解法:不追求详细 Spec,而是分层概要 + 代码指针

1
2
3
4
5
6
7
8
9
10
11
12
AI 工程化的上下文组织:
├── 服务概要:这个服务做什么、边界在哪
├── 业务概要:核心业务流程、关键概念
├── 模块概要:模块职责、依赖关系
├── 接口概要:对外接口、调用方式
└── 代码指针:具体细节在 xxx/xxx.go 的 xxx 函数

不维护:
├── ❌ 详细的数据结构定义(代码里有)
├── ❌ 完整的接口参数说明(代码里有)
├── ❌ 具体的实现逻辑描述(代码里有)
└── ❌ 任何可以从代码直接获取的信息

核心原则:概要层帮助 AI 快速定位,细节层直接读代码。避免维护一份”像代码一样详细的 Spec 文档”——那只是换了个格式的代码,没有降低复杂度,反而增加了同步成本。


进阶能力:插件、Skill、MCP 的融合

对于大多数研发同学来说,可能还停留在 speckit、openspec 这类规范驱动工具的认知上。但 AI 工程化把更多能力融合在了一起:

Skill:可复用的能力单元

Skill 是过程执行层的基本单元,每个 Skill 负责一个具体的固定流程任务:

1
2
3
4
5
6
7
.codebuddy/skills/
├── req-create/ # 需求创建
│ ├── SKILL.md # 技能定义
│ └── templates/ # 模板资源
├── design-create/ # 方案创建
├── workspace-setup/ # 环境搭建
└── code-commit/ # 代码提交

Skill 的特点:

  • 单一职责:每个 Skill 只做一件事
  • 可复用:多个流程可以调用同一个 Skill
  • 可组合:复杂流程由多个 Skill 组合完成
  • 可演进:Skill 可以独立升级,不影响其他部分

Agent:自主决策层

Agent 负责意图识别、流程路由、上下文管理:

1
2
3
4
5
6
.codebuddy/agents/
├── phase-router.md # 阶段路由,意图识别
├── requirement-manager.md # 需求全生命周期管理
├── design-manager.md # 方案全生命周期管理
├── implementation-executor.md # 开发实施执行
└── experience-depositor.md # 经验沉淀(独立上下文)

Agent 与 Skill 的分工:

  • Agent:决定”做什么”
  • Skill:执行”怎么做”

多 Agent 协作:从上下文窗口爆满到高效分工

在实践 AI 工程化的过程中,我们遇到了一个关键瓶颈:上下文窗口爆满

问题的根源

早期使用 speckit 等工具时,最痛苦的体验是:

1
2
3
4
5
6
7
8
执行复杂需求时:
├── 加载业务背景(5000 tokens)
├── 加载技术上下文(8000 tokens)
├── 加载历史经验(3000 tokens)
├── 当前对话记录(持续增长)
└── ...

窗口频繁爆满 → 强制截断 → 丢失关键上下文 → AI 行为异常

Anthropic 工程团队精准描述了这个问题:

“想象一个软件项目由轮班工程师负责,每个新工程师到来时对上一班发生的事情毫无记忆。”

解决方案:Subagent 架构

借鉴 Anthropic 的双 Agent 架构思想,我们设计了 主 Agent + Subagent 的协作模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
传统模式(单一 Agent):
用户输入 → 一个大 Agent 处理所有事情 → 上下文持续膨胀 → 窗口爆满 → 任务失败

Subagent 模式:
用户输入 → 主 Agent(决策层)

意图识别 + 任务拆分

┌─────────┼─────────┐
↓ ↓ ↓
Subagent1 Subagent2 Subagent3
(独立窗口) (独立窗口) (独立窗口)
└─────────┼─────────┘

结果汇总 → 主 Agent 继续

核心优势

特性 说明
独立上下文窗口 每个 Subagent 有自己的上下文空间,不会互相污染
专注单一任务 每个 Subagent 只处理一件事,认知负担小
并行执行 多个 Subagent 可以同时工作,提升效率
结构化状态传递 通过文件传递结果,而非依赖”记忆”

效果对比

指标 单 Agent 模式 Subagent 模式
窗口爆满频率 70%(复杂需求几乎必爆) 5%(偶发于极端场景)
任务完成率 60%(经常中途失败) 95%(可靠完成)
上下文利用效率 30%(大量冗余信息) 80%(按需加载)

状态传递机制

Subagent 之间不共享上下文窗口,通过结构化状态文件保证信息传递:

1
2
3
4
5
6
7
8
9
核心文件:
├── requirements/INDEX.md # 需求状态索引
├── requirements/in-progress/ # 进行中的需求详情
└── context/session/ # 会话级临时上下文

工作流程:
1. Subagent 启动时:读取状态文件,快速理解当前状态
2. Subagent 执行中:专注自己的任务
3. Subagent 结束时:更新状态文件,提交"干净的交接"

核心原则:每个 Subagent 只完成一个”原子任务”,不是一个工程师连续工作 48 小时,而是轮班工程师每人 4 小时但交接清晰。

与 speckit 的本质差异

1
2
3
4
5
6
7
speckit:依赖"一个 Agent 记住所有事情"
Constitution → Specify → Plan → Tasks → Implement
上下文持续累积,到 Implement 阶段时窗口已经很满

Subagent:依赖"结构化的状态传递"
每个阶段独立的 Subagent,独立的上下文窗口
状态通过文件传递,而非上下文累积

前者是人脑模型(记忆有限),后者是团队协作模型(交接清晰)。

MCP:外部系统集成

MCP(Model Context Protocol)让 AI 能够直接对接外部系统:

1
2
3
4
5
6
7
8
9
10
11
12
13
基础集成:
├── TAPD MCP(需求管理)
│ ├── 自动获取需求详情
│ ├── 关联相关需求
│ └── 更新需求状态
├── 工蜂 MCP(代码管理)
│ ├── 自动创建分支
│ ├── 提交代码变更
│ └── 创建合并请求
└── iWiki MCP(知识管理)
├── 检索历史技术方案
├── 获取业务背景文档
└── 关联团队知识库

MCP 的价值:

  • 自动化操作:不需要人手动操作 TAPD、工蜂、iWiki
  • 信息同步:AI 自动获取最新信息
  • 减少错误:避免手动操作的遗漏和错误

插件市场:能力的分发与升级

工具包以插件形式发布到插件市场:

  • 版本管理:每个版本独立,可回滚
  • 灵活升级:update 即可获得最新能力
  • 团队共享:团队成员共享同一套能力集

与脚手架的配合:

  • 脚手架存放业务上下文(随项目迭代)
  • 工具包提供通用能力(独立版本管理)

落地策略:从零到一的实践路径

前面各节从理论角度阐述了 AI 工程化的设计,本节聚焦具体怎么落地。以 2.5 节提到的”商品发放”场景为例,展示完整的实践路径。

冷启动:新项目接入

冷启动是 AI 工程化的核心优势之一。传统工具的知识在人脑,需要传授;AI 工程化的知识在工具链里,开箱即用。

步骤 1:安装 AgentProjectKit 插件(5 分钟)

首先需要添加插件市场并安装 AgentProjectKit:

1
2
3
4
5
# 安装 AgentProjectKit 插件
/plugin install agent-project-kit@tmap-codebuddy-plugin

# 验证安装
/plugin list

步骤 2:脚手架初始化(15 分钟)

1
2
# 初始化 AI 工程项目
/agent-project-kit:init-project

命令会自动完成:

  1. 克隆 AI 工程项目模板
  2. 引导配置项目基本信息(业务线名称、定位等)
  3. 初始化 AGENTS.md 项目记忆文件

步骤 3:加载服务上下文(30 分钟)

这是冷启动的关键步骤。/agent-project-kit:load-service 命令实现项目级别长期记忆初始化

1
2
3
4
# 加载相关服务,生成技术总结
/agent-project-kit:load-service
/agent-project-kit:load-service
/agent-project-kit:load-service

/agent-project-kit:load-service 的工作流程

1
2
3
4
5
6
7
8
9
10
11
12
13
用户执行 /agent-project-kit:load-service 

1. 克隆服务代码到 workspace/loadservice/ 目录

2. 分析服务架构、业务逻辑、API 接口:
- 业务定位、核心职责、技术栈
- 依赖关系、对外接口、数据模型
- 关键模块、配置要点、常见坑点

3. 生成技术文档到 context/tech/services/ 目录

结果:AI 获得该服务的完整上下文,后续任何涉及该服务的需求
都会自动加载这份上下文

为什么这很重要?

  • speckit/openspec:每次需要描述服务背景时,依赖人记住并手动描述
  • AI 工程化:一次 /agent-project-kit:load-service,永久复用,新成员也能立即获得”老兵视角”

步骤 4:开始需求研发

使用 /req-dev 命令开始你的第一个需求:

1
2
3
4
5
# 创建新需求
/req-dev 实现用户认证功能

# 或者指定已有需求继续工作
/req-dev REQ-001

工具包自带常用研发工具集成(MCP),开箱即用:

MCP 集成 功能 传统方式
TAPD MCP 自动获取需求详情、关联需求、更新状态 手动复制粘贴需求内容
工蜂 MCP 自动创建分支、提交代码、创建 MR 手动操作 Git 命令
iWiki MCP 检索历史技术方案、业务背景文档、团队知识库 手动搜索翻阅 Wiki 页面

MCP 集成的价值

  • 不是”又多了几个工具要学”,而是”AI 自动帮你操作这些系统”
  • 需求来了 → AI 自动从 TAPD 拉取详情 → 自动检索 iWiki 历史方案 → 自动生成方案
  • 人只需要 review 和确认

冷启动效果对比

阶段 speckit/openspec AI 工程化
学习工具 1-2 小时 5 分钟(插件安装)
初始化项目 手动搭建 15 分钟(/agent-project-kit:init-project)
了解服务架构 2-4 小时(需老人讲解) 30 分钟(/agent-project-kit:load-service 自动分析)
准备总计 4-7 小时 50 分钟
首次工作质量 不稳定(依赖记忆和传授) 稳定(context/ 提供完整信息)

关键差异

  • speckit/openspec:工具是”空壳”,知识在人脑,需要传授
  • AI 工程化:工具包含”知识”(context/+MCP),新人第一天就能高质量工作

持续迭代:知识的复利沉淀

第 1 个需求:建立 context/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
需求:实现 12 月活动的商品发放

执行过程中发现问题:
- Apollo 配置有特殊格式要求
- 雅典娜 20 种商品类型,配置方式各不同
- 钱包选择要区分虚拟/实物
- 敏感 接口有合规要求

知识沉淀:
人:"@agent,记住这些坑"

自动生成/更新 context/:
├── context/tech/Apollo配置规范.md
├── context/experience/雅典娜配置注意事项.md
├── context/experience/商品发放历史问题.md
└── context/business/跨团队协作.md

耗时:45 分钟(首次建立)

第 2 个需求:复用 context/

1
2
3
4
5
6
7
8
9
10
11
12
需求:实现春节活动的商品发放(类似场景)

AI 自动加载 context/,自动提醒历史坑点
人 review:"嗯,都考虑到了" ✓

新发现:春节活动需要限制地域

"@agent,记住地域限制"

context/ 自动更新

耗时:15 分钟(大量复用,少量新增)

第 6-10 个需求:封装为 skill

1
2
3
4
5
6
7
8
9
10
11
12
当 context/ 足够完善,封装为能力层:

skill/product-distribution-helper:
- 自动加载所有商品发放相关 context/
- 自动校验 Apollo 配置格式
- 自动检查雅典娜商品类型
- 自动提醒钱包选择、地域限制
- 自动生成监控配置

使用:/implement-product-distribution → 一键完成

耗时:3 分钟(高度自动化)

团队协作:知识的共享与传承

新成员第一天

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
speckit/openspec:
1. 学习工具用法(1-2 小时)
2. 了解服务架构(需老人讲解,2-4 小时)
3. 熟悉流程规范(1 小时)
4. 开始工作:依赖记忆和老人传授,首次质量不稳定
总计:4-7 小时准备 + 不稳定的首次质量

AI 工程化:
1. 脚手架初始化(15 分钟)
2. 工具包安装(5 分钟)
3. 立即开始工作:
- context/ 提供服务上下文
- MCP 自动集成 TAPD/工蜂/Apollo
- cmd/skill 引导完成任务
- 首次就能高质量完成
总计:20 分钟准备 + 稳定的首次质量

团队效应

1
2
3
4
5
6
7
8
9
10
11
12
13
14
5 人团队,各做 2 次商品发放:

speckit:5 人 × 2 次 × 45 分钟 = 450 分钟

AI 工程化:
第 1 人第 1 次:45 分钟 → context/ 建立
第 1 人第 2 次:15 分钟
第 2 人第 1 次:15 分钟(复用第 1 人 context/)
第 2 人第 2 次:10 分钟
...
第 5 人第 2 次:5 分钟

总计:126 分钟
节省:450 - 126 = 324 分钟(72%)

未来展望:工具终将消失

第 4.2 节讨论了极简主义如何影响当前设计,本节从行业发展趋势角度展望工具的演进方向。

模型吞噬脚手架

随着模型能力的提升,很多外部辅助会被模型内化:

1
2
3
4
5
Opus 4.1 需要的东西,Sonnet 4.5 已经内化了

系统提示可以删 2000 个 tokens

工具每周都在变简单

这意味着什么? 今天我们在 context/、Skill、Agent 中编码的知识和流程,未来可能直接被模型”学会”。AI 工程化的架构设计需要为这种迁移做好准备——当某个 Skill 不再需要时,能够平滑删除而不影响整体。

多 Agent 架构的演进方向

从”工具调用”到”团队协作”

当前的 AI 辅助编程主要是”人调用 AI”模式:

1
人 → 发指令 → AI 执行 → 人检查 → 人发下一个指令

Subagent 架构开启了新的可能:

1
人 → 设定目标 → 主 Agent 拆解 → 多个 Subagent 协作 → 主 Agent 汇总 → 人验收

未来可能演进为:

1
人 → 设定目标 → Agent 团队自主协作数小时/数天 → 人验收最终结果

长时间运行 Agent 的关键挑战

Anthropic 的实践揭示了几个核心挑战:

挑战 当前解法 未来方向
上下文窗口限制 Subagent 分解 + 状态文件传递 更高效的 compaction + 更智能的上下文选择
任务连续性 结构化状态文件(JSON/Markdown) 更丰富的”工作记忆”机制
质量保证 端到端测试 + 人工 Review 专门的 QA Agent + 自动化验收
错误恢复 状态文件支持断点续做 更智能的错误分析和自动修复

Agent 专门化 vs 通用化的权衡

一个开放问题:应该用一个强大的通用 Agent,还是多个专门化的 Agent?

1
2
3
4
5
6
7
8
9
通用 Agent 路线:
├── 优势:简单,不需要协调
├── 劣势:上下文负担重,需要"知道所有事情"
└── 适合:简单任务、短时间任务

专门化 Agent 路线:
├── 优势:每个 Agent 更专精,上下文更精简
├── 劣势:需要协调机制,状态传递成本
└── 适合:复杂任务、长时间任务、团队协作场景

我们的选择:对于企业级复杂场景,专门化 Agent 更适合。原因是:

  1. 企业场景本身就是”团队协作”,Agent 架构应该反映这一现实
  2. 上下文窗口是硬约束,专门化可以更高效利用
  3. 专门化 Agent 更容易独立迭代和优化

与人类团队的类比

最好的 Agent 架构设计,灵感来自人类高效团队的工作方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
人类团队:
├── 产品经理:理解需求、拆解任务
├── 技术 Leader:设计方案、分配工作
├── 开发工程师:实现功能
├── 测试工程师:验证质量
└── 每个人有自己的专业领域,通过"会议"和"文档"协调

Agent 团队:
├── phase-router:理解意图、路由任务
├── design-manager:设计方案
├── implementation-executor:实现功能
├── test-agent(计划中):验证质量
└── 每个 Agent 有自己的专业 Prompt,通过"状态文件"协调

Anthropic 工程团队的洞察:”这些实践的灵感来自于了解高效软件工程师每天做什么。”

当前范式:Claude 做一步,你检查,批准,它继续。

未来范式:

1
2
3
4
5
当模型可以自主工作几天甚至几周:
早上:"我想完成 X"
晚上:看结果

中间的过程?它自己处理。

人的角色从”操作者”变成”监督者”,从”指令发出者”变成”目标设定者”。

AI 工程化的定位:在这个转型过程中,AI 工程化是”过渡期基础设施”——帮助团队在当前阶段高效工作,同时为未来的全自动化积累知识和经验。

研发工作的本质变化

AI 工程化不只是引入新工具,而是重新定义了研发的工作方式。这种变化已经在 AI 技术最前沿的团队中发生。

首先要避免的认知误区

工程师在使用 AI 时最常见的两种误解:

误区 表现 结果
AI 是”银弹” 期望 AI 自动理解需求、写出完美代码 过度依赖,缺乏监督,质量不稳定
AI 是”思考替代品” 把 AI 当作可以替代人类思考的工具 不理解业务,一直捣鼓 AI,适得其反

正确的定位是:AI 是强大的执行工具,但决策权和判断力必须留在人手中。

来自 OpenAI 与 Anthropic 的实践经验

理解 AI 的真实能力边界

参考 OpenAI 团队使用 Codex 构建 Sora 安卓应用的经验,将 AI 定位为**”一位新入职的资深工程师”**:

需要人类指导 表现卓越
无法推断隐性上下文(团队偏好、内部规范) 快速理解大型代码库,精通主流编程语言
缺乏真实用户体感(无法感知”滚动不顺畅”) 热衷于编写单元测试,能根据 CI 日志修复问题
深层架构判断力不足(本能是”让功能跑起来”) 支持大规模并行,同时探索多种方案

三步协作工作流(借鉴 OpenAI 与 Anthropic 经验):

阶段 人的职责 AI 的职责
奠定基石 定义架构、编写范例代码、设定标准 学习并遵循
共同规划 校准理解、确认方案 总结现状、生成设计文档
执行交付 架构把关、质量审查 编码实现、测试修复

Anthropic 内部调查数据(2025年8月,132名工程师,20万条使用记录):

  • 工程师在 60% 的工作中使用 AI,实现 50% 的生产力提升,年同比增长 2-3 倍
  • 27% 的 AI 辅助工作是原本不会完成的任务(如交互式仪表板、探索性工作)
  • 工程师倾向于委托易于验证、定义明确、代码质量不关键、重复无聊的任务

“我可以非常胜任前端、事务性数据库的工作…而以前我会害怕触碰这些东西。” —— 后端工程师

“我以为我真的很享受编写代码,但实际上我只是享受编写代码带来的结果。” —— 高级工程师

核心理念:寻找 AI 的”舒适区”

工程师的核心工作之一,已经从纯粹的编码转变为识别 AI 的能力边界,并将复杂任务转化为落入 AI “舒适区”内的子任务

  • 低标准、高容错场景:任务对精确度要求不高,容忍多次失败。AI 尝试 N 次只要一次成功,就是显著提效
  • 迭代式开发场景:形成”AI 初步实现 → 人验证修正 → 快速反馈”的闭环,不追求一次完美

工作模式的具体变化

工作内容的迁移

工作环节 传统模式 AI 工程化模式 角色变化
需求理解 反复阅读文档、追问产品 Agent 自动加载 context/,主动提示 信息收集者 → 信息确认者
方案设计 从零构思、翻阅历史代码 基于模板生成,AI 提示已知风险 方案起草者 → 方案审核者
代码实现 逐行编写、查文档、调试 AI 生成初版,人 review 调整 代码生产者 → 代码把关者
知识沉淀 写文档(经常忘记) /optimize-flow 即时沉淀 文档维护者 → 经验触发者

时间分配的重构

1
2
3
4
5
传统研发:                         AI 工程化后:
├── 40% 信息收集 ├── 10% 信息确认
├── 30% 重复劳动 ├── 10% 结果审核
├── 20% 核心决策 → ├── 50% 核心决策
└── 10% 知识沉淀 └── 30% 知识沉淀

一个具体的对比——以”商品发放需求”为例:

1
2
3
4
5
6
7
8
传统模式的一天:                              AI 工程化模式的一天:
09:00-10:30 阅读需求文档,追问产品 09:00-09:30 /req-dev,确认需求边界
10:30-12:00 翻阅历史代码,理解逻辑 09:30-10:30 review AI 方案,调整决策点
14:00-15:30 询问老人"以前怎么做" 10:30-12:00 review AI 代码,优化核心逻辑
15:30-18:00 写代码,边写边查文档 14:00-15:00 AI 辅助测试,修复问题
18:00-19:00 遇到配置问题,排查 15:00-15:30 /optimize-flow 沉淀经验
19:00-20:00 继续写代码 15:30-17:00 处理下一个需求
产出:完成 60%,知识留在脑子里 产出:完成 100%,经验沉淀到 context/

能力要求的升级

能力维度 传统要求 AI 工程化要求
编码能力 熟练编写各类代码 能判断 AI 生成代码的质量和风险
知识储备 记住各种细节和坑点 知道如何组织知识让 AI 能用
问题解决 自己动手排查 会描述问题让 AI 辅助分析
效率提升 写更多代码、加更多班 设计更好的 Skill、沉淀更多经验

新的核心竞争力体现为三种能力:

  1. 系统理解能力:AI 能实现功能,但只有人能判断它是否以正确方式融入系统
  2. AI 协作能力:设计上下文、拆解计划、通过反馈循环持续优化
  3. 设计质量标准:当”写出能工作的代码”门槛降低,架构设计和交付质量成为区分标准

监督悖论:有效使用 AI 需要监督能力,而监督能力可能因过度依赖 AI 而退化。Anthropic 的一些工程师故意在没有 AI 的情况下练习以”保持敏锐”。

本质洞察

黄仁勋有一个精准的判断:**AI 改变的是”任务”,而非”职业”**。

  • 被 AI 接管的任务:信息检索、样板代码、格式化、重复配置
  • 人依然主导的核心:系统设计、架构决策、质量判断、创新突破

AI 工程化的价值,就是让这种”任务迁移”在团队中系统化落地——通过 context/ 让信息检索自动化,通过 Skill 让重复流程标准化,通过经验沉淀让知识持续复利。

最终目标:让研发把时间花在”只有人能做的事”上,而不是”AI 也能做的事”上。

工具隐形化:从”使用工具”到”完成工作”

工具消失的含义:不是工具不存在了,而是工具变得如此无缝,你感受不到它的存在。

1
2
就像现在你用搜索引擎,不会想"我在使用一个信息检索系统"。
你只是在找答案。工具隐形了。

隐形化的三个层次

层次一:操作隐形——从”记住命令”到”表达意图”

1
2
3
4
5
6
7
8
9
10
过去:记住 20 个命令,选择正确的那个
├── /speckit.constitution
├── /speckit.specify
├── /speckit.plan
├── /speckit.tasks
└── ...

现在:只说你要什么
├── "/req-dev 实现商品发放" → Agent 自动判断是创建还是继续
└── 不需要知道底层调用了哪些 Skill

层次二:知识隐形——从”想起经验”到”系统提醒”

1
2
3
4
5
6
7
8
9
过去:做需求时,人要想起历史上有什么坑
├── "上次商品发放好像有个钱包问题..."
├── "Apollo 配置格式是什么来着..."
└── 认知负担在人身上

现在:experience-index 自动检索,主动提醒
├── "检测到商品发放场景,已加载相关经验..."
├── "风险提示:注意钱包类型匹配"
└── 知识在系统里,人只需确认

层次三:流程隐形——从”遵循步骤”到”自然完成”

1
2
3
4
5
6
7
8
9
过去:严格按 Constitution → Specify → Plan → Tasks → Implement 执行
├── 人要知道"现在该执行哪个阶段"
├── 人要判断"前置条件是否满足"
└── 流程感知在人身上

现在:Agent 自主决策流程路由
├── 人说"继续做 REQ-001"
├── phase-router 自动判断当前阶段和下一步
└── 人感受到的是"工作在推进",而非"在执行流程"

AI 工程化的隐形化进度

维度 当前状态 目标状态
命令入口 ✅ 2 个命令覆盖全流程 自然语言直接触发
上下文加载 ✅ experience-index 自动检索 完全无感知加载
阶段流转 ✅ phase-router 自动路由 Agent 自主推进多步
经验沉淀 🔄 需要 /optimize-flow 触发 自动识别并沉淀
跨会话连续性 🔄 依赖状态文件 无缝断点续做

隐形化的终极形态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
今天:
人:"我要做一个商品发放需求"
AI:执行一步,等待确认
人:确认,继续
AI:执行下一步,等待确认
...

明天:
人:"我要做一个商品发放需求"
AI:分析、设计、实现、测试、提交 PR
人:Review 最终结果

后天:
人:(在 TAPD 创建需求单)
AI:(自动感知、自动完成、自动提交 Review)
人:(只在关键决策点介入)

最后一步:你不再”使用”工具,你只是在思考业务问题,而工具已经把代码写好了。

写在最后:从第一性原理出发

回顾这段历程,我最大的收获是:不要为了用工具而用工具

speckit 和 openspec 都是优秀的工具,它们定义的流程、模板、检查清单都很有价值。但正如 2.5 节(AI 工程化如何破局)的对比所示,它们解决的是”规范化”问题,而企业真实场景的核心问题是:

  1. 上下文缺失:AI 看不到历史经验、业务边界、配置规范
  2. 知识不沉淀:每次都从头开始,边际成本恒定
  3. 范围太窄:只管单个仓库,无法覆盖跨服务、跨系统的复杂场景

AI 工程化试图解决这些问题:

1
2
3
上下文工程 → 让 AI 自动获取完整信息
复合工程 → 让每次实践都降低下次成本
项目级方案 → 管理所有仓库和外部系统

核心思路

1
2
3
4
5
6
7
8
9
能够落地的最高效流程 → 已存在于高效的人的行为过程中

把高效流程 AI 化 → 推广到全团队应用

细节流程在具体业务线中迭代 → 自定义探索

实践中发现问题 → 提取可复用信息 → AI 工程化融入工具

下次通用场景使用时可复用

最后想说的是

AI 工程化不是要替代 speckit 或 openspec,而是在它们的基础上,融合上下文工程、复合工程、插件市场、MCP 集成等能力,形成一套更适合企业复杂场景的解决方案。

如果你也在探索 AI 辅助研发,希望这篇文章能给你一些启发:

  1. 从真实工作场景出发,而不是从工具出发
  2. 把知识编码进工具,而不是只写文档
  3. 追求边际成本递减,而不是固定成本
  4. 让工具适配人,而不是让人适配工具

工具的终极形态是消失。在那一天到来之前,我们要做的是让工具越来越”懂”我们的工作,越来越”记得”我们的经验,越来越”自然”地融入我们的日常。

这就是 AI 工程化的意义所在。

参考资料

谁在开车: 西游战车与心智模型

最近看了不少 Ego、观察者相关的内容,想着能不能结合丹尼尔·卡尼曼在《思考,快与慢》一书中提到的「系统一」和「系统二」来构建一个心智模型。于是就想出了这么一个场景:西游战车

1. 司机与噪音

坐在驾驶位的是孙悟空(系统一)。他反应极快,直觉敏锐,肌肉记忆发达。为了生存,这辆车(身体)必须由他来驾驶。只有他能在极短时间内对突发的危险做出反应。

孙悟空是个好司机,但他有一个致命弱点:他听觉敏锐,且极易受惊。

这就引出了这个系统里最大的设计缺陷——那个摆在仪表台上的装饰物:猪八戒(Ego)。在这个模型里,他是一个连着油箱的、大功率的有源音箱。这个音箱的功能只有一个:制造叙事(Narrative)

2. 低像素的广播

猪八戒音箱的工作机制,是典型的 「低像素采样」。当一辆车加塞到你前面,这本是一个拥有海量细节的物理事件(光影、速度、距离)。但猪八戒的大脑处理不了这么大的数据量。他会迅速抓取一个模糊的截图,压缩细节,然后贴上一个巨大的标签——「侮辱」

紧接着,音箱开始通电,循环广播:“他在羞辱我们!我们得想办法还击!”

孙悟空分辨不出事实(Raw Data)与广播(Narrative)的区别。他听到了威胁,于是肾上腺素飙升,猛踩油门。 司机(悟空)就这样被噪音(八戒)劫持了。你不再看路,你在听故事。

3. 沙僧的无效辩论

当你意识到自己失控时,试图讲道理往往行不通。此时如果唤醒副驾驶上的沙僧(系统二,代表逻辑和理性),让沙僧去解决问题,他要解开安全带,扑向仪表台,用手捂住那个正在震耳欲聋的音箱,或者试图跟音箱辩论:“别吵了,撞车是不划算的!”

但这通常是无效的。原因有两个:

  1. 太慢: 在沙僧列出三个逻辑论点之前,孙悟空已经把车开进沟里了。
  2. 太累: 用逻辑去压抑情绪(跟音箱拔河),极其消耗能量。「意志力」就是这样被耗尽的。

所以,试图用「压抑」来解决「内耗」,在架构上是行不通的。

4. 唐僧的审视

那个一直坐在后座、很容易被忽略的人是唐僧(观察者)。在这个模型中,唐僧不需要会念经,也不需要有法力,他只需要做一件事:审视

神奇的事情发生了:当猪八戒被唐僧平静地「看着」时,他的喇叭会自动哑火。

因为叙事无法在审视下存活。这时候,孙悟空依然握着方向盘,他看到了那个摆件在剧烈抖动,甚至看到了它张大的嘴巴。但是,因为没有了煽动性的广播,孙悟空不会感到恐惧或愤怒。他或许会想:“噢,那个猪头又在抽风了。” 然后,他继续看着前方的路,平稳地驾驶。

5. 夺回驾驶权

这种状态,心理学上叫做 「认知解离」。正如冥想,并不是要把猪八戒扔出车外,也不是要让反应迟钝的沙僧去开车(那会出车祸),而是练习「审视」的能力。

大多数人的痛苦在于,他们的唐僧或是睡着了,或是太把猪八戒的广播当真,沉浸在那些虚构的剧情里。一旦唐僧睁开眼开始审视,就会发现并不需要去「关掉」声音,因为审视本身,就是一种静音。

6. 引擎盖之下:能量守恒与 TPN

为什么这一招有效?可以从神经科学层面来解释。首先,能量是有限的,这就像战车的发电机功率是固定的。

  • 猪八戒模式 = DMN/Ego:当你发呆、反刍过去、担忧未来时激活。它极其耗能,因为它在不停地编造故事。
  • 孙悟空专注模式 = TPN:当你全神贯注处理外部任务时激活。

神经科学发现了一个反相关现象:这两个网络就像跷跷板。当一个活跃时,另一个就会被抑制。所以你不需要去跟猪八戒打架(那是在消耗能量),你只需要把电流切断,输送给另一条线路: DEN(直接体验网络,Direct Experience Network),这是 TPN 的一种特殊形态。当你切换到这个模式时,会强迫大脑放弃概念化(猪八戒的叙事),转而进入纯粹感知。

  • 猪八戒模式(DMN/Ego): 看到前车 -> 联想「混蛋」 (概念) -> 感到「侮辱」 (叙事)。
  • 审视模式(DEN): 看到前车 -> 审视「我在生气吗?」 -> 感知「光线、距离」 (事实)。

当你全力感知「脚底板的触感」或「呼吸的温度」时,猪八戒之所以闭嘴,是因为他的电被拔了——大脑把所有的带宽都拿去处理「高清感官直播」了,根本没有余力去运行猪八戒的「低像素广播」。

这就是为什么「活在当下」能治愈焦虑。它不是心灵鸡汤,它是物理层面的抢占带宽

最后,再来说说冥想(Meditation)。冥想不是发呆,更不是为了成佛。冥想是对唐僧进行的「肌肉记忆训练」。每一次你在冥想中发现自己走神了(觉察到猪八戒开始广播),然后温和地把注意力拉回到呼吸上(审视,激活 DEN),你就是在做一次「举铁」。你每把注意力拉回一次,唐僧的「二头肌」就强壮一分。

我们无法消灭猪八戒,离不开孙悟空和沙僧,还需要后座的唐僧在场,并在必要时进行审视,这样才能在混乱的现实公路上,穿越噪音,驶向真正的彼岸。

AppStore卡审依旧存在,预计下周将逐渐恢复常态!

背景

圣诞节🎄虽然结束了,后劲儿依旧在。最直观的感受就是AppStore审核节奏还未恢复正常。依然存在审核时间较久或等待审核时间过长的问题。

举一个直观的例子🌰:

一座5层高的商场,每层都预备了洗手间🚾。正常情况下,足够满足整座商城客流量的需求。但是赶上了节假日高峰,并且只有3层洗手间可用。那么在常态客流量不变的情况也已经拥挤,更不要说节假日高峰期。

就第三方上架&更新趋势来看,AppStore审核节奏也将逐步正常。

非必要迭代

如果不是遇到重大线上问题或重大功能迭代,建议不更新或不上新包。避免正常产品遭遇卡审状态,导致难以定位问题或者审核员摆烂直接一手4.3a。

毕竟AppStore审核团队,刚刚经历了年关肯定积压了大量待审核的产品,多少也有些烦躁。(PS:单纯从心理角度来讲

新包、新账号和新代码,“三新原则”基本上叠满了卡审buffer。【特指中国大陆的开发者,海外账号亲测影响不大。】

重大更新

对于产品有着节前活动或市场战略布局的产品,那么也不用担心。在AppStore依然存在便捷通道:即加急审核!

常规产品,不必担心,这是官方提供的合理渠道,确实保障开发者的紧急需求【AppStore中的急诊室】。

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

相关推荐

# 苹果开发者续费大坑及成功续费方案!亲测有效

# AppStore敏感词排查手册,多维度分析Guideline 2.3.1隐藏功能,轻松过审。

# 如何主动提防苹果3.2f的进攻,自查防御手册(代码篇)

# 如何主动提防苹果3.2f的进攻,自查防御手册(ASO篇)

# 苹果加急审核是“绿色通道”还是“死亡陷阱”?

# 苹果开发者邮箱,突然收到11.2通知严重么?

# 不想被苹果卡审最好错开这两个提审时间

# 手撕苹果审核4.3是代码问题还是设计问题?

# 有幸和Appstore审核人员进行了一场视频会议特此记录。

2026 年 Expo + React Native 项目接入微信分享完整指南

2026 年 Expo + React Native 项目接入微信分享完整指南

本文基于 Expo SDK 54 + React Native 0.81 + react-native-wechat-lib 1.1.27 的实战经验,详细记录了在 Expo 管理的 React Native 项目中接入微信分享功能的完整流程和踩坑记录。

前言

在 React Native 生态中,react-native-wechat-lib 是目前最常用的微信 SDK 封装库。但由于该库更新较慢,加上 Expo 的特殊性,接入过程中会遇到不少坑。本文将分享我们在生产项目中的完整接入方案。

技术栈

  • Expo SDK: 54.0.30
  • React Native: 0.81.5
  • react-native-wechat-lib: 1.1.27
  • 构建方式: EAS Build

整体流程

准备工作 → 安装依赖 → 创建 Expo 插件 → 配置 app.config.js → 
编写 JS 服务层 → 服务器配置 → 微信开放平台配置 → 构建测试

第一步:准备工作

1.1 微信开放平台配置

  1. 登录 微信开放平台
  2. 创建移动应用,获取 AppID
  3. 配置 iOS 应用信息:
    • Bundle ID: com.yourapp
    • Universal Link: https://yourdomain.com/open/

1.2 Apple Developer 配置

  1. 获取 Team ID(格式如 A1B2C3D4E5
  2. 确认 Bundle ID 与微信开放平台一致

第二步:安装依赖

npm install react-native-wechat-lib@1.1.27

⚠️ 注意:在 Expo 管理的项目中,不需要手动执行 pod install,EAS Build 会自动处理。

第三步:创建 Expo Config Plugin

由于 Expo 管理原生代码,我们需要通过 Config Plugin 来配置微信 SDK 所需的原生设置。

创建 plugins/withWechat.js

const { withInfoPlist, withAndroidManifest } = require("expo/config-plugins");

/**
 * 微信 SDK Expo Config Plugin
 * 自动配置 iOS 和 Android 的微信相关设置
 */
function withWechat(config, { appId, universalLink }) {
  if (!appId) {
    throw new Error("withWechat: appId is required");
  }

  // iOS 配置
  config = withInfoPlist(config, (config) => {
    // 添加微信 URL Scheme
    const urlTypes = config.modResults.CFBundleURLTypes || [];
    const wechatScheme = {
      CFBundleURLSchemes: [appId],
      CFBundleURLName: "wechat",
    };

    const hasWechatScheme = urlTypes.some(
      (type) =>
        type.CFBundleURLSchemes &&
        type.CFBundleURLSchemes.includes(appId)
    );

    if (!hasWechatScheme) {
      urlTypes.push(wechatScheme);
    }
    config.modResults.CFBundleURLTypes = urlTypes;

    // 添加 LSApplicationQueriesSchemes
    const queriesSchemes = config.modResults.LSApplicationQueriesSchemes || [];
    const wechatSchemes = ["weixin", "weixinULAPI"];
    wechatSchemes.forEach((scheme) => {
      if (!queriesSchemes.includes(scheme)) {
        queriesSchemes.push(scheme);
      }
    });
    config.modResults.LSApplicationQueriesSchemes = queriesSchemes;

    return config;
  });

  // Android 配置
  config = withAndroidManifest(config, (config) => {
    const mainApplication = config.modResults.manifest.application?.[0];
    if (!mainApplication) return config;

    const packageName = config.android?.package || "com.yourapp";
    const activities = mainApplication.activity || [];
    const wxActivityName = `${packageName}.wxapi.WXEntryActivity`;

    const hasWxActivity = activities.some(
      (activity) => activity.$?.["android:name"] === wxActivityName
    );

    if (!hasWxActivity) {
      activities.push({
        $: {
          "android:name": wxActivityName,
          "android:exported": "true",
          "android:launchMode": "singleTask",
          "android:taskAffinity": packageName,
          "android:theme": "@android:style/Theme.Translucent.NoTitleBar",
        },
      });
    }

    mainApplication.activity = activities;
    return config;
  });

  return config;
}

module.exports = withWechat;

第四步:配置 app.config.js

module.exports = {
  expo: {
    name: "你的应用名",
    slug: "your-app",
    version: "1.0.0",
    
    extra: {
      wechatAppId: "wx你的AppID", // 微信 AppID
    },
    
    ios: {
      bundleIdentifier: "com.yourapp",
      associatedDomains: [
        "applinks:yourdomain.com",
        "webcredentials:yourdomain.com",
      ],
      infoPlist: {
        LSApplicationQueriesSchemes: ["weixin", "weixinULAPI", "wechat"],
      },
    },
    
    android: {
      package: "com.yourapp",
    },
    
    plugins: [
      [
        "./plugins/withWechat",
        {
          appId: "wx你的AppID",
          universalLink: "https://yourdomain.com/open/",
        },
      ],
    ],
  },
};

第五步:编写微信服务层

创建 src/services/wechatService.ts

import { Platform, Alert } from "react-native";
import Constants from "expo-constants";

// 从 Expo 配置中获取微信 AppID
const WECHAT_APP_ID = Constants.expoConfig?.extra?.wechatAppId || "";

// 动态加载微信 SDK
let WeChat: any = null;
let sdkLoadAttempted = false;

const getWechatSDK = () => {
  if (sdkLoadAttempted) return WeChat;
  sdkLoadAttempted = true;
  
  if (Platform.OS === "web") {
    return null;
  }
  
  try {
    const module = require("react-native-wechat-lib");
    WeChat = module.default || module;
    
    if (!WeChat || typeof WeChat.registerApp !== "function") {
      WeChat = null;
    }
    
    return WeChat;
  } catch (error) {
    console.warn("微信 SDK 加载失败:", error);
    return null;
  }
};

class WechatService {
  private isRegistered = false;

  // 检查 SDK 是否可用
  isAvailable(): boolean {
    if (Platform.OS === "web") return false;
    const sdk = getWechatSDK();
    return sdk !== null && typeof sdk.registerApp === "function";
  }

  // 注册微信 SDK
  async register(): Promise<boolean> {
    if (this.isRegistered) return true;
    
    const sdk = getWechatSDK();
    if (!sdk) return false;
    
    try {
      const result = await sdk.registerApp(WECHAT_APP_ID);
      this.isRegistered = result;
      return result;
    } catch (error) {
      console.error("微信 SDK 注册失败:", error);
      return false;
    }
  }

  // 检查微信是否已安装
  async isWechatInstalled(): Promise<boolean> {
    const sdk = getWechatSDK();
    if (!sdk) return false;
    
    try {
      return await sdk.isWXAppInstalled();
    } catch (error) {
      return false;
    }
  }

  // 分享网页到微信
  async shareWebpage(params: {
    title: string;
    description: string;
    thumbImageUrl?: string;
    webpageUrl: string;
    scene?: "session" | "timeline" | "favorite";
  }): Promise<{ success: boolean; message: string }> {
    
    if (!this.isAvailable()) {
      return { 
        success: false, 
        message: Platform.OS === "web" 
          ? "Web 端暂不支持微信分享" 
          : "微信分享功能需要在正式构建版本中使用"
      };
    }

    try {
      const registered = await this.register();
      if (!registered) {
        return { success: false, message: "微信 SDK 初始化失败" };
      }

      const isInstalled = await this.isWechatInstalled();
      if (!isInstalled) {
        return { success: false, message: "请先安装微信" };
      }

      const sceneMap = {
        session: 0,   // 聊天界面
        timeline: 1,  // 朋友圈
        favorite: 2,  // 收藏
      };

      const sdk = getWechatSDK();
      await sdk.shareWebpage({
        title: params.title,
        description: params.description,
        thumbImageUrl: params.thumbImageUrl || "",
        webpageUrl: params.webpageUrl,
        scene: sceneMap[params.scene || "session"],
      });

      return { success: true, message: "分享成功" };
    } catch (error: any) {
      if (error?.errCode === -2) {
        return { success: false, message: "已取消分享" };
      }
      return { success: false, message: error?.message || "分享失败" };
    }
  }

  // 分享图片到微信
  async shareImage(params: {
    imageUrl?: string;
    imageBase64?: string;
    scene?: "session" | "timeline" | "favorite";
  }): Promise<{ success: boolean; message: string }> {
    if (!this.isAvailable()) {
      return { success: false, message: "微信分享不可用" };
    }

    try {
      await this.register();
      
      const isInstalled = await this.isWechatInstalled();
      if (!isInstalled) {
        return { success: false, message: "请先安装微信" };
      }

      const sceneMap = { session: 0, timeline: 1, favorite: 2 };
      const sdk = getWechatSDK();
      
      await sdk.shareImage({
        imageUrl: params.imageBase64 || params.imageUrl,
        scene: sceneMap[params.scene || "session"],
      });

      return { success: true, message: "分享成功" };
    } catch (error: any) {
      if (error?.errCode === -2) {
        return { success: false, message: "已取消分享" };
      }
      return { success: false, message: "分享失败" };
    }
  }
}

export const wechatService = new WechatService();

第六步:服务器配置 (Universal Link)

在你的服务器上创建 apple-app-site-association 文件。

文件路径

https://yourdomain.com/.well-known/apple-app-site-association

文件内容

{
  "applinks": {
    "apps": [],
    "details": [
      {
        "appIDs": ["TEAMID.com.yourapp"],
        "components": [
          { "/": "/open/*" },
          { "/": "/topic/*" }
        ]
      }
    ]
  },
  "webcredentials": {
    "apps": ["TEAMID.com.yourapp"]
  }
}

⚠️ 将 TEAMID 替换为你的 Apple Team ID,com.yourapp 替换为你的 Bundle ID。

服务器配置要求

  1. 必须通过 HTTPS 访问
  2. Content-Type 应为 application/json
  3. 文件名不能有 .json 后缀
  4. 不能有重定向

Nginx 配置示例

location /.well-known/apple-app-site-association {
    default_type application/json;
}

第七步:在组件中使用

import React from "react";
import { Button, Alert } from "react-native";
import { wechatService } from "@/services/wechatService";

export function ShareButton() {
  const handleShare = async () => {
    const result = await wechatService.shareWebpage({
      title: "分享标题",
      description: "分享描述",
      thumbImageUrl: "https://example.com/thumb.jpg",
      webpageUrl: "https://example.com/share-page",
      scene: "session", // 或 "timeline" 分享到朋友圈
    });

    if (result.success) {
      Alert.alert("成功", "分享成功");
    } else {
      Alert.alert("提示", result.message);
    }
  };

  return <Button title="分享到微信" onPress={handleShare} />;
}

第八步:构建和测试

使用 EAS Build

# 构建 iOS 生产版本
eas build -p ios --profile production

# 构建并自动提交到 TestFlight
eas build -p ios --profile production --auto-submit

测试注意事项

  1. Expo Go 不支持:微信 SDK 是原生模块,必须使用 EAS Build 构建的版本测试
  2. 重启手机:安装新版本后建议重启手机,让 iOS 刷新 Associated Domains 缓存
  3. 验证 Universal Link:访问 https://app-site-association.cdn-apple.com/a/v1/yourdomain.com 确认 Apple 已缓存配置

常见问题排查

问题 1:分享时微信没有被唤起

可能原因:

  • Universal Link 配置不一致(微信开放平台、App 代码、服务器三端必须完全一致)
  • apple-app-site-association 文件内容错误或无法访问
  • Apple 还未缓存你的配置

排查步骤:

  1. 确认三端域名完全一致(注意 www 和非 www 的区别)
  2. 直接访问 https://yourdomain.com/.well-known/apple-app-site-association 确认可以下载
  3. 检查 Apple CDN 缓存:https://app-site-association.cdn-apple.com/a/v1/yourdomain.com

问题 2:SDK 注册失败

可能原因:

  • AppID 配置错误
  • 在 Expo Go 中运行(不支持)

解决方案:

  • 确认 app.config.js 中的 AppID 与微信开放平台一致
  • 使用 EAS Build 构建的版本测试

问题 3:提示"请先安装微信"

可能原因:

  • LSApplicationQueriesSchemes 未正确配置

解决方案: 确认 app.config.js 中包含:

infoPlist: {
  LSApplicationQueriesSchemes: ["weixin", "weixinULAPI", "wechat"],
}

调试技巧

在开发阶段,可以添加调试弹窗来追踪问题:

const DEBUG_MODE = true;

const debugAlert = (title: string, message: string) => {
  if (DEBUG_MODE) {
    Alert.alert(`[调试] ${title}`, message);
  }
};

// 在关键步骤添加调试
debugAlert("开始分享", `AppID: ${WECHAT_APP_ID}`);
debugAlert("注册结果", `registered: ${registered}`);
debugAlert("微信安装检查", `isInstalled: ${isInstalled}`);

总结

在 Expo 项目中接入微信分享的关键点:

  1. 使用 Config Plugin 配置原生设置,而不是手动修改原生代码
  2. 三端配置一致 是成功的关键(微信开放平台、App、服务器)
  3. Universal Link 配置正确且可访问
  4. 必须使用 EAS Build 构建的版本测试,Expo Go 不支持原生模块

希望这篇文章能帮助你顺利接入微信分享功能!如有问题欢迎评论区交流。


参考资料:

Luban 2 Flutter:一行代码在 Flutter 开发中实现图片压缩功能

Luban 2 Flutter —— 高效简洁的 Flutter 图片压缩插件,像素级还原微信朋友圈压缩策略。

📑 目录

📖 项目描述

开源地址:Gitee | Github

目前做 App 开发总绕不开图片这个元素。但是随着手机拍照分辨率的提升,图片的压缩成为一个很重要的问题。单纯对图片进行裁切,压缩已经有很多文章介绍。但是裁切成多少,压缩成多少却很难控制好,裁切过头图片太小,质量压缩过头则显示效果太差。

于是自然想到 App 巨头"微信"会是怎么处理,Luban(鲁班)就是通过在微信朋友圈发送近100张不同分辨率图片,对比原图与微信压缩后的图片逆向推算出来的压缩算法。

因为是逆向推算,效果还没法跟微信一模一样,但是已经很接近微信朋友圈压缩后的效果,具体看以下对比!

本库是 LubanFlutter 版本,使用 TurboJPEG 进行高性能图片压缩,提供简洁易用的 API 和接近微信朋友圈的压缩效果。

📊 效果与对比

图片类型 原图(分辨率, 大小) Luban(分辨率, 大小) Wechat(分辨率, 大小)
标准拍照 3024×4032, 5.10MB 1440×1920, 305KB 1440×1920, 303KB
高清大图 4000×6000, 12.10MB 1440×2160, 318KB 1440×2160, 305KB
2K 截图 1440×3200, 2.10MB 1440×3200, 148KB 1440×3200, 256KB
超长记录 1242×22080, 6.10MB 758×13490, 290KB 744×13129, 256KB
全景横图 12000×5000, 8.10MB 1440×600, 126KB 1440×600, 123KB
设计原稿 6000×6000, 6.90MB 1440×1440, 263KB 1440×1440, 279KB

🔬 核心算法特性

本库采用自适应统一图像压缩算法 (Adaptive Unified Image Compression),通过原图的分辨率特征,动态应用差异化策略,实现画质与体积的最优平衡。

智能分辨率决策

  • 高清基准 (1440p):默认以 1440px 作为短边基准,确保在现代 2K/4K 屏幕上的视觉清晰度
  • 全景墙策略:自动识别超大全景图(长边 >10800px),锁定长边为 1440px,保留完整视野
  • 超大像素陷阱:对超过 4096万像素的超高像素图自动执行 1/4 降采样处理
  • 长图内存保护:针对超长截图建立 10.24MP 像素上限,通过等比缩放防止 OOM

自适应比特率控制

  • 极小图 (<0.5MP):几乎不进行有损压缩,防止压缩伪影
  • 高频信息图 (0.5-1MP):提高编码质量,补偿分辨率损失
  • 标准图片 (1-3MP):应用平衡系数,对标主流社交软件体验
  • 超大图/长图 (>3MP):应用高压缩率,显著减少体积

健壮性保障

  • 膨胀回退:压缩后体积大于原图时,自动透传原图,确保绝不"负优化"
  • 输入防御:妥善处理极端分辨率输入(0、负数、1px 等),防止崩溃

📦 安装

pubspec.yaml 文件中添加依赖:

dependencies:
  luban: ^2.0.1

然后运行:

flutter pub get

💻 使用

压缩单张图片

使用 File 对象

import 'dart:io';
import 'package:luban/luban.dart';

Future<void> compressImage() async {
  final file = File('/path/to/image.jpg');
  final result = await Luban.compress(file);
  
  if (result.isSuccess) {
    final compressionResult = result.value;
    print('压缩完成');
    print('原图大小: ${compressionResult.originalSizeKb} KB');
    print('压缩后大小: ${compressionResult.compressedSizeKb} KB');
    print('压缩率: ${(compressionResult.compressionRatio * 100).toStringAsFixed(1)}%');
    print('输出文件: ${compressionResult.file.path}');
  } else {
    print('压缩失败: ${result.error}');
  }
}

使用字符串路径

import 'package:luban/luban.dart';

Future<void> compressImage() async {
  final result = await Luban.compressPath('/path/to/image.jpg');
  
  result.fold(
    (error) => print('压缩失败: $error'),
    (compressionResult) {
      print('压缩完成,大小: ${compressionResult.compressedSizeKb} KB');
      print('输出文件: ${compressionResult.file.path}');
    },
  );
}

指定输出文件

import 'dart:io';
import 'package:luban/luban.dart';

Future<void> compressImage() async {
  final inputFile = File('/path/to/image.jpg');
  final outputFile = File('/path/to/output/compressed.jpg');
  
  final result = await Luban.compressToFile(inputFile, outputFile);
  
  if (result.isSuccess) {
    final compressionResult = result.value;
    print('压缩完成,文件已保存到: ${compressionResult.file.path}');
  }
}

指定输出目录

import 'dart:io';
import 'package:luban/luban.dart';

Future<void> compressImage() async {
  final inputFile = File('/path/to/image.jpg');
  final outputDir = Directory('/path/to/output');
  
  final result = await Luban.compress(inputFile, outputDir: outputDir);
  
  if (result.isSuccess) {
    final compressionResult = result.value;
    print('压缩完成,文件已保存到: ${compressionResult.file.path}');
  }
}

批量压缩图片

批量压缩返回 Result<BatchCompressionResult>,需要先检查成功或失败状态,然后访问 BatchCompressionResult 获取所有图片的压缩结果。

使用文件列表

import 'dart:io';
import 'package:luban/luban.dart';

Future<void> compressBatchImages() async {
  final files = [
    File('/path/to/image1.jpg'),
    File('/path/to/image2.jpg'),
    File('/path/to/image3.jpg'),
  ];
  
  final result = await Luban.compressBatch(files);
  
  if (result.isSuccess) {
    final batchResult = result.value;
    print('批量压缩完成');
    print('总数: ${batchResult.total}');
    print('成功: ${batchResult.successCount}');
    print('失败: ${batchResult.failureCount}');
    
    for (final item in batchResult.items) {
      if (item.isSuccess) {
        final compressionResult = item.result.value;
        print('${item.originalPath}: ${compressionResult.compressedSizeKb} KB');
      } else {
        print('${item.originalPath}: 压缩失败 - ${item.result.error}');
      }
    }
  } else {
    print('批量压缩失败: ${result.error}');
  }
}

使用路径列表

import 'package:luban/luban.dart';

Future<void> compressBatchImages() async {
  final paths = [
    '/path/to/image1.jpg',
    '/path/to/image2.jpg',
    '/path/to/image3.jpg',
  ];
  
  final result = await Luban.compressBatchPaths(paths);
  
  result.fold(
    (error) => print('批量压缩失败: $error'),
    (batchResult) {
      print('批量压缩完成,成功 ${batchResult.successCount}/${batchResult.total} 张');
      
      for (final compressionResult in batchResult.successfulResults) {
        print('${compressionResult.file.path}: ${compressionResult.compressedSizeKb} KB');
      }
    },
  );
}

批量压缩并指定输出目录

import 'dart:io';
import 'package:luban/luban.dart';

Future<void> compressBatchImages() async {
  final files = [
    File('/path/to/image1.jpg'),
    File('/path/to/image2.jpg'),
  ];
  final outputDir = Directory('/path/to/output');
  
  final result = await Luban.compressBatch(files, outputDir: outputDir);
  
  if (result.isSuccess) {
    final batchResult = result.value;
    print('批量压缩完成,成功 ${batchResult.successCount} 张');
    
    for (final compressionResult in batchResult.successfulResults) {
      print('压缩文件: ${compressionResult.file.path}');
    }
  } else {
    print('批量压缩失败: ${result.error}');
  }
}

答题者心态

维克多·弗兰克在《活出生命的意义》中写过这么一段话:

我们不应该问“人生的意义是什么”,而应该意识到,“我们才是那个被生活提问的人”。

这句话极具嚼劲。因为「人生的意义是什么?」这个问题太正常、太顺口了,以至于我们忽略了它背后的假设:我们默认自己是索取者,认为意义藏在某处,等待着谁来给我们一个满意的答案。

抱着这种心态,我们很容易在缺乏「现成意义」支撑时感到虚无,甚至用一生去等待那个可能永远不会出现的答案。

但如果我们反过来想:生活才是那个提问者,而我们是答题人,一切就变得具体而清晰。生活的每一天、每一小时,通过我们遇到的具体处境——无论是工作的挑战、亲人的离去,还是平淡琐碎的日常——都在向我们抛出问题。

我们是努力作答,还是潦草应付,甚至拒绝交卷?这些都是我们的答案,而人生的意义,或许就藏在这些具体的答案里。

站在提问者视角,我们期待的意义往往是宏大抽象的;但作为答题者,意义是具体的,且千人千面,每一刻的考题都不同:

  • 上班累了一天,回家还要辅导孩子功课,这题怎么解?
  • 晚饭后有一堆碗要洗,但只想躺着刷手机,这题又怎么解?

生活没有标准答案,就像每个人的指纹不同,生活给每个人的考题也不同。所谓的「人生的意义」,不是靠脑袋想出来的,而是靠手脚做出来的。我们通过承担责任、做出选择,来书写回应。

既然是考试,就难免遇到难题。如果缺乏答题者心态,就很容易抱怨:「为什么是我?这种事为什么会发生在我身上?」

但一个优秀的答题者,会利用难题升级自己。塔勒布在《反脆弱》这本书中提出了一个概念:反脆弱(Antifragile)。与仅仅能抵抗冲击的「强韧」不同,反脆弱还能从压力、混乱和不确定性中获益。

前阵子,我在一件小事上体会到了这种心态的妙用。除了博客,我还有一个 Telegram Channel。原本只是发些碎碎念,结果招来了一大堆 SPAM(垃圾评论)。实在太烦,就关了评论,后来觉得还是需要互动,于是又开了,SPAM 自然如期而至。但这次,我决定换个解法。我把删除 SPAM 这个行为设定为一个 Trigger:每删一条垃圾评论,我就深呼吸一次,做一次几秒钟的微冥想。

结果很神奇,我不仅不讨厌 SPAM 了,甚至还有点期待它们的出现。这其实就是《福格行为模型》中提到的珍珠习惯:像蚌将沙粒包裹成珍珠一样,将负面的烦恼转化为积极行为的提示。通过这些小事磨练解题能力,等到人生的大题出现时,我们才能在心态上有所准备。

如果把「答题者心态」贯彻到底,人生会变成什么样?迈克·A·辛格在《臣服实验》中给出了示范。为了摆脱内心喋喋不休的「小我」,他制定了一个激进的规则:不再听从个人好恶的指挥,全然接受生活给出的任务。

如果生活在他面前呈现出某个机会,而他拒绝的唯一理由是「我不喜欢」或「这会打扰我的冥想」,那么他就必须放下个人偏好,接受这个任务。

这些任务就是生活递给他的一张张考卷。比如,有人请他帮忙盖房子。迈克本能地想拒绝,因为这破坏了他的隐修,但他想起了规则,于是答应了。接着,更多的人找上门。尽管他只想静静冥想,但他选择顺从生命的安排。

奇妙的是,这种看似违背初衷的行为,让他从对「空性」的执着中走了出来,在具体的劳动中磨练了心性。他发现:真正的灵性不是逃避世界,而是在做任何事时都保持全神贯注和不执着。

这样做还有一个巨大的红利:极度减少内耗。你不再需要在「想做」和「不想做」之间来回拉锯,只是专注于「把眼前的题答好」。

这种心态上升到哲学高度,便是斯多葛学派的 Amor Fati(热爱命运)。这是一种面对生活中一切遭遇的终极态度:不仅是接受,更是拥抱,甚至热爱。罗马皇帝、斯多葛哲学家马可·奥勒留在《沉思录》中这么说道:

普通人像一支蜡烛,遇到强风(逆境)就会被吹灭;而践行 Amor Fati 的人,则像一团烈火。 无论你往这团火里扔什么——木头、纸张,甚至是垃圾(困难、失败、悲剧)——火都会吞噬它,将其转化为自身的光和热。

这意味着,发生在你身上的每一件事,无论好坏,都是你成长的燃料。当我们不再执着于向生活索要一个标准答案,而是开始认真回应每一次提问时,焦虑就消失了,取而代之的是一种踏实的掌控感。

告别“可移植汇编”:我已让 Swift 在 MCU 上运行七年

在苹果官方正式开启嵌入式支持之前,Andy Liu 和他的 MadMachine 团队就已经在这个领域深耕多年。他们认为,在功能日益复杂的开发场景中,Swift 的现代语言特性将展现出巨大的优势。在数年前便选择了一套与社区主流不同的理念与技术路线。 我邀请 Andy 分享他们过去几年在 Swift 嵌入式开发中的实战经历分享出来。这既是一份宝贵的历史记录,也希望能为社区提供一个不一样的思考维度。

iOS应用(App)生命周期、视图控制器(UIViewController)生命周期和视图(UIView)生命周期

清晰的理解它们能帮你更好地管理应用状态和资源。

一、iOS 应用(App)生命周期

应用生命周期描述了 App 从启动到终止的整个过程,由UIApplicationDelegate(应用代理)来管理。

核心阶段与代理方法(按执行顺序)

import UIKit

@UIApplicationMain

class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    // 1. App启动完成(最核心的入口)
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

        print("应用启动完成 - didFinishLaunchingWithOptions")

        // 通常在这里初始化根视图控制器、配置全局设置等

        return true

    }


    // 2. App即将进入前台(还未激活,可做界面刷新)
    func applicationWillEnterForeground(_ application: UIApplication) {
    
        print("即将进入前台 - applicationWillEnterForeground")

    }


    // 3. App已进入前台并激活(用户可交互)
    func applicationDidBecomeActive(_ application: UIApplication) {

        print("已激活 - applicationDidBecomeActive")

        // 恢复定时器、重新开始播放音频、刷新数据等

    }

    // 4. App即将进入后台(用户按Home键/切换App)
    func applicationWillResignActive(_ application: UIApplication) {

        print("即将失活 - applicationWillResignActive")

        // 暂停定时器、保存临时数据、暂停音频播放等

    }


    // 5. App已进入后台
    func applicationDidEnterBackground(_ application: UIApplication) {

        print("已进入后台 - applicationDidEnterBackground")

        // 持久化数据、释放不必要的资源(有大约5秒时间,耗时操作需申请后台任务)

    }

    // 6. App即将终止(仅在后台时可能触发,如系统回收内存)
    func applicationWillTerminate(_ application: UIApplication) {

        print("即将终止 - applicationWillTerminate")

        // 最终的资源清理、数据保存

    }

}

关键说明

  • 启动流程:用户点击 App 图标 → 系统加载可执行文件 → 调用didFinishLaunchingWithOptions → 显示界面 → 进入活跃状态。
  • 后台与前台切换:活跃 → 失活(WillResignActive)→ 后台(DidEnterBackground)→ 前台(WillEnterForeground)→ 活跃(DidBecomeActive)。
  • 终止:后台状态下系统回收内存,触发applicationWillTerminate(若 App 在前台,直接终止,不触发此方法)。

二、UIViewController 生命周期

视图控制器是管理 UIView 的核心,其生命周期围绕视图的创建、显示、隐藏、销毁展开,是 iOS 开发中最常接触的生命周期。

核心方法(按执行顺序)

import UIKit

class ViewController: UIViewController {

    // 1. 初始化(创建VC对象)
    init?(coder: NSCoder) {
        super.init(coder: coder)
        print("1. 初始化 - init")
        // 初始化非UI相关的属性
    }

    // 2. 加载视图(首次访问view属性时触发)
    override func loadView() {
        super.loadView()
        print("2. 加载视图 - loadView")
        // 手动创建view(若不重写,系统会加载storyboard/xib的view)
        self.view = UIView(frame: UIScreen.main.bounds)
        self.view.backgroundColor = .white
    }

    // 3. 视图加载完成(view已创建完成)
    override func viewDidLoad() {
        super.viewDidLoad()
        print("3. 视图加载完成 - viewDidLoad")
        // 初始化UI控件、绑定数据、添加监听(只执行一次)
    }

    // 4. 视图即将布局子视图(view的bounds变化时触发,如旋转屏幕)
    override func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()
        print("4. 视图即将布局子视图 - viewWillLayoutSubviews")
        // 调整控件布局(执行多次)
    }

    // 5. 视图已布局子视图
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        print("5. 视图已布局子视图 - viewDidLayoutSubviews")
        // 获取控件最终的frame(执行多次)
    }

    // 6. 视图即将显示在屏幕上(每次显示都触发,如push/pop后重新显示)
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        print("6. 视图即将显示 - viewWillAppear")
        // 刷新数据、开始动画、注册通知等
    }

    // 7. 视图已显示在屏幕上
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        print("7. 视图已显示 - viewDidAppear")
        // 启动定时器、请求网络数据、播放视频等
    }

    // 8. 视图即将从屏幕上消失
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        print("8. 视图即将消失 - viewWillDisappear")
        // 暂停动画、移除通知、保存数据等
    }

    // 9. 视图已从屏幕上消失
    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        print("9. 视图已消失 - viewDidDisappear")
        // 释放不必要的资源(如图片缓存)
    }

    // 10. 内存警告(系统内存不足时触发)
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        print("10. 内存警告 - didReceiveMemoryWarning")
        // 释放缓存、非必要的视图等
    }

    // 11. 视图控制器销毁(deinit)
    deinit {
        print("11. 视图控制器销毁 - deinit")
        // 最终的资源释放(如移除监听、取消网络请求)
    }
}

关键说明

  • 核心流程:初始化 → 加载视图 → 视图加载完成 → 布局子视图 → 即将显示 → 已显示 → 即将消失 → 已消失 → 销毁。
  • viewDidLoad:只执行一次,适合做一次性初始化;viewWillAppear/viewDidAppear:每次显示都执行,适合刷新动态数据。
  • 内存警告didReceiveMemoryWarning中需主动释放非必要资源,避免 App 被系统杀死。
  • deinit:只有当 VC 的引用计数为 0 时才会触发,需确保无循环引用(如闭包未捕获 self 为 weak/unowned)。

三、UIView 生命周期

UIView 的生命周期依附于视图控制器,核心是 “创建 - 布局 - 绘制 - 销毁”,重点关注布局和绘制相关方法。

核心阶段与方法

import UIKit

class CustomView: UIView {

    // 1. 初始化(创建View)
    override init(frame: CGRect) {
        super.init(frame: frame)
        print("1. View初始化 - init(frame:)")
        // 设置默认属性(如背景色、圆角)
        self.backgroundColor = .lightGray
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        print("1. View初始化 - init(coder:)")
    }

    // 2. 准备布局(iOS 6+,替代autoresizingMask)
    override func prepareForLayout() {
        super.prepareForLayout()
        print("2. 准备布局 - prepareForLayout")
        // 布局前的准备工作(如设置约束优先级)
    }

    // 3. 布局子视图(bounds变化时触发,如frame、center修改)
    override func layoutSubviews() {
        super.layoutSubviews()
        print("3. 布局子视图 - layoutSubviews")
        // 手动调整子视图frame(若不用AutoLayout)
        for subview in self.subviews {
            subview.center = self.center
        }
    }

    // 4. 绘制内容(首次显示/setNeedsDisplay()触发)
    override func draw(_ rect: CGRect) {
        super.draw(rect)
        print("4. 绘制内容 - draw(_:)")
        // 手动绘制图形(如绘制线条、文字)
        let context = UIGraphicsGetCurrentContext()
        context?.setStrokeColor(UIColor.red.cgColor)
        context?.stroke(CGRect(x: 10, y: 10, width: 100, height: 100))
    }

    // 5. 即将添加到父视图
    override func willMove(toSuperview newSuperview: UIView?) {
        super.willMove(toSuperview: newSuperview)
        print("5. 即将添加到父视图 - willMove(toSuperview:)")
        // 父视图变化前的处理
    }

    // 6. 已添加到父视图
    override func didMoveToSuperview() {
        super.didMoveToSuperview()
        print("6. 已添加到父视图 - didMoveToSuperview")
        // 父视图变化后的处理(如根据父视图调整自身大小)
    }

    // 7. 即将添加到窗口
    override func willMove(toWindow newWindow: UIWindow?) {
        super.willMove(toWindow: newWindow)
        print("7. 即将添加到窗口 - willMove(toWindow:)")
    }

    // 8. 已添加到窗口
    override func didMoveToWindow() {
        super.didMoveToWindow()
        print("8. 已添加到窗口 - didMoveToWindow")
        // 只有添加到window后,View才会真正显示在屏幕上
    }

    // 9. 销毁(deinit)
    deinit {
        print("9. View销毁 - deinit")
        // 释放View相关资源(如移除子视图、取消动画)
    }
}

关键说明

  • layoutSubviews:最常用的方法,每次 View 的尺寸变化都会触发,适合手动调整子视图布局(若使用 AutoLayout,系统会自动处理,无需重写)。
  • draw(_:) :仅在需要手动绘制内容时重写,避免在其中做耗时操作(会影响渲染性能);调用setNeedsDisplay()可触发重新绘制。
  • Window 关联:View 只有添加到UIWindow(应用的主窗口)后,才会被渲染并显示在屏幕上;didMoveToWindow是 View 真正 “可见” 的标志。
  • 销毁:当 View 从父视图移除且无强引用时,deinit触发,需确保子视图也被正确释放。

总结

  1. 应用生命周期:全局层面,管理 App 从启动到终止的状态,核心是UIApplicationDelegate的代理方法,关注前台 / 后台切换和资源保存。
  2. 视图控制器生命周期:页面层面,核心是viewDidLoad(一次性初始化)、viewWillAppear(每次显示刷新)、deinit(资源释放),是业务逻辑的主要载体。
  3. 视图生命周期:控件层面,依附于 VC,核心是layoutSubviews(布局)和draw(_:)(绘制),关注控件尺寸调整和视觉渲染。

三者的关联:App 启动后创建根 VC → VC 创建并加载 View → View 添加到 Window 显示 → App 进入前台活跃状态;App 进入后台时,VC 的 View 会被隐藏,资源可按需释放。

Swift 6.2 列传(第十六篇):阿朱的“易容术”与阿紫的“毒药测试”

在这里插入图片描述

摘要:在 Swift 6.2 的并发江湖中,我们迎来了两项截然不同的新功能:一项是关于极度精妙的文本侦查术(SE-0448 正则表达式向后查找断言),另一项则是关于面对应用崩溃时的从容不迫(ST-0008 退出测试)。大熊猫侯佩将与阿朱、阿紫这对姐妹花,共同演绎这冰火两重天的技术奥秘。

0️⃣ 🐼 序章:雁门关前的技术难题

雁门关,数据流与现实交错的虚拟战场。

大熊猫侯佩正对着一块全息屏幕发呆,屏幕上是无数条交易记录,他正努力寻找他藏匿的竹笋基金。他用手摸了摸自己的头顶,确定了头绝对没有秃之后,才稍微心安。

他身旁站着一位温柔婉约的绿衣女子,正是阿朱。阿朱以易容术闻名江湖,擅长在纷乱的文本中寻找和伪装信息,她的心愿是天下太平,性格宽厚善良。

在这里插入图片描述

“侯大哥,”阿朱指着一堆交易记录说,“我想找到所有以 金币符号 $ 结算的价格,但我只想匹配出后面的数字,而不要把那个 $符号也匹配进去。我要用这些数字去结算账单,符号留着下次易容用。”

在本次大模型中,您将学到如下内容:

  • 0️⃣ 🐼 序章:雁门关前的技术难题
  • 1️⃣ 🔎 阿朱的易容术:Regex lookbehind assertions
  • 2️⃣ 🧪 阿紫的毒药测试:Exit Tests 的“置之死地” (ST-0008)
    • #expect(processExitsWith:) 的安全结界
  • 3️⃣ 🎁 尾声:崩溃现场的“遗物”与下一章的伏笔

侯佩为难地挠了挠头:“以前的 Regex(正则表达式),要么就全部匹配进去,要么就得用复杂的捕获组再分离。要想实现‘只看前因,不取前因’,简直难如登天啊!”

在这里插入图片描述


1️⃣ 🔎 阿朱的易容术:Regex lookbehind assertions

阿朱的问题,正是 SE-0448 所要解决的:向后查找断言(lookbehind assertions)

传统的正则表达式,可以轻松地实现“向前看”(Lookahead),例如 A(?=B),匹配 A,但前提是 A 后面跟着 B。

在这里插入图片描述

而现在,Swift 6.2 赋予了我们 “向后看” 的能力,即 (?<=A)B:匹配 B,但前提是 B 前面紧跟着 A。最关键的是,A(前置条件)不会被纳入最终的匹配结果中。

侯佩拿起代码卷轴,为阿朱演示了这招“庖丁解牛”般的绝技:

let string = "Buying a jacket costs $100, and buying shoes costs $59.99."

// (?<=\$): 向后查找断言,确认当前位置前面紧跟着一个 $ 符号。
// \d+     : 匹配至少一个数字(价格的整数部分)。
// (?:\.\d{2})?: 匹配可选的小数点和小数部分(?: 是非捕获组)。
let regex = /(?<=\$)\d+(?:\.\d{2})?/ 

for match in string.matches(of: regex) {
    // 最终输出的 match.output 只有数字,不包含 $ 符号
    print(match.output) 
}

// 输出:
// 100
// 59.99

“看到了吗,阿朱姑娘?”侯佩得意洋洋,“这个 (?<=$) 就是你的易容术精髓。它帮你确认了身份(前面必须是金币),但在匹配结果中,它却完美地把自己隐藏了起来,片叶不沾身!

在这里插入图片描述

阿朱喜出望外:“太妙了!这样我就可以精准地提取数据,再也不用担心多余的符号来捣乱了!”

2️⃣ 🧪 阿紫的毒药测试:Exit Tests 的“置之死地” (ST-0008)

就在侯佩和阿朱沉浸在正则表达式的精妙中时,一阵刺鼻的硫磺味突然袭来!

另一位身着紫衣的少女,阿紫,从烟雾中走了出来。阿紫的特点是心狠手辣,喜欢用毒,而且热衷于测试“极限”

在这里插入图片描述

“姐姐,你在玩这么幼稚的游戏?”阿紫轻蔑一笑,“我的任务才刺激。我要测试我最新的**‘鹤顶红’代码**,确保它能让整个应用彻底崩溃并退出!”

侯佩吓得连退三步:“你要测试崩溃?阿紫姑娘,你知道这意味着什么吗?应用崩溃,测试系统也会跟着崩溃啊!这叫一锅端!”

在这里插入图片描述

阿紫的测试目标,正是那些会触发 precondition()fatalError() 导致进程退出的代码。

struct Dice {
    // 掷骰子功能
    func roll(sides: Int) -> Int {
        // 🚨 前提条件:骰子面数必须大于零!
        // 如果 sides <= 0,程序将立即崩溃退出!
        precondition(sides > 0) 
        return Int.random(in: 1...sides)
    }
}

“以前,我们要么不能测,要么就得用各种奇技淫巧来捕获这种‘致命错误’。”侯佩擦着汗说,“但现在 Swift Testing 带来了 ST-0008:Exit Tests,让我们能优雅地‘置之死地而后生’!”

在这里插入图片描述

#expect(processExitsWith:) 的安全结界

Swift 6.2 引入了 #expect(processExitsWith:),它就像是一个安全结界,允许我们在隔离的子进程中执行可能导致崩溃的代码,然后捕获并验证这个退出行为。

@Test func invalidDiceRollsFail() async throws {
    let dice = Dice()

    // 🛡️ 关键:使用 #expect 包裹,并等待结果
    await #expect(processExitsWith: .failure) {
        // 在这里,roll(sides: 0) 会导致隔离的子进程崩溃退出
        let _ = dice.roll(sides: 0)
    }
    
    // 如果子进程如期以 .failure 状态退出,则测试通过。
    // 如果它没有崩溃,或者崩溃状态不对,则测试失败。
}

🔍 异步执行的关键:await 注意,这里必须使用 await。这是因为在幕后,测试框架必须启动一个专用的、独立的进程来执行危险代码。它会暂停当前测试,直到子进程运行完毕并返回退出状态。这才是真正的隔离测试

在这里插入图片描述

阿紫满意地拍了拍手:“现在我的毒药(代码)终于可以在实验室(测试环境)里安全地爆炸了!我不仅可以测试它会死(failure),还可以测试它死得很安详(success)或其他退出状态。”

3️⃣ 🎁 尾声:崩溃现场的“遗物”与下一章的伏笔

侯佩摸了摸自己的头发,确认没有被阿紫的毒气熏掉,然后问道:“阿紫姑娘,你这个毒药测试虽然厉害,但是你有没有想过一个问题?”

在这里插入图片描述

“什么问题?”阿紫挑了挑眉。

“如果这个 roll(sides: 0) 崩溃了,但它在崩溃前,生成了一个关键的调试日志文件,或者一个记录了现场数据的**‘遗物’**,你能不能把这个遗物附着到测试报告里?”

阿紫一愣:“不能。测试报告里只显示了‘崩溃了’这个结果,但我不知道崩溃前骰子(程序)到底在想什么!我需要那个遗物来分析我的毒药配方!”

在这里插入图片描述

阿朱也附和道:“是啊,侯大哥。就像我易容时,如果失败了,我希望在失败的记录旁边,能附上一张当时的照片,这样下次就知道是哪个环节出了错。”

侯佩微微一笑,从怀里掏出了一张写着 ST-0009 的秘籍:“两位姑娘,不必烦恼。下一章,Swift Testing 就能帮你们把这些日志、数据和现场文件,像附着‘随身物品’一样,直接捆绑到失败的测试报告上。这招就叫……”

在这里插入图片描述

(欲知后事如何,且看下回分解:Swift Testing: Attachments —— 如何将崩溃现场的证据(日志、截图、数据文件)直接附着到测试报告上,让 Bug 无所遁形。)

拒绝“假死”:为何上滑关闭是测试大忌?揭秘 iOS 真实 OOM 触发指南

在这里插入图片描述

☔️ 引子

在赛博都市“新硅谷”(Neo-Silicon Valley)的第 1024 层地下室里,资深 iOS 赏金猎人——老李(Old Li),正盯着全息屏幕上一行行红色的报错代码发愁。他嘴里叼着一根早已熄灭的合成电子烟,眉头皱得能夹死一只纳米苍蝇。

旁边漂浮着的 AI 助手“小白”发出了机械的合成音:“警报,内存溢出测试失败。目标 App 依然像个赖皮一样活着。”

在这里插入图片描述

老李叹了口气:“这年头的 App,一个个都练成了‘金刚不坏之身’。我想测一下后台上传功能在**低内存(Low RAM)**情况下的表现,结果这破手机内存大得像海一样,怎么都填不满。”

“老板,直接在 App Switcher(多任务切换器)里把它划掉不就行了?”小白天真地问道。

**在本篇博文中,您将学到如下内容: **

  • ☔️ 引子
  • 🕵️‍♂️ 第一章:真死还是假死?这是一个问题
  • 🔮 第二章:失传的“清内存大法”
  • 🛠️ 步骤一:召唤“假肢”(Assistive Touch)
  • 🧨 步骤二:准备“关机仪式”
  • 🩸 步骤三:致命一击(The Purge)
  • 🧟‍♂️ 第三章:为什么我们需要这种“假死”?
  • ⚖️ 第四章:技术验尸——“被杀”与“自杀”的区别
  • 🎬 终章:深藏功与名

老李冷笑一声,敲了一下小白的金属外壳:“图样图森破!手滑杀掉那是‘斩立决’,系统因内存不足杀掉那是‘自然死亡’。对于后台任务来说,这区别可大了去了。要想骗过死神,我们得用点‘阴招’。”

老李从积灰的档案袋里掏出一份绝密文档——《iOS 内存清空指南》。

在这里插入图片描述


🕵️‍♂️ 第一章:真死还是假死?这是一个问题

最近老李接了个大活儿,要为一个 App 开发 Background Uploading(后台上传)功能。这活儿最棘手的地方在于:你得确保当系统因为 RAM constraints(内存限制)或其他不可抗力把你的 App 挂起甚至杀掉时,这上传任务还得能像“借尸还魂”一样继续跑。

要想测试这个场景,最直接的办法就是清空设备的 RAM memory。但这可不像在电脑上拔掉电源那么简单。

小白不解:“不就是上划杀进程吗?”

在这里插入图片描述

“错!”老李严肃地解释道,“打开 Task Switcher 然后强行关闭 App,这在系统眼里属于‘用户主动终止’。这就像是不仅杀了人,还顺手把复活点给拆了。而我们需要的是模拟 App 被系统‘挤’出内存,这才是真正的Forced out of memory。”

简而言之,我们需要制造一场完美的“意外”,让 App 以为自己只是因为太胖被系统踢了出去,而不是被用户嫌弃。


🔮 第二章:失传的“清内存大法”

幸运的是,在 iOS 的底层代码深处,藏着一个不为人知的“秘技”。这招能像灭霸打响指一样,瞬间清空 iOS 设备的 RAM memory,让你的 App 享受到和真实内存不足时一样的“暴毙”待遇。

老李按灭了烟头,开始向小白传授这套“还我漂漂拳”:

在这里插入图片描述

🛠️ 步骤一:召唤“假肢”(Assistive Touch)

如果你的测试机是全面屏(没有 Home 键),你得先搞个虚拟的。 “去 Settings → Accessibility → Touch → Enable Assistive Touch。”老李指挥道。

在这里插入图片描述

屏幕上瞬间浮现出一个半透明的小圆球。 “这就是通往内存地狱的钥匙。”

技术批注: 对于有实体 Home 键的老古董设备,这一步可以跳过。

🧨 步骤二:准备“关机仪式”

在这里插入图片描述

这一步需要一点手速,就像是在玩格斗游戏搓大招。 “听好了:Volume Up(音量加),Volume Down(音量减),然后死死按住 Power Button(电源键)!”

在这里插入图片描述

老李的手指在机身上飞舞,直到屏幕上出现了那个熟悉的“滑动来关机”界面。

🩸 步骤三:致命一击(The Purge)

“就是现在!”老李大喝一声。

在关机界面出现后,千万别滑那个关机条。点击刚才召唤出来的 Assistive Touch 小圆球,找到里面的 Home Button(主屏幕按钮),然后——长按它

在这里插入图片描述

一直按着,直到屏幕一闪,或者突然跳回输入密码的界面。

“恭喜你,”老李擦了擦额头的汗,“你刚刚成功把这台设备的 RAM memory 洗劫一空。现在,后台那些苟延残喘的 App 已经被系统无情地踢出了内存。”

在这里插入图片描述


🧟‍♂️ 第三章:为什么我们需要这种“假死”?

小白看着屏幕上被清理得干干净净的后台,数据流终于开始正常波动了。

“这就好比演习,”老李解释道,“当我们在开发那些依赖于 Background Resuming(后台恢复)的功能时——比如后台上传、下载,或者定位服务——模拟 Out of Memory 场景简直是救命稻草。”

在这里插入图片描述

最让老李爽的一点是,这个操作完全脱离了 Xcode。 “以前还要连着线看 Debugger,现在我可以把手机扔给隔壁 QA 部门那个只会吃薯片的测试员,告诉他:‘按这个秘籍操作,如果上传断了,就是你们的问题,如果没断,就是我的功劳。’”


⚖️ 第四章:技术验尸——“被杀”与“自杀”的区别

为了防止小白以后出去乱说,老李决定再深入科普一下其中的Hardcore原理。

在这里插入图片描述

一个被 Forced out of RAM 的 App,在用户眼里并没有完全死透。它依然会出现在 App Switcher 里,就像个植物人。更重要的是,任何已经注册的 Background Processes(后台进程,比如 NSURLSession 的后台任务)依然在系统的监管下继续运行。

  • 正常死亡(Low Memory): 当用户开了个吃内存的大游戏,或者你的 App 很久没用了,系统为了腾地儿,会把你的 App 从内存里踢出去。当用户再次点击图标时,App 会经历一次 Fresh Launch(冷启动),但系统会给机会让它处理之前没干完的后台活儿。
  • 非正常死亡(Force Close): 当你在多任务界面上滑杀掉 App 时,iOS 会判定:“这刁民不想让这个 App 活了。”于是,系统会大义灭亲,禁止该 App 继续在后台搞小动作。所有的上传、下载任务会被立即 Cancelled(取消)。

在这里插入图片描述

所以,只有用老李刚才那招“清内存大法”,才能真实模拟用户在刷抖音、玩原神导致内存不足时,你的 App 在后台是否还能坚强地把文件传完。


🎬 终章:深藏功与名

测试通过,全息屏幕上显示出了令人安心的绿色 SUCCESS 字样。

在这里插入图片描述

老李站起身,伸了个懒腰,骨头发出噼里啪啦的响声。“行了,小白,打包发布。今晚不用加班修 Bug 了。”

他看了一眼窗外新硅谷那绚烂而又冰冷的霓虹灯。在这个充满 Bug 和 Patch 的世界里,有时候,你必须学会如何正确地“杀死”你的 App,才能让它更好地活下去。

在这里插入图片描述

“记住,”老李走出门口前回头对小白说,“杀进程不是目的,目的是为了验证它有没有重生的勇气。

大门缓缓关闭,只留下那个悬浮的 Assistive Touch 按钮,在黑暗中微微闪烁,仿佛一只窥探内存深处的眼睛。

在这里插入图片描述

SSE Connect 数据解析详解

前言

SSE(Server-Sent Events) 是一种基于 HTTP 的服务器单向推送技术。相比 WebSocket 的双向通信,SSE 更轻量、实现更简单,非常适合服务器向客户端持续推送数据的场景。ChatGPT、Claude 等 AI 产品都使用 SSE 来实现流式输出。

本文以 iOS 客户端实现为例,详细讲解 SSE 数据的接收与解析过程。


一、完整示例:一个 SSE 事件从发送到接收的全过程

服务器发送的数据

event: chunk
id: 1
data: {"content":"Hello"}

⚠️ 注意:最后有一个空行,这是事件结束的标志!


第一步:服务器发送,网络传输

服务器发送的原始字节流:

e v e n t :   c h u n k \n i d :   1 \n d a t a :   { . . . } \n \n

问题:网络传输时,数据可能被分成多个块到达客户端。

假设网络把数据分成了 3 块:

数据块 内容
块 1 "event: chu"
块 2 "nk\nid: 1\nda"
块 3 "ta: {\"content\":\"Hello\"}\n\n"

第二步:行解析器处理(OKGrowthUTF8LineParser)

2.1 收到块 1:"event: chu"

┌────────────────────────────────────────────────────────────┐
│ 输入: "event: chu"                                         │
│                                                            │
│ 处理过程:                                                   │
│   1. 缓冲区当前为空: ""                                     │
│   2. 合并: "" + "event: chu" = "event: chu"                │
│   3. 扫描换行符: 没找到 \n                                  │
│   4. 没有完整行,全部存入缓冲区                              │
│                                                            │
│ 缓冲区: "event: chu"                                       │
│ 输出: []  ← 空数组,没有完整行                              │
└────────────────────────────────────────────────────────────┘

2.2 收到块 2:"nk\nid: 1\nda"

┌────────────────────────────────────────────────────────────┐
 输入: "nk\nid: 1\nda"                                      
                                                            
 处理过程:                                                   
   1. 缓冲区当前: "event: chu"                               
   2. 合并: "event: chu" + "nk\nid: 1\nda"                  
         = "event: chunk\nid: 1\nda"                        
   3. 扫描换行符:                                            
      - 位置 12 找到 \n  提取 "event: chunk"               
      - 位置 18 找到 \n  提取 "id: 1"                      
      - "da" 后面没有 \n,存入缓冲区                         
                                                            
 缓冲区: "da"                                               
 输出: ["event: chunk", "id: 1"]                            
└────────────────────────────────────────────────────────────┘

2.3 收到块 3:"ta: {\"content\":\"Hello\"}\n\n"

┌────────────────────────────────────────────────────────────┐
 输入: "ta: {\"content\":\"Hello\"}\n\n"                    
                                                            
 处理过程:                                                   
   1. 缓冲区当前: "da"                                       
   2. 合并: "da" + "ta: {...}\n\n"                          
         = "data: {\"content\":\"Hello\"}\n\n"              
   3. 扫描换行符:                                            
      - 位置 27 找到 \n  提取 "data: {...}"                
      - 位置 28 找到 \n  提取 ""   空行!                  
                                                            
 缓冲区: ""   清空                                         
 输出: ["data: {\"content\":\"Hello\"}", ""]                
└────────────────────────────────────────────────────────────┘

2.4 行解析器总结

经过 3 次数据块处理,行解析器依次输出:

次序 输出的完整行
块 1 后 [] (无)
块 2 后 ["event: chunk", "id: 1"]
块 3 后 ["data: {...}", ""]

合计得到 4 行: "event: chunk", "id: 1", "data: {...}", ""


第三步:事件解析器处理(OKGrowthEventParser)

SSEClient 将行解析器输出的每一行,依次传给事件解析器。

3.1 解析第 1 行:"event: chunk"

┌────────────────────────────────────────────────────────────┐
│ 输入: "event: chunk"                                       │
│                                                            │
│ 处理过程:                                                   │
│   1. 行长度 > 0,不是空行                                   │
│   2. 查找冒号位置: 5                                        │
│   3. 字段名 = "event"                                      │
│   4. 字段值 = "chunk" (冒号后面,跳过空格)                   │
│   5. 字段名是 "event",存储 eventType                       │
│                                                            │
│ 当前状态:                                                   │
│   eventType = "chunk"  ✓                                   │
│   eventId   = ""                                           │
│   data      = ""                                           │
│                                                            │
│ 动作: 继续等待下一行                                        │
└────────────────────────────────────────────────────────────┘

3.2 解析第 2 行:"id: 1"

┌────────────────────────────────────────────────────────────┐
│ 输入: "id: 1"                                              │
│                                                            │
│ 处理过程:                                                   │
│   1. 行长度 > 0,不是空行                                   │
│   2. 查找冒号位置: 2                                        │
│   3. 字段名 = "id"                                         │
│   4. 字段值 = "1"                                          │
│   5. 字段名是 "id",存储 eventId                            │
│                                                            │
│ 当前状态:                                                   │
│   eventType = "chunk"  ✓                                   │
│   eventId   = "1"      ✓                                   │
│   data      = ""                                           │
│                                                            │
│ 动作: 继续等待下一行                                        │
└────────────────────────────────────────────────────────────┘

3.3 解析第 3 行:"data: {\"content\":\"Hello\"}"

┌────────────────────────────────────────────────────────────┐
 输入: "data: {\"content\":\"Hello\"}"                      
                                                            
 处理过程:                                                   
   1. 行长度 > 0,不是空行                                   
   2. 查找冒号位置: 4                                        
   3. 字段名 = "data"                                       
   4. 字段值 = "{\"content\":\"Hello\"}"                    
   5. 字段名是 "data",追加到 data                           
      (当前 data 为空,直接赋值)                              
                                                            
 当前状态:                                                   
   eventType = "chunk"                                     
   eventId   = "1"                                         
   data      = "{\"content\":\"Hello\"}"                   
                                                            
 动作: 继续等待下一行                                        
└────────────────────────────────────────────────────────────┘

3.4 解析第 4 行:"" (空行) ⚡

┌────────────────────────────────────────────────────────────┐
│ 输入: ""  (空行)                                           │
│                                                            │
│ 处理过程:                                                   │
│   1. 行长度 == 0,是空行!                                  │
│   2. ⚡ 空行触发事件分发!                                   │
│                                                            │
│ 当前状态 (即将分发):                                        │
│   eventType = "chunk"                                      │
│   eventId   = "1"                                          │
│   data      = "{\"content\":\"Hello\"}"                    │
│                                                            │
│ 执行 dispatchEvent():                                      │
│   1. 调用回调: onEvent("chunk", "1", "{...}")              │
│   2. 重置状态:                                              │
│      eventType = ""                                        │
│      eventId   = ""                                        │
│      data      = ""                                        │
│                                                            │
│ 动作: 🎯 触发回调!准备解析下一个事件                        │
└────────────────────────────────────────────────────────────┘

第四步:事件分发,回调业务层

┌────────────────────────────────────────────────────────────┐
│                      回调链                                 │
├────────────────────────────────────────────────────────────┤
│                                                            │
│  EventParser.dispatchEvent()                               │
│       │                                                    │
│       │  onEvent("chunk", "1", "{\"content\":\"Hello\"}")  │
│       ↓                                                    │
│  SSEClient.handleEvent()                                   │
│       │                                                    │
│       │  判断: eventType != "connected",不更新状态         │
│       │                                                    │
│       │  onTextChunk("chunk", "1", "{...}")                │
│       ↓                                                    │
│  MLNSSETool                                                │
│       │                                                    │
│       │  [onTextChunk addStringArgument:@"chunk"];         │
│       │  [onTextChunk addStringArgument:@"1"];             │
│       │  [onTextChunk addStringArgument:@"{...}"];         │
│       │  [onTextChunk callIfCan];                          │
│       ↓                                                    │
│  Lua 业务层                                                │
│       │                                                    │
│       │  onEvent("chunk", "1", '{"content":"Hello"}')      │
│       ↓                                                    │
│  业务代码处理                                               │
│       │                                                    │
│       │  local json = cjson.decode(data)                   │
│       │  print(json.content)  -- 输出: Hello               │
│       │  更新 UI 显示                                       │
│       ↓                                                    │
│  ✅ 完成!                                                  │
│                                                            │
└────────────────────────────────────────────────────────────┘

完整流程图(从服务器到 Lua)

服务器发送: "event: chunk\nid: 1\ndata: {...}\n\n"
                          
                           网络分块传输
┌─────────────────────────────────────────────────────────────┐
                     第一步:网络层                            
├─────────────────────────────────────────────────────────────┤
1: "event: chu"                                          
2: "nk\nid: 1\nda"                                       
3: "ta: {...}\n\n"                                       
└─────────────────────────────────────────────────────────────┘
                          
                           NSURLSession.didReceiveData
┌─────────────────────────────────────────────────────────────┐
                 第二步:行解析器                               
                 (UTF8LineParser)                            
├─────────────────────────────────────────────────────────────┤
1  []                                                   
2  ["event: chunk", "id: 1"]                            
3  ["data: {...}", ""]                                  
                                                             
  合计得到4行: `"event: chunk"`, `"id: 1"`, `"data: {...}"`, `""`  
└─────────────────────────────────────────────────────────────┘
                          
                           逐行传递
┌─────────────────────────────────────────────────────────────┐
                 第三步:事件解析器                             
                 (EventParser)                               
├─────────────────────────────────────────────────────────────┤
1"event: chunk"   eventType = "chunk"                 
2"id: 1"          eventId = "1"                       
3"data: {...}"    data = "{...}"                      
4""                触发 dispatchEvent()              
└─────────────────────────────────────────────────────────────┘
                          
                           onEvent 回调
┌─────────────────────────────────────────────────────────────┐
                     第四步:事件分发                           
├─────────────────────────────────────────────────────────────┤
  SSEClient.handleEvent("chunk", "1", "{...}")               
                                                            
  MLNSSETool.onTextChunk("chunk", "1", "{...}")              
                                                            
  Lua: onEvent("chunk", "1", '{"content":"Hello"}')          
                                                            
      业务代码: 解析 JSON,更新 UI                              
└─────────────────────────────────────────────────────────────┘
                          
                          
                     处理完成!


二、关键点总结

2.1 为什么需要行解析器?

网络数据分块到达,一行可能被拆成多块。行解析器用缓冲区解决这个问题。

2.2 为什么空行这么重要?

event: chunk     ← 存储 eventType,不触发
id: 1            ← 存储 eventId,不触发
data: {...}      ← 存储 data,不触发
                 ← ⚡ 只有空行才触发事件分发!

空行 = 事件结束的信号

2.3 一个事件的完整生命周期

阶段 输入 输出
网络传输 字节流 数据块
行解析器 数据块 文本行数组
事件解析器 文本行 event/id/data
空行触发 "" dispatchEvent()
回调链 event/id/data Lua onEvent

苹果开发者账号申请的痛点,包含新限制说明!

背景

上车AppStore必经之路,苹果开发者账号注册。简单盘点一下,申请苹果开发者痛点问题。

账号注册

正常的个人开发账号,基本上直接使用 126、163或者QQ邮箱都可以直接使用。

对于公司开发者账号来说,最近新增了限制条件:申请的邮箱必须为公司邮箱!

这一点限制是在最近申请公司开发账号遇到的问题,对于个人账号账号目前没有影响。[这里感谢粉丝贡献的情报。]

设备问题

设备问题主要是在Apple ID登录踩的坑。首当其冲的就是设备登录限制。

无解直接换新设备,不用想了。不然果子怎么卖的动新手机?

注册开发者的 Developer App,也需要更新到新版本。【有最低版本限制】不然果子怎么卖的动新手机?

9135238bf439b2f3a9611a0cfb5e7c8f.jpg

在注册开发者账号的过程中,切记不要更换设备,避免遇到各种奇奇怪怪的问题。也能最大程度的保保证,在注册流程不会被账号关联,避免提交代码就夭折。

信息验证问题

1.账号主体

对于公司层面的账号,场景最多的问题就是:

Q: 法人用个人账号注册了开发者,那么还可以用公司身份去注册么?

A: 其实是可以的,这一点已经咨询过了苹果客服。因为对于主体而言,一个是邓白氏编码对应的账号,一个是个人身份证对应的账号。所以本质上也是2个独立的主体。

2.忘记老账户

对于小部分一些人来说,可能之前注册了开发者流程,也提交的了相应信息。在最后付费环境,考虑到暂时没有产品提交又或者不知道了注册了干嘛,就把账号搁置了。

那这种情况是最头疼的,对于苹果而言信息已经被占用。如果无法使用首次注册开发者的账号,重新进行开发者验证。那么将陷入无法注册的死循环。简而言之:打苹果客服,也只能告诉你用老账户。如果忘记密码或者AppleID【也就是注册的邮箱】,那么对不起奶不回来。苹果客服没有权限获悉之前注册的任何信息。【上海端口没有这么高的权限!】

激活开发身份

如果顺顺利利的完成了,所有前置流程,并且成功支付苹果开发者的会员费¥688.00。那么恭喜你完成了90%

但是,别高兴的太早。很多支付了费用,超过30个小时依旧没有成功获取开发者资格。

这种情况,必须要主动与苹果技术支持联系。对于个人账号大概率是需要补充身份证信息,也就是身份证正反面

苹果会通过开发者邮箱,提供一个附件资料上传地址。上传成功之后,预计2~3个小时将会激活。

之所以遇到这种问题,是因为中国大陆区有些小区名称或者街道过于离谱。比如:

  • 江苏南京神马路:位于南京市栖霞区,连接马群街道与仙林地区,因谐音与网络流行语 “神马都是浮云” 契合,成为网红路名。

  • 江苏南京马桶巷:位于南京秦淮区,传说因明代此处有制作马桶的手工作坊而得名,现已更名为 “饮马巷”,但老南京人仍习惯称其旧名。

  • 江苏苏州裤裆巷:巷子分岔呈 “Y” 形,形似裤裆,故得此名,后改名 “邾长巷”,但老苏州人仍爱调侃 “穿裤衩的路”。

  • 四川成都肥猪市街:该地以前是卖猪的市场,所以取了这样的名字。同理还有草市街、羊市街等。

  • 广东揭阳普宁二奶街:因上世纪 90 年代街道售卖的衣物价格昂贵,人们调侃称只有 “二奶” 才消费得起,故而得名,如今已发展成为当地有名的人气美食夜市。

f3aa64439608fb9ef785ee0acce490ea.png

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

相关推荐

# 苹果开发者续费大坑及成功续费方案!亲测有效

# AppStore敏感词排查手册,多维度分析Guideline 2.3.1隐藏功能,轻松过审。

# 如何主动提防苹果3.2f的进攻,自查防御手册(代码篇)

# 如何主动提防苹果3.2f的进攻,自查防御手册(ASO篇)

# 苹果加急审核是“绿色通道”还是“死亡陷阱”?

# 苹果开发者邮箱,突然收到11.2通知严重么?

# 不想被苹果卡审最好错开这两个提审时间

# 手撕苹果审核4.3是代码问题还是设计问题?

# 有幸和Appstore审核人员进行了一场视频会议特此记录。

抖音思路复刻:iOS 卡死(ANR)监控 + 自动符号化全流程实战

在 iOS 上,App 在启动 / 退出 / 响应系统事件等关键阶段如果长时间卡住,超过系统阈值就会触发保护机制,最终被 Watchdog 以 SIGKILL 强制终止。这类异常的共同特点是:不是进程内异常抛出,而是“进程外指令”直接结束进程,因此传统基于 signal/exception 的崩溃捕获往往覆盖不到,也就导致它在生产环境中经常“只见数据、不见堆栈”,长期被忽视。

为了解决这个盲区,我选择站在巨人的肩膀上:本文实践并复现字节跳动团队的文章《iOS 稳定性问题治理:卡死崩溃监控原理及最佳实践》,从 原理 → 代码实现 → 候选文件保存 → dSYM/atos/脚本符号化 + Swift demangle,把一整套卡死(ANR/Watchdog)监控链路完整跑通,并沉淀成一个可复用 Demo。

文末给出完整代码链接


1. 痛点:卡死最难的不是“知道卡”,而是“知道卡在哪”

线上卡死/假死常见现象:

  • UI 不响应(点击无反应、滑动卡住),几秒后恢复或被系统杀掉
  • Crash 日志没有(不是进程内 crash),只剩用户反馈:“刚刚卡住了”
  • 拿到的堆栈是一堆 0x00000001.... 地址:不符号化就等于没有结论

所以要解决这类问题,本质是两件事:

  1. 卡死当下抓到主线程堆栈(或卡死期间持续采样)
  2. 自动符号化:把地址变成可读函数名,并尽可能 demangle Swift 符号

2. 原理:RunLoop 心跳 + 超阈值采样 = 还原卡死现场

2.1 监控“主线程卡死”的本质

主线程 RunLoop 正常情况下会不断在这些状态间流转:

  • BeforeTimers
  • BeforeSources
  • BeforeWaiting
  • AfterWaiting

当主线程执行重任务(大解析 / 同步 IO / 复杂布局 / 锁等待等),RunLoop 会长期卡在某个阶段不动,表现为 UI 无响应。

2.2 本 Demo 的检测策略(尽量向“抖音方案”靠拢)

Demo 采用的策略(参数对齐你当前实现):

  • 阈值:8 秒hangThreshold = 8
  • 检测周期:1 秒窗口tickInterval = 1
  • 超过阈值后:每秒采样一次主线程栈(从 Live Report 中提取 Thread 0
  • 最多保留 10 帧样本maxMainThreadSamples = 10,保留最近 10 次)

这套策略的意义是:
先判断“已经卡死到足够严重”(接近 Watchdog 风险),再进入“持续采样”,避免把轻微卡顿也当成卡死去抓栈/写文件。


3. Demo 实现结构

3.1 项目目录骨架

LagMonitorDemo/
├── LagMonitorDemo.xcodeproj
├── Sources/
│   └── Monitor/
│      ├── HMDANRMonitor.swift
│      ├── HMDLiveReportCapture.swift
│      ├── HMDANRRecord.swift
│      ├── HMDANRCandidateStore.swift
│      └── HMDDebugCacheCleaner.swift
├── Scripts/
│   └── hmd_anr_symbolicate.py
└── Samples/
    ├── hmd_anr_candidate.json
    └── symbolicated.txt

3.2 Monitor:HMDANRMonitor.swift(核心监控器)

3.2.1 监控目标

  • 主线程装 CFRunLoopObserver,每次回调认为 RunLoop 推进:heartbeat += 1
  • 监控线程用 1 秒观察窗口检查 heartbeat 是否变化
    • 1 秒内有推进:健康/恢复,清理候选
    • 1 秒内无推进:认为卡住,hangSeconds += 1
  • 卡住累计达到 8 秒阈值:创建 candidate
  • 超阈值后仍未恢复:每秒采样一次主线程栈,最多 10 帧

3.2.2 为什么看起来“没 sleep 1 秒”,却实现了“每秒检查一次”?

核心就在这一句:

_ = wakeSemaphore.wait(timeout: .now() + .seconds(config.tickInterval))

它等价于:

  • 最多等待 1 秒tickInterval = 1)作为一个观察窗口
  • 如果期间 RunLoop 有推进,observer 会 signal() → 监控线程会提前醒来
  • 窗口结束后比较 heartbeat:若 1 秒内完全没变,才算这一秒“卡住”

所以这里不是“固定每秒到点触发一次”,而是:

“最多等 1 秒,但只要 RunLoop 一推进就立刻醒来重置状态”
这比 Timer 的“固定周期触发”更贴合我们想观察的对象(RunLoop 推进)。

3.2.3 为什么不用 NSTimer / GCD 定时器,而用信号量?

Timer 触发本身就依赖调度与 RunLoop/线程状态,卡死时最容易抖动或延迟;信号量 + 超时是更稳定的“观察窗口”,还能被 RunLoop 推进即时唤醒。


3.3 抓栈:HMDLiveReportCapture.swift(PLCrashReporter Live Report)

抓栈使用 PLCrashReporter 的 Live Report 能力:

  • generateLiveReportAndReturnError():生成“当下全线程现场”
  • 再从文本中提取 Thread 0 作为主线程样本
import CrashReporter

/// 抓一次“全线程现场报告”
static func captureAllThreadsText() -> String? {
    let config = PLCrashReporterConfig(signalHandlerType: .BSD, symbolicationStrategy: .all)
    guard let reporter = PLCrashReporter(configuration: config) else { return nil }

    do {
        let data = try reporter.generateLiveReportAndReturnError()
        let report = try PLCrashReport(data: data)
        return PLCrashReportTextFormatter.stringValue(for: report, with: PLCrashReportTextFormatiOS)
    } catch {
        print("[HMDLiveReportCapture] parse report error: \(error)")
        return nil
    }
}

这里的思路就是:
卡死现场抓“全线程”,用于兜底;超阈值后持续采“主线程”,用于定位稳定卡点。


3.4 保存:HMDANRCandidateStore.swift(hmd_anr_candidate.json)

Demo 保存的核心数据结构是 HMDANRRecord,通过 Codable 编码为 JSON:

public struct HMDANRRecord: Codable {
    public let recordID: String
    public let timestamp: Date
    public var hangSeconds: Int

    /// 超过阈值那一刻:全线程现场(PLCrash live report,文本)
    public var allThreadsReportText: String?

    /// 超阈值后:每秒采样主线程调用栈(最多保留最近 N 条)
    public var mainThreadSamples: [String]
}

保存到 Caches/hmd_anr_candidate.json 后,大致字段长这样:

{
  "recordID": "E2D0...-....",
  "timestamp": "2026-01-05T12:34:56Z",
  "hangSeconds": 9,
  "allThreadsReportText": "PLCrashReporter live report text ...",
  "mainThreadSamples": [
    "Thread 0 ...\n0 LagMonitorDemo 0x...\n1 UIKitCore ...",
    "Thread 0 ...\n0 LagMonitorDemo 0x...\n1 UIKitCore ..."
  ]
}

HMDANRCandidateStore卡住时把记录保存到缓存文件;一旦主线程恢复推进就立刻删除;如果进程被系统强杀来不及上报,这个文件会留到下次启动再读取导出/符号化。


4. 复现场景(Demo 内置)

Demo 里包含几种典型卡死触发方式:

stack.addArrangedSubview(makeButton("主线程 Busy 2s(轻微卡顿)") { [weak self] in self?.busy(seconds: 2) })
stack.addArrangedSubview(makeButton("主线程 Busy 20s(触发 candidate + 采样)") { [weak self] in self?.busy(seconds: 20) })
stack.addArrangedSubview(makeButton("锁竞争:子线程持锁 12s → 主线程尝试加锁") { [weak self] in self?.lockContention() })
stack.addArrangedSubview(makeButton("死锁:串行队列 sync + 主队列 sync(必卡死)") { [weak self] in self?.deadlock() })

5. 自动符号化:从 hmd_anr_candidate.json 到“可读堆栈”

我点击 “主线程 Busy 20s(触发 candidate + 采样)”,在第 10 秒手动杀掉 App,然后导出沙盒里的 hmd_anr_candidate.json

你会看到类似信息:

  • hangSeconds = 13(很明确的主线程长时间阻塞)
  • mainThreadSamples 有多次采样(证明卡住期间栈稳定)
  • 但这些堆栈仍然是地址/偏移,无法直接定位业务代码

1.jpg

因此需要做:

  • hmd_anr_candidate.json 里解析 frame
  • dSYM + atos 还原符号;并对 Swift 符号 demangle$s... → Foundation.Date.init()

本项目用脚本 hmd_anr_symbolicate.py 自动完成批量符号化:

python3 hmd_anr_symbolicate.py   --record hmd_anr_candidate.json   --app-dsym LagMonitorDemo.app.dSYM   --arch arm64   --demangle   --out symbolicated.txt

符号化后的symbolicated.txt的大致内容如下:

2.jpg

hmd_anr_symbolicate.pyGITHUB项目的Scripts文件夹下,hmd_anr_candidate.jsonsymbolicated.txt在Samples文件下

5.1 符号化后如何“从栈定位问题”?

symbolicated.txt 里抽 mainThreadSamples[0] 的关键几帧(你这次 5 次采样基本一致):

1   libsystem_c.dylib          gettimeofday
3   Foundation                 Date.init
4   LagMonitorDemo.debug.dylib ... (ViewController.swift:52)
5   LagMonitorDemo.debug.dylib ... (ViewController.swift:33)
6   LagMonitorDemo.debug.dylib ... (ViewController.swift:44)
7   UIKitCore                  ...

这说明卡死期间主线程一直在跑 ViewController.swift 的某段逻辑,并且频繁调用 Date()(最终落到 gettimeofday/clock_gettime),典型特征就是忙等/死循环式等待

对应 Demo 中的实现:

private func busy(seconds: Int) {
    let end = Date().addingTimeInterval(TimeInterval(seconds))
    while Date() < end {
        _ = 1 + 1
    }
}

这类栈顶常见现象就是:看起来“卡在 Date()”,其实根因是 while 循环让主线程一直跑
采样刚好截在 Date() 这一行,于是栈顶表现为 Date.init -> gettimeofday


6. 结语:复刻的意义,是把“文章里的方案”变成“项目里能用的工具”

大厂的稳定性方案往往更深、更体系化,但很多时候只停留在文章层面:看懂了思路,却很难在项目里直接落地。本文的目标就是把它“拆开 + 跑通”:

  • 把抖音文章里的链路拆成可运行代码(监控、采样、保存、恢复、导出)
  • 把最后一公里补齐(dSYM/atos 自动符号化 + Swift demangle)
  • 让“卡死问题”从 只有 SIGKILL 数字,变成 能指向具体业务函数/代码行 的结论

你真正需要的不是“我们检测到了卡死”,而是:

卡死那 8~20 秒内,主线程到底在跑什么?它卡在谁身上?

当你把“采样 + 保存 + 下次启动捞取 + 自动符号化 + demangle”这条链路跑通,线上卡死排查效率会明显提升。


GITHUB源码

❌