南峰子的技术博客

攀登,一步一个脚印,方能知其乐


  • 首页

  • 知识小集

  • Swift

  • Objective-C

  • Cocoa

  • 翻译

  • 源码分析

  • 杂项

  • 归档

iOS中流(Stream)的使用

发表于 2014-07-17   |   分类于 Cocoa

流提供了一种简单的方式在不同和介质中交换数据,这种交换方式是与设备无关的。流是在通信路径中串行传输的连续的比特位序列。从编码的角度来看,流是单向的,因此流可以是输入流或输出流。除了基于文件的流外,其它形式的流都是不可查找的,这些流的数据一旦消耗完后,就无法从流对象中再次获取。

在Cocoa中包含三个与流相关的类:NSStream、NSInputStream和NSOutputStream。NSStream是一个抽象基类,定义了所有流对象的基础接口和属性。NSInputStream和NSOutputStream继承自NSStream,实现了输入流和输出流的默认行为。下图描述了流的应用场景:

image

从图中看,NSInputStream可以从文件、socket和NSData对象中获取数据;NSOutputStream可以将数据写入文件、socket、内存缓存和NSData对象中。这三处类主要处理一些比较底层的任务。

流对象有一些相关的属性。大部分属性是用于处理网络安全和配置的,这些属性统称为SSL和SOCKS代理信息。两个比较重要的属性是:

  1. NSStreamDataWrittenToMemoryStreamKey:允许输出流查询写入到内存的数据
  2. NSStreamFileCurrentOffsetKey:允许操作基于文件的流的读写位置

可以给流对象指定一个代理对象。如果没有指定,则流对象作为自己的代理。流对象调用唯一的代理方法stream:handleEvent:来处理流相关的事件:

  1. 对于输入流来说,是有可用的数据可读取事件。我们可以使用read:maxLength:方法从流中获取数据
  2. 对于输出流来说,是准备好写入的数据事件。我们可以使用write:maxLength:方法将数据写入流

Cocoa中的流对象与Core Foundation中的流对象是对应的。我们可以通过toll-free桥接方法来进行相互转换。NSStream、NSInputStream和NSOutputStream分别对应CFStream、CFReadStream和CFWriteStream。但这两者间不是完全一样的。Core Foundation一般使用回调函数来处理数据。另外我们可以子类化NSStream、NSInputStream和NSOutputStream,来自定义一些属性和行为,而Core Foundation中的流对象则无法进行扩展。

上面主要介绍了iOS中流的一些基本概念,我们下面将介绍流的具体使用,首先看看如何从流中读取数据。

从输入流中读取数据

从一个NSInputStream流中读取数据主要包括以下几个步骤:

  1. 从数据源中创建和初始化一个NSInputStream实例
  2. 将流对象放入一个run loop中并打开流
  3. 处理流对象发送到其代理的事件
  4. 当没有更多数据可读取时,关闭并销毁流对象。

准备流对象

要使用一个NSInputStream,必须要有数据源。数据源可以是文件、NSData对象和网络socket。创建好后,我们设置其代理对象,并将其放入到run loop中,然后打开流。代码清单1展示了这个准备过程.

代理清单1

