同样是实现"灰度化"功能,
一个函数、一个类的方法、一个协议的实现,结果一样,设计完全不同。
这一天我们来聊聊这个框架的设计决策背后的思考,
以及什么样的代码算是"工业级"的。
一、从需求到设计的思维过程
需求:实现灰度化、亮度、对比度、阈值四个图像滤镜,并支持链式调用。
方案 A:函数式
swift
func applyGrayscale(_ bitmap: MLBitmap) -> MLBitmap { ... }
func applyBrightness(_ bitmap: MLBitmap, adjustment: Int) -> MLBitmap { ... }
// 使用:
let result = applyBrightness(applyGrayscale(bitmap), adjustment: 30)
// 问题:嵌套调用难以阅读,顺序从里到外读
方案 B:命令式方法
swift
extension MLBitmap {
mutating func applyGrayscale() { ... }
mutating func applyBrightness(adjustment: Int) { ... }
}
// 使用:
bitmap.applyGrayscale()
bitmap.applyBrightness(adjustment: 30)
// 问题:修改原始数据,无法保留中间结果,难以测试
方案 C:协议 + 链式(本框架选择)
swift
protocol ImageFilter {
func apply(to bitmap: MLBitmap) -> MLBitmap
}
// 使用:
let result = bitmap
.applying(GrayscaleFilter())
.applying(BrightnessFilter(adjustment: 30))
// 清晰、可组合、可测试
二、ImageFilter 协议的设计哲学
协议定向编程(POP)
Swift 的核心设计理念之一:用协议而非继承来定义行为。
swift
public protocol ImageFilter {
func apply(to bitmap: MLBitmap) -> MLBitmap
}
这个协议只定义一件事:把一个 bitmap 转换成另一个 bitmap。极度简单,但这种简单性是强大的。
为什么不用继承(class hierarchy)?
swift
// 面向对象风格(不好):
class ImageFilter {
func apply(to bitmap: MLBitmap) -> MLBitmap {
fatalError("Subclass must override")
}
}
class GrayscaleFilter: ImageFilter {
override func apply(to bitmap: MLBitmap) -> MLBitmap { ... }
}
继承的问题:
- 强制使用
class(引用类型),增加内存管理复杂度 - 强耦合:子类依赖父类实现
- 扩展困难:第三方代码无法"继承扩展"
协议的优势:
struct实现,值类型语义- 第三方可以轻松实现自己的 Filter
- 组合优于继承
纯函数(Pure Function)
swift
func apply(to bitmap: MLBitmap) -> MLBitmap
纯函数的定义:
- 相同输入 → 永远产生相同输出(确定性)
- 没有副作用(不修改外部状态)
纯函数的好处:
- 易于测试:无需 mock,直接传入测试数据
- 易于并行:多个 Filter 可以并行处理不同图像,没有竞争条件
- 易于组合:输出直接作为下一个的输入
三、值类型(Struct)与 Copy-on-Write
为什么 MLBitmap 是 struct?
swift
public struct MLBitmap {
public var pixels: [UInt8]
...
}
值类型的语义:
swift
var bitmap1 = MLBitmap(width: 10, height: 10, filling: .white)
var bitmap2 = bitmap1 // 看起来像复制
bitmap2[0, 0] = .red // 修改 bitmap2
// bitmap1[0, 0] 仍然是 .white!
// 两者完全独立
如果 MLBitmap 是 class:
swift
class MLBitmap {
var pixels: [UInt8]
}
var bitmap2 = bitmap1 // 实际上是引用复制
bitmap2.pixels[0] = 255 // bitmap1.pixels[0] 也变了!
图像处理中,每个 Filter 应该生成新图像,不影响原图。值类型的语义天然满足这个需求。
Copy-on-Write(写时复制)
Swift 的数组([UInt8])实现了 CoW:
swift
var pixels1 = [UInt8](repeating: 0, count: 1000)
var pixels2 = pixels1 // 此时不复制,只是共享引用
pixels2[0] = 255 // 第一次写入时,才真正复制
// pixels1 不受影响
这使得 var result = bitmap 的操作几乎没有成本------只有当你真正写入 result 时,内存才会复制。
性能影响:
swift
// 三次 Filter 链式调用
let result = bitmap
.applying(GrayscaleFilter()) // 第 1 次 CoW 触发,复制 bitmap
.applying(BrightnessFilter()) // 第 2 次 CoW 触发,复制中间结果
.applying(ThresholdFilter()) // 第 3 次 CoW 触发,复制中间结果
// 共有 3 次内存复制
// 对 100×100 图:3 × 40 KB = 120 KB,几乎不可感知
// 对 4K 图:3 × 32 MB = 96 MB,链式调用时峰值内存较高
工业级优化:Fusion(把多个 Filter 的计算合并到一次遍历)。
四、some ImageFilter vs any ImageFilter
swift
// MLBitmap 的链式调用方法
func applying(_ filter: some ImageFilter) -> MLBitmap {
filter.apply(to: self)
}
some Protocol(Opaque Type):
- 调用时类型固定,编译器知道具体类型
- 零运行时开销(不需要 existential box)
- 适合:每次调用类型确定的场景
any Protocol(Existential Type):
- 类型在运行时动态决定
- 有运行时开销(existential box + vtable 查找)
- 适合:把不同类型的 Filter 放入同一个数组
swift
// 需要存放不同 Filter 的数组时,用 any:
let pipeline: [any ImageFilter] = [
GrayscaleFilter(),
BrightnessFilter(adjustment: 30),
ThresholdFilter()
]
let result = pipeline.reduce(bitmap) { $1.apply(to: $0) }
五、Precondition vs Guard vs Throw:三种防御方式
框架代码中有三种处理错误的方式,选择哪种取决于错误性质:
precondition:编程错误(Bug)
swift
// 调用方传了不合法的参数,这是 bug,应该在开发阶段崩溃暴露
precondition(width > 0 && height > 0, "Width and height must be positive")
precondition(factor.isFinite, "factor must be finite")
precondition(values.count % 2 == 1, "Kernel height must be odd")
适用:不变量被违反,是调用方的编程错误。在 Debug 下崩溃(暴露 bug),在 Release 下行为未定义(Swift 优化掉 precondition 检查)。
guard + throw:运行时错误(预期可能发生)
swift
// 图像可能真的很大,这不是 bug,而是正常运行时的条件
guard width <= maxDimension && height <= maxDimension else {
throw LoadError.dimensionTooLarge(width: width, height: height)
}
适用:外部资源(文件大小、内存限制、网络状态)不可控,调用方需要处理这些情况。
return nil / 默认值:可恢复的退化
swift
// CGDataProvider 创建失败,返回 nil,调用方检查
guard let provider = CGDataProvider(data: data as CFData) else { return nil }
适用:失败是轻量级的,调用方可以通过 optional 判断处理。
选择原则:
- "这种情况不应该发生,发生了说明有 bug" →
precondition - "这种情况可能发生,调用方必须处理" →
throw - "这种情况可能发生,调用方可以忽略" →
return nil
六、@inline(__always) 和 @discardableResult
@inline(__always)
swift
@inline(__always)
func index(x: Int, y: Int) -> Int {
(y * width + x) * Self.bytesPerPixel
}
这个函数在像素遍历的内层循环中被调用,100×100 图调用 10,000 次,4K 图调用 800 万次。普通函数调用有开销(压栈/出栈、跳转)。@inline(__always) 让编译器把函数体直接嵌入调用处,消除调用开销。
权衡:内联会增加代码体积(每个调用处都展开一份代码),但对热路径的小函数是合理的。
@discardableResult
swift
@discardableResult
public static func process(_ bitmap: MLBitmap, to url: URL, scenario: ExportScenario) -> ExportResult
Swift 默认情况下,如果你忽略一个有返回值的函数的返回值,编译器会给出警告。@discardableResult 表示"忽略返回值是可以接受的"。
适用场景:返回值提供额外信息(如成功/失败详情),但调用方也可能只关心副作用(文件是否写出),而不在乎详细的返回值。
七、单一可信来源(SSOT)原则
swift
// ❌ 错误:同样的常量在两个地方定义
// ImageLoader.swift
let bitmapInfo = CGImageAlphaInfo.premultipliedLast.rawValue | CGBitmapInfo.byteOrder32Big.rawValue
// ImageExporter.swift
let bitmapInfo = CGImageAlphaInfo.premultipliedLast.rawValue | CGBitmapInfo.byteOrder32Big.rawValue
// 问题:如果只改了一处,另一处不同步,导致颜色错乱,且没有编译器提示
// ✅ 正确:单一定义,双端引用
// MLBitmap.swift(单一可信来源)
public static let bitmapInfo: CGBitmapInfo = CGBitmapInfo(rawValue:
CGImageAlphaInfo.premultipliedLast.rawValue |
CGBitmapInfo.byteOrder32Big.rawValue
)
// ImageLoader.swift
let bitmapInfo = MLBitmap.bitmapInfo.rawValue // 引用
// ImageExporter.swift
let bitmapInfo = MLBitmap.bitmapInfo // 引用
SSOT 原则(Single Source of Truth):每个知识(常量、配置、逻辑)只在一个地方定义,其他地方引用。
八、测试驱动的工程化
每一个重要功能都有对应的测试:
testBitmapMemoryLayout() ← 验证内存布局公式
testCoordinateOriginIsTopLeft() ← 验证坐标系约定(最容易出错的地方)
testGrayscaleLuminanceFormula() ← 验证 BT.709 公式精度
testBrightnessClampMax() ← 验证溢出截断(不是回绕)
testContrastAnchorPoint() ← 验证 128 锚点不变性
testSobelDetectsVerticalEdge() ← 验证边缘检测有效性
testAutoFormatTransparentImage() ← 验证透明度检测
testResampleReducesOversized() ← 验证等比缩放
测试的价值:
- 文档化了预期行为(代码即文档)
- 重构时有安全网(改代码不怕破坏已有功能)
- 发现设计缺陷(如
testCoordinateOriginIsTopLeft暴露了坐标系 bug)
测试的粒度
好的测试只测一件事:
swift
// ❌ 测试太多,失败时不知道哪里出了问题
func testGrayscale() {
// 测试白色、黑色、亮度公式、Alpha 保护......全放在一起
}
// ✅ 每个测试一个断言
func testGrayscaleWhiteStaysWhite() { ... }
func testGrayscaleBlackStaysBlack() { ... }
func testGrayscaleLuminanceFormula() { ... }
func testGrayscaleAlphaUnchanged() { ... }
九、代码注释的层次
本框架的注释分为三层:
Layer 1:文件头注释(解释"为什么这个文件存在")
swift
// ImageProcessor.swift
// 工业级图像预处理管线
//
// 职责:在导出/上传前,根据场景策略对图像进行:
// 1. 尺寸重采样(Resample)
// 2. 格式选择(Format Selection)
// 3. 质量决策(Quality Decision)
Layer 2:函数注释(解释"这个函数做什么,参数是什么")
swift
/// 将 UIImage 解码为 MLBitmap(RGBA8888 / sRGB)。
///
/// 流程:UIImage → CGImage → CGContext(重新绘制)→ [UInt8]
/// 通过重新绘制确保颜色空间统一(Display P3 / sRGB 均归一化为 sRGB)
///
/// - Throws: `LoadError`(尺寸超限 / 内存超限 / CGImage 缺失)
public static func load(from image: UIImage) throws -> MLBitmap
Layer 3:关键步骤注释(解释"为什么这么做,不是这么做会怎样")
swift
// ⚠️ 不要加 translateBy/scaleBy flip:
// flip 会把 CGImage row 0 翻到 buffer 末尾,
// 反而使 bitmap[0,0] 变成视觉「左下角」。
context.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height))
原则:注释解释"为什么",而不是重复"做什么"(代码本身已经说明做什么了)。
十、阶段一学习完整架构回顾
MLImageCore
│
├── Core/
│ └── MLBitmap.swift # 核心数据结构(struct + CoW)
│
├── Filters/
│ ├── ImageFilter.swift # 协议定义(POP)
│ ├── GrayscaleFilter.swift # BT.709 灰度化
│ ├── BrightnessFilter.swift # 线性亮度调整
│ ├── ThresholdFilter.swift # 二值化
│ └── ContrastFilter.swift # 对比度调整
│
├── Algorithms/
│ ├── Convolution.swift # 2D 卷积引擎(通用)
│ ├── GaussianBlur.swift # 可分离高斯模糊
│ └── SobelEdge.swift # Sobel 边缘检测
│
└── IO/
├── ImageLoader.swift # UIImage → MLBitmap(颜色空间归一化)
├── ImageExporter.swift # MLBitmap → UIImage / 文件(回退链)
└── ImageProcessor.swift # 工业级管线(重采样 + 格式决策 + 体积控制)
每一层都遵循单一职责原则(SRP):
ImageLoader:只负责加载和格式归一化ImageExporter:只负责编码和写文件ImageProcessor:只负责决策和调度(不操作像素)Convolution:只是纯数学引擎,不知道 Filter 业务
十一、工业级 vs 学习级代码
| 维度 | 学习级 | 工业级 |
|---|---|---|
| 错误处理 | 强制解包 !,打印错误 |
结构化 Error,throw/Result |
| 日志 | print() |
os.log,带级别和类别 |
| 内存 | 随意分配,不考虑峰值 | 预估峰值,设置上限,提前拦截 |
| 边界 | "应该不会发生" | precondition + guard + throw |
| API | 功能正确即可 | 命名清晰,访问控制合理,文档完善 |
| 测试 | 手动跑一下 | 单元测试覆盖核心路径 |
| 可扩展性 | 直接改代码 | 协议 + 预设 + 自定义接口 |
| 常量管理 | 魔法数字散落各处 | SSOT,单一定义 |
工业级代码的核心追求:在 3 个月后,由另一个人来维护这段代码时,他能快速理解、安全修改。
十二、小结与展望
Phase 1 建立了:
- 图像处理的基础数据结构和坐标系约定
- CPU 层的完整算法实现(灰度、亮度、对比度、二值化、卷积、高斯模糊、Sobel)
- 工业级的 IO 管线(颜色空间归一化、格式决策、体积控制)
- 良好的代码架构和测试覆盖
Phase 2 目标:从"手写 CPU 算法"升级到"调用 Apple 系统框架加速":
- Core Image:GPU 渲染管线,CIFilter 包装
- vImage / Accelerate:SIMD 向量化,更快的卷积
- 直方图分析:Otsu 自适应阈值,直方图均衡
- 颜色空间转换:RGB ↔ HSV ↔ Lab
Phase 3 目标:Metal Compute Shader,真正的 GPU 并行:
- 数千个像素同时计算
- 实时滤镜(30fps 视频处理)
- Custom Compute Kernel(自定义 GPU 程序)
思考题
- 如果要把
MLBitmap从 struct 改为 class,需要修改哪些地方?会带来哪些新的问题? - 设计一个
CompositFilter,它包含一个[any ImageFilter]数组,调用apply时依次执行所有 Filter。写出这个类型的定义,并说明它是 struct 还是 class,理由是什么? - 在 iOS 开发中,如果你的图像处理需要在后台线程运行(避免主线程阻塞),现有的
ImageFilter协议设计需要做什么改动?(提示:Swift Concurrency 的Sendable)
答案:2. 应该是 struct(值类型),因为它只是 Filter 序列的组合,没有共享状态;定义:
struct CompositFilter: ImageFilter { let filters: [any ImageFilter]; func apply(to bitmap: MLBitmap) -> MLBitmap { filters.reduce(bitmap) { $1.apply(to: $0) } } };3. 需要让所有 Filter 标注Sendable(struct GrayscaleFilter: ImageFilter, Sendable {}),以及让MLBitmap也标注Sendable,这样才能在不同 actor 之间安全传递。
♥️喜欢我的内容,欢迎大家点赞、转发、关注。
♥️本人专注于技术+投资+认知三位一体的内容分享。
往期推荐: