卡顿原因
概述
iOS开发中,由于UIKit的非线程安全性,所有UI操作必须在主线程执行。系统每16ms(1/60帧)重绘UI至屏幕。若主线程进行耗时操作或发生死锁,会阻碍UI刷新,导致卡顿甚至卡死。主线程基于Runloop机制处理任务,Runloop支持多种事件回调,包括事件进入、处理前后等时机。若主线程在任一环节被阻塞,会导致UI和交互都无法进行,这是卡死、卡顿的根本原因。
线程和锁
线程和锁的使用是导致卡死的主要原因,常见问题包括:
- 死锁问题:如dispatch_once中同步访问主线程导致的死锁。
- 锁竞争:子线程占用锁资源导致主线程卡死。
- 磁盘IO密集:主线程磁盘IO耗时过长。
- 跨进程通信:如UIPasteBoard、NSUserDefaults等导致的卡死。
- OC方法调用死锁:如dyld lock、selector lock和OC runtime lock互相等待。
同步原语
同步原语(synchronization primitive)会阻塞读写任务执行。以下是iOS中常用的会阻塞读写任务执行的同步原语:
锁(Locks)
- NSLock- 基本的互斥锁,用于保护临界区,确保同一时间只有一个线程访问资源。
- 示例:
 1 
 2
 3
 4NSLock *lock = [[NSLock alloc] init]; 
 [lock lock];
 // 访问共享资源
 [lock unlock];
- NSRecursiveLock- 递归锁,允许同一个线程多次获取锁,适用于需要递归调用的场景。
- 示例:
 1 
 2
 3
 4NSRecursiveLock *recursiveLock = [[NSRecursiveLock alloc] init]; 
 [recursiveLock lock];
 // 递归访问共享资源
 [recursiveLock unlock];
- NSCondition- 条件锁,结合互斥锁和条件变量,可用于线程之间的等待和通知机制。
- 示例:
 1 
 2
 3
 4
 5
 6NSCondition *condition = [[NSCondition alloc] init]; 
 [condition lock];
 // 等待某个条件
 [condition wait];
 // 条件满足后继续执行
 [condition unlock];
- NSConditionLock- 带有条件的锁,可以基于条件值来锁定和解锁。
- 示例:
 1 
 2
 3
 4NSConditionLock *conditionLock = [[NSConditionLock alloc] initWithCondition:0]; 
 [conditionLock lockWhenCondition:1];
 // 访问共享资源
 [conditionLock unlockWithCondition:0];
GCD(Grand Central Dispatch)
- 信号量(Dispatch Semaphore)- 用于控制同时访问特定资源的线程数量,可以用于实现简单的锁机制。
- 示例:
 1 
 2
 3
 4let semaphore = DispatchSemaphore(value: 1) 
 semaphore.wait()
 // 访问共享资源
 semaphore.signal()
- 屏障(Dispatch Barrier)- 用于在并发队列中创建同步点,确保在屏障之前的任务完成后,再执行屏障任务,屏障任务完成后,才继续执行后续任务。
- 示例:
 1 
 2
 3
 4let concurrentQueue = DispatchQueue(label: "com.example.queue", attributes: .concurrent) 
 concurrentQueue.async(flags: .barrier) {
 // 写操作,确保独占访问
 }
POSIX 线程(pthread)
- 读写锁(pthread_rwlock_t) - 允许多个线程同时读,或者一个线程写,适用于读多写少的场景。
- 示例:
 - 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10- pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER; 
 // 读锁
 pthread_rwlock_rdlock(&rwlock);
 // 读取共享资源
 pthread_rwlock_unlock(&rwlock);
 // 写锁
 pthread_rwlock_wrlock(&rwlock);
 // 写入共享资源
 pthread_rwlock_unlock(&rwlock);
- 互斥锁(pthread_mutex_t) - 基本的互斥锁,类似于NSLock。
- 示例:
 - 1 
 2
 3
 4- pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; 
 pthread_mutex_lock(&mutex);
 // 访问共享资源
 pthread_mutex_unlock(&mutex);
Objective-C 的 @synchronized 指令
- @synchronized- 语法糖,用于在Objective-C中创建互斥锁,保护代码块。
- 示例:
 1 
 2
 3@synchronized(self) { 
 // 访问共享资源
 }
低级锁
- os_unfair_lock- 低级别的锁,替代了过时的OSSpinLock,适用于需要高性能的锁场景。
- 示例:
 1 
 2
 3
 4var unfairLock = os_unfair_lock() 
 os_unfair_lock_lock(&unfairLock)
 // 访问共享资源
 os_unfair_lock_unlock(&unfairLock)
