標籤:

實時Android語音對講系統架構

本文屬於Android區域網內的語音對講項目(github.com/yhthu/interc)系列,《通過UDP廣播實現Android區域網Peer Discovering》(jianshu.com/p/cc62e070a)實現了區域網內的廣播及多播通信,本文將重點說明系統架構,音頻信號的實時錄製、播放及編解碼相關技術。

本文主要包含以下內容:

1、AudioRecord、AudioTrack

2、Speex編解碼

3、Android語音對講系統架構

01AudioRecord、AudioTrack

AudioRecorder和AudioTracker是Android中獲取實時音頻數據的介面。在網路電話、語音對講等場景中,由於實時性的要求,不能採用文件傳輸,因此,MediaRecorder和MediaPlayer就無法使用。

AudioRecorder和AudioTracker是Android在Java層對libmedia庫的封裝,所以效率較高,適合於實時語音相關處理的應用。在使用時,AudioRecorder和AudioTracker的構造器方法入參較多,這裡對其進行詳細的解釋。

AudioRecord

public AudioRecord(int audioSource, int sampleRateInHz, int channelConfig, int audioFormat, int bufferSizeInBytes)n

其中,audioSource表示錄音來源,在AudioSource中列舉了不同的音頻來源,包括:

AudioSource.DEFAULT:默認音頻來源nAudioSource.MIC:麥克風(常用)nAudioSource.VOICE_UPLINK:電話上行nAudioSource.VOICE_DOWNLINK:電話下行nAudioSource.VOICE_CALL:電話、含上下行nAudioSource.CAMCORDER:攝像頭旁的麥克風nAudioSource.VOICE_RECOGNITION:語音識別nAudioSource.VOICE_COMMUNICATION:語音通信n

這裡比較常用的有MIC,VOICE_COMMUNICATION和VOICE_CALL。

sampleRateInHz表示採樣頻率。音頻的採集過程要經過抽樣、量化和編碼三步。抽樣需要關注抽樣率。聲音是機械波,其特徵主要包括頻率和振幅(即音調和音量),頻率對應時間軸線,振幅對應電平軸線。採樣是指間隔固定的時間對波形進行一次記錄,採樣率就是在1秒內採集樣本的次數。量化過程就是用數字表示振幅的過程。編碼是一個減少信息量的過程,任何數字音頻編碼方案都是有損的。PCM編碼(脈衝編碼調製)是一種保真水平較高的編碼方式。在Android平台,44100Hz是唯一目前所有設備都保證支持的採樣頻率。但比如22050、16000、11025也在大多數設備上得到支持。8000是針對某些低質量的音頻通信使用的。

channelConfig表示音頻通道,即選擇單聲道、雙聲道等參數。系統提供的選擇如下:

public static final int CHANNEL_IN_DEFAULT = 1;n// These directly match nativenpublic static final int CHANNEL_IN_LEFT = 0x4;npublic static final int CHANNEL_IN_RIGHT = 0x8;npublic static final int CHANNEL_IN_FRONT = 0x10;npublic static final int CHANNEL_IN_BACK = 0x20;npublic static final int CHANNEL_IN_LEFT_PROCESSED = 0x40;npublic static final int CHANNEL_IN_RIGHT_PROCESSED = 0x80;npublic static final int CHANNEL_IN_FRONT_PROCESSED = 0x100;npublic static final int CHANNEL_IN_BACK_PROCESSED = 0x200;npublic static final int CHANNEL_IN_PRESSURE = 0x400;npublic static final int CHANNEL_IN_X_AXIS = 0x800;npublic static final int CHANNEL_IN_Y_AXIS = 0x1000;npublic static final int CHANNEL_IN_Z_AXIS = 0x2000;npublic static final int CHANNEL_IN_VOICE_UPLINK = 0x4000;npublic static final int CHANNEL_IN_VOICE_DNLINK = 0x8000;npublic static final int CHANNEL_IN_MONO = CHANNEL_IN_FRONT;npublic static final int CHANNEL_IN_STEREO = (CHANNEL_IN_LEFT | CHANNEL_IN_RIGHT);n

常用的是CHANNEL_IN_MONO和CHANNEL_IN_STEREO分別表示單通道輸入和左右兩通道輸入。

