HTTP的缓存策略(Expires、Last-Modified、Etag缓存控制)

当请求一个页面时,如果浏览器使用本地缓存,因此我们经常会看到一个HTTP请求为304状态。或者显示200状态,在chrome下标注是from cache,在火狐下会标注BFCache;

我们希望在服务器端更新了静态文件(如css、js、图片),能够在客户端得到及时的更新,但又不想让浏览器每次请求都从服务器端获取静态资源。那么就需要了解一些下面的知识:

Last-Modified / If-Modified-Since

当浏览器第一次请求一个url时,服务器端的返回状态码为200,同时HTTP响应头会有一个Last-Modified标记着文件在服务器端最后被修改的时间。

浏览器第二次请求上次请求过的url时,浏览器会在HTTP请求头添加一个If-Modified-Since的标记,用来询问服务器该时间之后文件是否被修改过。

如果服务器端的资源没有变化,则自动返回304状态,使用浏览器缓存,从而保证了浏览器不会重复从服务器端获取资源,也保证了服务器有变化是,客户端能够及时得到最新的资源。

Etag / If-None-Match

当浏览器第一次请求一个url时,服务器端的返回状态码为200,同时HTTP响应头会有一个Etag,存放着服务器端生成的一个序列值。

浏览器第二次请求上次请求过的url时,浏览器会在HTTP请求头添加一个If-None-Match的标记,用来询问服务器该文件有没有被修改。

Etag 主要为了解决 Last-Modified 无法解决的一些问题:

  1. 一些文件也许会周期性的更改,但是他的内容并不改变(仅仅改变的修改时间),这个时候我们并不希望客户端认为这个文件被修改了,而重新GET;
  2. 某些文件修改非常频繁,比如在秒以下的时间内进行修改,(比方说1s内修改了N次),If-Modified-Since能检查到的粒度是s级的,这种修改无法判断(或者说UNIX记录MTIME只能精确到秒)
  3. 某些服务器不能精确的得到文件的最后修改时间;

Expires

<meta http-equiv="expires" content="Fri, 22 Aug 2014 00:52:49 GMT" />

HTTP 1.0,设置缓存的截止时间,在此之前,浏览器对缓存的数据不重新发请求。它与Last-Modified/Etag结合使用,用来控制请求文件的有效时间,当请求数据在有效期内,浏览器从缓存获得数据。Last-Modifed/Etag能够节省一点宽带,但是还会发一个HTTP请求。

Cache-Control

<!--Cache-Control: max-age=秒 -->
<meta http-equiv="Cache-Control" content="max-age=120"/>

HTTP 1.1,设置资源在本地缓存多长时间。

如果Cache-Control与expires同时存在,Cache-Control生效。expires 的一个缺点就是,返回的到期时间是服务器端的时间,这样存在一个问题,如果客户端的时间与服务器的时间相差很大,那么误差就很大,所以在HTTP 1.1版开始,使用Cache-Control: max-age=秒替代。

用户操作与缓存

禁止缓存

<!--禁止浏览器本地缓存 -->
<meta http-equiv="Cache-Control" content="no-cache"/>

<!-- 或者 -->
<meta http-equiv="Cache-Control" content="max-age=0"/>

还有POST请求不使用缓存,HTTP响应头不包含Last-Modified/Etag,也不包含Cache-Control/Expires不会使用缓存。

除非有特殊需求,最好还是不要禁用缓存,毕竟是用缓存能节省宽带,节省服务器资源,节省money...

浏览器第一次请求过程

浏览器第二次请求过程

我们希望服务器端更新了文件,客户端可以及时的更新文件,根具上面流程,我们需要针对静态文件的响应头添加expires,设置为永久过期,浏览器每次请求静态文件,就会询问服务器文件有没有做过更改,如果更改了就从服务器端获取资源,否则直接使用缓存。

apache的配置:

#开启mod_expires模块
LoadModule expires_module modules/mod_expires.so

ExpiresActive On
ExpiresDefault "access plus 0 seconds" #默认缓存0s
<Directory  "根目录">
    #Options FollowSymLinks
    #AllowOverride all
    Order deny,allow
    Allow from all
    #ExpiresByType application/* "access plus 0 seconds"
    #ExpiresByType image/* "access plus 0 seconds"
    #ExpiresByType text/css "access plus 0 seconds"
</Directory>

这样的做法有个弊端,就是每次请求都会询问服务器端资源是否过期,当然还有更好的办法。

不管怎样,适合自己项目的就是好方法。

2018/9/18 posted in  HTTP

HTTP 中 GET 与 POST 的区别

GET和POST是HTTP请求的两种基本方法,要说它们的区别,接触过WEB开发的人都能说出一二。

最直观的区别就是GET把参数包含在URL中,POST通过request body传递参数。

你可能自己写过无数个GET和POST请求,或者已经看过很多权威网站总结出的他们的区别,你非常清楚知道什么时候该用什么。

当你在面试中被问到这个问题,你的内心充满了自信和喜悦。

你轻轻松松的给出了一个“标准答案”:

  • GET在浏览器回退时是无害的,而POST会再次提交请求。
  • GET产生的URL地址可以被Bookmark,而POST不可以。
  • GET请求会被浏览器主动cache,而POST不会,除非手动设置。
  • GET请求只能进行url编码,而POST支持多种编码方式。
  • GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留。
  • GET请求在URL中传送的参数是有长度限制的,而POST么有。
  • 对参数的数据类型,GET只接受ASCII字符,而POST没有限制。
  • GET比POST更不安全,因为参数直接暴露在URL上,所以不能用来传递敏感信息。
  • GET参数通过URL传递,POST放在Request body中。 (本标准答案参考自w3schools)

“很遗憾,这不是我们要的回答!”

请告诉我真相。。。

如果我告诉你GET和POST本质上没有区别你信吗?
让我们扒下GET和POST的外衣,坦诚相见吧!

GET和POST是什么?HTTP协议中的两种发送请求的方法。

HTTP是什么?HTTP是基于TCP/IP的关于数据如何在万维网中如何通信的协议。

HTTP的底层是TCP/IP。所以GET和POST的底层也是TCP/IP,也就是说,GET/POST都是TCP链接。GET和POST能做的事情是一样一样的。你要给GET加上request body,给POST带上url参数,技术上是完全行的通的。

那么,“标准答案”里的那些区别是怎么回事?

在我大万维网世界中,TCP就像汽车,我们用TCP来运输数据,它很可靠,从来不会发生丢件少件的现象。但是如果路上跑的全是看起来一模一样的汽车,那这个世界看起来是一团混乱,送急件的汽车可能被前面满载货物的汽车拦堵在路上,整个交通系统一定会瘫痪。为了避免这种情况发生,交通规则HTTP诞生了。HTTP给汽车运输设定了好几个服务类别,有GET, POST, PUT, DELETE等等,HTTP规定,当执行GET请求的时候,要给汽车贴上GET的标签(设置method为GET),而且要求把传送的数据放在车顶上(url中)以方便记录。如果是POST请求,就要在车上贴上POST的标签,并把货物放在车厢里。当然,你也可以在GET的时候往车厢内偷偷藏点货物,但是这是很不光彩;也可以在POST的时候在车顶上也放一些数据,让人觉得傻乎乎的。HTTP只是个行为准则,而TCP才是GET和POST怎么实现的基本。

但是,我们只看到HTTP对GET和POST参数的传送渠道(url还是requrest body)提出了要求。“标准答案”里关于参数大小的限制又是从哪来的呢?
在我大万维网世界中,还有另一个重要的角色:运输公司。不同的浏览器(发起http请求)和服务器(接受http请求)就是不同的运输公司。 虽然理论上,你可以在车顶上无限的堆货物(url中无限加参数)。但是运输公司可不傻,装货和卸货也是有很大成本的,他们会限制单次运输量来控制风险,数据量太大对浏览器和服务器都是很大负担。业界不成文的规定是,(大多数)浏览器通常都会限制url长度在2K个字节,而(大多数)服务器最多处理64K大小的url。超过的部分,恕不处理。如果你用GET服务,在request body偷偷藏了数据,不同服务器的处理方式也是不同的,有些服务器会帮你卸货,读出数据,有些服务器直接忽略,所以,虽然GET可以带request body,也不能保证一定能被接收到哦。

好了,现在你知道,GET和POST本质上就是TCP链接,并无差别。但是由于HTTP的规定和浏览器/服务器的限制,导致他们在应用过程中体现出一些不同。

你以为本文就这么结束了?

我们的大BOSS还等着出场呢。。。

这位BOSS有多神秘?当你试图在网上找“GET和POST的区别”的时候,那些你会看到的搜索结果里,从没有提到他。他究竟是什么呢。。。

GET和POST还有一个重大区别,简单的说:

GET产生一个TCP数据包;POST产生两个TCP数据包。

长的说:

对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据);

而对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)。

也就是说,GET只需要汽车跑一趟就把货送到了,而POST得跑两趟,第一趟,先去和服务器打个招呼“嗨,我等下要送一批货来,你们打开门迎接我”,然后再回头把货送过去。

因为POST需要两步,时间上消耗的要多一点,看起来GET比POST更有效。因此Yahoo团队有推荐用GET替换POST来优化网站性能。但这是一个坑!跳入需谨慎。为什么?

  1. GET与POST都有自己的语义,不能随便混用。

  2. 据研究,在网络环境好的情况下,发一次包的时间和发两次包的时间差别基本可以无视。而在网络环境差的情况下,两次包的TCP在验证数据包完整性上,有非常大的优点。

  3. 并不是所有浏览器都会在POST中发送两次包,Firefox就只发送一次。

现在,当面试官再问你“GET与POST的区别”的时候,你的内心是不是这样的?

2018/9/18 posted in  HTTP

iOS 中的 Promise 设计模式

做iOS开发的同学都非常熟悉代理模式,为避免代码耦合,代理模式的委托者任务交给代理执行,代理执行完毕之后再把回调告诉委托者。委托者不关心代理是怎么执行任务的,只关心结果是成功还是失败。代理模式就像是杀手与雇主的关系一样。

但是代理模式也不完美,代理多了,雇主也管不过来了,委托在A处,收结果却要在B处。有的时候,雇主也希望能在同一个地方既可以发配任务,也可以接收结果。闭包Block就能帮雇主解决这个问题了。无论是系统的GCD,还是平时随手封装一个 UIAlertView 的block实现,都让代码的可读性有了一定的提升。

