卡顿原因

YVTU

概述

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
    4
    NSLock *lock = [[NSLock alloc] init];
    [lock lock];
    // 访问共享资源
    [lock unlock];
  • NSRecursiveLock
    • 递归锁,允许同一个线程多次获取锁,适用于需要递归调用的场景。
    • 示例:
    1
    2
    3
    4
    NSRecursiveLock *recursiveLock = [[NSRecursiveLock alloc] init];
    [recursiveLock lock];
    // 递归访问共享资源
    [recursiveLock unlock];
  • NSCondition
    • 条件锁,结合互斥锁和条件变量,可用于线程之间的等待和通知机制。
    • 示例:
    1
    2
    3
    4
    5
    6
    NSCondition *condition = [[NSCondition alloc] init];
    [condition lock];
    // 等待某个条件
    [condition wait];
    // 条件满足后继续执行
    [condition unlock];
  • NSConditionLock
    • 带有条件的锁,可以基于条件值来锁定和解锁。
    • 示例:
    1
    2
    3
    4
    NSConditionLock *conditionLock = [[NSConditionLock alloc] initWithCondition:0];
    [conditionLock lockWhenCondition:1];
    // 访问共享资源
    [conditionLock unlockWithCondition:0];

GCD(Grand Central Dispatch)

  • 信号量(Dispatch Semaphore)
    • 用于控制同时访问特定资源的线程数量,可以用于实现简单的锁机制。
    • 示例:
    1
    2
    3
    4
    let semaphore = DispatchSemaphore(value: 1)
    semaphore.wait()
    // 访问共享资源
    semaphore.signal()
  • 屏障(Dispatch Barrier)
    • 用于在并发队列中创建同步点,确保在屏障之前的任务完成后,再执行屏障任务,屏障任务完成后,才继续执行后续任务。
    • 示例:
    1
    2
    3
    4
    let 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
    4
    var 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
    9
    NSOperationQueue *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
    12
    actor 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
2
3
4
5
let ciImage = CIImage(image: inputImage)
let filter = CIFilter(name: "CISepiaTone")
filter?.setValue(ciImage, forKey: kCIInputImageKey)
filter?.setValue(0.8, forKey: kCIInputIntensityKey)
let outputImage = filter?.outputImage

Core Animation

  • Core Animation 是iOS的高效动画框架,它会将大部分动画的执行过程自动转移到GPU上。这包括视图的平移、缩放、旋转、淡入淡出等基本动画效果。
  • 通过使用CALayer和各种动画属性(如position、transform等),你可以创建平滑的动画,这些动画将在GPU上硬件加速执行。
  • 示例代码:
1
2
3
4
5
6
let layer = CALayer()
layer.position = CGPoint(x: 100, y: 100)
let animation = CABasicAnimation(keyPath: "position")
animation.toValue = CGPoint(x: 200, y: 200)
animation.duration = 1.0
layer.add(animation, forKey: "positionAnimation")

SpriteKit 和 SceneKit

  • SpriteKit 和 SceneKit 是两个高层次的框架,分别用于2D和3D游戏开发。它们内部利用GPU进行图形渲染和物理模拟,极大地减少了CPU的负担。
  • SpriteKit 用于2D游戏,可以通过简单的代码创建和渲染复杂的场景。
  • SceneKit 用于3D游戏,支持光照、阴影、物理引擎等高级特性,这些都在GPU上进行计算。
  • 示例代码:
1
2
3
4
let scene = SKScene(size: CGSize(width: 1024, height: 768))
let spriteNode = SKSpriteNode(imageNamed: "Spaceship")
spriteNode.position = CGPoint(x: scene.size.width/2, y: scene.size.height/2)
scene.addChild(spriteNode)

OpenGL ES

  • 虽然OpenGL ES已经被Metal取代为主要的图形API,但它仍然可以用于将图形渲染任务转移到GPU上。使用OpenGL ES,开发者可以编写自定义着色器,并在GPU上执行图形渲染。
  • 示例代码:
1
2
3
// 创建OpenGL ES上下文
EAGLContext *context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3];
[EAGLContext setCurrentContext:context];

