MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

探索Objective-C中Block与多线程的结合应用

2022-01-273.1k 阅读

Block简介

在Objective-C中,Block是一种带有自动变量(局部变量)的匿名函数。它允许我们将一段代码作为一个对象来处理,这在很多场景下都提供了极大的灵活性。

Block的基本语法

Block的声明和定义语法如下:

// 声明
returnType (^blockName)(parameterTypes);
// 定义并初始化
blockName = ^returnType(parameters) {
    // 代码块
};

例如,定义一个简单的Block,用于计算两个整数的和:

int (^sumBlock)(int, int) = ^int(int a, int b) {
    return a + b;
};
int result = sumBlock(3, 5);
NSLog(@"The sum is %d", result);

这里,sumBlock是一个Block变量,它接受两个int类型的参数并返回一个int类型的值。

Block捕获变量

Block可以捕获其定义时所在作用域内的自动变量。例如:

int num = 10;
void (^printBlock)() = ^{
    NSLog(@"The number is %d", num);
};
num = 20;
printBlock();

在上述代码中,printBlock捕获了num变量。尽管在Block定义之后num的值发生了改变,但Block打印的仍然是其定义时num的值,即10。这是因为Block默认捕获的是变量的副本。

如果想要捕获变量的引用,可以使用__block修饰符:

__block int num = 10;
void (^printBlock)() = ^{
    num = 20;
    NSLog(@"The number is %d", num);
};
printBlock();
NSLog(@"Outside block, the number is %d", num);

此时,printBlock捕获的是num的引用,在Block内对num的修改会反映到外部。

多线程基础

在现代应用开发中,多线程编程是提高应用性能和响应性的关键技术。多线程允许应用在同一时间执行多个任务,避免主线程阻塞,从而提供流畅的用户体验。

线程的概念

线程是程序执行流的最小单元,一个进程可以包含多个线程,这些线程共享进程的资源。在iOS开发中,主线程负责处理用户界面的更新和事件响应。如果在主线程中执行耗时操作,如网络请求或文件读取,会导致界面卡顿,用户体验变差。因此,需要将这些耗时操作放到子线程中执行。

iOS中的多线程技术

  1. NSThread:是Objective-C对线程的基础封装,使用起来较为简单,但需要手动管理线程的生命周期,包括创建、启动、暂停和销毁等操作。
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(backgroundTask) object:nil];
[thread start];
- (void)backgroundTask {
    // 耗时操作
    NSLog(@"Background task is running on %@", [NSThread currentThread]);
}
  1. NSOperationQueue:是一个基于队列的异步任务执行框架。它管理着一组NSOperation对象,按照一定的规则(如优先级、依赖关系等)将这些任务添加到队列中并执行。NSOperation是一个抽象类,有两个常用的子类:NSInvocationOperationNSBlockOperation
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
    // 耗时操作
    NSLog(@"Operation is running on %@", [NSThread currentThread]);
}];
[queue addOperation:operation];
  1. GCD(Grand Central Dispatch):是Apple开发的一个用于多核编程的技术,它基于队列和Block。GCD提供了一种更简洁、高效的方式来管理异步任务,自动处理线程的创建、调度和销毁,大大简化了多线程编程。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
    // 耗时操作
    NSLog(@"Task is running on %@", [NSThread currentThread]);
});

Block与多线程结合应用

使用Block与NSThread

虽然NSThread更倾向于传统的面向对象方式来创建和管理线程,但我们也可以结合Block来使用它。NSThread类提供了一个类方法detachNewThreadWithBlock:,允许我们以Block的形式定义线程执行的任务。

[NSThread detachNewThreadWithBlock:^{
    // 模拟耗时操作
    for (int i = 0; i < 1000000; i++) {
        // 一些计算
    }
    NSLog(@"Thread with block is running on %@", [NSThread currentThread]);
}];

这种方式创建线程非常简洁,不需要手动管理线程对象的生命周期,系统会在任务完成后自动销毁线程。然而,由于NSThread没有提供任务队列管理等高级功能,在处理复杂任务关系时,使用NSThread结合Block可能会显得力不从心。

Block与NSOperationQueue

  1. NSBlockOperation NSBlockOperation是NSOperation的子类,专门用于执行Block类型的任务。我们可以将多个Block添加到一个NSBlockOperation对象中,NSOperationQueue会自动并发执行这些Block,前提是系统资源允许。
NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
    // 第一个任务
    NSLog(@"First task in block operation on %@", [NSThread currentThread]);
}];
[blockOperation addExecutionBlock:^{
    // 第二个任务
    NSLog(@"Second task in block operation on %@", [NSThread currentThread]);
}];
NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init];
[operationQueue addOperation:blockOperation];

