我的iOS开发之旅

你若安好,便是晴天☀️

阅读《程序员的自我修养》学习笔记

第一部分 简介

计算机硬件组成

其中最关键的三个部件为:cpu(中央处理器) 内存 硬盘IO控制芯片

总线的作用

协调高速和低速缓存,因为IO设备(键盘、鼠标等)比cpu、内存的速度要慢很多,因此,为了协调IO设备与总线之间的速度,保证cpu和 IO设备之间的正常高效通信。

单核、多核处理器与多线程

  单核即单个cpu执行(通过不断的上下文切换实现并发多线程执行),多核即多个cpu同时执行(实现真正的并发多线程执行)
  线程的访问权限:一个线程由线程ID、当前指令指针、寄存器集合、栈组成,是自己独有的,各个线程之间可以共享程序内存空间(包括代码段、数据段、堆等)
  线程调度:由于在单核处理器下的多线程实际上是通过线程调度实现的,所谓线程调度即不断在单核处理器上切换不同线程的行为
  线程调度方式:轮转法(各个 线程轮流执行一小段时间,带来的问题是有些UI线程会被卡住)、优先级调度(决定线程按照什么顺序轮流执行。存在的问题是对某些优先级低的线程会出现饿死的现象,在它执行之前,总有高优先级的线程需要执行)

OC运行时及消息转发

类与对象的关系结构图

前面两个图合起来即表示后面这个图,理解图中实例、类、元类以及isa指针、superclass指针的原理非常重要,后面的消息发送及转发过程就需要基于这些图来更好的理解

Git 学习笔记

Git 原理

版本控制系统

版本控制系统目前有三大类,分别是本地版本控制系统、集中式版本控制系统、分布式版本控制系统,Git则是一个非常强大的分布式版本控制系统 Alt text

Git分布式版本控制实现

(1)首先从git远程仓库克隆下来的是整个仓库文件的完整拷贝
(2)在本地修改文件,进行提交之前会保存修改文件的完整快照,而不是只保存差异变化或者文件补丁,对于当前版本没有修改的文件,会保存指向上一个版本该文件的指针(即未变化的文件只会保存上一个版本的指针),这样随着版本越来越多,git所占用的空间不会越来越大。

NSURL Loading System

1、概述

NSURL Loading System主要用来描述使用标准internet协议与server端交互,以及与URLs进行交互的基础框架类。它是一组类和协议的集合,允许你的app去获取URL指定的内容,核心类即NSURL,通过它使我们的app可以操作URLs以及URLs指向的资源。

除了NSURL,这个基础框架还提供了包括加载URLs、上传数据到服务器、管理cookie存储、控制返回数据的缓存、处理证书存储和身份验证、编写自定义的协议扩展等一套丰富的功能集合。

NSURL Loading System主要支持通过以下协议来获取相应资源,包括文件传输协议、超文本传输协议、超文本加密传输协议、本地文件url传输协议、Data URLs传输协议(ftp:// http:// https:// file:/// data://),同时支持代理服务器和socks网关。

除了NSURL Loading System,iOS还支持其他API可以在其他应用如safari中打开网页等,即通过在UIApplication中使用openURL:打开相关URL,具体可参考UIApplication Class Reference.

NSURL Loading System包含了一系列helper类来协助URL loading类完成加载过程和相关行为,主要可以分为以下5大类,即协议支持、身份验证与证书、cookie存储、配置管理、缓存管理,如下图链接

https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/URLLoadingSystem/Art/nsobject_hierarchy_2x.png

2、URL Loading

URL Loading是在NSURL Loading System中通过url获取相关内容使用最普遍的一个类,你可以通过很多种方式获取相关内容,这依赖于你app的需求、app系统版本,以及你是否希望以文件的形式获取数据,还是以内存的形式获取。

在iOS7及之后的版本,推荐使用NSURLSession API来执行URL请求,如果相应应用必须支持旧的版本,你可以通过NSURLConnectionx下载数据到内存,然后根据需要看是否将数据写入磁盘,或者使用NSURLDownload下载数据到磁盘文件,你选择的方法很大程度上依赖于你希望获取数据到内存还是磁盘文件。

如何使用NSURLSession

概览:NSURLSession及其相关类主要提供了通过http实现下载功能的API。提供了包括支持请求认证、后台下载(app在没有运行状态、暂停状态下的一些)等功能。这些功能都通 过其代理方法获取。

理解NSURLSession的基本概念

一个session中tasks的行为依赖于三件事情:session的类型(依赖于创建时配置对象的类型),task的类型,当task创建的时候app是否在前台。

1.session的类型

Default sessions(默认):与其它下载url的基础方法非常相似,使用了持久的基于磁盘的缓存,并且将认证信息存储到用户的keychain里面。

Ephemeral sessions(短暂):它不存储任何数据到磁盘,所有的缓存、认证信息等都存储在RAM 随机存取存储器中与session进行关联,当app的session失效时,它们会自动清除。

Background sessions(后台):与默认session非常相似,除了它需要使用一个单独的进程来处理所有的数据传输,并且它在某些方面存在着一些限制。

2.task的类型

data tasks:用于发送和接受NSData类型的对象,适用于与服务端的短暂频繁交互的客户端请求。返回的数据可以通过一个comletion handler block一定时间内的部分或者所有的数据。

download tasks:以file文件的形式获取数据,并且支持app没有运行时的后台下载

upload tasks:以file文件的形式上传数据,并且也支持app没有运行时的后台上传

3.后台数据传输的注意事项

对于Background sessions,由于实际的数据传输是i通过一个进程来执行的,并且重新启动app的过程相对代价也很大,有很多features功能也不可用,因此会存在如下一些限制:

(3-1)session必须提供一个event delivery事件交互的delegate,delegate的行为和当前进程数据传输过程保持一致

(3-2)仅仅支持http和https协议(不支持custom protocols)

(3-3)Redirects are always followed(重定义一直存在?)

(3-4)仅仅支持file文件格式的上传任务(上传data类型或stream类型的对象在程序退出时会失败)

(3-5)如果后台数据传输是在程序处于后台的时候初始化的,configuration配置对象的discretionary属性会设置为true。

注意:在iOS 8 and OS X 10.10之前,data tasks 不支持Background sessions。

对于iOS 和 OS X,在app重新启动时,处理方式也有细微差别。

在iOS中,当一个后台数据传输任务完成或者需要鉴权,如果你的app不在运行状态,iOS会在后台自动重启你的app,并且调用当前app的UIApplicationDelegate 对象的application:handleEventsForBackgroundURLSession:completionHandler: 方法,这个调用方法提供了引起当前app启动的session的 identifier 标识符信息,当前你的app应该存储completion handler信息,并且用同样的identifier创建一个background configuration object,并且用这个configuration object创建一个session,新的session就会自动与正在进行的后台任务建立关联。接下来,当session完成最后一个下载任务,它会给session代理发送一个URLSessionDidFinishEventsForBackgroundURLSession:消息,在这个代理方法中,切换到主线程上调用之前存储的completion handler,让你的操作系统知道 重新suspend 挂起你的app也是安全的。 同样的在iOS 和 OS X中,当用户重启你的app,对于上一次运行时的 outstanding tasks,你的app应该马上创建针对这个tasks的相关session,并且保证创建的background configuration object的identifier与相应session是一一对应的。这些新创建的session同样会自动与正在进行的后台任务建立关联,并且新下载文件的url会与此进行关联,

注意:你必须准备的创建每一个identifier对应的session,多个session共享同一个identifier的行为是undefined不支持的。

当你的app处于suspended挂起状态,有task 任务执行完成,则会调用task的URLSession:downloadTask:didFinishDownloadingToURL:代理方法。 同样的,如果task需要鉴权等认证过程,NSURLSession对象会调用它的URLSession:task:didReceiveChallenge:completionHandler: 代理方法,或者是URLSession:didReceiveChallenge:completionHandler:代理方法。 在出现网络错误的情况下,background sessions中的上传和下载任务,会由URL加载系统自动重试,不需要使用可达性api来确定何时重试失败的任务

对于使用NSURLSession的后台传输任务的例子,可以参考Simple Background Transfer.

生命周期及与代理的交互

充分理解session的生命循环,对于处理NSURLSession相关的任务是非常有帮助,包括session怎样与它的代理交互,代理方法调用的顺序,如果server端返回一个重定向会发生什么,当你的app恢复一个错误的下载会发生什么,等等,关于NSURLSession生命周期的完整描述,参考Life Cycle of a URL Session

NSCoping行为

session和task对象都遵循如下的NSCoping协议: 当你的app拷贝了一个session或者task对象,会返回一个同样的对象 当你的app拷贝了一个configuration对象,会返回一个你可以独立修改的新的对象

Reactive Cocoa学习笔记

入门

一、信号RACSignal

 单个信号的创建方式
(1)RACObserve观察成员变量值的信号,前提是该属性必须支持KVO
(2)RAC封装UI控件事件输入值变化传递的信号
(3)通过RACSignal类方法创建的信号
(4)将某个信号赋值给某个成员变量,也就是信号绑定
(5)对信号进行链式和过滤生成一个新的信号

 组合信号

 通过RACSignal的类方法combinelatest将上面的单个信号进行组合

  基本概念和用法

  信号的生命周期:RACSignal从创建到在block中发送sendNext sendCompleted.以及-对信号执行delay、等操作-被订阅者订阅subscribeNext subscribeCompleted的过程。类似于一个工厂,该工厂只有在消费者需要某个产品[somesignal subscribernext ]时,工厂才会去生产相应产品,即执行相应代码[subscriber sendnext:somedatat],这样的信号被称为冷信号,还要一种热信号可以参考http://tech.meituan.com/tag/ReactiveCocoa#rd


  创建自定义的RACSignal,实现方式,
  Signal获取到数据后,会调用Subscriber的sendNext, sendComplete, sendError方法来传送数据给Subscriber,Subscriber自然也有方法来获取传过来的数据,如:[signal subscribeNext:error:completed]。可以这样理解,RACSignal需要传入一个实现RACSubscriber协议的类,并且会调用相应协议的方法sendNext...。而这些相应协议方法的实现在subscribeNext...等的block中,相当于subscribeNext...替换sendNext...协议方法的实现,改了一下方法名称,本质是在subscribeNext...封装的方法中对协议方法实现进行了一些处理。具体可参考后面的分析。

  注意这个方法带有三个参数这样只要没有sendComplete和sendError,新的值就会通过sendNext源源不断地传送过来,举个简单的例子:
   RACSignal *signal = [RACSignal createSignal:^ RACDisposable * (id<RACSubscriber> subscriber) {
NSLog(@"triggered");
[subscriber sendNext:@"foobar"];
[subscriber sendCompleted];
return nil;
}];

[RACObserve(self, username) subscribeNext: ^(NSString *newName){
NSLog(@"newName:%@", newName);
}];

 可以在上面的方法后直接添加error completed参数的block,也可以分开写,如下所示:
 [signal subscribeCompleted:^{
NSLog(@"subscription %u", subscriptions);
 }];

    如果有多个subscriber的订阅者,那么signal就会又一次被触发,控制台里会输出两次triggered。这或许是你想要的,或许不是。如果要避免这种情况的发生,可以使用 replay 方法,它的作用是保证signal只被触发一次,然后把sendNext的value存起来,下次再有新的subscriber时,直接发送缓存的数据。 并且当一个信号存在多个订阅者时,也有可能产生的副作用。譬如,在一个block中修改外面的__block变量的值,会对后面的订阅者获取到的这个值产生影响。

二、相关实例代码

// KVO
[RACObserve(self, username) subscribeNext:^(id x) {
NSLog(@" 成员变量 username 被修改成了:%@", x);}];
// target-action
self.button.rac_command = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
 NSLog(@" 按钮被点击 ");
 return [RACSignal empty];}];
 // Notification
 [[[NSNotificationCenter defaultCenter]rac_addObserverForName:UIKeyboardDidChangeFrameNotificationobject:nil]subscribeNext:^(id x) {
 NSLog(@" 键盘 Frame 改变 ");}];
 // Delegate
 [[self rac_signalForSelector:@selector(viewWillAppear:)] subscribeNext:^(id x) {
 debugLog(@"viewWillAppear 方法被调用 %@", x);}];