1
2
3
4
5
6
7
8
- (void)setUpStreamForFile:(NSString *)path
{
NSInputStream *inputStream = [[NSInputStream alloc] initWithFileAtPath:path];
inputStream.delegate = self;
[inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[inputStream open];
}

在流对象放入run loop且有流事件(有可读数据)发生时,流对象会向代理对象发送stream:handleEvent:消息。在打开流之前,我们需要调用流对象的scheduleInRunLoop:forMode:方法,这样做可以避免在没有数据可读时阻塞代理对象的操作。我们需要确保的是流对象被放入正确的run loop中,即放入流事件发生的那个线程的run loop中。

处理流事件

打开流后,我们可以使用streamStatus属性查看流的状态,用hasBytesAvailable属性检测是否有可读的数据,用streamError来查看流处理过程中产生的错误。

流一旦打开后,将会持续发送stream:handleEvent:消息给代理对象,直到流结束为止。这个消息接收一个NSStreamEvent常量作为参数,以标识事件的类型。对于NSInputStream对象,主要的事件类型包括NSStreamEventOpenCompleted、NSStreamEventHasBytesAvailable和NSStreamEventEndEncountered。通常我们会对NSStreamEventHasBytesAvailable更感兴趣。代理清单2演示了从流中获取数据的过程

代理清单2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode
{
switch (eventCode) {
case NSStreamEventHasBytesAvailable:
{
if (!data) {
data = [NSMutableData data];
}
uint8_t buf[1024];
unsigned int len = 0;
len = [(NSInputStream *)aStream read:buf maxLength:1024]; // 读取数据
if (len) {
[data appendBytes:(const void *)buf length:len];
}
}
break;
}
}

当NSInputStream在处理流的过程中出现错误时,它将停止流处理并产生一个NSStreamEventErrorOccurred事件给代理。我们同样的上面的代理方法中来处理这个事件。

清理流对象

当NSInputStream读取到流的结尾时,会发送一个NSStreamEventEndEncountered事件给代理,代理对象应该销毁流对象,此过程应该与创建时相对应,代码清单3演示了关闭和释放流对象的过程。

代理清单3

1
2
3
4
5
6
7
8
9
10
11
12
- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode
{
switch (eventCode) {
case NSStreamEventEndEncountered:
{
[aStream close];
[aStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
aStream = nil;
}
break;
}
}

写入数据到输出流

类似于从输入流读取数据,写入数据到输出流时,需要下面几个步骤:

  1. 使用要写入的数据创建和初始化一个NSOutputStream实例,并设置代理对象
  2. 将流对象放到run loop中并打开流
  3. 处理流对象发送到代理对象中的事件
  4. 如果流对象写入数据到内存,则通过请求NSStreamDataWrittenToMemoryStreamKey属性来获取数据
  5. 当没有更多数据可供写入时,处理流对象

基本流程与输入流的读取差不多,我们主要介绍不同的地方

  1. 数据可写入的位置包括文件、C缓存、程序内存和网络socket。
  2. hasSpaceAvailable属性表示是否有空间来写入数据
  3. 在stream:handleEvent:中主要处理NSStreamEventHasSpaceAvailable事件,并调用流的write:maxLength方法写数据。代码清单4演示了这一过程。
  4. 如果NSOutputStream对象的目标是应用的内存时,在NSStreamEventEndEncountered事件中可能需要从内存中获取流中的数据。我们将调用NSOutputStream对象的propertyForKey:的属性,并指定key为NSStreamDataWrittenToMemoryStreamKey来获取这些数据。

代理清单4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode
{
switch (eventCode) {
case NSStreamEventHasSpaceAvailable:
{
uint8_t *readBytes = (uint8_t *)[data mutableBytes];
readBytes += byteIndex;
int data_len = [data length];
unsigned int len = (data_len - byteIndex >= 1024) ? 1024 : (data_len - byteIndex);
uint8_t buf[len];
(void)memcpy(buf, readBytes, len);
len = [aStream write:(const uint_8 *)buf maxLength:len];
byteIndex += len;
break;
}
}
}

这里需要注意的是:当代理接收到NSStreamEventHasSpaceAvailable事件而没有写入任何数据到流时,代理将不再从run loop中接收该事件,直到NSOutputStream对象接收到更多数据,这时run loop会重启NSStreamEventHasSpaceAvailable事件。

流的轮循处理

在流的处理过程中,除了将流放入run loop来处理流事件外,还可以对流进行轮循处理。我们将流处理数据的过程放到一个循环中,并在循环中不断地去询问流是否有可用的数据供读取(hasBytesAvailable)或可用的空间供写入(hasSpaceAvailable)。当处理到流的结尾时,我们跳出循环结束流的操作。

具体的过程如代码清单5所示

代码清单5

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
- (void)createNewFile {
NSOutputStream *oStream = [[NSOutputStream alloc] initToMemory];
[oStream open];
uint8_t *readBytes = (uint8_t *)[data mutableBytes];
uint8_t buf[1024];
int len = 1024;
while (1) {
if (len == 0) break;
if ([oStream hasSpaceAvailable])
{
(void)strncpy(buf, readBytes, len);
readBytes += len;
if ([oStream write:(const uint8_t *)buf maxLength:len] == -1)
{
[self handleError:[oStream streamError]];
break;
}
[bytesWritten setIntValue:[bytesWritten intValue]+len];
len = (([data length] - [bytesWritten intValue] >= 1024) ? 1024 : [data length] - [bytesWritten intValue]);
}
}
NSData *newData = [oStream propertyForKey:NSStreamDataWrittenToMemoryStreamKey];
if (!newData) {
NSLog(@"No data written to memory!");
} else {
[self processData:newData];
}
[oStream close];
[oStream release];
oStream = nil;
}

这种处理方法的问题在于它会阻塞当前线程,直到流处理结束为止,才继续进行后面的操作。而这种问题在处理网络socket流时尤为严重,我们必须等待服务端数据回来后才能继续操作。因此,通常情况下,建议使用run loop方式来处理流事件。

错误处理

当流出现错误时,会停止对流数据的处理。一个流对象在出现错误时,不能再用于读或写操作,虽然在关闭前可以查询它的状态。

NSStream和NSOutputStream类会以几种方式来告知错误的发生:

  1. 如果流被放到run loop中,对象会发送一个NSStreamEventErrorOccurred事件到代理对象的stream:handleEvent:方法中
  2. 任何时候,可以调用streamStatus属性来查看是否发生错误(返回NSStreamStatusError)
  3. 如果在通过调用write:maxLength:写入数据到NSOutputStream对象时返回-1,则发生一个写错误。

一旦确定产生错误时,我们可以调用流对象的streamError属性来查看错误的详细信息。在此我们不再举例。

设置Socket流

在iOS中,NSStream类不支持连接到远程主机,幸运的是CFStream支持。前面已经说过这两者可以通过toll-free桥接来相互转换。使用CFStream时,我们可以调用CFStreamCreatePairWithSocketToHost函数并传递主机名和端口号,来获取一个CFReadStreamRef和一个CFWriteStreamRef来进行通信,然后我们可以将它们转换为NSInputStream和NSOutputStream对象来处理。

具体的处理流程我们会在后期详细讨论。

参考

Stream Programming Guide

URL加载系统之五:缓存、Cookies与协议

发表于 2014-07-16   |   分类于 翻译

缓存

URL加载系统为请求提供了基于磁盘和内存的组合响应缓存。这个缓存让应用减少了对网络连接的依赖,提高了性能。

一个NSURLRequest实例通过设置缓存策略来指定本地缓存。默认的缓存策略是NSURLRequestUseProtocolCachePolicy,其行为是由协议指定的针对该协议最好的实现方式。另外几种缓存策略描述如下:

  1. NSURLRequestReloadIgnoringCacheData:URL加载系统将从服务端加载数据,而完全忽略缓存。
  2. NSURLRequestReturnCacheDataElseLoad:URL加载系统使用缓存数据,忽略其过期时间;只有在没有缓存版本的时候才从源端加载数据。
  3. NSURLRequestReturnCacheDataDontLoad:允许应用指定只有在缓存中的数据应该被返回。如果在创建NSURLConnection或NSURLDownload实例时使用这个缓存策略,如果响应没有在本地缓存中,则直接返回nil。这类似于使用离线模式,且从来不进行网络连接。

需要注意的是,目前只有HTTP和HTTPS的响应还被缓存。

缓存最常用的场景是使用HTTP协议做网络请求,同时设置缓存策略为NSURLRequestUseProtocolCachePolicy。如果一个请求的NSCachedURLResponse不存在,则加载系统会从源端获取数据。如果请求的缓存响应存在于本地,则URL加载系统检查响应来确定它指定的内容必须被重新验证。如果内容必须验证,则加载系统发出一个HEAD请求到源端以确定资源是否已经改变。如果没有改变,则URL加载系统返回缓存响应对象。如果已经改变了,则URL加载系统从源端获取数据。

如果缓存响应对象没有指定内容必须被重新验证,则加载系统校验缓存响应对象的最大age或有效时间。如果缓存对象未过期,则加载系统返回缓存对象。如果响应过旧,则URL加载系统发起一个HEAD请求到源端查看资源是否已被修改。如果修改了,则URL加载系统从源端获取新的数据。否则,返回缓存响应对象。

默认情况下,连接的数据基于请求的缓存策略来进行缓存,同时由处理请求的NSURLProtocol子类来解析。如果我们需要对缓存做更精确的控制,我们可以实现一些代理方法来允许应用来确定请求是否应该缓存:

  1. 对于NSURLSession数据和上传任务,实现URLSession:dataTask:willCacheResponse:completionHandler:方法。这个代理方法只用于数据请求和上传任务。而下载任务的缓存由指定的缓存策略来决定。
  2. 对于NSURLConnection,实现connection:willCacheResponse:方法

对于NSURLSession,我们的代理方法调用一个完成处理器block来告知会话需要缓存什么东西。对于NSURLConnection,代理方法返回连接需要缓存的对象。不管是哪种情况,代理都会提供以下值之一:

  1. 允许缓存的响应对象
  2. 新创建的响应对象,用于缓存被修改的响应
  3. NULL,以阻止缓存

代理方法也可以提供与NSCacheURLResponse对象相关的userInfo字典,将这些对象作为响应的一部分存储在缓存中。

需要注意的是,如果我们使用NSURLSession眀实现了代理方法,则代理方法必须总是调用提供的完成处理器,否则会导致内存泄露。

Cookies

由于HTTP协议是无状态的,客户端通常使用cookie来提供URL请求间数据的持久化存储。URL加载系统提供接口来创建和管理cookie,将cookie作为HTTP请求的一部分进行发送,并在解析一个服务端的响应时获取cookie。

NSHTTPCookie类封装了一个cookie,并提供了大量访问器来访问cookie的各种属性。该类同样提供了方法用于HTTP cookie头与NSHTTPCookie实例之间的互转。URL加载系统自动发送与NSURLRequest对象匹配的任何存储的cookie,除非该请求指明不需要发送cookie。同样,NSURLResponse对象是返回的cookie将根据当前设定的cookie接收策略来处理。

NSHTTPCookieStorage提供接口来管理NSHTTPCookie对象的集合。与MacOS不同的是,iOS的cookie不能在应用间共享。NSHTTPCookieStorage允许一个应用指定cookie接收策略。

协议支持

URL加载系统允许客户端程序扩展协议,以支持自定义的传输数据的方式。URL加载系统默认只支持http, https, file, ftp和data协议。

我们可以继承NSURLProtocol来实现一个自定义的协议,然后使用NSURLProtocol类的的registerClass:方法将其注册到URL加载系统中。当NSURLSession, NSURLConnection和NSURLDownload对象初始化一个NSURLRequest的连接时,URL加载系统以注册顺序的倒序来查询每一个注册类。每个类都将调用canInitWithRequest:方法,第一个返回YES的类将被用于处理请求。

如果自定义协议需要额外的属性业支持请求与响应,则通过创建NSURLRequest, NSMutableRequest和NSResponse类的类别来提供这些属性的访问器。NSURLProtocol类提供方法在这些访问器中设置和获取属性值。

URL加载系统负责在连接开始和完成时创建和释放NSURLProtocol实例。我们的应用不应该直接创建NSURLProtocol的实例。

当NSURLProtocol子类通过URL加载系统初始化时,它提供了一个实现NSURLProtocolClient协议的客户端对象。NSURLProtocol子类将消息从NSURLProtocolClient协议发送到客户端对象,来告诉URL加载系统它创建了响应,接收数据,重定向到新的URL,请求认证,完成加载等操作。如果自定义协议支持认证,还必须实现NSURLAuthenticationChallengeSender协议。

参考

  1. URL Loading System Programming Guide

URL加载系统之四:认证与TLS链验证

发表于 2014-07-16   |   分类于 翻译

一个NSURLRequest对象经常会遇到认证请求,或者需要从其所连接的服务端请求证书。当需要认证请求时,NSURLConnection、NSURLSession和NSURLDownload类会通知它们的代理对象,以便能正确地做处理。不过需要注意的是,URL加载系统只有在服务端响应包含WWW-Authenticate头时才会调用代理来处理认证请求,而类似于代理认证和TLS信任验证这样的认证类型则不需要这个头。

确定如何响应一个认证请求

如果一个NSURLRequest对象需要认证时,则认证请求方式取决于使用的对象的类型:

  1. 如果请求是与NSURLSession对象关联,则所有认证请求都会传递给代理,而不考虑认证的类型。
  2. 如果请求是与NSURLConnection或NSURLDownload对象,则对象的代理接收一个connection:canAuthenticateAgainstProtectionSpace: (或者 download:canAuthenticateAgainstProtectionSpace:) 消息。这允许代理对象在尝试再次认证前分析服务端的属性,包括协议和认证方法。如果我们的代理对象不准备认证服务端的受保护空间,则返回NO,且系统尝试使用用户的keychain的信息进行认证。
  3. 如果NSURLConnection或NSURLDownload的代理对象没有实现connection:canAuthenticateAgainstProtectionSpace: (或者 download:canAuthenticateAgainstProtectionSpace:)方法,且保护空间使用客户端证书认证或服务端信任认证,则系统假设我们返回NO。而对象其它所有类型,系统都返回YES。

下一步,如果我们的代理对象同意处理认证,但是没有有效的证书(不管是作为请求URL的一部分或者在NSURLCredentialStorage中共享),则代理以收到以下消息:

  1. URLSession:didReceiveChallenge:completionHandler:
  2. URLSession:task:didReceiveChallenge:completionHandler:
  3. connection:didReceiveAuthenticationChallenge:
  4. download:didReceiveAuthenticationChallenge:

为了让连接能够继续,则代理对象有三种选择:

  1. 提供认证证书
  2. 尝试在没有证书的情况下继续
  3. 取消认证查询

为了确保操作的正确流程,传递给这些方法的NSURLAuthenticationChallenge实例会包含一些信息,包括是什么触发了认证查询、查询的尝试次数、任何先前尝试的证书、请求证书的NSURLProtectionSpace对象,及查询的发送者。

如果认证请求事先尝试认证且失败了(如用户在服务端修改了密码),我们可以通过在认证请求调用proposedCredential来获取尝试凭据。代理可以使用这些证书来填充一个显示给用户的话框。

调用认证请求的previousFailureCount可以返回身份验证尝试次数,这些尝试包括不同认证协议的尝试请求。代理可以将这些方法提供给用户,以确定先前提供的证书是否失败,或限制最大认证尝试次数。

响应认证请求

前面说过三种响应我们响应connection:didReceiveAuthenticationChallenge:代理方法的方式,我们将逐一介绍:

提供证书

为了进行认证,程序需要使用服务端期望的认证信息创建一个NSURLCredential对象。我们可以调用authenticationMethod来确定服务端的认证方法,这个认证方法是在提供的认证请求的保护空间中。NSURLCredential支持一些方法:

  1. HTTP基本认证(NSURLAuthenticationMethodHTTPBasic):需要用户名和密码。提示用户输入必要信息并使用credentialWithUser:password:persistence:方法创建一个NSURLCredential对象。
  2. HTTP数字认证(NSURLAuthenticationMethodHTTPDigest):类似于基本认证,需要用户名和密码。提示用户输入必要信息并使用credentialWithUser:password:persistence:方法创建一个NSURLCredential对象。
  3. 客户端证书认证(NSURLAuthenticationMethodClientCertificate): 需要系统标识和所有服务端认证所需要的证书。然后使用credentialWithIdentity:certificates:persistence:来创建一个NSURLCredential对象。
  4. 服务端信任认证(NSURLAuthenticationMethodServerTrust)需要一个由认证请求的保护空间提供的信任。使用credentialForTrust:来创建一个NSURLCredential对象。

在创建NSURLCredential对象后

  1. 对于NSURLSession,使用提供的完成处理block将该对象传递给认证请求发送者
  2. 对于NSURLConnection和NSURLDownload,使用useCredential:forAuthenticationChallenge:方法将对象传递给认证请求发送者。

尝试在没有证书的情况下继续

如果代理选择不提供证书,可以尝试继续操作:

  1. 对于NSURLSession,传递下面的值给完成处理block:
    NSURLSessionAuthChallengePerformDefaultHandling:处理请求。尽管代理没有提供代理方法来处理认证请求
    NSURLSessionAuthChallengeRejectProtectionSpace:拒绝请求。依赖于服务端响应允许的认证类型,URL加载类可能多次调用这个代理方法。
  2. 对于NSURLConnection和NSURLDownload,在[challenge sender]中调用continueWithoutCredentialsForAuthenticationChallenge:。

依赖于协议的实现,这种处理方法可能会导致连接失败而以送connectionDidFailWithError:消息,或者返回可选的不需要认证的URL内容。

取消连接

代理可以选择取消认证请求

  1. 对于NSURLSession,传递NSURLSessionAuthChallengeCancelAuthenticationChallenge给完成处理block
  2. 对于NSURLConnection和NSURLDownload,在[challenge sender]中调用cancelAuthenticationChallenge:。代理接收connection:didCancelAuthenticationChallenge:消息,以提供用户反馈的机会。

下面的代码演示了使用用户名和密码创建NSURLCredential对象来响应认证请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-(void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
{
if ([challenge previousFailureCount] == 0)
{
NSURLCredential *newCredential;
newCredential = [NSURLCredential credentialWithUser:[self preferencesName] password:[self preferencesPassword] persistence:NSURLCredentialPersistenceNone];
[[challenge sender] useCredential:newCredential forAuthenticationChallenge:challenge];
}
else
{ [[challenge sender] cancelAuthenticationChallenge:challenge]; // inform the user that the user name and password // in the preferences are incorrect [self showPreferencesCredentialsAreIncorrectPanel:self]; }
}

如果代理没有实现connection:didReceiveAuthenticationChallenge:,而请求需要认证,则有效的证书必须位于URL证书存储中或作为请求URL的一部分。如果证书无效或者认证失败,则底层实现会发送一个continueWithoutCredentialForAuthenticationChallenge:消息。

执行自定义TLS链验证

在NSURL系统的AIP中,TLS链验证由应用的认证代理方法来处理,但它不是提供证书给服务端以验证用户,而是在TLS握手的过程中校验服务端提供的证书,然后再告诉URL加载系统是否应该接受还是拒绝这些证书。

如果需要以非标准的方法(如接收一个指定的自标识的证书用于测试)来执行链验证,则应用必须如下处理:

  1. 对于NSURLSession,实现URLSession:didReceiveChallenge:completionHandler:和URLSession:task:didReceiveChallenge:completionHandler:代理方法。如果实现了两者,由会话级别的方法负责处理认证。
  2. 对于NSURLConnection和NSURLDownload,实现connection:canAuthenticateAgainstProtectionSpace:和download:canAuthenticateAgainstProtectionSpace:方法,如果保护空间有一个NSURLAuthenticationMethodServerTrust类型的认证,则返回YES。然后,实现connection:didReceiveAuthenticationChallenge:或download:didReceiveAuthenticationChallenge:方法来处理认证。

在认证处理代理方法中,我们需要确认认证保护空间是否有NSURLAuthenticationMethodServerTrust类型的认证,如果有,则从保护空间获取serverTrust信息。

URL加载系统之三:NSURLConnection

发表于 2014-07-15   |   分类于 翻译

NSURLConnection提供了简单的接口来创建和取消一个连接,并支持一个代理方法的集合来提供连接的响应,并对连接进行多方面的控制。这个类的方法可以分为5大类:URL加载、缓存管理、认证与证书、cookie存储、协议支持。

创建一个连接

NSURLConnection提供了三种方式来获取URL的内容:同步、异步使用完成处理器block、异步使用自定义的代理对象。

  1. 使用同步请求时,一般是在后台线程中独占线程运行,我们可以调用sendSynchronousRequest:returningResponse:error: 方法来执行HTTP请求。当请求完成或返回错误时,该方法会返回。
  2. 如果我们不需要监听请求的状态,而只是在数据完成返回时执行一些操作,则可以调用sendAsynchronousRequest:queue:completionHandler:方法来执行一个异步操作,其中需要传递一个block来处理结果。
  3. 我们也可以创建一个代理对象来处理异步请求,此时我们需要实现以下方法:connection:didReceiveResponse:、connection:didReceiveData:、connection:didFailWithError:和connectionDidFinishLoading: 。这些方法在NSURLConnectionDelegate、NSURLConnectionDownloadDelegate和 NSURLConnectionDataDelegate协议中定义。

代码清单1以代理对象异步请求为例,初始化了一个URL连接并实现代理方法来处理连接响应

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
@interface Conn : NSObject
{
    NSURLConnection *theConnection;
    NSMutableData *receivedData;
}
@end
@implementation Conn
- (void)createConnection
{
    // 创建一个请求
    NSURLRequest *theRequest=[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.apple.com/"]
                                              cachePolicy:NSURLRequestUseProtocolCachePolicy
                                          timeoutInterval:60.0];
    
    // 创建NSMutableData来保存接收到的数据
    receivedData = [NSMutableData dataWithCapacity: 0];
    
    // 使用theRequest创建一个连接并开始加载数据
    // 调用initWithRequest:delegate后会立即开始传输
    // 请求可以在connectionDidFinishLoading:或connection:didFailWithError:消息被发送前通过调用cancel来取消
    theConnection = [[NSURLConnection alloc] initWithRequest:theRequest delegate:self];
    
    if (!theConnection) {
        // 释放receivedData对象
        receivedData = nil;
        // 通知用户连接失败
    }
}
// 当服务端提供了有效的数据来创建NSURLResponse对象时,代理会收到connection:didReceiveResponse:消息。
// 这个代理方法会检查NSURLResponse对象并确认数据的content-type,MIME类型,文件 名和其它元数据。
// 需要注意的是,对于单个连接,我们可能会接多次收到connection:didReceiveResponse:消息;这咱情况发生在
// 响应是多重MIME编码的情况下。每次代理接收到connection:didReceiveResponse:时,应该重设进度标识
// 并丢弃之前接收到的数据。
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
    [receivedData setLength:0];
}
// 代理会定期接收到connection:didReceiveData:消息,该消息用于接收服务端返回的数据实体。该方法负责存储数据。
// 我们也可以用这个方法提供进度信息,这种情况下,我们需要在connection:didReceiveResponse:方法中
// 调用响应对象的expectedContentLength方法来获取数据的总长度。
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
    [receivedData appendData:data];
}
// 如果数据传输的过程中出现了错误,代理会接收到connection:didFailWithError:消息。其中error参数给出了错误信息。
// 在代理收到connection:didFailWithError:消息后,它不会再接收指定连接的代理消息。
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
    theConnection = nil;
    receivedData = nil;
    NSLog(@"Connection failed! Error - %@ %@", [error localizedDescription], [[error userInfo] objectForKey:NSURLErrorFailingURLStringErrorKey]);
}
// 如果成功获取服务端返回的所有数据,则代理会收到connectionDidFinishLoading:消息。
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
    NSLog(@"Succeeded! Receive %lu bytes of data(unsigned long)",[receivedData length]);
    theConnection = nil;
    receivedData = nil;
}
@end