在上述代码中,我们创建了一个NSBlockOperation,并向其中添加了两个执行Block。NSOperationQueue会自动调度这些任务,它们可能会并发执行。

  1. 依赖关系与优先级 NSOperationQueue允许我们设置任务之间的依赖关系和优先级。这对于需要按照特定顺序执行的任务非常有用。
NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"Operation 1 is running on %@", [NSThread currentThread]);
}];
NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"Operation 2 is running on %@", [NSThread currentThread]);
}];
NSBlockOperation *operation3 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"Operation 3 is running on %@", [NSThread currentThread]);
}];
[operation2 addDependency:operation1];
[operation3 addDependency:operation2];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperations:@[operation1, operation2, operation3] waitUntilFinished:NO];

这里,operation2依赖于operation1operation3依赖于operation2。因此,operation1会首先执行,然后是operation2,最后是operation3

我们还可以设置任务的优先级:

NSBlockOperation *highPriorityOperation = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"High priority operation on %@", [NSThread currentThread]);
}];
NSBlockOperation *lowPriorityOperation = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"Low priority operation on %@", [NSThread currentThread]);
}];
highPriorityOperation.queuePriority = NSOperationQueuePriorityHigh;
lowPriorityOperation.queuePriority = NSOperationQueuePriorityLow;
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperations:@[highPriorityOperation, lowPriorityOperation] waitUntilFinished:NO];

在这个例子中,highPriorityOperation的优先级高于lowPriorityOperation,在资源有限的情况下,highPriorityOperation会优先执行。

Block与GCD

  1. 全局队列与并发执行 GCD提供了全局队列,我们可以通过dispatch_get_global_queue函数获取不同优先级的全局队列。
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(globalQueue, ^{
    // 并发执行的任务
    NSLog(@"Task on global queue is running on %@", [NSThread currentThread]);
});

在全局队列中添加的任务会自动并发执行,系统会根据当前系统资源动态分配线程来执行这些任务。

  1. 自定义队列与串行执行 除了全局队列,我们还可以创建自定义队列。通过dispatch_queue_create函数创建的队列默认是串行队列。
dispatch_queue_t customQueue = dispatch_queue_create("com.example.customqueue", DISPATCH_QUEUE_SERIAL);
dispatch_async(customQueue, ^{
    NSLog(@"First task on custom serial queue is running on %@", [NSThread currentThread]);
});
dispatch_async(customQueue, ^{
    NSLog(@"Second task on custom serial queue is running on %@", [NSThread currentThread]);
});

在上述代码中,两个任务会被添加到自定义的串行队列customQueue中,它们会按照添加的顺序依次执行,不会并发执行。

  1. 同步与异步执行 GCD提供了dispatch_syncdispatch_async函数来控制任务的执行方式。dispatch_async函数会异步执行任务,即不会阻塞当前线程,而dispatch_sync函数会同步执行任务,会阻塞当前线程直到任务完成。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 异步执行
dispatch_async(queue, ^{
    NSLog(@"Asynchronous task is running on %@", [NSThread currentThread]);
});
// 同步执行
dispatch_sync(queue, ^{
    NSLog(@"Synchronous task is running on %@", [NSThread currentThread]);
});

在实际应用中,异步执行适用于大多数耗时操作,如网络请求、文件读取等,以避免阻塞主线程。而同步执行通常用于一些需要等待结果的操作,如在多线程环境中访问共享资源时,为了保证数据一致性,可能需要同步执行相关操作。

  1. Dispatch Group Dispatch Group允许我们将多个任务组合在一起,并在所有任务完成后执行一个完成回调。这在需要等待多个异步任务完成后再进行下一步操作时非常有用。
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_async(group, queue, ^{
    // 第一个任务
    NSLog(@"First task in group on %@", [NSThread currentThread]);
});
dispatch_group_async(group, queue, ^{
    // 第二个任务
    NSLog(@"Second task in group on %@", [NSThread currentThread]);
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    // 所有任务完成后的回调
    NSLog(@"All tasks in group are completed on %@", [NSThread currentThread]);
});

在这个例子中,我们创建了一个Dispatch Group,并将两个任务异步添加到全局队列中。当这两个任务都完成后,会在主线程中执行dispatch_group_notify中的回调代码。

  1. Dispatch Semaphore Dispatch Semaphore是一种信号量机制,用于控制并发访问的资源数量。例如,当我们有一个共享资源,只允许一定数量的线程同时访问时,可以使用Dispatch Semaphore。
