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

Objective-C中的PromiseKit异步编程模式

2024-06-015.2k 阅读

一、PromiseKit 简介

在传统的 Objective-C 异步编程中,我们常常会使用回调函数(block)来处理异步操作的结果。然而,当异步操作嵌套过多时,代码会变得难以阅读和维护,形成所谓的 “回调地狱”。PromiseKit 就是为了解决这一问题而诞生的一个强大的库,它提供了一种更优雅、更易读的方式来处理异步操作。

PromiseKit 基于 Promise 的概念。一个 Promise 代表一个异步操作的最终结果,它有三种状态:pending(进行中)、fulfilled(已完成)和 rejected(已拒绝)。当异步操作完成时,Promise 会从 pending 状态转换为 fulfilled 状态,并返回操作的结果;如果操作失败,Promise 会转换为 rejected 状态,并返回错误信息。

二、PromiseKit 的安装

  1. 使用 CocoaPods 安装 在项目的 Podfile 文件中添加以下内容:
pod 'PromiseKit'

然后在终端中执行 pod install 命令,CocoaPods 会自动下载并集成 PromiseKit 到项目中。 2. 手动安装 你也可以手动下载 PromiseKit 的源代码,将其添加到项目中。手动安装相对复杂一些,需要处理依赖等问题,因此推荐使用 CocoaPods 进行安装。

三、基本使用

  1. 创建 Promise 在 PromiseKit 中,创建一个 Promise 非常简单。以下是一个简单的示例,模拟一个异步加载图片的操作:
#import <PromiseKit/PromiseKit.h>

PMKPromise *loadImagePromise = [PMKPromise new:^(PMKFulfiller fulfill, PMKRejecter reject) {
    // 模拟异步操作,例如从网络加载图片
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        UIImage *image = [UIImage imageNamed:@"exampleImage"];
        if (image) {
            fulfill(image);
        } else {
            NSError *error = [NSError errorWithDomain:@"ImageLoadingError" code:1 userInfo:nil];
            reject(error);
        }
    });
}];

在上述代码中,我们使用 [PMKPromise new:] 方法创建了一个 Promise。在这个 block 中,我们有两个参数:fulfillrejectfulfill 用于在异步操作成功时传递结果,reject 用于在操作失败时传递错误。

  1. 处理 Promise 创建 Promise 后,我们需要处理它的结果。可以使用 then 方法来处理成功的情况,catch 方法来处理失败的情况:
[loadImagePromise then:^(UIImage *image) {
    // 图片加载成功,更新 UI 显示图片
    UIImageView *imageView = [[UIImageView alloc] initWithImage:image];
    [self.view addSubview:imageView];
} catch:^(NSError *error) {
    // 图片加载失败,显示错误信息
    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Error" message:error.localizedDescription preferredStyle:UIAlertControllerStyleAlert];
    UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil];
    [alertController addAction:okAction];
    [self presentViewController:alertController animated:YES completion:nil];
}];

then 方法接受一个 block,当 Promise 成功时,这个 block 会被执行,并且传递 Promise 的结果(这里是加载的图片)。catch 方法同样接受一个 block,当 Promise 失败时,这个 block 会被执行,并且传递错误信息。

四、链式调用

PromiseKit 的强大之处在于它支持链式调用,这使得我们可以轻松地处理多个异步操作的顺序执行。

  1. 顺序执行多个异步操作 假设我们有一个需求,先从网络获取用户信息,然后根据用户信息获取用户的订单列表。可以这样实现:
PMKPromise *fetchUserInfoPromise = [PMKPromise new:^(PMKFulfiller fulfill, PMKRejecter reject) {
    // 模拟从网络获取用户信息的异步操作
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSDictionary *userInfo = @{@"name": @"John Doe", @"age": @30};
        fulfill(userInfo);
    });
}];

PMKPromise *fetchOrderListPromise = [fetchUserInfoPromise then:^(NSDictionary *userInfo) {
    return [PMKPromise new:^(PMKFulfiller fulfill, PMKRejecter reject) {
        // 模拟根据用户信息获取订单列表的异步操作
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSArray *orderList = @[@"Order 1", @"Order 2"];
            fulfill(orderList);
        });
    };
}];

[fetchOrderListPromise then:^(NSArray *orderList) {
    // 处理订单列表
    NSLog(@"Order List: %@", orderList);
} catch:^(NSError *error) {
    // 处理错误
    NSLog(@"Error: %@", error.localizedDescription);
}];

