分类目录归档:Objective-C

重定向NSLog

平时我们写代码的时候,为了调试方便,总是会在代码中写入很多的NSLog(也可能是其它的日志框架等,例如大名鼎鼎的CocoaLumberjack),但是我们对于NSLog到底了解多少?NSLog的信息为什么Xcode能够获取的到?我们能自己写个程序获取所有的NSlog么?NSLog写入的信息到底在哪里?

NSLog输出到哪?

我们都知道,NSLog是一个C函数,它的函数声明是

1
void NSLog(NSString *format, ...)

系统对其说明是:Logs an error message to the Apple System Log facility.,它是用来输出信息到标准的Error控制台上去.其内部其实是使用Apple System Log(ASL:苹果自己实现的输出日志的一套接口)的API.在iOS真机设备上,使用ASL记录的log被缓存在一个文件中,直到设备被重启。

这里提到的ASL,都是放在ash.h这个头文件中,这套api可以获取指定的日志数据,具体可以参考ASL参考

从上面可以直到,NSLog默认被系统输出到了一个文件中,这个文件是哪个呢?NSLog默认的输出到了系统的 /var/log/syslog这个文件中,当然了,如果你的机器没有越狱,你是查看不了这个文件的.我手机是越狱的,于是乎验证了下,使用iTools等工具将真机的/var/log/syslog文件导出,下面就是这个文件的部分内容的截取 log.png 从中,我们可以看到,所有的APP的NSLog全部都是写到这个文件中的!!!

8579568eba9e647a921f503787c10.jpg

标准的err控制台

我们现在了解到了NSLog就是输出到文件syslog中,既然要往文件中写,那么肯定就有文件的句柄了,这个文件的句柄是多少呢? 在C语言中,我们有三个默认的句柄

1
2
3
#define stdin __stdinp
#define stdout __stdoutp
#define stderr __stderrp

其对应的iOS系统层面的上述三个句柄其实也就是下面的三个

1
2
3
#define STDIN_FILENO 0 /* standard input file descriptor */
#define STDOUT_FILENO 1 /* standard output file descriptor */
#define STDERR_FILENO 2 /* standard error file descriptor */

我们的NSLog输出的是到 STDERR_FILENO 上,我们可以使用c语言的输出到文件的fprintf来验证一下

1
2
NSLog(@"ViewController viewDidLoad");
fprintf (stderr, "%s\n""ViewController viewDidLoad222");

在Xcode的控制台可以看到输出

1
2
2016-06-15 12:57:17.286 TestNSlog[68073:1441419] ViewController viewDidLoad
ViewController viewDidLoad222

由于fprintf并不会像NSLog那样,在内部调用ASL接口,所以只是单纯的输出信息,并没有添加日期,进程名,进程id等,也不会自动换行。

NSLog的重定向

既然NSLog是写到STDERR_FILENO中去的,那么根据Unix的知识,我们可以重定向这个文件,让NSLog直接写到文件中去

1
2
3
4
5
6
//to log to document directory
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsPath = [paths objectAtIndex:0];
NSString *loggingPath = [documentsPath stringByAppendingPathComponent:@"/mylog.log"];
//redirect NSLog
freopen([loggingPath cStringUsingEncoding:NSASCIIStringEncoding], "a+", stderr);

利用c语言的freopen函数,进行重定向,将写往stderr的内容重定向到我们制定的文件中去,一旦执行了上述代码,那么在这个之后的NSLog将不会在控制台显示了,会直接输出在文件mylog.log中! 在模拟器中,我们可以使用终端的tail命令(tail -f mylog.log)对这个文件进行实时查看,就如同我们在xcode的输出窗口中看到的那样,你还可以结合grep命令进行实时过滤查看,非常方便在大量的日志信息中迅速定位到我们要的日志信息

1465984868218459.gif

在真机中,这种重定向有什么用处呢? 由于重定向到的文件是我们沙盒中的文件,那么就可以在我们的程序中写一段代码将这个文件发送给我们,远程的用户app出了问题,把日志发送给我们,我们就可以根据日志信息,找寻可能的问题所在!

也可以开启app的文件夹itunse共享。

配置共享文件夹:

在应用程序的Info.plist文件中添加UIFileSharingEnabled键,并将键值设置为YES。将您希望共享的文件放在应用程序的Documents目录。一旦设备插入到用户计算机,iTunes 9.1就会在选中设备的Apps标签中显示一个File Sharing区域。此后,用户就可以向该目录添加文件或者将文件移动到桌面计算机中

就是说,一旦设备连接上电脑,可以通过iTune查看指定应用程序的共享文件夹,将文件拷贝到你的电脑上看

一般我们都会在应用中放置一个开关,开启或者关闭Log日志的重定向,在上面,我们使用标准C的freopen将stderr重定向到我们的文件中了,那么问题来了,怎么重定向回去呢???

1
FILE * freopen ( const char * filename, const char * mode, FILE * stream );

要想重定向回去,那么我们需要知道stderr原来的文件路径,很遗憾,这个在不同平台中是不一样的,在iOS平台,由于沙盒机制,我们也并不能直接使用沙盒外的文件 对此,freopen将无能为力,要重定向回去,只能使用Unix的方法dup和dup2!

1
2
3
4
5
6
//在ios上可用的方式,还是得借助dup和dup2
int originH1 = dup(STDERR_FILENO);
FILE * myFile = freopen([loggingPath cStringUsingEncoding:NSASCIIStringEncoding], "a+", stderr);//这句话已经重定向了,现在NSLog都输出到文件中去了,
//……………….
//恢复原来的
dup2(originH1, STDERR_FILENO);//就可以了

其它重定向STDERR_FILENO的方式集锦

方式一 采用dup2的重定向方式

(选自:http://lizaochengwen.iteye.com/blog/1476080)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (void)redirectSTD:(int )fd{
    NSPipe * pipe = [NSPipe pipe] ;
    NSFileHandle *pipeReadHandle = [pipe fileHandleForReading] ;
    int pipeFileHandle = [[pipe fileHandleForWriting] fileDescriptor];
    dup2(pipeFileHandle, fd) ;
    [[NSNotificationCenter defaultCenter] addObserver:self
     selector:@selector(redirectNotificationHandle:)
     name:NSFileHandleReadCompletionNotification
     object:pipeReadHandle] ;
    [pipeReadHandle readInBackgroundAndNotify];
}
- (void)redirectNotificationHandle:(NSNotification *)nf{
    NSData *data = [[nf userInfo] objectForKey:NSFileHandleNotificationDataItem];
    NSString *str = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] ;
    //这里可以做我们需要的操作,例如将nslog显示到一个textview中,或者是存放到另一个文件中等等
    //self.logTextView.text = [NSString stringWithFormat:@"%@\n%@",self.logTextView.text, str];
    NSRange range;
    //range.location = [self.logTextView.text length] - 1;
    range.length = 0;
    //[self.logTextView scrollRangeToVisible:range];
    [[nf object] readInBackgroundAndNotify];
}

使用的时候

1
[self redirectSTD:STDERR_FILENO];

就可以将NSLOg的输出重定向到我们的通知中去!!!

方式二 使用GCD的dispatch Source

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
- (dispatch_source_t)_startCapturingWritingToFD:(int)fd  {
    int fildes[2];
    pipe(fildes);  // [0] is read end of pipe while [1] is write end
    dup2(fildes[1], fd);  // Duplicate write end of pipe "onto" fd (this closes fd)
    close(fildes[1]);  // Close original write end of pipe
    fd = fildes[0];  // We can now monitor the read end of the pipe
    char* buffer = malloc(1024);
    NSMutableData* data = [[NSMutableData alloc] init];
    fcntl(fd, F_SETFL, O_NONBLOCK);
    dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, fd, 0, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0));
    dispatch_source_set_cancel_handler(source, ^{
        free(buffer);
    });
    dispatch_source_set_event_handler(source, ^{
        @autoreleasepool {
            while (1) {
                ssize_t size = read(fd, buffer, 1024);
                if (size <= 0) {
                    break;
                }
                [data appendBytes:buffer length:size];
                if (size < 1024) {
                    break;
                }
            }
            NSString *aString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
            //printf("aString = %s",[aString UTF8String]);
            //NSLog(@"aString = %@",aString);
            //读到了日志,可以进行我们需要的各种操作了
        }
    });
    dispatch_resume(source);
    return source;
}