注意事项:

(1)RACObserve使用了KVO来监听property的变化,只要username被自己或外部改变,block就会被执行。但不是所有的property都可以被RACObserve,该property必须支持KVO,比如NSURLCache的currentDiskUsage就不能被RACObserve

(2)rac_textSignal是RAC为UITextField添加的category,只要usernameTextField的值有变化,这个值就会被返回(sendNext)。combineLatest需要每个signal至少都有过一次sendNext

 (3)UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"" message:@"Alert" delegate:nil cancelButtonTitle:@"YES" otherButtonTitles:@"NO", nil]; 
 [[alertView rac_buttonClickedSignal] subscribeNext:^(NSNumber *indexNumber) {     
  if ([indexNumber intValue] == 1) {         
     NSLog(@"you touched NO");     
   } else {         
     NSLog(@"you touched YES");     
  } }]; 
  [alertView show];

 (4)有了这些Category,大部分的Delegate都可以使用RAC来做。或许你会想,可不可以subscribe NSMutableArray.rac_sequence.signal,这样每次有新的object或旧的object被移除时都能知道,UITableViewController就可以根据dataSource的变化,来reloadData。但很可惜这样不行,因为RAC是基于KVO的,而NSMutableArray并不会在调用addObject或removeObject时发送通知,所以不可行。不过可以使用NSArray作为UITableView的dataSource,只要dataSource有变动就换成新的Array,这样就可以了。

 (5)说到UITableView,再说一下UITableViewCell,RAC给UITableViewCell提供了一个方法:rac_prepareForReuseSignal,它的作用是当Cell即将要被重用时,告诉Cell。想象Cell上有多个button,Cell在初始化时给每个button都addTarget:action:forControlEvents,被重用时需要先移除这些target,下面这段代码就可以很方便地解决这个问题:
 [[[self.cancelButton
rac_signalForControlEvents:UIControlEventTouchUpInside]
takeUntil:self.rac_prepareForReuseSignal]
subscribeNext:^(UIButton *x) {
// do other things
}];

三、RACTurple RACStream RACSubject RACScheduler RACSubscriber

  (1)在Objc下,输入输出的“值”都可以用id类型,遇到多个值的组合就用RACTurple
  (2)  RACStream作为一个描述抽象的父类,它定义的几个基本方法自己并没有实现,是由具体子类来实现,RACStream的两个子类分别是RACSignal和RACSequence,可以对应到下面的图进行理解。
  (3)RACSubject(继承自RACSignal,可以理解为自由度更高的signal)。比如一个异步网络操作,可以返回一个subject,然后将这个subject绑定到一个subscriber或另一个信号。
   - (void)doTest
{
RACSubject *subject = [self doRequest];

[subject subscribeNext:^(NSString *value){
    NSLog(@"value:%@", value);
}];
}

  - (RACSubject *)doRequest
 {
RACSubject *subject = [RACSubject subject];
// 模拟2秒后得到请求内容
// 只触发1次
// 尽管subscribeNext什么也没做,但如果没有的话map是不会执行的
// subscribeNext就是定义了一个接收体
[[[[RACSignal interval:2] take:1] map:^id(id _){
    // the value is from url request
    NSString *value = @"content fetched from web";
    [subject sendNext:value];
    return nil;
}] subscribeNext:^(id _){}];
return subject;
 }

 (4)对RACScheduler作用的理解,先 通过下面的文字了解了它的作用,还没有写具体代码
 RACScheduler是RAC里面对线程的简单封装,事件可以在指定的scheduler上分发和执行,不特殊指定的话,事件的分发和执行都在一个默认的后台线程里面做,大多数情况也就不用动了,有一些特殊的signal必须在主线程调用,使用-deliverOn:可以切换调用的线程。
订阅者执行时的block一定非并发执行,也就是说不会执行到一半被另一个线程进入,也意味着写subscribeXXX block的时候没必要做加锁处理            However, RAC guarantees that no two signal events will ever arrive concurrently. While an event is being processed, no other events will be delivered. The senders of any other events will be forced to wait until the current event has been handled.

 (5) RACSubscriber是定义的一个协议,参照博客http://nathanli.cn/2015/08/27/reactivecocoa2-%E6%BA%90%E7%A0%81%E6%B5%85%E6%9E%90/的内容,对它理解的更加深入,自己总结如下:
  RACSubscriber协议定义了sendNext sendError sendCompleted 等方法,首先明白协议使用的两个条件:1 有类实现了协议的方法 2 其他类中定义了协议对象,调用了协议的方法。在源码中的体现就是RACSignal里面(创建方法等方法中)封装了对这个协议方法的调用,在RACSignal的订阅方法subscriber中传入一个实现了协议方法RACSubscriber的对象。即外部要订阅这个信号就必须实现RACSubscriber协议中的方法,实际就是对订阅的值进行处理(在一层协议方法中在嵌套一层协议方法)。  其次源码对实现协议中方法的对象进行了一些封装,将协议方法的实现转化为block。更进一步将前面封装的block实现的类作为RACSignal的一个分类方法,这样对外部隐藏RACSubscriber,直接对订阅值value进行一些处理。

   - (void)nl_subscribeNext:(void (^)(id x))nextBlock error:(void (^)(NSError *error))errorBlock completed:(void (^)(void))completedBlock {
   NLSubscriber *subscriber = [NLSubscriber subscriberWithNext:nextBlock error:errorBlock completed:completedBlock];
  [self subscribe:subscriber];
 }

   前面的分析只是明白了 RACSignal内部给我们封装好了 RACSubscriber这个协议的使用,还应该要明白 RACSignal它最重要的方法 [self subscribe:subscriber];的实现细节。可以在源码里面看看下面这个方法试的实现细节
  @interface RACSignal (Subscription)
  /*
  *  `subscriber` 订阅 receiver 的变化。由 receiver 决定          怎么给 subscriber 发送事件。
  *简单来说,就是由这个被订阅的信号来给订阅者 subscriber 发送 `sendNext:` 等消息。*/
   - (RACDisposable *)subscribe:(id<RACSubscriber>)subscriber;
   @end

   在博客中完全从订阅者RACSubscriber (即先不考虑信号是怎么发送的)以及 设计模式的角度详细描述了它的实现过程,如下
   (1)定义了一个协议RACSubscriber ,申明它的三个方法sendNext sendError sendCompleted。定义一个实现这个协议方法的具体类,如NLSubscriber
   (2)在RACSignal的分类 (Subscription)中,定义一个subscribe方法,需要传入一个实现了RACSubscriber协议的类。如上面方框中代码所示。

    通过上面两步,我们就可以简单的定义一个RACSignal 一个NLSubscriber,并且把NLSubscriber传给RACSignal分类 (Subscription)中的subscribe方法。
   但是这样有一个问题就是,如果有多个订阅者,每个订阅者都要创建一个类,实现RACSubscriber协议以及协议的方法。因为每个订阅者的实现方法有很多种,很难实现复用。对于在某些场景中,譬如某个算法的实现,它的实现方式只可能是固定的几种方式(或者说几种策略,可称为策略模式),我们可以通过这种方式去实现,但是对于多个订阅者的实现方式不能复用的情况,用上面的方法就不是很好。解决办法是使用block,将协议的实现方法用block来代替。

