卡顿-原理

理解卡顿的原理是优化的基础。本文从屏幕显示的底层机制出发,沿着"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

卡顿-主线程优化

主线程阻塞是卡顿最常见的原因。本文介绍如何优化主线程的工作,减少阻塞时间。 主线程的职责 主线程(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

崩溃日志解读

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

崩溃-治理

本文详细介绍常见崩溃类型的修复方法、防崩溃保护机制以及线上监控体系。 崩溃治理是 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

崩溃-信号处理

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

耗电-网络优化

网络是iOS设备上仅次于屏幕的耗电大户。和其他模块不同,网络耗电的"坑"非常隐蔽——请求本身只持续几十毫秒,但无线电(Radio)会有10~20秒的"尾能耗"。本文从无线电能效模型出发,给出具体的网络优化手段。 一、无线电能效模型回顾 RRC状态机再看一眼 无线电不是"开就费电、关就不费电"这么简单。它在连接和空闲之间存在中间状态: stateDiagram-v2 [*] --> Idle: 初始/长时间空闲 Idle --> Connected: 发送/接收 Connected --> ShortDRX: 空闲~1秒 ShortDRX --> LongDRX: 空闲~5秒 LongDRX --> Idle: 空闲~10秒 Connected --> Connected: 持续通信 ShortDRX --> Connected: 新请求 LongDRX --> Connected: 新请求 状态 功耗 说明 Connected 最高(12W) 数据传输中 Short DRX 中等 周期性监听页 paging,仍持有连接 Long DRX 低 周期变长,仍在空中接口 Idle 极低(~几mW) 完全释放 一次请求从发起到彻底回到Idle大约 12~20秒。这个尾巴就是"Tail Energy"。 能效启示 10次请求,每次间隔2秒: - Radio持续处于Connected/DRX状态 ≈ 30秒 - 实际传输时间可能只有几百ms 10次请求合并成1次: - Radio活跃时间 ≈ 15秒(主要是尾巴) - 节省约50%~70%的Radio能耗 结论:网络能效的核心就是"减少Radio醒来的次数"。 ...

May 2, 2026

耗电-定位与传感器优化

定位是iOS设备上公认的"耗电大户",GPS冷启动一次动辄十几秒的高功耗;传感器(加速度计、陀螺仪等)虽然单次功耗不高,但高频采样会累积成可观的电量消耗;屏幕则是 持续 耗电的模块,其功耗和亮度、刷新率、像素颜色都强相关。本文聚焦这三类问题,给出工程化的优化方案。 一、定位精度与功耗的关系 iOS通过 CLLocationManager 提供定位服务,底层融合了 GPS、蜂窝基站、WiFi、蓝牙信标 等多源信号。精度越高,需要开启的硬件越多,功耗越高。 精度常量 精度范围 硬件使用 典型功耗 kCLLocationAccuracyBestForNavigation <5m GPS + 传感器融合(需插电) 极高 kCLLocationAccuracyBest ~10m GPS持续 高 kCLLocationAccuracyNearestTenMeters ~10m GPS + WiFi 高 kCLLocationAccuracyHundredMeters ~100m WiFi + 基站 中 kCLLocationAccuracyKilometer ~1km WiFi + 基站(低频) 低 kCLLocationAccuracyThreeKilometers ~3km 基站 极低 注:iOS 14+ 引入的 ReducedAccuracy 是用户授权层面的"粗略位置",通过 CLAccuracyAuthorization.reducedAccuracy 体现,而不是 desiredAccuracy 常量。即使 App 申请了高精度,只要用户选择了"粗略位置",系统就只会返回约5公里精度的数据,功耗也随之降到极低。 不同业务场景的精度选择 业务 推荐精度 导航 BestForNavigation(仅使用中 + 插电时) 打车、骑行记录 Best 外卖、附近的人 HundredMeters 天气、资讯推荐 Kilometer 或 ThreeKilometers 反欺诈、粗粒度风控 ReducedAccuracy(尊重隐私 + 省电) 常见反例 // Bad: 所有场景都用Best,页面打开就开,关闭不关 locationManager.desiredAccuracy = kCLLocationAccuracyBest locationManager.startUpdatingLocation() // Bad: distanceFilter用kCLDistanceFilterNone,每次都回调 locationManager.distanceFilter = kCLDistanceFilterNone 正确姿势:按需精度 + 距离过滤 class WeatherLocationClient: NSObject, CLLocationManagerDelegate { private let manager = CLLocationManager() private var completion: ((CLLocation) -> Void)? override init() { super.init() manager.delegate = self manager.desiredAccuracy = kCLLocationAccuracyKilometer manager.distanceFilter = 500 // 500米才更新 } func requestOnce(_ completion: @escaping (CLLocation) -> Void) { self.completion = completion manager.requestLocation() // 一次性定位,完成后自动停止 } func locationManager(_ manager: CLLocationManager, didUpdateLocations locs: [CLLocation]) { guard let loc = locs.last else { return } completion?(loc) completion = nil } func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { completion = nil } } requestLocation()(iOS 9+)是最省电的一次性定位入口,系统会在获取到满足精度的定位后自动停止硬件。 ...

May 2, 2026

耗电-原理

本文从硬件功耗模型、iOS系统的能效调度机制、以及各个硬件模块的功耗特性三个维度,系统性地讲解iOS App耗电的底层原理。 功耗的基本概念 功率、能量与电量 功率(Power):单位时间内消耗的能量,单位为瓦特(W)。 能量(Energy):功率在时间上的积分,单位为焦耳(J)或毫瓦时(mWh)。 电量(Battery Capacity):电池能存储的总能量,单位为毫安时(mAh)。 \[ Energy = \int P(t)\,dt \]对于一段时间内近似恒定的功率,可以简化为 \( E = P \times t \)。这也是所有耗电优化的起点——要么降功率(P),要么降时间(t)。 电池电压与电流 iPhone使用锂电池,标称电压约为3.7~3.8V。操作系统看到的电流则随着硬件负载实时变化。可以近似认为: \[ P(t) = V \times I(t) \]在App层面,我们无法直接测量V和I,但可以通过 UIDevice.current.batteryLevel 观察电量百分比,通过 IOKit 私有API读取更精细的电池状态(后面检测篇会详细讲)。 硬件功耗特性 不同硬件模块的功耗曲线差异非常大。理解每个模块的特性,才能找到最有效的优化点。 CPU:P-State与C-State 现代CPU(包括Apple Silicon)通过 动态电压频率调整(DVFS) 来平衡性能与功耗。 P-State(Performance State) P-State描述CPU在工作时的电压/频率档位。以Apple A系列芯片为例,大核和小核都有多个P-State: flowchart LR subgraph P-State P0["P0最高频率功耗最大"] P1["P1中高频率"] P2["P2中低频率"] P3["P3低频率功耗小"] end P0 -.->|负载下降| P1 P1 -.-> P2 P2 -.-> P3 P3 -.->|负载上升| P0 CPU的功耗与频率大致呈 立方关系(电压随频率升高,功耗 ≈ V² × f ≈ f³)。这意味着: ...

May 2, 2026

耗电-CPU与后台优化

CPU是App最容易"无意识"耗电的模块,而后台则是最容易"偷偷"耗电的场景。本文聚焦这两大问题域,给出具体的优化手段。 一、CPU耗电的根本原因 根据 耗电-原理 中的分析,CPU耗电由三部分构成: 活跃时间:CPU处于C0/P0的时间。 瞬时功率:和P-State频率相关。 唤醒次数:频繁从C-State唤醒无法进入深度休眠。 对应三条优化方向: 优化方向 策略 减少活跃 任务及时结束,该停就停 降低功率 避免无谓的满核满频,使用合适的QoS 合并唤醒 聚合Timer、聚合任务,让CPU能连续工作后进入深休眠 二、识别CPU耗电热点 从MetricKit入手 cumulativeCPUTime 是线上最直接的CPU指标。对比同版本前后、同机型前后的CPU时长,能快速定位回归。 Time Profiler的使用技巧 在Instruments中抓一份Time Profiler Trace后: 按"Heavy Stack"视图查看耗时函数。 关注后台的采样(通过Xcode的生命周期切换复现)。 查看线程调用频率——单次耗时低但高频调用的函数,同样是耗电大头。 代码层面的CPU异味 // 1. 死循环轮询 while isWaitingForData { if dataReady { break } } // 2. 主线程sleep循环 DispatchQueue.global().async { while true { checkStatus() Thread.sleep(forTimeInterval: 0.1) } } // 3. 密集Timer Timer.scheduledTimer(withTimeInterval: 0.016, repeats: true) { _ in self.updateUI() } // 4. 无限制的CADisplayLink displayLink = CADisplayLink(target: self, selector: #selector(tick)) displayLink.add(to: .main, forMode: .common) // 页面隐藏后忘记invalidate 这些代码在Time Profiler中都会显示出异常高的调用频率。 ...

May 2, 2026