iOS 集中和解耦网络:单例类的 AFNetworking 教程

已发表: 2022-03-11

对于 iOS 架构模式,模型-视图-控制器 (MVC) 设计模式非常适合应用程序代码库的寿命和可维护性。 它允许类通过相互解耦来轻松重用或替换以支持各种需求。 这有助于最大限度地发挥面向对象编程 (OOP) 的优势。

虽然这个 iOS 应用程序架构在微观层面(应用程序的单个屏幕/部分)运行良好,但随着应用程序的增长,您可能会发现自己向多个模型添加了类似的功能。 在网络等情况下,将通用逻辑从模型类中移出并放入单例辅助类中可能是一种更好的方法。 在这个 AFNetworking iOS 教程中,我将教你如何设置一个集中的单例网络对象,它与微级别的 MVC 组件解耦,可以在整个解耦架构应用程序中重用。

AFNetworking 教程:使用 Singleton 的集中和解耦网络

iOS网络的问题

Apple 在易于使用的 iOS SDK 中抽象出管理移动硬件的许多复杂性方面做得很好,但在某些情况下,例如网络、蓝牙、OpenGL 和多媒体处理,类可能会很麻烦,因为它们的目标是保持SDK 灵活。 值得庆幸的是,丰富的 iOS 开发人员社区创建了高级框架来简化最常见的用例,从而简化应用程序的设计和结构。 一个优秀的程序员,采用 ios 应用程序架构最佳实践,知道要使用哪些工具,为什么要使用它们,以及何时最好从头开始编写自己的工具和类。

AFNetworking 是一个很好的网络示例,也是最常用的开源框架之一,它简化了开发人员的日常任务。 它简化了 RESTful API 网络,并创建了带有成功、进度和失败完成块的模块化请求/响应模式。 这消除了对开发人员实现的委托方法和自定义请求/连接设置的需求,并且可以非常快速地包含在任何类中。

AFNetworking 的问题

AFNetworking 很棒,但它的模块化也可能导致它以分散的方式使用。 常见的低效实现可能包括:

  • 在单个视图控制器中使用类似方法和属性的多个网络请求

  • 多个视图控制器中几乎相同的请求导致分布式公共变量可能不同步

  • 一个类中对与该类无关的数据的网络请求

对于视图数量有限、要实现的 API 调用很少以及不太可能经常更改的应用程序,这可能不是什么大问题。 但是,您更有可能想得远大,并计划了多年的更新。 如果您的情况是后者,您可能最终需要处理:

  • 支持多代应用程序的 API 版本控制

  • 随着时间的推移添加新参数或更改现有参数以扩展功能

  • 全新 API 的实现

如果您的网络代码分散在您的代码库中,那么这现在是一个潜在的噩梦。 希望您至少在一个公共标头中静态定义了一些参数,但即便如此,即使是最细微的更改,您也可能会触及十几个类。

我们如何解决 AFNetworking 限制?

创建一个网络单例以集中处理请求、响应及其参数。

单例对象提供对其类资源的全局访问点。 单例用于需要这种单点控制的情况,例如提供一些通用服务或资源的类。 您可以通过工厂方法从单例类中获取全局实例。 - 苹果

因此,单例是一个类,在应用程序的整个生命周期中,您只会在应用程序中拥有一个实例。 此外,因为我们知道只有一个实例,所以任何其他需要访问其方法或属性的类都可以轻松访问它。

这就是我们应该使用单例网络的原因:

  • 它是静态初始化的,因此一旦创建,它将具有相同的方法和属性,任何尝试访问它的类都可以使用它。 不会出现奇怪的同步问题或从错误的类实例请求数据。

  • 您可以将 API 调用限制在速率限制内(例如,当您必须将 API 请求保持在每秒 5 个以下时)。

  • 主机名、端口号、端点、API 版本、设备类型、持久 ID、屏幕大小等静态属性可以放在一起,因此一项更改会影响所有网络请求。

  • 公共属性可以在许多网络请求之间重用。

  • 单例对象在实例化之前不会占用内存。 这对于具有某些用户可能永远不需要的非常特定用例的单身人士很有用,例如在他们没有设备的情况下将视频投射到 Chromecast。

  • 网络请求可以与视图和控制器完全解耦,因此即使在视图和控制器被销毁后它们也可以继续。

  • 网络日志可以集中和简化。

  • 常见的失败事件(例如警报)可以重新用于所有请求。

  • 这种单例的主要结构可以通过简单的顶级静态属性更改在多个项目中重用。