上面的实现类似策略模式,下面类似适配器模式。

  (1)定义一个实现RACSubscriber协议的通用类,在这个通用类中,定义与协议方法实现相关的block,并且作为这个通用类的属性,在通用类的初始化方法中给这些block属性传值,同时在协议方法实现中去执行这些block。
   这样,我们可以通过定义一个RACSignal,一个通过block初始化的通用类,将这个通用类传入RACSignal分类 (Subscription)中的subscribe方法。这样实现通过block的方式去定义i 不同的实现,而不需要去定义多个不同的类去实现了。
   源码中在上面的基础之上,将通用类的初始化,以及传给subscribe方法的过程封装到了RACSignal的一个分类方法中,只需要在参数定义中把对应的block传递过去即可。对于用户来说只需要创建RACSignal对象,调用它的sendNext...等方法,同时调用subscribeNext...等方法完成前面调用方法的具体实现。并且后面有实现的情况下触发前面的调用,与这个相关的实现细节应该在RACSignal源码的具体实现中去分析。

四、注意事项

  (1)RAC 在应用中大量使用了 block,由于 Objective-C 语言的内存管理是基于 引用计数的,为了避免循环引用问题,在 block 中如果要引用 self,需要使用@weakify(self)和@strongify(self)来避免强引用。另外,在使用时应该注意 block 的嵌套层数,不恰当的滥用多层嵌套 block 可能给程序的可维护性带来灾难。
  (2)RAC在使用时有一些注意事项,可以参考官方的DesignGuildLines。
  (3)当一个signal被一个subscriber subscribe后,这个subscriber何时会被移除?答案是当subscriber被sendComplete或sendError时,或者手动调用[disposable dispose]。当subscriber被dispose后,所有该subscriber相关的工作都会被停止或取消,如http请求,资源也会被释放。
  (4)Signal events是线性的,不会出现并发的情况,除非显示地指定Scheduler。所以-subscribeNext:error:completed:里的block不需要锁定或者synchronized等操作,其他的events会依次排队,直到block处理完成。
  (5)Errors有优先权,如果有多个signals被同时监听,只要其中一个signal sendError,那么error就会立刻被传送给subscriber,并导致signals终止执行。相当于Exception。生成Signal时,最好指定Name, -setNameWithFormat: 方便调试。
  (6)block代码中不要阻塞。

http://limboy.me/ http://blog.sunnyxx.com/tags/Reactive%20Cocoa%20Tutorial/

进阶

一、RAC原理

   关于原理性的知识应该在一开始学习的时候就要深刻理解的, 但是刚开始学总会好高骛远,总想着马上就会用了,其实虽然学会了一些简单的用法了,一到比较难的知识点理解起来就比较困难了,就是因为一开始没有理解它的根本原理。下面是经过RAC培训之后自己的总结。

   RAC来源于FRP(函数式响应式编程)思想:随时间变化的数据流在这个思想的基础上还有ReactiveX By Microsoft,它的一些基本操作与RAC也是比较类似的,http://rxmarbles.com/ 

   而RAC是通过订阅者的方式来控制数据流的开始时间点,结束时间点,并且通过一些方式对随时间变化的数据流做一些控制(过滤,整合等)。

   比较常见的一种场景,对网络请求返回数据的订阅,如果是冷信号,会导致网络请求数据的操作执行多次,而如果是热信号,可能会产生一定的副作用或者数据不一致,感觉在这种有网络请求的场景下使用信号要特别注意。如何正确的使用它,防止它不犯错误。

Swift 学习总结

Swift学习之基础部分

   常量和变量,主要掌握根据它们使用的场景,对不会变化的值使用常量,对不断变化的值使用变量。

   注释:单行注释使用//,对于多行注释还可以使用嵌套。

   分号:Swift中可以省略分号,如果在同一行有多条语句,需要用分号隔开。

   整数:Int UInt 可以通过它的最大值、最小值属性获取它表示的范围。

   浮点数:带有小数点的数据类型,Float Double。

   类型安全和类型推断:Swift会在编译的时候对代码进行类型检查,把不匹配的类型标记为错误。如果给变量或常量赋了初始值的时候,可以不申明类型,由系统进行类型推断。由此想到swift中两个基本概念,隐式类型推断和隐式类型可选的异同点),相同点:在使用这些特性的时候都有前提条件,隐式类型推断的前提条件是这个变量被赋予了初始值或者没有赋初始值但是指定了变量的类型。隐式类型可选的前提条件是在某些程序中,某个变量第一次被赋值之后,可以确定一个可选类型总会有值。不同点:隐式类型推断是编译的时候就会检查,隐式解析可选是在运行时的时候才会对尝试在隐式解析可选没有值而进行取值的情况检查。

1、基础数据类型(通常都是通过结构体或枚举类型定义的)

 常量定义 let  i = 10,常量一般必须要赋一个初值

 变量定义 var j : Int ,变量如果指定类型可以不赋初值,如果没有指定类型必须赋初始值

 它们的共同特点就是可以不指定值的类型。如果没有指定类型编译器会根据你赋的值进行类型推断。

 总结:Swift支持隐式类型推断,不支持隐式类型转换(把某种类型的值赋值给另一种类型的值)。

2、swift新增的数据类型

 元组(tuples)
 可选值
 隐式解析可选 

3、字符串数据类型

   首先理解字符串存储由来,我们都知道计算机只能识别0101的数字,那一些非数值类型的字符是如何存储的呢,首先需要制定一些统一的标准,将符号转化为数字,由此出现针对英文字符的ASCII编码,但是全世界除了英文,还要中文,俄文等等其他语言,ASCII编码已经不能满足我们的需求,因此出现一个支持全世界各种语言的国际标准Unicode标准,而swift就是支持Unicode标准的一门语言,并且它的变量名也是支持中文的。

4、运算符

   与其他语言的区别,一个赋值运算符整体构成的表达式不代表一个值,不能进行连续赋值a=b=1,并且赋值运算符也不能出现在if判断语句中了,如果出现就会报错,这样也能避免我们把==写成=,而不报错的情况出现。
   还有另外一个区别,swift新增了一种区间运算符,定义如下:0...5 表示0,1,2,3,4,5。0...<5 表示0,1,2,3,4。有一个需要注意左右区间不能用小数,否则会出现一直循环。

5、集合数据类型

   数组:相同类型的数据在数组中连续排列,因为是有序,元素检索速度非常快(c语言数组定义)。Swift数组的定义与此类似,还是有很多差别。定义如下:var array1 : Array<Int> = [1,2,3,4],因为数组中元素类型定义为范型,我们可以定义为任意一种类型。 并且Swift的数组中可以放置任意类型的数据。通过设置它的类型为Any,如下:var array1 : Array<Any> = [“hello”,2,3,4],至于它为什么能存储不同数据类型,可以从数组的结构体定义以及存储上去找原因。并且Swift允许在结构体中声明方法,更符合面向对象思想。

   数组操作:读数据,通过数组下标的方式,同样也要注意数组越界的问题。修改数据,前提数组是变量,可以通过append、insert等向数组中添加一个元素,或者以直接赋值的方式修改数组中某个元素,以及通过removeLast、removeAtIndex删除数组中的某个元素。

   slice:一种与数组非常相似的集合类型,定义也差不多一样,和数组可以进行相互转换,如:Slice sli = array1[0...5] 通过区间运算符或者使用Slice的结构体方法定义 Slice sli = Slice(array1),不能直接赋值

   range:用来存放区间表达式的值,它的本质是结构体,它也支持泛型(即任意类型,但也必须遵守forwardindex协议),var range : Range<int> = 0...3 或者通过构造方法定义 var range2 : Range<int> = Range(start:0, end:3),需要注意的是这里表示0,1,2,不包括3。而前面的定义包括3.

   数组批量操作,前面讲的slice、range都是为数组的批量操作做准备的。

   (1) 可以将range指定范围内的数替换成任意其他个范围内的数,var array1 = [1,2,3,4]array1[0...1] = [8,7,8,9]。最后得到的array1为:[8,7,8,9,3,4]。或者使用函数array1.replaceRange(Range(start:0, end:2), With:[8,7,8,9]).,实现和前面一样的结构

   (2) 数组还可以通过复合的赋值运算符往数组中添加元素、数组、slice等,array1+=9 等价于 array1.append(9),array1+=[1,2,3],array1+=slice,array1+=array2[0...5]
  数组遍历,第一种for in 快速枚举,第二种通过元组进行遍历
 for elem in array1{
 println(elem)
 }
 for  (index, value) in enumerate(array1){
   println("index:\(index) = \(value)");    //其中\(index)类似于对字符串中格式化占位符的处理
  }

