崩溃日志解读

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

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

卡顿-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

值类型和引用类型的区别

定义 值类型 值类型是指变量直接存储自身字段或描述信息的类型。值类型具有值语义,当将一个值类型变量赋值给另一个变量时,语义上会得到一份独立的值;底层是否立即复制完整数据,取决于编译器优化和写时拷贝等实现。 引用类型 引用类型是指变量存储的是指向数据在内存中位置的引用(指针)。当将一个引用类型变量赋值给另一个变量时,两个变量指向同一块内存区域。 Objective-C中的类型分类 在Objective-C中: 值类型:基础数据类型(int、float、double、BOOL、char等)、结构体(struct)、枚举(enum) 引用类型:除基础数据类型之外的大部分类型,包括NSObject及其子类(NSString、NSArray、NSDictionary、自定义类等) Swift中的类型分类 Swift对类型的分类更加清晰: 值类型:基础数据类型(Int、Float、Double、Bool等)、字符串(String)、结构体(Struct)、枚举(Enum)、数组(Array)、字典(Dictionary)、集合(Set)等 引用类型:类(Class)、闭包(Closure)、Actor等 拷贝机制差异 值类型 - 值语义拷贝 每个值类型变量都有自己的字段存储 对一个变量的操作不会影响另一个变量 赋值语义上会得到独立副本;如果字段是引用类型,复制的是引用本身,底层也可能通过写时拷贝延迟真正的数据复制 引用类型 - 浅拷贝 引用类型在内存中有一个指向该位置的引用 引用类型的变量可以指向相同类型的数据 对一个变量进行的操作会影响另一变量所指向的数据 内存分配机制 需要注意:值类型/引用类型描述的是语义模型,不等价于栈/堆分配规则。Swift编译器会根据生命周期、逃逸分析、优化级别、协议类型包装、集合存储等因素决定具体放在哪里。 理解Swift对象和字段的内存位置,可以先记住三个规则: class实例本体通常在堆上,通过引用计数管理生命周期 struct/enum的字段通常内联存储在这个值本身所在的位置 class类型的字段存储的是对象引用,也就是一个指针,真实class实例仍然在堆上 常见存储位置 局部值类型变量:生命周期明确、未逃逸时,通常可以放在当前线程的栈区,甚至被优化到寄存器中 class实例:实例本体在堆区,局部变量中保存的是指向堆对象的引用 class里的struct属性:struct属性内联存储在class实例这块堆内存中 struct里的class属性:struct内部只保存class引用,真实class实例仍然在堆上 被逃逸闭包捕获的值类型可能随闭包上下文一起存储在堆区 Array、Dictionary、Set、String等写时拷贝值类型通常只有一小段描述信息是值本身,真实元素或字符缓冲区可能在堆上 协议类型(existential container)持有大值类型时,如果超过存在容器的内联缓冲区大小,会使用堆分配 indirect enum的间接关联值会通过堆上的盒子存储,常见于递归枚举 全局变量、静态变量不属于栈区,也不是普通意义上的堆对象,它们通常位于全局/静态存储区 栈区内存分配和销毁通常只需移动栈顶指针,成本较低;堆区更动态,但分配、释放和引用计数维护都有额外成本。 struct中包含class属性 class Dog { var name: String init(name: String) { self.name = name } } struct Person { var age: Int var dog: Dog } var p1 = Person(age: 18, dog: Dog(name: "Lucky")) var p2 = p1 p2.age = 20 p2.dog.name = "Max" print(p1.age) // 18 print(p1.dog.name) // Max 内存关系可以近似理解为: ...

May 10, 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

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

卡顿-原理

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

iOS多线程方案对比 方案 简介 语言 生命周期管理 pthread POSIX标准的多线程API C 手动管理 NSThread 面向对象的线程封装 OC/Swift 手动管理 GCD Grand Central Dispatch C/OC/Swift 自动管理 NSOperation 基于GCD的面向对象封装 OC/Swift 自动管理 pthread pthread是POSIX标准的多线程API,是最底层的多线程方案,使用C语言编写。 #import <pthread.h> void *threadFunction(void *param) { NSLog(@"pthread执行任务: %@", [NSThread currentThread]); return NULL; } - (void)createPthread { pthread_t thread; pthread_create(&thread, NULL, threadFunction, NULL); } 特点: 跨平台,可移植性强 使用复杂,需要手动管理线程生命周期 实际开发中很少直接使用 NSThread NSThread是苹果对pthread的面向对象封装,使用更加简单。 创建线程的方式 // 方式1:实例方法创建,需要手动启动 NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(doTask) object:nil]; thread.name = @"MyThread"; [thread start]; // 方式2:类方法创建,自动启动 [NSThread detachNewThreadSelector:@selector(doTask) toTarget:self withObject:nil]; // 方式3:隐式创建 [self performSelectorInBackground:@selector(doTask) withObject:nil]; 常用方法 // 获取当前线程 NSThread *currentThread = [NSThread currentThread]; // 获取主线程 NSThread *mainThread = [NSThread mainThread]; // 判断是否是主线程 BOOL isMain = [NSThread isMainThread]; // 线程休眠 [NSThread sleepForTimeInterval:2.0]; [NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:2.0]]; // 退出当前线程 [NSThread exit]; 线程间通信 // 回到主线程执行 [self performSelectorOnMainThread:@selector(updateUI) withObject:nil waitUntilDone:NO]; // 在指定线程执行 [self performSelector:@selector(doTask) onThread:thread withObject:nil waitUntilDone:NO]; GCD(Grand Central Dispatch) GCD是苹果推出的多线程解决方案,基于C语言实现,自动管理线程的生命周期。GCD的核心概念是 队列(Queue) 和任务(Task)。 ...

