本文屬于Android局域網(wǎng)內(nèi)的語音對講項(xiàng)目系列,《實(shí)時(shí)Android語音對講系統(tǒng)架構(gòu)》闡述了局域網(wǎng)內(nèi)Android語音對講功能的框架,本文在此基礎(chǔ)上進(jìn)行了優(yōu)化,包括音頻的錄制、播放,通信方式,以及整體架構(gòu)的改進(jìn)。
本文主要包括以下內(nèi)容:
- 通過生產(chǎn)者-消費(fèi)者模式保證數(shù)據(jù)鏈路的魯棒性
- 改進(jìn)音頻錄制及播放,提高語音通信質(zhì)量
- 采用多播實(shí)現(xiàn)設(shè)備發(fā)現(xiàn)及跨路由通信
- 實(shí)現(xiàn)對講進(jìn)程與UI進(jìn)程的通信(AIDL)
一、通過生產(chǎn)者-消費(fèi)者模式保證數(shù)據(jù)鏈路的魯棒性
1. 從責(zé)任鏈到生產(chǎn)者-消費(fèi)者
在《實(shí)時(shí)Android語音對講系統(tǒng)架構(gòu)》對語音對講系統(tǒng)的數(shù)據(jù)鏈路的分析中提到,數(shù)據(jù)包要經(jīng)過Record、Encoder、Transmission、Decoder、Play這一鏈條的處理,這種數(shù)據(jù)流轉(zhuǎn)就是對講機(jī)核心抽象,鑒于這種場景,采用了責(zé)任鏈設(shè)計(jì)模式。
在后續(xù)實(shí)踐中發(fā)現(xiàn)這樣的結(jié)構(gòu)存在一些問題,責(zé)任鏈模式適用于數(shù)據(jù)即時(shí)流轉(zhuǎn),需要整個(gè)鏈路沒有阻塞、等待。而在本應(yīng)用場景中,編解碼及錄制播放均可能存在時(shí)間延遲,責(zé)任鏈模式無法兼顧網(wǎng)絡(luò)、編解碼的延時(shí)。
事實(shí)上,通過緩存隊(duì)列則可以保證數(shù)據(jù)鏈路的穩(wěn)定性,分別在編解碼和數(shù)據(jù)發(fā)送接收時(shí)加入阻塞隊(duì)列,可以實(shí)現(xiàn)數(shù)據(jù)包的緩沖,同時(shí)降低丟包的可能。因此,在本系統(tǒng)場景下,基于阻塞隊(duì)列實(shí)現(xiàn)了生產(chǎn)者-消費(fèi)者模式,是對責(zé)任鏈模式的優(yōu)化,意在提高數(shù)據(jù)鏈路的魯棒性。
2. 基于阻塞隊(duì)列實(shí)現(xiàn)生產(chǎn)者-消費(fèi)者模式
本節(jié)包括以下內(nèi)容:
- 阻塞隊(duì)列(數(shù)據(jù)結(jié)構(gòu))
- 阻塞隊(duì)列實(shí)現(xiàn)生產(chǎn)者-消費(fèi)者模式
阻塞隊(duì)列(數(shù)據(jù)結(jié)構(gòu))
阻塞隊(duì)列(BlockingQueue)是一個(gè)支持兩個(gè)附加操作的隊(duì)列。這兩個(gè)附加的操作是:
- 在隊(duì)列為空時(shí),獲取元素的線程會等待隊(duì)列變?yōu)榉强铡?/li>
- 當(dāng)隊(duì)列滿時(shí),存儲元素的線程會等待隊(duì)列可用。
阻塞隊(duì)列常用于生產(chǎn)者和消費(fèi)者的場景,生產(chǎn)者是往隊(duì)列里添加元素的線程,消費(fèi)者是從隊(duì)列里拿元素的線程。阻塞隊(duì)列就是生產(chǎn)者存放元素的容器,而消費(fèi)者也只從容器里拿元素。
阻塞隊(duì)列提供了四種處理方法:
| 方法 | 拋出異常 | 返回特殊值 | 一直阻塞 | 超時(shí)退出 |
|---|---|---|---|---|
| 插入方法 | add(e) | offer(e) | put(e) | offer(e,time,unit) |
| 移除方法 | remove() | poll() | take() | poll(time,unit) |
| 檢查方法 | element() | peek() | 不可用 | 不可用 |
- 拋出異常:是指當(dāng)阻塞隊(duì)列滿時(shí)候,再往隊(duì)列里插入元素,會拋出
IllegalStateException("Queue full")異常。當(dāng)隊(duì)列為空時(shí),從隊(duì)列里獲取元素時(shí)會拋出NoSuchElementException異常 。 - 返回特殊值:插入方法會返回是否成功,成功則返回true。移除方法,則是從隊(duì)列里拿出一個(gè)元素,如果沒有則返回null
- 一直阻塞:當(dāng)阻塞隊(duì)列滿時(shí),如果生產(chǎn)者線程往隊(duì)列里put元素,隊(duì)列會一直阻塞生產(chǎn)者線程,直到拿到數(shù)據(jù),或者響應(yīng)中斷退出。當(dāng)隊(duì)列空時(shí),消費(fèi)者線程試圖從隊(duì)列里take元素,隊(duì)列也會阻塞消費(fèi)者線程,直到隊(duì)列可用。
- 超時(shí)退出:當(dāng)阻塞隊(duì)列滿時(shí),隊(duì)列會阻塞生產(chǎn)者線程一段時(shí)間,如果超過一定的時(shí)間,生產(chǎn)者線程就會退出。
本文通過LinkedBlockingQueue的put和take方法實(shí)現(xiàn)線程阻塞。LinkedBlockingQueue是一個(gè)用鏈表實(shí)現(xiàn)的有界阻塞隊(duì)列。此隊(duì)列的默認(rèn)和最大長度為Integer.MAX_VALUE。此隊(duì)列按照先進(jìn)先出的原則對元素進(jìn)行排序。
首先看下LinkedBlockingQueue中核心的域:
static class Node<E> {
E item;
Node<E> next;
Node(E x) { item = x; }
}
private final int capacity;
private final AtomicInteger count = new AtomicInteger();
transient Node<E> head;
private transient Node<E> last;
private final ReentrantLock takeLock = new ReentrantLock();
private final Condition notEmpty = takeLock.newCondition();
private final ReentrantLock putLock = new ReentrantLock();
private final Condition notFull = putLock.newCondition();
-
LinkedBlockingQueue和LinkedList類似,通過靜態(tài)內(nèi)部類Node<E>進(jìn)行元素的存儲; -
capacity表示阻塞隊(duì)列所能存儲的最大容量,在創(chuàng)建時(shí)可以手動(dòng)指定最大容量,默認(rèn)的最大容量為Integer.MAX_VALUE; -
count表示當(dāng)前隊(duì)列中的元素?cái)?shù)量,LinkedBlockingQueue的入隊(duì)列和出隊(duì)列使用了兩個(gè)不同的lock對象,因此無論是在入隊(duì)列還是出隊(duì)列,都會涉及對元素?cái)?shù)量的并發(fā)修改,因此這里使用了一個(gè)原子操作類來解決對同一個(gè)變量進(jìn)行并發(fā)修改的線程安全問題。 -
head和last分別表示鏈表的頭部和尾部; -
takeLock表示元素出隊(duì)列時(shí)線程所獲取的鎖,當(dāng)執(zhí)行take、poll等操作時(shí)線程獲??;notEmpty當(dāng)隊(duì)列為空時(shí),通過該Condition讓獲取元素的線程處于等待狀態(tài); -
putLock表示元素入隊(duì)列時(shí)線程所獲取的鎖,當(dāng)執(zhí)行put、offer等操作時(shí)獲?。?code>notFull當(dāng)隊(duì)列容量達(dá)到capacity時(shí),通過該Condition讓加入元素的線程處于等待狀態(tài)。
其次,LinkedBlockingQueue有三個(gè)構(gòu)造方法,分別如下:
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
last = head = new Node<E>(null);
}
public LinkedBlockingQueue(Collection<? extends E> c) {
this(Integer.MAX_VALUE);
final ReentrantLock putLock = this.putLock;
putLock.lock(); // Never contended, but necessary for visibility
try {
int n = 0;
for (E e : c) {
if (e == null)
throw new NullPointerException();
if (n == capacity)
throw new IllegalStateException("Queue full");
enqueue(new Node<E>(e));
++n;
}
count.set(n);
} finally {
putLock.unlock();
}
}
默認(rèn)構(gòu)造函數(shù)直接調(diào)用LinkedBlockingQueue(int capacity),LinkedBlockingQueue(int capacity)會初始化首尾節(jié)點(diǎn),并置位null。LinkedBlockingQueue(Collection<? extends E> c)在初始化隊(duì)列的同時(shí),將一個(gè)集合的全部元素加入隊(duì)列。
最后,重點(diǎn)分析下put和take的過程:
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
int c = -1;
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly();
try {
while (count.get() == capacity) {
notFull.await();
}
enqueue(node);
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
}
public E take() throws InterruptedException {
E x;
int c = -1;
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try {
while (count.get() == 0) {
notEmpty.await();
}
x = dequeue();
c = count.getAndDecrement();
if (c > 1)
notEmpty.signal();
} finally {
takeLock.unlock();
}
if (c == capacity)
signalNotFull();
return x;
}
之所以把put和take放在一起,是因?yàn)樗鼈兪且粚ツ娴倪^程:
-
put在插入元素前首先獲得putLock和當(dāng)前隊(duì)列的元素?cái)?shù)量,take在去除元素錢首先獲得takeLock和當(dāng)前隊(duì)列的元素?cái)?shù)量; -
put時(shí)需要判斷當(dāng)前隊(duì)列是否已滿,已滿時(shí)當(dāng)前線程進(jìn)行等待,take時(shí)需要判斷隊(duì)列是否已空,隊(duì)列為空時(shí)當(dāng)前線程進(jìn)行等待; -
put調(diào)用enqueue在隊(duì)尾插入元素,并修改尾指針,take調(diào)用dequeue將head指向原來first的位置,并將first的數(shù)據(jù)域置位null,實(shí)現(xiàn)刪除原first指針,并產(chǎn)生新的head,同時(shí),切斷原head節(jié)點(diǎn)的引用,便于垃圾回收。
private void enqueue(Node<E> node) {
last = last.next = node;
}
private E dequeue() {
Node<E> h = head;
Node<E> first = h.next;
h.next = h; // help GC
head = first;
E x = first.item;
first.item = null;
return x;
}
- 最后,
put根據(jù)count決定是否觸發(fā)隊(duì)列未滿和隊(duì)列空;take根據(jù)count決定是否觸發(fā)隊(duì)列未空和隊(duì)列滿。
LinkedBlockingQueue在入隊(duì)列和出隊(duì)列時(shí)使用的是不同的Lock,這也意味著它們之間的操作不會存在互斥。在多個(gè)CPU的情況下,可以做到在同一時(shí)刻既消費(fèi)、又生產(chǎn),做到并行處理。
阻塞隊(duì)列實(shí)現(xiàn)生產(chǎn)者-消費(fèi)者模式
通過對LinkedBlockingQueue主要源碼的分析,實(shí)現(xiàn)生產(chǎn)者-消費(fèi)者模式就變得簡單了。
public class MessageQueue {
private static MessageQueue messageQueue1, messageQueue2, messageQueue3, messageQueue4;
private BlockingQueue<AudioData> audioDataQueue = null;
private MessageQueue() {
audioDataQueue = new LinkedBlockingQueue<>();
}
@Retention(SOURCE)
@IntDef({ENCODER_DATA_QUEUE, SENDER_DATA_QUEUE, DECODER_DATA_QUEUE, TRACKER_DATA_QUEUE})
public @interface DataQueueType {
}
public static final int ENCODER_DATA_QUEUE = 0;
public static final int SENDER_DATA_QUEUE = 1;
public static final int DECODER_DATA_QUEUE = 2;
public static final int TRACKER_DATA_QUEUE = 3;
public static MessageQueue getInstance(@DataQueueType int type) {
switch (type) {
case ENCODER_DATA_QUEUE:
if (messageQueue1 == null) {
messageQueue1 = new MessageQueue();
}
return messageQueue1;
case SENDER_DATA_QUEUE:
if (messageQueue2 == null) {
messageQueue2 = new MessageQueue();
}
return messageQueue2;
case DECODER_DATA_QUEUE:
if (messageQueue3 == null) {
messageQueue3 = new MessageQueue();
}
return messageQueue3;
case TRACKER_DATA_QUEUE:
if (messageQueue4 == null) {
messageQueue4 = new MessageQueue();
}
return messageQueue4;
default:
return new MessageQueue();
}
}
public void put(AudioData audioData) {
try {
audioDataQueue.put(audioData);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public AudioData take() {
try {
return audioDataQueue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
return null;
}
}
這里通過@IntDef來實(shí)現(xiàn)限定輸入類型的功能,同時(shí),阻塞隊(duì)列保持單實(shí)例,然后將隊(duì)列分別應(yīng)用到各個(gè)生產(chǎn)者-消費(fèi)者線程中。在本文的語音對講系統(tǒng)中,以音頻錄制線程和編碼線程為例,錄制線程是音頻數(shù)據(jù)包的生產(chǎn)者,編碼線程是音頻數(shù)據(jù)包的消費(fèi)者。
音頻錄制線程:
@Override
public void run() {
while (isRecording) {
if (audioRecord.getRecordingState() == AudioRecord.RECORDSTATE_STOPPED) {
audioRecord.startRecording();
}
// 實(shí)例化音頻數(shù)據(jù)緩沖
short[] rawData = new short[inAudioBufferSize];
audioRecord.read(rawData, 0, inAudioBufferSize);
AudioData audioData = new AudioData(rawData);
MessageQueue.getInstance(MessageQueue.ENCODER_DATA_QUEUE).put(audioData);
}
}
編碼線程:
@Override
public void run() {
AudioData data;
// 在MessageQueue為空時(shí),take方法阻塞
while ((data = MessageQueue.getInstance(MessageQueue.ENCODER_DATA_QUEUE).take()) != null) {
data.setEncodedData(AudioDataUtil.raw2spx(data.getRawData()));
MessageQueue.getInstance(MessageQueue.SENDER_DATA_QUEUE).put(data);
}
}
同樣的,編碼線程和發(fā)送線程,接收線程和解碼線程,解碼線程和播放線程同樣存在生產(chǎn)者-消費(fèi)者的關(guān)系。
二、改進(jìn)音頻錄制及播放,提高語音通信質(zhì)量
錄制,改變了音頻輸入源,將直接從麥克風(fēng)(
MIC)獲取改為MediaRecorder.AudioSource.VOICE_COMMUNICATION,VOICE_COMMUNICATION能自動(dòng)回聲消除和增益,因此,屏蔽了speex在C層的降噪和增益。播放,改變了音頻輸出端,將
STREAM_MUSIC換成STREAM_VOICE_CALL,因?yàn)椋瑢χv機(jī)應(yīng)用更類似于語音通信。換成STREAM_VOICE_CALL之后,遇到的問題是只能從聽筒聽到聲音,于是設(shè)置免提功能。
AudioManager audioManager =(AudioManager) getSystemService(Context.AUDIO_SERVICE);
audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
audioManager.setSpeakerphoneOn(true);
該設(shè)置必須要開放修改音頻的權(quán)限,不然沒有效果。
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
目前的語音通信質(zhì)量,個(gè)人感覺仍然需要繼續(xù)優(yōu)化,如果您有這方面的經(jīng)驗(yàn)(包括但不限于Java層和Speex音頻處理),不吝賜教!
三、采用多播實(shí)現(xiàn)設(shè)備發(fā)現(xiàn)及跨路由通信
《通過UDP廣播實(shí)現(xiàn)Android局域網(wǎng)Peer Discovering》中從編程的角度說明了TCP與UDP的區(qū)別,主要分析了TCP是面向連接的、可靠的服務(wù),建立連接需要經(jīng)過三次握手、銷毀連接需要四次揮手;UDP是無連接的傳輸層協(xié)議,提供面向事務(wù)的簡單不可靠信息傳送服務(wù)。
IP地址分為三類:單播、廣播和多播。廣播和多播僅用于UDP,它們用于將報(bào)文同時(shí)傳送給多個(gè)接收者。廣播分為:受限廣播、指向網(wǎng)絡(luò)的廣播、指向子網(wǎng)的廣播、指向所有子網(wǎng)的廣播。
舉個(gè)栗子:當(dāng)前IP為10.13.200.16/22,首先廣播地址為255.255.255.255,子網(wǎng)廣播地址為10.13.203.255。
《通過UDP廣播實(shí)現(xiàn)Android局域網(wǎng)Peer Discovering》采用子網(wǎng)廣播實(shí)現(xiàn)局域網(wǎng)Android設(shè)備的發(fā)現(xiàn),但在實(shí)踐中,一般路由器會禁止所有廣播跨路由器傳輸。所以,如果子網(wǎng)內(nèi)有多個(gè)路由器,那么就無法實(shí)現(xiàn)設(shè)備發(fā)現(xiàn)了。因此,本文將設(shè)備發(fā)現(xiàn)也改為多播實(shí)現(xiàn)。多播組地址包括為1110的最高4bit和多播組號,范圍為224.0.0.0到239.255.255.255。能夠接收發(fā)往一個(gè)特定多播組地址數(shù)據(jù)的主機(jī)集合稱為主機(jī)組,主機(jī)組可以跨越多個(gè)網(wǎng)絡(luò)。
IANA 把224.0.0.0 到 224.0.0.255 范圍內(nèi)的地址全部都保留給了路由協(xié)議和其他網(wǎng)絡(luò)維護(hù)功能。該范圍內(nèi)的地址屬于局部范疇,不論生存時(shí)間字段(TTL)值是多少,都不會被路由器轉(zhuǎn)發(fā);D類保留地址的完整的列表可以參見RFC1700。
224.0.1.0 到 238.255.255.255 地址范圍作為用戶組播地址,在全網(wǎng)范圍內(nèi)有效。其中233/8 為 GLOP 地址。GLOP 是一種自治系統(tǒng)之間的組播地址分配機(jī)制,將 AS 號直接填入組播地址的中間兩個(gè)字節(jié)中,每個(gè)自治系統(tǒng)都可以得到 255 個(gè)組播地址;
239.0.0.0 到 239.255.255.255 地址范圍為本地管理組播地址(administratively scoped addresses),僅在特定的本地范圍內(nèi)有效。
本文對比了子網(wǎng)廣播和多播,子網(wǎng)廣播地址為:192.168.137.255,多播組地址為:224.5.6.7。

發(fā)送接收采用同一MulticastSocket,MulticastSocket設(shè)置TTL,TTL表示跨網(wǎng)絡(luò)的級數(shù)。
try {
inetAddress = InetAddress.getByName(Constants.MULTI_BROADCAST_IP);
multicastSocket = new MulticastSocket(Constants.MULTI_BROADCAST_PORT);
multicastSocket.setLoopbackMode(true);
multicastSocket.joinGroup(inetAddress);
multicastSocket.setTimeToLive(4);
} catch (IOException e) {
e.printStackTrace();
}
joinGroup涉及到另一個(gè)協(xié)議:網(wǎng)路群組管理協(xié)議(Internet Group Management Protocol或簡寫IGMP),通過抓包可以觀察到初始化MulticastSocket時(shí)加入組協(xié)議的報(bào)文。

setTimeToLive用于設(shè)置生存時(shí)間字段。默認(rèn)情況下,多播數(shù)據(jù)報(bào)的TTL設(shè)置為1,使得多播數(shù)據(jù)報(bào)僅限于在同一個(gè)子網(wǎng)內(nèi)傳送,更大的TTL值能夠被多播路由器轉(zhuǎn)發(fā)。在實(shí)際傳輸過程中,多播組地址仍然需要轉(zhuǎn)換為以太網(wǎng)地址。實(shí)際轉(zhuǎn)換規(guī)則這里不再贅述。

上述多播地址224.5.6.7轉(zhuǎn)換后為01:00:5e:05:06:07。

代碼層面上,探測線程將子網(wǎng)廣播改為多播實(shí)現(xiàn)。
if (command != null) {
byte[] data = command.getBytes();
DatagramPacket datagramPacket = new DatagramPacket(
data, data.length, Multicast.getMulticast().getInetAddress(), Constants.MULTI_BROADCAST_PORT);
try {
Multicast.getMulticast().getMulticastSocket().send(datagramPacket);
} catch (IOException e) {
e.printStackTrace();
}
}
并且在接收端區(qū)分指令和音頻數(shù)據(jù)。
while (true) {
// 設(shè)置接收緩沖段
byte[] receivedData = new byte[512];
DatagramPacket datagramPacket = new DatagramPacket(receivedData, receivedData.length);
try {
// 接收數(shù)據(jù)報(bào)文
Multicast.getMulticast().getMulticastSocket().receive(datagramPacket);
} catch (IOException e) {
e.printStackTrace();
}
// 判斷數(shù)據(jù)報(bào)文類型,并做相應(yīng)處理
if (datagramPacket.getLength() == Command.DISC_REQUEST.getBytes().length ||
datagramPacket.getLength() == Command.DISC_LEAVE.getBytes().length ||
datagramPacket.getLength() == Command.DISC_RESPONSE.getBytes().length) {
handleCommandData(datagramPacket);
} else {
handleAudioData(datagramPacket);
}
}
四、實(shí)現(xiàn)對講進(jìn)程與UI進(jìn)程的通信(AIDL)
在實(shí)際工程應(yīng)用場景中,需要對講機(jī)進(jìn)程即使切換到后臺,也依然能收到信息。因此,為了提高進(jìn)程的優(yōu)先級,降低被系統(tǒng)回收的概率,采用了在Service中訪問網(wǎng)絡(luò)服務(wù),處理語音信息的發(fā)送和接收的方案。前臺Activity負(fù)責(zé)顯示組播組內(nèi)用戶(上線和下線,更新頁面),通過AIDL與Service進(jìn)行跨進(jìn)程通信和回調(diào)。Service的清單說明如下:
<service
android:name=".service.IntercomService"
android:process=":intercom" />
:intercom表示定義子進(jìn)程intercom。
使用多進(jìn)程相比于常見的單進(jìn)程,有一些需要注意的點(diǎn):
- 靜態(tài)成員和單例模式失效。因?yàn)槊總€(gè)進(jìn)程都會分配一個(gè)獨(dú)立的虛擬機(jī),不同的虛擬機(jī)對應(yīng)不同的地址空間;
- 線程同步機(jī)制失效。因此不同進(jìn)程鎖的并不是同一個(gè)對象;
- Application會多次創(chuàng)建。進(jìn)程與Application對應(yīng),多進(jìn)程會啟動(dòng)多個(gè)Application。
因此,通過process定義了多進(jìn)程之后,一定要避免單進(jìn)程模式下對象共享的思路。另外,在AS中調(diào)試多進(jìn)程應(yīng)用的時(shí)候,斷點(diǎn)一定要針對不同的進(jìn)程,以本文為例,添加斷點(diǎn)需要選擇主進(jìn)程和intercom進(jìn)程。給兩個(gè)進(jìn)程分別添加調(diào)試斷點(diǎn)后,可以看到有兩個(gè)Debugger:3156和3230(由于存在Jni代碼,所以顯示了Hybrid Debugger)。

1. 定義AIDL文件
由于既存在Activity到Service的通信,也存在Service接收到消息之后更新Activity頁面的需求,所以這里采用了跨進(jìn)程回調(diào)的方式。首先,AIDL方法如下:
package com.jd.wly.intercom.service;
import com.jd.wly.intercom.service.IIntercomCallback;
interface IIntercomService {
void startRecord();
void stopRecord();
void registerCallback(IIntercomCallback callback);
void unRegisterCallback(IIntercomCallback callback);
}
package com.jd.wly.intercom.service;
interface IIntercomCallback {
void findNewUser(String ipAddress);
void removeUser(String ipAddress);
}
IIntercomService定義了Activity到Service的通信方法,包含啟動(dòng)和停止音頻錄制,以及注冊和解除回調(diào)接口;IIntercomCallback定義了從Service到Activity的回調(diào)接口,用于在Service發(fā)現(xiàn)用戶上線、下線時(shí)通知前臺Activity的顯示。
AIDL文件的定義涉及一些規(guī)范:比如變量在同一包內(nèi)也需要import,非基本數(shù)據(jù)類型參數(shù)列表需要指明in、out,自定義參數(shù)類型需要同時(shí)編寫java文件和aidl文件等,本文篇幅有限,就不具體展開AIDL跨進(jìn)程通信的細(xì)節(jié)了。
2. 從Activity到Service的通信
Activity檢測用戶的按鍵操作,然后將事件傳遞給Service進(jìn)行對應(yīng)的邏輯處理。
將Service綁定到Activity首先需要定義ServiceConnection:
/**
* onServiceConnected和onServiceDisconnected運(yùn)行在UI線程中
*/
private IIntercomService intercomService;
private ServiceConnection serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
intercomService = IIntercomService.Stub.asInterface(service);
try {
intercomService.registerCallback(intercomCallback);
} catch (RemoteException e) {
e.printStackTrace();
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
intercomService = null;
}
};
在onStart()時(shí)綁定Service,onStop()時(shí)解除回調(diào)和綁定。
@Override
protected void onStart() {
super.onStart();
Intent intent = new Intent(AudioActivity.this, IntercomService.class);
bindService(intent, serviceConnection, BIND_AUTO_CREATE);
}
@Override
protected void onStop() {
super.onStop();
if (intercomService != null && intercomService.asBinder().isBinderAlive()) {
try {
intercomService.unRegisterCallback(intercomCallback);
} catch (RemoteException e) {
e.printStackTrace();
}
unbindService(serviceConnection);
}
}
Activity獲取了Service的服務(wù)后,分別在按鍵事件處理中進(jìn)行調(diào)用。
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if ((keyCode == KeyEvent.KEYCODE_F2 ||
keyCode == KeyEvent.KEYCODE_VOLUME_UP || keyCode == KeyEvent.KEYCODE_VOLUME_DOWN)) {
try {
intercomService.startRecord();
} catch (RemoteException e) {
e.printStackTrace();
}
return true;
}
return super.onKeyDown(keyCode, event);
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
if ((keyCode == KeyEvent.KEYCODE_F2 ||
keyCode == KeyEvent.KEYCODE_VOLUME_UP || keyCode == KeyEvent.KEYCODE_VOLUME_DOWN)) {
try {
intercomService.stopRecord();
} catch (RemoteException e) {
e.printStackTrace();
}
return true;
}
return super.onKeyUp(keyCode, event);
}
startRecord和stopRecord的具體實(shí)現(xiàn)定義在Service中:
public IIntercomService.Stub mBinder = new IIntercomService.Stub() {
@Override
public void startRecord() throws RemoteException {
if (!recorder.isRecording()) {
recorder.setRecording(true);
tracker.setPlaying(false);
threadPool.execute(recorder);
}
}
@Override
public void stopRecord() throws RemoteException {
if (recorder.isRecording()) {
recorder.setRecording(false);
tracker.setPlaying(true);
}
}
@Override
public void registerCallback(IIntercomCallback callback) throws RemoteException {
mCallbackList.register(callback);
}
@Override
public void unRegisterCallback(IIntercomCallback callback) throws RemoteException {
mCallbackList.unregister(callback);
}
};
3. 從Service到Activity的通信
Service通過RemoteCallbackList保持回調(diào)方法,使用時(shí)首先定義RemoteCallbackList對象,泛型類型為IIntercomCallback。
private RemoteCallbackList<IIntercomCallback> mCallbackList = new RemoteCallbackList<>();
RemoteCallbackList并不是List,內(nèi)部通過Map來保存,Key和Value分別為IBinder和Callback。
ArrayMap<IBinder, Callback> mCallbacks = new ArrayMap<IBinder, Callback>();
使用RemoteCallbackList回調(diào)Activity方法時(shí),通過beginBroadcast獲取數(shù)量,
/**
* 發(fā)現(xiàn)新的組播成員
*
* @param ipAddress IP地址
*/
private void findNewUser(String ipAddress) {
final int size = mCallbackList.beginBroadcast();
for (int i = 0; i < size; i++) {
IIntercomCallback callback = mCallbackList.getBroadcastItem(i);
if (callback != null) {
try {
callback.findNewUser(ipAddress);
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
mCallbackList.finishBroadcast();
}
removeUser(String ipAddress)方法與findNewUser(String ipAddress)方法類似。它們具體的實(shí)現(xiàn)在Activity中:
/**
* 被調(diào)用的方法運(yùn)行在Binder線程池中,不能更新UI
*/
private IIntercomCallback intercomCallback = new IIntercomCallback.Stub() {
@Override
public void findNewUser(String ipAddress) throws RemoteException {
sendMsg2MainThread(ipAddress, FOUND_NEW_USER);
}
@Override
public void removeUser(String ipAddress) throws RemoteException {
sendMsg2MainThread(ipAddress, REMOVE_USER);
}
};
需要注意的是,IIntercomCallback中的回調(diào)方法實(shí)現(xiàn)并不在UI線程中執(zhí)行,如果需要更新UI,需要實(shí)現(xiàn)多線程調(diào)用,多線程依然通過Handler來實(shí)現(xiàn),這里不再贅述,如果需要,請參考:《Android線程管理(一)——線程通信》。