改進(jìn)Android語音對講系統(tǒng)的方法

本文屬于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)容:

  1. 通過生產(chǎn)者-消費(fèi)者模式保證數(shù)據(jù)鏈路的魯棒性
  2. 改進(jìn)音頻錄制及播放,提高語音通信質(zhì)量
  3. 采用多播實(shí)現(xiàn)設(shè)備發(fā)現(xiàn)及跨路由通信
  4. 實(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)者線程就會退出。

本文通過LinkedBlockingQueueputtake方法實(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();
  • LinkedBlockingQueueLinkedList類似,通過靜態(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ā)修改的線程安全問題。
  • headlast分別表示鏈表的頭部和尾部;
  • 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)分析下puttake的過程:

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;
}

之所以把puttake放在一起,是因?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)用dequeuehead指向原來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_COMMUNICATIONVOICE_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。

子網(wǎng)廣播和多播組

發(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)文。

IGMP加入組報(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ī)則這里不再贅述。

D類IP地址到以太網(wǎng)多播地址的映射

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


多播組地址到以太網(wǎng)地址的轉(zhuǎn)換

代碼層面上,探測線程將子網(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)。

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);
}

startRecordstopRecord的具體實(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分別為IBinderCallback

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線程管理(一)——線程通信》。

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

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

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