audioFormat指定返迴音頻數據的格式,常見的選擇包括ENCODING_PCM_16BIT、ENCODING_PCM_8BIT和ENCODING_PCM_FLOAT。ENCODING_PCM_16BIT表示PCM 16bits每個樣本,所有設備保證支持。ENCODING_PCM_8BIT自然表示PCM 8bits每個樣本。ENCODING_PCM_FLOAT表示一個單精度浮點數表示一個樣本。

bufferSizeInBytes表示錄音時音頻數據寫入的buffer的大小。這個數值是通過另一個方法來獲取的:getMinBufferSize。getMinBufferSize是AudioRecord類的靜態方法,返回值就是bufferSizeInBytes。這裡我們來看下它的入參:

static public int getMinBufferSize(int sampleRateInHz, int channelConfig, int audioFormat)n

sampleRateInHz, channelConfig, audioFormat三個參數與上面的含義完全一樣,代表錄音的採樣率、通道以及數據輸出的格式。綜上,AudioRecord的初始化方法如下:

// 獲取音頻數據緩衝段大小 ninAudioBufferSize = AudioRecord.getMinBufferSize( Constants.sampleRateInHz, nConstants.inputChannelConfig, Constants.audioFormat); n// 初始化音頻錄製 naudioRecord = new AudioRecord(Constants.audioSource, Constants.sampleRateInHz, Constants.inputChannelConfig, Constants.audioFormat, inAudioBufferSize);n

其中,參數設置如下:

// 採樣頻率,44100保證兼容性npublic static final int sampleRateInHz = 44100;n// 音頻數據格式:PCM 16位每個樣本,保證設備支持。npublic static final int audioFormat = AudioFormat.ENCODING_PCM_16BIT;nn// 音頻獲取源npublic static final int audioSource = MediaRecorder.AudioSource.MIC;n// 輸入單聲道npublic static final int inputChannelConfig = AudioFormat.CHANNEL_IN_MONO;n

AudioTrack

public AudioTrack(int streamType, int sampleRateInHz, int channelConfig, int audioFormat,n int bufferSizeInBytes, int mode) throws IllegalArgumentException {n this(streamType, sampleRateInHz, channelConfig, audioFormat,n bufferSizeInBytes, mode, AudioManager.AUDIO_SESSION_ID_GENERATE);n}n

與AudioRecord類似,AudioTrack的構造器方法依然有很多需要選擇的參數。其中,streamType表示音頻流播放類型,AudioManager中列出了可選的類型如下:

/** The audio stream for phone calls */npublic static final int STREAM_VOICE_CALL = AudioSystem.STREAM_VOICE_CALL;n/** The audio stream for system sounds */npublic static final int STREAM_SYSTEM = AudioSystem.STREAM_SYSTEM;n/** The audio stream for the phone ring */npublic static final int STREAM_RING = AudioSystem.STREAM_RING;n/** The audio stream for music playback */npublic static final int STREAM_MUSIC = AudioSystem.STREAM_MUSIC;n/** The audio stream for alarms */npublic static final int STREAM_ALARM = AudioSystem.STREAM_ALARM;n/** The audio stream for notifications */npublic static final int STREAM_NOTIFICATION = AudioSystem.STREAM_NOTIFICATION;n/** @hide The audio stream for phone calls when connected to bluetooth */npublic static final int STREAM_BLUETOOTH_SCO = AudioSystem.STREAM_BLUETOOTH_SCO;n/** @hide The audio stream for enforced system sounds in certain countries (e.g camera in Japan) */npublic static final int STREAM_SYSTEM_ENFORCED = AudioSystem.STREAM_SYSTEM_ENFORCED;n/** The audio stream for DTMF Tones */npublic static final int STREAM_DTMF = AudioSystem.STREAM_DTMF;n/** @hide The audio stream for text to speech (TTS) */npublic static final int STREAM_TTS = AudioSystem.STREAM_TTS;n

常用的有STREAM_VOICE_CALL,STREAM_MUSIC等,需要根據應用特點進行選擇。

sampleRateInHz和audioFormat需與AudioRecord中的參數保持一致,這裡不再介紹。