dispatch_semaphore_t semaphore = dispatch_semaphore_create(2); // 允许同时有2个线程访问
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
for (int i = 0; i < 5; i++) {
    dispatch_async(queue, ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        // 访问共享资源
        NSLog(@"Thread %d is accessing shared resource on %@", i, [NSThread currentThread]);
        // 模拟耗时操作
        sleep(1);
        dispatch_semaphore_signal(semaphore);
    });
}

在上述代码中,我们创建了一个信号量semaphore,允许同时有2个线程访问共享资源。每个线程在访问共享资源前先调用dispatch_semaphore_wait获取信号量,如果信号量不足(即已经有2个线程在访问),则线程会等待。访问完成后,调用dispatch_semaphore_signal释放信号量,允许其他线程获取。

线程安全与数据共享

当多个线程同时访问和修改共享数据时,可能会导致数据不一致等问题。因此,在多线程编程中,保证线程安全至关重要。

互斥锁(Mutex)

互斥锁是一种最基本的线程同步机制,它确保在同一时间只有一个线程可以访问共享资源。在Objective-C中,我们可以使用NSLock类来实现互斥锁。

NSLock *lock = [[NSLock alloc] init];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
__block int sharedValue = 0;
for (int i = 0; i < 10; i++) {
    dispatch_async(queue, ^{
        [lock lock];
        sharedValue++;
        NSLog(@"Thread %d incremented sharedValue to %d on %@", i, sharedValue, [NSThread currentThread]);
        [lock unlock];
    });
}

在这个例子中,NSLock对象lock用于保护共享变量sharedValue。每个线程在访问sharedValue之前先获取锁,访问完成后释放锁,这样就避免了多个线程同时修改sharedValue导致的数据不一致问题。

读写锁

读写锁用于区分读操作和写操作。它允许多个线程同时进行读操作,但在写操作时,必须独占资源,以防止数据不一致。在Objective-C中,可以使用pthread_rwlock_t来实现读写锁。

pthread_rwlock_t rwLock;
pthread_rwlock_init(&rwLock, NULL);
dispatch_queue_t readQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_queue_t writeQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
__block NSString *sharedString = @"Initial value";
// 读操作
for (int i = 0; i < 5; i++) {
    dispatch_async(readQueue, ^{
        pthread_rwlock_rdlock(&rwLock);
        NSLog(@"Thread %d read sharedString: %@ on %@", i, sharedString, [NSThread currentThread]);
        pthread_rwlock_unlock(&rwLock);
    });
}
// 写操作
dispatch_async(writeQueue, ^{
    pthread_rwlock_wrlock(&rwLock);
    sharedString = @"New value";
    NSLog(@"Thread writing sharedString to %@ on %@", sharedString, [NSThread currentThread]);
    pthread_rwlock_unlock(&rwLock);
});

在上述代码中,读操作使用pthread_rwlock_rdlock获取读锁,允许多个线程同时读取sharedString。写操作使用pthread_rwlock_wrlock获取写锁,此时其他线程无论是读还是写操作都需要等待写操作完成并释放锁。

原子属性与非原子属性

在Objective-C中,属性的atomicnonatomic修饰符也与线程安全有关。atomic属性会自动生成一些同步代码,以确保属性的读写操作是线程安全的。然而,这并不意味着整个对象是线程安全的,只是针对该属性的访问是安全的。

@interface MyClass : NSObject
@property (nonatomic, strong) NSString *nonatomicString;
@property (atomic, strong) NSString *atomicString;
@end
@implementation MyClass
@end

nonatomic属性则不会生成同步代码,访问速度更快,但在多线程环境下需要手动进行同步。在大多数情况下,由于atomic属性带来的性能开销较大,且不能保证整个对象的线程安全,我们更倾向于使用nonatomic属性,并手动管理线程同步。

实际应用场景

网络请求

在iOS应用开发中,网络请求是常见的耗时操作。使用Block与多线程技术可以确保网络请求在后台线程执行,不会阻塞主线程,同时通过Block可以方便地处理请求结果。

dispatch_queue_t networkQueue = dispatch_queue_create("com.example.networkqueue", DISPATCH_QUEUE_SERIAL);
dispatch_async(networkQueue, ^{
    NSURL *url = [NSURL URLWithString:@"https://example.com/api"];
    NSData *data = [NSData dataWithContentsOfURL:url];
    if (data) {
        dispatch_async(dispatch_get_main_queue(), ^{
            // 处理网络请求结果,更新UI
            NSString *responseString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
            NSLog(@"Network response: %@", responseString);
        });
    }
});