6、字典数据类型

  基本概念,存放无序的数据,因此它的遍历需通过检索数据关键字实现,它类似于数据结构中hash函数和hash表的概念,首先根据key和hash函数计算出一个地址,addr = hash(key),然后从hashtable中根据这个地址获取到key对应的值,value = hashtable(addr)。所以key首先是必须可hash的,在swift中可hash,即需要遵守一个协议,一般的基本数据类型string、int、float等都遵守相应协议,因此,一个字典中它的key值也可以是多样的。 如:var dict = [”1“:a,2:b]
  字典操作,读:println(dict["1"])  写:dict[2] = 8 修改:dict.updatevalue(9,forkey("1"))。需要注意修改操作返回的是一个可选值,如果key存在,会替换key对应的值,并且返回key对应的之前的值,如果key不存在,会向字典中增加一个这个不存在key所对应的值,相当于往字典中添加了一个新的值,并且返回nil。删除:dict.removevalueforkey("1"),同样,如果存在这个key,就删除相应key对应的值,同时返回旧的value值,如果不存在这个key旧不做任何操作,返回nil。字典遍历,即类似元组遍历,同时也可以遍历所有key,或者所有value
 if let orignal = dict.updatevalue(9,forkey("1")){
    println("new:\(dict["1"])");
    println(orignal);
 }
 if let orignal = dict.removevalueforkey("1"){
    println(original)
 }
 for (key,value) in dict{
    println("\(key):\(value)")
  }
  for key in dict.keys{
     print(key)
  }
  for value in dict.values{
     println(value)
  }