无论是代理模式,还是闭包,在处理单一任务的时候,都出色的完成了任务。可是当两种模式要相互配合,一起完成一系列任务,并且每个任务之间还要共享信息,相互衔接,雇主就要头疼了。当然可以只用一种模式来实现,代理模式就不说了,过于分散,不善于处理这种流程性的事务。那我用闭包来举一个例子:我们需要顺序执行Task A、B、C 三个任务,A、B、C依次执行,任务完成之后都使用闭包来回调并开始下一个任务。代码如下:

  - (void)callbackHell
{
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self doTaskA:^{
            [self doTaskB:^{
               [self doTaskC:^{
                  // all task done
               }];
            }];
        }];
    });
}

上面的代码看起来挺清晰,可读性也还可。如果加上一些 ifelse 的分支判断,再加上一些参数的传递,代码不知不觉的向右延伸,最终超出了屏幕的宽度,形成一个倒金字塔的形状。写 JavaScript 的同学会说:你已经掉进了回调陷阱(CallbackHell),赶紧用Promise设计模式来跳坑吧。

Promise 设计模式的原理

Promise设计模式把每一个异步操作都封装成一个Promise对象,这个Promise对象就是这个异步操作执行完毕的结果,但是这个结果是可变的,就像薛定谔的猫,只有执行了才知道。通过这种方式,就能提前获取到结果,并处理下一步骤。

Promise 使用 then 作为关键字,回调最终结果。 then 是整个Promise设计模式的核心,必须要被实现。另外还有其它几个关键字用来表示一个Promise对象的状态:

  1. pending: 任务执行中,状态可能会进入下面的fullfill或者reject二者之一
  2. fufill/resolved: 任务完成了,返回结果
  3. reject: 任务失败,并返回错误更多可以参考 官方规范

    如上图所示,fullfill与reject的状态都是不可逆转的,保证了结果的唯一性。除了 then ,一些对 Promise 的实现还有几个关键字用来扩展,让代码可读性更强:

  4. catch: 任务失败,处理error

  5. finally: 无论是遇到 then 还是 catch 分支,最终都会执行的回调

  6. when: 多个异步任务执行完毕之后才会回调

Promise模式的实现

Promise设计模式在 iOS/MacOS 平台的最佳实践是由大名鼎鼎的homebrew的作者 Max Howell 写的一个支持iOS/MacOS 的异步编程框架 – PromiseKit , 作者的另一个广为人知的趣事是因为没有写出反转二叉树而没有拿到Google的offer。

我们先抛出对上面改良函数使用PromiseKit的实现,再看原理:

- (void)jumpOutCallbackHell
{
    [self promiseTaskA].then(^{
        return [self promiseTaskB];
    }).then(^{
        return [self promiseTaskC];
    }).then(^{
        NSLog(@"all task done");
    });
}

调试后,发现执行的结果与我们期待的一致,但是上面的代码对我来说有几个疑惑点:

then 是怎么串起来的;
怎么实现的顺序调用;
如果传递参数,参数是怎么传递的。
带着问题,来看Promise的源码:

- (PMKPromise *(^)(id))then {
    return ^(id block){
        return self.thenOn(dispatch_get_main_queue(), block);
    };
}

如果对block不是很熟悉,可能不太理解这段代码,实际上,PromiseKit灵活的使用了block作为函数的返回值来实现链式调用。相比原来的block嵌套模式,PromiseKit使用block将多个 then 串联起来,解决了callback hell。

接着来继续看下一个问题。

- (id)resolved:(PMKResolveOnQueueBlock(^)(id result))mkresolvedCallback
       pending:(void(^)(id result, PMKPromise *next, dispatch_queue_t q, id block, void (^resolver)(id)))mkpendingCallback
{
    __block PMKResolveOnQueueBlock callBlock;
    __block id result;

    dispatch_sync(_promiseQueue, ^{
        if ((result = _result))
            return;

        callBlock = ^(dispatch_queue_t q, id block) {

            block = [block copy];

            __block PMKPromise *next = nil;

            dispatch_barrier_sync(_promiseQueue, ^{
                if ((result = _result))
                    return;

                __block PMKPromiseFulfiller resolver;
                next = [PMKPromise new:^(PMKPromiseFulfiller fulfill, PMKPromiseRejecter reject) {
                    resolver = ^(id o){
                        if (IsError(o)) reject(o); else fulfill(o);
                    };
                }];
                [_handlers addObject:^(id value){
                    mkpendingCallback(value, next, q, block, resolver);
                }];
            });

             return next ?: mkresolvedCallback(result)(q, block);
        };
    });

     return callBlock ?: mkresolvedCallback(result);
}

代码有点长,不过也可以理解。这个方法是上面的thenon调用的,接受两个参数,第一个参数是一个resolve的block,第二个参数是一个pending的block。一个Promise在执行完毕之后,无论状态是变成resolve还是pending,都通过这个方法,执行对应的 then,并返回一个Promise对象。上面的函数中,有一个dispatch_barrier_sync ,barrier是栅栏的意思,一般来说如果我们有多个异步任务,但是希望他们按照一定的顺序执行,就可以使用这个方法。在这里PromiseKit通过barrier实现了then的依次调用。在这个barrier方法内部,一个是会去看当前是否已经有下一个要执行的Promise,如果没有就生成一个新的,另一个把对应的pending 放到handler队列,依次执行。

参数传递

这里需要思考的另外一个问题是,既然多个任务之间有依次调用的关系,那么这样的一种任务流之间如何互相通信呢?PromiseKit用了一个比较有趣的办法来实现相邻Promise对象的参数传递。

在万物皆消息的OC语言内部,每一个方法,包括Block在内都是有类型签名的。这个类型签名对象就是 NSMethodSignature

@interface NSMethodSignature : NSObject {
...
@property (readonly) NSUInteger numberOfArguments;
...
@property (readonly) const char *methodReturnType NS_RETURNS_INNER_POINTER;
...
@end
那么对于block,怎么获取类型签名呢?PromiseKit自己定义了一个block的结构体:

struct PMKBlockLiteral {
    void *isa; 
    int flags;
    int reserved;
    void (*invoke)(void *, ...);
    struct block_descriptor {
      unsigned long int reserved;       // NULL
        unsigned long int size;         // sizeof(struct Block_literal_1)
        void (*copy_helper)(void *dst, void *src);     // IFF (1<<25)
        void (*dispose_helper)(void *src);             // IFF (1<<25)
      const char *signature;                       // IFF (1<<30)
    } *descriptor;
};

熟悉block的同学都知道,flags按照bit位保存了一些block的附加信息,在 1<<30的这个bit可以找到是否有类型签名signature,剩下的就是通过flags移动指针,找到signature所在的内存空间了。找到了signature,也就获取到了参数个数与函数返回值这些信息。函数返回值的类型是经过编码的,具体的对照表可以参考官方文档

    id pmk_safely_call_block(id frock, id result) {
        NSMethodSignature *sig = NSMethodSignatureForBlock(frock);
        const NSUInteger nargs = sig.numberOfArguments;
        const char rtype = sig.methodReturnType[0];
        type (^block)(id, id, id) = frock; 
        return [result class] == [PMKArray class] 
                            ? block(result[0], result[1], result[2])
                            : block(result, nil, nil);
    }

有了函数签名,就能知道block的信息了。上面只截取了部分代码,简单来说,PromiseKit 通过动态的获取block的参数个数与返回类型来决定block的调用。一般来说, fullfill(id) 在调用的时候最多只支持传递一个参数,在必要的时候,PromiseKit把这些参数放在一个数组里面,这个数组就是 PMKArray ,当检测到这个参数是一个数组的时候,就依次取出数组内的元素作为参数传递。

从而支持了多个参数的传递。

总结

至此, 对PromiseKit的一些解释也就结束了,PromiseKit有OC的1.0版本,也有支持了swift的3.0版本。如果你非常享受这样的书写方式,可以接入很多扩展的版本,可以写出看起来优雅又舒服的代码,比如 NSURLSession :

URLSession.GET("http://example.com").asDictionary().then { json in

}.catch { error in
    //…
}

还有很多的扩展与关键字的支持,这里都不再展开。

而对于我来说,Promise设计模式能够解决我对散落在各处的代理模式产生的代码的烦恼,也让我避免了跳进回调陷阱,就值得总结了。

FROM:iOS 中的 Promise 设计模式

2018/9/17 posted in  内核编程

XCTest简介

准备工作

对于新项目,在新建项目界面勾选上UI Tests;

对于旧项目,在项目界面点击菜单栏中的File→New→Target…→iOS→Test→iOS UITesting Bundle。

sleepForTimeInterval:

线程休眠

[NSTread sleepForTimeInterval:1.0f];

也可以使用sleep(3),OC兼容C语言。

定义测试用例

XCTestCase

+ (void)setUp;
在类中的第一个测试方法调用之前调用,区别于-(void)setUp:在每个测试方法调用之前都调用。

+ (void)tearDown;
在类中的最后一个测试方法完成后调用。区别于-(void) tearDown:在每个测试方法调用后都调用。

异步测试表达式

*-(XCTestExpectation *)expectationWithDescription:(NSString )description;
指定时间内满足测试条件则测试通过,超时则输出description。

- (void)testAsynExample {
XCTestExpectation *exp =[self expectationWithDescription:@"这里可以是操作出错的原因描述。。。"];
NSOperationQueue *queue =[[NSOperationQueue alloc]init];

[queue addOperationWithBlock:^{
//模拟这个异步操作需要2秒后才能获取结果,比如一个异步网络请求
sleep(2);
//模拟获取的异步操作后,获取结果,判断异步方法的结果是否正确
XCTAssertEqual(@"a",@"a");
//如果断言没问题,就调用fulfill宣布测试满足
[exp fulfill];

}];

//设置延迟多少秒后,如果没有满足测试条件就报错

[self waitForExpectationsWithTimeout:3 handler:^(NSError * _Nullable error){

if(error){
NSLog(
    @"Timeout Error: %@", error);
}
}];

}

*-(XCTestExpectation *)expectationForPredicate:(NSPredicate )predicate evaluatedWithObject:(id)object handler:(XCPredicateExpectationHandler)handler;

利用谓词计算,如果限定时间内满足条件则通过测试

- (void)testThatBackgroundImageChanges {

    XCTAssertNil([self.button backgroundImageForState:UIControlStateNormal]);

    NSPredicate *predicate =[NSPredicate predicateWithBlock:^BOOL(UIButton * _Nonnull button,NSDictionary<NSString *,id>* _Nullable bindings){

    return[button backgroundImageForState:UIControlStateNormal]!=nil; 
    }];

[self expectationForPredicate:predicate evaluatedWithObject:self.button handler:nil];
[self waitForExpectationsWithTimeout:20 handler:nil];
}

*-(XCTestExpectation *)expectationForNotification:(NSString )notificationName object:(id)objectToObserve handler:(XCNotificationExpectationHandler)handler;

监听一个通知,如果在规定时间内正确收到通知则测试通过。

- (void)testAsynExample1 {
    [self expectationForNotification:(@"监听通知的名称xxx") object:nil handler:nil];
    [[NSNotificationCenter defaultCenter]postNotificationName:@"监听通知的名称xxx" object:nil];
    //设置延迟多少秒后,如果没有满足测试条件就报错
    [self waitForExpectationsWithTimeout:3 handler:nil];
}

*- (XCTestExpectation *)keyValueObservingExpectationForObject:(id)objectToObserve keyPath:(NSString )keyPath expectedValue:(id)expectedValue;

创建一个KVO观察模式

- (XCTestExpectation *)keyValueObservingExpectationForObject:(id)objectToObserve keyPath:(NSString *)keyPath handler:(XCKeyValueObservingExpectationHandler)handler;

创建一个KVO观察模式

-(void)waitForExpectationsWithTimeout:(NSTimeInterval)timeout handler:(XCWaitCompletionHandler)handler;

设定等待时间,等待时间内满足所有条件则测试通过,成功或超时都会执行handler block(optional)

typedef BOOL (^XCPredicateExpectationHandler)(void);

如果未提供Handle,第一次测试通过即满足条件,如果提供了Handle,它能覆盖原有的行为和条件,那么将重新判定是否满足条件。

typedef BOOL(^XCNotificationExpectationHandler)(NSNotification *notification);

获得符合期望的通知时将被调用,满足条件为Yes

typedef BOOL (^XCKeyValueObservingExpectationHandler)(id observedObject, NSDictionary *change);

当KVO监视的值反正改变是调用,满足条件为Yes

typedef void(^XCWaitCompletionHandler)(NSError *error);

当测试成功或超时时调用,需要指定error类型,否则error = nil;

@property BOOL continueAfterFailure;
默认为Yes,当case中某条测试语句失败时会继续向下执行,实测只向下执行了一步,待验证。

- (void)measureBlock:(void(^)(void))block;

测试块中代码的性能。

