卡顿-原理

理解卡顿的原理是优化的基础。本文从屏幕显示的底层机制出发,沿着"VSync信号 -> 渲染管线 -> RunLoop协作 -> 掉帧产生"的逻辑链条,逐步揭示卡顿产生的根本原因。 屏幕显示的基础:VSync与缓冲机制 要理解卡顿,首先要理解屏幕是如何显示画面的。 VSync信号 VSync(Vertical Synchronization,垂直同步)是显示器发出的信号,用于协调GPU渲染和屏幕显示的时机。在60Hz的屏幕上,每16.67ms发出一次VSync信号。当信号到来时,屏幕从帧缓冲区读取数据进行显示。 时间轴: │←── 16.67ms ──→│←── 16.67ms ──→│←── 16.67ms ──→│ ↑ ↑ ↑ VSync 1 VSync 2 VSync 3 │ │ │ 显示帧1 显示帧2 显示帧3 双缓冲与三缓冲 为了避免画面撕裂(GPU写入和屏幕读取同一块内存导致),iOS使用多缓冲机制: flowchart LR subgraph 双缓冲机制 direction TB GPU["GPU"] -->|渲染到| BB["后缓冲区(Back Buffer)"] BB <-->|VSync时交换| FB["前缓冲区(Frame Buffer)"] FB -->|读取显示| Display["显示器"] end 双缓冲(默认):GPU渲染到后缓冲区,VSync到来时交换前后缓冲区,显示器从前缓冲区读取。 三缓冲(高负载时自动启用):增加第三个缓冲区,当渲染无法在一个VSync周期内完成时,GPU可以在额外缓冲区继续渲染,不必等待交换完成。代价是增加一帧延迟和内存占用。 系统在两者之间动态切换,开发者无需手动干预。 ProMotion自适应刷新率 ProMotion设备(iPhone 13 Pro及以上)支持10Hz-120Hz的自适应刷新率,这意味着帧时间预算不再固定: 刷新率 帧时间 卡顿阈值建议 120Hz 8.33ms >16ms视为掉帧 60Hz 16.67ms >33ms视为掉帧 30Hz 33.33ms >66ms视为掉帧 // 检查设备最大刷新率 let maxFrameRate = UIScreen.main.maximumFramesPerSecond // CADisplayLink适配ProMotion if #available(iOS 15.0, *) { displayLink?.preferredFrameRateRange = CAFrameRateRange( minimum: 60, maximum: 120, preferred: 120 ) } iOS渲染架构 了解了屏幕显示机制后,接下来看iOS是如何将UI元素最终变成屏幕上的像素的。 ...

May 2, 2026

iOS响应者链与事件处理机制