在上述代码中,fetchUserInfoPromise 成功后,会执行 then 中的 block,在这个 block 中又返回了一个新的 Promise fetchOrderListPromise。这样就实现了两个异步操作的顺序执行。

  1. 链式调用中的错误处理 在链式调用中,如果任何一个 Promise 失败,整个链条都会中断,并执行 catch 块。例如:
PMKPromise *promise1 = [PMKPromise new:^(PMKFulfiller fulfill, PMKRejecter reject) {
    // 模拟第一个异步操作
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSError *error = [NSError errorWithDomain:@"Promise1Error" code:1 userInfo:nil];
        reject(error);
    });
}];

PMKPromise *promise2 = [promise1 then:^(id value) {
    // 这个 block 不会执行,因为 promise1 失败了
    return [PMKPromise new:^(PMKFulfiller fulfill, PMKRejecter reject) {
        // 模拟第二个异步操作
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            fulfill(@"Result of promise2");
        });
    };
}];

[promise2 then:^(id value) {
    NSLog(@"Success: %@", value);
} catch:^(NSError *error) {
    NSLog(@"Error: %@", error.localizedDescription);
}];

在这个例子中,promise1 故意返回了一个错误,因此 promise2 中的 then block 不会执行,而是直接执行 catch block 来处理错误。

五、并发操作

除了顺序执行异步操作,PromiseKit 还支持并发执行多个异步操作,并在所有操作完成后处理结果。

  1. 使用 when 方法并发执行多个 Promise 假设我们有三个异步操作,分别是获取用户信息、获取用户的好友列表和获取用户的收藏列表。我们可以并发执行这些操作,并在所有操作完成后统一处理结果:
PMKPromise *fetchUserInfoPromise = [PMKPromise new:^(PMKFulfiller fulfill, PMKRejecter reject) {
    // 模拟获取用户信息的异步操作
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSDictionary *userInfo = @{@"name": @"John Doe", @"age": @30};
        fulfill(userInfo);
    });
}];

PMKPromise *fetchFriendListPromise = [PMKPromise new:^(PMKFulfiller fulfill, PMKRejecter reject) {
    // 模拟获取好友列表的异步操作
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSArray *friendList = @[@"Friend 1", @"Friend 2"];
        fulfill(friendList);
    });
}];

PMKPromise *fetchFavoriteListPromise = [PMKPromise new:^(PMKFulfiller fulfill, PMKRejecter reject) {
    // 模拟获取收藏列表的异步操作
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSArray *favoriteList = @[@"Favorite 1", @"Favorite 2"];
        fulfill(favoriteList);
    });
}];

PMKPromise *whenAllPromise = [PMKPromise when:@[fetchUserInfoPromise, fetchFriendListPromise, fetchFavoriteListPromise]];

[whenAllPromise then:^(NSArray *results) {
    NSDictionary *userInfo = results[0];
    NSArray *friendList = results[1];
    NSArray *favoriteList = results[2];
    // 处理所有结果
    NSLog(@"User Info: %@", userInfo);
    NSLog(@"Friend List: %@", friendList);
    NSLog(@"Favorite List: %@", favoriteList);
} catch:^(NSError *error) {
    // 处理错误
    NSLog(@"Error: %@", error.localizedDescription);
}];

在上述代码中,我们使用 [PMKPromise when:] 方法将三个 Promise 组合在一起。当所有 Promise 都成功完成时,when 方法返回的 Promise 也会成功,并且 then block 会接收到一个包含所有 Promise 结果的数组。

  1. 并发操作中的错误处理 在并发操作中,如果任何一个 Promise 失败,整个 when 操作都会失败,并执行 catch 块。例如:
PMKPromise *promise1 = [PMKPromise new:^(PMKFulfiller fulfill, PMKRejecter reject) {
    // 模拟第一个异步操作
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        fulfill(@"Result of promise1");
    });
}];

PMKPromise *promise2 = [PMKPromise new:^(PMKFulfiller fulfill, PMKRejecter reject) {
    // 模拟第二个异步操作
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSError *error = [NSError errorWithDomain:@"Promise2Error" code:1 userInfo:nil];
        reject(error);
    });
}];

PMKPromise *promise3 = [PMKPromise new:^(PMKFulfiller fulfill, PMKRejecter reject) {
    // 模拟第三个异步操作
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        fulfill(@"Result of promise3");
    });
}];

PMKPromise *whenAllPromise = [PMKPromise when:@[promise1, promise2, promise3]];