原子属性
- 原子性属性(Atomic Properties)- 在Objective-C中,通过设置属性为atomic,编译器会自动生成线程安全的访问器,内部使用锁机制确保原子性。
- 示例:
 1 @property (atomic, strong) NSString *name; 
高层次抽象
- NSOperationQueue 和 操作依赖(Dependencies)- 虽然不是直接的锁机制,但通过设置操作的依赖关系,可以控制任务的执行顺序,间接实现同步。
- 示例:
 1 
 2
 3
 4
 5
 6
 7
 8
 9NSOperationQueue *queue = [[NSOperationQueue alloc] init]; 
 NSOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
 // 任务1
 }];
 NSOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
 // 任务2,依赖任务1
 }];
 [op2 addDependency:op1];
 [queue addOperations:@[op1, op2] waitUntilFinished:NO];
Swift 并发(Swift Concurrency)
- Actors- 在Swift 5.5及以上版本中引入的Actor模型,用于保护数据,确保同一时间只有一个任务可以访问其内部状态。
- 示例:
 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12actor MyActor { 
 var value: Int = 0
 
 func increment() {
 value += 1
 }
 }
 let myActor = MyActor()
 Task {
 await myActor.increment()
 }
这些同步原语各有优缺点,选择合适的同步机制取决于具体的应用场景。例如,pthread_rwlock_t适用于读多写少的情况,而NSLock或@synchronized则适用于简单的互斥需求。GCD的信号量和屏障则提供了更高层次的并发控制手段。
CPU 负载
CPU 负载过重也会导致耗时长,CPU主要负责用户交互的处理,如果能够将运算转移到 GPU 上,CPU 就可以更轻松的处理来自用户的交互。比如可以通过使用 CoreAnimation 中 layer.cornerRadius 属性和 masksToBounds 属性替代圆角图片、使用 CoreGraphics 绘制图形替代图片、使用 CoreAnimation 绘制动画替代 UIView 动画等方式优化性能。
以下是一些常见的方法和技术,可以在iOS中将计算任务从CPU转移到GPU:
Metal 框架
- Metal 是苹果为iOS和macOS提供的底层图形和计算API,它允许开发者直接访问GPU来执行图形渲染和并行计算任务。
- 使用场景包括:- 渲染3D图形:Metal提供高效的图形渲染功能,比OpenGL ES更高效。
- 图像处理:可以使用Metal编写自定义着色器(Shaders)进行实时图像处理,如滤镜应用、视频处理等。
- 并行计算:通过Metal的计算管线(Compute Pipeline),可以编写计算着色器(Compute Shaders)在GPU上执行大量并行计算任务,如物理模拟、数据分析等。
- 示例代码:
 1 
 2
 3
 4
 5
 6// 使用Metal进行简单的计算操作 
 let device = MTLCreateSystemDefaultDevice()
 let commandQueue = device?.makeCommandQueue()
 let shaderLibrary = device?.makeDefaultLibrary()
 let computeFunction = shaderLibrary?.makeFunction(name: "computeShader")
 let computePipelineState = try? device?.makeComputePipelineState(function: computeFunction!)
