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

2018-09-19 17:45:48 浏览数 (1)

前言

无奈这次推来的还是ios上的文章,继续权且当做开拓视野吧。老规矩,原文如下:

iOS Centralized and Decoupled Networking: AFNetworking Tutorial with a Singleton Class By JAMES CAHALL

文章正文

当涉及iOS架构模式时,模型 - 视图 - 控制器(MVC)设计模式对于应用程序的代码库的长寿和可维护性是非常有用的。通过将它们解耦从而使类可以很容易地被重用或替换来支持各种需求。这有助于最大化面向对象编程(OOP)的优势。

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

iOS网络的问题

果在轻松地使用iOS sdk管理移动硬件方面做了很多复杂的工作,但在某些情况下,如联网、蓝牙、OpenGL和多媒体处理等,由于它们的目标是保持sdk的灵活性,这些类可能会很麻烦。幸运的是,丰富的iOS开发者社区已经创建了高级框架,以简化最常见的用例,以简化应用程序的设计和结构。一个好的程序员,使用ios应用程序架构最佳实践,知道使用哪些工具,为什么要使用它们,以及何时更好地从头开始编写自己的工具和类。

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

AFNetworking的问题

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

  • 多个网络请求在一个视图控制器。
  • 在多个视图控制器中几乎相同的请求导致分布式公共变量可能会失去同步。
  • 在类中对与该类无关的数据进行网络请求。

对于视图数量有限的应用程序,实现的API调用很少,而且不太可能发生变化的应用程序,这可能不是很大的问题。然而,更有可能的是你正在思考大的问题,并且有许多年的更新计划。如果你的情况是后者,你很可能需要处理:

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

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

完全新api的实现

如果您的网络代码分散在您的代码库中,那么这将是一个潜在的噩梦。希望您至少有一些参数在公共头部中静态定义,但即使是最微小的变化,您也可能会接触到好多的类。

我们如何处理AFNetworking限制?

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

单例对象为其类的资源提供了一个全局访问点。单例在这种单点控制的情况下被使用,比如提供一些通用服务或资源的类。您可以通过工厂方法从单例类获得全局实例。– Apple

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

这就是为什么我们应该为网络使用一个单例:

  • 它是静态初始化的,一旦创建,它将具有相同的方法和属性可用于任何尝试访问它的类。不可能出现奇怪的同步问题或从错误的类实例请求数据。
  • 您可以将您的API调用限制在一个限制范围之内(例如,当您必须将API请求保持在每秒五个以下时)。
  • 诸如主机名,端口号,端点,API版本,设备类型,持久ID,屏幕尺寸等的静态属性可以位于同一位置,这样一个变化影响所有网络请求。
  • 公共属性可以在许多网络请求之间重用。
  • 单例对象在实例化之前不会占用内存。对于一些用户可能永远都不需要的特殊用例来说,这一点非常有用,比如视频投射到Chromecast上,如果他们没有设备,就可以不用考虑该功能。
  • 网络请求可以与视图和控制器完全分离,因此即使在视图和控制器被销毁后,它们也可以继续。
  • 网络日志记录可以集中和简化。
  • 诸如警报的常见故障事件可以重新用于所有请求。
  • 这种单例的主要结构可以在具有简单顶级静态属性变化的多个项目中重用。

一些不使用单例的理由:

  • 它们可能被过度使用,在单例类中提供多个职责。例如,视频处理方法可能混合使用网络方法或用户状态方法。这可能是一种糟糕的设计实践,并导致难以理解的代码。相反,应该创建具有特定职责的多个单例。
  • 单例对象不能被派生子类。
  • 单例可以隐藏依赖关系,因此变得不那么模块化。例如,如果一个单例被删除,并且一个类丢失了单例 imported的导入,那么它可能会导致将来的问题(特别是如果存在外部库依赖关系)。
  • 一个类可以在长操作中修改单例中的共享属性,这在另一个类中是不可预料的。如果没有适当的考虑,结果可能会有所不同。
  • 单例对象中的内存泄漏可能会成为一个重要问题,因为单例对象本身永远不会被释放。

然而,使用iOS应用程序架构最佳做法,可以减轻这些负面影响。一些最佳做法包括:

  • 每个单例对象都应该承担单一的责任。
  • 不要使用单例来存储数据,如果您需要很高的精度,这些数据将被多个类或线程快速地更改。
  • 基于可用的依赖项构建单例的启用/禁用特性。
  • 不要将大量数据存储在单例属性中,因为它们将在您的应用程序的生命周期中持续存在(除非手动管理)。

基于AFNetworking的简单单例示例

首先,作为先决条件,将AFNetworking添加到您的项目中。最简单的方法是通过Cocoapods,并在其 GitHub page寻找使用说明。