May 2, 2026

卡顿-图片优化

图片处理是iOS应用中常见的性能瓶颈。本文介绍图片解码原理以及各种优化方案。 图片解码原理 为什么需要解码 图片文件(PNG、JPEG等)是压缩格式,无法直接显示。GPU需要的是位图(Bitmap)格式: flowchart LR A[压缩图片PNG/JPEG] --> B[解码DecodeCPU密集操作] B --> C[位图BitmapGPU可直接使用] 位图大小 = 宽度 × 高度 × 每像素字节数 例如:1000×1000 RGBA图片 = 1000 × 1000 × 4 = 4MB 默认解码时机 // 加载图片(此时未解码) let image = UIImage(named: "large_image") // 设置到ImageView(仍未解码) imageView.image = image // 在 CATransaction commit 的 prepare 阶段,Core Animation 会在主线程对未解码的图片执行解码 // 未提前解码的图片一定会在此阶段被解码,从而阻塞主线程引发卡顿 异步解码 基本原理 将解码工作移到后台线程: sequenceDiagram participant M as 主线程 participant B as 后台线程 M->>B: 请求加载图片 Note over M: 继续处理其他事件 B->>B: 加载压缩数据 B->>B: 解码为位图 B->>B: 创建CGImage B->>M: 返回解码后的图片 M->>M: 更新UI 实现方案1:强制解码 extension UIImage { /// 强制解码图片 func decodedImage() -> UIImage? { guard let cgImage = self.cgImage else { return nil } let width = cgImage.width let height = cgImage.height // 创建位图上下文 let colorSpace = CGColorSpaceCreateDeviceRGB() let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedFirst.rawValue | CGBitmapInfo.byteOrder32Little.rawValue) guard let context = CGContext( data: nil, width: width, height: height, bitsPerComponent: 8, bytesPerRow: 0, space: colorSpace, bitmapInfo: bitmapInfo.rawValue ) else { return nil } // 绘制到上下文(触发解码) context.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height)) // 从上下文创建新图片 guard let decodedCGImage = context.makeImage() else { return nil } return UIImage(cgImage: decodedCGImage, scale: scale, orientation: imageOrientation) } } // 异步使用 func loadImageAsync(named name: String, completion: @escaping (UIImage?) -> Void) { DispatchQueue.global(qos: .userInitiated).async { let image = UIImage(named: name)?.decodedImage() DispatchQueue.main.async { completion(image) } } } 实现方案2:使用ImageIO import ImageIO class ImageDecoder { static func decodeImage(from url: URL) -> UIImage? { guard let source = CGImageSourceCreateWithURL(url as CFURL, nil) else { return nil } let options: [CFString: Any] = [ kCGImageSourceShouldCache: true, kCGImageSourceShouldCacheImmediately: true // 立即解码 ] guard let cgImage = CGImageSourceCreateImageAtIndex(source, 0, options as CFDictionary) else { return nil } return UIImage(cgImage: cgImage) } static func decodeImageAsync(from url: URL, completion: @escaping (UIImage?) -> Void) { DispatchQueue.global(qos: .userInitiated).async { let image = ImageDecoder.decodeImage(from: url) DispatchQueue.main.async { completion(image) } } } } 实现方案3:使用UIGraphicsImageRenderer extension UIImage { func decodedImageUsingRenderer() -> UIImage { let format = UIGraphicsImageRendererFormat() format.scale = scale format.opaque = true format.preferredRange = .standard let renderer = UIGraphicsImageRenderer(size: size, format: format) return renderer.image { context in draw(at: .zero) } } } 图片降采样 为什么需要降采样 当图片尺寸远大于显示尺寸时,加载原图是浪费: ...

May 2, 2026

卡顿-检测

准确检测卡顿是优化的前提。本文介绍多种卡顿检测方案,从开发调试到线上监控都有对应的方案。 检测方案概览 方案 原理 优点 缺点 适用场景 CADisplayLink 监控帧节奏、FPS、慢帧/掉帧 简单直接、适合趋势观察 无法直接给出堆栈,FPS 只能辅助判断 开发调试/线上辅助指标 RunLoop Observer 监控RunLoop状态 可获取堆栈 有一定开销 开发/线上 子线程Ping 定时检测主线程响应 实现简单 精度有限 线上监控 Instruments 系统级分析 信息详细 只能开发时用 性能分析 MetricKit 系统数据收集 无额外开销 iOS 13+ 线上监控 Sentry ANR V2 帧延迟分析 区分阻塞类型 需集成SDK 线上监控 1. CADisplayLink监控 基本原理 CADisplayLink 是一个和屏幕显示刷新节奏同步的回调。它适合回答“当前 UI 刷新是否顺畅、是否出现慢帧、掉帧或冻结帧”,但 FPS 本身只是辅助参考,不能单独作为卡顿结论。 原因是: FPS 只描述结果,不描述原因:FPS 下降只能说明某段时间显示帧变少了,不能告诉你是主线程计算、布局、图片解码、锁等待、I/O、GPU 渲染还是系统主动降刷新率导致的。 ProMotion 会动态调整刷新率:在支持 120Hz 的机型上,系统可能根据内容和功耗策略把刷新率降到 10Hz、24Hz、30Hz、60Hz、80Hz、120Hz 等。静止页面低 FPS 可能只是系统主动省电,不是卡顿。 主线程阻塞会让回调延迟:如果主线程真的被阻塞,CADisplayLink 回调本身也会延后,此时需要结合实际回调间隔、RunLoop 状态和堆栈采样判断。 因此线上卡顿监控更推荐记录 帧间隔 / 慢帧 / 冻结帧 / 交互场景,而不是只显示一个“当前 FPS”。 ...

May 7, 2026