channelConfig與AudioRecord中的參數保持對應,比如AudioRecord選擇了AudioFormat.CHANNEL_IN_MONO(單通道音頻輸入),這裡需要選擇AudioFormat.CHANNEL_OUT_MONO(單通道音頻輸出)。

bufferSizeInBytes表述音頻播放緩衝區大小,同樣,也需要根據AudioTrack的靜態方法getMinBufferSize來獲取。

static public int getMinBufferSize(int sampleRateInHz, int channelConfig, int audioFormat)n

sampleRateInHz, channelConfig, audioFormat三個參數與上面的含義完全一樣,代表輸出音頻的採樣率、通道以及數據輸出的格式。

最後說明下mode和AudioManager.AUDIO_SESSION_ID_GENERATE。mode代表音頻輸出的模式:MODE_STATIC或MODE_STREAM,分別表示靜態模式和流模式。AudioManager.AUDIO_SESSION_ID_GENERATE表示AudioSessionId,即AudioTrack依附到哪個音頻會話。

比如,要給AudioRecord添加回聲消除AcousticEchoCanceler,AcousticEchoCanceler的構建方法create的入參就是sessionId,通過AudioRecord實例的getAudioSessionId()方法獲取。

綜上,AudioTrack的初始化方法如下:

public Tracker() {n // 獲取音頻數據緩衝段大小n outAudioBufferSize = AudioTrack.getMinBufferSize(n Constants.sampleRateInHz, Constants.outputChannelConfig, Constants.audioFormat);n // 初始化音頻播放n audioTrack = new AudioTrack(Constants.streamType,n Constants.sampleRateInHz, Constants.outputChannelConfig, Constants.audioFormat,n outAudioBufferSize, Constants.trackMnode);n}n

其中,參數設置如下:

// 音頻播放端npublic static final int streamType = AudioManager.STREAM_VOICE_CALL;n// 輸出單聲道npublic static final int outputChannelConfig = AudioFormat.CHANNEL_OUT_MONO;n// 音頻輸出模式npublic static final int trackMode = AudioTrack.MODE_STREAM;n

02Speek 編解碼

Speex是一個聲音編碼格式,目標是用於網路電話、線上廣播使用的語音編碼,基於CELP(一種語音編碼演算法)開發,Speex宣稱可以免費使用,以BSD授權條款開放源代碼。

Speex是由C語言開發的音頻處理庫,在Android中使用,需要通過JNI來調用。因此,對NDK開發不熟悉的朋友,可以先了解下文檔:向您的項目添加C 和 C++ 代碼()。

在Android Studio中使用C/C++庫有兩種方式:cmake和ndk-build。cmake是最新支持的方法,通過配置CMakeLists.txt文件來實現;ndk-build是傳統的方式,通過配置Android.mk()文件來實現。具體語法參考相關文檔,這裡不做深入介紹。配置完上述文件之後,需要將Gradle關聯到原生庫,通過AS的Link C++ Project with Gradle功能實現。

完成上述配置之後,正式開始在Android中使用Speex進行音頻編解碼。主要包括以下步驟:

1、下載Speex源碼。推薦使用Speex 1.2.0穩定版,由於目前Speex 已不再繼續維護,官方建議使用Opus。但在某些場合,使用Speex已然足夠滿足需求。

Speex源碼

2、在src/main下創建jni文件夾,將上述Speex源碼中include和libspeex文件夾拷貝到jni文件夾下。

3、編寫Android.mk文件和Application.mk文件。

Android.mk:

LOCAL_PATH := $(call my-dir)ninclude $(CLEAR_VARS)nLOCAL_LDLIBS :=-llognLOCAL_MODULE := libspeexnLOCAL_CFLAGS = -DFIXED_POINT -DUSE_KISS_FFT -DEXPORT="" -UHAVE_CONFIG_HnLOCAL_C_INCLUDES := $(LOCAL_PATH)/includenLOCAL_SRC_FILES := speex_jni.cpp n ./libspeex/bits.c n ./libspeex/cb_search.c n ./libspeex/exc_10_16_table.c n ./libspeex/exc_10_32_table.c n ./libspeex/exc_20_32_table.c n ./libspeex/exc_5_256_table.c n ./libspeex/exc_5_64_table.c n ./libspeex/exc_8_128_table.c n ./libspeex/filters.c n ./libspeex/gain_table_lbr.c n ./libspeex/gain_table.c n ./libspeex/hexc_10_32_table.c n ./libspeex/hexc_table.c n ./libspeex/high_lsp_tables.c n ./libspeex/kiss_fft.c n ./libspeex/kiss_fftr.c n ./libspeex/lpc.c n ./libspeex/lsp_tables_nb.c n ./libspeex/lsp.c n ./libspeex/ltp.c n ./libspeex/modes_wb.c n ./libspeex/modes.c n ./libspeex/nb_celp.c n ./libspeex/quant_lsp.c n ./libspeex/sb_celp.c n ./libspeex/smallft.c n ./libspeex/speex_callbacks.c n ./libspeex/speex_header.c n ./libspeex/speex.c n ./libspeex/stereo.c n ./libspeex/vbr.c n ./libspeex/vorbis_psy.c n ./libspeex/vq.c n ./libspeex/window.c ninclude $(BUILD_SHARED_LIBRARY)n

Application.mk:

APP_ABI := armeabi armeabi-v7an

4、新建speex_config_types.h文件。在jni中speex源碼目錄下的include/speex文件夾下,有一個speex_config_types.h.in文件,在include/speex目錄下創建speex_config_types.h,把speex_config_types.h.in的內容拷貝過來,然後把@SIZE16@改成short,把@SIZE32@改成int,對應Java數據類型。這個文件的內容如下:

#ifndef __SPEEX_TYPES_H__n#define __SPEEX_TYPES_H__ntypedef short spx_int16_t;ntypedef unsigned short spx_uint16_t;ntypedef int spx_int32_t;ntypedef unsigned int spx_uint32_t;n#endifn

5、在Java層定義編解碼需要的介面。

public class Speex {n static {n try {n System.loadLibrary("speex");n } catch (Throwable e) {n e.printStackTrace();n }n }n public native int open(int compression);n public native int getFrameSize();n public native int decode(byte encoded[], short lin[], int size);n public native int encode(short lin[], int offset, byte encoded[], int size);n public native void close();n}n

6、在C層實現上述方法(以encode為例)。

extern "C"nJNIEXPORT jint JNICALL Java_com_jd_wly_intercom_audio_Speex_encoden (JNIEnv *env, jobject obj, jshortArray lin, jint offset, jbyteArray encoded, jint size) {nn jshort buffer[enc_frame_size];n jbyte output_buffer[enc_frame_size];n int nsamples = (size-1)/enc_frame_size + 1;n int i, tot_bytes = 0;nn if (!codec_open)n return 0;nn speex_bits_reset(&ebits);nn for (i = 0; i < nsamples; i++) {n env->GetShortArrayRegion(lin, offset + i*enc_frame_size, enc_frame_size, buffer);n speex_encode_int(enc_state, buffer, &ebits);n }nn tot_bytes = speex_bits_write(&ebits, (char *)output_buffer, enc_frame_size);n env->SetByteArrayRegion(encoded, 0, tot_bytes, output_buffer);nn return (jint)tot_bytes;n}n

7、命令行到Android.mk文件夾下,執行命令ndk-build:

D:devstudyintercomWlyIntercomappsrcmainjni>ndk-buildn[armeabi] Compile++ thumb: speex <= speex_jni.cppn[armeabi] Compile thumb : speex <= bits.cn[armeabi] Compile thumb : speex <= cb_search.cn[armeabi] Compile thumb : speex <= exc_10_16_table.cn[armeabi] Compile thumb : speex <= exc_10_32_table.cn[armeabi] Compile thumb : speex <= exc_20_32_table.cn[armeabi] Compile thumb : speex <= exc_5_256_table.cn[armeabi] Compile thumb : speex <= exc_5_64_table.cn[armeabi] Compile thumb : speex <= exc_8_128_table.cn[armeabi] Compile thumb : speex <= filters.cn[armeabi] Compile thumb : speex <= gain_table_lbr.cn[armeabi] Compile thumb : speex <= gain_table.cn[armeabi] Compile thumb : speex <= hexc_10_32_table.cn[armeabi] Compile thumb : speex <= hexc_table.cn[armeabi] Compile thumb : speex <= high_lsp_tables.cn[armeabi] Compile thumb : speex <= kiss_fft.cn[armeabi] Compile thumb : speex <= kiss_fftr.cn[armeabi] Compile thumb : speex <= lpc.cn[armeabi] Compile thumb : speex <= lsp_tables_nb.cn[armeabi] Compile thumb : speex <= lsp.cn[armeabi] Compile thumb : speex <= ltp.cn[armeabi] Compile thumb : speex <= modes_wb.cn[armeabi] Compile thumb : speex <= modes.cn[armeabi] Compile thumb : speex <= nb_celp.cn[armeabi] Compile thumb : speex <= quant_lsp.cn[armeabi] Compile thumb : speex <= sb_celp.cn[armeabi] Compile thumb : speex <= smallft.cn[armeabi] Compile thumb : speex <= speex_callbacks.cn[armeabi] Compile thumb : speex <= speex_header.cn[armeabi] Compile thumb : speex <= speex.cn[armeabi] Compile thumb : speex <= stereo.cn[armeabi] Compile thumb : speex <= vbr.cn[armeabi] Compile thumb : speex <= vorbis_psy.cn[armeabi] Compile thumb : speex <= vq.cn[armeabi] Compile thumb : speex <= window.cn[armeabi] StaticLibrary : libstdc++.an[armeabi] SharedLibrary : libspeex.son[armeabi] Install : libspeex.so => libs/armeabi/libspeex.son

生成libs/armeabi/libspeex.so和對應的obj文件,如需單獨使用,將上述過程生成的*.so包拷貝至jniLibs文件夾中。

8、最後,在Android中通過Java去調用encode方法進行音頻數據的編碼。

/**n * 將raw原始音頻文件編碼為Speex格式n *n * @param audioData 原始音頻數據n * @return 編碼後的數據n */npublic static byte[] raw2spx(short[] audioData) {n // 原始數據中包含的整數個encFrameSizen int nSamples = audioData.length / encFrameSize;n byte[] encodedData = new byte[((audioData.length - 1) / encFrameSize + 1) * encodedFrameSize];n short[] rawByte;n // 將原數據轉換成spx壓縮的文件n byte[] encodingData = new byte[encFrameSize];n int readTotal = 0;n for (int i = 0; i < nSamples; i++) {n rawByte = new short[encFrameSize];n System.arraycopy(audioData, i * encFrameSize, rawByte, 0, encFrameSize);n int encodeSize = Speex.getInstance().encode(rawByte, 0, encodingData, rawByte.length);n System.arraycopy(encodingData, 0, encodedData, readTotal, encodeSize);n readTotal += encodeSize;n }n rawByte = new short[encFrameSize];n System.arraycopy(audioData, nSamples * encFrameSize, rawByte, 0, audioData.length - nSamples * encFrameSize);n int encodeSize = Speex.getInstance().encode(rawByte, 0, encodingData, rawByte.length);n System.arraycopy(encodingData, 0, encodedData, readTotal, encodeSize); n return encodedData;n}n

這裡設置了每幀處理160個short型數據,壓縮比為5,每幀輸出為28個byte型數據。Speex壓縮模式特徵如下:

Speex壓縮模式特徵

原文綜合考慮音頻質量、壓縮比和演算法複雜度,最後選擇了Mode 5。

private static final int DEFAULT_COMPRESSION = 5;n

03Android 語音對講項目系統架構

再次說明,本文實現參考了論文:Android real-time audio communications over local wireless,因此系統架構如下圖所示:

Android對講機系統架構

數據包要經過Record、Encoder、Transmission、Decoder、Play這一鏈條的處理,這種數據流轉就是對講機核心抽象。鑒於這種場景,本文的實現採用了責任鏈設計模式。責任鏈模式屬於行為型模式,表徵對對象的某種行為。

創建型模式,共五種:工廠方法模式、抽象工廠模式、單例模式、建造者模式、原型模式。

結構型模式,共七種:適配器模式、裝飾器模式、代理模式、外觀模式、橋接模式、組合模式、享元模式。

行為型模式,共十一種:策略模式、模板方法模式、觀察者模式、迭代子模式、責任鏈模式、命令模式、備忘錄模式、狀態模式、訪問者模式、中介者模式、解釋器模式。

責任鏈設計模式的使用場景:在責任鏈模式里,很多對象里由每一個對象對其下家的引用而連接起來形成一條鏈。請求在這個鏈上傳遞,直到鏈上的某一個對象決定處理此請求。發出這個請求的客戶端並不知道鏈上的哪一個對象最終處理這個請求,這使得系統可以在不影響客戶端的情況下動態地重新組織和分配責任。下面來看下具體的代碼:

首先定義一個JobHandler,代表每個對象,其中包含抽象方法handleRequest():

/**n * 數據處理節點n *n * @param <I> 輸入數據類型n * @param <O> 輸出數據類型n * @author yanghao1n */npublic abstract class JobHandler<I, O> {nn private JobHandler<O, ?> nextJobHandler;nn public JobHandler<O, ?> getNextJobHandler() {n return nextJobHandler;n }nn public void setNextJobHandler(JobHandler<O, ?> nextJobHandler) {n this.nextJobHandler = nextJobHandler;n }nn public abstract void handleRequest(I audioData);nn /**n * 釋放資源n */n publnic void free() {nn }n}n

JobHandler<I, O>表示輸入數據類型為I,輸出類型為O。nextJobHandler表示下一個處理請求的節點,其類型為JobHandler<O, ?>,即輸入數據類型必須為上一個處理節點的輸出數據類型。

繼承類必須實現抽象方法handleRequest(),參數類型為I,實現對數據包的處理。free()方法實現資源的釋放,繼承類可根據情況重寫該方法。這裡分別定義Recorder、Encoder、Sender、Receiver、Decoder、Tracker,均繼承自JobHandler。

以Recorder、Encoder、Sender為例說明輸入側數據的處理(這裡僅列出部分代碼,具體代碼參考github)

/**n * 音頻錄製數據格式ENCODING_PCM_16BIT,返回數據類型為short[]n *n * @author yanghao1n */npublic class Recorder extends JobHandler<short[], short[]> {nn @Overriden public void handleRequest(short[] audioData) {n if (audioRecord.getRecordingState() == AudioRecord.RECORDSTATE_STOPPED) {n audioRecord.startRecording();n }n // 實例化音頻數據緩衝n audioData = new short[inAudioBufferSize];n audioRecord.read(audioData, 0, inAudioBufferSize);n getNextJobHandler().handleRequest(audinoData);n }n}n

Recorder完成音頻採集之後,通過getNextJobHandler()方法獲取對下一個處理節點的引用,然後調用其方法handleRequest(),並且入參類型為short[]。Recorder的下一個處理節點是Encoder,在Encoder的handleRequest()方法中,實現音頻數據的編碼,其輸入類型為short[],輸出為byte[]。

/**n * 音頻編碼,輸入類型為short[],輸出為byte[]n *n * @author yanghao1n */npublic class Encoder extends JobHandler<short[], byte[]> {nn @Overriden public void handleRequest(short[] audioData) {n byte[] encodedData = AudioDataUtil.raw2spx(audioData);n getNextJobHandler().handleRequest(encodedDatan);n }n}n

Encoder的下一個處理節點是Sender,在Sender的handleRequest()方法中,通過多播(組播),將音頻編碼數據發送給區域網內的其它設備。

/**n * UDP多播發送n *n * @author yanghao1n */npublic class Sender extends JobHandler<byte[], byte[]> {nn @Overriden public void handleRequest(byte[] audioData) {n DatagramPacket datagramPacket = new DatagramPacket(n audioData, audioData.length, inetAddress, Constants.MULTI_BROADCAST_PORT);n try {n multicastSocket.send(datagramPacket);n } catch (IOException e) {n e.printStackTrace();n }n }n}n

最後,在AudioInput類的構造函數中執行對象之間的關係:

/**n * 音頻錄製、編碼、發送線程n *n * @author yanghao1n */npublic class AudioInput implements Runnable {nn private Recorder recorder;n private Encoder encoder;n private Sender sender;n private Handler handler;nn // 錄製狀態n private boolean recording = false;nn public AudioInput(Handler handler) {n this.handler = handler;n initJobHandler();n }nn /**n * 初始化錄製、編碼、發送,並指定關聯n */n private void initJobHandler() {n recorder = new Recorder();n encoder = new Encoder();n sender = new Sender(handler);n recorder.setNextJobHandler(encoder);n encoder.setNexntJobHandler(sender);n }n}n

即:在界面初始化AudioInput對應的線程的時候,就完成這些類的實例化,並指定Recorder的下一個處理者是Encoder,Encoder的下一個處理者是Sender。這樣使得整個處理流程非常靈活,比如,如果暫時沒有開發編解碼的過程,在Encoder的handleRequest()方法中直接指定下一個處理者:

public class Encoder extends JobHandler {nn @Overriden public void handleRequest(byte[] audioData) {n getNextJobHandler().handleRequest(audioData)nn }n}n

同樣的,在初始化AudioOutput對應的線程時,完成Receiver、Decoder、Tracker的實例化,並且指定Receiver的下一個處理者是Decoder、Decoder的下一個處理者是Tracker。

在Activity中,分別申明輸入、輸出Runable、線程池對象、界面更新Handler:

// 界面更新Handlernprivate AudioHandler audioHandler = new AudioHandler(this);nn// 音頻輸入、輸出Runablenprivate AudioInput audioInput;nprivate AudioOutput audioOutput;nn// 創建緩衝線程池用於錄音和接收用戶上線消息(錄音線程可能長時間不用,應該讓其超時回收)nprivate ExecutorService inputService = Executors.newCachedThreadPool();nn// 創建循環任務線程用於間隔的發送上線消息,獲取區域網內其他的用戶nprivate ScheduledExecutorService discoverService = Executors.newScheduledThreadPool(1);nn// 設置音頻播放線程為守護線程nprivate ExecutorService outputService = Executors.newSingleThreadExecutor(new ThreadFactory() {n @Overriden public Thread newThread(@NonNull Runnable r) {n Thread thread = Executors.defaultThreadFactory().newThread(r);n thread.setDaemon(true);n return thread;n }n});n

可能有的同學會覺得這裡的責任鏈設計模式用法並非真正的責任鏈,真正的責任鏈模式要求一個具體的處理者對象只能在兩個行為中選擇一個:一是承擔責任,而是把責任推給下家。不允許出現某一個具體處理者對象在承擔了一部分責任後又把責任向下傳的情況。

本文中責任鏈設計模式的用法確實不是嚴格的責任鏈模式,但學習的目的不就是活學活用嗎?

Android線程池

上述代碼涉及Android中的線程池,與Android線程池相關的類包括:Executor,Executors,ExecutorService,Future,Callable,ThreadPoolExecutor等,為了理清它們之間的關係,首先從Executor開始:

Executor介面中定義了一個方法 execute(Runnable command),該方法接收一個 Runable實例,它用來執行一個任務,任務即一個實現了Runnable 介面的類。

ExecutorService介面繼承自Executor 介面,它提供了更豐富的實現多線程的方法,比如,ExecutorService提供了關閉自己的方法,以及可為跟蹤一個或多個非同步任務執行狀況而生成Future 的方法。 可以調用ExecutorService 的shutdown()方法來平滑地關閉 ExecutorService,調用該方法後,將導致 ExecutorService停止接受任何新的任務且等待已經提交的任務執行完成(已經提交的任務會分兩類:一類是已經在執行的,另一類是還沒有開始執行的),當所有已經提交的任務執行完畢後將會關閉 ExecutorService。因此我們一般用該介面來實現和管理多線程。

Executors 提供了一系列工廠方法用於創建線程池,返回的線程池都實現了 ExecutorService介面。包括:

1、newCachedThreadPool()

創建一個可緩存線程池,如果線程池長度超過處理需要,可靈活回收空閑線程,若無可回收,則新建線程;

2、newFixedThreadPool(int)

創建一個定長線程池,可控制線程最大並發數,超出的線程會在隊列中等待。

3、newScheduledThreadPool(int)

創建一個定長線程池,支持定時及周期性任務執行。

4、newSingleThreadExecutor()

創建一個單線程化的線程池,它只會用唯一的工作線程來執行任務,保證所有任務按照指定順序(FIFO, LIFO, 優先順序)執行。

5、Callable介面與Runnable介面類似,ExecutorService的<T> Future<T> submit(Callable<T> task)方法接受Callable作為入參,在 Java 5 之後,任務分兩類:一類是實現了 Runnable介面的類,一類是實現了 Callable 介面的類。兩者都可以被 ExecutorService 執行,但是 Runnable任務沒有返回值,而 Callable任務有返回值。並且Callable 的call()方法只能通過ExecutorService 的 submit(Callable task)方法來執行,並且返回一個 Future,是表示任務等待完成的Future。

6、ThreadPoolExecutor繼承自AbstractExecutorService,AbstractExecutorService實現了ExecutorService介面。ThreadPoolExecutor的構造器由於參數較多,不宜直接暴露給使用者。所以,Executors 中定義 ExecutorService實例的工廠方法,其實是通過定義ThreadPoolExecutor不同入參來實現的。

下面來看下ThreadPoolExecutor的構造器方法:

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,n BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) {nn if (corePoolSize < 0 || maximumPoolSize <= 0 || maximumPoolSize < corePoolSize || keepAliveTime < 0)n throw new IllegalArgumentException();nn if (workQueue == null || threadFactory == null || handler == null)n throw new NullPointerException();nn this.corePoolSize = corePoolSize;n this.maximumPoolSize = maximumPoolSize;n this.workQueue = workQueue;n this.keepAliveTime = unit.toNanos(keepAliveTime);n this.threadFactory = threadFactory;n this.handlenr = handler;n}n

其中,corePoolSize表示線程池中所保存的核心線程數,包括空閑線程;maximumPoolSize表示池中允許的最大線程數;keepAliveTime表示線程池中的空閑線程所能持續的最長時間;unit表示時間的單位;workQueue表示任務執行前保存任務的隊列,僅保存由execute 方法提交的Runnable任務;threadFactory表示線程創建的工廠,指定線程的特性,比如前面代碼中設置音頻播放線程為守護線程;handler表示隊列容量滿之後的處理方法。

ThreadPoolExecutor對於傳入的任務Runnable有如下處理流程:

1、如果線程池中的線程數量少於corePoolSize,即使線程池中有空閑線程,也會創建一個新的線程來執行新添加的任務;

2、如果線程池中的線程數量大於等於corePoolSize,但緩衝隊列workQueue 未滿,則將新添加的任務放到 workQueue中,按照 FIFO 的原則依次等待執行(線程池中有線程空閑出來後依次將緩衝隊列中的任務交付給空閑的線程執行);

3、如果線程池中的線程數量大於等於 corePoolSize,且緩衝隊列 workQueue 已滿,但線程池中的線程數量小於maximumPoolSize,則會創建新的線程來處理被添加的任務;

4、如果線程池中的線程數量等於了maximumPoolSize,交由RejectedExecutionHandler handler處理。

ThreadPoolExecutor主要用於某些特定場合,即上述工廠方法無法滿足的時候,自定義線程池使用。本文使用了三種特性的線程池工廠方法:newCachedThreadPool()、newScheduledThreadPool(int)和newSingleThreadExecutor。

首先,對於錄音線程,由於對講機用戶大部分時間可能是在聽,而不是說。錄音線程可能長時間不用,應該讓其超時回收,所以錄音線程宜使用CachedThreadPool;

其次,對於發現區域網內的其它用戶的功能,該功能需要不斷循環執行,相當於循環的向區域網內發送心跳信號,因此宜使用ScheduledThreadPool;

最後,對於音頻播放線程,該線程需要一直在後台執行,且播放需要串列執行,因此使用SingleThreadExecutor,並設置為守護線程,在UI線程(主線程是最後一個用戶線程)結束之後結束。

// 設置音頻播放線程為守護線程nprivate ExecutorService outputService = Executors.newSingleThreadExecutor(new ThreadFactory() {n @Overriden public Thread newThread(@NonNull Runnable r) {n Thread thread = Executors.defaultThreadFactory().newThread(r);n thread.setDaemon(true);n return thread;nn}n});n

以上。詳細代碼請移步github:intercom。

歡迎關注我們的微信公眾號:人工智慧LeadAI,ID:atleadai

推薦閱讀:

對WiFi工作原理和信號範圍的一些問題?
既插蘋果也插安卓,數據線革新之作!
Google是如何從Android中盈利的?
三星、小米、華為都參與的這個「聯盟」,會如何改變安卓生態?

TAG:Android |