发起一个POST请求

我们可以像发起其它URL请求一样,发起一个HTTP或HTTPS POST请求。主要的区别在于我们必须先配置好NSMutableURLRequest对象,并将其作为参数传递给initWithRequest:delegate:方法。

另外,我们还需要构造请求的body数据。可以以下面三种方式来处理

  1. 对于上传短小的内存数据,我们需要对已存在的数据块进行URL编码
  2. 如果是从磁盘中上传文件,则调用setHTTPBodyStream:方法来告诉NSMutableURLRequest从一个NSInputStream中读取并使用结果数据作为body的内容
  3. 对于大块的数据,调用CFStreamCreateBoundPair来创建流对象对,然后调用setHTTPBodyStream:方法来告诉NSMutableURLRequest使用这些流对象中的一个作为body内容的源。通过将数据写入其它流,可以一次发送一小块数据。

如果要上传数据到一个兼容的服务器中,URL加载系统同样支持100(继续)状态码,这样允许一个上传操作在发生认证错误或其它失败时仍能继续。为了开启这个操作,可以设置请求对象的expect头为100-continue。

代码清单2展示了如何配置一个POST请求的NSMutableURLRequest对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (void)setRequestForPost
{
    // 对于application/x-www-form-urlencoded类型的body数据,form域的参数由&号分开,
    NSString *bodyData = @"name=Jane+Doe&address=123+Main+St";
    NSMutableURLRequest *postRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"https://www.apple.com"]];
    
    // 设置content-type为application/x-www-form-urlencoded
    [postRequest setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"];
    
    // 指定请求方法为POST
    [postRequest setHTTPMethod:@"POST"];
    [postRequest setHTTPBody:[NSData dataWithBytes:[bodyData UTF8String] length:strlen([bodyData UTF8String])]];
    
    // Initialize the NSURLConnection and proceed as described in
    // Retrieving the Contents of a URL
    
}

