崩溃-采集

准确采集崩溃信息是分析和修复问题的前提。本文详细介绍iOS崩溃的捕获方案、堆栈回溯技术以及符号化原理。 崩溃采集架构 flowchart TB subgraph capture["崩溃捕获层"] direction LR mach["Mach Exception Handler"] signal["Unix Signal Handler"] exception["NSException Handler"] end subgraph collect["信息采集层"] direction LR backtrace["堆栈回溯(Backtrace)"] register["寄存器状态"] thread["线程信息"] device["设备/应用信息"] end subgraph storage["本地存储层"] direction LR async_write["异步安全写入"] file_manage["崩溃文件管理"] end subgraph report["上报处理层"] direction LR symbolicate["符号化解析"] aggregate["崩溃聚合"] analyze["数据分析"] end capture --> collect collect --> storage storage --> report Mach异常捕获 Mach是macOS/iOS的内核,提供了底层的异常处理机制。Mach异常是最底层的异常类型,发生在内核态,比Unix信号更早被触发。通过注册Mach异常处理器,可以在信号处理之前捕获到崩溃信息。 核心概念: Mach Port(端口):Mach内核中进程间通信(IPC)的基本单元,类似于文件描述符 Exception Port(异常端口):专门用于接收异常消息的端口 Task:Mach中的任务概念,对应一个进程,包含地址空间和资源 异常端口注册 #include <mach/mach.h> // 异常处理线程函数 static void *exception_handler_thread(void *arg) { mach_port_t exception_port = (mach_port_t)(uintptr_t)arg; while (1) { // 接收异常消息 struct { mach_msg_header_t head; mach_msg_body_t body; // ... 其他字段 } request; mach_msg_return_t result = mach_msg( &request.head, MACH_RCV_MSG | MACH_RCV_LARGE, 0, sizeof(request), exception_port, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL ); if (result == MACH_MSG_SUCCESS) { // 处理异常 handle_exception(&request); } } return NULL; } // 注册异常端口 kern_return_t register_exception_handler(void) { kern_return_t kr; mach_port_t exception_port; // 创建异常端口 kr = mach_port_allocate( mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &exception_port ); if (kr != KERN_SUCCESS) return kr; // 添加发送权限 kr = mach_port_insert_right( mach_task_self(), exception_port, exception_port, MACH_MSG_TYPE_MAKE_SEND ); if (kr != KERN_SUCCESS) return kr; // 设置任务异常端口 kr = task_set_exception_ports( mach_task_self(), EXC_MASK_BAD_ACCESS | EXC_MASK_BAD_INSTRUCTION | EXC_MASK_ARITHMETIC | EXC_MASK_BREAKPOINT, exception_port, EXCEPTION_DEFAULT | MACH_EXCEPTION_CODES, THREAD_STATE_NONE ); if (kr != KERN_SUCCESS) return kr; // 创建异常处理线程 pthread_t thread; pthread_create(&thread, NULL, exception_handler_thread, (void *)(uintptr_t)exception_port); return KERN_SUCCESS; } Mach异常处理流程 flowchart TD A["1. 异常发生"] --> B["2. 内核发送异常消息到异常端口"] B --> C["3. 异常处理线程接收消息"] C --> D["4. 解析异常信息"] D --> D1["exception type"] D --> D2["exception code"] D --> D3["thread state"] D1 & D2 & D3 --> E["5. 采集崩溃信息"] E --> E1["堆栈回溯"] E --> E2["寄存器状态"] E --> E3["其他上下文"] E1 & E2 & E3 --> F["6. 保存崩溃日志"] F --> G["7. 决定后续处理"] G --> G1["转发给原异常处理器"] G --> G2["让进程终止"] Unix信号捕获 Unix信号(Signal)是操作系统向进程发送的异步通知机制。当发生特定事件(如非法内存访问、算术错误等)时,内核会向进程发送相应的信号。iOS中,Mach异常通常会被转换为对应的Unix信号。 ...

June 21, 2026

崩溃

崩溃是影响App稳定性的最严重问题。当应用发生崩溃时,用户会被强制退出应用,严重影响用户体验和产品口碑。 本系列文章系统性地介绍iOS崩溃的原理、采集方案以及治理策略。 什么是崩溃 崩溃(Crash)是指应用程序因为异常或错误而被强制终止的现象。从技术角度看,崩溃可以分为以下几类: 崩溃类型 触发原因 典型场景 Mach异常 底层硬件/内核异常 访问无效内存、除零错误 Unix信号 系统信号导致进程终止 SIGABRT、SIGSEGV、SIGBUS NSException OC层未捕获的异常 数组越界、unrecognized selector 内存问题 OOM或内存损坏 内存不足、野指针 Watchdog 系统监控超时 启动超时、后台任务超时 崩溃率指标 崩溃率是衡量App稳定性的核心指标: 崩溃率计算方式: 1. 崩溃用户率(推荐) 崩溃用户率 = 发生崩溃的用户数 / 总活跃用户数 × 100% 2. 崩溃次数率 崩溃次数率 = 崩溃次数 / 启动次数 × 100% 3. 崩溃Session率 崩溃Session率 = 崩溃Session数 / 总Session数 × 100% 崩溃处理的整体架构 ┌─────────────────────────────────────────────────────────────┐ │ 崩溃处理架构 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ 崩溃发生 │ │ │ │ Mach异常 / Unix信号 / NSException / OOM │ │ │ └─────────────────────────────────────────────────────┘ │ │ ↓ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ 崩溃捕获 │ │ │ │ • Mach异常处理 │ │ │ │ • Signal Handler │ │ │ │ • NSSetUncaughtExceptionHandler │ │ │ └─────────────────────────────────────────────────────┘ │ │ ↓ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ 信息采集 │ │ │ │ • 堆栈信息 │ │ │ │ • 设备信息 │ │ │ │ • 用户上下文 │ │ │ │ • 崩溃现场 │ │ │ └─────────────────────────────────────────────────────┘ │ │ ↓ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ 本地存储 │ │ │ │ • 写入文件(需要异步安全) │ │ │ │ • 避免使用OC/Swift API │ │ │ └─────────────────────────────────────────────────────┘ │ │ ↓ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ 上报分析 │ │ │ │ • 下次启动时上报 │ │ │ │ • 符号化解析 │ │ │ │ • 聚合分析 │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────┘ 文章导航 本系列包含以下文章,建议按顺序阅读: ...

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

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