[whenAllPromise then:^(NSArray *results) {
    // 这个 block 不会执行,因为 promise2 失败了
    NSLog(@"Results: %@", results);
} catch:^(NSError *error) {
    NSLog(@"Error: %@", error.localizedDescription);
}];

在这个例子中,promise2 故意返回了一个错误,导致 whenAllPromise 失败,catch block 会被执行来处理错误。

六、与其他异步框架的结合使用

  1. NSURLSession 结合 在网络请求中,NSURLSession 是常用的框架。我们可以将 NSURLSession 的异步操作与 PromiseKit 结合,使代码更易读。以下是一个使用 NSURLSession 进行 GET 请求,并使用 PromiseKit 处理结果的示例:
PMKPromise *fetchDataPromise = [PMKPromise new:^(PMKFulfiller fulfill, PMKRejecter reject) {
    NSURL *url = [NSURL URLWithString:@"https://example.com/api/data"];
    NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        if (error) {
            reject(error);
        } else {
            fulfill(data);
        }
    }];
    [task resume];
}];

[fetchDataPromise then:^(NSData *data) {
    // 处理返回的数据
    NSString *responseString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
    NSLog(@"Response: %@", responseString);
} catch:^(NSError *error) {
    // 处理错误
    NSLog(@"Error: %@", error.localizedDescription);
}];

在上述代码中,我们将 NSURLSessionDataTask 的异步操作封装在一个 Promise 中,这样可以更方便地处理请求的成功和失败情况。

  1. Core Data 结合 在数据持久化方面,Core Data 是一个强大的框架。我们可以使用 PromiseKit 来处理 Core Data 中的异步操作,例如保存数据。以下是一个简单的示例:
#import <CoreData/CoreData.h>

PMKPromise *saveDataPromise = [PMKPromise new:^(PMKFulfiller fulfill, PMKRejecter reject) {
    NSManagedObjectContext *context = [self managedObjectContext];
    MyEntity *entity = [NSEntityDescription insertNewObjectForEntityForName:@"MyEntity" inManagedObjectContext:context];
    entity.name = @"Example Name";
    if ([context save:nil]) {
        fulfill(nil);
    } else {
        NSError *error = [NSError errorWithDomain:@"CoreDataSaveError" code:1 userInfo:nil];
        reject(error);
    }
}];

[saveDataPromise then:^{
    NSLog(@"Data saved successfully");
} catch:^(NSError *error) {
    NSLog(@"Error saving data: %@", error.localizedDescription);
}];

在这个例子中,我们将 Core Data 的保存操作封装在一个 Promise 中,通过 PromiseKit 可以更优雅地处理保存操作的结果。

七、高级特性

  1. finally PromiseKit 提供了 finally 块,无论 Promise 是成功还是失败,finally 块中的代码都会被执行。这在一些需要清理资源或者执行通用操作的场景中非常有用。例如:
PMKPromise *examplePromise = [PMKPromise new:^(PMKFulfiller fulfill, PMKRejecter reject) {
    // 模拟异步操作
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSError *error = [NSError errorWithDomain:@"ExampleError" code:1 userInfo:nil];
        reject(error);
    });
}];

[examplePromise then:^(id value) {
    NSLog(@"Success: %@", value);
} catch:^(NSError *error) {
    NSLog(@"Error: %@", error.localizedDescription);
} finally:^{
    NSLog(@"This will always be printed");
}];

在上述代码中,无论 examplePromise 是成功还是失败,finally 块中的 NSLog 语句都会被执行。

  1. map 方法 map 方法用于对 Promise 的结果进行转换。它接受一个 block,这个 block 会接收到 Promise 的结果,并返回一个新的值,这个新的值会成为 map 方法返回的新 Promise 的结果。例如:
PMKPromise *originalPromise = [PMKPromise new:^(PMKFulfiller fulfill, PMKRejecter reject) {
    // 模拟异步操作,返回一个整数
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        fulfill(@10);
    });
}];

PMKPromise *mappedPromise = [originalPromise map:^(NSNumber *number) {
    // 将整数乘以 2
    NSNumber *newNumber = @(number.integerValue * 2);
    return newNumber;
}];

[mappedPromise then:^(NSNumber *result) {
    NSLog(@"Mapped Result: %@", result);
} catch:^(NSError *error) {
    NSLog(@"Error: %@", error.localizedDescription);
}];