使用Block来接收数据

NSURLConnection类提供了类方法sendAsynchronousRequest:queue:completionHandler:,该方法可以以异常的方式向服务端发起请求,并在数据返回或发生错误/超时时调用block来处理。该方法需要一个请求对象,一个完成处理block,及block运行的队列。当请求完成或错误发生时,URL加载系统调用该block来处理结果数据或错误信息。

如果请求成功,则会传递一个NSData对象和一个NSURLResponse对象给block。如果失败,则传递一个NSError对象。

这个方法有两个限制

  1. 对于需要认证的请求,只提供最小的支持。
  2. 没有办法来修改响应缓存和服务端重定向的默认行为。

参考

URL Loading System Programming Guide

URL加载系统之二:NSURLSession

发表于 2014-07-11   |   分类于 翻译

NSURLSession及相关的类提供通过HTTP协议下载数据的API。该类提供了大量代理方法来支持认证和后台下载(程序未运行或挂起时)功能。

为了使用NSURLSession,我们的应用会创建一系列的会话,每个会话负责协调一组相关数据的传输任务。在每个会话中,我们的应用添加一系列的任务,每个任务都表示一个对指定URL的请求。与大多数网络API一样,NSURLSession API是异步的。如果我们使用系统提供的代理,我们必须提供一个请求完成处理block,以便在请求成功或失败时返回数据给我们的应用。如果我们提供自定义的代理对象,则任务对象调用这些代理方法,并回传从服务端获取的数据(如果是文件下载,则当传输完成时调用)。

NSURLSession提供了status和progress属性,并作为额外的信息传递给代理。同时它支持取消、恢复、挂起操作,并支持断点续传功能。

要掌握NSURLSession的使用,我们需要了解下URL会话的一些内容

URL会话

在一个会话中的任务的行为取决于三个方面:

  1. session的类型(由创建会话时的配置对象确定)
  2. 任务的类型
  3. 当任务创建时应用是否在前台

NSURLSession支持以下三种会话类型:

  1. 默认会话:行为与其它下载URL的Foundation方法类似。使用基于磁盘的缓存策略,并在用户keychain中存储证书。
  2. 短暂会话(Ephemeral sessions):不存储任何数据在磁盘中;所有的缓存,证书存储等都保存在RAM中并与会话绑定。这样,当应用结束会话时,它们被自动释放。
  3. 后台会话(Background sessions):类似于默认会话,除了有一个独立的进程来处理所有的数据传输。

在一个会话中,NSURLSession支持三种任务类型

  1. 数据任务:使用NSData对象来发送和接收数据。数据任务可以分片返回数据,也可以通过完成处理器一次性返回数据。由于数据任务不存储数据到文件,所以不支持后台会话.
  2. 下载任务:以文件的形式接收数据,当程序不运行时支持后台下载
  3. 上传任务:通常以文件的形式发送数据,支持后台上传。

NSURLSession支持在程序挂起时在后台传输数据。后台传输只由使用后台会话配置对象创建的会话提供。使用后台会话时,由于实际传输是在一个独立的进程中传输,且重启应用进程相当损耗资源,只有少量特性可以使用,所以有以下限制:

  1. 会话必须提供事件分发的代理。
  2. 只支持HTTP和HTTPS协议
  3. 只支持上传和下载任务
  4. 总是伴随着重定义操作
  5. 如果当应用在后台时初始化的后台传输,则配置对象的discretionary属性为true

在iOS中,当我们的应用不再运行时,如果后台下载任务完成或者需要证书,则系统会在后台自动重启我们的应用,同时调用UIApplicationDelegate对象的application:handlerEventsForBackgroundURLSession:completionHandler:方法。这个调用会提供启动的应用的session的标识。我们的应用应该存储完成处理器,使用相同的标识来创建后台配置对象,然后使用配置对象来创建会话。新的会话会与运行的后台activity关联。当会话完成后台下载任务时,会给会话代理发送一个URLSessioinDidFinishEventsForBackgroundURLSession:消息。我们的代理对象然后调用存储的完成处理器。

如果在程序挂起时有任何任务完成,则会调用URLSession:downloadTask:didFinishDownloadingToURL:方法。同样的,如果任务需要证书,则NSURLSession对象会在适当的时候调用URLSession:task:didReceiveChallenge:completionHandler: 和URLSession:didReceiveChallenge:completionHandler:方法。

这里需要注意的是必须为每个标识创建一个会话,共享相同标识的多个会话的行为是未定义的。

会话和任务对象实现了NSCopying协议:

  1. 当应用拷贝一个会话或任务对象时,会获取相同对象的指针
  2. 当应用拷贝一个配置对象时,会获取一个可单独修改的新的对象

创建并配置NSURLSession

我们下面举个简单的实例来说明一个NSURLSession与服务端的数据交互。

代码清单1:声明三种类型会话对象

1
2
3
4
5
6
7
8
9
10
11
12
@interface URLSession : NSObject <NSURLSessionDelegate, NSURLSessionTaskDelegate, NSURLSessionDataDelegate, NSURLSessionDownloadDelegate>
@property (nonatomic, strong) NSURLSession *backgroundSession;
@property (nonatomic, strong) NSURLSession *defaultSession;
@property (nonatomic, strong) NSURLSession *ephemeralSession;
@property (nonatomic, strong) NSMutableDictionary *completionHandlerDictionary;
- (void)addCompletionHandler:(CompletionHandlerType)handler forSession:(NSString *)identifier;
- (void)callCompletionHandlerForSession:(NSString *)identifier;
@end

NSURLSession提供了大量的配置选项,包括:

  1. 支持缓存、cookie,认证及协议的私有存储
  2. 认证
  3. 上传下载文件
  4. 每个主机的配置最大数
  5. 超时时间
  6. 支持的TLS最小最小版本
  7. 自定义代理字典
  8. 控制cookie策略
  9. 控制HTTP管道行为

由于大部分设置都包含在一个独立的配置对象中,所以我们可以重用这些配置。当我们初始一个会话对象时,我们指定了如下内容

  1. 一个配置对象,用于管理其中的会话和任务的行为
  2. 一个代理对象,用于在收到数据时处理输入数据,及会话和任务中的其它事件,如服务端认证、确定一个资源加载请求是否应该转换成下载等。这个对象是可选的。但如果我们需要执行后台传输,则必须提供自定义代理。

在实例一个会话对象后,我们不能改变改变配置或代理。

代码清单2演示了如何创建一个会话

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
NSURLSessionConfiguration *defaultConfigObject = [NSURLSessionConfiguration defaultSessionConfiguration];
        
// 配置会话的缓存
NSString *cachePath = @"/MyCacheDirectory";
        
NSArray *pathList = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
NSString *path = [pathList objectAtIndex:0];
        
NSString *bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier];
        
NSString *fullCachePath = [[path stringByAppendingPathComponent:bundleIdentifier] stringByAppendingPathComponent:cachePath];
        
NSLog(@"Cache path: %@", fullCachePath);
        
NSURLCache *cache = [[NSURLCache alloc] initWithMemoryCapacity:16384 diskCapacity:268435456 diskPath:cachePath];
defaultConfigObject.URLCache = cache;
defaultConfigObject.requestCachePolicy = NSURLRequestUseProtocolCachePolicy;
        