当您如此做时,我建议添加UIAlertController BlocksMBProgressHUD(同样使用CocoaPods可以轻松完成)。这些显然是可选的,但这将极大地简化进度和警报,如果您希望在AppDelegate窗口中的单例模式中实现它们。

一旦AFNetworking添加,首先创建一个新的Cocoa Touch类,名为NetworkManagerNSObject的子类。添加一个用于访问管理器的类方法。您的NetworkManager.h文件应如下所示:

代码语言:javascript复制
#import <Foundation/Foundation.h>
#import “AFNetworking.h”

@interface NetworkManager : NSObject

  (id)sharedManager;

@end

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

代码语言:javascript复制
#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

代码语言:javascript复制
@property NSString *appID;

- (void)test;

以下到NetworkManager.m

代码语言:javascript复制
#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添加:

代码语言:javascript复制
[[NetworkManager sharedManager] test];

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

代码语言:javascript复制
Testing our the networking singleton for appID: 1, HOST: http://www.apitesting.dev/, and PORT: 80

好吧,你可能不会把 #define、静态const和@property都像这样混在一起,只是为了清晰地显示你的选择。“静态const”是一种更好的类型安全声明,但是 #define 在字符串构建中是有用的,因为它允许使用宏。为了它的价值,我在这个场景中使用了 #define 。除非您使用指针,否则这些声明方法之间并没有太多的实际差异。

网络示例

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

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

代码语言:javascript复制
  (void)initialize {
    NSDictionary *defaults = [NSDictionary dictionaryWithObjectsAndKeys:@"", @"token", nil];
    [[NSUserDefaults standardUserDefaults] registerDefaults:defaults];
}

我们将向NetworkManager添加令牌检查,并通过完成块获取有关检查的反馈。您可以按照您喜欢的方式设计这些完成块。在本例中,我使用了响应对象数据和错误响应字符串和状态代码的失败。注意:如果对接收方无关紧要,如分析中增加值,则可能会选择性地删除失败。

NetworkManager.h

Above @interface:

代码语言:javascript复制
typedef void (^NetworkManagerSuccess)(id responseObject);
typedef void (^NetworkManagerFailure)(NSString *failureReason, NSInteger statusCode);

In @interface:

@property (nonatomic, strong) AFHTTPSessionManager *networkingManager;

代码语言:javascript复制
- (void)tokenCheckWithSuccess:(NetworkManagerSuccess)success failure:(NetworkManagerFailure)failure;

NetworkManager.m:

定义我们的 BASE_URL:

代码语言:javascript复制
#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令牌):

代码语言:javascript复制
- (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,可以在这里使用:

代码语言:javascript复制
#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;
    }
}

和我们的令牌检查请求:

代码语言:javascript复制
- (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方面的微小实现。

代码语言:javascript复制
  [[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];
    }];

就是这样!请注意,在任何需要在启动时检查身份验证的应用程序中,这个代码片段几乎都可以使用。

类似地,我们可以处理一个登录请求:NetworkManager.h:

代码语言:javascript复制
- (void)authenticateWithEmail:(NSString*)email password:(NSString*)password success:(NetworkManagerSuccess)success failure:(NetworkManagerFailure)failure;

NetworkManager.m:

代码语言:javascript复制
- (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的alerts ,或者简单地将故障对象发送回视图控制器。此外,我们可以在这里保存用户凭据,或者让视图控制器处理。通常,我实现一个独立的UserManager单例,处理可直接与NetworkManager通信的凭据和许可(个人偏好)。

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

代码语言:javascript复制
- (void)loginUser {
    NSString *email = @"test@apitesting.dev";
    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”。由于我们集中我们的网络,这是非常容易更新。我们不需要挖掘我们的代码。由于我们将对所有请求使用这些参数,我们将创建一个helper。

代码语言:javascript复制
#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;
}

将来可以轻松添加任何数量的常见参数。

然后我们可以更新我们的令牌检查和验证方法,如下所示:

代码语言:javascript复制
…
    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/用户体验检查时,我们会知道我们的工作已经在网络上完成了!

在本文中,我们将重点放在一个网络单例上,但是这些原则同样适用于许多其他集中的功能,例如:

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

我们还专注于iOS应用程序架构,但这也可以很容易扩展到Android甚至JavaScript。作为一个额外的好处,通过创建高度定义和面向功能的代码,它使移植应用程序到新平台的速度更快。

总而言之,通过在早期的项目计划中花费一点额外的时间来建立关键的单例方法,比如上面的网络例子,您的未来代码可以更简洁、更简单、更易于维护。

了解基础知识

什么是AFNetworking?

一个面向iOS和macOS的开放源码网络库,它可以使用RESTful网络API简化开发人员的任务,并通过 success, progress,和ailure completion blocks创建模块化的请求/响应模式。它有一个非常活跃的开发者社区,并且在一些最好的应用中使用。

什么是单例对象?

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

0 人点赞