  • (void)measureMetrics:(NSArray*)metrics automaticallyStartMeasuring:(BOOL)automaticallyStartMeasuring forBlock:(void()(void))block;

measureBlock的拓展版,当需要自定义测量的开始点和结束点时,又或者要测量多个指标时调用此方法。

Metrics:是测量标准数组;automaticallyStartMeasuring为真时,自动开始测试,为假则需要startMeasuring作为启动点。

注意在一个代码块中开始点和结束点只能各有一个,出现一下情况时测试将会失败: automaticallyStartMeasuring = YES且代码块中调用了startMeasuring方法; automaticalltStattMeasuring = NO 且代码块中没调用或多次调用了startMeasuring方法;

在代码块中多次调用了stopMeasuring方法。

-(void)startMeasuring;

在measureBlock中调用此方法来标记一个测量起点。

-(void)stopMeasuring;

在measureBlock中调用此方法来标记一个结束点。

+(NSArray*)defaultPerformanceMetrics;

这是调用measureBlock时默认使用的测量标准数组。

-(id)addUIInterruptionMonitorWithDescription:(NSString *)handlerDescription handler:(BOOL()(XCUIElement

*interruptingElement))handler;

在当前上下文中添加一个Handle

handlerDescription:用于阐述这个Handle的作用和行为,主要被用来Debug和分析异步测试

XCTestExpectation

使用以下XCTestCase方法来创建XCTestExpectation实例: expectationWithDescription:

expectationForPredicate:evaluatedWithObject:handler: expectationForNotification:object:handler: keyValueObservingExpectationForObject:keyPath:expectedValue: keyValueObservingExpectationForObject:keyPath:handler:

-(void)fulfill;

为满足条件的表达式做标记

布尔值检测

XCTAssert / XCTAssertTrue

断言表达式为真,XCTAssert(expression, format...)当expression求值为TRUE时通过; XCTAssert([image exists]);

XCTAssertTrue(expression, format...)当expression求值为TRUE时通过; XCTAssertTure([image exists]);

XCTAssertFalse

表达式为假,XCTAssertFalse(expression, format...)当expression求值为False时通过; XCTAssertFalse(![image exists]);

空值检测

XCTAssertNil

表达式的值为空,XCTAssertNil(a1, format...)为空判断,a1为空时通过,反之不通过; NSArray *array =nil;

XCTAssertNil(array);

XCTAssertNotNil

表达式的值非空,XCTAssertNotNil(a1, format…)不为空判断,a1不为空时通过,反之不通过; NSArray *array =[NSArray array];

XCTAssertNotNil(array);

等式检测

XCTAssertEqual

XCTAssertEqual(a1, a2, format...)判断相等(当a1和a2是C语言标量、结构体或联合体时使用, 判断的是变量的地址,如果地址相同则返回TRUE,否则返回NO);

XCTAssertEqual(array,array2,@"失败时输出");

XCTAssertEqualObjects

XCTAssertEqualObjects(a1, a2, format...)判断相等,[a1 isEqual:a2]值为TRUE时通过,其中一个不为空时,不通过;

XCTAssertEqualObjects(array,array2,@"失败时输出"); XCTAssertEqualWithAccuracy

XCTAssertEqualWithAccuracy(a1, a2, accuracy, format...)判断相等,(double或float类型)提供一个误差范围,当在误差范围(+/-accuracy)以内相等时通过测试; XCTAssertEquallWithAccuracy(array,array2,@"失败时输出");

不等式检测

XCTAssertNotEqual

XCTAssertNotEqual(a1, a2, format...)判断不等(当a1和a2是C语言标量、结构体或联合体时使用);

XCTAssertNotEqual(array,array2,@"失败时输出"); XCTAssertNotEqualObjects

XCTAssertNotEqualObjects(a1, a2, format...)判断不等,[a1 isEqual:a2]值为False时通过; XCTAssertNotEqualObjects(array,array2,@"失败时输出"); XCTAssertNotEqualWithAccuracy

XCTAssertNotEqualWithAccuracy(a1, a2, accuracy, format...) 判断不等,(double或float类型)提供一个误差范围,当在误差范围以内不等时通过测试; XCTAssertNotEquallWithAccuracy(array,array2,@"失败时输出");

相对值检测

XCTAssertGreaterThan:A > B

XCTAssertGreaterThan(floatB,floatA,@"Fail Output"); XCTAssertGreaterThanOrEqual:A ≥ B

XCTAssertGreaterThanOrEqual(floatB,floatA,@"Fail Output"); XCTAssertLessThan:A< B

XCTAssertLessThan(floatB,floatA,@"Fail Output"); XCTAssertLessThanOrEqual:A ≤ B

XCTAssertLessThanOrEqual(floatB,floatA,@"Fail Output");

异常检测

XCTAssertThrows(expression, format...)

异常测试,当expression发生异常时通过;反之不通过;

XCTAssertThrowsSpecific(expression, specificException, format...)

异常测试,当expression发生specificException异常时通过;反之发生其他异常或不发生异常均不通过;

XCTAssertThrowsSpecificNamed(expression, specificException, exception_name, format...)

异常测试,当expression发生具体异常、具体异常名称的异常时通过测试,反之不通过; XCTAssertNoThrow(expression, format…)

异常测试,当expression没有发生异常时通过测试;

XCTAssertNoThrowSpecific(expression, specificException, format...)

异常测试,当expression没有发生具体异常、具体异常名称的异常时通过测试,反之不通过; XCTAssertNoThrowSpecificNamed(expression, specificException, exception_name, format...) 异常测试,当expression没有发生具体异常、具体异常名称的异常时通过测试,反之不通过。无条件失败断言

XCTFail

无条件产生一个失败的结果。

XCTFail();

UI Testing

XCUIElements API

exists

判断控件对象是否存在。BOOL类型。

[textField exists]

debugDescription

保存某控件的debug信息,这些信息只能用于调试case,NSString类型。

NSLog(@"%@",[textField debugDescription]);

hittable

BOOL类型的只读属性,表示当前元素能否获取到坐标。

descendantsMatchingType

从该控件下所有子控件中找到符合指定类型的控件,需要传入XCUIElementType(枚举类,定义了iOS中所有可定位的控件)类型的参数,返回包含了XCUIElementType类型的XCUIElementQuery数组。

XCUIElementQuery *textFields =[cell

childrenMatchingType:XCUIElementTypeTextField];

childrenMatchingType

只从与该控件有直接关系的子控件中找到符合指定类型的控件,需要传入XCUIElementType 类型的参数,返回包含了XCUIElementType类型的XCUIElementQuery数组。XCUIElementQuery *textFields =[cell

descendantsMatchingType:XCUIElementTypeTextField];

tap

单击

[app.tables.staticTexts[@"Groceries"]tap];

。doubleTap

双击

[buttondoubletap];

twoFingerTap

双指单击

[app twoFingerTap];

pressForDuration(duration: NSTimeInterval)

长按,时间由传入的参数定义,单位为秒

[textField pressForDuration:5.5];

pressForDuration(duration: NSTimeInterval, thenDragToElement otherElement: XCUIElement) 长按拖拽。在控件上长按后,拖拽到另外一个控件。传入2个参数:长按时间和拖拽到目标控件。

[textField pressForDuration:5.5, thenDragToElement:table];

swipeUp/ swipeDown/ swipeLeft/ swipeRight

从下划到上/从上滑到下/从右滑到左/从左滑到右

[app swipeUp];

typeText

输入字符。需要一个参数:NSString

[addItemTextField typeText:@"Hello"];

tapWithNumberOfTaps:numberOfTouches:

多触摸点及多次点击

[windows tapWithNumberOfTaps:3 numberOfTouches:2];

pinchWithScale:velocity:

捏合手势scale=0~1为捏合、>1为放大,velocity为捏合速度

[windows pinchWithScale:0.2 velocity:-0.05];

当01时,velocity必须大于0,time(s) = scale/velocity。

[img pinchWithScale:0.5 velocity:0.2];

rotate:withVelocity:

旋转手势rotate:要旋转的弧度withVelocity:每秒要旋转的弧度

Rotate和Velocity必须同号顺时针为正向。

[img rotate:2 withVelocity:0.4];

normalizedSliderPosition

只读属性,返回滑块控件中滑块的位置(0~1) adjustToNormalizedSliderPosition:

尽可能让滑块移动到指定的位置(0-1)

adjustToPickerWheelValue:

输入字符串让选择器显示对应内容,如果没有对应内容,返回Fail coordinateWithNormalizedOffset:

根据控件的原点坐标和偏移量来确定一个新坐标

[element coordinateWithNormalizedOffset:CGVectorMake(10,10)]; XCUIApplication API

XCTest新加的类,用于做UI测试,代表被测应用,父类为XCUIElement

launch

启动应用。如果目标应用已运行,首先终止应用,然后再次启动应用。[applaunch];

terminate

关闭应用。

[app terminate];

launchArguments

数组对象,保存启动参数。

NSArray*args=[applaunchArguments];

for(int i=0;i<[argscount];i++){

NSLog(@"arg : %@",[argsobjectAtIndex:i]);

}

launchEnvironment

字典对象,保存启动环境变量

NSDictionary *env =[app launchEnvironment];

for(id key in env){

NSString *object=[env objectForKey:key];

NSLog(@"env : %@",object);

}

XCUIElementAttributesAPI

协议类,XCUIElement遵守的协议

identifier

字符串类型Accessibility ID

NSString *identifier =[app identifier];

.frame

控件的矩形区域

CGRect frame =[app frame];

Value

获取元素的原值

id value =[app value];

placeholderValue

返回元素的占位值

title

标题,String类型

NSString *title =[app title];

label

标签值,String类型

NSString *label =[app label];

elementType

控件类型

XCUIElementType *elementType =[app elementType];

enabled

是否可用,BOOL类型

BOOL*isEnabled =[app isEnabled];

hasFocus

是否具有UI焦点

Selected

是否处于被选中状态

horizontalSizeClass

返回水平尺寸元素

XCUIUserInterfaceSizeClass *horizontalSizeClass =[app horizontalSizeClass];

verticalSizeClass

返回垂直尺寸元素

XCUIUserInterfaceSizeClass *verticalSizeClass =[app verticalSizeClass];

XCUIElementQueryAPI

定位元素的对象,可以理解为存放控件的容器

element

query用element表示形式,如果query中只有一个元素,可以讲element当成真正的element,执行点击等操作,从这一方面来讲XCUIElementQuery其实也是一种XCUIElement对象,只是是用来存放0~N个XCUIElement的容器。得到XCUIElement对象。

count

query中找到的元素数量,得到整数。

allElementsBoundByAccessibilityElement

query中根据accessibility element得到的元素数组。得到XCUIElement数组

allElementsBoundByIndex

query中根据索引值得到的元素数组。得到XCUIElement数组

debugDescription

调试信息