7、swift中语句的基本用法

   if语句,以及if中多分支执行语句,我们知道程序不可能只有一种执行顺序,因为实际生活中往往存在多种选择或者循环问题,下面从程序指令执行过程的角度考虑:
    顺序指令:比较好理解,选择指令:有些指令被执行,有些指令不会执行,循环指令:某些指令会循环不断的被执行。将这三种指令相结合,可以解决我们实际生活中的绝大部分问题。
    其中if else、while、for in、break、continue语句的使用与OC语言都非常相似,要特别注意的是switch语句与其他语言的switch语句有一些差别,主要区别为三点,
     (1)最后必须加default 
     (2)可以去掉break了,oc中如果case语句后面不加break,会造成switch贯穿执行,即匹配到某一个case选项后,后面的所以case的语句都会被执行,而在switch中,不需要显示添加break,它匹配到case后,会自动退出。
     (3)case后面可以添加多个匹配条件,如case “a","b" 类似于或,只要有一个满足要求即可。
     (4)因此,switch支持广义匹配,匹配类型可以是整形、区间运算符、元祖、通配符(如_,表示任意一个字符),实例如下。
     (5)switch具有值匹配模式特性,在第三个例子中,case中的let x let y表示:x y可取任意值,一旦有值,会把它绑定到x y中,赋给x y,x y页可以为常量,[let x, let y]与[let(x,y)]两者的写法是等价的。在case匹配语句使用值绑定的情况下,可以在后面加上where 条件,加上值绑定模式的匹配条件。
  let x = 1000 
  swich x{
     case 1...9 :
     println("个位数");
     case 10...49:
     println("十位数")
  default:
     println("不符合")
  }

 let point = (10,10)

  switch point {
   case (0,0) :
   println("坐标原点")
   case (1...10, 1...10) :
   println("x,y坐标位于1-10之间")
   case(_, 0) :   
    println("点在x轴")
  default :
   println("其他")
  }

   swith point {
     case (let x ,10)
     println(x)
     case let(x, y)
     println("\(x):\(y)“)
     case let x, let y
     println("\(x):\(y)“)
    case let (x, y) where x == -y
    println("\(x):\(y)“)

  //返回两个顶点的长和宽
  let po = (double, double) = (0,0)
  let p1 = (double, double) = (8,8)
  fun getlengthandwidth(p0:(double,double),     p1(double,double)) -> (length:double, width:double) {
   return (abs(p0.0-p1.0), abs(p0.1-p1.1))
   }

   let w = getlengthandwidth(p0, p1).width
   let h = getlengthandwidth(p0, p1).length
   //这里要通过名称访问元祖的某个元素,要定义返回值元组中每个元素的名称

8、函数的相关概念

   定义:完成某个特定功能的代码块,该代码块可重复使用。
   语法:func 函数名(参数名:参数类型,参数名:参数类型,...) -> 函数返回值类型 {      //函数实现部分      }     
   调用语法:函数名(实际参数...)   函数名遵循驼峰命名法 ,可以有多个参数,多个返回值,也可以没有参数没有返回值(没有返回值时,可以不写->,或者写-> void)。实例代码如上面第四个例子所示,
   内部参数与外部参数概念:因为函数的定义实现与函数的调用是分开的,因此当我们看到某个函数调用的代码或者想要调用某个函数时,希望通过参数名就可以了解传递参数的含义,而不用去看函数的定义和实现。因此swift提供了一个外部参数,我们在调用函数的时候可以显示对这个外部参数赋值,这样函数的参数信息一目了然。类似于oc中通过方法名帮助我们理解参数含义,

   swift中则通过外部参数帮助我们理解函数传入参数的含义,实例代码如8-1所示,注意:函数实现部分,只能使用内部参数。如果内部参数和外部参数想使用同一个名字,即一个参数即表示内部参数又表示外部参数,可在参数名前面加上#,进行标识

   如果你不设置函数的外部参数名,但是调用函数的时候默认会把内部参数名作为外部参数名,但是第一个参数不会,如下所示
   func test(first:Int, second:Int)
   {
     print("hello");
    }
   test(1, second: 2)

   如果想在调用函数的时候想省略第二个外部参数名,即加上下划线_即可
   如下所示
  func test(first:Int, _ second:Int)
  {
     print("hello");
   }
  test(1, 2)


 func divisionoperation(dividend a:double, divisor b:double) -> double {
  return a/b
 }
 let res = divisionoperation(dividend:3.5, divisor:1.2)
 func joinstring(s1:string, tostring s2:string, joiner s3:string = "#") -> string {
 return s1+s2+s3
}
 let str = joinstring("hello", tostring:"world")
 func swap(a:int, b:int){
   let t = a
   a = b //这里会报错
   b = t
 }


     函数默认值参数概念:
     即对函数中的某个参数指定默认值,相应在调用时可以不给这个默认值参数赋值,有一个需要注意的:当带有缺省值或默认值的这样一个参数,如果只有一个参数名,内部参数名将作为外部参数名使用,因为调用函数,修改其参数默认值,必须使用其外部参数名,否则报错,因此,在函数默认值参数的定义中,如果只定义了内部参数名,没有定义外部参数名,编译器会把默认值参数的内部参数名当作外部参数名使用,这属于编译器做的一个优化,实例代码如8-2所示,注意:对比c++中默认参数必须写在函数参数列表最尾部,不能移到参数列表前面,但是swift中现在是可以的,即可以出现在参数列表的任一位置。

    常量参数与变量参数概念:
    默认函数的参数是常量类型,即只可以读,不可以写或者进行修改操作,如图8-3的使用,编译器会报错,我们可以把它定义为var类型,就可以修改了inout参数的使用:主要用在输入输出函数中,首先看8-4的代码,调用 swap(x,y)之后,最后的输出结果x还是10,y还是28,我们一般会认为x y值应该变化,其实并没有,它实际执行是将10赋值给a,19赋值给b,实际只修改了a b,没有修改x y,在c语言中也同样存在这样的问题,c语言的处理方式是可以 传递x y变量的地址,相应在swap函数中申明a b为指针。但是swift中没有指针,因此引入了一个inout关键字,通过这个关键字修饰参数。改进后的代码如8-5所示:注意:调用swap函数使用&符号不是c语言中取地址的含义,而是将x y变量赋值给函数a b参数。总结:如果我们希望一个函数能修改外部的变量,首先在函数定义时需把参数定义为inout,输入输出参数类型,其次传递实参时需在变量前面加一个&的形式传递过去,等价于a是x的引用,b是y的引用,传递的时候是把x y整个变量都传递过去,而不只是传递变量对应的值10 28,通过&x &y与inout a inout b的配合,最终达到修改函数传递参数的目的。注意:符号&后面不能直接跟一个具体值,需要传一个变量名。

 func swap(var a : int, var b : int){
   let t = a
   a = b
   b =t }

  var x = 10
  var y =  28
  swap(x,y)
  func swap(inout a : int, inout b : int){
   let t = a
   a = b
   b =t }

  var x = 10
  var y =  28
  swap(&x,&y)
  func add (array : [int] -> int {
  var sun = 0
  for i in array {
    sum +=i
   }
  println(add([1,2,3,4,5]))
  for array(array : int...) -> int {
  var sum = 0
  for i in array {
    sum += i
   }
  println(add(2,3,4))
   fun add (a:int, b:int) -> int {
   return a+b
   }
  fun sub (a:int, b:int) -> int {
   return a-b
  }
 变参参数,即参数个数可以是不确定的,要实现变参参数,我们首先想到的可能是通过数组的形式实现,如8-6实例代码所示,在swift中对变参参数进行了定义,语法格式为,参数名:参数类型...  ,只需要修改变量类型,即在参数个数不确定,但是参数类型都是一致的情况下可以这样使用,即把它当作一个数组(集合),并且 在调用时不用传数组,而是像调用函数一样,传递任意个相应类型的参数,如8-7所示,函数实现不变,并且在调用函数时做一些修改即可。注意:变参函数中变参必须放在参数列表最后面。

 函数类型:(参数、返回值类型)类型相同的函数定义如下:函数参数类型以及返回值类型都一样只是实现不一样。如8-8所示,可以将上面的两个函数抽象出公共的函数类型(int, int) -> (int),可以对这个函数类型做如下的一些定义。
 定义一个函数类型常量或变量:var calfunc: (int, int) -> int = add,这样非常类似c语言中函数指针以及oc中的block,申明这样一个变量有一个好处即可以对它重新赋值,calfunc = sub println(calfunc(3,5),此时得到是sub函数计算的结果。因为一个函数类型的变量非常灵活,它可以指向相同类型的其他函数,可以达到c语言中函数指针及oc中block一样的功能。
 函数类型作为一个参数,如8-9所示,将其关联。


 func subcalfunc(a:int, b:int, #op:(int, int) -> int) {
  return op(a, b)
}
println(subcalfunc(20.35,op:sub)
  func max(a: int, b:int) -> int {
  return a>b?a:b
 }
func min(a:int, b:int) -> int {
  return a>b?b:a
}
func choosefunc(#getmax:bool ) -> (int, int) -> (int){
 return getmax?max:min
 }
     var myfunc:(int, int) -> (int) =    choosefunc(getmax:true)
  println(myfunc(2,9))
  func funcname(参数) -> 返回值类型 {
 执行语句
}
{ (参数) -> 返回值类型 in 
  执行语句
}
 let sayhello:() -> void = {
   println("hello word")
  }  //无参闭包,() -> void可以不写
  sayhello()
 let add:(int, int) -> (int) = {
  (a:int , b:int) -> int in 
   return a+b
  }   //有参闭包,
 println(add(3,5))

函数返回一个函数类型的返回值:当根据输入参数判断执行哪一个函数时使用,如8-10所示

9、闭包的基本概念

    闭包:从函数的定义来理解,完成某个特定功能的代码块,在swift中可以理解为闭包是一个更轻量级的函数(自包含的函数代码块),可以把函数分为三大类(1)全局函数(有名函数)。(2)闭包表达式(匿名),能捕获上下文中的常量或者变量,牵涉到内存管理,值捕获。(3)嵌套函数。

    闭包表达式的定义、申明和使用,与函数的区别,闭包没有函数名,但是它还是由参数类型和返回值类型构成,并且参数类型和返回值类型是写在大括号里面的,并且还有注意加一个关键字in,如果没有参数和返回值可以不加in,如9-1所示,上面是函数定义,下面是闭包定义。理解闭包表达式它实际也是有类型的, 在swift中很多都可以称之为一个类型,而类型又可以定义一些常量或变量。如9-2所示。上面的用法只属于非典型性用法,因为这些用法跟函数的使用还没有很多区别。

    闭包表达式的回调用法:即闭包比较典型的用法,下面9-3是一个冒泡排序的实例代码,对它进行改造,将array[j] > array[j+1]的比较通过闭包实现,假设这个比较需要处理比较多的内容,得到9-4所示代码,在9-4的bubbleSort2(&array, intCmp)方法中,还可以把intCmp方法的实现代码直接移过来,不定义为变量。如果要修改比较策略,譬如字符串比较,数字最低位比较,可以只修改闭包函数。类似于block回调,我们可以定义多种block函数的实现,调用bubblesort只需要告诉它一个排序数据以及排序策略。相比于函数来说,闭包可以省略函数名,直接函数实现。通过闭包、block实现多种变化的功能,我们可以传递闭包表达式的多种不同实现,至于闭包表达式何时被调用,是由外层的函数决定的。

func showArray(array:[Int]){
for x in array{
    print("\(x)")
}
print("")
}

  func bubbleSort(inout array:[Int]){
let cnt:Int = array.count
for var i = 1; i < cnt ; i++ {
    for var j = 0; j < cnt - i; j++ {
        if (array[j] > array[j+1]){
            let t = array[j]
            array[j] = array[j+1]
            array[j+1] = t
        }
    }}
 }

 var array = [6,5,9,3,5,2,10]
 showArray(array)
 bubbleSort(&array)
 showArray(array)
 let intCmp = { (a:Int,b:Int) -> Int in
if a > b {
    return -1
} else if a < b {
    return 1
} else{
    return 0
}
}

 func bubbleSort2(inout array:[Int]){
let cnt:Int = array.count
for var i = 1; i < cnt ; i++ {
    for var j = 0; j < cnt - i; j++ {
        if (intCmp(array[j], array[j+1]) == -1){
            let t = array[j]
            array[j] = array[j+1]
            array[j+1] = t
        }
    }
} }
 showArray(array)
 bubbleSort(&array)
 bubbleSort2(&array, intCmp)
 showArray(array)

 bubbleSort2(&array, {
(a,b) in
if a > b {
    return -1
} else if a < b {
    return 1
} else{
    return 0
}
})


 bubbleSort2(&array, {
if $0 > $1 {
    return -1
} else if $0 < $1 {
    return 1
} else{
    return 0
}
 })

 sort($array,{
return $0 < $1
})
 sort($array,{
  $0 < $1
 })

  9-6-1
  func bubbleSortFunc(inout array: [Int]){
let cnt = array.count
func swapValue(inout a:Int, inout b:Int){
    let t = a
    a = b
    b = c
}
....
  }
   9-6-2
  func getIntFunc(inc :Int) -> (Int) -> (Int){
func incFunc(v: Int) -> Int{
    return inc + v
}
return incFunc
}

 9-6-3
 let incFunc1 = getIntFunc(12)
 print(incFunc1(15))
 func getIncFuc(inc : Int) -> (Int) -> (Int){
var mt = 20
func incFunc2(v:Int) -> Int{
    mt++
    return inc + v + mt }
return incFunc2
}

   闭包表达式的语法优化:
   为什么要优化,如何优化,借用swift强大的类型推断功能,实现闭包表达式类型优化,  (1)在函数中传递闭包表达式时省略类型,前提是函数申明中包含了闭包类型时,可以根据上下文推断,省略闭包参数类型及返回值类型。如9-5所示,(2)进一步优化,参数都可以省略,而是使用$0 $1来代替,如9-5下面所示

  尾随闭包:
  由于闭包表达式主要用于函数回调,在函数调用时,为了表达更清楚,书写更方便,应该将闭包作为函数最后一个参数,即尾随闭包。当闭包的执行语句,只有单个return表达式的情况下,可以简写去掉return,如9-5下面的代码所示。

   嵌套函数:
   在函数内部申明一个函数,并且这个内部函数只能在内部使用,即它的作用域是在内部函数左右开始,往下到它所在函数结束的位置,同时函数通过闭包捕获上下文的值也属于嵌套函数。如9-6所示,嵌套函数仅仅为它所在的函数服务,所以有一个优点就是可以让程序更直观更清楚。

   闭包捕获值定义:
   嵌套/内部函数可以引用不是它自己而是它所在外部函数申明的变量或常量,因此我们需要研究它捕获的原则,捕获的行为。在9-6-2的实例代码中,定义了一个嵌套函数,我们知道在函数内部申明的局部变量它的作用域和生命周期只在调用函数的时候有效,函数调用结束就不生效了,但是在上面这种情况下,inc被返回的嵌套函数保留了,我们可以暂时理解为一个副本,这个过程可理解为一个上下文的捕获。从内存管理和函数调用栈的角度考虑,一般认为inc随着函数getIntFunc的调用结束已经没有了,但是嵌套函数中使用了inc局部变量,我们就称之为值捕获,从9-6-3的例子中,会发现每次调用这个函数,返回值都会加1,说明嵌套函数捕获mt后,相当于拷贝了mt的一个副本,会记录mt上一次的值,我们可以理解为mt相当于incFuc的一个全局变量,我猜想如果mt是self,不做保护措施,就会出现循环引用啦。

   泛型:
   可以创建泛型的函数或者类型,之前对泛型的了解只停留在它的使用,对于如何自己去自定义一个泛型没有想过,看了文档上的例子也还不是太理解,后面还要多看看。以
  func swapTwoValues<T>(inout a: T, inout b: T) {} 这个函数为例,通过<T> 占位类型名 同时指定函数参数的类型,在调用的时候直接传入一个任意类型即可。如果需要传入多个占位类型,可以用逗号隔开,进行如下表示 <T,T>。数组是非常典型的泛型函数,它支持任意类型,现在如果自定义一个特定的数组:栈,就需要使用泛型函数了。


   协议和扩展:
   主要使用关键字protocol和extension来进行定义,它的使用和oc语言差不多(在语法定义上有一些差别,并且swift继承某个类和使用协议的语法是一样的在 : 后面,感觉这样不是很好,只是推荐把父类写在前面),可以将协议名作为一种命名类型来使用的做法还不是很理解(现在理解了,可以把协议类型作为函数参数类型、返回值类型等用法,充分发挥协议的优势,可以灵活的创建和传递同一个方法的不同实现),文档上的例子也标注了这样使用的注意事项。为什么swift的协议中要申明属性呢,与以前只申明方法相比,申明属性有什么好处呢?通常理解实现了协议的类型,该类型中必须拥有协议中相应的属性和方法(即协议只关心必须要有某些属性和方法,不关心属性和方法的具体值和具体实现)。注意:在协议中使用class关键字申明的类属性和类方法,当枚举和结构体实现这个协议时,需要使用static关键字。且协议中的方法不支持默认参数值。

  理解扩展的使用场景:在无法修改源代码,或者修改已有类不可行的情况下,需要对已有的类、结构体、枚举、协议等进行扩展。也可以对协议扩展以及补充某个类型为协议类型。


   枚举和机构体:
   枚举与其他语言的一个区别是它可以定义方法,并且它的语法也有了一些变化。文档上说结构体和类的一个最大区别是结构体是传值,类是传引用,这个概念跟我之前的理解好像有偏差, 还要多看看。


   对象和类:
   对类的定义类似c语言有构造器(有继承关系的情况下,会做以下几步,1、设置子类的属性值,2、调用父类的构造器,3、还可以改变父类定义的属性值等等)和析构函数,创建类的时候不需要标准根类,与oc语言的区别。子类重写父类的方法需要用override标记。可以在类中定义属性的getter和setter方法,注意它的语法格式。同时swift中还多了一个willset和didset的方法概念。方法与函数有一个重要区别,方法的参数名(除了第一个参数以外的其他参数名)需要在调用的时候显示说明,最后提到如何处理变量的可选值,看起来感觉有些怪,这个还要多看看。

   函数和闭包;
   可以通过元组让一个函数返回多个值。并且函数参数可以是一个可变参数(numbers: Int...) ,在函数内部使用时类似于数组。函数可以嵌套,可以作为另一个函数的返回值,可以作为另一个函数的参数。

   可以使用{}来创建一个匿名闭包,使用in将参数和返回值类型申明与闭包函数体进行连接。闭包在很多种情景下可以进行一些简化,如单个语句的闭包可以省略参数返回值类型申明以及in关键字,

   可以使用参数位置$0 $1来直接引用参数。当闭包作为某个函数的最后一个参数时,可以直接跟在括号后面。


   控制流:相比于oc,有一些简化,如if后面的条件和for循环变量括号可以省略,但是语句体的大括号是必须的。有一个问题不是太明白,if语句后面必须跟一个布尔表达式,那为什么可以使用
  if let name = optionalName {}这样的类似语法处理值缺失的情况,其中var optionalName: String? = "hello"是一个可选值。 swift中的switch相对于其他语言更加强大,可以支持任意类型的数据以及各种比较操作。并且它可以省略break语句,但是一定不能省略default。
  可以使用for in 加上元组来遍历一个字典。在循环中可以使用1..<n表示范围,但是不包括上界,使用1...n才包括上界

相关参考:

  http://special.csdncms.csdn.net/the-swift-programming-language-in-chinese/index.shtml  中文的

  https://itunes.apple.com/cn/book/the-swift-programming-language/id881256329?mt=11 英文的
  遇到一些坑
  在swift中使用uitableview设置它的样式参数时,总是报找不到我输入的plain样式的参数,找了很多原因,结果发现枚举名要加名称前缀或者是点 .Plain UITableViewStyle.Plain。
  如果报下面这样的错误:execution was interrupted, reason: EXC_BAD_INSTRUCTION (code=EXC_1386_INVOP, subcode=0x0)  先直接在控制台输出的打印信息中找原因,通过看控制台,发现是我调用某个函数获取的是可选值,但是我对这个返回值做申明时没有定义为可选值。
  现在又出现了一个问题是,调用oc方法或者oc第三方库的方法,oc中没有可选值的概念,再swift中调用时,看调用提示默认返回的是一个隐式解析可选,按道理使用隐式解析可选是保证他初始一定会有值的,但是结果oc中没有可选值概念调用的时候却没有值,然后调用相应方法一运行就会报错,这个应该怎么处理。需要再对可选链以及oc方法调用进行研究?

SDWebImage源码学习

一、SDWebImage库的组成

  1、通过UIImageView分类提供的常用方法

  (1)在UIImageView (WebCache)分类提供的方法中,可以看到这些方法最终都调用了下面这个方法,在这个方法中封装了对图片进行缓存和异步下载的功能
  - (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options completed:(SDWebImageCompletionBlock)completedBlock;
     在这个分类中还提供了与展示动画图片相关的API:- (void)sd_setAnimationImagesWithURLs:(NSArray *)arrayOfURLs;,即设置一组图片的url,并且以动画的形式展示。

  (2)UIImageView (HighlightedWebCache):这个分类提供了为UIImageView设置高亮图片时调用的方法,这些方法的实现与前面的分类方法类似。
  (3)UIView (WebCacheOperation) 以字典的形式存储与当前UIView相关的key和SDWebImageOperation,用来添加、移除、取消图片的key对应的operation。
  (4)NSData (ImageContentType) 根据image的NSData判断图片的类型
  (5)UIImage (GIF) 提供gif图片处理,以及图片缩放和裁剪的处理方法
  (6)UIImage (MultiFormat)  将通过NSData的分类方法得到的图片类型,进行不同处理
  (7) UIButton (WebCache)  该分类与UIImageView (WebCache)分类的实现是类似的。

2、下面分析要实现上面分类方法中图片的缓存和异步下载功能要用到的几个核心类,及其功能

   SDWebImageManager:首先判断从内存缓存和硬盘缓存中查找url对应的图片是否存在,如果都不存在从网络异步下载图片,如果存在使用block进行回调。同时,如果是从网络下载图片,会对这个图片在内存和硬盘进行缓存,如果从硬盘缓存获取也会保存到内存。

   SDImageCache:具体实现前面SDWebImageManager中从内存缓存和硬盘缓存查找图片,以及保存缓存图片等功能,使用了NSCache以key-value的形式实现缓存功能,以图片的url作为缓存的key,当内存不足时会清除缓存图片。相对于字典缓存,它有下面几个优势

  SDWebImageDownloader:提供异步下载图片功能,当缓存中不存在的情况下,并且使用了imageIO来实现图片的渐进加载。

  SDWebImageDownloaderOperation:封装异步下载网络请求的相关功能。

  SDWebImageDecoder:异步对NSData类型的数据进行解压操作,为了避免UIImage的imageWithData函数在画图的时候对Data进行解压消耗内存,提前进行解压操作,以消耗存储空间提高执行时间和效率。

  SDWebImagePrefetcherDelegate 以预先下载图片,方便后续使用,具体差别还需要仔细看一下源码

二、SDWebImage实现原理及执行流程

   通过给UIImageView添加相应的分类方法,在分类方法中实现相应功能。下面是整理的UIImageView (WebCache)分类的程序执行流程。
   SDWebImage同时也提供了内存清理的功能:通过在初始化的时会注册一些消息通知,在内存警告或退到后台的时候清理内存图片缓存,应用结束的时候清理过期图片。
   以及内置的下载进度,在block回调方法里面可以根据接受到的数据和总的数据大小看下载进度

三、SDWebImage的设计思想

1、使用分类为类扩展更多的功能,让使用者使用图片异步加载就好像直接为UIImageView设置image一样,通过一个图片设置方法就封装了很多功能
2、面向对象编程和面向协议编程的有效结合
3、使用了以空间换时间的一些性能优化策略
4、使用imageIO结合URLConnnection,实现相应代理,每次接收到数据时更新图片

四、SDWebImage的使用和扩展

   如果我们要在下载图片的过程中实现一些其他自定义的功能,可以通过覆写他提供的一些方法来实现
   pod search SDWebImage 搜索到现在版本为:pod 'SDWebImage', '~> 3.7.3'
   关于图片更深入的学习,可参考:http://blog.cnbang.net/tech/2578/

   学习完上面这篇文章后对里面一些知识点还有些不理解,需要自己去求证。
   如:内核缓冲区(高速/低速缓存) 与 用户空间(内存)区别,
   虚拟内存(从硬盘上开辟的一块空间) 和 内存的区别。
   字节对齐(以c语言中的结构体为例,结构体中元素类型可能有多种,如int  float,我们应该以所占位数最大的float类型为标准,给int元素也分配这么大的空间,这样保证struct的所以类型大小都是一样大,并且是对齐的。这样在一片连续空间中查找结构体的各个元素时,只需要按固定大小查找即可,如果不对齐,就需要一个一个判断大小之后再取)。
    那上面讲的位图图像数据会出现多种不对齐的数据吗



 FastImageCache引伸阅读
 前期参考     
 http://www.cocoachina.com/bbs/read.php?tid=162504
 https://github.com/path/FastImageCache
 使用场景:譬如需要展示很多图片的社交类应用,比较直接和传统的方法是,从api中请求图片数据,处理原始的图片去创建期望的大小和类型,并且在设备中存储这些处理的图片。当应用需要展示这些图片的时候,将它们从磁盘加载到内存,展示到image view上或者渲染到屏幕上。

CocoaPods实践

1、cocoapods概念

   用来方便的统一管理项目中引用的第三方库的工具,它的原理是将所有第三方的依赖库放到https://github.com/CocoaPods/Specs远程仓库的pods项目中,让使用cocoapods的项目依赖这 个pods 项目来管理它所引用的第三方库,即使用这个工具将本地项目对第三方库管理的职责转移出去。

2、cocoapods安装和使用

安装常用命令如下:
ruby -v    //查看当前ruby的版本
sudo gem update --system //升级gem
gem sources -l    //查看ruby镜像的位置,看是否指向http://   ruby.taobao.org/,具体原因可打开链接
sudo gem install cocoapods   //下载安装CocoaPods
pod setup


使用方法:
  (1)终端中,cd到项目总目录,创建podfile文件,指定项目需要依赖的第三方库
  (2)使用pod search <>命令,可以预先查找自己需要的第三方库资源
  (3)创建好podfile文件后,执行pod install /pod install --verbose命令,执行安装第三方库的操作。如果前面命令的执行速度非常慢,可以使用pod install --verbose --no-repo-update命令替 换,该命令的作用是不把cocoapods远程仓库上的podspec索引文件更新到本地
   (4)  命令执行成功之后,会生成几个新的文件,一定要注意先关闭xcode,此时需要直接打开新生成的.xcworkspace来打开项目了,否则会报错
  (5)如果修改了podfile文件,执行pod update命令即可

3、podspec文件的作用

  (1)cocoapods上所有第三方库的项目都对应有一个podspec文件,放置在https://github.com/CocoaPods/Specs仓库上,第一次执行pod setup时,就会将这个仓库上所有的这些podsepc索 引文件下载到本地的~/.cocoapods/目录下。
  (2)我们也可以为自己的项目创建podspec文件,不放到cocoapods公有的远程仓库上,放到自己私有的某个git仓库上,只作为本地私有pods使用,创建好之后,其他项目就可以通过指定podspec文件信息来使用本地私有的公 共库,在podfile文件中,添加如下信息。  pod 'LfDemoCode', :podspec =>'http://git.XXX.com/users/liangfang/repos/testlf/LfDemoCode.podspec'

4、podfile.lock文件的作用

  (1)该文件用于在多人协作开发中,锁定项目中各个依赖库的版本,如果有人多次执行pod install不会更改版本,但是执行pod update就修改podfile.lock文件了,因为,在多人协作开发时,会将 该文件纳入版本控制中,如果有人修改了这个文件,大家都可以同步。

5、创建cocoapods私有库方法

   通过cocoapods创建自己的私有库,在公司内部供其他项目使用,首先需要具备如下条件:

  (1)创建一个私有的git远程仓库,类似于cocoapods的https://github.com/CocoaPods/Specs仓库,它的作用就是用于存放依赖库的podspec文件,称之为Spec Repo。然后在本地把这个仓库 添加为一个pod repo,在终端执行如下命令:
   pod repo add LFSpecs ssh://git@git.XXX.com/~liangfang/lfspecs.git,其中LFSpecs是指定的私有repo的名字,命令执行成功后,就可以在本地的~/.cocoapods/repos目录 下,看到添加的这个repo的文件夹(这里还看不到文件夹下相应的podspec文件,因为还没有push相应文件),其他人如果要共用这个repo上传他们的库文件的话,执行相同的命令即可,即也可以在本地push自己的podspec文件。

   (2)创建一个管理项目工程文件的版本控制地址,git仓库或者其他仓库地址都可以,用来存放依赖库实际代码的项目文件,方式一、使用自己已经创建好的现成的一个项目,push到这个版本 控制地址,方式二、使用pod提供的一个命令开始创建项目,如pod lib create LFPodTestLibrary,其中LFPodTestLibrary是指定的项目名字,然后再push到这个版本控制地址。这里主要考虑第一种方式的使用。具体步骤如下:

     第一步:在本地创建一个工程项目PracticeForPodProject,cd到这个工程目录下,执行git init,添加一个远程管理仓库,提交代码到这个仓库
     git remote add origin ssh://git@git.XXX.com/~liangfang/customcreatepodproject.git
     git push origin master 
     这里需要注意的是要保证打开git网址直接打开工程项目的所有文件,不要在所有文件外面再包含一层文件夹,因为pod spec lint验证的时候会报找不到source file文件的错误。一开始我是先git clone远程仓库地址,然后在这个文件夹下创建新的工程文件,就会有一个以工程名命名的总文件夹,这样就出现上面一直验证不通过的问题。

    第二步:创建一个podfile文件,如下所示,添加一些第三方依赖,保存后执行pod install, 成功之后,把相关修改提交到仓库。使用下面的命令给这个项目添加一个tag号:git tag -m “first release”  0.0.1 ,然后使用命令 git push —tags 来推送tag到远程仓库。

source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '9.0'
pod 'ReactiveCocoa', '~> 2.4.2'
pod 'Mantle', '~> 2.0.4'
pod 'SVProgressHUD-0.8.1', '~> 0.8'
pod 'Masonry', '~> 0.6.2'

   第三步:给这个项目添加一个podspec文件,可使用如下命令进行创建:
   pod spec create PracticeForPodProject git地址。
   执行完后就创建了一个podsepc文件,指定git仓库地址,修改相关内容如下, 注意相关信息与前面定义的保持一致,在本地工程主目录下,创建Pod文件夹,以及Pod下的Classes Assets子文件夹,存放相关测试的.h .m 文件。同时从其他项目拷贝一个license文件和readme.md到当前工程主目录下。保存修改的podspec文件,将前面的所以修改先提交到远程仓库,尤其是添加的文件夹以及使用最新的tag号指向你提交到远程的最新版本,然后执行pod lib lint 以及 pod spec lint PracticeForPodProject.podspec .查看验证是否通过,可以加上--no-clean查看具体信息。

 Pod::Spec.new do |s|
 s.name         = "PracticeForPodProject"
 s.version      = "0.0.1"
 s.summary      = "A test for PracticeForPodProject."
 s.description  = <<-DESC
               A test  for PracticeForPodProject in Markdown format.
               DESC
 s.homepage    = "http://git.XXX.com/users/liangfang/repos/customcreatepodproject/browse"
  s.license      = "MIT"
  s.author             = { "liangfang" => "liangfang@163.com" }
  s.platform     = :ios, "9.0"
  s.source       = { :git => "ssh://git@git.XXX.com/~liangfang/customcreatepodproject.git", :tag => "0.0.1" }
  s.source_files  = "Pod/Classes/**/*.{h,m}"
 end

 第四步:在第三步podspec文件验证通过之后,要把所有修改提交到远程仓库,同时还要把podspec文件提交到远程的spec repo,使用如下命令:
pod repo push LFSpecs PracticeForPodProject.podspec //LFSpecs即为前面创建的repo的名字,文件内容中相关字段可参考http://guides.cocoapods.org/syntax/podspec.html#specification。
 提交成功之后,就可以在本地~/.cocoapods/repos目录下以及LFSpecs对应的远程仓库上找到相应podspec文件。通过pod search PracticeForPodProject也可以找到相应工程信息。

 第五步:修改本地的podfile加上如下语句,执行pod install,再重新打开项目工程,就可以看到前面创建的Pods/Classes/.h .m,已经存放到Pods工程的Development Pods/PracticeForPodProject目录下了。每次在Pods工程下新添加了文件,主要要提交到远程仓库。操作方式与前面类似,方便其他项目对他的依赖。

  source 'ssh://git@git.XXX.com/~liangfang/lfspecs.git'
  pod "PracticeForPodProject", :path => "PracticeForPodProject.podspec"


   (3)下面在一个本地的其他项目中,来测试上面的项目是否可以通过cocoapods来使用,可通过在podfile文件中添加如下语句,如下所示:执行pod install ,再打开项目就可以在Pods工程中的Pods目录下找到依赖的本地的PracticeForPodProject工程

source 'ssh://git@git.XXX.com/~liangfang/lfspecs.git'
pod "PracticeForPodProject", :git => 'ssh://git@git.XXX.com/~liangfang/customcreatepodproject.git', :commit => 'd2804be542c'

   (4) 在其他项目中也可以通过 pod ‘PracticeForPodProject’, ‘~>1.0’ 来直接添加了
   (5)关于cocoapods的subspec属性的使用,后面再添加,如果修改了pod里面已存在的文件,不需要pod install,如果新添加了文件需要pod install.

参考链接

http://objccn.io/issue-6-4/ http://guides.cocoapods.org/syntax/podfile.html http://www.cocoachina.com/ios/20150916/13384.html http://blog.wtlucky.com/blog/2015/02/26/create-private-podspec/ http://blog.devtang.com/blog/2014/05/25/use-cocoapod-to-manage-ios-lib-dependency/ https://guides.cocoapods.org/

OC单元测试

1、XCode自带XCTest框架了解

   (1)创建一个工程,会自动为你创建一个单元测试的target,特点:文件名都是以tests结尾
   (2)分析一下X...XTests.m文件的内容,首先该类继承自XCTestsCase类,并且有三个方法,它们各自的功能如下:
     setUp方法用于在测试前设置好要测试的方法,
     tearDown则是在测试后将设置好的要测试的方法拆卸掉。
     testExample顾名思义就是一个示例。
    (3)运行单元测试,使用command+u快捷键,我好像没有想到别的方法
     在这个文件中可以增加其他测试方法,注意方法名都以test开头,保证规范。可以在方法中编写系统提供的18个断言的测试用例。可以参考博客:http://blog.csdn.net/jymn_chen/article/details/21552941,
    (4)也可以新建其他Objective-C test case class的类来编写与上面类似方法的测试用例。

2、Kiwi和Specta单元测试框架的对比

   首先知道这些框架在github上是可以找到的,与其他第三方库Masonry AFNetworking等框架的使用方式其实是差不多的。

  (1)Specta (BDD框架) 
   Specta是基于XCTest进行封装的,使用Specta,需要依赖别的第三方库,因此还要再引入OCmock/OCMockito以及Expecta/OCHamcrest一起配合使用,但是一般都会引入OCMock 和OCHamcrest 这两个一起使用。再加上OHHTTPStubs(http stub打桩)框架,

    目前主要使用Specta+OCMock+Expeata+OHHTTPStubs结合的方式。其中各个框架的作用如下:

    OCMock Or OCMockito :这两个都是用来mock对象,Stub方法的,区别在于使用OCMock的库比OCMockito的库多,而且文档和教程更加丰富。mock测试就是在测试过程中,对于某些不容易构造或者不容易获取的对象,用一个虚拟的对象来创建以便测试的测试方法。mock对象,这个虚拟的对象就是mock对象。mock对象就是真实对象在调试期间的代替品。在实际工程中的使用,我的理解是由于在面向对象编程中,需要测试的某个类,往往又依赖于其他的类,而我们不想去真正创建这个类,调用这个类的某些方法,而是模拟mock出一个这样的依赖类,stub它的方法,指定相应方法的返回值。

    Expecta Or OCHamcrest  : 都是断言的扩展框架,Expecta不成熟,框架还有一些的问题。OCHamcrest更加成熟,而且可扩展性高,可以自定义自己的断言,更灵活。这是网上别人的观点,因为自己还没有尝试做很多实例代码,后面还有待完善。

    OHHTTPStubs:在实际工程使用中主要用于测试model层在进行网络数据请求时,对接口返回数据的处理(数据解析)是否正确。同时用这个工具来模拟从网络请求返回的json数据,这样可以在本地对这个数据进行任意修改来测试,而不是真正去网络请求数据。https://github.com/AliSoftware/OHHTTPStubs

 (2)Kiwi框架

    Kiwi包含了Specta和OCmock以及Expeata所有的功能。

   总结:对于常用的specta和wiki框架,我们可以根据需要进行选择,对于他们各自的优势,还要后面多测试和学习。注意这两个框架不能同时使用,因为网上有人测试,Kiwi与Specta是不能同时在项目中使用的,会Crash。

3、框架的使用

http://www.bubuko.com/infodetail-1030528.html
http://www.cocoachina.com/ios/20150731/12859.html
https://github.com/6david9/WWDC2015
参照网上的一段
  BDD的理念: 不是写代码,而是讲故事。整个故事是由Given…When…Then组成。
  eg:BDD框架Kiwi的一段测试代码:
  describe(@"Team", ^{
  context(@"when newly created", ^{
    it(@"has a name", ^{
        id team = [Team team]; [[team.name should] equal:@"Black Hawks"];
     });
    it(@"has 11 players", ^{
        id team = [Team team]; [[[team should] have:11] players];
     });
});
});

  这个测试用例就是在说Given a Team,When newly created,it should have a name, and should have 11 players,基本上不需要注释就能知道在干嘛

结合之前做的基于MVVM框架些的代码,做了一个打桩的测试,大致实现过程

 (1)创建ADModelHTTPStub类(.h .m文件),定义打桩的方法,截取符合相应字符串的网络请求的url,对这个请求的url返回本地的一个json文件来模拟返回的数据,前提是在创建好json文件,存放返回的数据
   + (void)stubFetchADInfoSucceed{
[OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) {
    NSRegularExpression *re = [NSRegularExpression regularExpressionWithPattern:@"/test/v3/guanggao"
                                                                        options:0
                                                                          error:nil];
    return [re numberOfMatchesInString:[request.URL absoluteString] options:0 range:NSMakeRange(0, [[request.URL absoluteString] length])];
} withStubResponse:^OHHTTPStubsResponse *(NSURLRequest *request) {
    NSData *data = [NSData dataWithContentsOfFile:[[NSBundle bundleForClass:[self class]] pathForResource:@"AdertisementInfo" ofType:@"json" inDirectory:nil]];
    return [OHHTTPStubsResponse responseWithData:data statusCode:200 headers:nil];
}];
}


 + (void)stubFetchADConfigSucceed{
[OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) {
    NSRegularExpression *re = [NSRegularExpression regularExpressionWithPattern:@"/test/v3/perzhi"
                                                                        options:0
                                                                          error:nil];
    return [re numberOfMatchesInString:[request.URL absoluteString] options:0 range:NSMakeRange(0, [[request.URL absoluteString] length])];
} withStubResponse:^OHHTTPStubsResponse *(NSURLRequest *request) {
    NSData *data = [NSData dataWithContentsOfFile:[[NSBundle bundleForClass:[self class]] pathForResource:@"ADConfig" ofType:@"json" inDirectory:nil]];
    NSDictionary *json = [NSJSONSerialization
                          JSONObjectWithData:data
                          options:kNilOptions
                          error:nil];
    NSLog(@"json:%@",json);
    return [OHHTTPStubsResponse responseWithData:data statusCode:200 headers:nil];
}];
}

(2)创建一个objective-c的.m文件,用spec语法进行创建

  SpecBegin(MyBannerModel)
  describe(@"MyBannerModel", ^{
context(@"", ^{
    beforeAll(^{
        [ADModelHTTPStub stubFetchADInfoSucceed];
        [ADModelHTTPStub stubFetchADConfigSucceed];
    });        
    afterAll(^{
        [OHHTTPStubs removeAllStubs];
    });    
    it(@"", ^AsyncBlock{
        MyBannerModel *infoModel = [[MyBannerModel alloc] init];
        [[infoModel fetchSomeADInfoByID:@"2" withVersion:@"0.6.2"] subscribeNext:^(NSArray *infoArray) {
            expect(infoArray).notTo.beNil();
            expect([infoArray count]).to.equal(3);
            done(); 
        }];            
    });
    it(@"", ^AsyncBlock{            
        MyConfigModel *configModel = [[MyConfigModel alloc] init];
        [[configModel fetchWithApp:@"iphone"] subscribeNext:^(MTADConfig *config) {
            expect(config).to.beKindOf([MyADConfig class]);
            done();
        }]; 
    });    
});
 });
SpecEnd

4、其他相关

(1)logit test与application test区别
   logit test类似于白盒测试,用于测试工程中较细节的逻辑,application test类似于黑盒测试,或者接口测试,用于测试直接与用户交互的接口(服务器接口返回的数据在客户端展示是否正常)
  logit test与application test的区别还表现在setUp方法上, logit test只需要在setUp方法中初始化一些测试数据,application test需要在setUp方法中获取主应用的AppDelegate,供test方法调用。这样是网上有人这么说的, 但是自己不太明白真正的原因,猜想是应用测试需要调用接口?

   网上说要注意对于会侵入主应用的test bundle,在使用过程中要十分注意,不要让单元测试的资源覆盖主应用资源,否则会造成诡异的bug,我猜想这个可能就是要使用mock stub 等测试框架来模拟数据的原因,而不是直接使用主应用返回的数据。

 (2) xcode现在也支持与ui操作相关的测试,可以参照网上别人的探索
 http://www.cocoachina.com/ios/20150702/12253.html

(3)RAC绑定相关测试的注意事项
  下面以一个登录页面文本输入框的绑定为例,摘录自网上实例。这里有一个关键点,emailTextField或passwordTextField必须调用sendActionsForControlEvents:UIControlEventEditingChanged方法,才能触发textField的text属性改变。

  技巧:要找到相应调用方法需要通过查看对应控件的RAC源码中将通知(相应方法)转化为信号的处理过程。
  SPEC_BEGIN(LoginViewControllerSpec)

 describe(@"LoginViewController", ^{
__block LoginViewController *controller;

beforeEach(^{
    controller = [UIViewController loadViewControllerWithIdentifierForMainStoryboard:@"LoginViewController"];
    [controller view];
});

afterEach(^{
    controller = nil;
});

describe(@"Email Text Field", ^{
    context(@"when touch text field", ^{
        it(@"should not be nil", ^{
            [[controller.emailTextField shouldNot] beNil];
        });
    });

    context(@"when text field's text is hello", ^{
        it(@"shoud euqal view model's email property", ^{
            controller.emailTextField.text = @"hello";
            [controller.emailTextField sendActionsForControlEvents:UIControlEventEditingChanged];
            [[controller.viewModel.email should] equal:@"hello"];
        });
    });
});

describe(@"Password Text Field", ^{
    context(@"when touch text field", ^{
        it(@"should not be nil", ^{
            [[controller.passwordTextField shouldNot] beNil];
        });
    });

    context(@"when text field' text is hello", ^{
        it(@"should equal view model's password property", ^{
            controller.passwordTextField.text = @"hello";
            [controller.passwordTextField sendActionsForControlEvents:UIControlEventEditingChanged];

            [[controller.viewModel.password should] equal:@"hello"];
        });
    });
});
});

 绑定的测试通过之后,可以进一步模拟绑定得到的数据,进行其他相关测试。

(4)写与MVVM框架相关的单元测试的方法
    model层:主要通过打桩测试网络请求返回的数据,通过订阅相应方法的signal返回的信号进行测试
    viewmodel层:主要执行相应commad,然后订阅command中被赋值的属性数据,进行测试
    viewcontroller层:对一些绑定操作等的测试。

 (5)写单测过程中遇到的问题和解决办法
  在测试vm层相关command执行后返回的结果时,通常做法是通过RACObserver来观察返回值,使用
   [RACObserve(viewModel, sectionCollectionResults) subscribeNext:.... 或者
   [RACObserve(viewModel.sectionCollectionResults, objectsCount).....
   是需要取决于你要测试的代码,如果是在执行command返回时初始化sectionCollectionResults并添加元素,第一个可以订阅到信号。否则好似订阅不到的。需要通过下面的方式。

   使用[RACObserve(viewModel.sectionCollectionResults, objectsCount).订阅时,又由于commad在返回结果时会对sectionCollectionResults先进行clearall操作。

(5) 各个框架的api文档

https://github.com/specta/specta https://github.com/specta/expecta http://ocmock.org/reference/ https://github.com/AliSoftware/OHHTTPStubs