卡顿-主线程优化

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

卡顿-原理

理解卡顿的原理是优化的基础。本文从屏幕显示的底层机制出发,沿着"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应用中常见的性能瓶颈。本文介绍图片解码原理以及各种优化方案。 图片解码原理 为什么需要解码 图片文件(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

卡顿-离屏渲染

离屏渲染(Offscreen Rendering)是GPU渲染中的性能杀手。本文介绍离屏渲染的原理、触发条件以及优化方案。 什么是离屏渲染 正常渲染流程 正常情况下,GPU直接将渲染结果写入帧缓冲区(Frame Buffer): flowchart LR subgraph 正常渲染["正常渲染 On-Screen Rendering"] A[GPU渲染] --> B[Frame Buffer帧缓冲区] --> C[屏幕显示] end style A fill:#e1f5fe style B fill:#fff3e0 style C fill:#e8f5e9 特点: 直接渲染到帧缓冲区 渲染完成即可显示 效率最高 离屏渲染流程 离屏渲染需要先渲染到离屏缓冲区,再合成到帧缓冲区: flowchart LR subgraph 离屏渲染["离屏渲染 Offscreen Rendering"] A[GPU渲染] --> B[Offscreen Buffer离屏缓冲区] B --> C[Frame Buffer帧缓冲区] C --> D[屏幕显示] end E[额外的缓冲区] -.-> B style A fill:#e1f5fe style B fill:#f48fb1 style C fill:#fff3e0 style D fill:#e8f5e9 style E fill:#f48fb1,stroke:#f44336,stroke-dasharray: 5 5 性能开销: ...

May 2, 2026

卡顿

卡顿是影响用户体验的关键问题之一。当应用无法在一帧的时间内(通常为16.67ms)完成渲染工作时,就会出现掉帧,用户会感知到界面不流畅。 本系列文章从渲染原理入手,系统性地介绍iOS卡顿的检测与优化方法。 卡顿监控是 APM 流畅性子系的核心能力,线上指标体系、检测采集、上报策略请参考 APM 系列:指标体系、数据采集、业界方案(微信 Matrix / 字节 Slardar 卡死监控)。 什么是卡顿 iOS设备的屏幕刷新率通常为60Hz(ProMotion设备可达120Hz),这意味着每秒需要渲染60帧画面。每一帧的渲染时间窗口约为16.67ms(1000ms / 60)。 当CPU或GPU无法在这个时间窗口内完成工作时,就会发生掉帧(Frame Drop),用户会感知到卡顿。 掉帧情况 用户感知 严重程度 偶尔掉1-2帧 几乎无感知 轻微 连续掉帧 明显卡顿 中等 长时间阻塞(>250ms) 界面冻结 严重 渲染流程概述 iOS的渲染流程涉及CPU和GPU的协作,通过Core Animation Pipeline完成: flowchart TB subgraph APP["App 进程(CPU 阶段)"] direction TB Layout["Layout(布局计算)layoutSubviews、约束计算"] Display["Display(绘制显示)drawRect:、文本绘制"] Prepare["Prepare(准备)图片解码、格式转换"] Commit["Commit(提交)打包图层树"] Layout --> Display --> Prepare --> Commit end subgraph RS["Render Server 进程(GPU 阶段)"] direction TB Decode["解码图层树"] Vertex["顶点着色器"] Raster["光栅化"] Fragment["片段着色器"] Buffer["帧缓冲区写入"] Decode --> Vertex --> Raster --> Fragment --> Buffer end APP -->|"IPC 传输"| RS RS --> VSync["VSync 信号到来"] VSync --> Screen["显示到屏幕"] CPU与GPU的职责 阶段 主要工作 常见瓶颈 CPU 布局计算、视图创建、文本计算、图片解码 复杂布局、大量文本、主线程阻塞 GPU 纹理渲染、图层混合、离屏渲染 离屏渲染、大量透明图层、超大图片 卡顿的常见原因 CPU瓶颈 主线程阻塞:耗时操作在主线程执行 复杂布局:大量约束计算、频繁布局 文本计算:复杂富文本、大量文本渲染 图片解码:大图片在主线程解码 对象创建:大量对象的创建和销毁 GPU瓶颈 离屏渲染:圆角、阴影、遮罩触发离屏渲染 图层混合:大量透明图层叠加 超大图片:纹理尺寸超过GPU限制 复杂特效:模糊、滤镜等GPU密集操作 文章导航 本系列包含以下文章,建议按顺序阅读: ...

May 2, 2026