  • 。:

获得传入的索引值所在的元素,返回XCUIElement对象。

elementMatchingPredicate

根据NSPredicate定义的匹配条件查找元素。返回XCUIElement对象。只能从当前对象中查找。更深层次的元素不在查找范围内

elementMatchingType

根据元素类型(XCUIElementType)和id号来匹配查找元素。返回XCUIElement对象。只能从当前对象中查找。更深层次的元素不在查找范围内

descendantsMatchingType

传入XCUIElementType作为匹配条件,得到匹配的XCUIElementQuery对象,查找对象为当前控件的子子孙孙控件。返回XCUIElementQuery对象

childrenMatchingType

传入XCUIElementType作为匹配条件,得到匹配的XCUIElementQuery对象,查找对象为当前控件的子控件。返回XCUIElementQuery对象

matchingPredicate

传入NSPredicate作为过滤器,得到XCUIElementQuery对象。返回XCUIElementQuery对象

matchingType

传入XCUIElementType和id号作为匹配条件,得到XCUIElementQuery。返回XCUIElementQuery对象

matchingIdentifier

传入id号作为匹配条件,得到XCUIElementQuery。返回XCUIElementQuery对象

containingPredicate

传入NSPredicate过滤器作为匹配条件。从子节点中找到包含该条件的XCUIElementQuery 对象

containingType

传入XCUIElementType和id作为匹配条件。从子节点中找到包含该条件的XCUIElementQuery对象。

XCUIElementTypeAPI & XCUIElementTypeQueryProvider API 枚举类,定义了iOS中所有的可用于搜索类型

XCUIElementTypeQueryProvider协议中定义了76个变量,与XCUIElementType定义的枚举元素相比少了3个:Any,Unknown,Application。因为XCUIApplication也遵循该协议,所以Application对象包含XCUIElementTypeQueryProvider定义的所有属性,所以要过滤掉以上三个大于Application的类型。

除了特殊注明的,XCUIElementQuery都是原来类型的复数形式

UIView的定位方式:app.otherElements[@”id”];

2018/9/13 posted in  Xcode

有代理导致的Cocoapods连接失败错误的解决办法

From:https://www.jianshu.com/p/118130ec55cf

pod拒绝连接 遇到如下错误:

错误描述 :
[!] /usr/bin/git clone https://github.com/CocoaPods/Specs.git master --progress

Cloning into 'master'...
fatal: unable to access 'https://github.com/CocoaPods/Specs.git/': Failed to connect to 127.0.0.1 port 1080: Connection refused

第一☝️:
查询是否使用代理: git config --global http.proxy
取消掉代理: git config --global --unset http.proxy

结果我发现不管用,

第二☝️:
查看你的git配置
终端输入: git config --global -l
下面👇就是说的问题咯,

If you have nothing related to https proxy like https_proxy=... the problem is not here.

If you have something related to https proxy then remove it from the file ~/.gitconfig and try again

就是说如果没有使用代理,那可能问题不在代理这里,删除 ~/.gitconfig 这个文件。
(如果没显示快捷键 command+shift+. 显示点开头的隐藏文件)

注意⚠️:这个解决办法是删除pod的配置和设置,我没有找到好的办法所以重新删除了,再重新安装一下pod。
首先进入 ~/.gitconfig 这个文件夹,我直接把这个文件夹里面的设置都删除。,,,参考了这篇

<>如何快速安装Cocoapods
Cocoapods来回下载好多次,下载超级慢,试了好多次都是下载失败,后来看到这篇里面就自己补充写下来了。
先删除干净pod残余

sudo gem uninstall cocoapods

再删除安装过的cocopods相关东西,执行

gem list --local | grep cocoapods
显示如下:
cocoapods (1.0.1)
cocoapods-core (1.0.1)
cocoapods-deintegrate (1.0.1)
cocoapods-downloader (1.1.1)
cocoapods-plugins (1.0.0)
cocoapods-search (1.0.0)
cocoapods-stats (1.0.0)
cocoapods-trunk (1.0.0)
cocoapods-try (1.1.0)

如果出现上述列 使用命令逐个删除,
使用终端输入 sudo gem uninstall 加上 👆列表显示的内容

例如:
sudo gem uninstall cocoapods
sudo gem uninstall cocoapods-core
sudo gem uninstall cocoapods-deintegrate
......等等

然后继续执行一下

sudo rm -rf ~/.cocoa-pod

再执行:

pod repo remove master

//然后执行替换成下面这个索引库的镜像

pod repo add master https://gitcafe.com/akuandev/Specs.git

此时打开 ~/.cocoapods/repos 这个文件之后,看看里面有 master 文件夹没有,没有就手动创建一个。

继续执行

git clone https://git.coding.net/CocoaPods/Specs.git ~/.cocoapods/repos/master

稍等几十秒,就可以看到下载进度了。(之前下载了三次都失败,而且还超级🐢慢,看这个进度比之前的快多了,瞬间心里得到点小安慰😁)

当进度走完之后,继续执行安装

gem install cocoapods

如果使用 oschina 上的镜像
执行 "pod repo add master http://git.oschina.net/akuandev/Specs.git"
替换索引库镜像的 https://gitcafe.com/akuandev/Specs.git 替换成 http://git.oschina.net/akuandev/Specs.git 即可。

pod search AFNetworking

如果搜索到了就安装成功了,

如果搜索报👇下面的错
[!] Unable to find a pod with name, author, summary, or description matching AFNetworking

解决办法
删除cocoapods的索引,执行:

rm ~/Library/Caches/CocoaPods/search_index.json

然后重新search一下,就可以了.

2018/9/6 posted in  Cocoapods

Xcode 插件主题-Dracula

Dracula for Xcode

A dark theme for Xcode.

Screenshot

Install

All instructions can be found at draculatheme.com/xcode.

Team

This theme is maintained by the following person(s) and a bunch of awesome contributors.

Harrison Heck
Harrison Heck

License

MIT License

2018/8/31 posted in  Xcode

InHouse 下载

<?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
    <dict>
        <key>items</key>
        <array>
            <dict>
                <key>assets</key>
                <array>
                    <dict>
                        <key>kind</key>
                        <string>software-package</string>
                        <key>url</key>
                        <string>https://www.xt.top/folder/xtoken-1.0.ipa</string>
                    </dict>
                    <dict>
                        <key>kind</key>
                        <string>full-size-image</string>
                        <key>needs-shine</key>
                        <true/>
                        <key>url</key>
                        <string>https://www.xt.top/folder/images/icon.57x57.png</string>
                    </dict>
                    <dict>
                        <key>kind</key>
                        <string>display-image</string>
                        <key>needs-shine</key>
                        <true/>
                        <key>url</key>
                        <string>https://www.xt.top/folder/images/icon.512x512.png</string>
                    </dict>
                </array>
                <key>metadata</key>
                <dict>
                    <key>bundle-identifier</key>
                    <string>com.biyuan.xtoken</string>
                    <key>bundle-version</key>
                    <string>1.0</string>
                    <key>kind</key>
                    <string>software</string>
                    <key>title</key>
                    <string>XToken</string>
                </dict>
            </dict>
        </array>
    </dict>
    </plist>

然后把 .ipa 和 .plist 文件都上传到支持HTTPS协议的服务器,在网页源码里写入:

<a href="itms-services://?action=download-manifest&url=https://s3-us-west-2.amazonaws.com/folder/appName-version.plist" id="text">Install the In-House App</a> 
2018/7/25 posted in  杂七杂八

时间的格式

NSDateFormatter的作用

//NSString * -> NSDate *
- (nullable NSDate *)dateFromString:(NSString *)string;
//NSDate * -> NSString *
- (NSString *)stringFromDate:(NSDate *)date;

常见的日期格式

http://www.cnblogs.com/mailingfeng/archive/2011/07/28/2120422.html

NSString * -> NSDate *

"2016-10-03 14:01:00"

// 时间字符串
NSString *string = "2016-10-03 14:01:00";

// 日期格式化类
NSDateFormatter *fmt = [[NSDateFormatter alloc] init];
// 设置日期格式(为了转换成功)
fmt.dateFormat = @"yyyy-MM-dd HH:mm:ss";

// NSString * -> NSDate *
NSDate *date = [fmt dateFromString:string];

NSLog(@"%@", date);

10月-03号/2016年 09-10:05秒

// 时间字符串
NSString *string = @"10月-03号/2016年 09-10:05秒";

// 日期格式化类
NSDateFormatter *fmt = [[NSDateFormatter alloc] init];
fmt.dateFormat = @"MM月-dd号/yyyy年 HH-mm:ss秒";

NSLog(@"%@", [fmt dateFromString:string]);

Tue May 31 17:46:55 +0800 2011

// 时间字符串
NSString *string = @"Tue May 31 17:46:55 +0800 2011";

// 日期格式化类
NSDateFormatter *fmt = [[NSDateFormatter alloc] init];
fmt.dateFormat = @"EEE MMM dd HH:mm:ss Z yyyy";
// fmt.dateFormat = @"EEE MMM dd HH:mm:ss ZZZZ yyyy";
// 设置语言区域(因为这种时间是欧美常用时间)
fmt.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"];

NSLog(@"%@", [fmt dateFromString:string]);

1745645645645

// 时间戳 : 从1970年1月1号 00:00:00开始走过的毫秒数

// 时间字符串 - 时间戳
NSString *string = @"1745645645645";
NSTimeInterval second = string.longLongValue / 1000.0;

// 时间戳 -> NSDate *
NSDate *date = [NSDate dateWithTimeIntervalSince1970:second];
NSLog(@"%@", date);

NSCalendar的注意点

#define iOS(version) ([UIDevice currentDevice].systemVersion.doubleValue >= (version))

NSCalendar *calendar = nil;
if ([UIDevice currentDevice].systemVersion.doubleValue >= 8.0) {
    calendar = [NSCalendar calendarWithIdentifier:NSCalendarIdentifierGregorian];
} else {
    calendar = [NSCalendar currentCalendar];
}

NSCalendar *calendar = nil;
if ([NSCalendar respondsToSelector:@selector(calendarWithIdentifier:)]) {
    calendar = [NSCalendar calendarWithIdentifier:NSCalendarIdentifierGregorian];
} else {
    calendar = [NSCalendar currentCalendar];
}

NSDate * -> NSString *

NSDate *date = [NSDate date];

NSDateFormatter *fmt = [[NSDateFormatter alloc] init];
fmt.dateFormat = @"yyyy年MM月dd号 HH:mm:ss";

NSString *string = [fmt stringFromDate:date];

获得日期元素

NSString *string = @"2016-10-03 14:01:00";

NSString *month = [string substringWithRange:NSMakeRange(5, 2)];

NSLog(@"%@", month);
// 时间字符串
NSString *string = @"2016-10-03 14:01:00";

// 日期格式化类
NSDateFormatter *fmt = [[NSDateFormatter alloc] init];
// 设置日期格式(为了转换成功)
fmt.dateFormat = @"yyyy-MM-dd HH:mm:ss";

// NSString * -> NSDate *
NSDate *date = [fmt dateFromString:string];

// 利用NSCalendar处理日期
NSCalendar *calendar = [NSCalendar currentCalendar];
NSInteger month = [calendar component:NSCalendarUnitMonth fromDate:date];
NSInteger hour = [calendar component:NSCalendarUnitHour fromDate:date];
NSInteger minute = [calendar component:NSCalendarUnitMinute fromDate:date];

NSLog(@"%zd %zd %zd", month, hour, minute);
// 时间字符串
NSString *string = @"2016-10-03 14:01:00";

// 日期格式化类
NSDateFormatter *fmt = [[NSDateFormatter alloc] init];
// 设置日期格式(为了转换成功)
fmt.dateFormat = @"yyyy-MM-dd HH:mm:ss";

// NSString * -> NSDate *
NSDate *date = [fmt dateFromString:string];

// 利用NSCalendar处理日期
NSCalendar *calendar = [NSCalendar currentCalendar];

NSCalendarUnit unit = NSCalendarUnitYear | NSCalendarUnitMonth | NSCalendarUnitDay | NSCalendarUnitHour | NSCalendarUnitMinute | NSCalendarUnitSecond;
NSDateComponents *cmps = [calendar components:unit fromDate:date];

// NSLog(@"%zd %zd %zd", cmps.year, cmps.month, cmps.day);
NSLog(@"%@", cmps);
日期比较

// 时间字符串
NSString *createdAtString = @"2016-10-03 14:01:00";
NSDateFormatter *fmt = [[NSDateFormatter alloc] init];
fmt.dateFormat = @"yyyy-MM-dd HH:mm:ss";
NSDate *createdAtDate = [fmt dateFromString:createdAtString];

// 手机当前时间
NSDate *nowDate = [NSDate date];

/**
NSComparisonResult的取值
NSOrderedAscending = -1L, // 升序, 越往右边越大
NSOrderedSame, // 相等
NSOrderedDescending // 降序, 越往右边越小
*/
// 获得比较结果(谁大谁小)
NSComparisonResult result = [nowDate compare:createdAtDate];
if (result == NSOrderedAscending) { // 升序, 越往右边越大
NSLog(@"createdAtDate > nowDate");
} else if (result == NSOrderedDescending) { // 降序, 越往右边越小
NSLog(@"createdAtDate < nowDate");
} else {
NSLog(@"createdAtDate == nowDate");
}
// 时间字符串
NSString *createdAtString = @"2016-10-03 14:01:00";
NSDateFormatter *fmt = [[NSDateFormatter alloc] init];
fmt.dateFormat = @"yyyy-MM-dd HH:mm:ss";
NSDate *createdAtDate = [fmt dateFromString:createdAtString];

// 手机当前时间
// NSDate *nowDate = [NSDate date];

// 获得createdAtDate和nowDate的时间间隔(间隔多少秒)
// NSTimeInterval interval = [nowDate timeIntervalSinceDate:createdAtDate];
NSTimeInterval interval = [createdAtDate timeIntervalSinceNow];
NSLog(@"%f", interval);
NSDateFormatter *fmt = [[NSDateFormatter alloc] init];
fmt.dateFormat = @"yyyy-MM-dd HH:mm:ss";

// 时间字符串
NSString *createdAtString = @"2016-10-03 14:01:00";
NSDate *createdAtDate = [fmt dateFromString:createdAtString];

// 其他时间
NSString *otherString = @"2016-10-03 14:01:00";
NSDate *otherDate = [fmt dateFromString:otherString];

// 获得NSCalendar
NSCalendar *calendar = nil;
if ([NSCalendar respondsToSelector:@selector(calendarWithIdentifier:)]) {
calendar = [NSCalendar calendarWithIdentifier:NSCalendarIdentifierGregorian];
} else {
calendar = [NSCalendar currentCalendar];
}

// 获得日期之间的间隔
NSCalendarUnit unit = NSCalendarUnitYear | NSCalendarUnitMonth | NSCalendarUnitDay | NSCalendarUnitHour | NSCalendarUnitMinute | NSCalendarUnitSecond;
NSDateComponents *cmps = [calendar components:unit fromDate:createdAtDate toDate:otherDate options:0];

NSLog(@"%@", cmps);
条件判断的一些注意点

1.判断一个数组中是否有具体内容
1> 正确
if (array.count) {

}

2> 错误
if (array) {

}

2.判断一个字符串是否有具体内容
1> 正确
if (string.length) {

}

2> 错误
if (string) {

}

2018/7/23 posted in  苹果开发

Objective-C编码规范

原文链接: GitHub
原文作者: raywenderlich.com Team
译文出自: raywenderlich.com Objective-C编码规范

这篇编码风格指南概括了raywenderlich.com的编码规范,可能有些删减或修改。

介绍

我们制定Objective-C编码规范的原因是我们能够在我们的书,教程和初学者工具包的代码保持优雅和一致。即使我们有很多不同的作者来完成不同的书籍。

这里编码规范有可能与你看到的其他Objective-C编码规范不同,因为它主要是为了打印和web的易读性。

关于作者

这编码规范的创建是由很多来自raywenderlich.com团队成员在Nicholas Waynik的带领下共同完成的。团队成员有:Soheil Moayedi Azarpour, Ricardo Rendon Cepeda, Tony Dahbura, Colin Eberhardt, Matt Galloway, Greg Heo, Matthijs Hollemans, Christopher LaPollo, Saul Mora, Andy Pereira, Mic Pringle, Pietro Rea, Cesare Rocchi, Marin Todorov, Nicholas WaynikRay Wenderlich

我们也非常感谢New York TimesRobots & Pencils'Objective-C编码规范的作者。这两个编码规范为本指南的创建提供很好的起点。

背景

这里有些关于编码风格Apple官方文档,如果有些东西没有提及,可以在以下文档来查找更多细节:

目录

语言

应该使用US英语.

应该:

UIColor *myColor = [UIColor whiteColor];

不应该:

UIColor *myColour = [UIColor whiteColor];

代码组织

在函数分组和protocol/delegate实现中使用#pragma mark -来分类方法,要遵循以下一般结构:

#pragma mark - Lifecycle
- (instancetype)init {}
- (void)dealloc {}
- (void)viewDidLoad {}
- (void)viewWillAppear:(BOOL)animated {}
- (void)didReceiveMemoryWarning {}

#pragma mark - Custom Accessors
- (void)setCustomProperty:(id)value {}
- (id)customProperty {}

#pragma mark - IBActions/Event Response
- (IBAction)submitData:(id)sender {}
- (void)someButtonDidPressed:(UIButton*)button

#pragma mark - Protocol conformance
#pragma mark - UITextFieldDelegate
#pragma mark - UITableViewDataSource
#pragma mark - UITableViewDelegate

#pragma mark - Public
- (void)publicMethod {}

#pragma mark - Private
- (void)privateMethod {}

#pragma mark - NSCopying
- (id)copyWithZone:(NSZone *)zone {}

#pragma mark - NSObject
- (NSString *)description {}

空格