self.defaultSession = [NSURLSession sessionWithConfiguration:defaultConfigObject delegate:self delegateQueue:[NSOperationQueue mainQueue]];

除了后台配置对象外,我们可以重用会话的配置对象来创建新的会话,正如上面所讲的,拷贝一个配置对象会生成一个新的独立的配置对象。我们可以在任何时候安全的修改配置对象。当创建一个会话时,会话会对配置对象进行深拷贝,所以修改只会影响到新的会话。代理清单3演示了创建一个新的会话,这个会话使用重用的配置对象。

代码清单3:重用会话对象

1
2
3
4
5
6
self.ephemeralSession = [NSURLSession sessionWithConfiguration:ephemeralConfigObject delegate:self delegateQueue:[NSOperationQueue mainQueue]];
        
ephemeralConfigObject.allowsCellularAccess = YES;
        
// ...
NSURLSession *ephemeralSessionWifiOnly = [NSURLSession sessionWithConfiguration:ephemeralConfigObject delegate:self delegateQueue:[NSOperationQueue mainQueue]];

使用NSURLSession获取数据基本就是两步:

  1. 创建一个配置对象及基于这个对象的会话
  2. 定义一个请求完成处理器来处理获取到的数据。

如果使用系统提供的代理,只需要代码清单4这几行代码即可搞定

代码清单4:使用系统提供代理

1
2
3
4
5
NSURLSession *delegateFreeSession = [NSURLSession sessionWithConfiguration:defaultConfigObject delegate:self delegateQueue:[NSOperationQueue mainQueue]];
[delegateFreeSession dataTaskWithRequest:@"http://www.sina.com"
                               completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {    
                                   NSLog(@"Got response %@", response);
                               }];

只是系统提供的代理只提供有限的网络行为。如果应用需要更多的处理,如自定义认证或后台下载等,则需要使用自定义的代理。使用自定义代理来获取数据时,代理必须实现以下方法:

  1. URLSession:dataTask:didReceiveData: 从请求提供数据给我们的任务,一次一个数据块
  2. URLSession:task:didCompleteWithError: 表示任务已经接受了所有的数据。

如果我们在URLSession:dataTask:didReceiveData:方法返回后使用数据,则需要将数据存储在某个地方。

代码清单5:演示了一个数据访问实例:

1
2
3
NSURL *url = [NSURL URLWithString:@"http://www.sina.com"];
NSURLSessionDataTask *dataTask = [self.defaultSession dataTaskWithURL:url];
[dataTask resume];

如果远程服务器返回的状态表示需要一个认证,且认证需要连接级别的处理时,NSURLSession将调用认证相关代理方法。这个具体我们后面文章将详细讨论。

处理iOS后台Activity

在iOS中使用NSURLSession时,当一个下载完成时,会自动启动我们的应用。应用的代理方法application:handleEventsForBackgroundURLSession:completionHandler: 负责创建一个合适的会话,存储请求完成处理器,并在会话调用会话代理的URLSessionDidFinishEventsForBackgroundURLSession: 方法时调用这个处理器。代码清单6与代码清单7演示了这个处理流程

代码清单6:iOS后台下载的会话代理方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session
{
    NSLog(@"background url session %@", session);
    
    if (session.configuration.identifier)
    {
        [self callCompletionHandlerForSession:session.configuration.identifier];
    }
}
- (void)callCompletionHandlerForSession:(NSString *)identifier
{
    CompletionHandlerType handler = [self.completionHandlerDictionary objectForKey:identifier];
    
    if (handler) {
        [self.completionHandlerDictionary removeObjectForKey:identifier];
        
        handler();
    }
}

代码清单7:iOS后台下载的App 代理方法

1
2
3
4
5
6
7
8
9
10
11
12
- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler
{
    NSURLSessionConfiguration *backgroundConfigObject = [NSURLSessionConfiguration backgroundSessionConfiguration:identifier];
    
    URLSession *sessionDelegate = [[URLSession alloc] init];
    
    NSURLSession *backgroundSession = [NSURLSession sessionWithConfiguration:backgroundConfigObject
                                                                    delegate:sessionDelegate
                                                               delegateQueue:[NSOperationQueue mainQueue]];
    
    [sessionDelegate addCompletionHandler:completionHandler forSession:identifier];
}

参考

  1. URL Loading System Programming Guide

URL加载系统之一:基本结构

发表于 2014-07-11   |   分类于 翻译

URL加载系统是一组类和协议的集合,它允许我们的App访问URL指定的内容的。

URL加载系统的核心类是NSURL,该类提供了大量方法让我们操作URLs和它指向的资源。另外它还提供了一系列的类来加载URL的内容,上传数据到服务器,管理Cookie存储,控制响应缓存,处理认证存储和授权信息,及自定义协议扩展。

URL Loading System可支持以下协议

  1. ftp://
  2. http://
  3. https://
  4. file://
  5. data://

另外它还支持代理服务和网关处理。

URL加载系统定义了用于加载URL的类,另外还定义了一些辅助类来修改加载类的行为。这些辅助类可以分为五大类:

  1. 协议支持
  2. 授权与认证
  3. Cookie存储
  4. 配置管理
  5. 缓存管理

整个URL加载系统的结构如下图所示:

image

下面对这张图做个简单的介绍

URL Loading

在这张图中,我们用得最多的就是URL Loading中的这几个类。这些类允许我们从URL指定的源获取内容。根据不同的需求,我们可以使用不同的类,这主要依赖于我们应用所支持的系统版本,以及我们希望内容是以文件的形式获取还是以数据块的方式获取。对于系统的版本,主要有以下几点作为参考:

  1. 在iOS7及后续版本中,推荐使用NSURLSession。
  2. 对于iOS7以前的版本,可以使用NSURLConnection来获取数据并加载到本地内存中。如果要保存数据,可以再将数据写入磁盘。

而对于获取数据,主要看我们是获取数据到内存中还是下载文件并保存。如果只是获取数据到内存中,则有两种方法:

  1. 对于简单的请求,可以使用NSURLSession
  2. 对于复杂的请求(如上传数据请求),提供了NSURLRequest对象来与NSURLSession和NSURLConnection一起使用。

不管使用哪种方法,我们都可以获取到响应数据,为此,我们可以如下处理响应

  1. 提供一个响应处理block。当URL Loading类完成从服务端接收数据时调用该block.
  2. 提供自定义有delegate。URL Loading类间断性地调用我们的代理方法来获取数据。在需要的情况下,我们的程序负责收集这些数据。

另外,URL Loading提供了一个返回对象来封装与请求相关的元数据,如MIME类型等。

而如果我们需要下载文件,则有两个基本方法来处理:

  1. 对于简单请求,可以使用NSURLSession
  2. 对于复杂请求,提供了NSURLRequest对象来与NSURLSession和NSURLDownload一起使用。

相较于NSURLDownload,NSURLSession有两个明显的优势:NSURLSession可用于iOS系统,而NSURLDownload在iOS中不被支持;当应用挂起、终止或异常退出时,下载可以在后台继续进行。

URL Loading中还提供了两个类用于处理元数据,一个用于表示客户端请求(NSURLRequest),一个用于表示服务端响应(NSURLResponse)。我们分别介绍一下这两个类。

NSURLRequest

NSURLRequest对象封装了URL和协议指定的属性,及依赖于协议的行为。同时也指定了本地缓存策略及连接超时时间。一些协议支持协议指定的属性,如HTTP协议可以添加返回HTTP请求体,请求报头和传输方法到NSURLRequest中。

这里需要注意的是,当我们使用NSURLRequest的子类NSMutableURLRequest初始化一个连接或下载时,将会对NSMutableURLRequest实例进行深拷贝。因此在初始的请求上做修改时不会影响到连接和下载对象。

NSURLResponse

一个响应可以分为两个部分:描述内容的元数据和内容数据本身。而NSURLResponse类封装了大部分协议的响应元数据,这些元数据包括MIME类型,期望的Content-Length,编码格式,及提供响应的URL。NSURLResponse的一些子类提供了与协议相关的额外元数据。如NSHTTPURLResponse存储了web服务器返回的响应头和状态码。

需要注意的是NSURLResponse对象只存储响应的元数据,而不存储响应数据本身。响应数据由URL Loading通过响应处理block和对象的代理来接收并处理。

认证和证书

针对认证和证书,URL加载系统提供了以下几个类:

  1. NSURLCredential:封装了由认证信息和持久化行为组成的证书。
  2. NSURLProtectionSpace:表示需要特定证书的区域。一个保护区域可以限制到单独的URL,拥有web服务器的区域,或引用一个代理。
  3. NSURLCredientialStorage:一般是一个共享实例,用于管理证书存储和提供NSURLCredential对象到NSURLProductionSpace对象的映射。
  4. NSURLAuthenticationChallenge:封装了认证一个请求的的NSURLProtocol实现所需要的信息:一个建议的证书、保护空间、错误信息或者协议用于确定所需要认证的响应、以及认证尝试次数等。初始对象(即请求发送者)必须实现NSURLAuthenticationChallengeSender协议。NSURLAuthenticationChallenge实例被用于NSURLProtocol的子类来告诉URL加载系统需要认证。他们同样为NSURLConnection和NSURLDownload的代理方法提供了便利的自定义认证处理。