Core Image
- Core Image 是一个强大的图像处理框架,内置了许多优化的滤镜(Filters),并能够自动将图像处理任务分配到GPU上执行。
- 通过Core Image,你可以非常容易地进行图像增强、滤镜应用、面部识别等,而这些操作都会尽量在GPU上执行,以减轻CPU的负担。
- 示例代码:
| 1 | let ciImage = CIImage(image: inputImage) | 
Core Animation
- Core Animation 是iOS的高效动画框架,它会将大部分动画的执行过程自动转移到GPU上。这包括视图的平移、缩放、旋转、淡入淡出等基本动画效果。
- 通过使用CALayer和各种动画属性(如position、transform等),你可以创建平滑的动画,这些动画将在GPU上硬件加速执行。
- 示例代码:
| 1 | let layer = CALayer() | 
SpriteKit 和 SceneKit
- SpriteKit 和 SceneKit 是两个高层次的框架,分别用于2D和3D游戏开发。它们内部利用GPU进行图形渲染和物理模拟,极大地减少了CPU的负担。
- SpriteKit 用于2D游戏,可以通过简单的代码创建和渲染复杂的场景。
- SceneKit 用于3D游戏,支持光照、阴影、物理引擎等高级特性,这些都在GPU上进行计算。
- 示例代码:
| 1 | let scene = SKScene(size: CGSize(width: 1024, height: 768)) | 
OpenGL ES
- 虽然OpenGL ES已经被Metal取代为主要的图形API,但它仍然可以用于将图形渲染任务转移到GPU上。使用OpenGL ES,开发者可以编写自定义着色器,并在GPU上执行图形渲染。
- 示例代码:
| 1 | // 创建OpenGL ES上下文 | 
Core Graphics (Quartz 2D)
- Core Graphics 虽然主要在CPU上执行,但是在一些情况下,特别是当使用硬件加速的绘图操作时(如在UIView上进行绘图操作),系统会自动将部分绘图操作转移到GPU上。
- Core Graphics主要用于绘制矢量图形、处理图像、渲染文本等。
VideoToolbox
- VideoToolbox 是一个用于硬件加速的视频编码、解码的框架。通过使用VideoToolbox,可以将视频编解码任务转移到GPU上,从而大大减轻CPU的压力,尤其在处理高清视频时效果显著。
- 示例代码:
| 1 | var videoSession: VTDecompressionSession? | 
Accelerate 框架
- Accelerate 框架包含了一组高度优化的数学计算函数库,包括向量运算、矩阵运算、FFT(快速傅里叶变换)等。虽然主要在CPU上运行,但在某些情况下(如使用vImage),可以通过Metal Performance Shaders (MPS)将部分计算任务转移到GPU上。
磁盘 IO 过于密集
磁盘 IO 过于密集可能会导致应用程序卡顿,这是因为磁盘操作通常是阻塞性的,尤其是在读取或写入大量数据时。为了缓解这一问题,可以将磁盘 IO 操作放到后台线程中执行,避免阻塞主线程。这时,Swift Concurrency 技术(如 async/await 和 Task)可以派上用场。
下面是一个使用 SwiftUI 和 Swift Concurrency 处理磁盘 IO 的示例代码。这个例子展示了如何在后台线程中执行磁盘读取操作,并将结果更新到 UI 上。
| 1 | import SwiftUI | 
跨进程通信导致卡顿
进程间通信(IPC)是一种重要的机制,它允许不同的进程或应用程序之间交换信息。然而,某些系统API的调用可能会导致卡顿或性能问题,特别是在以下几种情况下:
CNCopyCurrentNetworkInfo 获取 WiFi 信息
CNCopyCurrentNetworkInfo 用于获取当前的 WiFi 网络信息。由于它涉及到与系统的网络服务交互,可能会导致性能问题,特别是在频繁调用时。
| 1 | 
 | 
在获取 WiFi 信息时,如果调用频繁,可能会对性能产生负面影响。
设置系统钥匙串 (Keychain) 中的值
钥匙串用于存储敏感数据,如密码。操作钥匙串通常涉及加密操作,这可能会导致一定的性能开销,特别是在操作较多或数据较大时。
| 1 | 
 | 
NSUserDefaults 调用写操作
使用 NSUserDefaults 造成的跨进程通信导致的卡顿,可以使用 Swift Concurrency 来实现一个轻量级的键值存储方案,利用 Swift 的 async/await 来处理并发操作。以下是一个使用 Swift 和 SwiftUI 的可运行示例,展示了如何实现和使用一个基于 Swift Concurrency 的轻量级键值存储类。
轻量级 UserDefaults 替代实现
| 1 | import Foundation | 
SwiftUI 示例
| 1 | import SwiftUI | 
CLLocationManager 获取当前位置权限状态
CLLocationManager 用于定位服务,如果频繁调用定位权限状态的获取,可能会导致性能问题。特别是在获取权限状态时,系统会进行一些额外的操作,这可能会造成卡顿。
| 1 | CLLocationManager *locationManager = [[CLLocationManager alloc] init]; | 
频繁调用 authorizationStatus 可能导致性能问题,特别是当应用在进行定位相关的操作时。
UIPasteboard 设置和获取值
UIPasteboard 用于实现剪贴板功能,读取和写入剪贴板数据可能会涉及到 IPC 操作。如果操作过于频繁或数据过大,可能会导致卡顿。
| 1 | UIPasteboard *pasteboard = [UIPasteboard generalPasteboard]; | 
UIApplication 通过 openURL 打开其他应用
使用 UIApplication 的 openURL: 方法可以打开其他应用程序。如果调用频繁或目标应用处理慢,可能会导致卡顿。
| 1 | NSURL *url = [NSURL URLWithString:@"myapp://"]; | 
如果目标应用的响应时间较长,可能会影响当前应用的流畅度。