  • 缩进使用4个空格,确保在Xcode偏好设置来设置。(raywenderlich.com使用2个空格)
  • 方法大括号和其他大括号(if/else/switch/while 等.)总是在同一行语句打开但在新行中关闭。

应该:

if (user.isHappy) {
    //Do something
} else {
    //Do something else
}

不应该:

if (user.isHappy)
{
  //Do something
}
else {
  //Do something else
}
  • 在方法之间应该有且只有一行,这样有利于在视觉上更清晰和更易于组织。在方法内的空白应该分离功能,但通常都抽离出来成为一个新方法。
  • 优先使用auto-synthesis。但如果有必要,@synthesize@dynamic应该在实现中每个都声明新的一行。
  • 应该避免以冒号对齐的方式来调用方法。因为有时方法签名可能有3个以上的冒号和冒号对齐会使代码更加易读。请不要这样做,尽管冒号对齐的方法包含代码块,因为Xcode的对齐方式令它难以辨认。

应该:

// blocks are easily readable
[UIView animateWithDuration:1.0 animations:^{
  // something
} completion:^(BOOL finished) {
  // something
}];

不应该:

// colon-aligning makes the block indentation hard to read
[UIView animateWithDuration:1.0
                 animations:^{
                     // something
                 }
                 completion:^(BOOL finished) {
                     // something
                 }];

注释

当需要注释时,注释应该用来解释这段特殊代码为什么要这样做。任何被使用的注释都必须保持最新或被删除。

一般都避免使用块注释,因为代码尽可能做到自解释,只有当断断续续或几行代码时才需要注释。例外:这不应用在生成文档的注释

命名

Apple命名规则尽可能坚持,特别是与这些相关的memory management rules (NARC)。

长的,描述性的方法和变量命名是好的。

应该:

UIButton *settingsButton;

不应该:

UIButton *setBut;

三个字符前缀应该经常用在类和常量命名,但在Core Data的实体名中应被忽略。对于官方的raywenderlich.com书、初学者工具包或教程,前缀'RWT'应该被使用。

常量应该使用驼峰式命名规则,所有的单词首字母大写和加上与类名有关的前缀。

应该:

static NSTimeInterval const RWTTutorialViewControllerNavigationFadeAnimationDuration = 0.3;

不应该:

static NSTimeInterval const fadetime = 1.7;

属性也是使用驼峰式,但首单词的首字母小写。对属性使用auto-synthesis,而不是手动编写@ synthesize语句,除非你有一个好的理由。

应该:

@property (strong, nonatomic) NSString *descriptiveVariableName;

不应该:

id varnm;

下划线

当使用属性时,实例变量应该使用self.来访问和改变。这就意味着所有属性将会视觉效果不同,因为它们前面都有self.

但有一个特例:在初始化方法里,实例变量(例如,_variableName)应该直接被使用来避免getters/setters潜在的副作用。

局部变量不应该包含下划线。

方法

在方法签名中,应该在方法类型(-/+ 符号)之后有一个空格。在方法各个段之间应该也有一个空格(符合Apple的风格)。在参数之前应该包含一个具有描述性的关键字来描述参数。

"and"这个词的用法应该保留。它不应该用于多个参数来说明,就像initWithWidth:height以下这个例子:

应该:
objc
- (void)setExampleText:(NSString *)text image:(UIImage *)image;
- (void)sendAction:(SEL)aSelector to:(id)anObject forAllCells:(BOOL)flag;
- (id)viewWithTag:(NSInteger)tag;
- (instancetype)initWithWidth:(CGFloat)width height:(CGFloat)height;

不应该:

-(void)setT:(NSString *)text i:(UIImage *)image;
- (void)sendAction:(SEL)aSelector :(id)anObject :(BOOL)flag;
- (id)taggedView:(NSInteger)tag;
- (instancetype)initWithWidth:(CGFloat)width andHeight:(CGFloat)height;
- (instancetype)initWith:(int)width and:(int)height;  // Never do this.

变量

变量尽量以描述性的方式来命名。单个字符的变量命名应该尽量避免,除了在for()循环。

星号表示变量是指针。例如, NSString *text 既不是 NSString* text 也不是 NSString * text,除了一些特殊情况下常量。

私有变量 应该尽可能代替实例变量的使用。尽管使用实例变量是一种有效的方式,但更偏向于使用属性来保持代码一致性。

通过使用'back'属性(_variable,变量名前面有下划线)直接访问实例变量应该尽量避免,除了在初始化方法(init, initWithCoder:, 等…),dealloc 方法和自定义的setters和getters。想了解关于如何在初始化方法和dealloc直接使用Accessor方法的更多信息,查看这里

应该:

@interface RWTTutorial : NSObject

@property (strong, nonatomic) NSString *tutorialName;

@end

不应该:

@interface RWTTutorial : NSObject {
  NSString *tutorialName;
}

属性特性

所有属性特性应该显式地列出来,有助于新手阅读代码。属性特性的顺序应该是storage、atomicity,与在Interface Builder连接UI元素时自动生成代码一致。

应该:

@property (weak, nonatomic) IBOutlet UIView *containerView;
@property (strong, nonatomic) NSString *tutorialName;

不应该:

@property (nonatomic, weak) IBOutlet UIView *containerView;
@property (nonatomic) NSString *tutorialName;

NSString应该使用copy 而不是 strong的属性特性。

为什么?即使你声明一个NSString的属性,有人可能传入一个NSMutableString的实例,然后在你没有注意的情况下修改它。

应该:

@property (copy, nonatomic) NSString *tutorialName;

不应该:

@property (strong, nonatomic) NSString *tutorialName;

点符号语法

点语法是一种很方便封装访问方法调用的方式。当你使用点语法时,通过使用getter或setter方法,属性仍然被访问或修改。想了解更多,阅读这里

点语法应该总是被用来访问和修改属性,因为它使代码更加简洁。[]符号更偏向于用在其他例子。

应该:
objc
NSInteger arrayCount = [self.array count];
view.backgroundColor = [UIColor orangeColor];
[UIApplication sharedApplication].delegate;

不应该:
objc
NSInteger arrayCount = self.array.count;
[view setBackgroundColor:[UIColor orangeColor]];
UIApplication.sharedApplication.delegate;

字面值

NSString, NSDictionary, NSArray, 和 NSNumber的字面值应该在创建这些类的不可变实例时被使用。请特别注意nil值不能传入NSArrayNSDictionary字面值,因为这样会导致crash。

应该:

NSArray *names = @[@"Brian", @"Matt", @"Chris", @"Alex", @"Steve", @"Paul"];
NSDictionary *productManagers = @{@"iPhone": @"Kate", @"iPad": @"Kamal", @"Mobile Web": @"Bill"};
NSNumber *shouldUseLiterals = @YES;
NSNumber *buildingStreetNumber = @10018;

不应该:

NSArray *names = [NSArray arrayWithObjects:@"Brian", @"Matt", @"Chris", @"Alex", @"Steve", @"Paul", nil];
NSDictionary *productManagers = [NSDictionary dictionaryWithObjectsAndKeys: @"Kate", @"iPhone", @"Kamal", @"iPad", @"Bill", @"Mobile Web", nil];
NSNumber *shouldUseLiterals = [NSNumber numberWithBool:YES];
NSNumber *buildingStreetNumber = [NSNumber numberWithInteger:10018];

常量

常量是容易重复被使用和无需通过查找和代替就能快速修改值。常量应该使用static来声明而不是使用#define,除非显式地使用宏。

应该:

static NSString * const RWTAboutViewControllerCompanyName = @"RayWenderlich.com";

static CGFloat const RWTImageThumbnailHeight = 50.0;

不应该:

#define CompanyName @"RayWenderlich.com"

#define thumbnailHeight 2

枚举类型

当使用enum时,推荐使用新的固定基本类型规格,因为它有更强的类型检查和代码补全。现在SDK有一个宏NS_ENUM()来帮助和鼓励你使用固定的基本类型。

例如:

typedef NS_ENUM(NSInteger, RWTLeftMenuTopItemType) {
  RWTLeftMenuTopItemMain,
  RWTLeftMenuTopItemShows,
  RWTLeftMenuTopItemSchedule
};

你也可以显式地赋值(展示旧的k-style常量定义):

typedef NS_ENUM(NSInteger, RWTGlobalConstants) {
  RWTPinSizeMin = 1,
  RWTPinSizeMax = 5,
  RWTPinCountMin = 100,
  RWTPinCountMax = 500,
};

旧的k-style常量定义应该避免除非编写Core Foundation C的代码。

不应该:

enum GlobalConstants {
  kMaxPinSize = 5,
  kMaxPinCount = 500,
};

Case语句

大括号在case语句中并不是必须的,除非编译器强制要求。当一个case语句包含多行代码时,大括号应该加上。

switch (condition) {
  case 1:
    // ...
    break;
  case 2: {
    // ...
    // Multi-line example using braces
    break;
  }
  case 3:
    // ...
    break;
  default: 
    // ...
    break;
}

有很多次,当相同代码被多个cases使用时,一个fall-through应该被使用。一个fall-through就是在case最后移除'break'语句,这样就能够允许执行流程跳转到下一个case值。为了代码更加清晰,一个fall-through需要注释一下。

switch (condition) {
  case 1:
    // ** fall-through! **
  case 2:
    // code executed for values 1 and 2
    break;
  default: 
    // ...
    break;
}

当在switch使用枚举类型时,'default'是不需要的。例如:

RWTLeftMenuTopItemType menuType = RWTLeftMenuTopItemMain;

switch (menuType) {
  case RWTLeftMenuTopItemMain:
    // ...
    break;
  case RWTLeftMenuTopItemShows:
    // ...
    break;
  case RWTLeftMenuTopItemSchedule:
    // ...
    break;
}

私有属性

私有属性应该在类的实现文件中的类扩展(匿名分类)中声明,命名分类(比如RWTPrivateprivate)应该从不使用除非是扩展其他类。匿名分类应该通过使用+Private.h文件的命名规则暴露给测试。

例如:

@interface RWTDetailViewController ()

@property (strong, nonatomic) GADBannerView *googleAdView;
@property (strong, nonatomic) ADBannerView *iAdView;
@property (strong, nonatomic) UIWebView *adXWebView;

@end

布尔值

Objective-C使用YESNO。因为truefalse应该只在CoreFoundation,C或C++代码使用。既然nil解析成NO,所以没有必要在条件语句比较。不要拿某样东西直接与YES比较,因为YES被定义为1和一个BOOL能被设置为8位。

这是为了在不同文件保持一致性和在视觉上更加简洁而考虑。

应该:

if (someObject) {}
if (![anotherObject boolValue]) {}

不应该:

if (someObject == nil) {}
if ([anotherObject boolValue] == NO) {}
if (isAwesome == YES) {} // Never do this.
if (isAwesome == true) {} // Never do this.

如果BOOL属性的名字是一个形容词,属性就能忽略"is"前缀,但要指定get访问器的惯用名称。例如:

@property (assign, getter=isEditable) BOOL editable;

文字和例子从这里引用Cocoa Naming Guidelines

条件语句

条件语句主体为了防止出错应该使用大括号包围,即使条件语句主体能够不用大括号编写(如,只用一行代码)。这些错误包括添加第二行代码和期望它成为if语句;还有,even more dangerous defect可能发生在if语句里面一行代码被注释了,然后下一行代码不知不觉地成为if语句的一部分。除此之外,这种风格与其他条件语句的风格保持一致,所以更加容易阅读。

应该:

if (!error) {
  return success;
}

不应该:

if (!error)
  return success;

if (!error) return success;

三元操作符

当需要提高代码的清晰性和简洁性时,三元操作符?:才会使用。单个条件求值常常需要它。多个条件求值时,如果使用if语句或重构成实例变量时,代码会更加易读。一般来说,最好使用三元操作符是在根据条件来赋值的情况下。

Non-boolean的变量与某东西比较,加上括号()会提高可读性。如果被比较的变量是boolean类型,那么就不需要括号。

应该:

NSInteger value = 5;
result = (value != 0) ? x : y;

BOOL isHorizontal = YES;
result = isHorizontal ? x : y;

不应该:

result = a > b ? x = c > d ? c : d : y;

Init方法

Init方法应该遵循Apple生成代码模板的命名规则。返回类型应该使用instancetype而不是id

- (instancetype)init {
  self = [super init];
  if (self) {
    // ...
  }
  return self;
}

查看关于instancetype的文章Class Constructor Methods

类构造方法

当类构造方法被使用时,它应该返回类型是instancetype而不是id。这样确保编译器正确地推断结果类型。

@interface Airplane
+ (instancetype)airplaneWithType:(RWTAirplaneType)type;
@end

关于更多instancetype信息,请查看NSHipster.com

CGRect函数

当访问CGRect里的x, y, width, 或 height时,应该使用CGGeometry函数而不是直接通过结构体来访问。引用Apple的CGGeometry:

在这个参考文档中所有的函数,接受CGRect结构体作为输入,在计算它们结果时隐式地标准化这些rectangles。因此,你的应用程序应该避免直接访问和修改保存在CGRect数据结构中的数据。相反,使用这些函数来操纵rectangles和获取它们的特性。

应该:

CGRect frame = self.view.frame;

CGFloat x = CGRectGetMinX(frame);
CGFloat y = CGRectGetMinY(frame);
CGFloat width = CGRectGetWidth(frame);
CGFloat height = CGRectGetHeight(frame);
CGRect frame = CGRectMake(0.0, 0.0, width, height);

不应该:

CGRect frame = self.view.frame;

CGFloat x = frame.origin.x;
CGFloat y = frame.origin.y;
CGFloat width = frame.size.width;
CGFloat height = frame.size.height;
CGRect frame = (CGRect){ .origin = CGPointZero, .size = frame.size };

黄金路径

当使用条件语句编码时,左手边的代码应该是"golden" 或 "happy"路径。也就是不要嵌套if语句,多个返回语句也是OK。

应该:

- (void)someMethod {
  if (![someOther boolValue]) {
    return;
  }

  //Do something important
}

不应该:

- (void)someMethod {
  if ([someOther boolValue]) {
    //Do something important
  }
}

错误处理

当方法通过引用来返回一个错误参数,判断返回值而不是错误变量。

应该:

NSError *error;
if (![self trySomethingWithError:&error]) {
  // Handle Error
}

不应该:

NSError *error;
[self trySomethingWithError:&error];
if (error) {
  // Handle Error
}

在成功的情况下,有些Apple的APIs记录垃圾值(garbage values)到错误参数(如果non-NULL),那么判断错误值会导致false负值和crash。

单例模式

单例对象应该使用线程安全模式来创建共享实例。

+ (instancetype)sharedInstance {
  static id sharedInstance = nil;

  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    sharedInstance = [[self alloc] init];
  });

  return sharedInstance;
}