在这个例子中,originalPromise 返回一个整数 10,通过 map 方法将其乘以 2mappedPromise 的结果就是 20

  1. flatMap 方法 flatMap 方法与 map 方法类似,但它接受的 block 需要返回一个 Promise。flatMap 方法会等待这个返回的 Promise 完成,并将其结果作为 flatMap 方法返回的新 Promise 的结果。例如:
PMKPromise *firstPromise = [PMKPromise new:^(PMKFulfiller fulfill, PMKRejecter reject) {
    // 模拟第一个异步操作,返回一个字符串
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        fulfill(@"Hello");
    });
}];

PMKPromise *secondPromise = [firstPromise flatMap:^(NSString *string) {
    return [PMKPromise new:^(PMKFulfiller fulfill, PMKRejecter reject) {
        // 模拟第二个异步操作,将字符串拼接并返回
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSString *newString = [string stringByAppendingString:@" World"];
            fulfill(newString);
        });
    };
}];

[secondPromise then:^(NSString *result) {
    NSLog(@"Final Result: %@", result);
} catch:^(NSError *error) {
    NSLog(@"Error: %@", error.localizedDescription);
}];

在上述代码中,firstPromise 返回一个字符串 HelloflatMap 中的 block 返回了一个新的 Promise,这个 Promise 完成后返回 Hello World,作为 secondPromise 的结果。

八、性能考虑

  1. 避免不必要的 Promise 创建 虽然 PromiseKit 提供了很方便的异步编程方式,但在一些简单的异步场景中,如果频繁创建 Promise,可能会带来一定的性能开销。例如,在一些短时间内需要执行多次的简单异步任务中,直接使用 GCD 的 dispatch_after 等方法可能会更高效。因此,在使用 PromiseKit 时,要根据具体的业务场景来判断是否有必要创建 Promise。

  2. 并发操作的资源管理 在使用并发操作(如 when 方法)时,要注意资源的管理。如果并发执行的异步操作过多,可能会导致内存、网络等资源的过度消耗。例如,同时发起大量网络请求可能会使网络拥堵,影响用户体验。因此,在并发执行多个异步操作时,要合理控制并发数量,避免资源过度消耗。

  3. Promise 链式调用的深度 虽然 PromiseKit 的链式调用非常方便,但如果链式调用的深度过深,也可能会影响性能。因为每一个 then 或者 flatMap 等方法都会创建新的 Promise 和相关的 block 等对象。在实际开发中,要尽量避免链式调用过深,如果业务逻辑确实需要复杂的异步操作,可以考虑将部分操作封装成独立的函数或者类,以提高代码的可读性和性能。

九、最佳实践

  1. 保持代码简洁 在使用 PromiseKit 时,要尽量保持代码简洁明了。避免在 thencatch 等 block 中编写过于复杂的逻辑。如果某个 block 中的逻辑过于复杂,可以将其封装成独立的函数,这样不仅可以提高代码的可读性,也方便调试和维护。

  2. 统一错误处理 在项目中,要建立统一的错误处理机制。在 Promise 的 catch 块中,可以根据不同的错误类型进行统一的处理,例如显示不同的错误提示给用户。这样可以提高用户体验,并且使错误处理逻辑更加清晰。

  3. 文档化异步操作 对于复杂的异步操作,要做好文档化工作。在代码中添加注释,说明每个 Promise 的作用、输入和输出,以及整个异步流程。这样可以方便其他开发人员理解和维护代码。

  4. 测试异步代码 由于 PromiseKit 主要用于异步编程,因此要重视异步代码的测试。可以使用 XCTest 等测试框架来编写测试用例,验证 Promise 的正确执行和错误处理。在测试异步代码时,要注意处理异步操作的完成和超时等情况。

十、总结

PromiseKit 为 Objective-C 开发者提供了一种强大而优雅的异步编程模式。通过使用 PromiseKit,我们可以避免传统回调函数带来的 “回调地狱”,使异步代码更加易读、易维护。无论是顺序执行异步操作、并发执行异步操作,还是与其他异步框架结合使用,PromiseKit 都提供了丰富的功能和灵活的方式。在实际开发中,合理使用 PromiseKit,并遵循最佳实践,可以提高开发效率,提升代码质量,为用户带来更好的体验。同时,也要注意性能方面的考虑,避免因不当使用 PromiseKit 而带来的性能问题。希望通过本文的介绍,读者能够对 Objective-C 中的 PromiseKit 异步编程模式有更深入的理解和掌握,并在实际项目中灵活运用。