IOS应用架构思考一(网络层)

简介:

最近看到Casa Taloyum同学的关于IOS架构的文章,分享的概念和观点很值得一看,于是不禁心痒,也做些分享吧,我会从实际设计过程中需要思考的问题的角度着手来讲述,毕竟无论什么样的架构,什么样的设计都是要解决这些问题的。

今天就先讲讲网络层的需要思考的问题吧。

1.requestOperation的设计

我们都知道在客户端发送请求是需要成本的,那么设计异步的请求就是首要的问题。我们知道Cocoa提供了非常丰富和易于使用的异步api, 有NSOperationQueue, dispatch queue, NSThread等。那么如何选择呢,答案毫无疑问的必须是NSOperation。 不信你去看ASIHTTPRequest和AFNetworking. 好像这个理由不够充分是吧,而且很多人就是使用了这些框架,而不清楚它们到底为何优秀,那我就列举下一些它们的优点吧

首先举个反例吧:曾经有人问过我“为什么要用AFNetworking和ASI这样的框架呢,我直接dispatch到后台用类似dataWithURL:拉数据这样不是很简单?”。 那么这样的使用案例有2个很致命的缺陷:第一个就是无法cancel这个请求,第二个是占用了完整的一个线程。

可以cancel

请求可以cancel的需求的重要性不用说了吧,别说你没用过。使用NSOperation可以让你方便的设计cancel一个请求的方法。

线程

说道线程可能很多人对发送请求的时候线程有个很大的误解: 每个请求占用一个线程。其实不是的,无论同时并发多少个请求,AFNetworking和ASI都是只有一个线程在等待的。不信请看AFNetworking的实现:



+ (void)networkRequestThreadEntryPoint:(id __unused)object {
    @autoreleasepool {
        [[NSThread currentThread] setName:@"AFNetworking"];

        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
}

+ (NSThread *)networkRequestThread {
    static NSThread *_networkRequestThread = nil;
    static dispatch_once_t oncePredicate;
    dispatch_once(&oncePredicate, ^{
        _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
        [_networkRequestThread start];
    });

    return _networkRequestThread;
}

- (void)start {
    [self.lock lock];
    if ([self isReady]) {
        self.state = AFOperationExecutingState;

        [self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
    }
    [self.lock unlock];
}

 

从上面的代码中我们可以看到,AFNetworking在等待请求时其实只有个一个Thread, 然后在这个Thread上启动一个runLoop监听 NSURLConnectionNSMachPort 类型源。 start里面直接就跳到这个线程去执行了, 在加入NSOperationQueue时,顶多start方法执行的时候占用一个线程,然后真正的发送请求和等待都是在这个networkRequestThread里面进行的。

补充一下,上面的描述可能引起误会,已经有人问我这个疑问了。注意, 上面讲的networkRequestThread 并不是最终访问网络并拉取数据的线程。真正的下载数据是NSURLConnection统一调度的,networkRequestThread 只是启用了一个runLoop来监听NSURLConnection的回调事件。

了解上面两点后我们再看前面的例子,就会发现问题有多致命了。

如果同时并发了很多个请求,那就是实实在在的占用了n个线程了,而且请求不能cancel,对于这些被占用掉得大量资源就束手无策了。

并发数量限制

如果用NSOperationQueue来说明,就是maxConcurrentOperationCount, 最大并发数量。

可能有人指出了, 刚才不是讲请求都是在一个线程等待的吗,那并发数量岂不是没有太大意义? NO, 并发数量还是有重要的意义的,它的主要意义就是控制连接数。> 这里多谢 CasaTaloyum 同学的勘误,我原本不知道2G, 3G, 等网络协议的限制。

2G网络下一次只能维持一个链接,3G是2个,4G和wifi是不限。这个是对应协议的限制。如果超过这个限制发出的请求,就会报超时。

除掉这个连接数的影响外,还有次要的影响就是带宽,下载数据是要流量的,如果同时并发了太多请求,每个连接都占用带宽,可能导致每个请求的时间均会延长,就好像你在迅雷同时下载10个文件,那么下载速度都慢了。不过考虑到现在的网速,这个场景一般都比较极限,在做一些特殊场景优化的时候或许可以考虑到。

当然maxConcurrentOperationCount也不能设置太小,太小了的话,如果个别的请求太慢,导致后面的任务就启动不起来了。

并发数量的考虑应该从上面两个点出发,因此如果有人再扯到线程上去,只能说走远了。

pause & dependency

可暂停的,可添加依赖的,这些可能不常会用到,但是如果要设计网络层框架还是要考虑的,这也是要用NSOperation的原因之一。

本节内容说出了开源框架在operation的设计上的一些优点,因此也推荐这部分直接使用AFNetworking之类的框架,因为优秀的开源组件总有其优点,他们考虑了很多你甚至都没有意识到的问题,以前有是遇到过一些同学质疑AFNetworking,质疑arc, 质疑SDWebImage, 质疑Masonry等,当然不是说没有缺点,但是这些东西当你真正深入了解学习后我觉得才能做出正确的选择。

2. 安全性

很多IOS客户端开发者不注重安全性,当然也因为IOS系统已经做得很不错的原因,还有安全的主要工作是在后端,比如使用https啊,比如加签啊,那么前段的网络框架设计要注意哪些安全性的问题呢。

数据加密和防篡改

如无特殊情况,数据加密就推荐用https就够了,好处就不一一列举了,现在各大互联网公司(如BAT)都在做“全站https”. 而且现在免费的SSL证书也非常好申请,几乎不要什么成本了。

要保证接口参数不被篡改,不会被第三方恶意攻击,就要对参数进行签名。记得以前在某国内知名电商,由于接口没有类似防护,一两个小时被人利用移动端的注册接口注册了80w+账号,惨不忍睹。 而且签名密钥(其实不是密钥,就是个干扰串)不要写死,要动态下发。

https中间人

https是非常安全的协议,可以参考我以前的博客RSA加密数字证书里面。但是可能还是会存在中间人攻击的问题,那么就需要打ssl钢钉,AFNetworking中的AFURLConnectionOperationSSLPinningMode就是可以设置ssl钢钉的,原理就是把证书或者公钥 打包到bundle中,发送请求的时候会与请求过来的证书比较,因此避免中间人发放的伪造证书可能。

DNS防劫持

进一步的安全策略就可以考虑到DNS防劫持,原理比较简单,可以本地维护一个路由表,然后本地实现 NSURLProtocol 对host进行ip映射。如果你还不太了解NSURLProtocol,可以参考NSURLProtocol进行学习

3. 组件设计

无论你是用AFNetworking之类的开源组件还是自己封装的方式,一般我们都会封装下中间组件,一来可以降低业务代码和底层组件的耦合,二来方便拓展。那么设计这些组件的时候需要注意哪些呢?

回调方式

这部分选择不少,主要是能统一和方便,可能会考虑下面2个问题 1、是选择block回调方式,还是delegate, 还是target-action, 可以根据自己的喜好来设置,也可以兼容多种。 2、success和fail分开回调还是同一个方法回调。

拦截器

request的部分可能会有些场景需要请求前和请求后的拦截器的设计, 即AOP入口,在这样的拦截器接口中,我们就可以做一些事情如: 1、session过期后自动登录 2、需要登录的接口进入登录

统一cancel还是单独cancel

在一个页面中发送请求的时候,无疑会碰到一个问题,那就是“页面退出时请求要取消”。

我们最开始是如何做的呢,就像内存管理,谁启动,谁cancel, 即是单独cancel. 使用方式类似下面:


@interface AViewController: UIViewController {
  HTTPRequest *_request;
}
@end
@implementation AViewController
- (void)dealloc {
  [_request cancel];
}
- (void)sendRequest {
  [_request cancel];
  _request = [[HTTPRequest alloc] init];
  [HttpClient sendRequest:_request];
}
@end

然后就觉得每个地方都要在dealloc里面写cancel,很麻烦,于是可能想要做个统一的cancel,类似封装个方法在父类里面调用:


@implementation BaseViewController
- (void)dealloc {
  [HttpClient cancelRequestForDelegate:self];
}
@end

这样的话好像就更方便了,代码量也少了一些,调用的类也不用去管cancel了, 但是这样真的好么,虽然可能让你暂时方便了,但是这样的设计还是不紧凑,其实这个请求是否应该被取消应该是调用者的逻辑,这样就相当于一部分的逻辑放在父类里面,是有点坑的,第一种写法虽然可能麻烦写,但是就像内存管理,谁创建,谁负责销毁,这样的代码可读性和维护性更高些。不然如果遇到特殊的逻辑,可能要去动父类或底层。

说到这里就是建议上面代码中的request对象的封装就是Operation本身,这样其实就不需要再在viewController里面写很多isLoading, isLoaded的状态位了啊,NSOperation自带的。

4. 缓存

说道网络请求,就不能不提网络缓存,数据缓存是提升用户体验不可缺少的重要一环。而对于缓存可以讲的太多了,比如图片缓存可以单独开篇文章来讲了,这里就讲一下关于普通接口数据的缓存。一般我们对于缓存的方式有两种做法,一种是客户端写缓存实现和缓存逻辑,一种是遵循HTTP协议的缓存。

4.1 自己实现缓存逻辑

一般需要这种缓存的逻辑无非是考虑下面的需求:

1、接口返回的数据很少变动,不希望做重复请求 2、网络慢或者服务器等异常状况容灾。

自己实现也比较灵活,你可以写个数据库来缓存、也可以直接序列化存文件。但是缺点也比较明显,那就是缓存时间不好定义。

既然做了缓存,那么就会遇到这个问题,就是数据刷新了之后,缓存不更新怎么办。那么其实在你选择这种缓存实现之前就需要做权衡,是数据实时性重要,还是数据加载速度重要。想好这个问题才能确定是否要这样的缓存。

4.2 HTTP缓存

这种缓存方案是比较推荐的方案,HTTP协议已经定义了好了一套缓存方案,为什么不用呢。 而且苹果已经帮我们实现好了缓存的代码,NSURLCache,数据缓存在本地sqlite里。不过就是要和服务端同学一块推进。

可以缓存的接口,可以在返回的 responseHeaders 中添加 cache-control 和 Expires 来告诉前端是否可以缓存和缓存时间等。

而客户端可以通过设置 cachePolicy 来决定是否使用缓存


typedef NS_ENUM(NSUInteger, NSURLRequestCachePolicy)
{
    NSURLRequestUseProtocolCachePolicy = 0,

    NSURLRequestReloadIgnoringLocalCacheData = 1,
    NSURLRequestReloadIgnoringLocalAndRemoteCacheData = 4, // Unimplemented
    NSURLRequestReloadIgnoringCacheData = NSURLRequestReloadIgnoringLocalCacheData,

    NSURLRequestReturnCacheDataElseLoad = 2,
    NSURLRequestReturnCacheDataDontLoad = 3,

    NSURLRequestReloadRevalidatingCacheData = 5, // Unimplemented
};

13
typedef NS_ENUM(NSUInteger, NSURLRequestCachePolicy)
{
    NSURLRequestUseProtocolCachePolicy = 0,

    NSURLRequestReloadIgnoringLocalCacheData = 1,
    NSURLRequestReloadIgnoringLocalAndRemoteCacheData = 4, // Unimplemented
    NSURLRequestReloadIgnoringCacheData = NSURLRequestReloadIgnoringLocalCacheData,

    NSURLRequestReturnCacheDataElseLoad = 2,
    NSURLRequestReturnCacheDataDontLoad = 3,

    NSURLRequestReloadRevalidatingCacheData = 5, // Unimplemented
};

考虑一种场景,你不能确定数据什么时候更新,可能随时刷新,也可能几天才更新,那么我们如何使用缓存呢。

可以使用 ETag/If-None-Match 或者 Last-Modified/If-Modified-Since

两种方式原理差不多,就是在发送请求的 requestHeaders 中加入 If-None-Match 或者 If-Modified-Since 字段,和服务端数据进行比较是否有更新,有更新则返回body,没有的话就在responseHeaders中告知使用缓存。 两者中, ETag是比较hash, Last-Modified是比较最后更改时间。

ASIHTTPRequest实现了这种模式,如下:



if ([self cachePolicy] & (ASIAskServerIfModifiedWhenStaleCachePolicy|ASIAskServerIfModifiedCachePolicy)) {

              NSDictionary *cachedHeaders = [[self downloadCache] cachedResponseHeadersForURL:[self url]];
              if (cachedHeaders) {
                  NSString *etag = [cachedHeaders objectForKey:@"Etag"];
                  if (etag) {
                      [[self requestHeaders] setObject:etag forKey:@"If-None-Match"];
                  }
                  NSString *lastModified = [cachedHeaders objectForKey:@"Last-Modified"];
                  if (lastModified) {
                      [[self requestHeaders] setObject:lastModified forKey:@"If-Modified-Since"];
                  }
              }
          }

 

5. 服务器推

对于很多需要精细化体验的app来说,HTTP协议的模式已经不能够满足需求了, 比如支付宝可以随时随地的推活动。那么就需要服务器推的技术了。

5.1 SPDY 或 HTTP/2

spdy 和 HTTP2 协议都定义了关于服务端推送的协议,还有一些其他的相对于http的优良特性. 一些大公司应该都做了对于spdy的支持,ios中可以使用twitter开源的CocoaSPDY。 不过Google刚刚宣布了不再支持SPDY,以后都走HTTP/2了。

关于这些我还没有详细的学习,这里就不多讲了,以后多多学习。

5.2 长连接

另一种比较灵活和强大的方法服务器推送的方案就是TCP长连接了,长连接可以让你做很多事情,唯一的难题就是要处理好心跳包。

6. 其他

6.1 国际化

在做国际化的时候,需要向服务器提供用户的语言偏好,也就是需要设置 Accept-Language 字段,如下:



[request setValue:[NSString stringWithFormat:@"%@", [[NSLocale preferredLanguages] componentsJoinedByString:@", "]], forHTTPHeaderField:@"Accept-Language"];

 

即使你的服务器还不支持国际化,把这个属性放进去会让你在需要的时候用到,而不必更新客户端。AFNetworking已经实现了改逻辑。

小结

就写到这里吧,后面想到了再补充,欢迎讨论和指正。


目录
相关文章
|
20小时前
|
机器学习/深度学习 人工智能 监控
【AI 场景】如何应用人工智能来增强企业网络的网络安全?
【5月更文挑战第4天】【AI 场景】如何应用人工智能来增强企业网络的网络安全?
|
3天前
|
Cloud Native 安全 持续交付
构建未来:云原生架构在现代企业中的应用与挑战
【5月更文挑战第1天】 随着数字化转型的深入,云原生技术以其灵活性、可扩展性和敏捷性成为现代企业IT架构的核心。本文将探讨云原生架构的关键组件,包括容器化、微服务、持续集成/持续部署(CI/CD)以及DevOps实践,并分析它们如何共同塑造企业的运营模式。同时,文章还将讨论在采纳云原生过程中企业可能遇到的挑战,如安全性问题、技术复杂性以及组织文化的转变,并提出应对策略。
19 8
|
4天前
|
机器学习/深度学习 自动驾驶 安全
基于深度学习的图像识别技术在自动驾驶系统中的应用网络安全与信息安全:防御前线的关键技术与意识
【4月更文挑战第30天】随着人工智能技术的飞速发展,深度学习已成为推动多个技术领域革新的核心力量。特别是在图像识别领域,深度学习模型已展现出超越传统算法的性能。在自动驾驶系统中,准确的图像识别是确保行车安全和高效导航的基础。本文将探讨深度学习在自动驾驶中图像识别的应用,分析关键技术挑战,并提出未来的发展方向。
|
4天前
|
机器学习/深度学习 PyTorch TensorFlow
【Python机器学习专栏】卷积神经网络(CNN)的原理与应用
【4月更文挑战第30天】本文介绍了卷积神经网络(CNN)的基本原理和结构组成,包括卷积层、激活函数、池化层和全连接层。CNN在图像识别等领域表现出色,其层次结构能逐步提取特征。在Python中,可利用TensorFlow或PyTorch构建CNN模型,示例代码展示了使用TensorFlow Keras API创建简单CNN的过程。CNN作为强大深度学习模型,未来仍有广阔发展空间。
|
4天前
|
前端开发 JavaScript 安全
【TypeScript技术专栏】TypeScript在微前端架构中的应用
【4月更文挑战第30天】微前端架构通过拆分应用提升开发效率和降低维护成本,TypeScript作为静态类型语言,以其类型安全、代码智能提示和重构支持强化这一架构。在实践中,TypeScript定义公共接口确保跨微前端通信一致性,用于编写微前端以保证代码质量,且能无缝集成到构建流程中。在微前端架构中,TypeScript是保障正确性和可维护性的有力工具。
|
4天前
|
安全 网络安全 Android开发
云端防御策略:融合云服务与网络安全的未来构建高效的Android应用:从内存优化到电池寿命
【4月更文挑战第30天】 随着企业加速向云计算环境转移,数据和服务的云端托管成为常态。本文探讨了在动态且复杂的云服务场景下,如何构建和实施有效的网络安全措施来保障信息资产的安全。我们将分析云计算中存在的安全挑战,并展示通过多层次、多维度的安全框架来提升整体防护能力的方法。重点关注包括数据加密、身份认证、访问控制以及威胁检测与响应等关键技术的实践应用,旨在为读者提供一种结合最新技术进展的网络安全策略视角。 【4月更文挑战第30天】 在竞争激烈的移动市场中,Android应用的性能和资源管理已成为区分优秀与平庸的关键因素。本文深入探讨了提升Android应用效率的多个方面,包括内存优化策略、电池
|
4天前
|
存储 Swift iOS开发
使用Swift开发一个简单的iOS应用的详细步骤。
使用Swift开发iOS应用的步骤包括:创建Xcode项目,设计界面(Storyboard或代码),定义数据模型,实现业务逻辑,连接界面和逻辑,处理数据存储(如Core Data),添加网络请求(必要时),调试与测试,根据测试结果优化改进,最后提交至App Store或其它平台发布。
13 0
|
4天前
|
安全 Swift iOS开发
【Swift 开发专栏】Swift 与 UIKit:构建 iOS 应用界面
【4月更文挑战第30天】本文探讨了Swift和UIKit在构建iOS应用界面的关键技术和实践方法。Swift的简洁语法、类型安全和高效编程模型,加上与UIKit的紧密集成,使开发者能便捷地创建用户界面。UIKit提供视图、控制器、布局、动画和事件处理等功能,支持灵活的界面设计。实践中,遵循设计原则,合理组织视图层次,运用布局和动画,以及实现响应式设计,能提升界面质量和用户体验。文章通过登录、列表和详情界面的实际案例展示了Swift与UIKit的结合应用。
|
4天前
|
存储 安全 Swift
【Swift 开发专栏】使用 Swift 开发一个简单的 iOS 应用
【4月更文挑战第30天】本文介绍了使用 Swift 开发简单 iOS 待办事项应用的步骤。首先,阐述了 iOS 开发的吸引力及 Swift 语言的优势。接着,详细说明了应用的需求和设计,包括添加、查看和删除待办事项的功能。开发步骤包括创建项目、界面搭建、数据存储、功能实现,并提供了相关代码示例。最后,强调了实际开发中需注意的细节和优化,旨在帮助初学者掌握 Swift 和 iOS 开发基础。
|
5天前
|
安全 网络协议 算法
【计算机网络】http协议的原理与应用,https是如何保证安全传输的
【计算机网络】http协议的原理与应用,https是如何保证安全传输的