这会防止possible and sometimes prolific crashes.

换行符

换行符是一个很重要的主题,因为它的风格指南主要为了打印和网上的可读性。

例如:

self.productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:productIdentifiers];

一行很长的代码应该分成两行代码,下一行用两个空格隔开。

self.productsRequest = [[SKProductsRequest alloc] 
  initWithProductIdentifiers:productIdentifiers];

Xcode工程

物理文件应该与Xcode工程文件保持同步来避免文件扩张。任何Xcode分组的创建应该在文件系统的文件体现。代码不仅是根据类型来分组,而且还可以根据功能来分组,这样代码更加清晰。

尽可能在target的Build Settings打开"Treat Warnings as Errors,和启用以下additional warnings。如果你需要忽略特殊的警告,使用 Clang's pragma feature

其他Objective-C编码规范

如果我们的编码规范不符合你的口味,可以查看其他的编码规范:

2018/7/3 posted in  Program

使用Objective-C实现自定义的RunLoop

//
//  main.m
//  ZCRunLoop
//
//  Created by Zenny Chen on 2016/11/21.
//  Copyright © 2016年 GreenGames Studio. All rights reserved.
//

@import Foundation;

#include <sys/event.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/time.h>
#include <unistd.h>
#include <semaphore.h>

#include <stdbool.h>
#include <stdint.h>
#include <stdatomic.h>
#include <stdalign.h>