缓存管理

URL加载系统提供基于磁盘和内存的缓存,允许程序减少对网络连接的依赖,并提供对缓存响应的快速访问。缓存存储在每个app的缓存文件夹下。NSURLConnection会根据缓存策略(初始化NSURLRequest对象中指定的)来查询缓存。

NSURLCache提供了配置缓存大小和磁盘存储位置的方法。同时提供了包含缓存响应的NSCacheURLResponse对象集合的方法。NSCacheURLResponse对象封装了NSURLResponse对象和URL数据,同时提供用户信息字典,这些信息可以用于缓存任何用户数据。

不是所有的协议实现都支持响应缓存。当前只有http和https请求可被缓存。

一个NSURLConnection对象可以通过connection:willCacheResponse:代理访求来控制是否缓存响应,响应是否只应该存储在内存中。

Cookie存储

由于HTTP协议是无状态的,所以客户端通常使用cookie来保存URL请求的数据。URL加载系统提供了接口来创建和管理cookie,将cookie作为HTTP请求的一部分来发送,及解析web服务端响应数据时接收cookie.

iOS提供了NSHTTPCookieStorage类来管理一个NSHTTPCookie对象的集合。

协议支持

URL加载系统默认支持http, https, file, ftp, data协议。另外,URL加载系统也允许我们注册自己的类来支持额外的系统层级的网络协议。我们也可以添加指定协议的属性到URL请求和URL响应对象

Swift和Objective-C混合编程

发表于 2014-07-08   |   分类于 Swift

Swift和Objective-C可以在同一个工程中共存。不管创建工程时选择的是Swift还是Objective-C作为初始语言,我们都可以在工程中添加另一种语言的文件。

混合编程的处理过程在App工程和库工程中稍微有点不同。具体的工作模型如下图所示

image

下面我们讨论下具体的操作

同一App工程中导入代码

在Swift工程中导入Objective-C

如果我们需要在一个Swift工程中导入Objective-C代码,需要依托于Objective-C Bridging header(桥接头文件)。在Swift工程中,当我们添加一个Objective-C文件时,如果工程中没有现存的Bridging header文件,则XCode会提示我们是否创建该文件。如果我们点击Yes,则XCode会自动创建头文件,并命名为”工程名-Bridging-Header.h”。

我们需要编辑这个文件,以导入我们的Objective-C代码。

如果是从同一个target中导入Objective-C代码,则我们需要做如下操作

  1. 在Bridging header文件中,import所有需要在Swift中使用的头文件
1
2
3
#import "XYZCustomCell.h"
#import "XYZCustomView.h"
#import "XYZCustomViewController.h"
  1. 在Build Settings中,确保Swift Compiler Code Generation->Objective-C Bridging Header下的头文件的路径是对的。路径必须直接指向文件本身,而不是文件所在的文件夹。

所有在Bridging header中的公有Objective-C头文件在Swift都是可见的,并且其中的所有功能在所有Swift中都是可用的,而不需要任何导入处理。可以像使用Swift代码一样使用Objective-C代码。如下所示

1
2
let myCell = XYZCustomCell()
myCell.subtitle = "A custom cell"

在Objective-C中导入Swift

如果我们要在一个Objective-C工程中导入Swift,则需要依托于XCode-generated header文件。这个自动生成的文件是一个Objective-C头文件,其声明了在我们的target中使用的所有Swift接口。

Swift对C指针实现浅析

发表于 2014-07-06   |   分类于 Swift

个人认为,Swift对指针的处理略显复杂。

我们通过调试可以看到存在这样一个类型Builtin.RawPointer,我们可以假设其为C指针在Swife中的内部表示,但不可以直接使用。相反,在Swift中,定义了7种指针类型,可以通过这7种类型来操作C指针。

COpaquePointer

在C语言中,常使用typedef来定义一些指针类型的别名,如

1
typedef struct stack *stack_t

该定义表示一个指向栈结构的指针,但并没有给出结构的任何信息,我们不知道该结构都有哪些成员。类似于stack_t这样的指针就是一个不透明的指针。在程序中,我们可以自由的操纵这种指针,但无法反引用以获取指针的内部信息,只有接口的实现才有这种特权。这所以使用不透明指针,在于其隐藏了具体的实现细节,有助于捕获错误。即只能传相同的参数给函数,否则将产一变异错误。(参看《C语言接口与实现:创建可重用软件的技术》)

在Swift中,针对这种不透明指针,定义了一个包装器:COpaquePointer。该类型(实际为结构体)是一个不透明指针的包装器,主要用于Bridge Header中表示C中的复杂结构指针,当我们的指针指向的类型无法在Swift中有效地表示出来时,就可以使用该类型,而如果在Swift能找到对应的类型表示指针指向的类型,则可以使用UnsafePointer。

该类的实现及其扩展实现了四个协议,我们看看其具体代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct COpaquePointer : Equatable, Hashable, LogicValue {
init()
static func null() -> COpaquePointer
func getLogicValue() -> Bool
var hashValue: Int { get }
}
extension COpaquePointer {
/// 将UnsafePointer类型转换为不透明C指针
init<T>(_ from: UnsafePointer<T>)
}
extension COpaquePointer : CVarArg {
func encode() -> Word[]
}

可以看到该结构体并没有做太多的事情,仅仅实现了四个协议的接口,同时做了个UnsafePointer类型的转换操作。该类型在Swift使用得比较多,例如在NSData中,bytes属性的类型就是COpaquePointer,而该属性在Objective-C中的类型是const void*。

UnsafePointer

UnsafePointer一个泛型结构体,可以说是处理C指针最主要的结构体了。它包装并存储了类型为T的C指针,主要用于与C标准库交互。该类没有提供自动管理内存功能,所以我们在使用的时候需要注意内存的分配与释放。当在Swift能找到与T类型相对应的类型时,可以使用该类型,否则就考虑使用COpaquePointer。

在该类型的声明中,我们可以看到它定义了大量的方法来处理指针,具体如下

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
struct UnsafePointer<T> : BidirectionalIndex, Comparable, Hashable, LogicValue {
/// 构造一个空指针
init()
/// 将一个不透明指针转换为类型指定的C指针,注意这种转换是不安全的
init(_ other: COpaquePointer)
/// 从给定的内存地址中构造一个UnsafePointer。注意这种转换是不安全的
init(_ value: Int)
/// 从一个不同类型的指针中转换。注意这种转换是不安全的
init<U>(_ from: UnsafePointer<U>)
static func null() -> UnsafePointer<T>
static func alloc(num: Int) -> UnsafePointer<T>
func dealloc(num: Int)
/// 访问底层原始内存,获取并设置其值
var memory: T
/// 初始化指针指向的值,用于构造一个事先未存储对象的对象
func initialize(newvalue: T)
/// 获取指针指向的值,并将其从内存引用的位置移除。
/// 后置条件:值被销毁,且内存在再次使用前必须被初始化
func move() -> T
/// 将起始于source处的count个值赋值给未初始化的内存,将原始值转换到原始内存中,并且
/// 是从最后开始处理,一直到第一个。使用该方法将值拷贝到可能与原范围重叠的内存区域
/// 这要求source的位置先于self或者在self+count之后
func moveInitializeBackwardFrom(source: UnsafePointer<T>, count: Int)
/// Assign from count values beginning at source into initialized
/// memory, transforming the source values into raw memory.
/// 将起始于source处的count个值赋值给已初始化的内存,将原始值转换到原始内存中
func moveAssignFrom(source: UnsafePointer<T>, count: Int)
/// 拷贝起始于source的count个值到内存中,将源值转换到内存中
func moveInitializeFrom(source: UnsafePointer<T>, count: Int)
/// 拷贝起始于source的count个值到内存中
func initializeFrom(source: UnsafePointer<T>, count: Int)
/// 拷贝C元素到内存中
func initializeFrom<C : Collection where T == T>(source: C)
/// 销毁指针指向的对象
func destroy()
/// 销毁指针指向的count个对象
func destroy(count: Int)
func getLogicValue() -> Bool
subscript (i: Int) -> T
var hashValue: Int { get }
func succ() -> UnsafePointer<T>
func pred() -> UnsafePointer<T>
/// 以下是将其它类型的指针转换化UnsafePoint的构造器
init(_ cp: CConstPointer<T>)
init(_ cm: CMutablePointer<T>)
init(_ op: AutoreleasingUnsafePointer<T>)
init(_ cp: CConstVoidPointer)
init(_ cp: CMutableVoidPointer)
}
extension UnsafePointer<T> : Printable {
var description: String { get }
}

UnsafePointer提供了多个构造器,以从其它指针类型创建一个UnsafePointer。另外,CMutablePointer、CMutableVoidPointer、CConstPointer、CConstVoidPointer都提供了一个withUnsafePointer方法,该方法是让这些指针类型在一个闭包中可以像UnsafePointer类型一样使用。我们来看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func StringFromCptr(inChars:CMutablePointer<CChar>) -> String {
var ptr:UnsafePointer<UInt8> = UnsafePointer<UInt8>(0)
inChars.withUnsafePointer({ p in ptr = UnsafePointer<UInt8>(p) })
var str = CString(ptr)
return NSString(CString:str)
}
var Len = 10
var Str = CChar[](count:Len, repeatedValue:CChar(0))
for var i = 0; i < Len; i++ {
Str[i] = CChar(i + 65)
}
var Result = StringFromCptr(&Str)
println("\(Result)")
// 输出:ABCDEFGHIJ