在这个例子中,网络请求在自定义的串行队列networkQueue中执行,当请求完成后,将结果切换到主线程进行处理和UI更新。

图片加载与缓存

在展示大量图片的应用中,图片加载和缓存是提高性能的关键。可以使用多线程结合Block来实现高效的图片加载和缓存机制。

NSMutableDictionary *imageCache = [NSMutableDictionary dictionary];
dispatch_queue_t imageQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
NSURL *imageURL = [NSURL URLWithString:@"https://example.com/image.jpg"];
dispatch_async(imageQueue, ^{
    if (imageCache[imageURL]) {
        // 从缓存中获取图片
        UIImage *cachedImage = imageCache[imageURL];
        dispatch_async(dispatch_get_main_queue(), ^{
            // 在主线程更新UI
            UIImageView *imageView = [[UIImageView alloc] initWithImage:cachedImage];
            [self.view addSubview:imageView];
        });
    } else {
        NSData *imageData = [NSData dataWithContentsOfURL:imageURL];
        if (imageData) {
            UIImage *loadedImage = [UIImage imageWithData:imageData];
            imageCache[imageURL] = loadedImage;
            dispatch_async(dispatch_get_main_queue(), ^{
                // 在主线程更新UI
                UIImageView *imageView = [[UIImageView alloc] initWithImage:loadedImage];
                [self.view addSubview:imageView];
            });
        }
    }
});

在这个代码中,首先检查图片是否在缓存中,如果在则直接从缓存中获取并在主线程更新UI。如果不在,则在后台线程加载图片,加载完成后将图片存入缓存并在主线程更新UI。

复杂计算任务

对于一些复杂的计算任务,如音频处理、图像处理等,可以将这些任务放到子线程中执行,以避免阻塞主线程。

dispatch_queue_t computeQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(computeQueue, ^{
    // 模拟复杂计算任务
    double result = 0;
    for (int i = 0; i < 1000000; i++) {
        result += sin(i) * cos(i);
    }
    dispatch_async(dispatch_get_main_queue(), ^{
        // 在主线程处理计算结果
        NSLog(@"Computation result: %f", result);
    });
});

这里,复杂的计算任务在全局队列中执行,完成后将结果切换到主线程进行处理和展示。

性能优化与注意事项

性能优化

  1. 合理选择队列:根据任务的性质和需求,合理选择全局队列、自定义串行队列或自定义并发队列。对于I/O密集型任务,如网络请求和文件读取,使用全局队列通常能获得较好的性能;对于计算密集型任务,可以考虑使用自定义并发队列,并根据系统资源调整并发度。
  2. 减少锁的使用:锁的使用会带来一定的性能开销,尽量减少不必要的锁操作。如果可能,通过数据结构的设计或任务调度方式来避免共享资源的竞争,从而减少锁的使用。
  3. 优化Block代码:尽量减少Block中捕获变量的数量,避免捕获不必要的变量,以减少内存开销。同时,对于频繁执行的Block,尽量将其定义为全局变量,避免重复创建。

注意事项

  1. 死锁:在使用锁或同步机制时,要注意避免死锁。死锁通常发生在多个线程相互等待对方释放锁的情况下。例如,线程A持有锁1并等待锁2,而线程B持有锁2并等待锁1,就会导致死锁。要仔细设计锁的获取和释放顺序,避免出现这种循环等待的情况。
  2. 内存管理:在多线程环境下,要注意内存管理。特别是在Block捕获变量时,要注意变量的生命周期和引用计数。如果Block被长时间持有,而捕获的变量在其他地方被释放,可能会导致野指针和内存泄漏问题。
  3. 线程安全的UI更新:在iOS开发中,只有主线程可以更新UI。因此,在子线程中完成任务后,如果需要更新UI,一定要切换到主线程进行操作,否则可能会导致应用崩溃或出现奇怪的UI问题。

通过合理运用Block与多线程技术,并注意性能优化和相关注意事项,我们可以开发出高效、稳定且响应性良好的iOS应用。无论是处理网络请求、图片加载还是复杂计算任务,这种结合方式都为我们提供了强大的工具和灵活的解决方案。在实际开发中,需要根据具体的业务需求和场景,选择最合适的多线程技术和Block应用方式,以达到最佳的性能和用户体验。同时,不断地进行测试和优化,确保应用在各种情况下都能稳定运行。多线程编程是一个复杂且具有挑战性的领域,需要开发者不断学习和实践,积累经验,才能更好地掌握和应用这一重要技术。