卡顿原因
概述
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
10pthread_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
4pthread_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://"]; |
如果目标应用的响应时间较长,可能会影响当前应用的流畅度。