HTTP状态码——405 vs 501

HTTP状态码中有一些比较相似的状态码,容易让人产生混淆。其中比较冷门的一组是405(Method Not Allowed)与501(Not Implemented)。一个请求如果请求的方法不支持,不也算是没有实现么?这两者有何区别?我们就来仔细分析一下。

状态码的定义

说起状态码定义,最具权威性的就是rfc-2616 文档了:

6.5.5. 405 Method Not Allowed
The 405 (Method Not Allowed) status code indicates that the method received in the request-line is known by the origin server but not supported by the target resource. The origin server MUST generate an Allow header field in a 405 response containing a list of the target resource’s currently supported methods.

405状态码表示请求中的方法(回忆一下,HTTP请求行由3部分组成,请求方法、请求资源以及请求版本号,例如:GET /some/amazing/resource HTTP/1.1)目标服务器认识,但是对于这次请求的资源,服务器无法用请求的方法响应。同时协议要求,响应头重必须包含Allow头,返回支持对应资源的请求方法。

6.6.2. 501 Not Implemented
The 501 (Not Implemented) status code indicates that the server does not support the functionality required to fulfill the request. This is the appropriate response when the server does not recognize the request method and is not capable of supporting it for any resource.

501状态码则表示目的服务器不认识你请求的方法,对所有请求资源,也都无法响应了。

比较

仔细比较有细微差别。

405

405是说,这个请求方法(GET、PUT、DELETE)我们api server认识,但是对于你请求的资源(URI),你使用的方法我们不支持,这叫做方法不允许,同时response要包含一个Allow头,返回支持的HTTP请求方法。

