iOS 藍(lán)牙傳輸 -- MultipeerConnectivity

一、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)界面
廣播和發(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框架講解完畢。

三、代碼下載

MultipeerConnectivity

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容