Core Graphics (Quartz 2D)

  • Core Graphics 虽然主要在CPU上执行,但是在一些情况下,特别是当使用硬件加速的绘图操作时(如在UIView上进行绘图操作),系统会自动将部分绘图操作转移到GPU上。
  • Core Graphics主要用于绘制矢量图形、处理图像、渲染文本等。

VideoToolbox

  • VideoToolbox 是一个用于硬件加速的视频编码、解码的框架。通过使用VideoToolbox,可以将视频编解码任务转移到GPU上,从而大大减轻CPU的压力,尤其在处理高清视频时效果显著。
  • 示例代码:
1
2
var videoSession: VTDecompressionSession?
VTDecompressionSessionCreate(nil, formatDescription, nil, nil, nil, &videoSession)

Accelerate 框架

  • Accelerate 框架包含了一组高度优化的数学计算函数库,包括向量运算、矩阵运算、FFT(快速傅里叶变换)等。虽然主要在CPU上运行,但在某些情况下(如使用vImage),可以通过Metal Performance Shaders (MPS)将部分计算任务转移到GPU上。

磁盘 IO 过于密集

磁盘 IO 过于密集可能会导致应用程序卡顿,这是因为磁盘操作通常是阻塞性的,尤其是在读取或写入大量数据时。为了缓解这一问题,可以将磁盘 IO 操作放到后台线程中执行,避免阻塞主线程。这时,Swift Concurrency 技术(如 async/await 和 Task)可以派上用场。

下面是一个使用 SwiftUI 和 Swift Concurrency 处理磁盘 IO 的示例代码。这个例子展示了如何在后台线程中执行磁盘读取操作,并将结果更新到 UI 上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import SwiftUI

struct ContentView: View {
@State private var data: String = "Loading..." // `data` 用于存储从磁盘读取的数据,并在 UI 中显示。

var body: some View {
VStack {
Text(data)
.padding()
Button("Load Data") {
loadData()
}
}
}

func loadData() {
// 通过 `Task` 创建一个并发上下文来运行异步代码块。在这个代码块中执行耗时的磁盘 IO 操作。
Task {
// 在后台执行磁盘 IO 操作
let loadedData = await performDiskIO()
// 在主线程更新 UI
await MainActor.run {
data = loadedData
}
}
}

// 模拟一个磁盘 IO 操作,可能是从文件中读取大数据
func performDiskIO() async -> String {
// 模拟磁盘操作耗时
try? await Task.sleep(nanoseconds: 2_000_000_000) // 2 seconds delay

// 这里可以进行实际的磁盘读取操作
// 例如读取文件内容:
// let fileURL = ...
// let data = try? String(contentsOf: fileURL)

return "Data Loaded Successfully!"
}
}

@main
struct DiskIOApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}

跨进程通信导致卡顿

进程间通信(IPC)是一种重要的机制,它允许不同的进程或应用程序之间交换信息。然而,某些系统API的调用可能会导致卡顿或性能问题,特别是在以下几种情况下:

CNCopyCurrentNetworkInfo 获取 WiFi 信息

CNCopyCurrentNetworkInfo 用于获取当前的 WiFi 网络信息。由于它涉及到与系统的网络服务交互,可能会导致性能问题,特别是在频繁调用时。

1
2
3
4
#import <SystemConfiguration/CaptiveNetwork.h>

NSDictionary *networkInfo = (__bridge NSDictionary *)CNCopyCurrentNetworkInfo((__bridge CFStringRef)@"en0");
NSLog(@"Network Info: %@", networkInfo);

在获取 WiFi 信息时,如果调用频繁,可能会对性能产生负面影响。

设置系统钥匙串 (Keychain) 中的值

钥匙串用于存储敏感数据,如密码。操作钥匙串通常涉及加密操作,这可能会导致一定的性能开销,特别是在操作较多或数据较大时。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#import <Security/Security.h>

NSString *service = @"com.example.myapp";
NSString *account = @"user@example.com";
NSString *password = @"password123";

NSDictionary *query = @{
(id)kSecClass: (id)kSecClassGenericPassword,
(id)kSecAttrService: service,
(id)kSecAttrAccount: account,
(id)kSecValueData: [password dataUsingEncoding:NSUTF8StringEncoding]
};