举个例子,参考AWS S3对象生命周期(lifecycle)相关请求,这类资源只支持GET、PUT、DELETE方法,如果我们以HEAD方法请求lifecycle的资源,会返回以下错误:(注意Allow这个响应头)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
curl -vvv -I "https://s3-ap-southeast-1.amazonaws.com/test-voidmain-method/?lifecycle"
* Trying 52.219.40.137...
* TCP_NODELAY set
* Connected to s3-ap-southeast-1.amazonaws.com (52.219.40.137) port 443 (#0)
* TLS 1.2 connection using TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
* Server certificate: *.s3-ap-southeast-1.amazonaws.com
* Server certificate: DigiCert Baltimore CA-2 G2
* Server certificate: Baltimore CyberTrust Root
> HEAD /test-voidmain-method/?lifecycle HTTP/1.1
> Host: s3-ap-southeast-1.amazonaws.com
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 405 Method Not Allowed
HTTP/1.1 405 Method Not Allowed
< x-amz-request-id: 5ECA8F9A66AC236B
x-amz-request-id: 5ECA8F9A66AC236B
< x-amz-id-2: mM6O8oq7yytIjjg+CtB94SN09CZjZE975f1cIhLPzRlFmJEYclANCVxnzgyoAMdd3SBsm2uqBnQ=
x-amz-id-2: mM6O8oq7yytIjjg+CtB94SN09CZjZE975f1cIhLPzRlFmJEYclANCVxnzgyoAMdd3SBsm2uqBnQ=
< Allow: DELETE, GET, PUT
Allow: DELETE, GET, PUT
< Content-Type: application/xml
Content-Type: application/xml
< Transfer-Encoding: chunked
Transfer-Encoding: chunked
< Date: Sat, 30 Sep 2017 03:31:57 GMT
Date: Sat, 30 Sep 2017 03:31:57 GMT
< Server: AmazonS3
Server: AmazonS3
<
* Connection #0 to host s3-ap-southeast-1.amazonaws.com left intact

需要特别注意的是,响应头中的Allow头返回了GET、PUT和DELETE方法,也就是所有本资源支持的HTTP方法。

501

501是说,你的请求方法(例如PROPFIND方法),我们api server不认识(无法进行匹配),因此叫做未实现。
返回501主要是受限制于API Server实现。公司使用的jetty版本,只支持 rfc-2068 中定义的请求方法,因此使用PROPFIND方法请求时,就会返回501。例如:(考虑到保密等信息,隐藏掉了具体的域名和端口以及IP)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
curl -vvv -X PROPFIND "http://example.com"
* Rebuilt URL to: http://example.com/
* Trying xx.xx.xx.xx...
* TCP_NODELAY set
* Connected to example.com (xx.xx.xx.xx) port 80 (#0)
> ASDFASDF / HTTP/1.1
> Host: example.com
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 501 Method PROPFIND is not defined in RFC 2068 and is not supported by the Servlet API
< Server: XXX
< Date: Sat, 30 Sep 2017 03:12:55 GMT
< Content-Length: 0
< Connection: keep-alive
< X-Application-Context: application
<
* Connection #0 to host example.com left intact

这个行为是jetty完成的:

1
2
3
4
5
6
7
8
9
10
11
12
13
10:51:05.498 415559652 [6061] [qtp2047899112-1676226 - /] DEBUG o.e.jetty.server.Server REQUEST PROPFIND / on HttpChannelOverHttp@55741056{r=1,c=false,a=DISPATCHED,uri=/}
10:51:05.499 415559653 [6061] [qtp2047899112-1676226 - /] DEBUG o.e.j.s.h.ContextHandler scope null||/ @ o.s.b.c.e.j.JettyEmbeddedWebAppContext@4d56b779{/,null,AVAILABLE}
10:51:05.499 415559653 [6061] [qtp2047899112-1676226 - /] DEBUG o.e.j.s.h.ContextHandler context=||/ @ o.s.b.c.e.j.JettyEmbeddedWebAppContext@4d56b779{/,null,AVAILABLE}
10:51:05.499 415559653 [6061] [qtp2047899112-1676226 - /] DEBUG o.e.jetty.server.session sessionManager=org.eclipse.jetty.server.session.HashSessionManager@6011c7cf
10:51:05.499 415559653 [6061] [qtp2047899112-1676226 - /] DEBUG o.e.jetty.server.session session=null
10:51:05.499 415559653 [6061] [qtp2047899112-1676226 - /] DEBUG o.e.j.s.ServletHandler servlet ||/ -> dispatcherServlet@7ef5559e==org.springframework.web.servlet.DispatcherServlet,-1,true
10:51:05.499 415559653 [6061] [qtp2047899112-1676226 - /] DEBUG o.e.j.s.ServletHandler chain=apiFilter->applicationContextIdFilter->webRequestLoggingFilter->dispatcherServlet@7ef5559e==org.springframework.web.servlet.DispatcherServlet,-1,true
10:51:05.499 415559653 [6061] [qtp2047899112-1676226 - /] DEBUG o.e.j.s.ServletHandler call filter apiFilter
10:51:05.499 415559653 [6061] [qtp2047899112-1676226 - /] DEBUG o.e.j.s.ServletHandler call filter applicationContextIdFilter
10:51:05.499 415559653 [6061] [qtp2047899112-1676226 - /] DEBUG o.e.j.s.ServletHandler call filter webRequestLoggingFilter
10:51:05.499 415559653 [6061] [qtp2047899112-1676226 - /] DEBUG o.e.j.s.ServletHandler call servlet dispatcherServlet@7ef5559e==org.springframework.web.servlet.DispatcherServlet,-1,true
10:51:05.499 415559653 [6061] [qtp2047899112-1676226 - /] DEBUG o.e.j.s.HttpConnection org.eclipse.jetty.server.HttpConnection$SendCallback@4521a8a2[PROCESSING][i=ResponseInfo{HTTP/1.0 501 Method PROPFIND is not defined in RFC 2068 and is not supported by the Servlet API ,-1,false},cb=org.eclipse.jetty.server.HttpChannel$CommitCallback@24c64723] generate: NEED_HEADER (null,[p=0,l=0,c=0,r=0],true)@START
10:51:05.499 415559653 [6061] [qtp2047899112-1676226 - /] DEBUG o.e.j.s.HttpConnection org.eclipse.jetty.server.HttpConnection$SendCallback@4521a8a2[PROCESSING][i=ResponseInfo{HTTP/1.0 501 Method PROPFIND is not defined in RFC 2068 and is not supported by the Servlet API ,-1,false},cb=org.eclipse.jetty.server.HttpChannel$CommitCallback@24c64723] generate: FLUSH ([p=0,l=227,c=8192,r=227],[p=0,l=0,c=0,r=0],true)@COMPLETING

但是AWS并不是这么实现的:(AWS返回400)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
curl -vvv -X ROPFAS "https://s3-ap-southeast-1.amazonaws.com/test-voidmain-method"
* Trying 52.219.32.101...
* TCP_NODELAY set
* Connected to s3-ap-southeast-1.amazonaws.com (52.219.32.101) port 443 (#0)
* TLS 1.2 connection using TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
* Server certificate: *.s3-ap-southeast-1.amazonaws.com
* Server certificate: DigiCert Baltimore CA-2 G2
* Server certificate: Baltimore CyberTrust Root
> ROPFAS /test-voidmain-method HTTP/1.1
> Host: s3-ap-southeast-1.amazonaws.com
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 400 Bad Request
< Date: Sat, 30 Sep 17 03:20:19 GMT
< Connection: close
< Transfer-Encoding: chunked
< x-amz-id-2: KMMj8crIsT1SPVKbK5sVnuxbqkNSMbzq4XHVAfIA9V6BUuzYIR7AX5XXeInRgJg9tlnaE9wO2ba0SvIIB/VSebPu87jR9Q5o
< x-amz-request-id: 69FAB6DEE37AC9B9
< Content-Type: application/xml
<
* Closing connection 0
<?xml version="1.0" encoding="UTF-8"?><Error><Code>BadRequest</Code><Message>An error occurred when parsing the HTTP request.</Message><RequestId>69FAB6DEE37AC9B9</RequestId><HostId>KMMj8crIsT1SPVKbK5sVnuxbqkNSMbzq4XHVAfIA9V6BUuzYIR7AX5XXeInRgJg9tlnaE9wO2ba0SvIIB/VSebPu87jR9Q5o</HostId></Error>

另外值得讨论的一点是,如果我们用一个Jetty认识(在 rfc-2068 中定义),但API不支持(没有注册requestMapper)的请求方法访问API,会返回什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
curl -vvv -X PATCH "http://example.com/12345/?lifecycle"
* Trying xx.xx.xx.xx...
* TCP_NODELAY set
* Connected to example.com (xx.xx.xx.xx) port 80 (#0)
> PATCH /12345/?lifecycle HTTP/1.1
> Host: example.com
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 405 Request method 'PATCH' not supported
< Server: XXX
< Date: Sat, 30 Sep 2017 03:26:03 GMT
< Content-Length: 0
< Connection: keep-alive
< X-Application-Context: application
< Allow: DELETE, GET, PUT, HEAD, OPTIONS, POST
<
* Connection #0 to host example.com left intact

但这个返回并不标准,因为我请求的资源是lifecycle,lifecycle不支持POST、OPTIONS等请求。原因是,这个405也是Jetty返回的,Jetty无法理解业务资源类型,所以把所有我们支持的请求类型都返回了。

查看日志可以确认这个行为是Jetty做的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
10:49:03.228 415437382 [6061] [qtp2047899112-1676226 - /] DEBUG o.e.jetty.server.Server REQUEST PATCH / on HttpChannelOverHttp@462350f6{r=1,c=false,a=DISPATCHED,uri=/}
10:49:03.228 415437382 [6061] [qtp2047899112-1676226 - /] DEBUG o.e.j.s.h.ContextHandler scope null||/ @ o.s.b.c.e.j.JettyEmbeddedWebAppContext@4d56b779{/,null,AVAILABLE}
10:49:03.228 415437382 [6061] [qtp2047899112-1676226 - /] DEBUG o.e.j.s.h.ContextHandler context=||/ @ o.s.b.c.e.j.JettyEmbeddedWebAppContext@4d56b779{/,null,AVAILABLE}
10:49:03.228 415437382 [6061] [qtp2047899112-1676226 - /] DEBUG o.e.jetty.server.session sessionManager=org.eclipse.jetty.server.session.HashSessionManager@6011c7cf
10:49:03.228 415437382 [6061] [qtp2047899112-1676226 - /] DEBUG o.e.jetty.server.session session=null
10:49:03.228 415437382 [6061] [qtp2047899112-1676226 - /] DEBUG o.e.j.s.ServletHandler servlet ||/ -> dispatcherServlet@7ef5559e==org.springframework.web.servlet.DispatcherServlet,-1,true
10:49:03.228 415437382 [6061] [qtp2047899112-1676226 - /] DEBUG o.e.j.s.ServletHandler chain=apiFilter->applicationContextIdFilter->webRequestLoggingFilter->dispatcherServlet@7ef5559e==org.springframework.web.servlet.DispatcherServlet,-1,true
10:49:03.228 415437382 [6061] [qtp2047899112-1676226 - /] DEBUG o.e.j.s.ServletHandler call filter apiFilter
10:49:03.228 415437382 [6061] [qtp2047899112-1676226 - /] DEBUG o.e.j.s.ServletHandler call filter applicationContextIdFilter
10:49:03.228 415437382 [6061] [qtp2047899112-1676226 - /] DEBUG o.e.j.s.ServletHandler call filter webRequestLoggingFilter
10:49:03.228 415437382 [6061] [qtp2047899112-1676226 - /] DEBUG o.e.j.s.ServletHandler call servlet dispatcherServlet@7ef5559e==org.springframework.web.servlet.DispatcherServlet,-1,true
10:49:03.228 415437382 [6061] [qtp2047899112-1676226 - /] DEBUG o.s.w.s.DispatcherServlet DispatcherServlet with name 'dispatcherServlet' processing PATCH request for [/]
10:49:03.228 415437382 [6061] [qtp2047899112-1676226 - /] DEBUG o.s.b.a.e.m.EndpointHandlerMapping Looking up handler method for path /
10:49:03.228 415437382 [6061] [qtp2047899112-1676226 - /] DEBUG o.s.b.a.e.m.EndpointHandlerMapping Did not find handler method for [/]
10:49:03.228 415437382 [6061] [qtp2047899112-1676226 - /] DEBUG o.s.w.s.m.m.a.RequestMappingHandlerMapping Looking up handler method for path /ExceptionHandlerExceptionResolver Resolving exception from handler [null]: org.springframework.web.HttpRequestMethodNotSupportedException: Request method 'PATCH' not supported
10:49:03.229 415437383 [6061] [qtp2047899112-1676226 - /] DEBUG o.s.w.s.m.a.ResponseStatusExceptionResolver Resolving exception from handler [null]: org.springframework.web.HttpRequestMethodNotSupportedException: Request method 'PATCH' not supported

结合以上实验,我们可以看出jetty处理区分405和501的方法是:在 rfc-2068中定义过的方法,如果请求的url无法匹配,就返回405,同时返回所有API中定义过的请求方法;如果请求的方法没有在 rfc-2068 中定义,就直接返回501。

如何使用405与501?

那么对于应用开发者来说,如何使用这两者呢?

分析下来,最合理的处理方式是,如果请求方法与请求资源能够匹配,且能正常处理,则走正常逻辑;如果没有匹配成功,则先解析请求需要的资源,根据资源搜索支持此资源的请求方法,并将这些方法拼接到Allow头返回给客户端;如果根据资源无法找到任何能处理的方法,则返回501。

另外需要注意的是,由于HTTP状态码5xx一般认为是服务端错误,所以可能有公司的监控系统监控了所有5xx请求,所以501可能会影响监控报警,需要注意。如果实现合理,可以在监控中忽略501状态码。

Share Comments

实战stroke动画

iOS支持对许多属性进行动画操作,比如center,rotation等等。其中有两个属性常常被人忽略,但又能做出很有趣的效果,那就是strokeStartstrokeEnd。最近在做一个小东西的时候想要用动画在搜索图标与返回图表之间切换,效果如下:

final animation

具体是如何实现的,且容我慢慢说来。

设计动画

在具体做之前首先需要设计如何让这两个图标可以较为自然的切换。首先我们来分析下返回图标,

如下图:

magnifier state

返回图标非常简单,就是三根线,分别标记为1-3。然后分析下放大镜图标,如下图:

magnifier state

如果我们按照放大镜手柄做一根延长线,恰好也可以把放大镜可以分为三部分,上下半圆(1,2)和放大镜手柄(3)。

what a coincidence

所以我就让两个半圆“变形”成两个返回按钮,同时为了增加不同类型的动画,我决定让放大镜的手柄旋转一下,具体效果如下图:

magnifier state

放大镜的两个半圆在缩小的同时,返回图标的两条线在生长,同时放大镜的手柄向上摆动,看起来就像放大镜变形成了返回键。

让我们一起来看一个慢速版本,这样可以比较直观的理解动画过程。

slow version

定义状态

根据上面的分析可以看出这个动画有两个状态,分别是放大镜状态和返回键状态。可以用一个枚举来表示两个状态:

1
2
3
4
5
6
7
8
enum AnimationState {
case Magnifier
case Back
func nextState() -> AnimationState {
return self == .Magnifier ? .Back : .Magnifier
}
}

这里我增加了一个nextState方法,主要用来计算下一个状态是什么,逻辑也非常简单,如果是放大镜,下一个状态就是返回;如果是返回,下一个状态就是放大镜。

创建形状图层

那我们怎么表示圆形和直线呢?答案就是用UIBezierPathUIBezierPath定义了许多形状绘制的方法,包括圆弧和直线。创建形状的代码非常简单,但是在创建之前我们需要一个“容器”来展示定义好的曲线,这就需要用到CAShapeLayer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func setupCircleLayer(layer: CAShapeLayer, center: CGPoint, radius: CGFloat, startAngle: CGFloat, endAngle: CGFloat, strokeStart: CGFloat = 0, strokeEnd: CGFloat = 1, lineWidth: CGFloat = magnifierLineWidth, strokeColor: CGColor = magnifierColor) {
layer.path = UIBezierPath(arcCenter: center,
radius: radius,
startAngle: startAngle,
endAngle: endAngle,
clockwise: true).CGPath
layer.lineWidth = lineWidth
layer.strokeColor = strokeColor
layer.fillColor = nil
layer.strokeStart = strokeStart
layer.strokeEnd = strokeEnd
}
// 调用方式
magnifierLayerTop = CAShapeLayer()
setupCircleLayer(magnifierLayerTop,
center: magnifierCircleCenter,
radius: magnifierRadius,
startAngle: 5 * CGFloat(M_PI_4),
endAngle: CGFloat(M_PI_4),
strokeStart: 0,
strokeEnd: 1)

首先我们创建一个CAShapeLayer,然后根据我们计算的一些参数,来设置圆弧的起点和终点。这只是一个简略的版本,更完整的代码请查看StrokeAnimation项目

这里唯一需要终点强调的是颜色的设置。因为我们要用strokeStartstrokeEnd两个属性,所以这里一定要设置strokeColor而不是fillColor。如果设置了fillColor而不是strokeColor,那动画将没有任何效果。

直线的代码与之类似:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func setupLineLayer(layer: CAShapeLayer, length: CGFloat, rotation: CGFloat, startPoint: CGPoint, strokeStart: CGFloat, strokeEnd: CGFloat, strokeColor: CGColor = magnifierColor) {
let linePath = UIBezierPath()
linePath.moveToPoint(CGPointMake(0, 0))
linePath.addLineToPoint(CGPointMake(length, 0))
layer.path = linePath.CGPath
layer.fillColor = nil
layer.strokeColor = strokeColor
layer.setAffineTransform(CGAffineTransformMakeRotation(rotation))
layer.position = startPoint
layer.strokeStart = strokeStart
layer.strokeEnd = strokeEnd
}
// 调用方式
magnifierHandle = CAShapeLayer()
setupLineLayer(magnifierHandle,
length: magnifierRadius * 2,
rotation: CGFloat(M_PI_4),
startPoint: backStartPoint,
strokeStart: 0,
strokeEnd: 1)

组织图形结构

有了上述创建和设置形状的基本工具后,我们就能组织我们的场景了。根据之前对动画的描述,我们需要5个CAShapeLayer:放大镜上下半圆各一个,放大镜手柄一个,返回按钮里面除了手柄复用的那个以外,还需要2个。剩下的其实就是一些数学计算了,而且这些计算是因图形而已的,我就不贴大段的代码了,有兴趣的小伙伴可以查看StrokeAnimation项目

动起来-第一次尝试

终于要到重点了,那就是如何实现半圆到直线的动画呢。我们首先来看看strokeStartstrokeEnd的属性介绍:

The value of this property must be in the range 0.0 to 1.0. The default value of this property is 0.0.

Combined with the strokeEnd property, this property defines the subregion of the path to stroke. The value in this property indicates the relative point along the path at which to begin stroking while the strokeEnd property defines the end point. A value of 0.0 represents the beginning of the path while a value of 1.0 represents the end of the path. Values in between are interpreted linearly along the path length.

也就是说,这两个属性可以用来指定画曲线的哪一部分!用这个属性我们就可以做类似写字笔顺之类的效果了。当然我们的动画也需要依赖这两个属性。让我们再来看一下状态转换中这张图片:

magnifier state

这张图片可以解释成:在某个时间点,半圆的strokeStart从0变到了a(0 < a < 1),strokeEnd保持为1;而返回按钮上半部分直线的strokeStart保持为0,但是strokeEnd从0变到了b(0 < b < 1)。这就是我们整个动画的关键。

原理清楚后首现我们仍然要实现一个帮助方法,来帮我们动态的改变图层的strokeStartstrokeEnd属性,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
func animatePartTo(part: CAShapeLayer, startFrom: CGFloat, startTo: CGFloat, endFrom: CGFloat, endTo: CGFloat, rotationStart: CGFloat? = nil, rotationEnd: CGFloat? = nil, translationStart: CGFloat? = nil, translationEnd: CGFloat? = nil) {
part.strokeStart = startFrom
part.strokeEnd = endFrom
let anims: NSMutableArray = []
let start = CABasicAnimation(keyPath: "strokeStart")
start.fromValue = startFrom
start.toValue = startTo
anims.addObject(start)
let end = CABasicAnimation(keyPath: "strokeEnd")
end.fromValue = endFrom
end.toValue = endTo
anims.addObject(end)
let group = CAAnimationGroup()
group.animations = anims as NSArray as? [CAAnimation]
group.duration = animationDuration
group.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
part.addAnimation(group, forKey: nil)
}
// 使用方式
animatePartTo(magnifierLayerTop, startFrom: 1.0, startTo: 0.0, endFrom: 1.0, endTo: 1.0)
animatePartTo(backTopLine, startFrom: 0.0, startTo: 0.0, endFrom: 1.0, endTo: 0.0)

上面我为每个属性定义了一个CABasicAnimation,同时为了保证二者同时执行,我把它们放到了一个CAAnimationGroup中,然后又设置了一些动画时间、计时函数之类的属性。最后把这个动画组加到CAShapeLayer上,就开始动画了,来看看效果吧:

slow version

动起来-别闪来闪去了

动画看着倒是不错,为啥结束之后闪回去了?这说明显示定义的动画并不会实际改变属性值官方文档是这么说的:

Unlike an implicit animation, which updates the layer object’s data value, an explicit animation does not modify the data in the layer tree.

最简单的解决方案就是让动画结束之后不要移除,仍然保留原状。但是我们总是应该保持最终状态一致,而不是看起来一致。

其实要想保持最终状态一致也很简单,只需要在动画开始前设置好最重的属性即可。也就是把上面的:

1
2
part.strokeStart = startFrom
part.strokeEnd = endFrom

改为

1
2
part.strokeStart = startTo
part.strokeEnd = endTo

即可。具体原理可以参考官方文档Setting Interpolation Values这一节

代码仍然是参考StrokeAnimation项目

完成回调

好,目前为止我们的动画就结束了,但是怎么监听动画结束的事件呢?当然我们可以为每个动画设置delegate,然后在delegate里面监听。但是我们有多个动画,如果还要通过记完成了几个动画来判断是否所有动画都完成,就有点低端了(一旦动画数量变了,还得来改这里面的计数器)。

我用的方法是在设置动画前后包了一层CATransaction,然后通过CATransaction.setCompletionBlock来监听所有动作的完成,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
CATransaction.begin()
CATransaction.setCompletionBlock {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(animationDuration * Double(NSEC_PER_SEC))), dispatch_get_main_queue(), {
self.animateToState(self.state.nextState())
})
}
switch state {
case .Magnifier:
animatePartTo(magnifierLayerTop, startFrom: 1.0, startTo: 0.0, endFrom: 1.0, endTo: 1.0)
animatePartTo(magnifierLayerBottom, startFrom: 0.0, startTo: 0.0, endFrom: 0.0, endTo: 1.0)
animatePartRotation(magnifierHandle, rotationStart: 0, rotationEnd: CGFloat(M_PI_4))
animatePartTo(backTopLine, startFrom: 0.0, startTo: 0.0, endFrom: 1.0, endTo: 0.0)
animatePartTo(backBottomLine, startFrom: 0.0, startTo: 0.0, endFrom: 1.0, endTo: 0.0)
case .Back:
animatePartTo(magnifierLayerTop, startFrom: 0.0, startTo: 1.0, endFrom: 1.0, endTo: 1.0)
animatePartTo(magnifierLayerBottom, startFrom: 0.0, startTo: 0.0, endFrom: 1.0, endTo: 0.0)
animatePartRotation(magnifierHandle, rotationStart: CGFloat(M_PI_4), rotationEnd: 0)
animatePartTo(backTopLine, startFrom: 0.0, startTo: 0.0, endFrom: 0.0, endTo: 1.0)
animatePartTo(backBottomLine, startFrom: 0.0, startTo: 0.0, endFrom: 0.0, endTo: 1.0)
}
CATransaction.commit()

总结

可以看到iOS的属性动画非常强大,strokeStartstrokeEnd两个属性可以与其他的属性相互配合,做出很有趣的动画。在做好动画设计后从“静”到“动”也是比较容易的事情。

当然示例代码有许多地方是可以重构的,比如这里多次定义了同一个图层的开始和结束属性等等。但是这些并不是本文的重点,有兴趣的读者可以尝试自行解决此类问题。

Share Comments

函数式编程随想(一)——函数式编程的好处及柯里化的意义

这篇博客主要是为了记录我最近关于函数式编程的好处,以及柯里化的作用的一些思考。

首先来举个简单的例子,如果我们有一组数,比如[1,2,3,4,5,6],我们想取其中的奇数,然后把这些奇数取平方,并返回这个数组,即,我们期望的结果是[1,9,25],那么应该怎么写呢?

最简单的写法就是来一次循环:

1
2
3
4
5
6
7
8
9
10
list = [1, 2, 3, 4, 5, 6];
result = [];
for(idx = 0; idx < list.length; idx ++) {
if (list[idx] % 2 == 1) {
result = result.concat(list[idx] * list[idx]);
}
}
console.log(result);
// [1, 9, 25]

真的,我劝你不要这么写。

“为什么不?课本上都是这么写的啊!”

这么写确实正确,而且符合需求,但是看看我们做了多少额外的工作!我们不仅需要描述具体的业务逻辑(list[idx] % 2 == 1list[idx] * list[idx]),还要操心如何遍历每个元素(for循环)、如何取出当前要处理的值(维护idx变量,以及用list[idx]来取值),以及如何保存结果。

对于这么一个简单的任务我们几乎在基础工作上用了一半以上的代码,代价太高了。

有没有简单的写法呢?有啊!

1
2
3
4
5
// JS code
list = [1, 2, 3, 4, 5, 6];
result = list.filter((x) => x % 2).map((x) => x * x);
console.log(result);
// [1, 9, 25]

仔细观察这种写法。首先,这种写法只描述了要做什么,分别是过滤能被2整除的数和对每个数取平方。第二点就是我们不关心具体的实现,而是通过函数调用的方式来解决问题。

这就是函数式编程,全文完。


“别开玩笑了,函数调用的方式是什么鬼?”

注意看,这里我们没有描述如何遍历这个数组,我们只是调用了filtermap两个函数。所以我们没有关心如何一步步实现循环,记录结果,我们知道filtermap函数可以帮我们便利数组,同时对每个元素给我们执行一段代码的机会。

有趣的是这两个函数的参数。以map函数为例,这个函数的第一个参数是一个接受3个参数的函数。没错,这种函数就是所谓的高阶函数

高阶函数可以把函数当作参数,或者可以返回函数作为返回值。

正因为有了这种能力,我们才能把逻辑从循环的具体实现中抽象出来,才能使像filtermap这样的函数给调用方执行代码的机会。

“但是你说有三个参数?”

没错,filter函数在调用它的参数的时候会传递3个参数,但是在我们调用它的时候,却只接受了一个参数,剩下的两个参数被丢掉了。

“调用时传递过多的参数会被丢掉,那么参数如果不够呢?”

在JS里面会自动传undefined作为参数,这一点稍后会用到。

现在让我们开始时的例子。现在,如果我们要给[7,8,9,10,11,12,13]这个数组也执行同样的操作,为了避免复制、粘贴代码,我们可以通过函数进行抽象。做法也很简单,把之前代码里面的list变量作为参数传入一个函数,然后两次调用这个函数。就像这样:

1
2
3
4
5
function filterAndMap(data) {
return data.filter((x) => x % 2).map((x) => x * x);
}
console.log(filterAndMap([1, 2, 3, 4, 5, 6]));
console.log(filterAndMap([7,8,9,10,11,12,13]));

但是这里的业务太具体了,如果后面我们想对所有的偶数做取平方的操作,或者对所有的奇数取立方操作,那这个代码显然不能满足。

“等等,不是可以把函数当作参数?”

对!所以我们可以改一下filterAndMap方法,把具体的逻辑函数当作参数。同时考虑到我们会多次调用filterAndMap方法,为了避免每次调用都定义一次逻辑函数,所以顺便也提前定义这两个函数,分别是判断奇偶以及取平方:

1
2
3
4
5
6
7
8
function odd(x) { return x % 2; }
function sqr(x) { return x * x; }
function filterAndMap(data, callback1, callback2) {
return data.filter(callback1).map(callback2);
}
console.log(filterAndMap([1, 2, 3, 4, 5, 6], odd, sqr));
console.log(filterAndMap([7,8,9,10,11,12,13], odd, sqr));

但是,这也太挫了。我怎么知道第二个参数传odd而不是sqr?另外,我必须每次在调用的时候传递逻辑代码,而不是一次创建好,在更新数据的时候还必须附加上额外的参数。

显然我们还有更好的做法。上面这种写法的主要问题是我们不能提前配置过滤和映射函数。至于具体原因,让我们再来观察下filtermap这两个函数:

1
2
array.filter(callback)
array.map(callback)

这两个函数其实是Array.prototype定义的属性,所以我们的数组都可以调用。但是在调用的时候,前提是数组已经存在了,所以必须得先有数组,然后每个数组传一次参数。

不过这个是可以解决的,利用延迟执行。让我们先定义两个对应的函数,不过这一次,把array改成函数的参数。

1
2
function filter(callback, array) { return array.filter(callback); }
function map(callback, array) { return array.map(callback); }

可以看到,这两个函数跟原来版本的区别是,我们把原来的数组当作了参数。

特别注意:这里的参数顺序是有讲究的,推荐把回调函数放倒前面,把数据放倒后面,具体原因后面会讨论。

但是这没有解决问题,因为我们在调用的时候仍然需要提供这两个参数。别着急,如果我们让filter函数变成一个接受callback参数,同时返回一个接受array参数的函数,那情况就不一样了:

1
2
3
4
5
6
7
8
9
10
11
function filter(callback) {
return function (array) {
return array.filter(callback);
}
}
function map(callback) {
return function (array) {
return array.map(callback);
}
}

这里会涉及到一些关于自由变量、闭包的讨论。我们暂且不提,可以先记住一个原则,就是我们用callback参数调用filter后返回的那个函数的函数体中的callback就是之前传入的那个参数。

好,现在让我们看看怎么用:

1
2
var oddFilter = filter(odd);
var sqrMap = map(sqr);

这里的oddFiltersqrMap的类型签名都是[Array]->[Array],就是接受一个数组作为参数,同时返回一个数组,所以我们可以把这两个函数串联起来:

1
2
3
4
5
function filterAndMap(array) {
return sqrMap(oddFilter(array));
}
console.log(filterAndMap([1, 2, 3, 4, 5, 6]));
console.log(filterAndMap([7,8,9,10,11,12,13]));

看着不错,一旦我们构建好了大杀器filterAndMap方法,后续就只需要关心数据的变化即可。

那我们对filtermap函数做了什么呢?简单的说,我们实现了可以部分配置filtermap函数。我们可以提前配置好过滤和映射要做的操作,这样就可以在后续不断的更换数据,从而减少代码的复杂程度。同时,创建oddFiltersqrMap这两个对象的过程叫做部分调用(partial application)

其实将filter这种接受两个参数的函数变成多个没次只接受一个参数的函数的过程就叫柯里化(curry)。单独看柯里化可能觉得并没有什么用,但是如果你想把多个函数进行像刚刚做的这种串联,那就非常关键了。关于柯里化的实现和更多应用后面会讨论。

“完美了么?”

还没有!这种做法至少还有两个问题,第一个问题,我们的filterAndMap方法,如果我们要串联更多的操作,必须每次都去改这个方法,而且随着嵌套层次的增多,调整会越来越复杂。另外一个问题是,为了实现这个效果,我们的filtermap代码长了好几倍,显然我们不想每次都手撸一遍。

遇到这种问题,我们就得再请高阶函数出马做抽象啦。

先来说第一个问题,我期待的解决方案是这样的:

1
2
3
var filterAndMap = pipe(oddFilter, sqrMap);
console.log(filterAndMap([1, 2, 3, 4, 5, 6]));
console.log(filterAndMap([7,8,9,10,11,12,13]));

这里引入了一个pipe函数,我们马上就要实现它。先看看这种写法的好处是什么。首先,它对调用者来说是扁平的,调用者只需要传入符合要求的函数(函数只接受一个参数,且返回值与参数的类型一致),就可以串联多次,而且要调整顺序也十分简单,只需要改变传入pipe函数的参数的顺序即可,无需处理那一层层的括号。

其实实现pipe函数也很简单,但是需要两点基础知识。

  1. 在JavaScript中,每个函数的作用域中都有一个名为arguments的变量,这个变量是实际传入函数的参数对象。这个对象是所谓的类数组对象。这个意思是说这个对象有length属性,而且虽然是个对象,但是对象中的key是从0开始的下标。在ES5中可以用Array.prototype.slice函数转换成数组。这样我们就有了实际传入的参数数组。这个数组的元素与函数定义的参数的数量无关。
  2. 对于给定的函数我们可以通过callapply方法执行。这两者的第一个参数都是this指针,而区别是,apply方法的第二个参数是一个数组,而call方法从第二个参数开始是一个变长参数列表。

考虑pipe函数做的事情:pipe函数的参数是一组函数,这些函数接受一个参数,并返回一个特定的类型。我们只需要对每个参数使用call方法,传入上一个函数执行的结果即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function toArr(args) {
return Array.prototype.slice.call(args, 0);
}
function pipe () {
var args = toArr(arguments);
return function () {
var innerArgs = toArr(arguments);
result = void 0; // void 0 => undefined
args.forEach(function (fn, idx) {
if (idx == 0) {
result = fn.apply(this, innerArgs);
} else {
result = fn.call(this, result);
}
});
return result;
}
}

然后我们再来考虑第二个问题,我们不想也不能没次都手撸一次柯里化之后的filter函数。如果一个函数有10个参数,那手撸一次估计就废了。在JS中其实也很好实现,不过还是需要补充一个知识点:

  1. 如果一个对象是函数类型,那么它的length方法返回的是函数定义的参数列表的长度。

也就是说,如果要实现curry函数,需要每次接受一部分参数(不一定是一个),只有在总参数的长度大于等于定义的参数长度时,才真正的调用函数,否则就返回一个保存了已经传入参数的函数。实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function curry(fn) {
var definedArgsCount = fn.length;
function helper(args) {
return function () {
var newArgs = toArr(arguments);
var wholeArgs = args.concat(newArgs);
if (wholeArgs.length >= definedArgsCount) {
return fn.apply(this, wholeArgs);
} else {
return helper(wholeArgs);
}
}
}
return helper([]);
}

到这里关于函数式编程的好处、柯里化的意义的讨论我想说的就都说完了。

“那为什么要把callback放前面?”

这里又个原则是把容易变化的往后放。加入了柯里化之后,我们可以实现函数的部分配置。那么就应该尽可能把可以配置的参数放倒前面。当然这也与JS的bind机制有关,具体我就不讨论了。而这么做的好处就是,我们可以像前面的例子这样,简单的把多个函数串联起来。

“然而我用PHP!”

没关系,我简单实现了4个有用的函数,并且开源啦。

“性能啊!”

实际上我认为如果不进行像网络和数据库读写这种耗时操作,比起这种写法带来的好处,性能的损耗可以接受。如果真的出现了性能问题,再进行特定的优化也不晚。

##参考资料

  1. http://jrsinclair.com/articles/2016/gentle-introduction-to-functional-javascript-intro/
  2. https://hughfdjackson.com/javascript/why-curry-helps/
Share Comments

帮他人提交Mac应用

最近做的一个外包项目要提交到应用市场了,但是他的开发者账户是个人账户,无法添加其他成员;同时我又不能要求他修改自己的Apple ID密码,然后发给我,这太不安全了。因为无法在XCode里面添加开发者账户,所以就不能使用XCode内置的工具上传应用了。对于这种情况,只能使用Apple提供的ApplicationLoader来进行应用发布,但是如何创建一个正确签名、可发布的应用包就成了很大的问题。

根据开发者文档来看,ApplicationLoader只接受ipa(iOS)、pkg(OSX与IAP)还有zip文件,因为我做的是Mac应用,所以就要想办法创建一个可用的pkg包。

经过一些搜索与尝试,最终还是成功把应用包提交了,下面就把过程总结一下。

生成app文件

具体如何archive,如何校对设置我就不详细说明了,如果有需要的话请自行google “App Distribution Guide”,值得一提的是,因为我们在XCode中没有证书与签名,所以在导出app的时候,只能选择最后一项(”Export as a Mac Application”)。

Export as Mac Application

所需证书

创建pkg需要两步签名,首先要对刚刚生成的”.app”签名,这里需要用到”Mac App Distribution”这个证书;接下来还要为生成的安装包签名,这里要用的是”Mac Installer Distribution”这个证书。

生成签名请求

虽然不能直接访问开发者账户,但是要将应用提交到应用市场一定要有开发者签名,这就需要有账户的人配合了。首先你要做好准备工作,或者说写申请。这里说的申请就是在本地创建一个签名请求。打开Keychain Access工具,按照下图选择:

Create Signing Request

因为要生成两个证书,而且根据我个人的测试,证书跟签名请求是一一对应的,所以在这一步需要创建两个签名请求文件,建议用”AppCertificateSigningRequest.certSigningRequest”跟”InstallerCertificateSigningRequest.certSigningRequest”来命名,其他的能区分的命名方式都可以。

指导他人生成证书

接下来就要用这个签名去请求证书了,具体的过程是:

  1. 访问开发者网站,登陆Member Center。
  2. 在Mac应用页面中,选择”Certificates, Identifiers & Profiles”中的”Certificates”那一项。
  3. 点击右上角的”+”按钮,创建新证书。
  4. 在”Production”分类中选择”Mac App Store”。
  5. 在下一步页面中,选择”Mac App Distribution”。
  6. 在下一步页面中,选择”AppCertificateSigningRequest.certSigningRequest”文件。
  7. 点击”Generate”来生成证书(证书文件名默认为”mac_app.cer”)。
  8. 重复1-7步,在第5步选择”Mac Installer Distribution”,在第六步中上传”InstallerCertificateSigningRequest.certSigningRequest”文件。这里第7步生成的证书的默认文件名是”mac_installer.cer”。

导入证书

分别双击导入”mac_app.cer”与”mac_installer.cer”,导入的时候选择“login”来导入当前用户的钥匙链中。导入之后最好通过名字过滤搜索,确认导入成功。

应用签名

有了证书之后就可以对之前生成的”.app”文件签名了。命令如下:

codesign -f -s "3rd Party Mac Developer Application: XXX" --entitlements “YYY.entitlements” "ZZZ.app"

其中”3rd Party Mac Developer Application: XXX”就是证书中的那个名字,”YYY.entitlements”就是应用对应的entitlements的路径,”ZZZ.app”就是之前生成的app文件。

安装包签名

接着使用productbuild工具来生成安装包,命令如下:

productbuild --component “ZZZ.app" /Applications --sign "3rd Party Mac Developer Installer: XXX" UUU.pkg

同样的,将”ZZZ.app”跟”3rd Party Mac Developer Installer: XXX”替换为具体的应用名称和开发者名称,注意这里用的是Installer这个证书;”UUU.pkg”就是最终的安装包的名字。

验证安装包

到这里这个应用包就生成完毕了,最后要验证一下,命令是

sudo installer -store -pkg UUU.pkg -target /

在输出的内容中,特别要关注是否有一条关于未正确签名的警告。如果一切顺利的话这个包就可以用ApplicationLoader提交到应用市场了。

后记

这种方法不仅适用于帮他人打包,也适用于一些公司的自动化过程;另外,可能还有别的方法(比如将证书导入XCode中),我就没有详细研究了。

Share Comments

开始站立式编程

最开始创立公司的时候我就准备尝试站立式编程,但是因为身高(~186cm)的原因,很难找到合适的桌子,我们甚至买了一个简单的吧台高桌,但是仍然不够高。

吧台桌

后来因为颈椎不舒服等原因吧,这件事又提上了议程。因为没有足够高的桌子,所以只能靠外设笔记本支架来补充了,我是在京东上挑选了这个支架。选择这个的时候我主要关注最大高度,稳定性以及可调节性,刚刚提到的那个支架这三点看上去都不错。

下单之后足足等了一个星期才到,到了之后有一点硬件问题,不过影响不是很大,而且售后的态度也非常不错。在简单组装之后,就可以很好的满足我的要求了。

正面:

正面照

背面:

背面照

真人版:

真人版

早上装好之后工作了3个小时左右,支架稍微有点抖动(因为我没用外接键盘),但支点比较稳固,可以接受;腿还是有点儿累,而且不断的出汗(正面照里面可以看到我准备的擦汗毛巾),中间休息了10分钟左右,身体感觉还行;工作效率方面,感觉比坐着的时候稍微有一点差距,但影响不大。等我再试用一段时间再来聊聊看吧。

Share Comments

跟郭总学开发(1)——Mac应用使用OpenCV的问题与解决方案

最近有机会跟郭总合作开发一些项目,真是每天都有新姿势啊,不记录下来都觉得可惜。

起因

先来描述一下遇到的问题:我们的项目要用到OpenCV,如果是为iOS开发的话,直接编译生成opencv2.framework就可以了,iOS默认将framework静态编译到最终的二进制中,但是在Mac上没有现成的framework framework不会静态编译,仍然要复制到最终的bundle中,这就可能带来问题。

我是用brew install opencv安装的opencv,没有其他特殊指令,最终是在/usr/local/Cellar/opencv/2.4.9/lib下生成了一堆dylib文件,然后把这些文件放到项目里面,同时选择复制到最终的bundle中。但是这样产生的.app在运行时会有一些路径相关的错误(一般是image not found什么的),直接就崩了。

这样我们就只能选择用静态链接库了。如果是我来解决这个问题的话,我一定会想办法安装一个生成.a版本的opencv,很有可能会自己编译源代码。

如果你比较急着用的话,可以直接下载opencv_osx.a.zip

郭总说

让我们看看郭总是怎么搞的:

最后是去这下了个 iOS 的 framework 把里面二进制拿出来 去掉了arm http://sourceforge.net/projects/opencvlibrary/files/opencv-ios/

反正要给 iOS 模拟器就有 x64 i386

framework里面有个核心的二进制 加个.a后缀 就是 static lib 直接拿过来就行了

我嫌大 用lipo拆开 去掉了arm的重合了一个

一共只有3行操作

喂喂,略显高端了吧!

这里的要点一方面是有的iOS framework因为要支持iOS模拟器,所以会在核心二进制里面包含x64和i386可用的static lib,我们如果有需要可以利用这些生成好的二进制;

另外一方面就是lipo,如果你跟我一样从来没见过这个命令,赶快去man lipo一下吧!

实战

接下来我们来实战一下,首先从郭总提供的url里面下载好这个framework,找到这个核心的二进制:

Core Binary

接下来先看一下这个二进制包含了哪些arch:

➜ voidmain@MBP  ~/Desktop  lipo -info opencv2
Architectures in the fat file: opencv2 are: armv7 armv7s i386 x86_64 arm64

正如郭总所说,这里面除了给iOS准备的arm系列外,还包含着i386跟x86_64这两个mac上可用的framework,然后我们就需要剔除arm系列,给这个二进制瘦身一下:

➜ voidmain@MBP  ~/Desktop  lipo opencv2 -extract i386 -extract x86_64 -output opencv2_osx.a
➜ voidmain@MBP  ~/Desktop  lipo -info opencv2_osx.a
Architectures in the fat file: opencv2_osx.a are: i386 x86_64

这样就可以用了!

总结

iOS跟OS X的差别其实真的没有那么大~

Share Comments

NSImage在读取高DPI图像时的bug及解决方案

这可能是所有用NSImage的开发者都会遇到的一个坑:为什么我的图像用NSImage打开之后变小了?

比如下面这个图:

Image Size with High DPI

它的分辨率是2848x4288,但是,如果我用下面的代码打印出来的话,大小却只有854.4x1286.4。

1
2
NSImage *srcImage = [[NSImage alloc] initWithContentsOfURL:url];
NSLog(@"before: %@", NSStringFromSize(srcImage.size));

这段代码看起来已经简洁的不能再简洁了吧,应该没有问题才对啊。但是实际问题出现在DPI这里。NSImage的size计算是按照DPI为72的值计算的,做个简单的实验,如果用Photoshop打开刚刚这幅图,然后用Image Size工具将DPI调成72(保持各种比例关系不变),就能看到这个854.4怎么来的了:

Change DPI

其实要解决这个问题并不是特别困难。尽管NSImage在计算尺寸的时候是按72dpi来计算的,但是NSImage的内部表示NSBitmapImageRep还是有字段保留着图像的实际尺寸,分别是pixelsWidepixelsHigh。因此只要利用这两个实际大小来计算,或者干脆中心绘制一下这个NSImage就可以了。核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
- (NSBitmapImageRep *)bitmapImageRepresentation {
// NSImage可能包含很多representation,需要迭代一下
NSArray * imageReps = [self representations];
float width = 0;
float height = 0;
for (NSImageRep * imageRep in imageReps) {
// 利用pixelsWide跟pixelsHigh来获得实际图像分辨率
if ([imageRep pixelsWide] > width) width = [imageRep pixelsWide];
if ([imageRep pixelsHigh] > height) height = [imageRep pixelsHigh];
}
if(width < 1 || height < 1)
return nil;
// 重新绘制
NSBitmapImageRep *rep = [[NSBitmapImageRep alloc]
initWithBitmapDataPlanes: NULL
pixelsWide: width
pixelsHigh: height
bitsPerSample: 8
samplesPerPixel: 4
hasAlpha: YES
isPlanar: NO
colorSpaceName: NSDeviceRGBColorSpace
bytesPerRow: 0
bitsPerPixel: 0];
NSGraphicsContext *ctx = [NSGraphicsContext graphicsContextWithBitmapImageRep: rep];
[NSGraphicsContext saveGraphicsState];
[NSGraphicsContext setCurrentContext: ctx];
// 实际绘制代码,把全部图像(fromRect: NSZeroRect)画到全尺寸的矩形中(drawInRect:NSMakeRect(0, 0, width, height))
[self drawInRect:NSMakeRect(0, 0, width, height) fromRect:NSZeroRect operation:NSCompositeCopy fraction:1.0];
[ctx flushGraphics];
[NSGraphicsContext restoreGraphicsState];
return rep;
}

这个核心文件我已经托管到github了。

Share Comments

我的工作时间表

对于创业者来说,保持每天精力充沛十分重要,因此良好和稳定的作息习惯是必不可少的。本文就来分享一下我每天的工作、休息时间。

其实最开始我的时间表不是现在这样,大致说来就是晚上12点-1点睡觉,早上8点起床,中午有2个小时的午休。后来因为合伙人租的房子附近每天早上6点开始施工,我们就把时间表前移了两个小时:

  • 6:10 起床
  • 6:10 - 6:30 洗漱
  • 6:30 - 7:00 晨练 (注1)
  • 7:00 - 7:10 消消汗,冷静冷静,准备开始工作
  • 7:10 - 11:00 上午工作时间 (注2)
  • 11:00 - 11:40 吃午饭
  • 11:40 - 12:00 午休前随便刷会社交网络+视频网站
  • 12:00 - 14:00 午休
  • 14:00 - 17:00 下午工作时间 (注3)
  • 17:00 - 18:00 晚饭时间
  • 18:00 - 20:00 自由支配时间 (注4)
  • 20:00 - 21:00 消遣时间 (注5)
  • 21:00 - 22:00 洗澡、给家里打电话 (注6)
  • 22:00 - 23:00 躺在床上刷手机 (注7)
  • 23:00 熄灯睡觉

这个时间表每周会使用6天,因为我已经脱团,所以每周会有一天时间去妹子学校刷脸。这一天仍然会早起、早睡,但是没有午休了。

注释

  1. 晨练会根据北京天气情况,如果是雾霾的话就不晨练了。这一点有待改进,雾霾的话应该改为室内锻炼。一般我的晨练就是跑步+快走。大概各占15分钟。
  2. 上午工作时间是最宝贵的,因为刚睡醒头脑清醒,加上晨练结束比较兴奋,这段时间是生产力最高的时间。一般我会用来看需要研究的论文或者没解决的疑难问题。
  3. 因为我的午睡习惯不太好,午睡时间较长,所以下午起床之后刚开始的一段时间比较昏昏沉沉,这段时间我一般会去研究一个比较能刺激大脑的,而且比较有趣的问题,比如如何实现XXXX效果,如何改进XXXX代码等等。
  4. 这段时间比较自由,不太确定。有的时候可能晚上出去吃大餐,这段时间都在吃饭;有的时候可能突然有了一个兴趣点,这段时间就拿来做20%项目;每周三晚上是我们公司的”Halo Night”+”Laundary Night”(TBBT粉有没有很耳熟),所以这段时间就去洗衣服+打Halo;还有的时候我自己直接就用来打游戏了。
  5. 这段消遣时间也不是很固定,如果不在消遣可能就在继续写代码。这段消遣时间一般会跟小伙伴一起打打星际什么的,或者看看星际视频。
  6. 给家里打电话其实挺重要的,也许并没有什么特别的话题,但是重要的是让父母知道你一切平安。
  7. 一般我会先看一集美剧/日剧/日漫,然后听《网易轻松一刻语音版》或者《网易轻松一刻新闻七点整》,然后慢慢入睡。

可以改进的地方

我觉得最需要改进的就是戒掉躺在床上刷手机这个环节,改称做仰卧起坐/俯卧撑之类的比较好。但是手机中毒过深,慢慢戒吧。

Share Comments

在Swift代码中整合C++类库

最近想用Swift开发一些小玩具,其中一个应用需要用到Box2d这个物理引擎,所以就遇到了如何将C++代码与Swift代码整合的问题。

在项目中整合Box2d并不困难,可以直接在Podfile里面添加pod 'box2d',比较麻烦的是怎么在代码中使用。

在WWDC的Session 406: Integrating Swift with Objective-C中,Apple只是介绍了怎么将Swift代码跟Objective-C代码做整合,但是没有提C++,后来在官方文档中看到了这样一段话:

You cannot import C++ code directly into Swift. Instead, create an Objective-C or C wrapper for C++ code.

这就很简单了,首先我们需要创建一个ObjC的类,用类创建向导很容易就能完成这个工作:

创建ObjCWrapper

在创建过程中Xcode会提示是否需要创建bridge,选择创建就好了。

接下来就可以编辑XXXX-Bridging-Header.h这个文件了,根据我的需要,这里应该#import <Box2d/Box2d.h>,所以我就直接把这句话放到bridging header里面了,编译,BOOM!

\:0: error: /path/to/project/Pods/Headers/Box2D/Common/b2Settings.h:22: ‘cassert’ file not found

如果google这个问题的话,可以看到各种答案都是说应该把.m文件替换成.mm文件,但是我现在压根没用上我刚刚创建的VMBox2dWrapper.m,这就是问题所在。这里需要做2个修改,一个是把VMBox2dWrapper.m的后缀替换成.mm,另外一个是把#import <Box2d/Box2d.h>移动到这个.mm文件里面,而XXXX-Bridging-Header.h这个文件里面#import "VMBox2dWrapper.h"。经过这两个改动以后,就可以顺利编译了。

还有一个问题是这个Wrapper类里面写什么,基本就是看项目需要用到什么再添加什么方法了,因为我的项目才刚刚开始,如果后续有什么需要注意的地方再来添加。

Share Comments

使用Swift(1)

序言

Apple新推出的Swift编程语言无疑会成为最近码农研究的热点,现在官方有一本官方Guide,这本书已经有国内的开发者开始翻译了(#该来的总会来的#)。

当然只看完Swift的语法还不足以进行Cocoa应用开发,用郭总的话说,语言并不会带来新的起跑线,要想开发优秀的Cocoa应用还是需要对Cocoa这个框架的深入理解。这就跟用RubyMotion仍然要学习Cocoa是一个道理,万变不离其宗。这方面就可以参考Using Swift with Cocoa and Objective-C这篇文档。

另外,Swift刚刚推出一天就已经有开发者用它开发了FlappyBird,可见大家对这门语言的热情。这个系列的博客主要是记录我在学习Swift语言过程中感觉有趣的语法、用法,并不会对所有语法都逐一进行分析,而是举几个简单的例子帮我自己来理解Swift这门语言。如果这些例子能帮助你对Swift这门语言建立一些基本的认识或者感觉,那就再好不过了!

PS:博客内容的记录顺序大部分是根据官方Guide来的,因为这是我的阅读顺序,在阅读过程中可能会根据我的理解跳跃穿插一些内容。另外因为我也是在逐渐阅读文档,所以可能写过的内容会有多次修订。

环境要求

要使用Swift必须要安装Xcode 6 Beta版,安装Xcode6不需要10.10,在10.9上就可以使用。

代码

本文中使用道德测试代码都可以在SwiftWithCocoa这个repo中找到,大部分代码都在MyPlayground中。

“变量”不变

第一次接触这个概念是在了解Scala的时候,Scala中有两个关键字,分别是valvar,用var声明的变量跟其他语言中的一样,可以改变值,但是val声明的变量,一旦第一次赋值之后就无法改变了,也就是所谓的“变量”不变。这种类型主要应用在多线程的场景中,可以有效的避免资源抢占,死锁等情况的发生,从语言级保证了代码的稳定性和执行效率。

Swift也提供了类似的声明方法,分别是letvar,用let声明的是常量,用var声明的是变量。

基本数据类型

Tuple

Tuple应该是我从python、ruby转到OC之后感觉最需要的类型。Tuple最大的贡献在于能轻便的创建一些临时对象,并在不同的领域使用。比如函数返回的时候可以利用tuple便捷的返回多个值,这是现在很多流行语言都支持的。

1
2
3
4
5
6
7
8
// Returning from func
func response() -> (Int, String) {
return (404, "Not Found")
}
var code: Int, description: String
(code, description) = response()
// code == 404
// description == "Not Found"

我记得我最开始学python,交换变量的方法真是让我震惊了,Swift里面(因为支持了tuple,所以)也有类似的方法了:

1
2
3
4
5
6
// Swapping vars
var first = 1
var second = 2 // Change 2 to "ASDF" and see the error
(first, second) = (second, first)
// first == 2
// second == 1

需要注意的是,因为Swift强调的是类型安全,所以上面例子中的firstsecond必须要是同样的类型才能交换,如果类型不同需要进行显式的类型转换,下面会进行讨论。

总而言之有了tuple之后,用Swift传递和处理数据将变得非常灵活。

类型推断(Inference)与Optionals

Swift自夸的一个特性就是类型安全,变量声明(或第一次赋值)之后,编译器会根据开发者显式指定(let num: Int)或者隐式赋值推断(let num = 1)得到变量类型,一旦确定,之后就不能改变类型了,例如:

1
2
var num = 1
num = "123" // Error!

第二行就会报错,因为num已经被编译器推测为Int类型了,所以再赋值为String就会出错,所以上面才提到的,使用tuple赋值的时候两种类型必须一致。为了解决这个问题,我们可以用强制类型转型:

1
2
3
4
5
6
// Explict type conversion and unwarp
first = 1
var third = "123"
(first, third) = (third.toInt()!, String(first))
first
second

上面的例子中,就是把third:String利用toInt()强制转换成了Int,这样first:Int就能接受了。下面我们重点说一下toInt()!最后的这个!

Optionals

Optionals实际是一种类型,这种类型就是在原有类型的后面加上一个大大的?。对于Cocoa开发者来说,这种变化应该并不陌生,因为Objective-C里面的SEL类型,:也是类型的一部分,比如gotNotificationgotNotification:就是两个SEL对象。

如果说一个变量的类型是XX?,那么代表的意思是,要么这个变量有值,且类型为XX,要么这个变量没有值。这个类型主要用在可能会出错的时候,例如将字符串转换为数字,可能字符串根本代表的不是数字,对于这种情况有的语言选择抛出运行时异常,Swift则选择了返回一个值为空的optionals对象。在Swift的代码框架中,大量系统级的API返回的都是optionals对象。

但是如何使用XX?的值呢,或者说如何将XX?转换为XX呢?这就要用到!了。所以上一节中说到的third.toInt()!实际过程是"123"这个String类型的变量通过toInt()转换为了Int?,但是咱们的first对象类型为Int,所以需要将Int?通过!转换为Int。这下整个过程就清楚了。

另外optionals还常用在if判断中,可以通过if let来避免使用!

1
2
3
4
var text:String? = "123"
if let v = text {
println(v)
}

最后值得一提的是隐式optionals对象,就是在定义的时候使用!而不是?,这样的好处是系统可以自动将XX?转换为XX对象了。

基于optionals还有更多神奇的语法糖,比如Craig在Keynote里面展示的那段代码,我们以后再分析。

语法糖 - Trailing closure syntax

本来准备再下篇博客中再分析closure,但是有一个语法感觉可以提前说明一下,就是Trailing closure syntax。在Objective-C中,大家都非常熟悉block的使用了,基本上很多常用的API都提供了block方式,比如NSBlockOperation

1
2
3
4
5
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:{
NSLog(@"Got block!");
}];
[queue addOperation:blockOperation];

如果直接对应过来的话,应该写成:

1
2
3
4
5
let operationQ = NSOperationQueue()
let operation = NSBlockOperation(block: {
println("Got operation!")
})
operationQ.addOperation(operation)

对于这种最后一个参数接受closure的情况,可以将最后一个参数移到调用括号的外面,看起来就像定义了一个block一样:

1
2
3
4
let operation = NSBlockOperation() {
println("Got operation!")
}
operationQ.addOperation(operation)

这样写起来不仅省了代码,而且看上去也比较舒服了。

Share Comments