不使用单例的一些原因:

  • 它们可以被过度使用以在单个类中提供多种职责。 例如,视频处理方法可以与网络方法或用户状态方法混合。 这可能是一种糟糕的设计实践,并导致难以理解的代码。 相反,应该创建多个具有特定职责的单例。

  • 单例不能被子类化。

  • 单例可以隐藏依赖关系,从而变得不那么模块化。 例如,如果一个单例被删除并且一个类缺少单例导入的导入,它可能会导致未来的问题(特别是如果有外部库依赖项)。

  • 一个类可以在另一个类中意外的长时间操作期间修改单例中的共享属性。 如果没有适当的考虑,结果可能会有所不同。

  • 单例中的内存泄漏可能成为一个重要问题,因为单例本身永远不会被释放。

但是,使用 iOS 应用架构最佳实践,这些负面因素可以得到缓解。 一些最佳实践包括:

  • 每个单例应该处理一个单一的职责。

  • 如果您需要高精度,请不要使用单例来存储将被多个类或线程快速更改的数据。

  • 构建单例以根据可用依赖项启用/禁用功能。

  • 不要在单例属性中存储大量数据,因为它们会在应用程序的整个生命周期中持续存在(除非手动管理)。

使用 AFNetworking 的简单单例示例

首先,作为先决条件,将 AFNetworking 添加到您的项目中。 最简单的方法是通过 Cocoapods,说明可以在其 GitHub 页面上找到。

当您使用它时,我建议添加UIAlertController+BlocksMBProgressHUD (再次轻松地使用 CocoaPods 添加)。 这些显然是可选的,但是如果您希望在 AppDelegate 窗口上的单例中实现它们,这将大大简化进度和警报。

添加AFNetworking ,首先创建一个名为NetworkManager的新 Cocoa Touch 类,作为NSObject的子类。 添加访问管理器的类方法。 您的NetworkManager.h文件应类似于以下代码:

 #import <Foundation/Foundation.h> #import “AFNetworking.h” @interface NetworkManager : NSObject + (id)sharedManager; @end

接下来,实现单例的基本初始化方法并导入 AFNetworking 标头。 您的类实现应如下所示(注意:假设您使用的是自动引用计数):

 #import "NetworkManager.h" @interface NetworkManager() @end @implementation NetworkManager #pragma mark - #pragma mark Constructors static NetworkManager *sharedManager = nil; + (NetworkManager*)sharedManager { static dispatch_once_t once; dispatch_once(&once, ^ { sharedManager = [[NetworkManager alloc] init]; }); return sharedManager; } - (id)init { if ((self = [super init])) { } return self; } @end

伟大的! 现在我们正在烹饪并准备添加属性和方法。 作为了解如何访问单例的快速测试,让我们将以下内容添加到NetworkManager.h

 @property NSString *appID; - (void)test;

以及NetworkManager.m的以下内容:

 #define HOST @”http://www.apitesting.dev/” static const in port = 80; … @implementation NetworkManager … //Set an initial property to init: - (id)init { if ((self = [super init])) { self.appID = @”1”; } return self; } - (void)test { NSLog(@”Testing out the networking singleton for appID: %@, HOST: %@, and PORT: %d”, self.appID, HOST, port); }

然后在我们的主ViewController.m文件(或任何你有的文件)中,导入NetworkManager.h ,然后在viewDidLoad中添加:

 [[NetworkManager sharedManager] test];

启动应用程序,您应该在输出中看到以下内容:

Testing our the networking singleton for appID: 1, HOST: http://www.apitesting.dev/, and PORT: 80

好的,所以您可能不会像这样同时混合#define 、 static const 和@property ,而只是为了清楚起见而显示您的选项。 “static const” 是一种更好的类型安全声明,但#define在字符串构建中很有用,因为它允许使用宏。 对于它的价值,我在这种情况下使用#define来简洁。 除非您使用指针,否则这些声明方法之间在实践中没有太大区别。

现在您已经了解了#defines 、常量、属性和方法,我们可以删除这些并转到更相关的示例。

网络示例

想象一个应用程序,用户必须登录才能访问任何内容。 在应用程序启动时,我们将检查是否保存了身份验证令牌,如果是,则对我们的 API 执行 GET 请求以查看令牌是否已过期。

AppDelegate.m中,让我们为我们的令牌注册一个默认值:

 + (void)initialize { NSDictionary *defaults = [NSDictionary dictionaryWithObjectsAndKeys:@"", @"token", nil]; [[NSUserDefaults standardUserDefaults] registerDefaults:defaults]; }

我们将向 NetworkManager 添加令牌检查,并通过完成块获得有关检查的反馈。 您可以随心所欲地设计这些完成块。 在此示例中,我将成功与响应对象数据一起使用,并将失败与错误响应字符串和状态代码一起使用。 注意:如果失败对接收方无关紧要,例如在分析中增加值,则可以选择忽略失败。

网络管理器.h

