个性化阅读
专注于IT技术分析

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

点击下载

本文概述

对于iOS体系结构模式, “模型-视图-控制器(MVC)”设计模式非常适合应用程序代码库的持久性和可维护性。通过相互分离, 可以轻松重用或替换类以支持各种需求。这有助于最大化面向对象编程(OOP)的优势。

尽管此iOS应用程序体系结构在微观级别(应用程序的各个屏幕/部分)上运作良好, 但随着应用程序的增长, 你可能会发现自己向多个模型添加了相似的功能。在诸如网络之类的情况下, 将公共逻辑移出模型类并移入单例助手类可能是一种更好的方法。在此AFNetworking iOS教程中, 我将教你如何设置与微型MVC组件分离的集中式单例网络对象, 该对象可在整个分离的架构应用程序中重复使用。

AFNetworking教程:使用单例进行集中式和解耦式网络

iOS联网问题

苹果在简化易于使用的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 + Blocks和MBProgressHUD(同样可以轻松地与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];

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

测试我们的网络单例的appID:1, HOST:http://www.apitesting.dev/和PORT:80

好的, 因此你可能不会像这样同时混合#define, 静态const和@property, 而只是为了清楚地显示你的选择。 ” static const”是用于类型安全的更好的声明, 但是#define在字符串构建中很有用, 因为它允许使用宏。为了简单起见, 在这种情况下, 为了简洁起见, 我使用#define。除非你使用指针, 否则这两种声明方法在实践上没有太大区别。

现在你已经了解了#define, 常量, 属性和方法, 我们可以将其删除, 然后继续进行更相关的示例。

网络示例

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

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

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

我们将令牌检查添加到NetworkManager并通过完成块获取有关检查的反馈。你可以根据需要设计这些完成模块。在此示例中, 我将成功与响应对象数据一起使用, 并将失败与错误响应字符串和状态代码一起使用。注意:如果对接收方来说无关紧要, 例如增加分析中的值, 则可以选择不包括失败。

网络管理器

在@interface上方:

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

在@interface中:

@property(非原子的, 强的)AFHTTPSessionManager * networkingManager;

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

NetworkManager.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网络令牌):

- (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;

NetworkManager.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教程

我们将在这里停止, 但是, 正如你所看到的, 我们已经将常见的联网参数和方法集中在一个Singleton Manager中, 这大大简化了我们的视图控制器实现。未来的更新将变得简单而快捷, 最重要的是, 它将使我们的网络与用户体验脱钩。下次设计团队要求对UI / UX进行大修时, 我们将知道我们的工作已经在网络端完成!

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

  • 处理用户状态和权限
  • 将触摸操作路由到应用程序导航
  • 视音频管理
  • 分析工具
  • 通知事项
  • 周边设备
  • 还有更多……

我们还专注于iOS应用程序体系结构, 但这可以轻松扩展到Android甚至JavaScript。另外, 通过创建高度定义和面向函数的代码, 它使将应用程序移植到新平台上的任务变得更快。

综上所述, 通过在早期项目规划中花费一些额外的时间来建立关键的单例方法(如上面的网络示例), 你的未来代码可以变得更简洁, 更简单, 更可维护。

赞(0)
未经允许不得转载:srcmini » iOS集中和解耦的网络:一个单例类的AFNetworking教程

评论 抢沙发

评论前必须登录!