当手指触碰iPhone屏幕上的一个按钮时,背后经历了从硬件感知、系统传递、命中测试、事件分发到最终响应的完整链路。本文将系统性地拆解iOS事件处理机制的每个环节。 一、响应者与响应者链 理解事件处理的前提是理解"谁有能力处理事件"。在iOS中,这个问题的答案是 响应者(Responder)。 1.1 UIResponder 所有能够接收并处理事件的对象都继承自 UIResponder: classDiagram NSObject <|-- UIResponder UIResponder <|-- UIView UIResponder <|-- UIViewController UIResponder <|-- UIApplication UIView <|-- UIWindow UIView <|-- UIControl UIView <|-- UIScrollView UIControl <|-- UIButton UIControl <|-- UISlider UIScrollView <|-- UITableView class UIResponder { +nextResponder: UIResponder? +touchesBegan(touches, event) +touchesMoved(touches, event) +touchesEnded(touches, event) +touchesCancelled(touches, event) } UIResponder 定义了处理触摸事件的四个核心方法: - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event; - (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event; - (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event; - (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event; 1.2 nextResponder与响应者链 每个 UIResponder 都有一个 nextResponder 属性,指向"下一个响应者"。所有响应者通过这个属性串联成一条 响应者链(Responder Chain)。 ...

May 2, 2026

卡顿-主线程优化

主线程阻塞是卡顿最常见的原因。本文介绍如何优化主线程的工作,减少阻塞时间。 主线程的职责 主线程(UI线程)负责: mindmap root((主线程职责)) 事件处理 触摸事件 手势识别 UI更新 布局计算 视图绘制 动画执行 Core Animation UIView动画 系统回调 生命周期 AppDelegate 定时器 NSTimer CADisplayLink 通知处理 NotificationCenter KVO 原则:主线程应该只做UI相关的轻量级工作 常见的主线程阻塞场景 1. 耗时计算 // 问题代码:在主线程进行复杂计算 func processData() { let result = heavyComputation(data) // 阻塞主线程 updateUI(with: result) } // 优化后:异步计算 func processDataAsync() { DispatchQueue.global(qos: .userInitiated).async { let result = self.heavyComputation(self.data) DispatchQueue.main.async { self.updateUI(with: result) } } } 2. 文件I/O // 问题代码:主线程读写文件 func loadConfig() { let data = try? Data(contentsOf: configURL) // 阻塞 parseConfig(data) } // 优化后:异步I/O func loadConfigAsync() { DispatchQueue.global(qos: .utility).async { let data = try? Data(contentsOf: self.configURL) DispatchQueue.main.async { self.parseConfig(data) } } } 3. 数据库操作 // 问题代码:主线程数据库查询 func loadUsers() { let users = database.query("SELECT * FROM users") // 阻塞 tableView.reloadData() } // 优化后:异步查询 func loadUsersAsync() { database.queryAsync("SELECT * FROM users") { [weak self] users in self?.users = users DispatchQueue.main.async { self?.tableView.reloadData() } } } 任务异步化 基本原则 flowchart TB subgraph main["主线程任务"] direction LR A1["UI更新视图刷新、动画"] A2["用户交互点击响应、手势处理"] A3["轻量计算简单数据转换"] end subgraph bg["后台线程任务"] direction LR B1["复杂计算数据处理、算法"] B2["I/O操作文件读写、网络请求"] B3["数据库操作查询、写入"] B4["图片处理解码、缩放、滤镜"] end main --> |"轻量、快速"| UI((用户界面)) bg --> |"耗时、阻塞"| main GCD任务调度 class AsyncTaskManager { // 计算密集型任务 static func compute<T>(_ work: @escaping () -> T, completion: @escaping (T) -> Void) { DispatchQueue.global(qos: .userInitiated).async { let result = work() DispatchQueue.main.async { completion(result) } } } // I/O任务 static func io<T>(_ work: @escaping () throws -> T, completion: @escaping (Result<T, Error>) -> Void) { DispatchQueue.global(qos: .utility).async { do { let result = try work() DispatchQueue.main.async { completion(.success(result)) } } catch { DispatchQueue.main.async { completion(.failure(error)) } } } } // 低优先级后台任务 static func background(_ work: @escaping () -> Void) { DispatchQueue.global(qos: .background).async { work() } } } // 使用示例 AsyncTaskManager.compute({ // 复杂计算 return self.processLargeDataSet() }) { result in // 主线程更新UI self.displayResult(result) } Swift Concurrency // 使用async/await class DataProcessor { func processAsync() async throws -> ProcessedData { // 在后台执行 return try await Task.detached(priority: .userInitiated) { return self.heavyProcessing() }.value } @MainActor func loadAndDisplay() async { do { let data = try await processAsync() // 自动在主线程更新UI updateUI(with: data) } catch { showError(error) } } } // 使用TaskGroup并行处理 func processImagesParallel(urls: [URL]) async -> [UIImage] { await withTaskGroup(of: UIImage?.self) { group in for url in urls { group.addTask { await self.loadImage(from: url) } } var images: [UIImage] = [] for await image in group { if let image = image { images.append(image) } } return images } } 任务拆分与调度 大任务拆分 当必须在主线程执行大量工作时,可以拆分成小块: ...

May 2, 2026

卡顿-TableView优化

列表滚动是用户最敏感的交互场景之一。本文介绍UITableView和UICollectionView的性能优化方案。 列表卡顿的常见原因 flowchart TB subgraph frame["每一帧(16.67ms内)需要完成"] A["1. 计算可见Cell"] --> B["2. 复用或创建Cell"] B --> C["3. 配置Cell内容"] C --> C1["设置文本"] C --> C2["加载图片"] C --> C3["计算布局"] C --> C4["渲染视图"] C1 --> D["4. 提交渲染"] C2 --> D C3 --> D C4 --> D end D --> E["⚠️ 任何一步超时都会导致掉帧"] 常见问题 问题 原因 影响 高度计算慢 复杂布局、Auto Layout 滚动卡顿 Cell配置慢 大量视图操作、图片加载 滚动卡顿 复用失效 未正确注册、identifier错误 内存暴涨、卡顿 离屏渲染 圆角、阴影 GPU瓶颈 图片加载 主线程解码 滚动卡顿 Cell复用机制 正确使用复用 class OptimizedTableViewController: UITableViewController { private let cellIdentifier = "OptimizedCell" override func viewDidLoad() { super.viewDidLoad() // 注册Cell类(推荐) tableView.register(OptimizedCell.self, forCellReuseIdentifier: cellIdentifier) // 或注册Nib // tableView.register(UINib(nibName: "OptimizedCell", bundle: nil), forCellReuseIdentifier: cellIdentifier) } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { // 使用dequeueReusableCell(带indexPath的版本会自动创建) let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as! OptimizedCell // 配置Cell let item = items[indexPath.row] cell.configure(with: item) return cell } } Cell的prepareForReuse class OptimizedCell: UITableViewCell { private let titleLabel = UILabel() private let avatarImageView = OptimizedImageView() private var currentTask: URLSessionTask? override func prepareForReuse() { super.prepareForReuse() // 重置状态 titleLabel.text = nil avatarImageView.image = nil // 取消进行中的任务 currentTask?.cancel() currentTask = nil avatarImageView.cancelLoading() } func configure(with item: Item) { titleLabel.text = item.title // 异步加载图片 avatarImageView.setImage(from: item.avatarURL, placeholder: UIImage(named: "placeholder")) } } 高度缓存 问题:每次都计算高度 // 问题代码:每次滚动都重新计算 func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { let item = items[indexPath.row] return calculateHeight(for: item) // 耗时操作 } 解决方案1:预计算并缓存 class HeightCachedTableViewController: UITableViewController { private var items: [Item] = [] private var heightCache: [IndexPath: CGFloat] = [:] func setItems(_ newItems: [Item]) { items = newItems heightCache.removeAll() // 预计算高度(可以在后台线程) precomputeHeights() tableView.reloadData() } private func precomputeHeights() { let width = tableView.bounds.width for (index, item) in items.enumerated() { let indexPath = IndexPath(row: index, section: 0) heightCache[indexPath] = calculateHeight(for: item, width: width) } } private func calculateHeight(for item: Item, width: CGFloat) -> CGFloat { // 计算文本高度 let textHeight = item.content.boundingRect( with: CGSize(width: width - 32, height: .greatestFiniteMagnitude), options: [.usesLineFragmentOrigin, .usesFontLeading], attributes: [.font: UIFont.systemFont(ofSize: 16)], context: nil ).height return ceil(textHeight) + 60 // 加上其他元素的高度 } override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { return heightCache[indexPath] ?? UITableView.automaticDimension } // 数据更新时更新缓存 func updateItem(at indexPath: IndexPath, with item: Item) { items[indexPath.row] = item heightCache[indexPath] = calculateHeight(for: item, width: tableView.bounds.width) tableView.reloadRows(at: [indexPath], with: .automatic) } } 解决方案2:使用estimatedHeight class EstimatedHeightTableViewController: UITableViewController { private var heightCache: [IndexPath: CGFloat] = [:] override func viewDidLoad() { super.viewDidLoad() // 设置估算高度(重要!) tableView.estimatedRowHeight = 100 tableView.rowHeight = UITableView.automaticDimension } override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { // 缓存实际显示的高度 heightCache[indexPath] = cell.bounds.height } override func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { // 优先返回缓存的高度 return heightCache[indexPath] ?? 100 } } 解决方案3:固定高度 // 如果所有Cell高度相同,直接设置固定值 override func viewDidLoad() { super.viewDidLoad() tableView.rowHeight = 80 // 固定高度,性能最好 tableView.estimatedRowHeight = 0 // 关闭估算 } 异步渲染 基本原理 将复杂的渲染工作移到后台线程: ...

May 2, 2026

weak详解

本文将深入探讨iOS中weak引用的实现原理,包括底层数据结构、核心函数实现、生命周期管理,以及weak、unowned、unsafe_unretained三者的对比。 weak的基本概念 weak是一种弱引用修饰符,它不会增加对象的引用计数,也就是说不会持有对象。当对象被释放时,所有指向该对象的weak引用会自动被置为nil,这是weak最核心的特性。 // Objective-C中使用weak @property (nonatomic, weak) id<SomeDelegate> delegate; __weak NSObject *weakObj = strongObj; // Swift中使用weak weak var delegate: SomeDelegate? weak var weakObj = strongObj weak的底层数据结构 要理解weak的实现原理,首先需要了解几个核心数据结构。 SideTable SideTable是Runtime中非常重要的数据结构。关于SideTable在引用计数存储中的作用,请参考iOS中的内存管理-侧表存储。 struct SideTable { os_unfair_lock slock; // 锁,保证线程安全 RefcountMap refcnts; // 引用计数哈希表 weak_table_t weak_table; // 弱引用表 }; 系统维护了一个固定大小的SideTable数组,称为StripedMap。通过对对象地址做哈希和取模来定位对应的SideTable: // 通过对象地址获取对应的SideTable static SideTable& table = SideTables()[obj]; // 内部等效逻辑:index = hash(obj) % StripeCount // StripeCount为StripedMap的大小 由于对象数量远大于SideTable数量,多个对象会被映射到同一个SideTable,这类似于哈希表中的哈希冲突。这种设计的核心目的是分散锁竞争——每个SideTable拥有独立的锁,不同SideTable上的操作可以并行执行,相比单一全局表大幅提升了多线程性能。 weak_table_t weak_table_t是存储弱引用关系的哈希表,采用 开放寻址法(线性探测) 解决哈希冲突: struct weak_table_t { weak_entry_t *weak_entries; // 连续分配的数组,作为开放寻址哈希表的底层存储 size_t num_entries; // 当前已使用的条目数量 uintptr_t mask; // 容量掩码(= 数组容量 - 1),用于 hash & mask 快速取模 uintptr_t max_hash_displacement; // 最大哈希冲突偏移量 }; 虽然 weak_entries 的类型是 weak_entry_t *(即一块连续内存),但元素不是按顺序填入的——插入时通过 hash(referent地址) & mask 计算目标槽位,冲突时向后线性探测。因此它本质上是一个用数组实现的开放寻址哈希表,而非普通的顺序数组。 ...

May 2, 2026

崩溃日志解读

iOS 崩溃日志(.ips / 老版 .crash)是排查线上问题的一手证据。它不仅记录了"谁崩了",更隐藏着异常类型、终止命名空间、寄存器现场、线程堆栈、二进制映射、资源限额等多维度事实。看懂每一个字段,才能把"不能复现"的崩溃拆成"寄存器 x0 是 nil 的后果"这种可复现假设。 本文聚焦系统生成的崩溃日志本身:格式演变、bug_type 全景、Header/Body 逐字段、Exception Type / Termination Namespace 深度解读、寄存器视角、符号化工具链、实战案例库、自动化脚本。OOM 专用的 JetsamEvent 日志见 JetsamEvent 日志解读;崩溃采集与治理方法论见 崩溃-采集、崩溃-治理。 1. .ips / .crash 格式演变 iOS 的崩溃日志格式几经迭代: 时代 文件扩展名 格式 说明 iOS 13 及以前 .crash / .ips 类 plist 纯文本 Key-Value 混排,人眼可读但难解析 iOS 14+ .ips 双段 JSON(Header JSON + Body JSON,以换行分隔) 机器可解析,字段更标准化 Xcode Organizer 导出 .crash 纯文本渲染视图 由 .ips 通过 CrashReporter.framework 渲染而来 解析规则和 JetsamEvent 完全一致——第一行是 Header JSON,剩余部分是 Body JSON,不能整体 parse。iOS 14+ .ips 内容可用 log show --archive 或 Xcode Organizer 转为旧式可读文本,但字段原始数据始终在 JSON 里。 ...

May 2, 2026

Runtime

什么是 Runtime Runtime 是 Objective-C 的运行时系统,是一套底层的 C 语言 API。Objective-C 是一门动态语言,很多操作都是在运行时而非编译时决定的,这一切都依赖于 Runtime。 Runtime 提供的核心能力: 消息发送与转发 方法交换(Method Swizzling) 关联对象(Associated Objects) 动态创建类和对象 动态添加和修改方法、属性 关于对象、类、isa 指针等底层数据结构的详细介绍,请参考 Objective-C底层原理-NSObject。本文专注于 Runtime 的动态能力和实际应用。 消息发送机制 Objective-C 的消息发送机制是其动态性的核心体现。与 Swift 支持静态派发不同,Objective-C 的方法调用都是动态消息派发。更多关于两者的对比,请参考 Objective-C与Swift区别。 objc_msgSend Objective-C 的方法调用本质上是消息发送,编译器会将方法调用转换为 objc_msgSend 函数: // 源代码 [obj doSomething]; // 编译后 objc_msgSend(obj, @selector(doSomething)); 消息发送流程 1. 检查 receiver 是否为 nil,如果是则直接返回 2. 通过 isa 找到 receiver 的类对象 3. 在类对象的方法缓存(cache_t)中查找方法 4. 如果缓存命中,直接调用方法实现(IMP) 5. 如果缓存未命中,在类对象的方法列表中查找 6. 如果找到,缓存方法并调用 7. 如果未找到,沿着 superclass 链向上查找 8. 如果最终未找到,进入消息转发流程 方法缓存 为了提高消息发送效率,Runtime 在类对象中使用哈希表缓存最近调用的方法: ...

May 2, 2026

崩溃-治理

本文详细介绍常见崩溃类型的修复方法、防崩溃保护机制以及线上监控体系。 崩溃治理是 APM 稳定性子系的核心模块。完整的监控建设、多类型稳定性问题(Watchdog/OOM/CPU/IO)的统一治理与 Zombie/Coredump/MemoryGraph 等高级归因能力,请参考 APM 系列:指标体系、业界方案。 崩溃治理流程 flowchart TB subgraph S1[1. 崩溃发现] A1[线上监控报警] A2[用户反馈] A3[测试发现] end subgraph S2[2. 崩溃分析] B1[符号化堆栈] B2[问题定位] B3[根因分析] end subgraph S3[3. 问题修复] C1[代码修复] C2[防护措施] C3[回归测试] end subgraph S4[4. 效果验证] D1[灰度发布] D2[监控崩溃率] D3[持续跟踪] end A1 --> S2 A2 --> S2 A3 --> S2 S2 --> S3 S3 --> S4 常见崩溃类型及修复 1. 数组越界 // 崩溃示例 NSArray *array = @[@1, @2, @3]; id obj = array[10]; // NSRangeException // 修复方案1:边界检查 - (id)safeObjectAtIndex:(NSUInteger)index fromArray:(NSArray *)array { if (index < array.count) { return array[index]; } return nil; } // 修复方案2:分类安全方法 @implementation NSArray (Safe) - (id)safeObjectAtIndex:(NSUInteger)index { if (index < self.count) { return [self objectAtIndex:index]; } return nil; } @end // 修复方案3:Swift可选绑定 extension Array { subscript(safe index: Index) -> Element? { return indices.contains(index) ? self[index] : nil } } // 使用 let value = array[safe: 10] // 返回nil而不是崩溃 2. 字典插入nil // 崩溃示例 NSMutableDictionary *dict = [NSMutableDictionary dictionary]; NSString *value = nil; [dict setObject:value forKey:@"key"]; // NSInvalidArgumentException // 修复方案1:nil检查 - (void)setObject:(id)object forKey:(id)key inDict:(NSMutableDictionary *)dict { if (object && key) { dict[key] = object; } } // 修复方案2:分类安全方法 @implementation NSMutableDictionary (Safe) - (void)safeSetObject:(id)object forKey:(id)key { if (object && key) { [self setObject:object forKey:key]; } } @end // 修复方案3:使用NSNull dict[@"key"] = value ?: [NSNull null]; 3. Unrecognized Selector // 崩溃示例 NSString *str = @"hello"; [str performSelector:@selector(count)]; // unrecognized selector // 修复方案1:respondsToSelector检查 if ([obj respondsToSelector:@selector(someMethod)]) { [obj performSelector:@selector(someMethod)]; } // 修复方案2:消息转发防护 @implementation NSObject (CrashProtection) + (void)load { // Hook forwardingTargetForSelector: Method original = class_getInstanceMethod(self, @selector(forwardingTargetForSelector:)); Method swizzled = class_getInstanceMethod(self, @selector(safe_forwardingTargetForSelector:)); method_exchangeImplementations(original, swizzled); } - (id)safe_forwardingTargetForSelector:(SEL)aSelector { // 如果找不到方法,返回一个空实现的对象 id target = [self safe_forwardingTargetForSelector:aSelector]; if (target) return target; // 记录错误 NSLog(@"Unrecognized selector: %@ on %@", NSStringFromSelector(aSelector), self); // 返回一个能处理任何消息的对象 return [CrashStub shared]; } @end 4. KVO崩溃 // 崩溃场景1:重复移除观察者 [obj removeObserver:self forKeyPath:@"value"]; [obj removeObserver:self forKeyPath:@"value"]; // 崩溃 // 崩溃场景2:未移除观察者就释放 - (void)dealloc { // 忘记移除观察者 } // 修复方案:安全的KVO封装 @interface SafeKVOProxy : NSObject @property (nonatomic, weak) id observer; @property (nonatomic, weak) id target; @property (nonatomic, copy) NSString *keyPath; @property (nonatomic, copy) void (^callback)(id newValue); @end @implementation SafeKVOProxy - (instancetype)initWithTarget:(id)target keyPath:(NSString *)keyPath observer:(id)observer callback:(void (^)(id))callback { self = [super init]; if (self) { _target = target; _keyPath = keyPath; _observer = observer; _callback = callback; [target addObserver:self forKeyPath:keyPath options:NSKeyValueObservingOptionNew context:NULL]; } return self; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if (self.callback) { self.callback(change[NSKeyValueChangeNewKey]); } } - (void)dealloc { [_target removeObserver:self forKeyPath:_keyPath]; } @end 5. 多线程崩溃 // 崩溃场景:非线程安全的集合操作 // 线程1 [mutableArray addObject:obj1]; // 线程2 [mutableArray removeObjectAtIndex:0]; // 可能崩溃 // 修复方案1:加锁 @property (nonatomic, strong) NSLock *arrayLock; - (void)safeAddObject:(id)object { [self.arrayLock lock]; [self.mutableArray addObject:object]; [self.arrayLock unlock]; } // 修复方案2:使用GCD串行队列 @property (nonatomic, strong) dispatch_queue_t arrayQueue; - (void)safeAddObject:(id)object { dispatch_sync(self.arrayQueue, ^{ [self.mutableArray addObject:object]; }); } // 修复方案3:使用Actor(Swift Concurrency) actor SafeStorage { private var items: [Any] = [] func append(_ item: Any) { items.append(item) } func remove(at index: Int) { guard index < items.count else { return } items.remove(at: index) } func getAll() -> [Any] { return items } } let storage = SafeStorage() await storage.append(obj1) 6. 野指针崩溃 // 崩溃场景 __unsafe_unretained id unsafeRef = obj; obj = nil; [unsafeRef description]; // EXC_BAD_ACCESS // 修复方案1:使用weak而非unsafe_unretained __weak id weakRef = obj; obj = nil; [weakRef description]; // 安全,weakRef为nil // 修复方案2:Zombie Objects检测(调试用) // 在Scheme中启用Zombie Objects // 修复方案3:野指针检测工具 // 使用Address Sanitizer // Product -> Scheme -> Edit Scheme -> Diagnostics -> Address Sanitizer 7. Swift强制解包崩溃 // 崩溃示例 let value: String? = nil print(value!) // Fatal error: Unexpectedly found nil // 修复方案1:可选绑定 if let value = optionalValue { print(value) } // 修复方案2:空合运算符 let value = optionalValue ?? "default" // 修复方案3:guard语句 guard let value = optionalValue else { return } print(value) // 修复方案4:可选链 let length = optionalString?.count // 返回Int? 8. OOM(Out Of Memory)崩溃 OOM 是 iOS 最难治理的崩溃类型之一:系统因进程占用内存超过限制而将其强杀,不会产生标准崩溃日志,App 也没有机会在崩溃现场同步写下详细堆栈。因此 OOM 治理必须在事故前就把现场数据预埋下来,分为三个时段: ...

May 2, 2026

+load与+initialize的区别

+load和+initialize是Objective-C中两个特殊的类方法,它们都会被Runtime自动调用,但调用时机、调用方式和使用场景有很大区别。理解它们的差异对于iOS开发和性能优化非常重要。 基本定义 +load方法 +load方法在类被加载到内存时由Runtime自动调用,发生在main函数执行之前。 @implementation MyClass + (void)load { NSLog(@"MyClass loaded"); } @end +initialize方法 +initialize方法在类第一次收到消息时由Runtime自动调用,是一种懒加载机制。 @implementation MyClass + (void)initialize { NSLog(@"MyClass initialized"); } @end 核心区别对比 特性 +load +initialize 调用时机 main函数之前,类加载时 类首次收到消息时 调用方式 直接调用函数指针 通过objc_msgSend 调用次数 每个类只调用一次 可能被调用多次 是否阻塞启动 是 否 是否需要显式调用父类 否,自动调用 否,自动调用 Category的行为 都会被调用 会覆盖主类实现 线程安全 是 是 调用顺序 父类 -> 子类 -> Category 父类 -> 子类 未实现时的行为 不调用 可能调用父类实现 调用时机详解 +load的调用时机 +load的调用发生在dyld加载镜像的过程中: dyld加载Mach-O ↓ 映射到内存 ↓ 读取__DATA段中的__objc_nlclslist(非懒加载类列表) ↓ 调用所有类的+load方法 ↓ 读取__objc_nlcatlist(非懒加载分类列表) ↓ 调用所有分类的+load方法 ↓ main()函数执行 +initialize的调用时机 +initialize在类第一次收到消息时调用: ...

May 2, 2026

崩溃-信号处理

Unix信号是进程间通信和异常通知的重要机制。本文详细介绍Unix信号的基础知识、常见崩溃信号以及Signal Handler的正确实现方式。 Unix信号基础 什么是信号 信号的本质: ┌─────────────────────────────────────────────────────────────┐ │ Unix信号 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ 信号是一种软件中断机制: │ │ • 用于通知进程发生了某个事件 │ │ • 可以由内核、其他进程或进程自身发送 │ │ • 进程可以选择处理、忽略或使用默认行为 │ │ │ │ 信号的来源: │ │ • 硬件异常(如除零、非法内存访问) │ │ • 用户输入(如Ctrl+C) │ │ • 系统调用(如kill) │ │ • 软件条件(如定时器到期) │ │ │ └─────────────────────────────────────────────────────────────┘ 信号的生命周期 flowchart TD A["1. 信号产生(Generation)某个事件触发信号的产生"] --> B["2. 信号投递(Delivery)信号被投递到目标进程此时信号处于pending状态"] --> C["3. 信号处理(Handling)进程响应信号,执行以下之一"] C --> D["执行默认动作"] C --> E["执行自定义处理函数"] C --> F["忽略信号"] 常见信号类型 崩溃相关信号 信号 编号(macOS/iOS) 默认动作 说明 SIGABRT 6 终止+core 调用abort()产生 SIGBUS 10 终止+core 总线错误 SIGFPE 8 终止+core 算术异常 SIGILL 4 终止+core 非法指令 SIGSEGV 11 终止+core 段错误 SIGTRAP 5 终止+core 断点陷阱 SIGSYS 12 终止+core 非法系统调用 其他重要信号 信号 编号 默认动作 说明 SIGKILL 9 终止 强制终止(不可捕获) SIGTERM 15 终止 请求终止 SIGINT 2 终止 中断(Ctrl+C) SIGQUIT 3 终止+core 退出(Ctrl+\) SIGPIPE 13 终止 管道破裂 SIGALRM 14 终止 定时器到期 信号详解 SIGABRT (6) ──────────────────────────────────────── 触发条件: • 调用abort()函数 • 未捕获的NSException • C++ exception未处理 • assert失败(某些情况) 特点: • 可以捕获和处理 • 通常表示程序主动终止 SIGSEGV (11) ──────────────────────────────────────── 触发条件: • 访问无效内存地址 • 空指针解引用 • 栈溢出 • 访问已释放的内存 对应Mach异常: • EXC_BAD_ACCESS (KERN_INVALID_ADDRESS) • EXC_BAD_ACCESS (KERN_PROTECTION_FAILURE) SIGBUS (10) ──────────────────────────────────────── 触发条件: • 内存对齐错误 • 访问不存在的物理地址 • mmap映射失效 与SIGSEGV的区别: • SIGSEGV: 虚拟地址层面的问题(地址未映射或权限不足) • SIGBUS: 物理地址层面的问题(对齐错误、硬件问题等) SIGILL (4) ──────────────────────────────────────── 触发条件: • 执行非法指令 • Swift强制解包nil • 代码段损坏 对应Mach异常: • EXC_BAD_INSTRUCTION SIGFPE (8) ──────────────────────────────────────── 触发条件: • 整数除零 • 浮点异常 • 算术溢出 对应Mach异常: • EXC_ARITHMETIC SIGTRAP (5) ──────────────────────────────────────── 触发条件: • 断点指令 • Swift的fatalError • precondition失败 • 调试器断点 对应Mach异常: • EXC_BREAKPOINT Signal Handler 基本用法 #include <signal.h> // 简单的信号处理函数 void simple_handler(int sig) { // 处理信号 // 注意:这里只能调用异步信号安全的函数 } // 注册信号处理函数 void setup_simple_handler(void) { signal(SIGSEGV, simple_handler); signal(SIGABRT, simple_handler); } 使用sigaction(推荐) #include <signal.h> // 带详细信息的信号处理函数 void detailed_handler(int sig, siginfo_t *info, void *context) { // sig: 信号编号 // info: 信号详细信息 // context: 上下文(包含寄存器状态) } // 使用sigaction注册 void setup_sigaction_handler(void) { struct sigaction action; // 清零结构体 memset(&action, 0, sizeof(action)); // 设置处理函数 action.sa_sigaction = detailed_handler; // 设置标志 action.sa_flags = SA_SIGINFO // 使用sa_sigaction而非sa_handler | SA_ONSTACK; // 使用备用栈 // 清空信号掩码 sigemptyset(&action.sa_mask); // 注册信号处理器 sigaction(SIGSEGV, &action, NULL); sigaction(SIGABRT, &action, NULL); sigaction(SIGBUS, &action, NULL); sigaction(SIGFPE, &action, NULL); sigaction(SIGILL, &action, NULL); sigaction(SIGTRAP, &action, NULL); } siginfo_t结构 typedef struct { int si_signo; // 信号编号 int si_errno; // 错误码 int si_code; // 信号代码(更详细的原因) pid_t si_pid; // 发送信号的进程ID uid_t si_uid; // 发送信号的用户ID void *si_addr; // 故障地址(SIGSEGV/SIGBUS) int si_status; // 退出状态 // ... 其他字段 } siginfo_t; // 解析siginfo void parse_siginfo(int sig, siginfo_t *info) { printf("Signal: %d (%s)\n", sig, strsignal(sig)); printf("Code: %d\n", info->si_code); if (sig == SIGSEGV || sig == SIGBUS) { printf("Fault address: %p\n", info->si_addr); } // 解析信号代码 switch (sig) { case SIGSEGV: switch (info->si_code) { case SEGV_MAPERR: printf("Address not mapped\n"); break; case SEGV_ACCERR: printf("Invalid permissions\n"); break; } break; case SIGBUS: switch (info->si_code) { case BUS_ADRALN: printf("Invalid address alignment\n"); break; case BUS_ADRERR: printf("Nonexistent physical address\n"); break; } break; // ... 其他信号 } } 备用信号栈 为什么需要备用栈 问题场景: 当栈溢出时: 1. 栈空间已耗尽 2. 系统发送SIGSEGV信号 3. 信号处理函数需要栈空间来执行 4. 但是栈已经溢出了! 5. 信号处理函数无法执行 解决方案: 使用备用信号栈(Alternate Signal Stack) 设置备用栈 #include <signal.h> #include <stdlib.h> // 设置备用信号栈 void setup_alternate_stack(void) { // 分配栈空间 void *stack = malloc(SIGSTKSZ); if (stack == NULL) { return; } // 配置备用栈 stack_t ss; ss.ss_sp = stack; ss.ss_size = SIGSTKSZ; ss.ss_flags = 0; // 设置备用栈 if (sigaltstack(&ss, NULL) != 0) { free(stack); return; } // 注册信号处理器时使用SA_ONSTACK标志 struct sigaction action; action.sa_sigaction = signal_handler; action.sa_flags = SA_SIGINFO | SA_ONSTACK; sigemptyset(&action.sa_mask); sigaction(SIGSEGV, &action, NULL); } 异步信号安全 什么是异步信号安全 异步信号安全(Async-Signal-Safe): 信号可能在任何时刻打断正常执行: • 可能在malloc中间被打断 • 可能在printf中间被打断 • 可能持有某个锁时被打断 如果信号处理函数调用了相同的函数: • 可能导致死锁(重复获取锁) • 可能导致数据损坏(数据结构不一致) • 可能导致未定义行为 因此,信号处理函数只能调用"异步信号安全"的函数 异步信号安全函数列表 // 可以在信号处理函数中安全调用的函数(部分): // 文件操作 write() read() open() close() fsync() // 进程控制 _exit() _Exit() abort() raise() kill() getpid() // 信号相关 signal() sigaction() sigprocmask() sigemptyset() sigfillset() sigaddset() sigdelset() // 其他(注意:以下函数不在POSIX异步信号安全列表中,但大多数实现是安全的) strlen() // 大多数实现安全,但非POSIX标准 memcpy() // 大多数实现安全,但非POSIX标准 不安全的函数 // 不能在信号处理函数中调用的函数: // 内存分配 malloc() free() realloc() new / delete // 标准I/O printf() fprintf() fopen() fclose() // Objective-C objc_msgSend() NSLog() 任何OC方法调用 // C++ 大多数C++标准库函数 异常处理 // 其他 syslog() strtok() localtime() 完整的崩溃信号处理器 实现示例 #include <signal.h> #include <execinfo.h> #include <unistd.h> #include <fcntl.h> #include <string.h> // 需要捕获的信号 static const int kFatalSignals[] = { SIGABRT, SIGBUS, SIGFPE, SIGILL, SIGSEGV, SIGTRAP, SIGSYS, }; static const int kFatalSignalsCount = sizeof(kFatalSignals) / sizeof(kFatalSignals[0]); // 原始信号处理器 static struct sigaction g_originalActions[32]; // 崩溃日志文件路径(预先设置) static char g_crashLogPath[1024]; // 防止重入的标志 static volatile sig_atomic_t g_handling = 0; // 异步安全的字符串写入 static void safe_write_string(int fd, const char *str) { if (str) { write(fd, str, strlen(str)); } } // 异步安全的整数转字符串 static void safe_write_int(int fd, long value) { char buffer[32]; char *ptr = buffer + sizeof(buffer) - 1; *ptr = '\0'; int negative = value < 0; if (negative) value = -value; do { *--ptr = '0' + (value % 10); value /= 10; } while (value > 0); if (negative) *--ptr = '-'; write(fd, ptr, buffer + sizeof(buffer) - 1 - ptr); } // 异步安全的十六进制写入 static void safe_write_hex(int fd, unsigned long value) { char buffer[32]; char *ptr = buffer + sizeof(buffer) - 1; *ptr = '\0'; static const char hex[] = "0123456789abcdef"; do { *--ptr = hex[value & 0xf]; value >>= 4; } while (value > 0); *--ptr = 'x'; *--ptr = '0'; write(fd, ptr, buffer + sizeof(buffer) - 1 - ptr); } // 信号处理函数 static void crash_signal_handler(int sig, siginfo_t *info, void *context) { // 防止重入 if (g_handling) { return; } g_handling = 1; // 打开崩溃日志文件 int fd = open(g_crashLogPath, O_WRONLY | O_CREAT | O_TRUNC, 0644); if (fd < 0) { // 无法打开文件,尝试写入stderr fd = STDERR_FILENO; } // 写入基本信息 safe_write_string(fd, "=== Crash Report ===\n"); safe_write_string(fd, "Signal: "); safe_write_int(fd, sig); safe_write_string(fd, " ("); // 信号名称 switch (sig) { case SIGABRT: safe_write_string(fd, "SIGABRT"); break; case SIGBUS: safe_write_string(fd, "SIGBUS"); break; case SIGFPE: safe_write_string(fd, "SIGFPE"); break; case SIGILL: safe_write_string(fd, "SIGILL"); break; case SIGSEGV: safe_write_string(fd, "SIGSEGV"); break; case SIGTRAP: safe_write_string(fd, "SIGTRAP"); break; default: safe_write_string(fd, "UNKNOWN"); break; } safe_write_string(fd, ")\n"); // 信号代码 safe_write_string(fd, "Code: "); safe_write_int(fd, info->si_code); safe_write_string(fd, "\n"); // 故障地址 if (sig == SIGSEGV || sig == SIGBUS) { safe_write_string(fd, "Fault Address: "); safe_write_hex(fd, (unsigned long)info->si_addr); safe_write_string(fd, "\n"); } // 获取堆栈 // 注意:backtrace()严格来说也不是异步信号安全的函数 // 但为了获取崩溃堆栈,这是一个常见的权衡取舍 safe_write_string(fd, "\nBacktrace:\n"); void *callstack[128]; int frames = backtrace(callstack, 128); // 注意:backtrace_symbols使用malloc,不是异步安全的 // 这里我们只写入地址,符号化在后续处理 for (int i = 0; i < frames; i++) { safe_write_int(fd, i); safe_write_string(fd, ": "); safe_write_hex(fd, (unsigned long)callstack[i]); safe_write_string(fd, "\n"); } // 同步写入 if (fd != STDERR_FILENO) { fsync(fd); close(fd); } // 恢复原始处理器并重新发送信号 sigaction(sig, &g_originalActions[sig], NULL); raise(sig); } // 安装信号处理器 void install_crash_signal_handlers(const char *logPath) { // 保存日志路径 strncpy(g_crashLogPath, logPath, sizeof(g_crashLogPath) - 1); // 设置备用栈 static char alternate_stack[SIGSTKSZ]; stack_t ss; ss.ss_sp = alternate_stack; ss.ss_size = SIGSTKSZ; ss.ss_flags = 0; sigaltstack(&ss, NULL); // 配置信号处理器 struct sigaction action; memset(&action, 0, sizeof(action)); action.sa_sigaction = crash_signal_handler; action.sa_flags = SA_SIGINFO | SA_ONSTACK; sigemptyset(&action.sa_mask); // 注册所有致命信号 for (int i = 0; i < kFatalSignalsCount; i++) { int sig = kFatalSignals[i]; sigaction(sig, &action, &g_originalActions[sig]); } } // 卸载信号处理器 void uninstall_crash_signal_handlers(void) { for (int i = 0; i < kFatalSignalsCount; i++) { int sig = kFatalSignals[i]; sigaction(sig, &g_originalActions[sig], NULL); } } 获取上下文信息 从ucontext获取寄存器 #include <signal.h> #include <sys/ucontext.h> void signal_handler(int sig, siginfo_t *info, void *context) { ucontext_t *uc = (ucontext_t *)context; #if defined(__arm64__) || defined(__aarch64__) // ARM64架构 mcontext_t mc = uc->uc_mcontext; // 通用寄存器 for (int i = 0; i < 29; i++) { printf("x%d: 0x%llx\n", i, mc->__ss.__x[i]); } // 特殊寄存器 printf("fp: 0x%llx\n", mc->__ss.__fp); // 帧指针 printf("lr: 0x%llx\n", mc->__ss.__lr); // 链接寄存器 printf("sp: 0x%llx\n", mc->__ss.__sp); // 栈指针 printf("pc: 0x%llx\n", mc->__ss.__pc); // 程序计数器 printf("cpsr: 0x%x\n", mc->__ss.__cpsr); // 状态寄存器 #elif defined(__x86_64__) // x86_64架构 mcontext_t mc = uc->uc_mcontext; printf("rax: 0x%llx\n", mc->__ss.__rax); printf("rbx: 0x%llx\n", mc->__ss.__rbx); printf("rcx: 0x%llx\n", mc->__ss.__rcx); printf("rdx: 0x%llx\n", mc->__ss.__rdx); printf("rsi: 0x%llx\n", mc->__ss.__rsi); printf("rdi: 0x%llx\n", mc->__ss.__rdi); printf("rbp: 0x%llx\n", mc->__ss.__rbp); printf("rsp: 0x%llx\n", mc->__ss.__rsp); printf("rip: 0x%llx\n", mc->__ss.__rip); #endif } 从寄存器回溯堆栈 // 基于帧指针的堆栈回溯 void backtrace_from_context(ucontext_t *context) { mcontext_t mc = context->uc_mcontext; #if defined(__arm64__) uintptr_t pc = mc->__ss.__pc; uintptr_t fp = mc->__ss.__fp; printf("Backtrace:\n"); printf("0: 0x%lx (PC)\n", (unsigned long)pc); int frame = 1; while (fp != 0 && frame < 128) { // ARM64栈帧布局: // [fp+0]: 上一帧的fp // [fp+8]: 返回地址 uintptr_t *frame_ptr = (uintptr_t *)fp; // 安全检查 if ((uintptr_t)frame_ptr < 0x1000) break; uintptr_t return_addr = frame_ptr[1]; printf("%d: 0x%lx\n", frame, (unsigned long)return_addr); // 移动到上一帧 fp = frame_ptr[0]; frame++; } #endif } 信号掩码 阻塞信号 #include <signal.h> // 阻塞特定信号 void block_signals(void) { sigset_t mask; sigemptyset(&mask); sigaddset(&mask, SIGINT); sigaddset(&mask, SIGTERM); // 阻塞信号 sigprocmask(SIG_BLOCK, &mask, NULL); // 执行关键代码... // 解除阻塞 sigprocmask(SIG_UNBLOCK, &mask, NULL); } // 在信号处理期间阻塞其他信号 void setup_handler_with_mask(void) { struct sigaction action; action.sa_sigaction = signal_handler; action.sa_flags = SA_SIGINFO; // 在处理SIGSEGV时阻塞其他信号 sigfillset(&action.sa_mask); sigaction(SIGSEGV, &action, NULL); } 检查待处理信号 // 检查是否有信号待处理 void check_pending_signals(void) { sigset_t pending; sigpending(&pending); if (sigismember(&pending, SIGINT)) { printf("SIGINT is pending\n"); } } 信号与多线程 线程与信号 多线程环境下的信号处理: 1. 信号处理器是进程级的 • 所有线程共享信号处理器设置 • 一个线程设置的处理器影响整个进程 2. 信号掩码是线程级的 • 每个线程有自己的信号掩码 • pthread_sigmask()用于设置线程的信号掩码 3. 信号投递 • 同步信号(如SIGSEGV)投递给触发它的线程 • 异步信号投递给任意一个不阻塞该信号的线程 线程信号掩码 #include <pthread.h> #include <signal.h> // 设置线程信号掩码 void setup_thread_signal_mask(void) { sigset_t mask; sigemptyset(&mask); sigaddset(&mask, SIGPIPE); // 阻塞SIGPIPE pthread_sigmask(SIG_BLOCK, &mask, NULL); } // 专门的信号处理线程 void *signal_handler_thread(void *arg) { sigset_t mask; sigemptyset(&mask); sigaddset(&mask, SIGTERM); sigaddset(&mask, SIGINT); int sig; while (1) { // 同步等待信号 sigwait(&mask, &sig); printf("Received signal: %d\n", sig); if (sig == SIGTERM) { // 处理终止信号 break; } } return NULL; } // 主线程设置 void setup_signal_thread(void) { // 在主线程中阻塞这些信号 sigset_t mask; sigemptyset(&mask); sigaddset(&mask, SIGTERM); sigaddset(&mask, SIGINT); pthread_sigmask(SIG_BLOCK, &mask, NULL); // 创建信号处理线程 pthread_t thread; pthread_create(&thread, NULL, signal_handler_thread, NULL); } 常见问题与解决方案 信号处理器中的死锁 问题:信号处理器中调用了非异步安全函数导致死锁 场景: 1. 主线程调用malloc(),获取了内存分配锁 2. 此时收到SIGSEGV信号 3. 信号处理器中调用malloc() 4. 尝试获取同一把锁 -> 死锁 解决方案: • 只使用异步信号安全的函数 • 使用预分配的内存 • 将复杂处理延迟到信号处理器外部 信号处理器重入 // 问题:信号处理器被再次调用 // 解决:使用标志位防止重入 static volatile sig_atomic_t g_handling = 0; void signal_handler(int sig, siginfo_t *info, void *context) { // 检查是否正在处理 if (g_handling) { // 已经在处理中,直接返回 return; } // 设置标志 g_handling = 1; // 处理信号... // 注意:不要在这里清除标志 // 因为我们会重新发送信号 } 与其他SDK的冲突 // 问题:多个SDK都注册了信号处理器 // 解决方案:保存并调用原始处理器 static struct sigaction g_original_action; void my_signal_handler(int sig, siginfo_t *info, void *context) { // 我的处理逻辑 handle_crash(sig, info, context); // 调用原始处理器 if (g_original_action.sa_flags & SA_SIGINFO) { g_original_action.sa_sigaction(sig, info, context); } else if (g_original_action.sa_handler != SIG_DFL && g_original_action.sa_handler != SIG_IGN) { g_original_action.sa_handler(sig); } else { // 恢复默认行为 signal(sig, SIG_DFL); raise(sig); } } void install_handler(void) { struct sigaction action; action.sa_sigaction = my_signal_handler; action.sa_flags = SA_SIGINFO | SA_ONSTACK; sigemptyset(&action.sa_mask); // 保存原始处理器 sigaction(SIGSEGV, &action, &g_original_action); } 信号与Mach异常的关系 转换关系 flowchart LR subgraph Mach["Mach异常"] A1[EXC_BAD_ACCESS] A2[EXC_BAD_INSTRUCTION] A3[EXC_ARITHMETIC] A4[EXC_BREAKPOINT] A5[EXC_CRASH] A6[EXC_SOFTWARE] end subgraph Signal["Unix信号"] B1[SIGSEGV / SIGBUS] B2[SIGILL] B3[SIGFPE] B4[SIGTRAP] B5[SIGABRT] B6[取决于具体代码] end A1 --> B1 A2 --> B2 A3 --> B3 A4 --> B4 A5 --> B5 A6 --> B6 转换时机: ...

May 2, 2026