#if defined(__i386__) || defined(__x86_64__)
#define CPU_PAUSE()     asm("pause")
#elif defined(__arm__) || defined(__arm64__)
#define CPU_PAUSE()     asm("yield")
#else
#define CPU_PAUSE()
#endif


#pragma mark - ZCEvent

/** 自己定制的事件类 */
@interface ZCEvent : NSObject

/** 事件响应时,将消息所发送给的目标 */
@property (nonatomic, retain) id target;

/** 事件响应时,对目标所发送的消息,这里使用NSValue其实是对SEL类型的封装 */
@property (nonatomic, retain) NSValue *message;

/** 将消息发送给目标时,随带的用户自定义参数 */
@property (nonatomic, retain) id parameter;

@end

@implementation ZCEvent

@synthesize target, message, parameter;

- (void)dealloc
{
    self.target = nil;
    self.message = nil;
    self.parameter = nil;
    
    [super dealloc];
}

@end


#pragma mark - ZCTimerEvent

/** 自己定制的定时器事件类 */
@interface ZCTimerEvent : ZCEvent
{
@public
    
    /** 当前定时器到期时间 */
    struct timeval expireDate;
}

@end

@implementation ZCTimerEvent

@end


#pragma mark - ZCRunLoop

/** 自己定制的消息循环类 */
@interface ZCRunLoop : NSObject

/** 获取ZCRunLoop单例实例对象 */
+ (instancetype)runLoop;

/**
 * 添加定时器事件
 * @param target 消息接收者
 * @param selector 消息签名
 * @param param 用户自定义参数
 * @param timeoutInterval 超时时间,单位为秒
 */
- (void)addTimerEvent:(id)target message:(SEL)selector userParam:(id)param timeout:(NSTimeInterval)timeoutInterval;

/**
 * 添加消息事件,用于在当前消息队列中处理
 * @param target 消息接收者
 * @param selector 消息签名
 * @param param 用户自定义参数
 */
- (void)addMessageEvent:(id)target message:(SEL)selector param:(id)param;

/** 运行消息循环 */
- (void)run;

@end

static ZCRunLoop *sMainRunLoop;

@implementation ZCRunLoop
{
@private
    
    /** 用于对时间事件队列操作的循环锁的原子标志 */
    atomic_bool alignas(64) mTimerEventFlag;
    
    /** 定时器事件队列 */
    NSMutableArray<ZCTimerEvent*> *mTimerEvents;
    
    /** 用于即将处理的定时器事件队列 */
    NSArray<ZCTimerEvent*> *mTimerEventsForProcessing;
    
    /** 消息事件队列 */
    NSMutableArray<ZCEvent*> *mMessageEvents;
    
    /** 信号量,当当前没有消息时,将当前线程阻塞 */
    sem_t *mSemaphore;
    
    /** 当前即将到期的事件索引 */
    int mMinimumIntervalTimeEventIndex;
    
    /** 用于标识当前消息循环是否即将退出 */
    volatile BOOL mWillBeTerminated;
    
    /** 用于对消息事件队列操作的循环锁的原子标志 */
    atomic_bool alignas(64) mMessageEventFlag;
}

+ (instancetype)runLoop
{
    return sMainRunLoop;
}

/** 定时器响应函数 */
static void alarm_wakeup(int i)
{
    [[ZCRunLoop runLoop] addCurrentTimerEventsToProcess];
}

- (instancetype)init
{
    self = [super init];
    
    atomic_init(&mTimerEventFlag, true);
    atomic_init(&mMessageEventFlag, true);
    
    mTimerEvents = [[NSMutableArray alloc] initWithCapacity:128];
    mMessageEvents = [[NSMutableArray alloc] initWithCapacity:128];
    mMinimumIntervalTimeEventIndex = -1;
    mSemaphore = sem_open("My semaphore", O_CREAT, S_IRUSR | S_IWUSR, 0);
    
    signal(SIGALRM, alarm_wakeup);
    
    return self;
}