目前感觉在通常情况下,UnsafePointer使用得不多,平时接触到的更多的是CMutablePointer、CMutableVoidPointer、CConstPointer、CConstVoidPointer、AutoreleasingUnsafePointer这五种指针类型,这五种指针的基本使用方法我们在Swift中C指针的基本使用方法有简单介绍过,下面我们也讲讲它们的基本实现。

AutoreleasingUnsafePointer

该类型的基本定义是一个指向Objective-C指针的可变指针。这个类型有几种隐式转换来允许传递下面几种参数类型给一个一个C或ObjC API:

  1. nil, 作为null指针传入
  2. 被引用类型的in-out参数,作为一个带有自动释放所有权语义的可回写变量的指针传递
  3. UnsafePointer

与CMutablePointer不同的是,Swift不直接支持传递一个指向元素是Objc类指针数组的指针。与UnsafePointer不同的是,AutoreleasingUnsafePointer必须引用一个存储,该存储没有其引用值的引用计数。相反,UnsafePointer的操作假设引用的存储拥有其加载的值且可以对其进行修改。

该类型不像其它的C*Pointer类型一样携带一个所有者指针,因为它只需要引用in-out参数转换的结果,而这个参数已经有一个回写域的生命周期了。

我们来看看其声明:

1
2
3
4
5
6
7
struct AutoreleasingUnsafePointer<T> : Equatable, LogicValue {
func getLogicValue() -> Bool
/// Access the underlying raw memory, getting and
/// setting values.
var memory: T
}

CConstPointer与CConstVoidPointer

这两个类型都是C常量指针,分别对应于C中的const T和const void。它们没有自己的操作。它的值由owner-value对组成,也正因此,它不能直接传递给C函数。在桥接的过程中,它会维护一个owner的强引用,且指针值被传递到C或者Objective-C的入口点。这允许拥有堆存储的类型(如数组)将自己转换为一个指针,同时仍保证在调用的过程中仍然维护它们的存储。我们可以看下它们的具体实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct CConstPointer<T> : Equatable {
let owner: AnyObject?
/// True if this is a scoped pointer, meaning it has a owner reference
/// that guarantees the lifetime of the referenced memory.
var scoped: Bool { get }
/// Make the pointer available as an UnsafePointer within a closure.
func withUnsafePointer<U>(f: UnsafePointer<T> -> U) -> U
}
struct CConstVoidPointer : Equatable {
let owner: AnyObject?
/// True if this is a scoped pointer, meaning it has a owner reference
/// that guarantees the lifetime of the referenced memory.
var scoped: Bool { get }
/// Make the pointer available as an UnsafePointer within a closure.
func withUnsafePointer<T, U>(f: UnsafePointer<T> -> U) -> U
}

CMutablePointer与CMutableVoidPointer

这两个类型都是C可变指针,分别对应于C中的T 和void ,其基本内容同上。

总结

由上面的分析,依据类型是否可以直接用于C函数声明,可以将这7种指针类型分为两类:

  1. 可直接用于C函数声明:COpaquePointer,UnsafePointer,AutoreleasingUnsafePointer,它们是对Builtin.RawPointer的封装,直接对应于C指针,其sizeof是单位字长
  2. 不可直接用于C函数声明:CMutablePointer, CConstPointer, CMutableVoidPointer, CConstVoidPointer。这几个类型都有一个owner属性用于管理实例的生命周期,可以直接从Swift对象的引用获得,另外还可以使用withUnsafePointer方法,该方法是让这些指针类型在一个闭包中可以像UnsafePointer类型一样使用。

另外有两点需要注意

  1. 非常量指针都实现了LogicValue协议,因此可以直接使用if a_pointer来判断其是否为NULL。
  2. nil类型实现了到所有指针类型的隐式类型转换,等价于C中的NULL,可以直接判断。

至于这些指针的使用,需要在根据实现情况来具体处理。

参考

  1. 《C语言接口与实现:创建可重用软件的技术》
  2. Swift.String to CMutablePointer
  3. Swift and C Interop Cont. (简析 Swift 和 C 的交互,Part 二)

Cover flow基本原理及Tapku实现方法

发表于 2014-07-05   |   分类于 源码分析

这篇是两年前在CocoaChina上写的,现在把它归集到这边来。大家也可以查看原文。

Cover flow是苹果首创的将多首歌曲的封面以3D界面的形式显示出来的方式。如下图所示:

image

从图中可以看到,显示在中间的图片为目标图片,两侧的图片在y轴都旋转了一定的角度,并且每两张图片之间都保持了一定的距离。在交互(如点击两侧的图片)的时候,滑动到中间的图片会逐渐放大,旋转的角度由原来的旋转角度a变为0,且位置上移动中间,变成新的目标图片;同时原处于中间位置的图片则缩小、旋转一定的角度、位置偏移到一侧。所以在整个过程中,主要有两个属性发生了变化:角度与位置(缩放只是视觉上的,并没有进行缩放操作)。

在每次点击一张图片时,如果这张图片在目标图片的左边,则所有的图片都会向右移动,同时做相应的旋转;相反,点击目标图片右侧的图片时,所有图片都会向左移动并做相应的旋转。

从如上描述的效果,可以看出在Cover Flow中最主要的的操作有两个:3D仿射变换与动画。仿射变换实质上是一种线性变换,通常主要用到的仿射变换有平移(Translation)、旋转(Rotation)、缩放(Scale)。
对于这两种操作,iOS都提供了非常简便的接口来实现。接下来我们便以tapku的实现方法为例,来说明实现Cover Flow的基本过程。

图片的布局

从效果图可以看出,图片是按一条直接排列,图片与图片之间有一定的间距,目标图片是正向显示,两侧的图片则按一定的角度进行了旋转,与目标图片形成一定的角度。同时我们还能看到每个图片都有一个倒影,并且这个倒影是渐变的,由上而下逐渐透明度逐渐减小。

Cover Flow单元的定义

在tapku中,每一个图片附属于一个视图(TKCoverflowCoverView),这个视图相当于UITableViewCell,它包含了三个要素:图片(imageView),倒影图片(reflected),渐变层(gradientLayer)。渐变层覆盖于倒影图片上,且大小位置一致。

TKCoverflowCoverView的声明及布局代码如下所示:

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
@interface TKCoverflowCoverView : UIView {
float baseline;
UIImageView *imageView;
UIImageView *reflected;
CAGradientLayer *gradientLayer;
}
@end
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.opaque = NO;
self.backgroundColor = [UIColor clearColor];
self.layer.anchorPoint = CGPointMake(0.5, 0.5);
imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, self.frame.size.width, self.frame.size.width)];
[self addSubview:imageView];
reflected = [[UIImageView alloc] initWithFrame:CGRectMake(0, self.frame.size.width, self.frame.size.width, self.frame.size.width)];
reflected.transform = CGAffineTransformScale(reflected.transform, 1, -1);
[self addSubview:reflected];
gradientLayer = [CAGradientLayer layer];
gradientLayer.colors = [NSArray arrayWithObjects:(id)[UIColor colorWithWhite:0 alpha:0.5].CGColor,(id)[UIColor colorWithWhite:0 alpha:1].CGColor,nil];
gradientLayer.startPoint = CGPointMake(0, 0);
gradientLayer.endPoint = CGPointMake(0, 0.4);
gradientLayer.frame = CGRectMake(0, self.frame.size.width, self.frame.size.width, self.frame.size.width);
[self.layer addSublayer:gradientLayer];
}
return self;
}

注意:此次将视图的锚点(anchorPoint属性)设置为(0.5, 0.5),即视图的中心点,目的是让视图以中心点为基点进行旋转。

在进行仿射变换时,视图作为一个整体进行变换。

图片的布局

tapku中,图片的布局与交互是在TKCoverflowView类中完成的。类TKCoverflowView继承自UIScrollView,相当于是TableView。

该类中定义是两个仿射变量:

1
CATransform3D leftTransform, rightTransform

这两个变量分别设置了两侧图片的仿射变换,具体的设置方法为

1
2
3
4
5
6
- (void) setupTransforms{
leftTransform = CATransform3DMakeRotation(coverAngle, 0, 1, 0);
leftTransform = CATransform3DConcat(leftTransform,CATransform3DMakeTranslation(-spaceFromCurrent, 0, -300));
rightTransform = CATransform3DMakeRotation(-coverAngle, 0, 1, 0);
rightTransform = CATransform3DConcat(rightTransform,CATransform3DMakeTranslation(spaceFromCurrent, 0, -300));
}

其中coverAngle为旋转的角度。对每个仿射变量同时设置了旋转也平移变换。

Cover Flow单元是存储在一个数组中:

1
NSMutableArray *coverViews;