使用的时候

1
_sourt_t = [self _startCapturingWritingToFD:STDERR_FILENO];

记得,要自己保留返回的dispatchsourcet对象,不然其释放了,你就获取不到了!

ASL读取日志

以上的方式,都是重定向文件,一旦重定向后,那么NSLog就不会再写到系统的syslog中去了,也就意味着不能使用ASL接口获取到重定向后的数据了。

不重定向NSLog,怎么读取所有的log呢?

ASL读取log的核心代码

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
+ (NSMutableArray*)allLogMessagesForCurrentProcess
{
    asl_object_t query = asl_new(ASL_TYPE_QUERY);
    // Filter for messages from the current process. Note that this appears to happen by default on device, but is required in the simulator.
    NSString *pidString = [NSString stringWithFormat:@"%d", [[NSProcessInfo processInfo] processIdentifier]];
    asl_set_query(query, ASL_KEY_PID, [pidString UTF8String], ASL_QUERY_OP_EQUAL);
    aslresponse response = asl_search(NULL, query);
    aslmsg aslMessage = NULL;
    NSMutableArray *logMessages = [NSMutableArray array];
    while ((aslMessage = asl_next(response))) {
        [logMessages addObject:[SystemLogMessage logMessageFromASLMessage:aslMessage]];
    }
    asl_release(response);
    return logMessages;
}
//这个是怎么从日志的对象aslmsg中获取我们需要的数据
+(instancetype)logMessageFromASLMessage:(aslmsg)aslMessage
{
    SystemLogMessage *logMessage = [[SystemLogMessage alloc] init];
    const char *timestamp = asl_get(aslMessage, ASL_KEY_TIME);
    if (timestamp) {
        NSTimeInterval timeInterval = [@(timestamp) integerValue];
        const char *nanoseconds = asl_get(aslMessage, ASL_KEY_TIME_NSEC);
        if (nanoseconds) {
            timeInterval += [@(nanoseconds) doubleValue] / NSEC_PER_SEC;
        }
        logMessage.timeInterval = timeInterval;
        logMessage.date = [NSDate dateWithTimeIntervalSince1970:timeInterval];
    }
    const char *sender = asl_get(aslMessage, ASL_KEY_SENDER);
    if (sender) {
        logMessage.sender = @(sender);
    }
    const char *messageText = asl_get(aslMessage, ASL_KEY_MSG);
    if (messageText) {
        logMessage.messageText = @(messageText);//NSLog写入的文本内容
    }
    const char *messageID = asl_get(aslMessage, ASL_KEY_MSG_ID);
    if (messageID) {
        logMessage.messageID = [@(messageID) longLongValue];
    }
    return logMessage;
}

ASL的好处是没有重定向文件,所以不会影响Xcode等控制台的输出,它是一种非侵入式的读取的方式,类似于我们读取数据库的文件,我们只是读取数据,并没有将原来的数据库文件删除。

在app中内置一个小型的http web服务器

上面的方式,当测试,或者平时我们没有连接XCode时,想查看日志信息,还是不太方便,试想,如果我们在需要的时候,可以直接用浏览器查看输出的log信息那该多好?

结合上面的ASL和一个小型的web服务器,我们就可以实现了,

对于httpserver github上比较知名的有

CocoaHTTPServer,这个已经三年没更新了,不推荐使用 GCDWebServer 作者一直在维护,据说性能也不错,推荐使用这个,下面的demo也使用的这个