- (void)dealloc
{
    [mTimerEvents removeAllObjects];
    [mTimerEvents release];
    
    [mMessageEvents removeAllObjects];
    [mMessageEvents release];
    
    if(mTimerEventsForProcessing != nil)
        [mTimerEventsForProcessing release];
    
    sem_close(mSemaphore);
    
    NSLog(@"ZCRunLoop deallocated!");
    
    [super dealloc];
}

- (void)addMessageEvent:(id)target message:(SEL)selector param:(id)param
{
    ZCEvent *event = [ZCEvent new];
    event.target = target;
    event.message = [NSValue valueWithPointer:selector];
    event.parameter = param;
    
    while(!atomic_exchange(&mMessageEventFlag, false))
        CPU_PAUSE();
    
    [mMessageEvents addObject:event];
    [event release];
    
    atomic_store(&mMessageEventFlag, true);
    
    sem_post(mSemaphore);
}

- (void)addTimerEvent:(id)target message:(SEL)selector userParam:(id)param timeout:(NSTimeInterval)timeoutInterval
{
    ZCTimerEvent *newEvent = [ZCTimerEvent new];
    newEvent.target = target;
    newEvent.message = [NSValue valueWithPointer:selector];
    newEvent.parameter = param;
    
    struct timeval specDate;
    
    typeof(specDate.tv_sec) secInterval = (typeof(specDate.tv_sec))timeoutInterval;
    typeof(specDate.tv_usec) usecInterval = (timeoutInterval - (double)secInterval) * 1000000.0;
    specDate.tv_sec = secInterval;
    specDate.tv_usec = usecInterval;
    
    // 每添加了一个新的事件,说明当前run-loop一直会处于运行状态
    mWillBeTerminated = NO;
    
    // 上旋锁
    while(!atomic_exchange(&mTimerEventFlag, false))
        CPU_PAUSE();
    
    struct timeval currTime;
    gettimeofday(&currTime, NULL);
    
    // 将specDate设置为到期日期,根据指定的超时时间
    timeradd(&specDate, &currTime, &specDate);
    
    newEvent->expireDate = specDate;
    
    [mTimerEvents addObject:newEvent];
    [newEvent release];
    
    struct itimerval tout_val;
    tout_val.it_interval.tv_sec = 0;
    tout_val.it_interval.tv_usec = 0;
    tout_val.it_value.tv_sec = 0;
    tout_val.it_value.tv_usec = 0;
    
    if(mMinimumIntervalTimeEventIndex == -1)
    {
        // 用于处理加入第一个事件的时候
        mMinimumIntervalTimeEventIndex = 0;
        tout_val.it_value.tv_sec = secInterval;
        tout_val.it_value.tv_usec = usecInterval;
    }
    else
    {
        ZCTimerEvent *minEvent = mTimerEvents[mMinimumIntervalTimeEventIndex];
        
        // 将当前离到期日期最近的日期与新添加的到期日期进行比较
        if(timercmp(&minEvent->expireDate, &specDate, >))
        {
            // 倘若当前离到期日期最近的日期比新添加的到期日期要大,
            // 那么将新添加的到期日期作为最小超时时间,并重新设定定时器的值
            mMinimumIntervalTimeEventIndex = (int)(mTimerEvents.count - 1);
            tout_val.it_value.tv_sec = secInterval;
            tout_val.it_value.tv_usec = usecInterval;
        }
    }
    
    if((tout_val.it_value.tv_sec > 0 || tout_val.it_value.tv_usec > 0))
        setitimer(ITIMER_REAL, &tout_val, NULL);
    
    // 解旋锁
    atomic_store(&mTimerEventFlag, true);
}

- (void)addCurrentTimerEventsToProcess
{
    // 上旋锁
    while(!atomic_exchange(&mTimerEventFlag, false))
        CPU_PAUSE();
    
    if(mTimerEventsForProcessing != nil)
        [mTimerEventsForProcessing release];
    
    mTimerEventsForProcessing = [[NSArray alloc] initWithArray:mTimerEvents];
    
    // 解旋锁
    atomic_store(&mTimerEventFlag, true);
}

/** 处理定时器事件 */
- (void)processTimerHandler
{
    struct timeval currTime;
    gettimeofday(&currTime, NULL);
    
    @autoreleasepool {
        
        NSMutableArray *clearArray = [NSMutableArray arrayWithCapacity:128];
        
        // 遍历每一个事件,看看当前是否即将或已经到期的事件,整个处理过程无需上锁
        for(ZCTimerEvent *event in mTimerEventsForProcessing)
        {
            struct timeval eventTime;
            timersub(&event->expireDate, &currTime, &eventTime);
            
            // 计算当前事件的到期日期离当前日期相差多少微秒
            __auto_type interval = eventTime.tv_sec * 1000000L + eventTime.tv_usec;
            
            // 这里设定小于10微秒的事件作为到期时间
            if(interval < 10)
            {
                // 执行相应的消息发送
                [event.target performSelector:(SEL)[event.message pointerValue] withObject:event.parameter];
                
                // 准备将当前事件移除
                [clearArray addObject:event];
            }
        }
        
        [mTimerEventsForProcessing release];
        mTimerEventsForProcessing = nil;
        
        // 上旋锁
        while(!atomic_exchange(&mTimerEventFlag, false))
            CPU_PAUSE();
        
        [mTimerEvents removeObjectsInArray:clearArray];
        
        const NSUInteger length = mTimerEvents.count;
        
        // 如果事件队列中还存有事件,那么挑选出最小的到期时间,并重新设置定时器
        if(length > 0)
        {
            mMinimumIntervalTimeEventIndex = 0;
            struct timeval minimumTime = mTimerEvents[0]->expireDate;
            
            for(int i = 1; i < length; i++)
            {
                if(timercmp(&minimumTime, &mTimerEvents[i]->expireDate, >))
                {
                    mMinimumIntervalTimeEventIndex = i;
                    minimumTime = mTimerEvents[i]->expireDate;
                }
            }
            
            struct itimerval tout_val;
            tout_val.it_interval.tv_sec = 0;
            tout_val.it_interval.tv_usec = 0;
            timersub(&minimumTime, &currTime, &tout_val.it_value);
            
            setitimer(ITIMER_REAL, &tout_val, NULL);
        }
        else    // 否则即将退出当前消息循环
            mWillBeTerminated = YES;
    }
    
    // 解旋锁
    atomic_store(&mTimerEventFlag, true);
}

- (void)processMessages
{
    while(!atomic_exchange(&mMessageEventFlag, false))
        CPU_PAUSE();
    
    // 处理当前每一个消息事件
    for(ZCEvent *event in mMessageEvents)
    {
        // 将消息事件放到定时器事件队列中处理,默认延迟100微秒
        [self addTimerEvent:event.target message:event.message.pointerValue userParam:event.parameter timeout:0.0001];
    }
    
    // 最后将所有消息事件全都移除
    [mMessageEvents removeAllObjects];
    
    atomic_store(&mMessageEventFlag, true);
}

- (void)run
{
    // 如果当前不退出消息循环,则挂起当前线程
    while(!mWillBeTerminated)
    {
        sem_wait(mSemaphore);
        
        // 处理当前的定时器消息
        [self processTimerHandler];
        
        // 唤醒一次之后处理所有消息事件
        [self processMessages];
    }
}

@end


#pragma mark - ZCObject

@interface ZCObject : NSObject

@end

@implementation ZCObject

- (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay
{
    [[ZCRunLoop runLoop] addTimerEvent:self message:aSelector userParam:anArgument timeout:delay];
}

- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg
{
    // 延迟100微秒在主线程的run-loop中发送消息
    [[ZCRunLoop runLoop] addMessageEvent:self message:aSelector param:arg];
}

@end

#pragma mark - test

@interface MyObject : ZCObject

- (void)hello:(NSNumber*)delaySeconds;
- (void)hey:(NSNumber*)delaySeconds;

@end


@implementation MyObject

- (void)hello:(NSNumber*)delaySeconds
{
    NSLog(@"Hello, world! delayed: %@ seconds", delaySeconds);
}

- (void)hey:(NSNumber*)delaySeconds
{
    dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^void(void) {
        [self performSelectorOnMainThread:@selector(hello:) withObject:delaySeconds];
    });
}

- (void)hi:(NSNumber*)delaySeconds
{
    NSLog(@"Hi, The following operation will be delayed for %@ seconds", delaySeconds);
    
    [self performSelector:@selector(hello:) withObject:delaySeconds afterDelay:delaySeconds.doubleValue];
}

- (void)dealloc
{
    NSLog(@"MyObject deallocated!");
    
    [super dealloc];
}

@end


int main(int argc, const char * argv[])
{
    sMainRunLoop = [ZCRunLoop new];
    
    MyObject *obj = [MyObject new];
    [obj performSelector:@selector(hello:) withObject:@2.0 afterDelay:2.0];
    
    [obj performSelector:@selector(hello:) withObject:@1.5 afterDelay:1.5];
    
    [obj performSelector:@selector(hello:) withObject:@5.0 afterDelay:5.0];
    
    [obj performSelector:@selector(hello:) withObject:@4.0 afterDelay:4.0];
    
    [obj performSelector:@selector(hello:) withObject:@3.0 afterDelay:3.0];
    
    [obj performSelector:@selector(hi:) withObject:@8.5 afterDelay:8.5];
    
    [obj performSelector:@selector(hey:) withObject:@7.75 afterDelay:7.75];
    
    [obj performSelector:@selector(hello:) withObject:@22.0 afterDelay:22.0];
    
    [obj performSelectorOnMainThread:@selector(hey:) withObject:@0.1];
    
    dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^void(void) {
        [obj performSelectorOnMainThread:@selector(hello:) withObject:@6.5];
        [obj performSelectorOnMainThread:@selector(hello:) withObject:@6.7];
    });
    
    [obj release];
    
    NSLog(@"Running...");
    
    [[ZCRunLoop runLoop] run];
    
    [sMainRunLoop release];
    
    return 0;
}

2018/6/19 posted in  RunLoop