初始化时设置数组的大小,并存入空对象。在后期获取某个索引位置的单元时,如果该单元为空,则生成一个新的TKCoverflowCoverView并放入相应位置。

1
2
3
4
5
if([coverViews objectAtIndex:cnt] == [NSNull null]){
TKCoverflowCoverView *cover = [dataSource coverflowView:self coverAtIndex:cnt];
[coverViews replaceObjectAtIndex:cnt withObject:cover];
......
}

每个Cover Flow单元的位置计算如下

1
2
3
4
CGRect r = cover.frame;
r.origin.y = currentSize.height / 2 - (coverSize.height/2) - (coverSize.height/16);
r.origin.x = (currentSize.width/2 - (coverSize.width/ 2)) + (coverSpacing) * cnt;
cover.frame = r;

其中currentSize,coverSize,coverSpacing都是固定值。从中可以看出所有单元的y值都是一样的,而x值则与其在数组中的索引值相关,索引越大,x值也越大。而这就涉及我们之后的一个问题。一会再讲。
假定目标图片的索引为currentIndex,则目标图片两侧的仿射属性设置如下:

1
2
3
4
5
6
if(cnt > currentIndex){
cover.layer.transform = rightTransform;
}
else {
cover.layer.transform = leftTransform;
}

如上即为Cover Flow的基本布局,可以与UITableView比较一下。

交互

Cover Flow的基本交互是点击两侧的图片,则被点击的图片成为新的目标图片并移动中屏幕中间,而其它图片一起移动,在这个过程中所需要做的就两件事:旋转与平移。

方法很简单:在动画块中重新设置Cover Flow单元的transform属性,这样就可以缓动实现这个动画的过程。实际上只有新旧目标图片及中间的图片需要做这种变换,其余图片的transform属性都是不变的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
float speed = 0.3;
[UIView beginAnimations:string context:nil];
[UIView setAnimationDuration:speed];
[UIView setAnimationCurve:UIViewAnimationCurveEaseOut];
[UIView setAnimationBeginsFromCurrentState:YES];
[UIView setAnimationDelegate:self];
[UIView setAnimationDidStopSelector:@selector(animationDidStop:finished:context:)];
for(UIView *v in views){
int i = [coverViews indexOfObject:v];
if(i < index) v.layer.transform = leftTransform;
else if(i > index) v.layer.transform = rightTransform;
else v.layer.transform = CATransform3DIdentity;
}
[UIView commitAnimations];

但这在做的只是旋转了Cover Flow的内容,并没有对Cover Flow进行水平平移,Cover Flow水平位置已由其origin.x值固定。那么水平上的平移是如何实现的呢,我们看下面的代码:

1
2
3
4
5
6
7
8
9
10
- (void) snapToAlbum:(BOOL)animated{
UIView *v = [coverViews objectAtIndex:currentIndex];
if((NSObject*)v!=[NSNull null]){
[self setContentOffset:CGPointMake(v.center.x - (currentSize.width/2), 0) animated:animated];
}
else
{
[self setContentOffset:CGPointMake(coverSpacing*currentIndex, 0) animated:animated];
}
}

其所做的就是以目标图片为中心,整体平移TKCoverflowView视图。

总结

由上可以看出,Cover Flow特效的原理很简单:对新旧目标图片及中间的图片以动画的形式做仿射变换。至于仿射变换如何处理,有不同的方法。tapku所实现的方法可以说相对简单灵活。

Android, Flash都有类似的Cover Flow特效实现方法,有兴趣的童鞋可以参考一下。

Swift中C指针的基本使用方法

发表于 2014-07-02   |   分类于 Swift

Swift尽可能避免让我们直接去访问指针。但当我们需要直接访问内存时,我们可以使用Swift提供的几种指针类型。在下面几个表中列出了各种情况下C类型指针语法与Swift语法对应关系,其中Type作为实际类型的占位符

对于函数参数,有以下对应关系

C语法 Swift语法
const void * CConstVoidPointer
void * CMutableVoidPointer
const Type * CConstPointer
Type * CMutablePointer

对于返回值,变量,二级以上的指针参数,有以下对应关系

C语法 Swift语法
void * COpaquePointer
Type * UnsafePointer

对于类类型,有以下对应关系

C语法 Swift语法
Type const CConstPointer
Type __strong CMutablePointer
Type ** AutoreleasingUnsafePointer

下面简单介绍一下这几种类型的指针

C可变指针

当我们声明一个包含CMutablePointer参数的指针时,可以接收以下类型的值

  1. nil, 作为空指针传入
  2. 一个CMutablePointer值
  3. 一个in-out表达式,它的操作数是Type类型的左值。该表达式作为左值地址传入
  4. 一个in-out Type[]值,它作为数组的首地址指针传入,且在调用时扩展了数据的生命周期

假如我们声明了如下一个函数:

1
func takesAMutablePointer(x: CMutablePointer<Float>) { /*...*/ }

那么我们可以用以下任何一种方式来调用

1
2
3
4
5
6
7
8
var x: Float = 0.0
var p: CMutablePointer<Float> = &x
var a: Float[] = [1.0, 2.0, 3.0]
takesAMutablePointer(nil)
takesAMutablePointer(p)
takesAMutablePointer(&x)
takesAMutablePointer(&a)

当声明一个包含CMutableVoidPointer参数的函数时,它可以接收与CMutablePointer相同的形参,其中Type为任意类型。需要注意的是如果直接传递CMutablePointer,目前的编译器会直接报编译错误

1
2
3
4
5
6
7
8
9
10
11
12
13
func takesAMutableVoidPointer(x: CMutableVoidPointer) { /* ... */ }
var x: Float = 0.0, y: Int = 0
var p: CMutablePointer<Float> = &x, q: CMutablePointer<Int> = &y
var a: Float[] = [1.0, 2.0, 3.0], b: Int[] = [1, 2, 3]
takesAMutableVoidPointer(nil)
//takesAMutableVoidPointer(p) 编辑错误:CMutablePointer<Float> is not convertible to CMutableVoidPointer
//takesAMutableVoidPointer(q)
takesAMutableVoidPointer(&x)
takesAMutableVoidPointer(&y)
takesAMutableVoidPointer(&a)
takesAMutableVoidPointer(&b)

C常量指针

当我们声明一个带有CConstPointer参数的函数时,可以接收以下类型的值:

  1. nil, 作为空指针传入
  2. 一个CMutablePointer, CMutableVoidPointer, CConstPointer, CConstVoidPointer, 或者AutoreleasingUnsafePointer类型的值,如果需要则会转换成CConstPointer
  3. 一个in-out表达式,它的操作数是Type类型的左值。该表达式作为左值地址传入
  4. 一个in-out Type[]值,它作为数组的首地址指针传入,且在调用时扩展了数据的生命周期

假如我们声明如下函数:

1
func takesAConstPointer(x: CConstPointer<Float>) { /*...*/ }

那么我们可以用以下任何一种方式来调用

1
2
3
4
5
6
7
var x: Float = 0.0
var p: CConstPointer<Float> = nil
takesAConstPointer(nil)
takesAConstPointer(p)
takesAConstPointer(&x)
takesAConstPointer([1.0, 2.0, 3.0])

当声明一个包含CConstVoidPointer参数的函数时,它可以接收与CConstPointer相同的形参,其中Type为任意类型。

1
2
3
4
5
6
7
8
9
10
11
12
func takesAConstVoidPointer(x: CConstVoidPointer) { /* ... */ }
var x: Float = 0.0, y: Int = 0
var p:CConstPointer<Float> = nil, q: CConstPointer<Int> = nil
takesAConstVoidPointer(nil)
//takesAConstVoidPointer(p)
//takesAConstVoidPointer(q)
takesAConstVoidPointer(&x)
takesAConstVoidPointer(&y)
takesAConstVoidPointer([1.0, 2.0, 3.0])
takesAConstVoidPointer([1, 2, 3])

AutoreleasingUnsafePointer

当我们声明一个带有AutoreleasingUnsafePointer参数的函数时,可以接收以下类型的值:

  1. nil, 作为空指针传入
  2. 一个AutoreleasingUnsafePointer值
  3. 一个in-out表达式,其操作数是临时非所属(nonowning)缓冲区,存储了原始值的拷贝。缓冲区的地址被传递给调用函数,且在返回时,缓冲区的值被加载(loaded)、保留(retained)并重新指派(reassigned)给操作数

注意上面清单中不包含数组

如果我们声明了以下函数

1
func takesAnAutoreleasingUnsafePointer(x: AutoreleasingUnsafePointer<NSDate?>) { /*...*/ }

则可以用以下方式来调用

1
2
3
4
5
6
var x: NSDate? = nil
var p: AutoreleasingUnsafePointer<NSDate?> = nil
takesAnAutoreleasingUnsafePointer(nil)
takesAnAutoreleasingUnsafePointer(p)
takesAnAutoreleasingUnsafePointer(&x)

最后需要注意的是在Swift中,没有导入C函数指针。

参考文档:Using Swift with Cocoa and Objective-C

1…789
南峰子

南峰子

86 日志
7 分类
© 2017 南峰子
由 Hexo 强力驱动
主题 - NexT.Pisces