SecItemAdd((__bridge CFDictionaryRef)query, NULL); // 添加或更新钥匙串项

NSUserDefaults 调用写操作

使用 NSUserDefaults 造成的跨进程通信导致的卡顿,可以使用 Swift Concurrency 来实现一个轻量级的键值存储方案,利用 Swift 的 async/await 来处理并发操作。以下是一个使用 Swift 和 SwiftUI 的可运行示例,展示了如何实现和使用一个基于 Swift Concurrency 的轻量级键值存储类。

轻量级 UserDefaults 替代实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import Foundation

// `LightweightUserDefaults` 类是一个 `actor`,用于确保其内部状态在并发环境中是安全的。`actor` 会自动处理并发的访问,因此不需要手动使用锁或队列。
actor LightweightUserDefaults {
private var storage: [String: Any] = [:]

func set(_ value: Any?, forKey key: String) async {
if let value = value {
storage[key] = value
} else {
storage.removeValue(forKey: key)
}
}

func value(forKey key: String) async -> Any? {
return storage[key]
}

func removeValue(forKey key: String) async {
storage.removeValue(forKey: key)
}
}

SwiftUI 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import SwiftUI

// 通过按钮调用异步方法将用户输入存储、读取和清除到自定义的键值存储类中。
struct ContentView: View {
@StateObject private var userDefaults = LightweightUserDefaultsWrapper()
@State private var inputText: String = ""

var body: some View {
VStack(spacing: 20) {
TextField("Enter some text", text: $inputText)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()

Button("Save to Custom UserDefaults") {
Task {
await userDefaults.save(inputText, forKey: "userInput")
}
}

Button("Load from Custom UserDefaults") {
Task {
if let loadedText = await userDefaults.load(forKey: "userInput") as? String {
inputText = loadedText
}
}
}

Button("Clear Custom UserDefaults") {
Task {
await userDefaults.clear(forKey: "userInput")
}
}
}
.padding()
}
}

// 用于与 SwiftUI 的视图绑定,并在需要时调用异步的存储和读取操作。
@MainActor
class LightweightUserDefaultsWrapper: ObservableObject {
private let lightweightUserDefaults = LightweightUserDefaults()

func save(_ value: Any?, forKey key: String) async {
await lightweightUserDefaults.set(value, forKey: key)
}

func load(forKey key: String) async -> Any? {
return await lightweightUserDefaults.value(forKey: key)
}

func clear(forKey key: String) async {
await lightweightUserDefaults.removeValue(forKey: key)
}
}

@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}

CLLocationManager 获取当前位置权限状态

CLLocationManager 用于定位服务,如果频繁调用定位权限状态的获取,可能会导致性能问题。特别是在获取权限状态时,系统会进行一些额外的操作,这可能会造成卡顿。

1
2
3
CLLocationManager *locationManager = [[CLLocationManager alloc] init];
CLAuthorizationStatus status = [CLLocationManager authorizationStatus];
// 获取当前的定位权限状态

频繁调用 authorizationStatus 可能导致性能问题,特别是当应用在进行定位相关的操作时。

UIPasteboard 设置和获取值

UIPasteboard 用于实现剪贴板功能,读取和写入剪贴板数据可能会涉及到 IPC 操作。如果操作过于频繁或数据过大,可能会导致卡顿。

1
2
3
UIPasteboard *pasteboard = [UIPasteboard generalPasteboard];
pasteboard.string = @"SomeString"; // 设置剪贴板内容
NSString *clipboardContent = pasteboard.string; // 获取剪贴板内容

UIApplication 通过 openURL 打开其他应用

使用 UIApplication 的 openURL: 方法可以打开其他应用程序。如果调用频繁或目标应用处理慢,可能会导致卡顿。

1
2
3
4
5
6
7
8
NSURL *url = [NSURL URLWithString:@"myapp://"];
[[UIApplication sharedApplication] openURL:url options:@{} completionHandler:^(BOOL success) {
if (success) {
NSLog(@"Opened URL successfully");
} else {
NSLog(@"Failed to open URL");
}
}];

如果目标应用的响应时间较长,可能会影响当前应用的流畅度。