@interface上方:

 typedef void (^NetworkManagerSuccess)(id responseObject); typedef void (^NetworkManagerFailure)(NSString *failureReason, NSInteger statusCode);

在@界面中:

@property (nonatomic, strong) AFHTTPSessionManager *networkingManager;

 - (void)tokenCheckWithSuccess:(NetworkManagerSuccess)success failure:(NetworkManagerFailure)failure;

网络管理器.m:

定义我们的 BASE_URL:

 #define ENABLE_SSL 1 #define HOST @"http://www.apitesting.dev/" #define PROTOCOL (ENABLE_SSL ? @"https://" : @"http://") #define PORT @"80" #define BASE_URL [NSString stringWithFormat:@"%@%@:%@", PROTOCOL, HOST, PORT]

我们将添加一些帮助方法来简化经过身份验证的请求以及解析错误(此示例使用 JSON Web 令牌):

 - (AFHTTPSessionManager*)getNetworkingManagerWithToken:(NSString*)token { if (self.networkingManager == nil) { self.networkingManager = [[AFHTTPSessionManager alloc] initWithBaseURL:[NSURL URLWithString:BASE_URL]]; if (token != nil && [token length] > 0) { NSString *headerToken = [NSString stringWithFormat:@"%@ %@", @"JWT", token]; [self.networkingManager.requestSerializer setValue:headerToken forHTTPHeaderField:@"Authorization"]; // Example - [networkingManager.requestSerializer setValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; } self.networkingManager.requestSerializer = [AFJSONRequestSerializer serializer]; self.networkingManager.responseSerializer.acceptableContentTypes = [self.networkingManager.responseSerializer.acceptableContentTypes setByAddingObjectsFromArray:@[@"text/html", @"application/json", @"text/json"]]; self.networkingManager.securityPolicy = [self getSecurityPolicy]; } return self.networkingManager; } - (id)getSecurityPolicy { return [AFSecurityPolicy defaultPolicy]; /* Example - AFSecurityPolicy *policy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeNone]; [policy setAllowInvalidCertificates:YES]; [policy setValidatesDomainName:NO]; return policy; */ } - (NSString*)getError:(NSError*)error { if (error != nil) { NSData *errorData = error.userInfo[AFNetworkingOperationFailingURLResponseDataErrorKey]; NSDictionary *responseObject = [NSJSONSerialization JSONObjectWithData: errorData options:kNilOptions error:nil]; if (responseObject != nil && [responseObject isKindOfClass:[NSDictionary class]] && [responseObject objectForKey:@"message"] != nil && [[responseObject objectForKey:@"message"] length] > 0) { return [responseObject objectForKey:@"message"]; } } return @"Server Error. Please try again later"; }

如果你添加了 MBProgressHUD,它可以在这里使用:

 #import "MBProgressHUD.h" @interface NetworkManager() @property (nonatomic, strong) MBProgressHUD *progressHUD; @end … - (void)showProgressHUD { [self hideProgressHUD]; self.progressHUD = [MBProgressHUD showHUDAddedTo:[[UIApplication sharedApplication] delegate].window animated:YES]; [self.progressHUD removeFromSuperViewOnHide]; self.progressHUD.bezelView.color = [UIColor colorWithWhite:0.0 alpha:1.0]; self.progressHUD.contentColor = [UIColor whiteColor]; } - (void)hideProgressHUD { if (self.progressHUD != nil) { [self.progressHUD hideAnimated:YES]; [self.progressHUD removeFromSuperview]; self.progressHUD = nil; } }

还有我们的令牌检查请求:

 - (void)tokenCheckWithSuccess:(NetworkManagerSuccess)success failure:(NetworkManagerFailure)failure { NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; NSString *token = [defaults objectForKey:@"token"]; if (token == nil || [token length] == 0) { if (failure != nil) { failure(@"Invalid Token", -1); } return; } [self showProgressHUD]; NSMutableDictionary *params = [NSMutableDictionary dictionary]; [[self getNetworkingManagerWithToken:token] GET:@"/checktoken" parameters:params progress:nil success:^(NSURLSessionTask *task, id responseObject) { [self hideProgressHUD]; if (success != nil) { success(responseObject); } } failure:^(NSURLSessionTask *operation, NSError *error) { [self hideProgressHUD]; NSString *errorMessage = [self getError:error]; if (failure != nil) { failure(errorMessage, ((NSHTTPURLResponse*)operation.response).statusCode); } }]; }

现在,在 ViewController.m viewWillAppear 方法中,我们将调用这个单例方法。 请注意请求的简单性和 View Controller 端的微小实现。

 [[NetworkManager sharedManager] tokenCheckWithSuccess:^(id responseObject) { // Allow User Access and load content //[self loadContent]; } failure:^(NSString *failureReason, NSInteger statusCode) { // Logout user if logged in and deny access and show login view //[self showLoginView]; }];

而已! 请注意,此代码段实际上是如何在任何需要在启动时检查身份验证的应用程序中使用的。

同样,我们可以处理登录的 POST 请求:NetworkManager.h:

 - (void)authenticateWithEmail:(NSString*)email password:(NSString*)password success:(NetworkManagerSuccess)success failure:(NetworkManagerFailure)failure;

网络管理器.m:

 - (void)authenticateWithEmail:(NSString*)email password:(NSString*)password success:(NetworkManagerSuccess)success failure:(NetworkManagerFailure)failure { if (email != nil && [email length] > 0 && password != nil && [password length] > 0) { [self showProgressHUD]; NSMutableDictionary *params = [NSMutableDictionary dictionary]; [params setObject:email forKey:@"email"]; [params setObject:password forKey:@"password"]; [[self getNetworkingManagerWithToken:nil] POST:@"/authenticate" parameters:params progress:nil success:^(NSURLSessionTask *task, id responseObject) { [self hideProgressHUD]; if (success != nil) { success(responseObject); } } failure:^(NSURLSessionTask *operation, NSError *error) { [self hideProgressHUD]; NSString *errorMessage = [self getError:error]; if (failure != nil) { failure(errorMessage, ((NSHTTPURLResponse*)operation.response).statusCode); } }]; } else { if (failure != nil) { failure(@"Email and Password Required", -1); } } }

我们可以在这里花哨并在 AppDelegate 窗口上使用 AlertController+Blocks 添加警报,或者简单地将失败对象发送回视图控制器。 此外,我们可以在这里保存用户凭据,或者让视图控制器处理它。 通常,我实现一个单独的 UserManager 单例来处理可以直接与 NetworkManager 通信的凭据和权限(个人偏好)。

再一次,视图控制器端非常简单:

 - (void)loginUser { NSString *email = @"[email protected]"; NSString *password = @"SomeSillyEasyPassword555"; [[NetworkManager sharedManager] authenticateWithEmail:email password:password success:^(id responseObject) { // Save User Credentials and show content } failure:^(NSString *failureReason, NSInteger statusCode) { // Explain to user why authentication failed }]; }

哎呀! 我们忘记对 API 进行版本化并发送设备类型。 此外,我们已将端点从“/checktoken”更新为“/token”。 由于我们集中了我们的网络,这非常容易更新。 我们不需要深入研究我们的代码。 由于我们将在所有请求上使用这些参数,因此我们将创建一个帮助程序。

 #define API_VERSION @"1.0" #define DEVICE_TYPE UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad ? @"tablet" : @"phone" - (NSMutableDictionary*)getBaseParams { NSMutableDictionary *baseParams = [NSMutableDictionary dictionary]; [baseParams setObject:@"version" forKey:API_VERSION]; [baseParams setObject:@"device_type" forKey:DEVICE_TYPE]; return baseParams; }

将来可以轻松地将任何数量的通用参数添加到其中。 然后我们可以更新我们的令牌检查和身份验证方法,如下所示:

 … NSMutableDictionary *params = [self getBaseParams]; [[self getNetworkingManagerWithToken:token] GET:@"/checktoken" parameters:params progress:nil success:^(NSURLSessionTask *task, id responseObject) { … … NSMutableDictionary *params = [self getBaseParams]; [params setObject:email forKey:@"email"]; [params setObject:password forKey:@"password"]; [[self getNetworkingManagerWithToken:nil] POST:@"/authenticate" parameters:params progress:nil success:^(NSURLSessionTask *task, id responseObject) {

结束我们的 AFNetworking 教程

我们将在此停止,但正如您所见,我们在单例管理器中集中了常见的网络参数和方法,这极大地简化了我们的视图控制器实现。 未来的更新将简单快速,最重要的是,它将我们的网络与用户体验分离。 下次设计团队要求对 UI/UX 进行大修时,我们会知道我们的工作已经在网络方面完成了!

在本文中,我们专注于网络单例,但这些相同的原则可以应用于许多其他集中式功能,例如:

  • 处理用户状态和权限
  • 将触摸操作路由到应用导航
  • 视频和音频管理
  • 分析
  • 通知
  • 外围设备
  • 还有更多……

我们还专注于 iOS 应用程序架构,但这可以很容易地扩展到 Android 甚至 JavaScript。 作为奖励,通过创建高度定义和面向函数的代码,它使将应用程序移植到新平台成为一项更快的任务。

总而言之,通过在早期项目规划中花费一些额外的时间来建立关键的单例方法,就像上面的网络示例一样,您未来的代码可以更干净、更简单、更易于维护。