一、MultipeerConnectivity框架簡(jiǎn)述
參考博客:《http://blog.csdn.net/phunxm/article/details/43450167》
iOS藍(lán)牙通訊的三種方式:
- GameKit.framework:iOS7之前的藍(lán)牙通訊框架,從iOS7開(kāi)始過(guò)期,但是目前已經(jīng)被淘汰。
- MultipeerConnectivity.framework:iOS7開(kāi)始引入的新的藍(lán)牙通訊開(kāi)發(fā)框架,用于取代GameKit。
- CoreBluetooth.framework:功能強(qiáng)大的藍(lán)牙開(kāi)發(fā)框架,要求設(shè)備必須支持藍(lán)牙4.0
本篇文章主要是講MultipeerConnectivity.framework框架。MultipeerConnectivity.framework并不僅僅支持藍(lán)牙連接,準(zhǔn)確的說(shuō)它是一種支持Wi-Fi網(wǎng)絡(luò)、Wi-FiP2P以及藍(lán)牙個(gè)人局域網(wǎng)的通信框架,它屏蔽了具體的連接技術(shù),讓開(kāi)發(fā)人員有統(tǒng)一的接口編程方法。通過(guò)MultipeerConnectivity連接的節(jié)點(diǎn)之間可以安全的傳遞信息(message)、流(Stream)或者其他文件資源(Resource)而不必通過(guò)網(wǎng)絡(luò)服務(wù)。此外使用MultipeerConnectivity進(jìn)行近場(chǎng)通信也不再局限于同一個(gè)應(yīng)用之間傳輸,而是可以在不同的應(yīng)用之間進(jìn)行數(shù)據(jù)傳輸(當(dāng)然你也可以選擇在一個(gè)應(yīng)用程序之間傳輸)。
對(duì)于MultipeerConnectivity.framework的使用,需要知道兩個(gè)重要的概念,即廣播(Advertisting)和發(fā)現(xiàn)(Disconvering),兩個(gè)設(shè)備要進(jìn)行數(shù)據(jù)傳輸,那兩者都得是開(kāi)放狀態(tài)。
廣播
- 廣播,就是將自己置于暴露狀態(tài)(即打開(kāi)廣播),讓尋找者(發(fā)現(xiàn))可以找到,用MCAdvertiserAssistant創(chuàng)建廣播對(duì)象;
- 由于廣播可能有多個(gè),所以每個(gè)廣播者都有一個(gè)名字,用MCPeerID來(lái)設(shè)置名稱或者說(shuō)標(biāo)識(shí),便于發(fā)現(xiàn)者區(qū)分。
- 廣播與發(fā)現(xiàn)之間的數(shù)據(jù)傳輸(發(fā)送或者接收),是通過(guò)一個(gè)會(huì)話對(duì)象來(lái)進(jìn)行的,即MCSession。
- 有連接,就有連接的狀態(tài),這里是通過(guò)一個(gè)代理方法來(lái)跟蹤會(huì)話的狀態(tài),即-(void)session:(MCSession
)session peer:(MCPeerID )peerID didChangeState:(MCSessionState)state - 廣播者可以接收數(shù)據(jù),接收即被動(dòng),所以是在一個(gè)代理方法中進(jìn)行,即-(void)session:(MCSession
)session didReceiveData:(NSData )data fromPeer:(MCPeerID *)peerID - 廣播者可以發(fā)送數(shù)據(jù),發(fā)送即主動(dòng),所以是調(diào)用對(duì)象方法來(lái)實(shí)現(xiàn),即-(void)sendData: toPeers:withMode: error:
發(fā)現(xiàn)
- 發(fā)現(xiàn),就是自己去尋找,根據(jù)標(biāo)識(shí)或名稱,去找到自己想要連接的設(shè)備,用MCBrowserViewController創(chuàng)建發(fā)現(xiàn)對(duì)象
- 當(dāng)發(fā)現(xiàn)廣播后,要與廣播進(jìn)行連接,這個(gè)連接要成功,是需要廣播者同意連接才行,那如果當(dāng)要連接“廣播”的“發(fā)現(xiàn)”有很多時(shí),就得需要MCPeerID(即“發(fā)現(xiàn)”的標(biāo)識(shí))來(lái)區(qū)分,便于“廣播連”接對(duì)應(yīng)的“發(fā)現(xiàn)”。
- 要與廣播進(jìn)行會(huì)話,發(fā)送或接受數(shù)據(jù),需要有個(gè)會(huì)話對(duì)象,即MCSession
- 同樣會(huì)話的連接狀態(tài),是通過(guò)代理方法來(lái)跟蹤的,即-(void)session:(MCSession
)session peer:(MCPeerID )peerID didChangeState:(MCSessionState)state - 和廣播一樣,發(fā)現(xiàn)者可以發(fā)送數(shù)據(jù),發(fā)送即主動(dòng),調(diào)用方法-(void)sendData: toPeers:withMode: error:來(lái)發(fā)送數(shù)據(jù)
- 發(fā)現(xiàn)者可以接收數(shù)據(jù),也是在代理方法中執(zhí)行,即-(void)session:(MCSession
)session didReceiveData:(NSData )data fromPeer:(MCPeerID *)peerID
綜上,可以看出,廣播和發(fā)現(xiàn)的創(chuàng)建使用不同的類,但他們遵循一個(gè)相同的協(xié)議MCSessionDelegate,標(biāo)識(shí)也都是使用MCPeerID來(lái)標(biāo)識(shí)
對(duì)于要傳輸?shù)臄?shù)據(jù),分為三種
- message---一般都是用這種
- Stream---數(shù)據(jù)流傳輸可以用在音頻播放傳輸
- Resource ---在傳輸比較大型的文件時(shí),我們通常使用數(shù)據(jù)源傳輸
介紹就到這里,下面直接上代碼
二、運(yùn)用
創(chuàng)建一個(gè)項(xiàng)目,然后分別給廣播和發(fā)現(xiàn)創(chuàng)建一個(gè)控制器,我這里是MultipeerConnectivityVC和MultipeerConVC,在兩個(gè)控制器中都引入頭文件
#import <MultipeerConnectivity/MultipeerConnectivity.h>
界面的控件創(chuàng)建就不細(xì)說(shuō)了,截個(gè)圖
廣播和發(fā)現(xiàn)建立連接
要建立連接,按上面說(shuō)的,就得有廣播者(MCAdvertiserAssistant)和發(fā)現(xiàn)者(MCBrowserViewController),同時(shí)都要有名稱(MCPeerID),然后還要有進(jìn)行會(huì)話的載體對(duì)象(MCSession)。
先在MultipeerConnectivityVC控制其中創(chuàng)建廣播對(duì)象
在.m文件中添加一個(gè)廣播屬性和會(huì)話屬性
@property(nonatomic,strong)MCAdvertiserAssistant * advertiserAssistant;//廣播助手
@property(nonatomic,strong)MCSession * session;
在viewDidLoad中初始化廣播
- (void)viewDidLoad {
[super viewDidLoad];
//創(chuàng)建ID
MCPeerID * peerID = [[MCPeerID alloc] initWithDisplayName:@"藍(lán)牙設(shè)備1"];
//根據(jù)ID創(chuàng)建會(huì)話對(duì)象
self.session = [[MCSession alloc] initWithPeer:peerID];
self.session.delegate =self;
//創(chuàng)建廣播
//ServiceType的值可以自定義,但是一定要和發(fā)現(xiàn)的相同
_advertiserAssistant = [[MCAdvertiserAssistant alloc] initWithServiceType:@"connect" discoveryInfo:nil session:self.session];
_advertiserAssistant.delegate = self;
}
會(huì)話對(duì)象有代理,廣播也有代理,所以要遵循協(xié)議
@interface MultipeerConnectivityVC ()<MCSessionDelegate,MCAdvertiserAssistantDelegate>
在“開(kāi)始廣播”按鈕中發(fā)起廣播
- (IBAction)beginBroadcast:(UIBarButtonItem *)sender {
[self.advertiserAssistant start];
}
廣播發(fā)出去了,該讓“發(fā)現(xiàn)”去發(fā)現(xiàn)“廣播”了,在MultipeerConVC控制器中創(chuàng)建“發(fā)現(xiàn)”.
添加發(fā)現(xiàn)屬性和會(huì)話對(duì)象屬性
@property(nonatomic,strong)MCBrowserViewController * browserController;
@property(nonatomic,strong)MCSession * session;
先初始化會(huì)話對(duì)象
- (void)viewDidLoad {
[super viewDidLoad];
//創(chuàng)建標(biāo)識(shí)
MCPeerID * peerID = [[MCPeerID alloc] initWithDisplayName:@"藍(lán)牙設(shè)備2"];
//創(chuàng)建會(huì)話對(duì)象
self.session = [[MCSession alloc] initWithPeer:peerID];
self.session.delegate = self;
}
在“搜索廣播”的點(diǎn)擊事件里初始化發(fā)現(xiàn)
- (IBAction)searchBroadcast:(UIBarButtonItem *)sender {
//ServiceType的值可以自定義,但是一定要和廣播的相同
_browserController = [[MCBrowserViewController alloc] initWithServiceType:@"connect" session:self.session];
_browserController.delegate = self;
[self presentViewController:_browserController animated:YES completion:nil];
}
同樣遵循代理
@interface MultipeerConVC ()<MCSessionDelegate,MCBrowserViewControllerDelegate>
下面來(lái)講一下兩個(gè)協(xié)議,即MCAdvertiserAssistantDelegate ,MCBrowserViewControllerDelegate的代理方法。通過(guò)操作流程來(lái)講
首先,點(diǎn)擊廣播頁(yè)面的“開(kāi)始廣播”按鈕,廣播就發(fā)出去了。然后點(diǎn)擊發(fā)現(xiàn)界面的“搜索廣播”按鈕,就會(huì)彈出搜索瀏覽界面也就是MCBrowserViewController控制器,這個(gè)時(shí)候,只要發(fā)現(xiàn)附近的廣播,就會(huì)走M(jìn)CBrowserViewControllerDelegate協(xié)議的代理方法
- (BOOL)browserViewController:(MCBrowserViewController *)browserViewController
shouldPresentNearbyPeer:(MCPeerID *)peerID
withDiscoveryInfo:(nullable NSDictionary<NSString *, NSString *> *)info{
NSLog(@"發(fā)現(xiàn)附近的廣播");
return YES;
}
搜索出來(lái)后,會(huì)列出廣播,如下圖
如果你點(diǎn)擊當(dāng)前界面的左上角的Cancel按鈕,就會(huì)走M(jìn)CBrowserViewControllerDelegate協(xié)議的代理方法
-(void)browserViewControllerWasCancelled:(MCBrowserViewController *)browserViewController{
NSLog(@"取消瀏覽.");
[self.browserController dismissViewControllerAnimated:YES completion:nil];
}
如果你點(diǎn)擊了右上角的Done按鈕就會(huì)走M(jìn)CBrowserViewControllerDelegate協(xié)議的代理方法
-(void)browserViewControllerDidFinish:(MCBrowserViewController *)browserViewController{
NSLog(@"已選擇");
[self.browserController dismissViewControllerAnimated:YES completion:nil];
}
當(dāng)然,一般初次進(jìn)來(lái)的時(shí)候,Done按鈕是灰色的,點(diǎn)不了,而連接成功后,列表界面會(huì)自動(dòng)消失,感覺(jué)都用不著Done按鈕,其實(shí)第二次進(jìn)到這個(gè)界面的時(shí)候就可以用了。
好,到這里的話我既不點(diǎn)“Cancel”,也不點(diǎn)“Done”,而是選擇廣播設(shè)備“藍(lán)牙設(shè)備1”,只要選擇,那廣播端就會(huì)收到信號(hào),會(huì)彈出一個(gè)接受請(qǐng)求的選擇彈框,如下圖
可以看到發(fā)現(xiàn)的設(shè)備名稱是“藍(lán)牙設(shè)備2”。彈出這個(gè)彈框之前,會(huì)走M(jìn)CAdvertiserAssistantDelegate協(xié)議的代理方法
- (void)advertiserAssistantWillPresentInvitation:(MCAdvertiserAssistant *)advertiserAssistant{
NSLog(@"一個(gè)邀請(qǐng)求將出現(xiàn)");
}
當(dāng)選擇彈框的“Decline”或者“Accept”按鈕,彈框就會(huì)消失,就會(huì)走代理方法
- (void)advertiserAssistantDidDismissInvitation:(MCAdvertiserAssistant *)advertiserAssistant{
NSLog(@"一個(gè)邀請(qǐng)將從屏幕消失");
}
如果點(diǎn)擊的是“ Accept”,那么連接成功,“發(fā)現(xiàn)”端的MCBrowserViewController控制器界面會(huì)自動(dòng)消失。到這一步,發(fā)現(xiàn)和廣播的連接已經(jīng)完成。下面就是傳送數(shù)據(jù)了
Message數(shù)據(jù)傳輸
我這里要實(shí)現(xiàn)的是,在廣播端從相冊(cè)選擇圖片,然后傳送給發(fā)現(xiàn)端;或者在發(fā)現(xiàn)端從相冊(cè)選擇圖片,然后傳送給廣播端。就這部分功能來(lái)說(shuō),兩個(gè)廣播和發(fā)現(xiàn)的功能是相同的,代碼也相同。所以這里只寫一遍.
首先,在控制器中添加UIImagePickerController屬性
@property(nonatomic,strong)UIImagePickerController * imagePickerController;
在選擇照片按鈕中初始化UIImagePickerController對(duì)象
- (IBAction)selectedPhoto:(UIBarButtonItem *)sender {
_imagePickerController = [[UIImagePickerController alloc] init];
_imagePickerController.delegate = self;
[self presentViewController:_imagePickerController animated:YES completion:nil];
}
遵循協(xié)議
UIImagePickerControllerDelegate,UINavigationControllerDelegate
實(shí)現(xiàn)兩個(gè)代理方法
#pragma mark - UIImagePickerController代理方法
-(void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info{
//獲取照片并展示
UIImage *image=[info objectForKey:UIImagePickerControllerOriginalImage];
[self.imageView setImage:image];
[self.imagePickerController dismissViewControllerAnimated:YES completion:nil];
}
-(void)imagePickerControllerDidCancel:(UIImagePickerController *)picker{
[self.imagePickerController dismissViewControllerAnimated:YES completion:nil];
}
到這一步,我已經(jīng)獲取到我要傳輸?shù)恼掌?,那就開(kāi)始傳輸吧,在上面的代理方法中添加代碼如下
-(void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info{
//獲取照片并展示
UIImage *image=[info objectForKey:UIImagePickerControllerOriginalImage];
[self.imageView setImage:image];
//發(fā)送數(shù)據(jù)給所有已連接設(shè)備
NSError *error=nil;
[self.session sendData:UIImagePNGRepresentation(image) toPeers:[self.session connectedPeers] withMode:MCSessionSendDataUnreliable error:&error];
NSLog(@"開(kāi)始發(fā)送數(shù)據(jù)...");
if (error) {
NSLog(@"發(fā)送數(shù)據(jù)過(guò)程中發(fā)生錯(cuò)誤,錯(cuò)誤信息:%@",error.localizedDescription);
}
[self.imagePickerController dismissViewControllerAnimated:YES completion:nil];
}
發(fā)送方法sendData:toPeers:withMode:error在這里解釋下
- 第一個(gè)參數(shù)Data:就是要發(fā)送的數(shù)據(jù)UIImagePNGRepresentation(image)
- 第二個(gè)參數(shù)Peers:指的是發(fā)送給誰(shuí),[self.session connectedPeers] 就是獲取與這個(gè)session建立連接的所有設(shè)備
- 第三個(gè)參數(shù)Mode:該參數(shù)是枚舉類型,有兩個(gè)
MCSessionSendDataReliable, // 保證可靠和順序發(fā)出
MCSessionSendDataUnreliable //立即發(fā)出沒(méi)有排隊(duì),不能保證交貨。
發(fā)送成功的話,則接收方就會(huì)收到數(shù)據(jù),可以從MCSessionDelegate協(xié)議的代理方法中獲得數(shù)據(jù),
-(void)session:(MCSession *)session didReceiveData:(NSData *)data fromPeer:(MCPeerID *)peerID{
NSLog(@"開(kāi)始接收數(shù)據(jù)...");
if ([UIImage imageWithData:data]) {
UIImage *image=[UIImage imageWithData:data];
dispatch_sync(dispatch_get_main_queue(), ^{
[self.imageView setImage:image];
});
//保存到相冊(cè)
UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil);
}
}
到此,廣播和發(fā)現(xiàn)之間的相片數(shù)據(jù)傳輸?shù)墓δ軐?shí)現(xiàn)完成。
注意:從相冊(cè)選擇照片,獲取照片,發(fā)送照片數(shù)據(jù),接收照片數(shù)據(jù)的代碼,在廣播和發(fā)現(xiàn)端是相同。因?yàn)橐獙?shí)現(xiàn)相互可以發(fā)送
字符串?dāng)?shù)據(jù)傳輸
在廣播和發(fā)現(xiàn)的界面上都添加一個(gè)文字傳輸按鈕,在廣播的界面按鈕事件中實(shí)現(xiàn)代碼如下
- (IBAction)textButtonAction:(UIButton *)sender {
NSString * string = @"住進(jìn)布達(dá)拉宮,你是雪域的王;走在拉薩街頭,你是世上最美的情郎";
NSData * data = [string dataUsingEncoding:(NSUTF8StringEncoding)];
NSError * error = nil;
[self.session sendData:data toPeers:[self.session connectedPeers] withMode:MCSessionSendDataUnreliable error:&error];
NSLog(@"開(kāi)始發(fā)送文字?jǐn)?shù)據(jù)...");
if (error) {
NSLog(@"發(fā)送數(shù)據(jù)過(guò)程中發(fā)生錯(cuò)誤,錯(cuò)誤信息:%@",error.localizedDescription);
}
}
在發(fā)現(xiàn)的按鈕事件中實(shí)現(xiàn)代碼如下
- (IBAction)textButtonAction:(UIButton *)sender {
NSString * string = @"白云山上白云邊,善男信女飾能仁,而我獨(dú)飾九龍泉";
NSData * data = [string dataUsingEncoding:(NSUTF8StringEncoding)];
NSError * error = nil;
[self.session sendData:data toPeers:[self.session connectedPeers] withMode:MCSessionSendDataUnreliable error:&error];
NSLog(@"開(kāi)始發(fā)送文字?jǐn)?shù)據(jù)...");
if (error) {
NSLog(@"發(fā)送數(shù)據(jù)過(guò)程中發(fā)生錯(cuò)誤,錯(cuò)誤信息:%@",error.localizedDescription);
}
}
觸發(fā)了按鈕,就會(huì)發(fā)送數(shù)據(jù),然后在接收方的代理方法-(void)session:(MCSession *)session didReceiveData:(NSData *)data fromPeer:(MCPeerID *)peerID中(廣播和發(fā)現(xiàn)都要添加)添加代碼如下
-(void)session:(MCSession *)session didReceiveData:(NSData *)data fromPeer:(MCPeerID *)peerID{
NSLog(@"開(kāi)始接收數(shù)據(jù)...");
if ([UIImage imageWithData:data]) {
UIImage *image=[UIImage imageWithData:data];
dispatch_sync(dispatch_get_main_queue(), ^{
[self.imageView setImage:image];
});
//保存到相冊(cè)
UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil);
}
if ([[NSString alloc] initWithData:data encoding:(NSUTF8StringEncoding)]) {
dispatch_sync(dispatch_get_main_queue(), ^{
self.textField.text = [[NSString alloc] initWithData:data encoding:(NSUTF8StringEncoding)];
});
}
}
這樣就可以實(shí)現(xiàn)廣播和發(fā)現(xiàn)的文字?jǐn)?shù)據(jù)傳輸,其實(shí),實(shí)現(xiàn)原理是一樣的,下面來(lái)說(shuō)說(shuō)不一樣的,即Stream和Resource傳輸
Stream數(shù)據(jù)傳輸
關(guān)于NSStream的介紹可以參考文章:NSStream我只簡(jiǎn)單的說(shuō)說(shuō)我的理解,便于使用就可以。
- NSStream有兩個(gè)子類即NSInputStream(輸入數(shù)據(jù)流通道)和NSOutputStream(輸出數(shù)據(jù)流通道)。
- NSStream有一個(gè)協(xié)議NSStreamDelegate,協(xié)議中只有一個(gè)代理方法,即
-(void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode
數(shù)據(jù)讀寫都是在這個(gè)代理方法中執(zhí)行的。
通過(guò)代碼來(lái)理清他們的操作流程。先在控制器中添加兩個(gè)子類的屬性,另外添加兩個(gè)屬性,下面會(huì)用的到(廣播和發(fā)現(xiàn)都需要添加)
@property(nonatomic,strong)NSOutputStream * outputStream;//輸出流
@property(nonatomic,strong)NSInputStream * inputStream;//輸入流
@property(nonatomic,assign)NSInteger byteIndex;//字節(jié)下標(biāo),用于記錄傳輸數(shù)據(jù)的字節(jié)數(shù)
@property(nonatomic,strong)NSMutableData * streamData;//用于存儲(chǔ)二進(jìn)制流數(shù)據(jù)
流數(shù)據(jù)要傳輸,我必須得有一個(gè)觸發(fā)事件,那我在"Stream"按鈕的點(diǎn)擊事件中,添加如下代碼
- (IBAction)streamButtonAction:(UIButton *)sender {
NSError *error;
//將輸出流通道初始化,即與連接上會(huì)話的藍(lán)牙設(shè)備進(jìn)行關(guān)聯(lián)----我是輸出數(shù)據(jù)流通道,我要流給誰(shuí)
self.outputStream = [self.session startStreamWithName:@"superStream" toPeer:[self.session.connectedPeers firstObject] error:&error];
self.outputStream.delegate = self;
//將輸出流通道放到runloop上,這一步可以理解為,輸出流通道需要?jiǎng)恿?,才能運(yùn)輸,那剛好runloop可以提供動(dòng)力,
[self.outputStream scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
if(error || !self.outputStream) {
NSLog(@"%@", error);
}
else{
//打開(kāi)輸出流通道
[self.outputStream open];
}
}
代碼注釋比較詳細(xì),應(yīng)該能看懂。然后往下,輸出數(shù)據(jù)流有動(dòng)力了,也有方向了,但是還需要一個(gè)緩沖容器,以便于控制,畢竟是隔空傳輸,這個(gè)地方就是內(nèi)存空間,檢測(cè)到有了內(nèi)存空間,數(shù)據(jù)流就會(huì)往內(nèi)存空間緩緩注入,完畢后就要關(guān)閉輸出數(shù)據(jù)流,然后還要把動(dòng)力系統(tǒng)關(guān)閉,當(dāng)然這些操作都是在代理方法中執(zhí)行的
- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode{
switch (eventCode) {
case NSStreamEventOpenCompleted:{//打開(kāi)輸出數(shù)據(jù)流通道會(huì)走到這一步
NSLog(@"數(shù)據(jù)流開(kāi)始");
self.byteIndex = 0;
self.streamData = [[NSMutableData alloc]init];
}
break;
case NSStreamEventHasSpaceAvailable:{//監(jiān)測(cè)到有內(nèi)存空間可用,就把輸出流通道中的流數(shù)據(jù)寫入到內(nèi)存空間
NSData *data = [NSData dataWithContentsOfURL:[NSURL fileURLWithPath:[self recordPath]]];
NSOutputStream *output = (NSOutputStream *)aStream;
NSUInteger len = ((data.length - self.byteIndex >= 1024) ? 1024 : (data.length-self.byteIndex));
NSData *data1 = [data subdataWithRange:NSMakeRange(self.byteIndex, len)];
[output write:data1.bytes maxLength:len];
self.byteIndex += len;
}
break;
case NSStreamEventEndEncountered:{//監(jiān)測(cè)到輸出流通道中的流數(shù)據(jù)寫入內(nèi)存空間完成
[aStream close];//關(guān)閉輸出流通道
[aStream removeFromRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];//將輸出流從runloop中清除
self.byteIndex = 0;
}
break;
case NSStreamEventErrorOccurred:{
//發(fā)生錯(cuò)誤
NSLog(@"error");
}
break;
default:
break;
}
}
代理方法中eventCode是數(shù)據(jù)流事件,監(jiān)測(cè)輸出數(shù)據(jù)流的操作到哪一步了。到這一步,剩下的就交給會(huì)話了即MCSession,會(huì)話會(huì)將數(shù)據(jù)流信號(hào)連接到匹配的藍(lán)牙設(shè)備,會(huì)觸發(fā)接收方所遵循的MCSessionDelegate協(xié)議的代理方法
-(void)session:(MCSession *)session didReceiveStream:(NSInputStream *)stream withName:(NSString *)streamName fromPeer:(MCPeerID *)peerID
從這個(gè)代理方法中獲取流數(shù)據(jù)信號(hào),注意,這里的流數(shù)據(jù)信號(hào),可以理解為NSInputStream的接口信號(hào),告訴接收設(shè)備,流數(shù)據(jù)輸出端通道已經(jīng)準(zhǔn)備好,當(dāng)你(輸入端)準(zhǔn)備好了,并對(duì)接上,就可以進(jìn)行流數(shù)據(jù)傳輸,那怎么準(zhǔn)備的呢?怎么對(duì)接呢?那就要用到NSInputStream了。將控制器的NSInputStream的屬性通過(guò)代理方法中的“stream”實(shí)例化,就對(duì)接上了。但是和輸出流一樣,需要?jiǎng)恿?,需要打開(kāi)輸出流。但是不需要空間了,因?yàn)檫@里是從輸出流端的空間獲取流,所以代理方法實(shí)現(xiàn)如下
- (void)session:(MCSession *)session didReceiveStream:(NSInputStream *)stream withName:(NSString *)streamName fromPeer:(MCPeerID *)peerID{
NSLog(@"獲取流數(shù)據(jù)");
//輸入流實(shí)例化---既然有流通道伸過(guò)來(lái)了,那我也得有可以對(duì)接的通道
self.inputStream = stream;
self.inputStream.delegate = self;
//有了載體,要將這個(gè)通道流通起來(lái),就需要添加動(dòng)力,即放到runloop上
[self.inputStream scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
[self.inputStream open];//打開(kāi)輸入流通道
}
打開(kāi)輸入流通道,就會(huì)調(diào)用數(shù)據(jù)流的代理方法
- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode{
switch (eventCode) {
case NSStreamEventOpenCompleted:{//打開(kāi)輸出數(shù)據(jù)流通道或者打開(kāi)輸入數(shù)據(jù)流通道就會(huì)走到這一步
NSLog(@"數(shù)據(jù)流開(kāi)始");
self.byteIndex = 0;
self.streamData = [[NSMutableData alloc]init];
}
break;
case NSStreamEventHasBytesAvailable:{//監(jiān)測(cè)到輸入流通道中有數(shù)據(jù)流,就把數(shù)據(jù)一點(diǎn)一點(diǎn)的拼接起來(lái)
NSInputStream *input = (NSInputStream *)aStream;
uint8_t buffer[1024];
NSInteger length = [input read:buffer maxLength:1024];
NSLog(@"%ld", length);
[self.streamData appendBytes:(const void *)buffer length:(NSUInteger)length];
}
break;
case NSStreamEventEndEncountered:{//監(jiān)測(cè)到輸入流通道中的流數(shù)據(jù)獲取完成
[aStream close];//關(guān)閉輸入流通道
[aStream removeFromRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];//將輸入流通道從runloop中清除
//輸入流數(shù)據(jù)拼接完成,可以直接獲取數(shù)據(jù)
if([aStream isKindOfClass:[NSInputStream class]]){
self.imageView.image = [[UIImage alloc] initWithData:self.streamData];
}
}
break;
case NSStreamEventErrorOccurred:{
//發(fā)生錯(cuò)誤
NSLog(@"error");
}
break;
default:
break;
}
}
由于廣播和發(fā)現(xiàn)是可以互傳數(shù)據(jù)的,所以我就將代理方法
-(void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode
的中輸入流和輸出流的數(shù)據(jù)處理都寫上,整理如下
- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode{
switch (eventCode) {
case NSStreamEventOpenCompleted:{//打開(kāi)輸出數(shù)據(jù)流通道或者打開(kāi)輸入數(shù)據(jù)流通道就會(huì)走到這一步
NSLog(@"數(shù)據(jù)流開(kāi)始");
self.byteIndex = 0;
self.streamData = [[NSMutableData alloc]init];
}
break;
case NSStreamEventHasBytesAvailable:{//監(jiān)測(cè)到輸入流通道中有數(shù)據(jù)流,就把數(shù)據(jù)一點(diǎn)一點(diǎn)的拼接起來(lái)
NSInputStream *input = (NSInputStream *)aStream;
uint8_t buffer[1024];
NSInteger length = [input read:buffer maxLength:1024];
NSLog(@"%ld", length);
[self.streamData appendBytes:(const void *)buffer length:(NSUInteger)length];
// 記住這邊的數(shù)據(jù)陸陸續(xù)續(xù)的
}
break;
case NSStreamEventHasSpaceAvailable:{//監(jiān)測(cè)到有內(nèi)存空間可用,就把輸出流通道中的流寫入到內(nèi)存空間
NSData *data = [NSData dataWithContentsOfURL:[NSURL fileURLWithPath:[self recordPath]]];
NSOutputStream *output = (NSOutputStream *)aStream;
NSUInteger len = ((data.length - self.byteIndex >= 1024) ? 1024 : (data.length-self.byteIndex));
NSData *data1 = [data subdataWithRange:NSMakeRange(self.byteIndex, len)];
[output write:data1.bytes maxLength:len];
self.byteIndex += len;
}
break;
case NSStreamEventEndEncountered:{//監(jiān)測(cè)到輸出流通道中的流數(shù)據(jù)寫入內(nèi)存空間完成或者輸入流通道中的流數(shù)據(jù)獲取完成
[aStream close];//關(guān)閉輸出流
[aStream removeFromRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];//將輸出流從runloop中清除
//輸入流數(shù)據(jù)拼接完成,可以直接獲取數(shù)據(jù)
if([aStream isKindOfClass:[NSInputStream class]]){
self.imageView.image = [[UIImage alloc] initWithData:self.streamData];
}
self.byteIndex = 0;
}
break;
case NSStreamEventErrorOccurred:{
//發(fā)生錯(cuò)誤
NSLog(@"error");
}
break;
default:
break;
}
}
補(bǔ)充一個(gè)落下方法 recordPath,也就是獲取本地文件的路徑
-(NSString*)recordPath{
NSString * path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).lastObject;
NSString * newPath = [path stringByAppendingPathComponent:@"nothing.jpeg"];
UIImage * image = [UIImage imageNamed:@"WechatIMG2"];
NSData * data = UIImageJPEGRepresentation(image, 0.1);
[data writeToFile:newPath atomically:YES];
return newPath;
}
這樣的話,不管誰(shuí)作為輸出方,誰(shuí)做輸入方,都是可以實(shí)現(xiàn)數(shù)據(jù)流傳輸?shù)?。到這一步,Stream的數(shù)據(jù)傳輸講解完畢,繼續(xù)看下一個(gè)。
Resource數(shù)據(jù)源傳輸
在傳輸比較大型的文件時(shí),我們通常使用數(shù)據(jù)源傳輸
數(shù)據(jù)傳輸都是在session的代理方法中進(jìn)行的。數(shù)據(jù)源傳送方式有兩個(gè)代理方法,分別為數(shù)據(jù)源傳輸剛剛開(kāi)始調(diào)用
- (void)session:(MCSession *)session didStartReceivingResourceWithName:(NSString *)resourceName fromPeer:(MCPeerID *)peerID withProgress:(NSProgress *)progress
和數(shù)據(jù)源傳輸結(jié)束時(shí)調(diào)用
- (void)session:(MCSession *)session didFinishReceivingResourceWithName:(NSString *)resourceName fromPeer:(MCPeerID *)peerID atURL:(NSURL *)localURL withError:(nullable NSError *)error
首先添加一個(gè)“Resource”按鈕,在它的點(diǎn)擊事件中調(diào)用MCSession的發(fā)送數(shù)據(jù)源的方法發(fā)送數(shù)據(jù)
- (IBAction)resourceButtonAction:(UIButton *)sender {
//獲取導(dǎo)數(shù)據(jù)的路徑
NSURL *fileURL = [NSURL fileURLWithPath:[self imagePath]];
//發(fā)送數(shù)據(jù)給匹配的藍(lán)牙設(shè)備,Name隨意取
[self.session sendResourceAtURL:fileURL withName:@"image_" toPeer:[self.session.connectedPeers firstObject] withCompletionHandler:^(NSError *error) {\
if (error) {
NSLog(@"發(fā)送源數(shù)據(jù)發(fā)生錯(cuò)誤:%@", error);
}
}];
}
獲取數(shù)據(jù)路徑的方法,如下
-(NSString *)imagePath{
NSString * path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).lastObject;
NSString * newPath = [path stringByAppendingPathComponent:@"nothing.jpeg"];
UIImage * image = [UIImage imageNamed:@"WechatIMG2"];
NSData * data = UIImageJPEGRepresentation(image, 0.1);
[data writeToFile:newPath atomically:YES];
return newPath;
}
數(shù)據(jù)發(fā)送了,在指定的藍(lán)牙設(shè)備上會(huì)收到數(shù)據(jù),會(huì)依次走M(jìn)CSession的兩個(gè)代理方法,即上面說(shuō)的剛開(kāi)始接收到和接受完成的代理方法,可以在相應(yīng)的時(shí)機(jī),進(jìn)行相關(guān)操作
- 數(shù)據(jù)源傳輸剛剛開(kāi)始調(diào)用:一般用于設(shè)置一些初始值,比如文件接受者的進(jìn)度Progress進(jìn)度KVO觀察。
- 數(shù)據(jù)源傳輸結(jié)束時(shí)調(diào)用:主要用于將傳輸?shù)奈募臅簳r(shí)存放的位置放到真正需要存放的位置。
//開(kāi)始接收Resource數(shù)據(jù)
- (void)session:(MCSession *)session didStartReceivingResourceWithName:(NSString *)resourceName fromPeer:(MCPeerID *)peerID withProgress:(NSProgress *)progress{
NSLog(@"開(kāi)始獲取文件數(shù)據(jù)");
}
//完成Resource數(shù)據(jù)的接收
- (void)session:(MCSession *)session didFinishReceivingResourceWithName:(NSString *)resourceName fromPeer:(MCPeerID *)peerID atURL:(NSURL *)localURL withError:(nullable NSError *)error{
NSLog(@"獲取文件數(shù)據(jù)結(jié)束");
NSURL *destinationURL = [NSURL fileURLWithPath:[self imagePath]];
//判斷文件是否存在,存在則刪除
if ([[NSFileManager defaultManager] isDeletableFileAtPath:[self imagePath]]) {
[[NSFileManager defaultManager] removeItemAtPath:[self imagePath] error:nil];
}
//轉(zhuǎn)移文件
NSError *error1 = nil;
if (![[NSFileManager defaultManager] moveItemAtURL:localURL toURL:destinationURL error:&error1]) {
NSLog(@"[Error] %@", error1);
}
//轉(zhuǎn)移成功展示數(shù)據(jù)
dispatch_sync(dispatch_get_main_queue(), ^{
NSData * data = [NSData dataWithContentsOfURL:destinationURL];
UIImage * image = [[UIImage alloc] initWithData:data];
self.imageView.image = image;
});
}
和上面一樣,在廣播和發(fā)現(xiàn)的控制器中都實(shí)現(xiàn)上面的代碼,就可以實(shí)現(xiàn)互傳。
到這一步,MultipeerConnectivity框架講解完畢。