被 Vibe 摧毁的版权壁垒,与开发者的新护城河 - 肘子的 Swift 周报 #131
Anthropic 不久前宣布,由于其最新模型 Mythos 在网络安全与代码漏洞挖掘方面的能力“过于强大”,已达到令人不安的程度,因此采取了极为罕见的克制措施:仅向 Project Glasswing 内的少数关键基础设施企业开放,不面向公众发布,普通开发者也无法通过 API 调用
Anthropic 不久前宣布,由于其最新模型 Mythos 在网络安全与代码漏洞挖掘方面的能力“过于强大”,已达到令人不安的程度,因此采取了极为罕见的克制措施:仅向 Project Glasswing 内的少数关键基础设施企业开放,不面向公众发布,普通开发者也无法通过 API 调用
本文只讲两件事: 👉 如何自定义 UICollectionView 布局 👉 如何优雅地切换布局并处理动画问题
系统的 UICollectionViewFlowLayout 有一个明显限制:
👉 只能做规则网格(grid)布局
但很多场景需要:
例如👇
┌───────────────┐
│ A │
├───────┬───────┤
│ B │ C │
└───────┴───────┘
👉 这种布局 FlowLayout 是做不了的
自定义布局本质只做三件事:
UICollectionViewLayoutAttributes
override func layoutAttributesForElements(in rect: CGRect)
override var collectionViewContentSize: CGSize
class MosaicLayout: UICollectionViewFlowLayout {
var layoutAttributes: [UICollectionViewLayoutAttributes] = []
var contentSize: CGSize = .zero
override func prepare() {
super.prepare()
guard let collectionView = collectionView else { return }
layoutAttributes.removeAll()
let count = collectionView.numberOfItems(inSection: 0)
guard count > 0 else { return }
let width = collectionView.bounds.width
let height = width
let frames: [CGRect] = [
CGRect(x: 0, y: 0, width: 1, height: 0.6),
CGRect(x: 0, y: 0.6, width: 0.5, height: 0.4),
CGRect(x: 0.5, y: 0.6, width: 0.5, height: 0.4)
]
for (index, relativeFrame) in frames.enumerated() {
let frame = CGRect(
x: relativeFrame.origin.x * width,
y: relativeFrame.origin.y * height,
width: relativeFrame.width * width,
height: relativeFrame.height * height
)
let attr = UICollectionViewLayoutAttributes(forCellWith: IndexPath(item: index, section: 0))
attr.frame = frame
layoutAttributes.append(attr)
}
contentSize = CGSize(width: width, height: height)
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
return layoutAttributes.filter { $0.frame.intersects(rect) }
}
override var collectionViewContentSize: CGSize {
return contentSize
}
}
collectionView.setCollectionViewLayout(newLayout, animated: true)
The invalidation context is not an instance of UICollectionViewFlowLayoutInvalidationContext
collectionView.setCollectionViewLayout(newLayout, animated: false)
collectionView.reloadData()
👉 自定义布局 = 自己控制 frame 👉 动画关键 = 避免同时更新数据源
如果你觉得有用,欢迎点赞 👍
Flutter iOS应用混淆与安全配置文档
本文档详细描述了iOS应用的混淆与安全配置过程。这些配置旨在保护应用代码、API密钥和敏感数据,防止逆向工程和恶意攻击。配置包括 Dart 代码混淆、原生代码混淆、运行时安全检查和数据安全措施。
Flutter提供了内置的代码混淆功能,通过以下参数启用:
--obfuscate --split-debug-info=./symbols
1
这将:
此外,使用像IpaGuard这样的专业混淆工具可以进一步增强应用安全性。IpaGuard是一款强大的iOS IPA文件混淆工具,无需源码即可对代码和资源进行混淆加密,支持Flutter等多种开发平台,有效增加反编译难度。
在iOS上,我们通过以下配置增强安全性:
// 启用代码混淆和优化
GCC_OPTIMIZATION_LEVEL = s
SWIFT_OPTIMIZATION_LEVEL = -O
SWIFT_COMPILATION_MODE = wholemodule
DEAD_CODE_STRIPPING = YES
// 安全设置
ENABLE_STRICT_OBJC_MSGSEND = YES
CLANG_WARN_SUSPICIOUS_MOVE = YES
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES
GCC_NO_COMMON_BLOCKS = YES
STRIP_STYLE = all
STRIP_INSTALLED_PRODUCT = YES
COPY_PHASE_STRIP = YES
DEBUG_INFORMATION_FORMAT = dwarf-with-dsym
// 启用应用传输安全
PRODUCT_SETTINGS_URL_SCHEMES = "$(inherit)"
PRODUCT_SETTINGS_APP_TRANSPORT_SECURITY_ALLOWS_ARBITRARY_LOADS = NO
// 添加其他安全属性
OTHER_LDFLAGS = $(inherited) -Wl,-no_pie
12345678910111213141516171819202122
xcodebuild -workspace Runner.xcworkspace -scheme Runner -configuration Release clean build \
ENABLE_BITCODE=YES STRIP_INSTALLED_PRODUCT=YES DEPLOYMENT_POSTPROCESSING=YES \
-sdk iphoneos -allowProvisioningUpdates
123
这些参数确保:
通过以下Swift代码实现运行时安全检查:
// 检查设备是否已越狱
func isJailbroken() -> Bool {
#if targetEnvironment(simulator)
return false
#else
// 检查常见的越狱文件路径
let jailbreakPaths = [
"/Applications/Cydia.app",
"/Library/MobileSubstrate/MobileSubstrate.dylib",
"/bin/bash",
"/usr/sbin/sshd",
"/etc/apt",
"/private/var/lib/apt/",
"/usr/bin/ssh"
]
for path in jailbreakPaths {
if FileManager.default.fileExists(atPath: path) {
return true
}
}
// 检查是否可以写入私有目录
let stringToWrite = "Jailbreak Test"
do {
try stringToWrite.write(toFile: "/private/jailbreak.txt", atomically: true, encoding: .utf8)
try FileManager.default.removeItem(atPath: "/private/jailbreak.txt")
return true
} catch {
// 无法写入,说明没有越狱
}
return false
#endif
}
// 检查是否连接调试器
func isDebuggerAttached() -> Bool {
#if DEBUG
return false
#else
var info = kinfo_proc()
var mib: [Int32] = [CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()]
var size = MemoryLayout<kinfo_proc>.stride
let status = sysctl(&mib, UInt32(mib.1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556
苹果应用上架与推广详细指南
作为苹果个人开发者,确保用户能够顺畅地下载并安装自己精心开发的应用,是不可或缺的一环。接下来,我们将深入探讨实现这一目标的一系列核心步骤。
01应用上架准备
注册开发者账号是发布应用的第一步,必须在苹果开发者官网完成信息填写和费用支付。你需要前往苹果开发者官网,填写包括姓名、联系方式在内的个人真实信息,并支付相应费用。完成注册与身份验证后,你将获得发布应用的权限。
在用户能够顺畅地下载并安装应用之前,开发者需要完成一系列的准备工作。这些准备工作的目标是确保应用的下载和安装过程尽可能顺畅,从而提升用户体验。
应用需符合苹果的质量与安全标准,不得包含恶意代码,需具备良好的UI设计和正常功能。苹果对应用的质量、功能及安全性都设有严格标准。应用不得包含恶意代码,必须具备良好的用户界面设计,且功能正常、无显著漏洞。此外,应用还需遵守法律法规,如不得侵犯他人知识产权,不得诱导用户进行不合理付费等。只有满足这些准则的应用,才有可能通过审核并上架供用户下载。
接下来,我们就将详细介绍这些准备工作。在着手构建和测试应用之前,有几个关键步骤需要完成。这些步骤旨在为应用的顺畅下载和安装铺平道路,进而优化用户体验。
使用Xcode进行开发,选择合适的编程语言并在多设备和系统版本上进行全面测试,确保功能完备、兼容性和性能。通常,Xcode被视为开发首选,它提供了从代码编写到编译、调试的一站式服务。依据应用特性,选择如Swift或Objective-C等编程语言。以一个简易笔记应用为例,我们可以运用Swift来构建用户界面,并实现笔记的增删改查功能。务必在不同型号的iOS设备和多种系统版本上进行详尽测试。这包括验证功能的完备性,例如确认笔记应用能否顺利存储和读取数据;确保兼容性,以保证应用在iPhone、iPad等设备上的一致性;以及评估性能,如测试应用的启动速度和响应时间。例如,在iPhone 13与iPhone 8上分别运行笔记应用,观察是否存在布局混乱或功能失效的问题。此外,开发者也可以使用AppUploader等工具来简化iOS证书的申请和管理,支持在Windows、Linux或Mac系统中操作,无需依赖Mac电脑。
02向App Store提交与推广
接下来,就可以将你的应用提交到App Store了。 提交应用前需准备应用名称、描述、截图及视频,并选择合适的分类以提高用户搜索的精准性。
为应用起一个简洁且能体现核心功能的名称,如“速记笔记”,同时撰写详细且吸引人的描述,突出应用特点、功能和优势。此外,还需提供展示关键界面和操作流程的截图及视频预览,使用户能直观了解应用。依此选择合适的分类,如笔记应用可归于“效率”类别。这样能帮助用户在App Store中更精准地搜索到应用。
通过App Store Connect提交审核,积极进行应用推广,包括社交媒体、博主合作及应用内推广,提高用户下载量。
使用Xcode完成应用打包后,通过App Store Connect提交应用进行审核。或者,使用AppUploader工具上传IPA文件到App Store,它支持多平台,比Application Loader更高效,且不携带设备信息。审核过程通常需数日,期间苹果团队将检查应用是否符合各项标准。若审核过程中发现任何问题,将收到反馈通知,需根据反馈进行相应修改后重新提交。同时,积极推广应用也是提高下载量的关键。
社交媒体推广:借助Twitter、Facebook、Instagram等社交媒体平台,广泛宣传您的应用。您可以分享应用截图、详细的功能介绍以及使用教程等,以此吸引更多潜在用户的关注。例如,在Twitter上发布一系列关于您的笔记应用使用技巧的推文,并附上应用的下载链接。
与博主和媒体合作:积极联系相关领域的博主、自媒体或科技媒体,向他们介绍您的应用,并努力争取他们的推荐或报道。例如,与专注于效率类应用评测的博主取得联系,邀请他们体验您的应用并分享他们的使用感受。
应用内推广:如果您还有其他已发布的应用,可以在这些应用内部推广您的新应用,从而引导现有用户下载并体验。
通过上述推广策略,苹果个人开发者可以成功地推动应用的下载安装,并通过广泛的推广活动提高应用的下载量和用户活跃度,从而让您的开发成果得到更多用户的认可和喜爱。
iOS三方库精读 · 第 11 期
DGCharts(原名 Charts)是一个用于 iOS/macOS/tvOS 的图表渲染库,它是 Android 端 MPAndroidChart 的 Swift 移植版,让在 UIKit 与 SwiftUI 中绘制专业级折线图、柱状图、饼图等 8 种图表类型变得像配置数据一样简单。
| 属性 | 信息 |
|---|---|
| ⭐ GitHub Stars | 27.5k+ |
| 最新稳定版 | 5.1.0 |
| License | Apache 2.0 |
| 支持平台 | iOS 13+ / macOS 10.14+ / tvOS 13+ |
| 语言 | Swift(纯 Swift,无 OC 接口) |
苹果官方没有提供专用图表框架(Swift Charts 直到 iOS 16 才随 SwiftUI 一起发布),在此之前,iOS 开发者要想画一条像样的折线图,要么:
DGCharts 解决的核心痛点:
| 维度 | DGCharts | Swift Charts(苹果官方) |
|---|---|---|
| 平台要求 | iOS 13+ | iOS 16+ |
| UI 框架 | UIKit + SwiftUI | SwiftUI only |
| 图表类型 | 8 种(含 K 线/雷达) | 折/柱/面积/点(4 种) |
| 动画控制 | 精细帧级控制 | 声明式,较难定制 |
| 手势交互 | 内置缩放/平移 | 需自己实现 |
| 维护状态 | 社区维护,活跃 | Apple 官方,稳定更新 |
SPM(推荐):
// Package.swift 或 Xcode Add Package Dependency
// URL: https://github.com/danielgindi/Charts.git
// from: "5.1.0"
CocoaPods:
pod 'DGCharts', '~> 5.1.0'
import DGCharts
let chartView = LineChartView(frame: view.bounds)
// 准备数据
let entries = (0..<10).map { ChartDataEntry(x: Double($0), y: Double.random(in: 10...100)) }
let dataSet = LineChartDataSet(entries: entries, label: "销售额")
chartView.data = LineChartData(dataSet: dataSet)
view.addSubview(chartView)
基础层 概念:
ChartDataEntry是最小数据单元(x, y),XxxChartDataSet是同类数据的集合(样式配置在这里),XxxChartData是图表的数据容器,XxxChartView是最终渲染视图。
| 类名 | 用途 |
|---|---|
LineChartView |
折线图(支持曲线插值) |
BarChartView |
柱状图(支持堆叠/分组) |
PieChartView |
饼图 / 甜甜圈图 |
RadarChartView |
雷达图(蜘蛛网图) |
ScatterChartView |
散点图 |
CandleStickChartView |
K 线图(OHLC) |
BubbleChartView |
气泡图 |
CombinedChartView |
组合图(折+柱叠加) |
// 折线数据集样式
let dataSet = LineChartDataSet(entries: entries, label: "趋势")
dataSet.colors = [.systemBlue] // 线条颜色
dataSet.lineWidth = 2.0 // 线宽
dataSet.circleColors = [.systemBlue] // 数据点颜色
dataSet.circleRadius = 4.0 // 数据点半径
dataSet.drawFilledEnabled = true // 开启渐变填充
dataSet.fillColor = .systemBlue
dataSet.fillAlpha = 0.3
dataSet.mode = .cubicBezier // 贝塞尔平滑曲线
// 图表视图样式
lineChartView.rightAxis.enabled = false // 隐藏右轴
lineChartView.xAxis.labelPosition = .bottom
lineChartView.animate(xAxisDuration: 1.0, easingOption: .easeInOutQuart)
chartView.scaleXEnabled = true // 允许 X 轴缩放
chartView.scaleYEnabled = false // 禁止 Y 轴缩放
chartView.dragEnabled = true // 允许拖拽
chartView.pinchZoomEnabled = true // 双指缩放
chartView.doubleTapToZoomEnabled = false // 关闭双击缩放
// 将数字索引转为日期字符串
class DateAxisValueFormatter: AxisValueFormatter {
private let dates = ["1月", "2月", "3月", "4月", "5月", "6月"]
func stringForValue(_ value: Double, axis: AxisBase?) -> String {
let index = Int(value)
return index < dates.count ? dates[index] : ""
}
}
chartView.xAxis.valueFormatter = DateAxisValueFormatter()
DGCharts 的核心渲染架构遵循 Renderer 模式:
ChartView → 入口,持有 ChartData 和多个 DataRenderer
DataRenderer(如 LineChartRenderer)→ 负责具体图形绘制,使用 CGContext
ChartViewBase → 提供手势识别、坐标变换 Transformer
Transformer → 负责将数据坐标系映射到屏幕坐标系(核心矩阵变换)每次 data 被 set,触发 notifyDataSetChanged() → calcMinMax() → setNeedsDisplay(),最终回调 draw(_ rect:) 由各 Renderer 完成绘制。
// Swift UIKit,iOS 17+
import UIKit
import DGCharts
class StockChartVC: UIViewController {
private lazy var combinedChart: CombinedChartView = {
let chart = CombinedChartView()
chart.legend.enabled = true
chart.rightAxis.enabled = false
chart.xAxis.labelPosition = .bottom
chart.animate(xAxisDuration: 0.8)
return chart
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
view.addSubview(combinedChart)
combinedChart.frame = CGRect(x: 16, y: 100,
width: view.bounds.width - 32,
height: 320)
setupChart()
}
private func setupChart() {
// ---- K 线数据 ----
let candleEntries: [CandleChartDataEntry] = [
CandleChartDataEntry(x: 0, shadowH: 185, shadowL: 162, open: 165, close: 178),
CandleChartDataEntry(x: 1, shadowH: 190, shadowL: 170, open: 178, close: 188),
CandleChartDataEntry(x: 2, shadowH: 195, shadowL: 175, open: 188, close: 177),
CandleChartDataEntry(x: 3, shadowH: 182, shadowL: 165, open: 177, close: 169),
CandleChartDataEntry(x: 4, shadowH: 175, shadowL: 160, open: 169, close: 172),
]
let candleSet = CandleChartDataSet(entries: candleEntries, label: "K 线")
candleSet.shadowColor = .darkGray
candleSet.shadowWidth = 1.5
candleSet.decreasingColor = .systemRed // 阴线颜色
candleSet.decreasingFilled = true
candleSet.increasingColor = .systemGreen // 阳线颜色
candleSet.increasingFilled = true
candleSet.neutralColor = .systemGray
// ---- 成交量柱状图 ----
let barEntries = [
BarChartDataEntry(x: 0, y: 3200),
BarChartDataEntry(x: 1, y: 5400),
BarChartDataEntry(x: 2, y: 4100),
BarChartDataEntry(x: 3, y: 6800),
BarChartDataEntry(x: 4, y: 3900),
]
let barSet = BarChartDataSet(entries: barEntries, label: "成交量(手)")
barSet.colors = [.systemBlue.withAlphaComponent(0.5)]
barSet.axisDependency = .right // 绑定右轴(虽然禁用了,仅用于数据缩放)
// ---- 组合 ----
let combined = CombinedChartData()
combined.candleData = CandleChartData(dataSet: candleSet)
combined.barData = BarChartData(dataSet: barSet)
// X 轴 Formatter
combinedChart.xAxis.valueFormatter = IndexAxisValueFormatter(
values: ["Mon", "Tue", "Wed", "Thu", "Fri"]
)
combinedChart.xAxis.granularity = 1
combinedChart.data = combined
}
}
// 自定义 Marker,继承 MarkerView
class BalloonMarker: MarkerView {
private var label: String = ""
override func refreshContent(entry: ChartDataEntry, highlight: Highlight) {
label = String(format: "%.1f", entry.y)
super.refreshContent(entry: entry, highlight: highlight)
}
override func draw(context: CGContext, point: CGPoint) {
// 用 CoreGraphics 绘制气泡背景 + 文字
let attrs: [NSAttributedString.Key: Any] = [
.font: UIFont.systemFont(ofSize: 12),
.foregroundColor: UIColor.white
]
let attrStr = NSAttributedString(string: label, attributes: attrs)
let size = attrStr.size()
let rect = CGRect(x: point.x - size.width / 2 - 8,
y: point.y - size.height - 12,
width: size.width + 16, height: size.height + 8)
context.setFillColor(UIColor.systemBlue.cgColor)
UIBezierPath(roundedRect: rect, cornerRadius: 6).fill()
attrStr.draw(in: rect.insetBy(dx: 8, dy: 4))
}
}
// 绑定
chartView.marker = BalloonMarker()
chartView.drawMarkers = true
DGCharts 用一个 CGAffineTransform 维护数据坐标 → 屏幕坐标的映射:
屏幕坐标 = 数据坐标 × scale + offset + chart padding
每次缩放/平移,只更新 transform 矩阵然后 setNeedsDisplay,避免重新计算所有点,是渲染性能的核心保障。
// 每种图表有独立 Renderer,统一协议
public protocol DataRenderer: AnyObject {
func drawData(context: CGContext)
func drawValues(context: CGContext)
func drawExtras(context: CGContext)
func drawHighlighted(context: CGContext, indices: [Highlight])
}
遵循开闭原则,新增图表类型只需实现此协议,不修改任何现有代码。
触摸事件 → getHighlight(x:y:) → 二分查找最近 x → 在该 x 的所有 DataSet 中找最近 y → 返回 Highlight(x:y:dataSetIndex:) → 触发 highlightValue。整套流程在主线程同步完成,保证交互响应 < 16ms。
问题 1:数据更新后图表没有刷新
entries 数组但没有通知图表chartView.data?.notifyDataChanged()
chartView.notifyDataSetChanged()
问题 2:X 轴标签显示 0, 1, 2 而不是自定义文字
xAxis.valueFormatter 或者 granularity 不正确xAxis.valueFormatter = IndexAxisValueFormatter(values: yourLabels)
xAxis.granularity = 1.0 // 必须设置,否则可能跳步
问题 3:饼图中间的 "洞" 大小无法控制
holeRadiusPercent 默认 0.5(即 50%)pieChart.holeRadiusPercent = 0.3 // 调整洞的比例,0 = 实心饼图
问题 4:动画结束后视图突然闪烁
animateX 和 animateY 同时调用,两个动画结束时各触发一次 setNeedsDisplay
animate(xAxisDuration:yAxisDuration:) 合并为一次调用问题 5:在 SwiftUI 中使用时手势冲突
chartView.dragEnabled = false // 在 ScrollView 内关闭拖拽
chartView.pinchZoomEnabled = false // 关闭缩放
问题 6:Swift Package Manager 拉取超时
package.json 中指定 .upToNextMinor(from: "5.1.0"),避免每次重新解析| 维度 | DGCharts | Swift Charts (Apple) | AAChartKit | Charts.js (WebView) |
|---|---|---|---|---|
| 平台要求 | iOS 13+ | iOS 16+ | iOS 11+ | iOS (via WKWebView) |
| 图表类型数 | 8 | 4 | 10+ | 20+ |
| 性能 | 原生,高 | 原生,高 | 原生,良好 | WebView,差 |
| K 线图 | ✅ | ❌ | ✅ | ✅ |
| SwiftUI 支持 | UIViewRepresentable | 原生 | UIViewRepresentable | WKWebViewRepresentable |
| 维护状态 | 社区活跃 | Apple 官方 | 活跃 | Web 项目 |
| 学习曲线 | 中等 | 低(SwiftUI 声明式) | 低 | 需了解 JS |
github.com/yourname/ios-lib-demos
使用 LineChartView 实现一个实时动态折线图:每秒追加一个随机数据点,并保持最多显示最近 20 个点(超出则移除最旧的点),同时保证图表 X 轴自动滚动跟随最新数据。完成后在评论区贴出你的核心更新逻辑。
DGCharts 的 Transformer 用矩阵变换把数据坐标映射到屏幕坐标,这种设计和 CALayer 的 transform 有何本质区别?如果让你自己实现一套坐标系映射,你会选择哪种方案?为什么?
下一期我们将深入 Hero(转场动画库)。你在项目中用过哪些自定义转场方案(Hero / UIViewControllerTransitioningDelegate / NavigationController Push 动画)?欢迎评论区分享你的实践经验,优质回答会收录进下一期《踩坑记录》。
📅 本系列每周五晚更新 ✅ 第1期:Alamofire · ✅ 第2期:SDWebImage · ✅ 第3期:Kingfisher · ✅ 第4期:SnapKit · ✅ 第5期:ListDiff · ✅ 第6期:RxSwift · ✅ 第7期:Lottie · ✅ 第8期:MarkdownUI · ✅ 第9期:AFNetworking · ➡️ 第11期:DGCharts · ○ 第12期:Hero
Objective-C(以下简称 OC)的灵活性、动态性,核心源于其底层的 Runtime 机制。而 Runtime 所有动态行为(消息发送
在 objc_class 的核心字段中,superclass(父类指针)、cache(方法缓存)、bits(类数据指针+标志位)三者缺一不可。其中,cache 决定了方法调用的效率,bits 存储了类的核心数据(方法、属性、协议等),二者是理解 Runtime 动态机制的关键。
很多开发者使用 OC 多年,却只停留在“会用”层面,对objc_class 的底层结构、cache 的缓存机制、bits 的数据存储逻辑一知半解。本文将基于 Apple 开源的 objc4 源码(最新稳定版),逐行解析 objc_class、cache、bits 的底层实现,结合 Runtime 核心流程,让你彻底吃透 OC 类的底层逻辑。
在解析具体源码前,先明确两个核心前提,避免陷入细节误区:
objc_msgSend)。先看最基础的 objc_object 结构体(所有对象的祖宗),它是理解 objc_class 的前提:
// 所有OC对象的底层结构体(精简版,保留核心字段)
struct objc_object {
isa_t isa; // 64位联合体,存储类指针、引用计数、标志位等信息
};
// isa_t 的核心结构(ARM64架构,iOS真机环境)
union isa_t {
uintptr_t bits; // 原始64位数值,承载所有信息
// 位域展开(64位按位分配)
struct {
uintptr_t nonpointer : 1; // bit 0:是否是优化后的isa(0=纯指针,1=包含额外信息)
uintptr_t has_assoc : 1; // bit 1:是否有关联对象
uintptr_t has_cxx_dtor : 1; // bit 2:是否有C++析构函数
uintptr_t shiftcls : 33; // bit 3-35:类指针(右移3位存储,节省空间)
uintptr_t magic : 6; // bit 36-41:固定值0x1a,用于调试校验
uintptr_t weakly_referenced : 1; // bit 42:是否被弱引用
uintptr_t unused : 1; // bit 43:未使用
uintptr_t has_sidetable_rc : 1; // bit 44:引用计数是否溢出到SideTable
uintptr_t extra_rc : 19; // bit 45-63:引用计数-1(存储额外引用计数)
};
};
简单来说,isa 的核心作用是“标识对象的类型”——通过shiftcls 字段,对象能找到自己对应的类(objc_class),而类的 isa 则指向元类(Meta Class),这是 OC 实现方法调用的基础。
OC 中的“类”(如 NSObject、自定义类),底层本质是 objc_class 结构体的实例。以下是从 objc4 源码中提取的精简版 objc_class 结构体(保留核心字段,省略辅助方法),也是本文的核心分析对象:
// 类的底层结构体(继承自objc_object,因此包含isa字段)
struct objc_class : objc_object {
// 1. 父类指针:指向当前类的父类(如NSObject的父类是nil)
Class superclass;
// 2. 方法缓存:哈希表结构,缓存最近调用的方法,提升调用效率
cache_t cache;
// 3. 类数据指针+标志位:存储类的核心数据(方法、属性、协议等)
class_data_bits_t bits;
// 核心方法:从bits中取出类的可读写数据(class_rw_t)
class_rw_t *data() const {
return bits.data();
}
};
从源码可以看出,objc_class 继承自 objc_object,因此它本身也有 isa 字段(继承而来),同时新增了三个核心字段:superclass、cache、bits。
三者的核心关系的是:superclass 负责继承链的构建,cache 负责方法调用的缓存优化,bits 负责存储类的核心业务数据,三者协同支撑起 OC 类的所有动态行为。
我们日常使用的 Class 类型,本质是 objc_class 的指针别名,源码定义如下:
typedef struct objc_class *Class;
这就是为什么我们可以用 Class cls = [NSObject class]; 获取类对象——本质是获取 objc_class 结构体的指针。
在 OC 中,方法调用是高频操作(如 [self method]),如果每次调用都遍历类的方法列表查找,会严重影响性能。cache_t 的核心作用就是“缓存最近调用的方法”,下次调用时直接从缓存中取出,无需重复查找,这是 Runtime 优化方法调用效率的关键。
// 方法缓存结构体(哈希表实现)
struct cache_t {
// 缓存存储的数组(数组元素是cache_entry_t类型,存储方法名和函数指针)
bucket_t *_buckets;
// 缓存的容量(总是2的幂,如4、8、16,方便哈希计算)
mask_t _mask;
// 已缓存的方法数量(当count > mask * 3/4时,会触发缓存扩容)
mask_t _occupied;
// 核心方法:插入方法缓存
void insert(SEL sel, IMP imp, id receiver);
// 核心方法:查找方法缓存
IMP lookup(SEL sel);
};
其中,bucket_t 是缓存的“桶”,存储单个方法的缓存信息,源码如下:
// 单个缓存项(存储一个方法的信息)
struct bucket_t {
SEL _sel; // 方法名(选择子,本质是const char*,如@selector(method))
IMP _imp; // 函数指针(指向方法的具体实现代码地址)
// 辅助方法:获取方法名和函数指针
SEL sel() const { return _sel; }
IMP imp() const { return (IMP)((uintptr_t)_imp ^ (uintptr_t)this); }
};
理解 cache_t,关键要掌握“哈希表存储”“缓存插入”“缓存查找”“缓存扩容”四个核心流程,结合源码逻辑逐一拆解:
cache_t 采用“开放寻址法”实现哈希表:
SEL 的哈希值,对_mask(缓存容量-1)取模,得到当前方法在 _buckets 数组中的索引;SEL 和 IMP;这里 _mask = 容量 - 1(如容量为8,_mask=7),取模操作可简化为 hash & _mask,效率远高于传统取模运算,这也是缓存容量必须是2的幂的原因。
当我们第一次调用某个方法时,Runtime 会先查找方法列表,找到后将其插入 cache_t,核心步骤如下(结合源码逻辑简化):
SEL 的哈希值 hash = sel_hash(sel);注意:IMP 存储时会进行“异或加密”(_imp = (IMP)((uintptr_t)imp ^ (uintptr_t)this)),读取时再解密,这是苹果的安全优化,防止恶意篡改方法实现。
当我们再次调用该方法时,Runtime 会先从 cache_t 中查找,核心步骤如下:
SEL 的哈希值,得到索引 index = hash & _mask;当 _occupied(已缓存数量)超过 _mask * 3/4(缓存容量的75%)时,会触发缓存扩容,核心逻辑:
_buckets 数组(容量为新容量);_mask(新容量-1)和 _occupied(重置为旧的数量),释放旧数组内存。理解 cache_t 的缓存机制,能帮我们解释很多实际开发中的现象:
如果说 cache_t 是“方法调用的加速器”,那么 bits 就是“类的核心数据仓库”——它存储了类的所有核心信息,包括方法列表、属性列表、协议列表、成员变量列表等,是 Runtime 实现动态特性的核心载体。
bits 的类型是 class_data_bits_t,它本身是一个“64位整数”,低位存储标志位,高位存储指向 class_rw_t 的指针(类的可读写数据),这种设计既能节省内存,又能高效访问数据。
// bits的类型:存储类数据指针+标志位
struct class_data_bits_t {
private:
uintptr_t bits; // 64位整数,核心存储载体
public:
// 核心方法:从bits中取出class_rw_t指针(核心数据)
class_rw_t *data() const {
// FAST_DATA_MASK:掩码,用于过滤标志位,取出高位的指针地址
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
// 标志位操作方法(示例)
bool isSwiftLegacy() const { return getBit(FAST_IS_SWIFT_LEGACY); }
bool isSwiftStable() const { return getBit(FAST_IS_SWIFT_STABLE); }
private:
// 读取指定位置的标志位
bool getBit(uintptr_t bit) const {
return (bits & bit) != 0;
}
};
其中,FAST_DATA_MASK 是关键掩码(ARM64架构下),源码定义如下:
#define FAST_DATA_MASK 0x00007ffffffffff8UL
该掩码的作用是“过滤低位的标志位,保留高位的指针地址”——ARM64架构下,bits 的 bit 346 存储 2 存储标志位,通过 class_rw_t 指针,bit 0bits & FAST_DATA_MASK 可快速取出指针。
bits 的低位(bit 0~2)存储了3个核心标志位,用于标识类的类型和特性,源码定义如下:
FAST_IS_SWIFT_LEGACY = 1 << 0(bit 0):是否是旧版 Swift 类(OC 类该标志位为0);FAST_IS_SWIFT_STABLE = 1 << 1(bit 1):是否是新版 Swift 类(OC 类该标志位为0);FAST_HAS_DEFAULT_RR = 1 << 2(bit 2):是否有默认的 retain/release 方法(ARC 环境下,OC 类默认有)。这些标志位的作用是“快速区分类的类型”,Runtime 在处理方法调用、内存管理时,会根据这些标志位执行不同的逻辑。
bits.data() 会返回 class_rw_t 指针,class_rw_t 是“类的可读写数据”结构体,存储了类的方法、属性、协议等核心信息,源码精简如下:
// 类的可读写数据(runtime运行时可修改)
struct class_rw_t {
// 版本号(用于兼容不同的Runtime版本)
uint32_t version;
// 类的flags(标志位,如是否是元类、是否有分类等)
uint32_t flags;
// 方法列表(存储类的所有方法,包括实例方法和类方法)
method_array_t methods;
// 属性列表(存储类的所有属性)
property_array_t properties;
// 协议列表(存储类遵循的所有协议)
protocol_array_t protocols;
// 成员变量列表(存储类的所有成员变量)
ivar_array_t ivars;
};
其中,method_array_t、property_array_t 等都是“动态数组”(本质是指针数组),支持 Runtime 运行时动态添加(比如分类添加方法、属性),这也是 OC 支持“动态扩展”的核心原因。
bits 的工作流程非常简单,核心是“通过掩码取出数据指针,访问类的核心信息”,结合 Runtime 方法查找流程,可总结为:
objc_class->bits.data() 取出 class_rw_t 指针;结合前面的解析,我们用一个“方法调用”的完整流程,串联起 objc_class、cache、bits 的协同工作,让你彻底理解三者的关联:
[obj method],OC 编译器将其转化为 Runtime 函数调用 objc_msgSend(obj, @selector(method));从这个流程可以看出:cache 负责“加速查找”,bits 负责“存储数据”,objc_class 负责“组织关联”,三者协同,构成了 OC 方法调用的底层逻辑,也是 Runtime 动态机制的核心。
很多开发者会问:“搞懂这些源码,对实际开发有什么用?” 其实,Runtime 源码解析的价值,在于“解决底层问题、实现高级特性”,以下是3个常见的实战场景:
当调用未实现的方法时,会触发 unrecognized selector sent to instance 崩溃。通过理解 cache 和 bits 的查找流程,我们可以通过 Runtime 钩子(如 resolveInstanceMethod),动态添加方法实现,避免崩溃:
// 动态添加未实现的方法
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(unimplementedMethod)) {
// 动态添加方法实现
class_addMethod([self class], sel, (IMP)dynamicMethod, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
// 动态方法实现
void dynamicMethod(id self, SEL _cmd) {
NSLog(@"动态添加的方法实现");
};
方法交换是 OC 开发中常用的高级技巧,其底层依赖 bits 中的方法列表。通过修改 class_rw_t->methods 中方法的 IMP,可以实现方法交换:
// 方法交换
+ (void)swizzleMethod {
Class cls = [self class];
// 获取两个方法的SEL
SEL originalSel = @selector(originalMethod);
SEL swizzledSel = @selector(swizzledMethod);
// 获取方法实例
Method originalMethod = class_getInstanceMethod(cls, originalSel);
Method swizzledMethod = class_getInstanceMethod(cls, swizzledSel);
// 交换方法实现
method_exchangeImplementations(originalMethod, swizzledMethod);
}
OC 中不能直接给分类添加属性,但可以通过 Runtime 的关联对象机制实现,其底层依赖 objc_object 的 has_assoc 标志位(存储在 isa 中)和 bits 中的相关逻辑:
// 给分类添加关联属性
@interface NSObject (Associated)
@property (nonatomic, copy) NSString *associatedStr;
@end
@implementation NSObject (Associated)
- (NSString *)associatedStr {
return objc_getAssociatedObject(self, _cmd);
}
- (void)setAssociatedStr:(NSString *)associatedStr {
objc_setAssociatedObject(self, @selector(associatedStr), associatedStr, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
@end
通过对 objc_class、cache、bits 的源码解析,我们可以发现:OC Runtime 的核心本质,是“用结构体存储类和对象的信息,用哈希表优化查找效率,用动态数组支持扩展”。
总结三个核心要点,帮你快速掌握本文重点:
objc_class 是类的底层载体,继承自 objc_object,包含 superclass、cache、bits 三个核心字段,负责组织类的继承关系和核心数据;理解这些底层源码,不仅能帮你解决实际开发中的底层问题,更能让你从根源上理解 OC 的动态性,为后续学习更高级的 Runtime 特性(如元类、消息转发、分类加载)打下基础。毕竟,只有看透底层,才能真正掌控 OC 开发。
Since the LLVM 22 branch was cut, I've landed patches thatparallelize more link phases and cut task-runtime overhead. This postcompares current main against lld 22.1,
Headline: a Release+Asserts clang --gc-sections link is1.37x as fast as lld 22.1; Chromium debug with --gdb-indexis 1.07x as fast. mold and wild are still ahead — the last sectionexplains why.
lld-0201 is main at 2026-02-01 (6a1803929817);lld-load is main plus the new[ELF] Parallelize input file loading. mold andwild run with --no-fork so the wall-clocknumbers include the linker process itself.
Three reproduce tarballs, --threads=8,hyperfine -w 1 -r 10, pinned to CPU cores withnumactl -C.
| Workload | lld-0201 | lld-load | mold | wild |
|---|---|---|---|---|
clang-23 Release+Asserts, --gc-sections
|
1.255 s | 917.8 ms | 552.6 ms | 367.2 ms |
clang-23 Debug (no --gdb-index) |
4.582 s | 4.306 s | 2.464 s | 1.565 s |
clang-23 Debug (--gdb-index) |
6.291 s | 5.915 s | 4.001 s | N/A |
Chromium Debug (no --gdb-index) |
6.140 s | 5.904 s | 2.665 s | 2.010 s |
Chromium Debug (--gdb-index) |
7.857 s | 7.322 s | 3.786 s | N/A |
Note that llvm/lib/Support/Parallel.cpp design keeps themain thread idle during parallelFor, so--threads=N really utilizes N+1 threads.
wild does not yet implement --gdb-index — it silentlywarns and skips, producing an output about 477 MB smaller on Chromium.For fair 4-way comparisons I also strip --gdb-index fromthe response file; the no --gdb-index rows above use thatsetup.
A few observations before diving in:
--gdb-index surcharge on the Chromium link is+1.42 s for lld (5.90 s → 7.32 s) versus+1.12 s for mold (2.67 s → 3.79 s). This is currently oneof the biggest remaining gaps.--gdb-index, mold is 1.66x–2.22x as fast andwild 2.5x–2.94x as fast on this machine. There is plenty of roomleft.clang-23 Release+Asserts --gc-sections (workload 1) hascollapsed from 1.255 s to 918 ms, a 1.37x speedup over 10 weeks. Most ofthat came from the parallel --gc-sections mark, parallelinput loading, and the task-runtime cleanup below — each contributing amultiplicative factor.The same clang-23 Release+Asserts link, --threads=8, onan Apple M4 (macOS 15, system allocator for all four linkers):
| Linker | Wall | User | Sys | (User+Sys)/Wall |
|---|---|---|---|---|
| lld-0201 | 324.4 ± 1.5 ms | 502.1 ms | 171.7 ms | 2.08x |
| lld-load | 221.5 ± 1.8 ms | 476.5 ms | 368.8 ms | 3.82x |
| mold | 201.2 ± 1.7 ms | 875.1 ms | 220.5 ms | 5.44x |
| wild | 107.1 ± 0.5 ms | 456.8 ms | 284.6 ms | 6.92x |
--gc-sections markGarbage collection had been a single-threaded BFS overInputSection graph. On a Release+Asserts clang link,markLive was ~315 ms of the 1562 ms wall time (20%).
markParallel, a level-synchronized BFS. Each BFS levelis processed with parallelFor; newly discovered sectionsland in per-thread queues, which are merged before the next level. Theparallel path activates when!TrackWhyLive && partitions.size() == 1.Implementation details that turned out to matter:
depth < 3) beforepushing to the next-level queue. Shallow reference chains stay hot incache and avoid queue overhead.On the Release+Asserts clang link, markLive dropped from315 ms to 82 ms at --threads=8 (from 199 ms to 50 ms at--threads=16); total wall time 1.16x–1.18x.
Two prerequisite cleanups were needed for correctness:
Symbol::used into the existingstd::atomic<uint16_t> flags. The bitfield waspreviously racing with other mark threads.SharedFile::isNeeded from the mark walk.--as-needed used to flip isNeeded insideresolveReloc, which would have required coordinated writesacross threads; it is now a post-GC scan of global symbols.Historically, LinkerDriver::createFiles walked thecommand line and called addFile serially.addFile maps the file (MemoryBuffer::getFile),sniffs the magic, and constructs an ObjFile,SharedFile, BitcodeFile, orArchiveFile. For thin archives it also materializes eachmember. On workloads with hundreds of archives and thousands of objects,this serial walk dominates the early part of the link.
The pending patch will rewrite addFile to record aLoadJob for each non-script input together with a snapshotof the driver's state machine (inWholeArchive,inLib, asNeeded, withLOption,groupId). After createFiles finishes,loadFiles fans the jobs out to worker threads. Linkerscripts stay on the main thread because INPUT() andGROUP() recursively call back intoaddFile.
A few subtleties made this harder than it sounds:
BitcodeFile and fatLTO construction callctx.saver / ctx.uniqueSaver, both of which arenon-thread-safe StringSaver /UniqueStringSaver. I serialized those constructors behind amutex; pure-ELF links hit it zero times.ctx.memoryBuffers directly. To keep the outputdeterministic across --threads values, each job nowaccumulates into a per-job SmallVector which is merged intoctx.memoryBuffers in command-line order.InputFile::groupId used to be assigned inside theInputFile constructor from a global counter. With parallelconstruction the assignment race would have been unobservable but stillugly; ++nextGroupId into the serial driver loop and storesthe value into each file after construction.The output is byte-identical to the old lld and deterministic across--threads values, which I verified with diffacross --threads={1,2,4,8} on Chromium.
A --time-trace breakdown is useful to set expectations.On Chromium, the serial portion of createFiles accounts foronly ~81 ms of the 5.9 s wall, and loadFiles (after thispatch) runs in ~103 ms in parallel. Serial readFile/mmap isnot the bottleneck. What moves the needle is overlapping the per-fileconstructor work — magic sniffing, archive member materialization,bitcode initialization — with everything else that now kicks off on themain thread while workers chew through the job list.
Relocation scanning has been parallel since LLVM 17, but three caseshad opted out via bool serial:
-z nocombreloc, because .rela.dyn mergedrelative and non-relative relocations and needed deterministicordering.MipsGotSection is mutated duringscanning.ctx.ppc64noTocRelax (aDenseSet of (Symbol*, offset) pairs) waswritten without a lock..rela.dyn withcombreloc=true; the only remaining effect of-z nocombreloc is suppressing DT_RELACOUNT. ctx.ppc64noTocRelax with the already-existingctx.relocMutex, which is only taken on rare slow paths.After these changes, only MIPS still runs scanning serially.
getSectionPiece
Merge sections (SHF_MERGE) split their input into"pieces". Every reference into a merge section needs to map an offset toa piece. The old implementation was always a binary search inMergeInputSection::pieces, called fromMarkLive, includeInSymtab, andgetRelocTargetVA.
getSectionPiece uses offset / entsizedirectly.Defined symbols pointing into mergesections, the piece index is pre-resolved duringsplitSections and packed into Defined::valueas ((pieceIdx + 1) << 32) | intraPieceOffset.The binary search is now limited to references via section symbols(addend-based), which is common on AArch64 but rare on x86-64 where theassembler emits local labels for .L references intomergeable strings. The clang-relassert link with--gc-sections is 1.05x as fast.
llvm/lib/Support/Parallel.cpp
All of the wins above rely onllvm/lib/Support/Parallel.cpp, the tiny work-stealing-ishtask runtime shared by lld, dsymutil, and a handful of debug-info tools.Four changes in that file mattered:
parallelFor used to pre-split work into up toMaxTasksPerGroup (1024) tasks and spawn each through theexecutor's mutex + condvar. It now spawns only ThreadCountworkers; each grabs the next chunk via an atomic fetch_add.On a clang-14 link (--threads=8), futex calls dropped from~31K to ~1.4K (glibc release+asserts); wall time 927 ms → 879 ms. Thisis the reason the parallel mark and parallel scan numbers are worthquoting at all — on the old runtime, spawn overhead was a real fractionof the work being parallelized.TaskGroup::spawn() replaced the mutex-basedLatch::inc() with an atomic fetch_add andpasses the Latch& through Executor::add()so the worker calls dec() directly. Eliminates onestd::function construction per spawn.Executor abstract base class.ThreadPoolExecutor was always the only implementation;add() and getThreadCount() are now directcalls instead of virtual dispatches.TaskGroup via work-stealing.Historically, nested groups ran serially to avoid deadlock (the threadthat was supposed to run a nested task might be blocked in the outergroup's sync()). Worker threads now actively execute tasksfrom the queue while waiting, instead of just blocking. Root-levelgroups on the main thread keep the efficient blockingLatch::sync(), so the common non-nested case pays nothing.In lld this lets SyntheticSection::writeTo calls withinternal parallelism (GdbIndexSection,MergeNoTailSection) parallelize automatically when calledfrom inside OutputSection::writeTo, instead of degeneratingto serial execution on a worker thread — which was the exact situationD131247 had worked aroundby threading a root TaskGroup all the way down.demoteAndCopyLocalSymbols. Each file collectslocal Symbol* pointers in a per-file vector viaparallelFor, which are merged into the symbol tableserially. Linking clang-14 (--no-gc-sections) with its 208K.symtab entries is 1.04x as fast.To locate the gap I ran lld --time-trace,mold --perf, and wild --time on the Chromium--gdb-index link (--threads=8). Grouped intocomparable phases:
| Phase | lld | mold |
|---|---|---|
| Parse input files | 2778 ms | 1034 ms |
| Scan relocations | 233 ms | 103 ms |
| Assign / finalize layout | 750 ms | ~150 ms |
| Symtab + synthetic finalize | 570 ms | ~80 ms |
| Write sections (copy chunks) | 533 ms | 558 ms |
| Create gdb index | 1317 ms | 911 ms |
| Wall | 6742 ms | 3428 ms |
That leaves four meaningful gaps, in order of absolute impact:
Parse input files: 2.78 s vs 1.03 s, ~52% of the totalgap. Same ratio on clang-debug (2.49 s vs 1.09 s). The phase isalready parallel; the gap is pure constant factor in the per-objectparse path (reading section headers, interning strings, splittingCIEs/FDEs, resolving symbols into the global table). wild is even moreextreme here — its whole "Load inputs into symbol DB" is ~255 ms onChromium, which is where most of its overall advantage comes from.
Assign / finalize / symtab finalize: ~1.3 s vs ~0.23s. finalizeAddressDependentContent,assignAddresses, finalizeSynthetic,Add symbols to symtabs, and Finalize .eh_frametogether cost ~1.3 s on Chromium. mold's equivalents(compute_section_sizes, compute_symtab_size,create_output_sections, set_osec_offsets)total ~230 ms. .symtab alone is ~127 ms lld vs ~27 ms moldon clang-debug; I have a local branch that turnsSymbolTableBaseSection::finalizeContents into aprefix-sum-driven parallel fill and replaces thestable_partition + MapVector shuffle withper-file lateLocals buffers. 1640 ELF tests pass; notposted yet.
Create gdb index: +1.32 s lld vs +0.91 s mold onChromium. Varies by workload — on clang-debug the two are within 200 ms(1.73 s vs 1.54 s). The work is embarrassingly parallel per input, butlld funnels a lot of string interning through a singleDenseMap (sharded, but still); mold uses a lock-freeConcurrentMap sized by HyperLogLog.
Scan relocations: 233 ms vs 103 ms. Small absolutebut a clean 2.3x ratio. Target-specific scanning (theAdd target-specific relocation scanning for … series fromlast year) already removed much of the dispatch overhead; what remainsis per-relocation work in the x86-64 path.
Interestingly, writing section content is not a gap.lld spends 533 ms in Write sections vs mold's 558 ms incopy_chunks vs wild's 574 ms inWrite data to file — all within noise of each other. Theearlier assumption that .debug_* section writes were a lldweakness didn't survive measurement; the --gdb-indexsurcharge really lives in index construction, not the write.
wild is worth calling out separately: its user time is comparable tolld's but its system time is roughly half, and its parse phase is 4-8xfaster than either of the C++ linkers. mold is at the other extreme —the highest user time on every workload, bought back by aggressiveparallelism.
Claude Code 支持多种认证方式,包括 AWS Bedrock、Google Vertex AI、Anthropic API Key 和 Claude 订阅(Pro/Max/Team/Enterprise)。当你从 Bedrock 切换到 Team 订阅时,需要清除 Bedrock 的配置,否则 Claude Code 会一直走 Bedrock 通道。
使用 Bedrock 认证时,/login 和 /logout 命令是被禁用的(官方设计如此)。因此你无法在 Bedrock 模式下直接切换登录方式。
Bedrock 配置的来源有两种:
export 或写在 ~/.zshrc / ~/.bashrc 中~/.claude/settings.json 的 env 字段中很多用户(尤其是通过 setup wizard 配置的)的 Bedrock 设置是写在 settings.json 里的,单纯 unset 环境变量并不能解决问题。
1 |
# 检查环境变量 |
如果在 settings.json 中看到类似以下内容,说明 Bedrock 配置在这里:
1 |
{ |
如果配置在 settings.json 中,编辑 ~/.claude/settings.json,删除 env 中所有 Bedrock 相关的键值对:
CLAUDE_CODE_USE_BEDROCKAWS_REGIONANTHROPIC_MODELCLAUDE_CODE_AWS_PROFILECLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS(Bedrock 专用)CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC(Bedrock 专用)保留你仍需要的配置(如代理、权限设置等)。清理后的文件示例:
1 |
{ |
如果配置在环境变量中,清除相关变量:
1 |
unset CLAUDE_CODE_USE_BEDROCK |
同时检查并清理 shell 配置文件:
1 |
grep -r "CLAUDE_CODE_USE_BEDROCK\|ANTHROPIC_MODEL\|ANTHROPIC_API_KEY" \ |
1 |
claude |
此时应该会弹出登录方式选择界面,选择 「Claude account with subscription」,然后在浏览器中授权你的 Team 计划。
启动后,欢迎界面底部应显示类似:
1 |
Sonnet 4.6 · Claude Pro(或 Team) |
而不是之前的 arn:aws:bedrock:...。
也可以在交互界面中输入 /status 确认当前认证方式。
如果需要使用 Opus 模型,在交互界面中输入:
1 |
/model |
用方向键选择 Opus 即可。
Claude Code 的认证优先级从高到低为:
CLAUDE_CODE_USE_BEDROCK / CLAUDE_CODE_USE_VERTEX / CLAUDE_CODE_USE_FOUNDRY)ANTHROPIC_AUTH_TOKEN 环境变量ANTHROPIC_API_KEY 环境变量apiKeyHelper 脚本/login)只要高优先级的认证方式存在,低优先级的就不会生效。所以必须彻底清除 Bedrock 配置,订阅认证才能生效。
api.anthropic.com,切换后可能需要更换代理或去掉代理配置。CLAUDE.md 等本地文件不受认证方式影响,切换后照常保留。对话历史不会跨会话保存,这点两种方式一样。在 Xcode 26.4 + iOS 26.4 模拟器上运行项目,app 卡在 Launching 界面,始终无法进入主界面。控制台有大量 objc 类重复实现的警告(AuthKitUI / AuthKit 框架重复),但这些是系统 bug,与本次崩溃无关。
使用 LLDB 暂停进程,thread list 看到主线程异常:
thread #1: tid = 0xb124db, 0x0000000118fc9c10 CoreFoundation`-[__NSArrayM insertObject:atIndex:] + 251, queue = 'com.apple.main-thread'
在 LLDB 中执行 thread select 1 + bt,得到完整调用栈:
frame #0: CoreFoundation`-[__NSArrayM insertObject:atIndex:] + 251
frame #1: FNCategory`-[NSMutableArray safe_insertObject:atIndex:] at NSMutableArray+FN.m:68
frame #2: FNCategory`-[NSMutableArray safe_addObject:] at NSMutableArray+FN.m:51
frame #3: CoreFoundation`-[NSEnumerator allObjects] + 189
frame #4: AXCoreUtilities`-[AXBinaryMonitor _frameworkNameForImage:]
frame #5: AXCoreUtilities`-[AXBinaryMonitor _handleLoadedImagePath:]
frame #6: AXCoreUtilities`___axmonitor_dyld_image_callback_block_invoke
关键结论:系统无障碍框架 AXCoreUtilities 在动态加载镜像(dyld image load)时,触发了一个回调,该回调内部调用了 NSEnumerator allObjects,而这个 allObjects 底层最终调用了 NSMutableArray addObject:。
由于项目通过 Method Swizzling 将系统的 addObject: 替换成了自定义的 safe_addObject:,这个系统内部调用被"劫持"进了我们的代码。
而 safe_addObject: 内部调用了 safe_insertObject:atIndex:,这里对 NSMutableArray 插入对象时发生了崩溃。
这是一个经典的 Method Swizzling 副作用问题,iOS 26 改变了 AXCoreUtilities 的内部实现,触发了长期潜伏的 bug。
完整调用链如下:
AXCoreUtilities(系统无障碍框架)在 dyld 加载镜像时触发内部回调insertObject:atIndex:
insertObject:atIndex: 已被替换成 safe_insertObject:atIndex:,系统内部调用被"劫持"进了我们的代码safe_insertObject:atIndex: 内部再调用 [self safe_insertObject:anObject atIndex:index](即原始方法),但此时 self 是系统内部的私有数组类型,不是普通的 __NSArrayM,导致无限递归或调用到了错误的 IMP,最终崩溃问题的本质是:Swizzling 作用在父类(NSMutableArray)上,但系统传入的是私有子类对象,Swizzling 后的方法实现与私有类的内存布局不兼容,在 iOS 26 收紧了 AXCoreUtilities 的调用时序之后,这个潜在冲突被激活。
正规的修复思路是在 SwizzlingMethod 里加类型保护,确保只 swizzle __NSArrayM 本身而不影响其私有子类。但由于 FNCategory 是 Pod,还有 AFNetworking、DoraemonKit 等我们无法直接修改源码的三方库存在同样问题,所以统一在 Podfile post_install 里做全局兼容处理。
坑 1:以为是 objc 类重复警告导致的
启动时控制台打印了大量 Class AKAlertImageURLProvider is implemented in both AuthKitUI and AuthKit 的警告,误以为是这些重复类导致崩溃。实际上这是 iOS 26.4 模拟器运行时自身的打包问题,与启动卡死无关。
坑 2:只修复了 FNCategory,没有扩大范围
最初只在 FNCategory 的 NSMutableArray+FN.m 里加了 nil 保护,但 AFNetworking 和 DoraemonKit 也有同样模式的 Swizzling,同样存在风险。
不针对单个文件做字符串替换,而是在 Podfile 的 post_install 阶段,全局扫描所有 Pod 源文件,找到所有 method_exchangeImplementations( 调用,在其前面统一注入 nil 保护。
post_install do |installer|
# ... 其他 post_install 逻辑 ...
# 全局修复:为所有 Pod 的 method_exchangeImplementations 调用注入 nil 保护
# 防止 iOS 26 系统框架在 dyld 镜像加载回调中触发 Swizzled 方法时崩溃
fixed_count = 0
Dir.glob('Pods/**/*.{m,mm}').each do |file|
content = File.read(file)
next unless content.include?('method_exchangeImplementations(')
new_content = content.gsub(
/^(\s*)(method_exchangeImplementations\((\w+)\s*,\s*(\w+)\s*\)\s*;)/
) do
indent = $1
full_call = $2
arg1 = $3
arg2 = $4
"#{indent}if (#{arg1} && #{arg2}) #{full_call}"
end
if new_content != content
File.chmod(0644, file)
File.write(file, new_content)
puts "✅ 已修复 #{file} 的 method_exchangeImplementations nil 保护"
fixed_count += 1
end
end
puts "共修复 #{fixed_count} 处 method_exchangeImplementations nil 保护" if fixed_count > 0
end
执行 pod install 后的输出:
✅ 已修复 Pods/AFNetworking/AFNetworking/AFNetworking/AFURLSessionManager.m 的 method_exchangeImplementations nil 保护
✅ 已修复 Pods/DoraemonKit/iOS/DoraemonKit/Src/Core/Category/NSObject+Doraemon.m 的 method_exchangeImplementations nil 保护
✅ 已修复 Pods/DoraemonKit/iOS/DoraemonKit/Src/Core/Plugin/Performance/StartTime/DoraemonStartTimeViewController.m 的 method_exchangeImplementations nil 保护
✅ 已修复 Pods/FNCategory/FNCategory/Classes/NSMutableArray+FN.m 的 method_exchangeImplementations nil 保护
✅ 已修复 Pods/FNCategory/FNCategory/Classes/NSObject+FNSwizzle.m 的 method_exchangeImplementations nil 保护
✅ 已修复 Pods/FNCategory/FNCategory/Classes/UIViewController+FNFullScreen.m 的 method_exchangeImplementations nil 保护
Integrating client project
Pod installation complete! There are 32 dependencies from the Podfile and 35 total pods installed.
共修复 6 处,涉及 AFNetworking、DoraemonKit、FNCategory 三个 Pod。
| 项目 | 说明 |
|---|---|
| 问题类型 | Method Swizzling 缺少 nil 保护,被系统内部回调触发 |
| 触发条件 | iOS 26 改变了 dyld 镜像加载回调时序,在类注册完成前触发 Swizzle |
| 崩溃位置 |
NSMutableArray insertObject:atIndex: → safe_insertObject:atIndex:
|
| 修复方式 | Podfile post_install 全局注入 if (A && B) nil 保护 |
| 优点 | 一次修复,覆盖所有 Pod,无需逐个修改,pod update 后自动重新修复 |
| 注意 | 这是 Swizzling 的通用最佳实践,不局限于 iOS 26,建议所有项目都加上 |
可可已经在三年级下学期了,数学似乎还是有点问题。这个阶段考试成绩其实都不会太差,但一旦作业或考卷上的错题并非粗心大意就值得警惕。乘除法是二年级学的,三年级已经在学两位数除一位数的除法。但会计算并不难,计算只是一项机械性技能,难的是理解乘除法的意义。理解乘除比理解加减法困难的多。
我翻出几个月前的一篇 blog,发现过了 4 个月,她的问题依旧:乘除法作为一项计算技能和其背后的意义是割裂的。这导致了很多问题到底如何解决一筹莫展。固然多作练习就能开悟,毕竟几乎没有成人回头看小学数学会觉得难以理解的。但我还是想尽力搞清楚她的小脑袋里到底是哪打结了。
今晚讲一道相当简单的数学题:
有 96 个鸡蛋,8 个一盒装,可以装多少盒?
可可不知道如何解决这个问题,我一开始是很诧异的。我先反复确认她理解了题目的文本,并非语言理解的问题。真的是无法联想到应该使用除法这个工具,而 96 这个数字过大,即使不使用除法,也不知道该如何处理。我默不作声,让她仔细想想,她愣在那里不知所措,都急得掉眼泪了。
我决定一步步推演这个问题。
先问一个简单的版本:有 12 个鸡蛋,10 个一盒装,最多可以装满几盒?
我本以为她能一口答出,但可能是前面的问题受挫,她还是不知道如何下手。我想想,从桌游盒中找了一堆 token 和若干小碗,说你自己装碗试试吧。装完 12 个后,又把问题改成了 30 个,她重新摆弄了一次,这下明白了。
我说,现在要把道具收起来了,换成草稿纸,你该如何解决这个问题呢?
我教她用减法:用 30 - 10 = 20 , 20 - 10 = 10 ,10 - 10 = 0 ;数一下一共减了 3 次。可可说,我知道了,其实不用数,只要看数字是几十,那么就是几盒了。
那么,回到一开始的问题,不是 10 个一盒而是 8 个一盒就不能直接看出来了,该怎么办呢?可可说那我也会:她从 96 - 8 = 88 开始一步步的做减法计算,很耐心的减到了 0 ,数了一下是 12 ,中间居然没有算错。
我说,96 / 8 = 12 ,并不真的要花这么多时间做减法。你其实会算除法,只是不知道除法有什么用。除法就是连续计算减法的次数,就好比乘法就是连续做多次加法一样。你需要把 token 一个个放进碗里的过程抽象化成数字写到草稿纸上,打草稿就是把脑子里想的东西具象化出来。这个过程借助数学符号可以更简单。数字是符号,加减乘除也是符号,符号能帮助你思考,但先要明白这些符号代表的道理。
我再换个问题:
有 80 个鸡蛋,8 个一盒装,可以装多少盒?
可可没犹豫,马上告诉我是 8 盒。我说你别着急,拿草稿纸仔细算一下。她算完不好意思的告诉我是 10 盒。我画了张矩形图,给她讲解了一下 8 x 10 = 10 x 8 的道理:10 行 8 个与 8 行 10 个其实只是图形旋转了一下,总数是一样的。
那么,从 96 个鸡蛋里先拿出 80 个装满 10 盒后,剩下的还可以装多少盒呢?她计算了一下 96 - 80 = 16 ,16 / 8 = 2 ;然后 10 盒与 2 盒合在一起也是 12 。
再看除法的竖式草稿,其实是一样的。
今天花了一个小时讲这道数学题(她的考卷上的错题),这次似乎真的懂了。
你也可以为这个项目出一份力,如果发现有价值的信息、文章、工具等可以到 Issues 里提给我们,我们会尽快处理。记得写上推荐的理由哦。有建议和意见也欢迎到 Issues 提出。
@Kyle-Ye: Swift 6.3 正式发布,带来了多项语言和工具链层面的重要更新。语言特性方面,新增 @c attribute 允许将 Swift 函数和枚举直接暴露给 C 代码并自动生成头文件,新增 :: 模块名选择器语法解决多模块同名 API 的歧义问题,同时为库作者提供了 @specialize、@inline(always)、@export(implementation) 等性能控制属性。构建工具方面,Swift Package Manager 预览集成了统一的 Swift Build 引擎,并新增预编译 Swift Syntax 支持和 swift package show-traits 命令。平台扩展方面,Embedded Swift 在 C 互操作和调试能力上有显著改进,同时本版本也是 Swift SDK for Android 的首个正式发布版本。此外 Swift Testing 新增了 warning 级别的 issue severity 和测试取消支持,DocC 也增加了 Markdown 输出和代码块标注等实验性功能。建议所有 Swift 开发者关注并评估升级。
@Barney:这篇文章记录了 Xcode 26.4 的一个很影响调试体验的回归问题:Mac 到 iOS Simulator 的剪贴板同步失效,Cmd + V 没反应,长按输入框也看不到 Paste。作者尝试了重启 Simulator、切换 Automatically Sync Pasteboard、killall pboard 和重置权限等常见手段都无效,最后给出一个可立即落地的 workaround:直接用 xcrun simctl pbcopy booted 把宿主机剪贴板内容写入当前启动中的模拟器。文末还补了一个更顺手的版本 pbpaste | xcrun simctl pbcopy booted,基本可以当作临时替代方案。适合最近升级到 Xcode 26.4、正好被这个问题卡住的同学收藏。
@Cooper Chen:这篇文章介绍了如何在 Apple Foundation Models 框架中追踪 token 使用情况,并将其作为优化大模型应用的关键指标。作者通过示例展示了如何统计指令、prompt 和完整对话的 token 消耗,并结合上下文窗口评估占用比例,判断是否接近限制。文章还总结了多种优化方法,如精简 prompt、减少冗余内容和拆分长对话,以提升性能和降低成本。同时提供可视化工具帮助开发者直观分析 token 分布。整体而言,这篇文章强调了以 token 为核心的工程优化思路,对构建高效 LLM 应用具有实用价值。
@AidenRao:Point-Free 在这篇 Beta Preview 里预告了 Composable Architecture 2.0(Composable Architecture 是 Point ‑ Free 团队开源的一套 Swift 应用架构 / 框架,用来“以一致且可理解的方式”组织业务逻辑,并把 组合(composition) 和 可测试性(testing) 当作一等公民。它既可以用于 SwiftUI,也能用于 UIKit 等场景。):这是一次从底层模型到日常写法都“重新推倒重来”的大版本更新。它把 API 词汇刻意对齐 SwiftUI(例如 onChange、preferences、生命周期回调等),让你用熟悉的视图心智模型去写业务逻辑:View 负责“渲染什么”,而新的 Feature 负责“要做什么”。
@阿权:作者介绍了自己的一套 AI Agent Skill,可以自动分析并优化 Xcode 项目的编译速度。原理是同城修改 Xcode 项目配置来优化编译流程。处理了影响编译速度的几个因素:代码复杂度、build phases、Swift Package 依赖、增量构建等(具体分析过程可参考 Build performance analysis for speeding up Xcode builds)。这套 skill 工作流程如下:
文章提供了 AI Agent 提升 iOS 研效的另一种思路,希望对你有所启发。
@DylanYang:本文作者主要讲解了 SwiftUI 中被 @observable 修饰的类初始化方法多次执行的问题,核心原因是使用 @State 存储 ViewModel 时,会随 View 频繁重建重复执行初始化逻辑,搭配 NavigationStack 导航场景会进一步加剧该问题。作者同时给出了.task 延迟赋值、将 ViewModel 托管至上层视图等解决方案,并提醒开发者不要在 init 中编写耗时操作与副作用逻辑。
重新开始更新「iOS 靠谱内推专题」,整理了最近明确在招人的岗位,供大家参考
具体信息请移步:https://www.yuque.com/iosalliance/article/bhutav 进行查看(如有招聘需求请联系 iTDriverr)
我们是「老司机技术周报」,一个持续追求精品 iOS 内容的技术公众号,欢迎关注。
关注有礼,关注【老司机技术周报】,回复「2024」,领取 2024 及往年内参
同时也支持了 RSS 订阅:https://github.com/SwiftOldDriver/iOS-Weekly/releases.atom 。
🚧 表示需某工具,🌟 表示编辑推荐
预计阅读时间:🐎 很快就能读完(1 - 10 mins);🐕 中等 (10 - 20 mins);🐢 慢(20+ mins)
你有没有这种体验:让 AI 帮你写个页面,它生成的代码颜色全是瞎编的、间距全靠猜、按钮样式跟你们产品完全不搭?
然后你甩给它一份设计规范的 PDF,指望它能“学会”你们的设计体系。
结果呢?AI 看 PDF 基本等于盲人摸象——它看到的是一堆碎片化的文字和完全无法理解的截图。那些精心排版的视觉示例,在 AI 眼里跟噪音差不多。
问题不是 AI 不行,而是我们给 AI 的“学习资料”不对。
传统设计规范长这样:一份精美的 PDF,里面有品牌色卡、组件截图、do/don’t 的对比图、各种排版示例。
这东西给人看,完美。给 AI 看,灾难。
原因很简单:
第一,PDF 是视觉媒介,AI 是文本动物。 PDF 里那些色卡截图,AI 根本“看”不出来里面的色值是什么。它需要的是 #1A73E8 这个字符串,不是一个蓝色方块的图片。
第二,设计规范的“规则”通常是散文式的。 比如“不要在一个页面里放太多主按钮”——这句话人类一看就懂,但 AI 很难把它转化成一个可执行的判断。太多是多少?什么算主按钮?什么算一个页面?
第三,知识是碎片化的。 颜色写在第 3 页,间距写在第 7 页,按钮的规范在第 12 页,而按钮用到的颜色和间距需要 AI 自己去关联。这种跨页面的信息拼装,AI 做起来很吃力。
一句话总结:视觉示例给人看,结构化数据给 AI 读。
具体来说,就是把传统设计规范里的每一个设计决策,都翻译成 AI 能精确解析的格式。
那用什么格式呢?我让 Claude Opus 帮我调研了一下,它推荐的方案是:Markdown + JSON + YAML 的组合。其中:
为什么不统一用一种格式?因为各有所长。JSON 适合定义纯数据(Design Token),YAML 适合描述有层次的组件规范(因为可读性更好),Markdown 适合写需要段落和叙事的内容(设计原则、模式指引)。
传统规范里,设计师说“主色调是品牌蓝”,然后在 PDF 里放一个色块。
AI 友好的方式是把它变成一个 Token:
1 |
{ |
注意这里不只有色值,还有 usage(什么场景用)和 wcag_aa(是否满足无障碍标准)。这些上下文信息对 AI 来说极其重要——它不只要知道“是什么颜色”,还要知道“什么时候用”和“为什么选这个颜色”。
同理,字号、间距、圆角、阴影、动画时长……所有数值类的设计决策,都应该 Token 化。
传统规范里,一个按钮组件的描述可能是一页截图加几段说明文字。
AI 友好的方式是用 YAML 写一个完整的结构化定义:
1 |
component: Button |
这里面有几个关键设计:
用花括号引用 Token,比如 {color.brand.primary}。这样 AI 在生成代码时,会自动去 Token 文件里查对应的值,而不是硬编码一个色值。整个系统是关联的。
明确列出所有状态。人类设计师可能觉得“hover 状态不用说大家都知道”,但 AI 需要你把它列出来。缺什么它就不做什么。
有变体(variants)和尺寸(sizes)的穷举。 AI 最擅长在有限集合里做选择,而不是在模糊描述里做推断。
这是最关键的一步。
传统规范里的“Don’t”通常配一张错误示例截图,AI 完全看不懂。
AI 友好的方式是把它写成带 ID、有严重等级、能机器检查的规则:
1 |
rules: |
这种格式有几个好处:
你的设计规范可能有几十个文件,AI 不知道该先看哪个。你需要一个 README.md 作为入口,就像给 AI 画一张地图:
1 |
## AI 使用指引 |
这个入口文件告诉 AI 三件事:有哪些文件、每个文件是干嘛的、不同任务应该按什么顺序查阅哪些文件。
传统规范里的设计原则通常很抽象:“我们追求简洁”。
AI 友好的方式是让原则可操作——不只说“是什么”,还说“怎么用”和“冲突时怎么办”:
1 |
### 清晰优先于美观 |
特别是要提供一个 原则冲突解决矩阵。比如“清晰”和“包容性”冲突时谁优先?“性能”和“一致性”冲突时呢?人类设计师靠直觉判断,AI 需要明确的规则。
说了这么多,最终的目录结构长这样:
1 |
design-system/ |
每层的分工很清晰:
不要一步到位。 你不需要一次把整个设计规范都改造完。可以先从 Design Token 开始——把颜色和字号从 PDF 里抽出来做成 JSON 文件,这一步投入产出比最高。
保持两个版本同源。 理想情况下,JSON/YAML 是“源文件”,PDF 版本从源文件自动生成。这样改一处,两边都更新。如果做不到自动生成,至少保证人工同步。
给每个决策加上“为什么”。 这是很多人最容易忽略的。AI 在遇到边缘情况时,rationale 字段就是它做判断的依据。没有 rationale,它只会机械执行规则;有了 rationale,它能理解意图,做出更灵活的判断。
把规范放到代码仓库里。 设计规范不应该是一个飞书文档或者 Figma 链接,而是一个 Git 仓库里的文件夹。这样 AI 工具可以直接读取,开发者可以在 CI/CD 里做自动检查,版本变更有迹可循。
实际测试。 改造完之后,拿你的 AI 工具(Claude、Cursor、Copilot 等)实际跑一遍:让它基于你的设计规范生成一个页面,看看它是不是真的引用了 Token、遵守了规则。不好使就迭代。
AI 时代的设计规范,本质上是一个 API——它不再只是给人“阅读”的文档,而是给机器“调用”的接口。
格式变了,但设计的本质没变。你仍然需要好的设计判断来决定什么颜色、什么间距、什么交互模式。只是表达方式要变一变:从“让人看懂”升级为“人机双读”。
如果你的设计师不知道如何输出上面的文件,没关系,把这篇文章发给你的 AI Agent(推荐使用 Claude Opus 4.6),然后说:我需要按照文章中的方案来产生一套面向 AI 的设计规范,你来帮我完成,现在你告诉我需要哪些文件和资料,我来负责提供。
放心,AI 会一步一步带着你完成这份规范。
希望对你有用。
本文是想通过一个例子来讲述,AI在修复Bug方面令人惊艳的能力。
先来看一个Crash日志的堆栈信息:
Termination Reason:<RBSTerminateContext| domain:10 code:0x8BADF00D
explanation:scene-create watchdog transgression: application<com.xxx.aaa>:
34689 exhausted real (wall clock) time allowance of 3.43 seconds
//
Thread 0 Crashed:
0 libsystem_pthread.dylib _pthread_mutex_lock$VARIANT$armv81 + 120
1 libc++.1.dylib std::__1::mutex::lock() + 12
2 libicucore.A.dylib icu::Locale::getDefault() + 32
3 libicucore.A.dylib icu::Locale::init(char const*, signed char) + 1400
4 libicucore.A.dylib _ures_getLocaleByType + 436
5 libicucore.A.dylib icu::DecimalFormatSymbols::initialize(icu::Locale const&, UErrorCode&, signed char, icu::NumberingSystem const*) + 256
6 libicucore.A.dylib icu::DecimalFormatSymbols::DecimalFormatSymbols(icu::Locale const&, icu::NumberingSystem const&, UErrorCode&) + 236
7 libicucore.A.dylib icu::number::LocalizedNumberFormatter::getDecimalFormatSymbols() const + 4608
8 libicucore.A.dylib icu::number::LocalizedNumberFormatter::getDecimalFormatSymbols() const + 1632
9 libicucore.A.dylib icu::number::LocalizedNumberFormatter::formatImpl(icu::number::impl::UFormattedNumberData*, UErrorCode&) const + 128
10 libicucore.A.dylib icu::SimpleDateFormat::zeroPaddingNumber(icu::NumberFormat const*, icu::UnicodeString&, int, int, int) const + 524
11 libicucore.A.dylib icu::SimpleDateFormat::subFormat(icu::UnicodeString&, char16_t, int, UDisplayContext, int, char16_t, icu::FieldPositionHandler&, icu::Calendar&, UErrorCode&) const + 904
12 libicucore.A.dylib icu::SimpleDateFormat::_format(icu::Calendar&, icu::UnicodeString&, icu::FieldPositionHandler&, UErrorCode&) const + 688
13 libicucore.A.dylib icu::SimpleDateFormat::format(icu::Calendar&, icu::UnicodeString&, icu::FieldPosition&) const + 80
14 libicucore.A.dylib icu::DateFormat::format(double, icu::UnicodeString&, icu::FieldPosition&) const + 124
15 libicucore.A.dylib _udat_format + 356
16 CoreFoundation ___cficu_udat_format + 64
17 CoreFoundation _CFDateFormatterCreateStringWithAbsoluteTime + 180
18 Foundation -[NSDateFormatter stringForObjectValue:] + 160
19 MyAPP -[HAMLaunchMonitor startUUID] + 136
... MYApp 其它调用函数
传统解决步骤:
0x8BADF00D,确定是被系统WatchDog检测到杀死了进程导致的崩溃。可以看到我们人工查看这个问题有几个瓶颈:
- 需要手动找到文件查看分析函数调用链,速度慢。
- 对于顶部的系统函数,我们一般是缺乏了解的,不知道里面发生了什么导致出了问题。
- 弄清楚了上面内容后,我们的解决方法也是基于自己认知的,存在偏差。
而这几个瓶颈,交给AI后那都不是事儿。
复制堆栈信息交给AI工具(我目前用的是Qoder),AI直接给出了根本原因及有效的方案:
Crash 类型
这是一个 Watchdog 超时 Crash(
0x8BADF00D= "ate bad food"),发生在 scene-create(应用启动/场景创建) 阶段。系统允许的时间为 3.43 秒,但应用在该时间内未能完成启动。根因分析
从调用栈来看,崩溃发生在 [
-[HAMLaunchMonitor startUUID]] 方法中:- (NSString *)startUUID { NSDateFormatter *formatter = [NSDateFormatter new]; formatter.dateFormat = @"yyMMddHHmmssSSS"; formatter.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]; NSString* time = [formatter stringFromDate:[NSDate date]]; // ... }主线程被阻塞在
pthread_mutex_lock上,具体路径是:
startUUID调用NSDateFormatter stringForObjectValue:进行日期格式化- 底层 ICU 库执行
SimpleDateFormat::format进行实际格式化- ICU 在格式化数字时需要获取
DecimalFormatSymbols- 这触发了
icu::Locale::getDefault()来获取系统默认 localegetDefault()内部需要获取一个 全局互斥锁(std::mutex)- 该锁被其他线程持有,导致主线程死等(
pthread_mutex_lock卡住)核心问题
锁竞争(Lock Contention) —— ICU 库内部有一个全局的
std::mutex保护Locale::getDefault()。在应用启动阶段,如果有多个线程同时使用NSDateFormatter、NSNumberFormatter或其他依赖 ICU 的 API,就会争抢这把锁。主线程被阻塞等锁,累计时间超过 watchdog 限制,系统杀掉了应用。加剧因素
- 启动阶段调用
startUUID:该方法在 [commonParameters] 中被调用,而commonParameters在 [appDidFinishLaunch] 流程中执行——正处于 watchdog 监控最严格的时间窗口。- 每次调用都创建新的
NSDateFormatter:NSDateFormatter的初始化本身就是重量级操作,且会触发 ICU locale 的初始化。- ICU 内部的全局锁:这是系统库的已知问题,在 iOS 启动早期阶段锁竞争尤为严重,因为各种系统框架也在并发初始化。
建议修复方案
- 缓存
NSDateFormatter(最直接有效):将NSDateFormatter创建为静态变量或单例属性,避免每次调用startUUID都重新创建:- (NSString *)startUUID { static NSDateFormatter *formatter = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ formatter = [NSDateFormatter new]; formatter.dateFormat = @"yyMMddHHmmssSSS"; formatter.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]; }); NSString *time = [formatter stringFromDate:[NSDate date]]; NSString *startUuid = [NSString stringWithFormat:@"%@2000000%@", time, [self getRandomNumber]]; return startUuid; }
- 延迟执行:将
startUUID和上报逻辑延迟到首帧渲染之后,脱离 watchdog 监控的关键时间窗口。- 替代方案:如果仅需要生成唯一标识,可以完全避免
NSDateFormatter,改用时间戳数字拼接: 这完全绕开了 ICU/NSDateFormatter,不存在锁竞争问题。- (NSString *)startUUID { NSTimeInterval now = [[NSDate date] timeIntervalSince1970]; long long ms = (long long)(now * 1000); NSString *startUuid = [NSString stringWithFormat:@"%lld2000000%@", ms, [self getRandomNumber]]; return startUuid; }推荐方案 3,因为它从根本上消除了 ICU 锁竞争的风险,同时性能开销最小。
AI时代,整体APP行业的性能将得到大幅提升
优秀大模型解决Bug的能力很强,且修复速度和质量都发生了颠覆性的变化,意味着如果有规划+APP开发人员有心,大部分问题都能解决;AI编程工具下,目前AI写复杂功能可能还有点问题,但如果是指定AI去写一个特定功能bug率可能会比资深工程师还要好;借助AI来深入了解底层知识也很方便,对于提升工程师认知也有帮助,进一步提升了性能。
AI时代,Bug的解决方式会发生变化
现在的热修复功能集成到APP后,往往需要编写修复后的脚本语言文件,下发到APP,APP动态运行时交换方法实现解决。
AI时代的方式可能是:
-》Crash发生后,自动分析原因,出解决方案,发出通知;
-》人工收到通知后,选择一个方案;
-》自动生成对应的脚本文件,自动下发到对应的APP版本。
-》APP再次打开时,Bug已经自动修复。
iOS三方库精读 · 第 8 期
RxSwift 是 ReactiveX 的 Swift 实现,将异步操作和事件统一为可观察序列(Observable),通过操作符进行声明式组合变换,极大简化复杂异步逻辑。
| 属性 | 值 |
|---|---|
| GitHub Stars | 24.5k+ |
| 最新版本 | 6.7.0 |
| License | MIT |
| 支持平台 | iOS 9+ / macOS 10.10+ / watchOS / tvOS |
在没有 RxSwift 之前,处理异步事件流往往面临:
| 场景 | 原生方式 | RxSwift 方式 |
|---|---|---|
| 网络请求 | URLSession + closure 嵌套 | Observable + flatMap 链式 |
| 输入框实时搜索 | addTarget + Timer 去抖 | rx.text + debounce |
| 表单验证 | 多个 KVO / Delegate | combineLatest 一行搞定 |
| Timer 管理 | Timer.scheduledTimer + invalidate | Observable.interval + disposed(by:) |
| 通知监听 | NotificationCenter + removeObserver | NotificationCenter.rx.notification |
基础层 概念解释、环境配置、基础用法
Swift Package Manager(推荐)
// Package.swift
dependencies: [
.package(url: "https://github.com/ReactiveX/RxSwift.git", from: "6.7.0")
]
CocoaPods
pod 'RxSwift', '~> 6.7'
pod 'RxCocoa', '~> 6.7' # UIKit 扩展
pod 'RxRelay', '~> 6.7' # Relay 组件
import RxSwift
let disposeBag = DisposeBag()
// 1. 创建 Observable
let observable = Observable<String>.create { observer in
observer.onNext("Hello")
observer.onNext("RxSwift")
observer.onCompleted()
return Disposables.create()
}
// 2. 订阅
observable
.subscribe(
onNext: { print("收到: \($0)") },
onCompleted: { print("完成") }
)
// 3. 管理 disposal
.disposed(by: disposeBag)
// 输出:
// 收到: Hello
// 收到: RxSwift
// 完成
进阶层 最佳实践、性能优化、线程安全
// PublishSubject: 只收到订阅后的事件
let publish = PublishSubject<String>()
publish.onNext("A") // 不会收到
publish.subscribe { print($0) } // 订阅
publish.onNext("B") // ✅ 收到
// BehaviorSubject: 保留最新一个值,新订阅者立即收到
let behavior = BehaviorSubject(value: "初始值")
behavior.onNext("新值")
behavior.subscribe { print($0) } // ✅ 立即收到 "新值"
// BehaviorRelay: UI 安全,永不触发 error/completed
let relay = BehaviorRelay(value: "初始值")
relay.accept("更新值") // 用 accept 替代 onNext
relay.asObservable().subscribe { print($0) }
// 过滤
Observable.of(1, 2, 3, 4, 5)
.filter { $0 % 2 == 0 } // [2, 4]
// 变换
Observable.of(1, 2, 3)
.map { $0 * 2 } // [2, 4, 6]
// 去重
Observable.of("a", "b", "a", "c")
.distinctUntilChanged() // ["a", "b", "a", "c"] - 相邻去重
// 扁平化(网络请求链式)
searchText
.flatMapLatest { query in
return networkAPI.search(query) // 自动取消上一个请求
}
// 组合
Observable.combineLatest(
usernameValid,
passwordValid
) { $0 && $1 } // 表单验证
Observable<String>.create { observer in
// 后台执行耗时任务
Thread.sleep(forTimeInterval: 1)
observer.onNext("计算结果")
observer.onCompleted()
return Disposables.create()
}
.subscribe(on: ConcurrentDispatchQueueScheduler(qos: .background)) // 订阅线程
.observe(on: MainScheduler.instance) // 观察线程
.subscribe(onNext: { value in
// UI 更新在主线程
label.text = value
})
.disposed(by: disposeBag)
深入层 源码解析、设计思想、扩展定制
ObservableType (协议)
↓
Observable (class)
↓ 创建
AnyObserver (观察者抽象)
↓ 订阅
Disposable (取消订阅)
↓ 持有
DisposeBag (资源回收容器)
// Observable.subscribe 核心流程
func subscribe(_ observer: Observer) -> Disposable {
// 1. 包装 observer
let disposable = Disposables.create()
// 2. 调用核心生产逻辑
let sink = AnonymousSink(observer: observer, dispose: disposable)
// 3. 返回 disposable 用于取消
return disposable
}
// dispose 时移除订阅
final class DisposeBag {
var disposables: [Disposable] = []
deinit {
disposables.forEach { $0.dispose() }
}
}
import UIKit
import RxSwift
import RxCocoa
final class LoginViewController: UIViewController {
private let disposeBag = DisposeBag()
// UI
private let emailField = UITextField()
private let passwordField = UITextField()
private let loginButton = UIButton(type: .system)
private let tableView = UITableView()
// 数据源
private let suggestions = BehaviorRelay<[String]>(value: [])
override func viewDidLoad() {
super.viewDidLoad()
setupBindings()
}
private func setupBindings() {
// 1. 实时搜索(防抖 500ms + 去重)
emailField.rx.text.orEmpty
.debounce(.milliseconds(500), scheduler: MainScheduler.instance)
.distinctUntilChanged()
.filter { !$0.isEmpty }
.flatMapLatest { [weak self] query -> Observable<[String]> in
// 模拟搜索建议 API
return self?.searchSuggestions(query) ?? .just([])
}
.bind(to: suggestions)
.disposed(by: disposeBag)
// 2. 表单验证(多输入组合)
let emailValid = emailField.rx.text.orEmpty
.map { $0.contains("@") && $0.contains(".") }
let passwordValid = passwordField.rx.text.orEmpty
.map { $0.count >= 6 }
Observable.combineLatest(emailValid, passwordValid) { $0 && $1 }
.bind(to: loginButton.rx.isEnabled)
.disposed(by: disposeBag)
// 3. TableView 数据绑定
suggestions
.bind(to: tableView.rx.items(cellIdentifier: "Cell")) { _, text, cell in
cell.textLabel?.text = text
}
.disposed(by: disposeBag)
// 4. 点击事件
loginButton.rx.tap
.withLatestFrom(Observable.combineLatest(
emailField.rx.text.orEmpty,
passwordField.rx.text.orEmpty
))
.subscribe(onNext: { [weak self] email, password in
self?.performLogin(email: email, password: password)
})
.disposed(by: disposeBag)
}
private func searchSuggestions(_ query: String) -> Observable<[String]> {
// 模拟 API 请求
return Observable.just(["\(query)@gmail.com", "\(query)@icloud.com"])
.delay(.milliseconds(300), scheduler: MainScheduler.instance)
}
private func performLogin(email: String, password: String) {
print("登录: \(email), 密码: \(password)")
}
}
进阶层 值得借鉴的用法
// 每个操作符返回新的 Observable,支持无限链式
observable
.map { transform($0) } // 返回 Map<Source>
.filter { predicate($0) } // 返回 Filter<Map<Source>>
.subscribe { ... } // 返回 Disposable
// 当 self.deallocated 时自动取消网络请求
networkRequest()
.takeUntil(self.rx.deallocated)
.subscribe(onNext: { ... })
深入层 设计思想解析
// Observable 是 Producer,产生事件
class Producer<Element>: Observable<Element> {
func run(_ observer: Observer, cancel: Cancel) -> Sink {
// 子类实现具体生产逻辑
}
}
// Sink 是 Consumer,消费事件并管理生命周期
class Sink<Observer: ObserverType>: Disposable {
let observer: Observer
var disposed = false
func dispose() {
disposed = true
}
}
以 map 为例:
final class MapSink<Source, Result>: Sink<Result>, ObserverType {
typealias Element = Source
private let transform: (Source) -> Result
func on(_ event: Event<Source>) {
switch event {
case .next(let element):
let result = transform(element) // 变换
forwardOn(.next(result)) // 传递给下游
case .error(let error):
forwardOn(.error(error))
case .completed:
forwardOn(.completed)
}
}
}
问题:在 ViewController 中订阅 Observable,页面释放后订阅仍在执行。
原因:未将 Disposable 加入 DisposeBag,或 DisposeBag 生命周期与 VC 不一致。
解决:
// ❌ 错误:未持有 Disposable
observable.subscribe { ... }
// ✅ 正确:加入 DisposeBag
private let disposeBag = DisposeBag()
observable.subscribe { ... }
.disposed(by: disposeBag)
问题:后台网络请求回调中更新 UI 导致崩溃。
原因:默认情况下 Observable 继承订阅者的线程上下文。
解决:
// ❌ 错误:可能在后台线程
networkRequest()
.subscribe(onNext: { label.text = $0 })
// ✅ 正确:显式切换到主线程
networkRequest()
.observe(on: MainScheduler.instance)
.subscribe(onNext: { label.text = $0 })
问题:快速输入搜索关键词,收到旧的请求结果。
原因:flatMap 会保留所有内部 Observable,flatMapLatest 会自动取消上一个。
解决:
// ❌ 错误:旧请求可能覆盖新结果
searchText.flatMap { searchAPI($0) }
// ✅ 正确:自动取消上一个请求
searchText.flatMapLatest { searchAPI($0) }
问题:PublishSubject 发送 .completed 后,后续订阅收不到事件。
原因:Subject 一旦 terminated,状态不可逆转。
解决:
// 方案 1:使用 Relay(不发送 completed)
let relay = PublishRelay<String>()
// 方案 2:重新创建 Subject
func resetSubject() {
subject = PublishSubject<String>()
}
问题:多个订阅者导致网络请求被执行多次。
原因:默认每个订阅者独立触发 Observable 执行。
解决:
// ❌ 错误:每个订阅触发一次请求
let request = api.fetchData()
request.subscribe { ... } // 请求 1
request.subscribe { ... } // 请求 2
// ✅ 正确:共享执行结果
let request = api.fetchData()
.share(replay: 1) // 缓存最近 1 个结果
request.subscribe { ... } // 请求 1
request.subscribe { ... } // 复用结果
问题:使用 withUnretained(self) 仍出现循环引用。
原因:闭包内额外强引用了 self。
解决:
// ❌ 错误:闭包内强引用
observable
.withUnretained(self)
.subscribe { self, value in
self.items.append(value) // 强引用
}
// ✅ 正确:使用 weak 或确保无循环
observable
.subscribe(onNext: { [weak self] value in
self?.items.append(value)
})
| 维度 | RxSwift | Combine |
|---|---|---|
| 开发商 | 社区 (ReactiveX) | Apple 官方 |
| 最低版本 | iOS 9+ | iOS 13+ |
| 操作符数量 | 100+ 极其丰富 | ~50 够用但较少 |
| UI 绑定 | RxCocoa 内建 | 需自行封装或用 SwiftUI |
| 调试支持 | RxSwift.Resources / debug() | print() / handleEvents() |
| 学习曲线 | 较陡(概念多) | 中等 |
| 包体积 | ~2MB | 系统内建,0 额外 |
| SwiftUI 集成 | 需桥接 | 原生支持 |
| 维护状态 | 活跃 | Apple 官方维护 |
选 RxSwift:
withLatestFrom 等 Combine 缺失的操作符选 Combine:
对于已有 RxSwift 项目:
RxCombine 库实现互相转换尝试用 RxSwift 实现一个「搜索建议」功能:输入框输入时防抖 300ms,发起网络请求获取建议列表,展示在 UITableView 中,并在评论区贴出你的关键代码片段。
如果让你从零实现 RxSwift 的 debounce 操作符,你会如何设计?需要考虑哪些边界情况?
你在使用 RxSwift 时踩过哪些坑?欢迎评论区分享,优质回答会收录进下一期《踩坑记录》。
📅 本系列每周五晚更新 · 已学习:[✓ Alamofire] [✓ Kingfisher] [✓ Lottie] [✓ MarkdownUI] [✓ SDWebImage] [✓ SnapKit] [✓ ListDiff] [→ RxSwift] [○ Charts]
做uni-app开发的同学,有没有遇到过这种崩溃场景:页面用了scroll-view做滚动容器,点击Tab切换锚点后,整个页面突然不能滑动了,刷新也没用,只有重新进入页面才能恢复?
我最近就踩了这个坑,花了大半天排查,最后发现居然只要一行CSS就能解决,今天把整个排查过程和原理分享出来,帮大家避坑,尤其是做iOS端开发的同学,建议直接收藏备用!
先给大家还原下我遇到的场景,方便大家对号入座:
页面结构:用 scroll-view 包裹整个页面内容,内部分3个模块(基本信息、买车意向、卖车意向),顶部有Tab切换,点击Tab通过 scroll-into-view 实现锚点定位。
问题现象:进入页面后,点击「卖车意向」Tab,锚点直接定位到模块最底部;此时尝试上下滑动页面,发现整个页面完全卡死,不能向上滑,只能向下滑(甚至向下滑也不流畅),刷新页面也无法恢复。
环境:iOS端(真机+模拟器+Safari浏览器都复现),Android端正常,小程序端正常。
一开始我以为是锚点定位逻辑写错了,反复检查scroll-into-view、锚点高度计算,改了半天还是卡死,直到加上一行CSS,瞬间复活!
排查过程中,我走了3个弯路,大家可以跳过这些无效操作,直接看解决方案:
一开始觉得是锚点高度获取有误——页面有「展示完整信息」的折叠/展开功能,初始化时获取的锚点高度是折叠状态的,展开后高度变化,导致定位偏移,进而触发滚动异常。
于是封装了锚点高度重新计算方法,在折叠/展开、Tab切换后重新查询DOM高度,虽然解决了锚点定位到底部的问题,但滚动卡死依然存在。
接着检查scroll-view的滚动监听方法(scrollChange),发现里面有复杂的高度判断逻辑,比如用anchor2TopCopy动态计算偏移量,以为是判断条件出错导致滚动锁死。
简化了滚动监听逻辑,改成简单的三段式判断(根据滚动距离切换Tab),锚点定位更精准了,但滚动卡死问题还是没解决。
检查scroll-view的样式,确认已经设置了scroll-y="true"、flex:1、height:100%,没有多余的overflow样式冲突,排除了样式配置问题。
因为只有iOS端有问题,Android端正常,猜测是iOS原生特性和uni-app scroll-view的冲突。想起iOS有个「橡皮筋回弹」效果(overscroll),当scroll-view滚动到边界时,继续拉拽会出现空白回弹,会不会是这个回弹导致滚动状态错乱?
抱着试试看的心态,加了一行禁止回弹的CSS,没想到——滚动瞬间恢复正常,卡死问题彻底解决!
就是这行CSS,直接复制到你的页面样式中,iOS端滚动卡死问题瞬间解决:
::v-deep .uni-scroll-view,
::v-deep .uni-scroll-view-content {
/* 禁止iOS橡皮筋回弹,解决scroll-view滚动卡死 */
overscroll-behavior: none;
}
::v-deep 必须加:因为uni-app的scroll-view是组件封装的,需要穿透样式到子组件。
适配范围:同时作用于.uni-scroll-view和.uni-scroll-view-content,确保所有滚动容器都禁止回弹。
不影响其他功能:这行代码只禁止“边界回弹”,不影响正常滚动、锚点定位,Android端不受影响(overscroll-behavior在Android上兼容性有限,不会生效,也不需要生效)。
如果你的页面也有折叠/展开模块,建议搭配锚点高度重新计算方法,实现“锚点精准+滚动流畅”:
// 重新计算所有锚点高度(折叠/展开后调用)
updateAnchorTop() {
const query = uni.createSelectorQuery().in(this);
query
.select('#anchor1') // 基本信息锚点
.select('#anchor2') // 买车意向锚点
.select('#anchor3') // 卖车意向锚点
.boundingClientRect((res) => {
if (res[0]) this.anchor1Top = res[0].top;
if (res[1]) this.anchor2Top = res[1].top;
if (res[2]) this.anchor3Top = res[2].top;
})
.exec();
},
// 折叠/展开按钮点击事件
arrowClick() {
this.arrow = !this.arrow;
// 等待DOM渲染完成后重新计算锚点高度
this.$nextTick(() => {
this.updateAnchorTop();
});
}
核心原因:iOS的橡皮筋回弹(overscroll)与uni-app的scroll-view锚点定位冲突,导致滚动状态锁死。
当点击Tab触发scroll-into-view锚点定位时,若定位到模块底部,会触发iOS的“越界回弹”(overscroll)。
uni-app的scroll-view组件底层对滚动状态的处理,与iOS原生回弹机制不兼容,回弹后会导致scroll-view的滚动事件被阻塞,出现“卡死”。
overscroll-behavior: none 的作用就是禁止元素的越界回弹行为,从根源上避免了冲突,滚动状态自然恢复正常。
补充:这不是你的代码写错了,而是uni-app在iOS端的一个经典兼容性bug,很多开发者都遇到过,一行CSS就能规避。
如果加上这行CSS后,滚动还是有问题,大概率是以下2个原因,对应解决即可:
确保scroll-view的父容器有明确高度,scroll-view本身设置:
scroll-view {
flex: 1;
height: 100%;
overflow-y: auto; /* 兜底,避免滚动异常 */
}
在Tab点击事件中,等待DOM渲染完成后再赋值锚点,避免异步高度问题:
tabClick(e) {
this.indexNum = e;
this.$nextTick(() => {
this.anchor = e === 0 ? 'anchor1' : e === 1 ? 'anchor2' : 'anchor3';
// 定位后兜底校准高度
setTimeout(() => this.updateAnchorTop(), 100);
});
}
如果你在uni-app开发中,遇到iOS端scroll-view滚动卡死、不能滑动的问题,尤其是结合锚点定位、Tab切换时,直接用这行CSS就能解决:
::v-deep .uni-scroll-view,
::v-deep .uni-scroll-view-content {
overscroll-behavior: none;
}
本质是规避iOS原生回弹与uni-app scroll-view的兼容性冲突,属于“一招制敌”的解决方案。
另外,结合锚点高度重新计算方法,能同时解决“锚点定位偏移”和“滚动卡死”两个问题,适配更多复杂页面场景。
希望这篇文章能帮你节省排查时间,避免踩坑~ 如果还有其他uni-app滚动相关的问题,欢迎在评论区交流!
最后,求个点赞收藏,你的支持是我分享的动力 😊
工作中的 iOS 应用内浏览器一直使用 WKWebView 直接实现,但存在架构层面的担忧:
load(_:)、goBack() 等页面操作UIView 在屏幕上显示这两种职责都集中在 WKWebView 这一个类型中。
工作中采用 UI / 表现层 / 业务逻辑 / 基础设施 四层架构,强调各层之间不应直接依赖具体实现。直接使用 WKWebView 会导致 UI 显示和网页操作都通过同一个类型完成,与架构理念不符。
WebKit 在 iOS 26 推出了新的 Swift API WebPage,设计理念与 WKWebView 截然不同:
| 职责 | 承担者 |
|---|---|
| Web 内容状态管理 | WebPage |
| 导航控制 | WebPage |
| JavaScript 执行 | WebPage |
| UI 显示 |
WebView(SwiftUI View) |
WebPage 遵循 @Observable,在 SwiftUI 中可以自然地订阅状态变化:
@State var webPage = WebPage()
var body: some View {
WebView(webPage)
.toolbar {
Button("Back") {
webPage.goBack()
}
.disabled(!webPage.canGoBack)
}
}
这种设计有效解决了之前的架构担忧。
尽管 WebPage 设计理想,但由于以下原因无法直接在生产环境使用:
flowchart LR
subgraph 版本对比
WP[WebPage API<br/>iOS 26+]
APP[应用支持<br/>iOS 18+]
end
WP -->|不兼容| APP
WebPage 需要 iOS 26,而应用目前仍支持 iOS 18,无法直接在生产代码中使用。
WebPage 内部持有 WKWebView,但并未将其作为属性公开,应用无法取出使用。
现有浏览器实现大量依赖 WKNavigationDelegate 和 KVO,从质量保证角度,无法一次性全部替换。需要在继续使用 WKWebView 的同时逐步迁移,这成为直接引入 WebPage 的障碍。
flowchart TB
subgraph 目标架构
UI[UI 层]
P[表现层]
BL[业务逻辑层]
INF[基础设施层]
end
UI --> P --> BL --> INF
style UI fill:#e1f5fe
style INF fill:#f3e5f5
四层架构要求业务逻辑层和表现层不直接依赖 UI 层的具体类型。但如果在业务逻辑层 import WebKit,WKWebView 等 UI 相关类型也会变得可用。虽然 WebPage 本身是抽象 API,但最终依赖 WebKit 模块,无法在类型层面强制分层边界。
大规模应用需要保持 UI 无关逻辑的可测试性。WebPage 并非为替换和模拟而设计,难以融入现有的依赖注入(DI)测试策略。
应用内浏览器有一些特殊需求:
URL 变化检测:
onChange(依赖 UI 渲染周期)或 Observation Framework(依赖事务边界)可能合并短时间内多次变化WebPage 目前没有提供同等钩子window.open 处理:
window.open(本应新开标签页)并在同一页面内打开WebPage 没有提供实现此行为的机制为满足现有应用需求同时实现职责分离,参考 WebKit 官方 WebPage 的设计理念,设计了自定义 API。
classDiagram
class WebPageRepresentable {
<<protocol>>
+url: URL?
+canGoBack: Bool
+estimatedProgress: Double
+load(request: URLRequest)
+reload()
+goBack()
}
class WebPage {
<<class>>
-backingWebView: WKWebView
+url: URL?
+canGoBack: Bool
+estimatedProgress: Double
}
class WebPageNavigationHandling {
<<protocol>>
+handleNavigationCommit()
}
class InAppBrowserNavigationHandler {
-owner: InAppBrowserViewModel?
+handleNavigationCommit()
}
WebPageRepresentable <|.. WebPage
WebPageNavigationHandling <|.. InAppBrowserNavigationHandler
WebPage --> WKNavigationDelegateAdapter
定义最小化的网页操作接口 WebPageRepresentable:
@MainActor
protocol WebPageRepresentable: Observable {
var url: URL? { get }
var canGoBack: Bool { get }
var estimatedProgress: Double { get }
func load(_ request: URLRequest)
func reload()
func goBack()
// ...
}
这种抽象实现了:
在 UI 层定义自定义 WebPage,内部持有 WKWebView:
import WebKit
@Observable
@MainActor
final class WebPage: WebPageRepresentable {
let backingWebView: WKWebView
var url: URL? {
backingWebView.url
}
func load(_ request: URLRequest) {
backingWebView.load(request)
}
// ...
}
关键约束:只有 UI 层 import WebKit,WebKit 类型不会泄露到其他层。
参考 WebKit 官方实现,构建了 KVO 与 Observation 的桥接机制:
sequenceDiagram
participant KVO as WKWebView (KVO)
participant Bridge as Observation Bridge
participant Obs as Observation Registrar
KVO->>Bridge: 属性变化通知
Bridge->>Obs: willSet(keyPath)
Bridge->>KVO: 更新值
Bridge->>Obs: didSet(keyPath)
Obs->>SwiftUI: 触发视图更新
private func createObservation<Value, BackingValue>(
for keyPath: KeyPath<WebPage, Value>,
backedBy backingKeyPath: KeyPath<WKWebView, BackingValue>
) -> NSKeyValueObservation {
return backingWebView.observe(
backingKeyPath,
options: [.prior, .old, .new]
) { [_$observationRegistrar, unowned self] _, change in
if change.isPrior {
_$observationRegistrar.willSet(self, keyPath: keyPath)
} else {
_$observationRegistrar.didSet(self, keyPath: keyPath)
}
}
}
这样 SwiftUI(或使用 Observation 的层)看到的是普通的 Observable 类型,而实际追踪的是 WKWebView 的状态变化。
另外,为 URL 变化添加了专门的通知逻辑,防止历史记录遗漏。
WKFrameInfo 等 WebKit 类型虽然是数据结构却被定义为 class,导致值语义和引用语义模糊。因此重新定义了只包含必要信息的 struct 类型(如 WebPageFrameInfo):
flowchart LR
subgraph 类型语义明确化
WK[WKFrameInfo<br/>class - 语义模糊]
WP[WebPageFrameInfo<br/>struct - 值语义明确]
end
WK -->|重定义| WP
收益:
参考 WebKit 官方实现,在内部持有委托适配器:
业务逻辑层:
@MainActor
protocol WebPageNavigationHandling {
func handleNavigationCommit()
// ...
}
UI 层:
@MainActor
@Observable
final class WebPage: WebPageRepresentable {
private let backingNavigationDelegate: WKNavigationDelegateAdapter
init(navigationHandler: some WebPageNavigationHandling) {
backingNavigationDelegate = WKNavigationDelegateAdapter(navigationHandler)
backingWebView.navigationDelegate = backingNavigationDelegate
}
// ...
}
@MainActor
final class WKNavigationDelegateAdapter: NSObject, WKNavigationDelegate {
private let navigationHandler: any WebPageNavigationHandling
func webView(_ webView: WKWebView, didCommit navigation: WKNavigation) {
navigationHandler.handleNavigationCommit()
}
// ...
}
flowchart TB
subgraph 委托隐藏
UI[UI 层<br/>WebPage]
Adapter[WKNavigationDelegateAdapter<br/>内部类]
Handler[WebPageNavigationHandling<br/>协议]
VM[业务逻辑层<br/>NavigationHandler]
end
UI --> Adapter --> Handler
VM ..> Handler
style Adapter fill:#fff3e0
这样隐藏了 NSObject 等功能过剩的类型和 WebKit 特有类型,只向外部公开必要的职责。
传统做法常扩展 UIViewController 或 UIView 来遵循各种委托,但这容易导致 ViewController 臃肿,导航和安全判断与 UI 紧密耦合。即使改为扩展 ViewModel,也只是 ViewModel 的扩展,职责边界仍然模糊。
因此参考 WebKit 官方实现,创建了专门处理导航相关事件并操作 ViewModel 的类:
@MainActor
final class InAppBrowserNavigationHandler: WebPageNavigationHandling {
weak var owner: InAppBrowserViewModel?
func handleNavigationCommit() {
// 操作 owner
}
}
flowchart LR
subgraph 职责分离
VM[InAppBrowserViewModel]
Handler[InAppBrowserNavigationHandler]
WebP[WebPage]
end
Handler -->|持有弱引用| VM
Handler -->|处理导航事件| WebP
VM -->|使用| WebP
style Handler fill:#e8f5e9
这样将网页相关事件处理从 ViewModel 中分离,明确了各自的职责。
flowchart TB
subgraph 演进路线
A[当前: 功能模块内实现]
B[中期: 独立 Package]
C[长期: SwiftUI 化]
end
A --> B --> C
subgraph Package 结构
P1[UI 模块<br/>public]
P2[逻辑模块<br/>package 访问级别]
end
B --> P1
B --> P2
目前实现封闭在应用内浏览器功能模块内,未来计划:
package 访问修饰符分离 UI 和非 UI(逻辑)的库结构WebKit 作为 Apple 官方开源库,罕见地体现了现代设计理念:
即使由于产品限制无法直接采用最新 API,也可以从中提取设计精髓,根据自研上下文重新构建,为未来的迁移打下基础。
flowchart TB
subgraph 核心收获
A[WebPage 设计理念]
B[职责分离架构]
C[现代 Swift 特性应用]
D[可测试性保障]
end
A --> B --> C --> D
通过这种方式,既能满足当前版本兼容性要求,又能为未来向官方 API 迁移做好准备。
UIKit 支持 @Observable 类型;数据(属性值)变更时,UI 自动更新;提升开发效率,减少手动刷新代码。
UIViewController 和 UIView 新增 updateProperties() 方法;通过修改属性值直接更新 UI
UINavigationItem 和 UIBarButtonItem 新增功能;转场效果 zoom 的触发条件扩展至 UIBarButtonItem
UITabBarController 新增 tabBarMinimizeBehavior 属性(类型:UITabBarController.MinimizeBehavior),用于设置 TabBar 最小化时的行为
UIVisualEffectView 新增 UIGlassEffect 和 UIGlassContainerEffect, 符合 Liquid Glass 风格的视觉效果
新增 Liquid Glass 风格配置方法
新增 cornerConfiguration 属性(类型:UICornerConfiguration),用于设置圆角并支持动画
新增拖拽时的样式设置, 支持在滑轨上添加刻度
Symbol Animations 新增动画效果:drawOn 和 drawOff
UIScene Open File
解决:
进入 Target 的 Build Settings 标签: 选中 Target → Build Settings → 搜索 Other Linker Flags。 手动修改链接参数: 点击 Other Linker Flags,首先移除:
-ld64
-ld_classic
添加:
-Xlinker
-dead_strip
-Xlinker
-allow_dead_duplicates
计算机网络---https(超文本传输安全协议)
HTTPS(HyperText Transfer Protocol Secure,超文本传输安全协议)并非独立于HTTP的“新协议”,而是 HTTP协议与TLS/SSL加密层的结合体——它在HTTP的应用层与TCP传输层(或HTTP/3的QUIC协议层)之间,增加了一套标准化的加密、认证与数据校验机制,核心目标是解决HTTP协议的安全缺陷,保障客户端与服务器之间的通信安全。
从技术本质来看,HTTPS的定位可概括为:
要理解HTTPS的必要性,需先明确HTTP的安全漏洞——这些漏洞在公网(如WiFi、运营商网络)中极易被利用,引发隐私泄露、财产损失等风险。HTTPS通过三大核心能力,针对性解决这些问题:
HTTP的致命缺陷是 明文传输:请求/响应内容(如密码、银行卡号、搜索记录)在网络中以“可读文本”形式传输,任何处于传输链路中的设备(如路由器、黑客的抓包工具)都能直接窃取数据。
例如,用户在HTTP网站输入“用户名:test,密码:123456”,抓包工具可直接捕获如下明文:
POST /login HTTP/1.1
Content-Type: application/x-www-form-urlencoded
username=test&password=123456
HTTPS通过 对称加密算法(如AES-256-GCM)解决此问题:客户端与服务器协商出一个“会话密钥”,所有数据传输前用该密钥加密,传输后用同一密钥解密——即使数据被截获,攻击者因无会话密钥,也无法还原为可读文本。
在开发或测试过程中,可以使用抓包工具如 Sniffmaster 来监控和分析 HTTPS 流量,以验证加密效果和排查问题,支持全平台操作且无需复杂代理设置。
HTTP不校验数据完整性:攻击者可通过“中间人攻击”(MITM)拦截HTTP数据包,修改内容后再转发给目标方,且双方均无法察觉。
典型场景:用户在HTTP电商网站下单,支付金额为100元,攻击者拦截请求后将金额改为10000元,服务器接收并处理修改后的请求,导致用户多支付。
HTTPS通过 哈希算法+数字签名 保证完整性:
HTTP无身份认证机制:攻击者可伪造服务器(如搭建虚假WiFi热点,伪装成“商场免费WiFi”),诱骗用户访问虚假网站,窃取用户信息(即“钓鱼攻击”)。
例如,用户想访问 http://www.bank.com,但攻击者伪造了一个域名相似的 http://www.bankk.com,页面样式与真实银行完全一致,用户输入账号密码后,数据直接发送给攻击者。
HTTPS通过 CA证书体系 实现身份认证:服务器需向权威的“证书颁发机构(CA)”申请证书,证书中包含服务器的域名、公钥、CA签名等信息。客户端(如浏览器)接收证书后,会通过内置的“根CA证书”验证服务器证书的合法性——只有验证通过,才确认服务器是真实的,而非伪造。
HTTPS的安全能力依赖两大核心组件:TLS/SSL协议(负责加密与握手)和CA证书(负责身份认证),二者协同工作,构成HTTPS的安全基石。
TLS(Transport Layer Security,传输层安全协议)是SSL(Secure Sockets Layer)的升级版,当前主流版本为 TLS 1.2 和 TLS 1.3(SSLv3因安全漏洞已被禁用)。TLS协议栈分为三层,自上而下分别为:
| 协议层 | 核心功能 | 关键技术/字段 |
|---|---|---|
| 警报层(Alert) | 传递TLS会话中的错误信息(如证书过期、加密套件不支持),触发连接关闭或重试 | 警报级别(Warning/Fatal)、错误代码(如42表示证书过期) |
| 握手层(Handshake) | 协商TLS会话参数(加密套件、会话密钥)、交换证书、验证身份 | 客户端Hello、服务器Hello、证书、密钥交换消息 |
| 记录层(Record) | 对应用层数据(HTTP请求/响应)进行分片、压缩、加密、添加认证标签 | 对称加密算法(AES)、哈希算法(SHA-256)、记录长度 |
TLS协议的迭代始终围绕“更安全、更高效”展开,各版本的关键差异如下:
| 版本 | 发布时间 | 核心特点 | 安全性/性能评价 |
|---|---|---|---|
| SSLv3 | 1996年 | 首个广泛应用的版本,但存在POODLE漏洞(可被破解加密) | 已废弃,完全不安全 |
| TLS 1.0 | 1999年 | 修复SSLv3漏洞,引入RSA密钥交换、AES加密 | 安全性不足(如BEAST漏洞),部分浏览器已禁用 |
| TLS 1.1 | 2006年 | 修复BEAST漏洞,改进IV(初始化向量)生成方式 | 安全性一般,逐步被淘汰 |
| TLS 1.2 | 2008年 | 支持SHA-2哈希算法、AEAD加密模式(如AES-GCM),增强完整性与机密性 | 当前主流版本,安全性可靠 |
| TLS 1.3 | 2018年 | 1. 简化握手流程(从4次交互减至3次,支持0-RTT); 2. 移除不安全加密套件; 3. 合并密钥交换与服务器Hello |
性能最优、安全性最高,逐步普及 |
关键优化:TLS 1.3的“0-RTT(Round-Trip Time)”握手可实现“首次重连时无需等待握手完成即可发送数据”,大幅降低延迟——例如,用户第二次访问某网站时,可直接用缓存的会话参数发送请求,无需重新协商密钥。
CA(Certificate Authority,证书颁发机构)是公认的“网络信任第三方”,负责验证服务器身份并颁发证书。CA证书体系采用“层级信任模型”,确保证书的合法性可追溯,核心构成如下:
一份合法的TLS证书包含以下关键信息(可通过浏览器“查看证书”功能查看):
客户端(如浏览器)验证服务器证书时,需通过“信任链”逐层校验,确保证书未被伪造,流程如下:
根据验证严格程度,CA证书分为三类,适用于不同场景:
HTTPS的通信过程分为两大阶段: TLS握手阶段(协商会话参数、交换证书、生成会话密钥)和 加密通信阶段(用会话密钥传输HTTP数据)。以下以主流的 TLS 1.2 和优化后的 TLS 1.3 为例,详细拆解流程。
TLS 1.2握手需客户端与服务器进行4次TCP交互(2个RTT),流程如下:
至此,TLS握手完成,客户端与服务器持有相同的会话密钥,进入加密通信阶段。
TLS 1.3通过合并消息、减少交互,将握手压缩至3次TCP交互(1个RTT),流程如下:
核心优化:TLS 1.3删除了“服务器Hello Done”消息,将密钥交换与服务器响应合并,减少1次交互;同时支持“0-RTT”模式——若客户端缓存了历史会话参数(如会话票证),首次重连时可直接发送加密的HTTP请求,无需等待握手完成,进一步降低延迟。
TLS握手完成后,客户端与服务器开始用“会话密钥”传输HTTP数据,流程如下:
GET /index.html HTTP/1.1);早期HTTPS因TLS握手延迟、加密计算开销,确实比HTTP慢,但随着协议优化(如TLS 1.3)、硬件升级(CPU支持AES指令集)、缓存机制改进,HTTPS的性能已接近HTTP,甚至通过优化可超越HTTP。以下是核心优化方向:
HTTP/3基于QUIC协议(UDP传输),QUIC内置TLS 1.3加密,无需单独的TLS握手——QUIC的“0-RTT”握手可同时完成QUIC连接建立与TLS加密协商,进一步降低延迟;同时QUIC支持“连接迁移”(如手机从WiFi切换到4G,无需重新握手),提升移动场景下的HTTPS体验。
HTTPS并非“绝对安全”,仍存在潜在风险,但需满足特定条件:
应对措施:定期轮换私钥、禁用弱TLS版本和套件、通过安全工具(如Qualys SSL Labs)检测证书配置。此外,工具如 Sniffmaster 提供 HTTPS 抓包功能,支持无需代理的设置,便于开发者进行安全审计和调试。
免费证书(如Let’s Encrypt)与付费证书的核心安全机制完全一致,均符合TLS标准,差异仅在验证严格程度和品牌信任度:
早期HTTPS的加密计算确实会增加服务器CPU负载(约10%-20%),但通过优化可大幅缓解:
当前主流互联网公司(如阿里、腾讯)的高并发业务(秒杀、直播)均使用HTTPS,证明其可支撑高并发场景。
HTTPS不仅是“安全协议”,更是当前Web生态的“基础设施”——它通过TLS加密解决了HTTP的安全漏洞,保障了用户隐私与数据安全;同时,HTTPS也是SEO(搜索引擎优化)、PWA(渐进式Web应用)、WebSocket等技术的前提条件,无HTTPS则无法使用这些功能。
未来,HTTPS的发展方向将聚焦于:
在 iOS 开发中,Runtime(运行时)是 Objective-C(以下简称 OC)语言的灵魂,也是区分 iOS 初级开发者与中高级开发者的核心门槛。它赋予 OC 动态特性,让代码在编译期无法确定的逻辑,能在运行时灵活调整、动态扩展。随着 Swift 生态的完善和 Apple 技术的迭代,Runtime 并未过时,反而在组件化、性能优化、逆向开发等场景中发挥着不可替代的作用。本文将从原理、实战、前沿三个维度,带你全面吃透 iOS Runtime,结合代码示例拆解核心用法,助力你在实际开发中灵活运用这门“黑魔法”。
Runtime 本质上是一套用 C 和汇编语言编写的 API 集合,是 OC 语言与底层系统之间的桥梁,负责将 OC 代码转换为底层可执行的机器指令,实现动态类型、动态绑定、动态加载等核心特性。简单来说,OC 是“动态语言”,核心就在于 Runtime——编译期我们写的 OC 方法调用、属性访问,最终都会被转换为 Runtime 的 C 函数调用,直到运行时才真正确定具体执行逻辑。
举个直观的例子:我们调用 [object method] 时,编译器并不会直接确定 method 方法的具体实现,而是在运行时通过 Runtime 查找该方法的实现并执行,这也是 Runtime 与静态语言(如 C++)的核心区别。
Runtime 的所有功能,都围绕以下几个核心结构体展开,理解它们是掌握 Runtime 的基础:
OC 中所有对象的底层都是 objc_object 结构体,核心字段是 isa 指针,用于指向对象所属的类。
// objc 对象的底层结构体
struct objc_object {
Class isa; // 指向类对象的指针,核心字段
};
// OC 对象的本质就是 objc_object 的指针
typedef struct objc_object *id;
类对象(Class)的底层是 objc_class 结构体,存储着类的元信息(方法列表、属性列表、协议列表等)。
struct objc_class {
Class isa; // 指向元类(Meta Class),用于存储类方法
Class super_class; // 指向父类
const char *name; // 类名
long instance_size; // 实例对象的内存大小
struct objc_ivar_list *ivars; // 实例变量列表
struct objc_method_list **methodLists; // 方法列表(可动态修改)
struct objc_cache *cache; // 方法缓存(提升查找效率)
struct objc_protocol_list *protocols; // 协议列表
};
SEL:方法选择器,本质是字符串,用于唯一标识一个方法(如 @selector(method:));IMP:函数指针,指向方法的具体实现,是方法执行的核心;Method:方法结构体,封装了 SEL 和 IMP 的对应关系。// 方法结构体
struct objc_method {
SEL method_name; // 方法选择器
char *method_types; // 方法类型编码(返回值、参数类型)
IMP method_imp; // 方法实现的函数指针
};
Runtime 的核心机制包括消息传递、方法缓存、动态解析、消息转发、方法交换等,其中消息传递是基础,其他机制都是基于消息传递的扩展。以下结合实战代码,拆解每个机制的原理与用法。
OC 中所有方法调用,本质上都是 Runtime 的 objc_msgSend 函数调用。当我们写下 [object method:arg] 时,编译器会自动转换为:
objc_msgSend(object, @selector(method:), arg);
isa 指针,找到对象所属的类;cache(方法缓存)中查找对应 SEL 的 IMP;methodLists 查找方法;super_class 父类链向上查找,直到找到 NSObject;IMP 并将方法加入缓存(提升下次查找效率);需导入 Runtime 头文件 #import <objc/runtime.h>,手动调用消息传递函数:
#import <objc/runtime.h>
@interface Person : NSObject
- (void)sayHello:(NSString *)name;
@end
@implementation Person
- (void)sayHello:(NSString *)name {
NSLog(@"Hello, %@", name);
}
@end
// 调用方式
Person *person = [[Person alloc] init];
// 1. 常规调用
[person sayHello:@"Runtime"];
// 2. 手动调用 objc_msgSend
SEL sel = @selector(sayHello:);
objc_msgSend(person, sel, @"Runtime"); // 输出:Hello, Runtime
Runtime 为每个类维护了一个 objc_cache(方法缓存),用于存储最近调用过的方法(SEL + IMP)。缓存采用哈希表实现,查找速度远快于遍历方法列表,这是 Runtime 优化性能的核心手段之一。
核心特点:
当消息传递流程中未找到方法时,Runtime 不会直接崩溃,而是提供了三层“补救机制”,让我们有机会动态补充方法实现,避免 App 闪退。
通过重写 +resolveInstanceMethod:(实例方法)或 +resolveClassMethod:(类方法),动态为未实现的方法添加实现。
@implementation Person
// 动态解析实例方法
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(sayHello:)) {
// 为 sel 动态添加实现:参数1=类,参数2=SEL,参数3=IMP,参数4=方法类型编码
class_addMethod(self, sel, (IMP)dynamicSayHello, "v@:@");
return YES;
}
return [super resolveInstanceMethod:sel];
}
// 动态添加的方法实现(C语言函数)
void dynamicSayHello(id self, SEL _cmd, NSString *name) {
NSLog(@"动态解析:Hello, %@", name);
}
@end
// 调用未声明的方法(不会崩溃)
Person *person = [[Person alloc] init];
[person sayHello:@"Dynamic Resolve"]; // 输出:动态解析:Hello, Dynamic Resolve
若动态解析未处理(返回 NO),则进入消息转发流程,分为两步:
-forwardingTargetForSelector:,将消息转发给另一个对象处理;-methodSignatureForSelector: 获取方法签名,再通过 -forwardInvocation: 手动处理消息。@interface Student : NSObject
- (void)sayHello:(NSString *)name;
@end
@implementation Student
- (void)sayHello:(NSString *)name {
NSLog(@"Student 打招呼:Hello, %@", name);
}
@end
@implementation Person
// 快速转发:将消息转发给 Student 对象
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(sayHello:)) {
return [[Student alloc] init];
}
return [super forwardingTargetForSelector:aSelector];
}
@end
// 调用方法,消息会转发给 Student
Person *person = [[Person alloc] init];
[person sayHello:@"Forward"]; // 输出:Student 打招呼:Hello, Forward
@implementation Person
// 1. 获取方法签名(必须实现,否则崩溃)
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if (aSelector == @selector(sayHello:)) {
// 方法签名:返回值void(v),参数id(@)、SEL(:)、NSString(@)
return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
}
return [super methodSignatureForSelector:aSelector];
}
// 2. 手动处理消息
- (void)forwardInvocation:(NSInvocation *)anInvocation {
SEL sel = anInvocation.selector;
Student *student = [[Student alloc] init];
if ([student respondsToSelector:sel]) {
[anInvocation invokeWithTarget:student]; // 转发给 Student
} else {
[super forwardInvocation:anInvocation];
}
}
@end
Method Swizzling(方法交换)是 Runtime 最常用的实战技巧,通过交换两个方法的 IMP,实现“hook”效果,无需修改原方法源码,即可拦截、扩展原方法的功能(如埋点、日志、性能监控)。
核心原理:交换两个 Method 结构体中的 IMP 指针,让原 SEL 指向新的实现,新 SEL 指向原实现。
#import <objc/runtime.h>
#import <UIKit/UIKit.h>
@implementation UIViewController (Swizzling)
// 在 +load 方法中执行方法交换(+load 方法会在类加载时自动调用,且只调用一次)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 1. 获取两个方法
Class cls = [self class];
SEL originalSel = @selector(viewDidLoad);
SEL swizzledSel = @selector(swizzled_viewDidLoad);
Method originalMethod = class_getInstanceMethod(cls, originalSel);
Method swizzledMethod = class_getInstanceMethod(cls, swizzledSel);
// 2. 交换方法实现
method_exchangeImplementations(originalMethod, swizzledMethod);
});
}
// 新的方法实现(拦截 viewDidLoad)
- (void)swizzled_viewDidLoad {
// 1. 执行原 viewDidLoad 方法(此时 swizzled_viewDidLoad 指向原实现)
[self swizzled_viewDidLoad];
// 2. 扩展功能(如埋点、日志)
NSLog(@"拦截到 %@ 的 viewDidLoad", self.class);
}
@end
dispatch_once_t 保证方法交换只执行一次,避免多次交换导致逻辑错乱;+load 方法中执行交换(类加载时执行,时机最早),避免在 +initialize 中执行(可能被多次调用);class_getClassMethod 获取方法,而非 class_getInstanceMethod;OC 中,分类(Category)默认不能添加实例变量(ivar),但通过 Runtime 的关联对象(Associated Object),可以间接为分类添加“属性”,本质是将属性值存储在外部哈希表中,与对象关联起来。
#import <objc/runtime.h>
#import <UIKit/UIKit.h>
@interface UIButton (Extension)
// 声明属性
@property (nonatomic, copy) NSString *customName;
@end
@implementation UIButton (Extension)
// 定义关联对象的 key(唯一标识)
static const void *CustomNameKey = &CustomNameKey;
// 重写 setter 方法
- (void)setCustomName:(NSString *)customName {
// 关联对象:参数1=对象,参数2=key,参数3=值,参数4=内存管理策略
objc_setAssociatedObject(self, CustomNameKey, customName, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
// 重写 getter 方法
- (NSString *)customName {
// 获取关联对象
return objc_getAssociatedObject(self, CustomNameKey);
}
@end
// 使用
UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
button.customName = @"我的按钮";
NSLog(@"按钮名称:%@", button.customName); // 输出:按钮名称:我的按钮
// 对应 OC 属性的内存修饰符
OBJC_ASSOCIATION_ASSIGN; // assign
OBJC_ASSOCIATION_RETAIN_NONATOMIC; // strong, nonatomic
OBJC_ASSOCIATION_COPY_NONATOMIC; // copy, nonatomic
OBJC_ASSOCIATION_RETAIN; // strong, atomic
OBJC_ASSOCIATION_COPY; // copy, atomic
随着 Swift 成为 iOS 开发的主流语言,以及 Apple 推出的新工具、新框架(如 Xcode 26、基础模型框架),Runtime 的应用场景也在不断扩展,不再局限于 OC 开发,而是与 Swift 生态深度融合,呈现出全新的发展趋势。
Swift 是静态语言,编译期会进行类型检查,但其底层仍然依赖 Runtime(尤其是与 OC 交互时),同时 Swift 也提供了自己的动态特性(如 @dynamicMemberLookup、@objc 关键字),与 OC Runtime 形成互补。
@objc 修饰的方法、属性,会被暴露给 Runtime,可通过 OC Runtime API 调用;@dynamicMemberLookup,允许动态访问属性,本质是 Runtime 动态特性的 Swift 封装;import ObjectiveC
class Person: NSObject {
@objc func sayHello(_ name: String) {
print("Hello, (name)")
}
}
// 1. 动态创建对象
let className = "RuntimeDemo.Person"
guard let cls = NSClassFromString(className) as? Person.Type else { return }
let person = cls.init()
// 2. 动态调用方法
let sel = NSSelectorFromString("sayHello:")
person.perform(sel, with: "Swift Runtime") // 输出:Hello, Swift Runtime
// 3. 动态添加关联对象
extension UIButton {
private static let customKey = UnsafeRawPointer(bitPattern: 0x123456)!
var customName: String? {
get {
objc_getAssociatedObject(self, UIButton.customKey) as? String
}
set {
objc_setAssociatedObject(self, UIButton.customKey, newValue, .OBJC_ASSOCIATION_COPY_NONATOMIC)
}
}
}
随着 Apple 发布 Xcode 26、基础模型框架等新工具,Runtime 的应用场景进一步扩展,尤其在智能开发、性能优化、跨平台适配等方面发挥着重要作用:
尽管 Swift 生态日益完善,但 Runtime 作为 iOS 底层核心技术,短期内不会被替代,反而会随着 Apple 技术的迭代不断升级:
objc_cache 的哈希算法,提升消息传递效率;iOS Runtime 是 OC 语言的灵魂,也是 iOS 开发的“内功”。它不仅能帮助我们理解 iOS 底层原理,更能在实际开发中解决很多常规语法无法解决的问题——从组件化解耦、性能优化,到逆向开发、安全防护,Runtime 都发挥着不可替代的作用。
随着 Swift 与 Apple 新生态的发展,Runtime 的应用场景不断扩展,它不再是“小众黑魔法”,而是中高级 iOS 开发者必须掌握的核心技能。学习 Runtime,不仅是学习一套 API,更是培养一种“底层思维”——跳出上层语法的限制,从底层理解代码的执行逻辑,才能写出更高效、更健壮、更具扩展性的 iOS 应用。
最后,希望本文能帮助你快速吃透 Runtime 的核心原理与实战用法,在实际开发中灵活运用这门技术,突破自身开发瓶颈,成为更优秀的 iOS 开发者。未来,Runtime 还会不断进化,期待我们一起探索它的更多可能性。