摘录其中的部分代码如下:

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
#define kMinRefreshDelay 500  // In milliseconds
@interface HttpServerLogger ()
@property (nonatomic,strong) GCDWebServer* webServer;
@end
@implementation HttpServerLogger
+ (instancetype)shared {
    static dispatch_once_t onceToken;
    static HttpServerLogger *shared;
    dispatch_once(&onceToken, ^{
        shared = [HttpServerLogger new];
    });
    return shared;
}
- (GCDWebServer *)webServer {
    if (!_webServer) {
        _webServer = [[GCDWebServer alloc] init];
        __weak __typeof__(self) weakSelf = self;
        // Add a handler to respond to GET requests on any URL
        [_webServer addDefaultHandlerForMethod:@"GET"
          requestClass:[GCDWebServerRequest class]
          processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
                  return [weakSelf createResponseBody:request];
          }];
        NSLog(@"Visit %@ in your web browser", _webServer.serverURL);
    }
    return _webServer;
}
- (void)startServer{
     // Use convenience method that runs server on port 8080
    // until SIGINT (Ctrl-C in Terminal) or SIGTERM is received
    [self.webServer startWithPort:8080 bonjourName:nil];
}
- (void)stopServer {
    [_webServer stop];
    _webServer = nil;
}
//当浏览器请求的时候,返回一个由日志信息组装成的html返回给浏览器
- (GCDWebServerDataResponse *)createResponseBody :(GCDWebServerRequest* )request{
    GCDWebServerDataResponse *response = nil;
    NSString* path = request.path;
    NSDictionary* query = request.query;
    //NSLog(@"path = %@,query = %@",path,query);
    NSMutableString* string;
    if ([path isEqualToString:@"/"]) {
        string = [[NSMutableString alloc] init];
        [string appendString:@""];
        [string appendString:@""];
        [string appendFormat:@"%s[%i]", getprogname(), getpid()];
        [string appendString:@""];
        [string appendFormat:@"", kMinRefreshDelay];
        [string appendString:@""];
        [string appendString:@""];
        [string appendString:@"
"];
        [self _appendLogRecordsToString:string afterAbsoluteTime:0.0];
        [string appendString:@""];
        [string appendString:@"
"];
        [string appendString:@""];
        [string appendString:@""];
    }
    else if ([path isEqualToString:@"/log"] && query[@"after"]) {
        string = [[NSMutableString alloc] init];
        double time = [query[@"after"] doubleValue];
        [self _appendLogRecordsToString:string afterAbsoluteTime:time];
    }
    else {
       string = [@"
无数据
" mutableCopy];
    }
    if (string == nil) {
        string = [@"" mutableCopy];
    }
    response = [GCDWebServerDataResponse responseWithHTML:string];
    return response;
}
- (void)_appendLogRecordsToString:(NSMutableString*)string afterAbsoluteTime:(double)time {
    __block double maxTime = time;
    NSArray *allMsg = [SystemLogManager allLogAfterTime:time];
    [allMsg enumerateObjectsUsingBlock:^(SystemLogMessage * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        const char* style = "color: dimgray;";
        NSString* formattedMessage = [self displayedTextForLogMessage:obj];
        [string appendFormat:@"
%@", style, formattedMessage];
        if (obj.timeInterval > maxTime) {
            maxTime = obj.timeInterval ;
        }
    }];
    [string appendFormat:@"
", maxTime];
}
- (NSString *)displayedTextForLogMessage:(SystemLogMessage *)msg{
    NSMutableString *string = [[NSMutableString alloc] init];
    [string appendFormat:@"
",[SystemLogMessage logTimeStringFromDate:msg.date ],msg.sender, msg.messageText];
%@    %@    %@    
    return string;
}
@end

使用的时候,开启webserver服务,在同一个局域网下, 使用 http://机子的ip:8080来请求 演示2.gif

1465986055665970.gif

上述演示代码下载 TestLog

几个优秀的第三方日志框架

CocoaLumberjack

另一个日志替代品XLFacility,其中实现了本地存储,重定向,web服务等,是本demo的重要参考代码

CCLogSystem

ASL的swift版本的封装CleanroomASLswift

轻量级的iOS和mac上的http serverCocoaHTTPServer

轻量级的iOS和mac上的http serverGCDWebServer

参考

官方的ASL说明

freopen实现

read-log-messages-posted-to-the-device-console

readout-at-runtime-in-an-application

how-to-nslog-into-a-file

iOS7新技术:如何使用Multipeer Connectivity

Multipeer Connectivity是一个使附近设备通过Wi-Fi网络、P2P Wi-Fi以及蓝牙个人局域网进行通信的框架。互相连接的节点可以不适用网络而安全地传递信息、流或是其他文件资源。

Advertising & Discovering
        通信的第一步是让大家互相发现彼此,我们通过广播(Advertising)和发现(Discovering)服务来实现。
        广播(Advertising)作为服务器,来搜索附近的节点,而节点同时也去搜索附近的广播。在很多情况下,客户端同时广播并且发现同一个服务,这可能导致一些混乱的场面,尤其是在Client-Server模式中。所以,每一个服务都应该有一个标示符来表明自己,它必须是由ASCII字母、数字和“-”组成的短文本串,最多15个字符。我建议,一个服务的名字应该由应用程序的名字开始,后边跟“-”和一个自定义的描述符号。(作者认为这和 com.apple.*标示符很像),就像下边:
  1. static NSString * const XXServiceType = @“xx-service”;
        每个节点都有一个唯一名称的MCPeerID对象,使用字符串名称进行初始化,它可能是用户指定的昵称,亦或是单纯的设备名称。
  1. MCPeerID *localPeerID = [[MCPeerID alloc] initWithDisplayName:[[UIDevice currentDevice] name]];
        节点内部使用NSNetService或者Bonjour C API进行手动广播和发现,但这是一个值得深入研究的问题,关于手动节点管理可具体参见MCSession文档,在此将不进行深入。
Advertising
        服务的广播通过MCNearbyServiceAdvertiser来操作,初始化时带着本地节点、服务类型以及任何可与发现该服务的节点进行通信的可选信息。
       可选信息使用Bonjour TXT records encoded(according to RFC 6763)编码进行发送。
  1. MCNearbyServiceAdvertiser *advertiser =
  2.     [[MCNearbyServiceAdvertiser alloc] initWithPeer:localPeerID
  3.                                       discoveryInfo:nil
  4.                                         serviceType:XXServiceType];
  5. advertiser.delegate = self;
  6. [advertiser startAdvertisingPeer];
        相关事件由Advertiser的代理来处理,需遵从MCNearbyServiceAdvertiserDelegate协议。
        在下例中,考虑到用户可以选择是否接受或拒绝传入的连接请求,并我们有权拒绝或屏蔽任何来自该节点的后续请求。
  1. #pragma mark – MCNearbyServiceAdvertiserDelegate
  2. – (void)advertiser:(MCNearbyServiceAdvertiser *)advertiser
  3. didReceiveInvitationFromPeer:(MCPeerID *)peerID
  4.        withContext:(NSData *)context
  5.  invitationHandler:(void(^)(BOOL accept, MCSession *session))invitationHandler
  6. {
  7.     if ([self.mutableBlockedPeers containsObject:peerID]) {
  8.         invitationHandler(NO, nil);
  9.         return;
  10.     }
  11.     [[UIActionSheet actionSheetWithTitle:[NSString stringWithFormat:NSLocalizedString(@“Received Invitation from %@”, @“Received Invitation from {Peer}”), peerID.displayName]
  12.                        cancelButtonTitle:NSLocalizedString(@“Reject”, nil)
  13.                   destructiveButtonTitle:NSLocalizedString(@“Block”, nil)
  14.                        otherButtonTitles:@[NSLocalizedString(@“Accept”, nil)]
  15.                                    block:^(UIActionSheet *actionSheet, NSInteger buttonIndex)
  16.     {
  17.         BOOL acceptedInvitation = (buttonIndex == [actionSheet firstOtherButtonIndex]);
  18.         if (buttonIndex == [actionSheet destructiveButtonIndex]) {
  19.             [self.mutableBlockedPeers addObject:peerID];
  20.         }
  21.         MCSession *session = [[MCSession alloc] initWithPeer:localPeerID
  22.                                             securityIdentity:nil
  23.                                         encryptionPreference:MCEncryptionNone];
  24.         session.delegate = self;
  25.         invitationHandler(acceptedInvitation, (acceptedInvitation ? session : nil));
  26.     }] showInView:self.view];
  27. }
        为了简单起见,本例中使用了一个带有Block的ActionSheet来作为操作框,它可以直接给invitationHandler传递信息,用来避免创建和管理delegate造成的过于凌乱的业务逻辑。这种方法可以用category来实现。
Creating a Session
        在下面的例子中,我们创建了session,并在接受连接时传递到节点。一个MCSession对象的初始化需要本地节点标识符、securityIdentity以及encryptionPreference等参数。
  1. MCSession *session = [[MCSession alloc] initWithPeer:localPeerID
  2.                                     securityIdentity:nil
  3.                                 encryptionPreference:MCEncryptionNone];
  4. session.delegate = self;
        securityIdentity是一个可选参数。通过X.509证书,它允许节点安全识别并连接其他节点。当设置了该参数时,第一个对象应该是识别客户端的SecIdentityRef,接着是一个或更多个用以核实本地节点身份的SecCertificateRef Objects。
        encryptionPreference参数指定是否加密节点之间的通信。MCEncryptionPreference枚举提供的三种值是:
MCEncryptionOptional:会话更喜欢使用加密,但会接受未加密的连接。
MCEncryptionRequired:会话需要加密。
MCEncryptionNone:会话不应该加密。
        启用加密会显著降低传输速率,所以除非你的应用程序很特别,需要对用户的敏感信息进行处理,否则建议使用MCEncryptionNone。
        MCSessionDelegate协议将会在发送和接受信息的部分被覆盖.
Discovering
        客户端使用MCNearbyServiceBrowser来发现广播,它需要Local Peer标识符,以及非常类似MCNearbyServiceAdvertiser的服务类型来初始化:
  1. MCNearbyServiceBrowser *browser = [[MCNearbyServiceBrowser alloc] initWithPeer:localPeerID serviceType:XXServiceType];
  2. browser.delegate = self;
        可能会有很多节点广播一个特定的服务,所以为了方便用户(或开发者),MCBrowserViewController将提供一个内置的、标准的方式来表示连接:
  1. MCBrowserViewController *browserViewController =
  2.     [[MCBrowserViewController alloc] initWithBrowser:browser
  3.                                              session:session];
  4. browserViewController.delegate = self;
  5. [self presentViewController:browserViewController
  6.                    animated:YES
  7.                  completion:
  8. ^{
  9.     [browser startBrowsingForPeers];
  10. }];
        当browser完成节点连接后,它将使用它的delegate调用browserViewControllerDidFinish:,用以通知展示视图控制器–它应该更新UI以适应新连接的客户端。
Sending & Receiving Information
        一旦节点彼此相连,它们将能互传信息。Multipeer Connectivity框架区分三种不同形式的数据传输:
Messages是定义明确的信息,比如端文本或者小序列化对象。
Streams 流是可连续传输数据(如音频,视频或实时传感器事件)的信息公开渠道。
Resources是图片、电影以及文档的文件。
Messages
Messages使用-sendData:toPeers:withMode:error::方法发送。
  1. NSString *message = @“Hello, World!”;
  2. NSData *data = [message dataUsingEncoding:NSUTF8StringEncoding];
  3. NSError *error = nil;
  4. if (![self.session sendData:data
  5.                     toPeers:peers
  6.                    withMode:MCSessionSendDataReliable
  7.                       error:&error]) {
  8.     NSLog(@“[Error] %@”, error);
  9. }
        通过MCSessionDelegate方法 -sessionDidReceiveData:fromPeer:收取信息。以下是如何解码先前示例代码中发送的消息:
  1. #pragma mark – MCSessionDelegate
  2. – (void)session:(MCSession *)session
  3.  didReceiveData:(NSData *)data
  4.        fromPeer:(MCPeerID *)peerID
  5. {
  6.     NSString *message =
  7.         [[NSString alloc] initWithData:data
  8.                               encoding:NSUTF8StringEncoding];
  9.     NSLog(@“%@”, message);
  10. }
        另一种方法是发送NSKeyedArchiver编码的对象:
  1. id <NSSecureCoding> object = // …;
  2. NSData *data = [NSKeyedArchiver archivedDataWithRootObject:object];
  3. NSError *error = nil;
  4. if (![self.session sendData:data
  5.                     toPeers:peers
  6.                    withMode:MCSessionSendDataReliable
  7.                       error:&error]) {
  8.     NSLog(@“[Error] %@”, error);
  9. }
  10. #pragma mark – MCSessionDelegate
  11. – (void)session:(MCSession *)session
  12.  didReceiveData:(NSData *)data
  13.        fromPeer:(MCPeerID *)peerID
  14. {
  15.     NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
  16.     unarchiver.requiresSecureCoding = YES;
  17.     id object = [unarchiver decodeObject];
  18.     [unarchiver finishDecoding];
  19.     NSLog(@“%@”, object);
  20. }
        为了防范对象替换攻击,设置requiresSecureCoding为YES很重要,这样的话如果根对象类没有遵从<NSSecureCoding>,就会抛出一个异常。欲了解更多信息,请参阅[NSHipster article on NSSecureCoding]。
Streams
Streams 使用 -startStreamWithName:toPeer:创建:
  1. NSOutputStream *outputStream =
  2.     [session startStreamWithName:name
  3.                           toPeer:peer];
  4. stream.delegate = self;
  5. [stream scheduleInRunLoop:[NSRunLoop mainRunLoop]
  6.                 forMode:NSDefaultRunLoopMode];
  7. [stream open];
  8. // …
        Streams通过MCSessionDelegate的方法session:didReceiveStream:withName:fromPeer:来接收:
  1. #pragma mark – MCSessionDelegate
  2. – (void)session:(MCSession *)session
  3. didReceiveStream:(NSInputStream *)stream
  4.        withName:(NSString *)streamName
  5.        fromPeer:(MCPeerID *)peerID
  6. {
  7.     stream.delegate = self;
  8.     [stream scheduleInRunLoop:[NSRunLoop mainRunLoop]
  9.                       forMode:NSDefaultRunLoopMode];
  10.     [stream open];
  11. }
输入和输出的streams必须安排好并打开,然后才能使用它们。这样,streams就可以进行读和写的操作了。
Resources
        Resources 发送使用 -sendResourceAtURL:withName:toPeer:withCompletionHandler::
  1. NSURL *fileURL = [NSURL fileURLWithPath:@“path/to/resource”];
  2. NSProgress *progress =
  3.     [self.session sendResourceAtURL:fileURL
  4.                            withName:[fileURL lastPathComponent]
  5.                              toPeer:peer
  6.                   withCompletionHandler:^(NSError *error)
  7. {
  8.     NSLog(@“[Error] %@”, error);
  9. }];
         返回的NSProgress对象可以是通过KVO(Key-Value Observed)来监视文件传输的进度,并且它提供取消传输的方法:-cancel。
         接收资源实现MCSessionDelegate两种方法:-session:didStartReceivingResourceWithName:fromPeer:withProgress: 和 -session:didFinishReceivingResourceWithName:fromPeer:atURL:withError:
  1. #pragma mark – MCSessionDelegate
  2. – (void)session:(MCSession *)session
  3. didStartReceivingResourceWithName:(NSString *)resourceName
  4.        fromPeer:(MCPeerID *)peerID
  5.    withProgress:(NSProgress *)progress
  6. {
  7.     // …
  8. }
  9. – (void)session:(MCSession *)session
  10. didFinishReceivingResourceWithName:(NSString *)resourceName
  11.        fromPeer:(MCPeerID *)peerID
  12.           atURL:(NSURL *)localURL
  13.       withError:(NSError *)error
  14. {
  15.     NSURL *destinationURL = [NSURL fileURLWithPath:@“/path/to/destination”];
  16.     NSError *error = nil;
  17.     if (![[NSFileManager defaultManager] moveItemAtURL:localURL
  18.                                                  toURL:destinationURL
  19.                                                  error:&error]) {
  20.         NSLog(@“[Error] %@”, error);
  21.     }
  22. }
        再次说明,在传输期间NSProgress parameter in -session:didStartReceivingResourceWithName:fromPeer:withProgress:允许接收节点来监控文件传输进度。在-session:didFinishReceivingResourceWithName:fromPeer:atURL:withError:中,delegate的责任是从临时localURL移动文件至永久位置。
Multipeer是突破性的API,其价值才刚刚开始被理解。在不远的将来,你应该会看到它成为让所有人期望的功能。
本人翻译自:http://nshipster.com/multipeer-connectivity/