本文屬於Android局域網內的語音對講項目系列,《經過UDP廣播實現Android局域網Peer Discovering》實現了局域網內的廣播及多播通訊,本文將重點說明系統架構,音頻信號的實時錄製、播放及編解碼相關技術。html
本文主要包含如下內容:android
AudioRecorder和AudioTracker是Android中獲取實時音頻數據的接口。在網絡電話、語音對講等場景中,因爲實時性的要求,不能採用文件傳輸,所以,MediaRecorder和MediaPlayer就沒法使用。c++
AudioRecorder和AudioTracker是Android在Java層對libmedia庫的封裝,因此效率較高,適合於實時語音相關處理的應用。在使用時,AudioRecorder和AudioTracker的構造器方法入參較多,這裏對其進行詳細的解釋。git
public AudioRecord(int audioSource, int sampleRateInHz, int channelConfig, int audioFormat, int bufferSizeInBytes)
其中,audioSource
表示錄音來源,在AudioSource
中列舉了不一樣的音頻來源,包括:github
AudioSource.DEFAULT:默認音頻來源 AudioSource.MIC:麥克風(經常使用) AudioSource.VOICE_UPLINK:電話上行 AudioSource.VOICE_DOWNLINK:電話下行 AudioSource.VOICE_CALL:電話、含上下行 AudioSource.CAMCORDER:攝像頭旁的麥克風 AudioSource.VOICE_RECOGNITION:語音識別 AudioSource.VOICE_COMMUNICATION:語音通訊
這裏比較經常使用的有MIC
,VOICE_COMMUNICATION
和VOICE_CALL
。算法
sampleRateInHz
表示採樣頻率。音頻的採集過程要通過抽樣、量化和編碼三步。抽樣須要關注抽樣率。聲音是機械波,其特徵主要包括頻率和振幅(即音調和音量),頻率對應時間軸線,振幅對應電平軸線。採樣是指間隔固定的時間對波形進行一次記錄,採樣率就是在1秒內採集樣本的次數。量化過程就是用數字表示振幅的過程。編碼是一個減小信息量的過程,任何數字音頻編碼方案都是有損的。PCM編碼(脈衝編碼調製)是一種保真水平較高的編碼方式。在Android平臺,44100Hz是惟一目前全部設備都保證支持的採樣頻率。但好比22050、16000、11025也在大多數設備上獲得支持。8000是針對某些低質量的音頻通訊使用的。設計模式
channelConfig
表示音頻通道,即選擇單聲道、雙聲道等參數。系統提供的選擇以下:緩存
public static final int CHANNEL_IN_DEFAULT = 1; // These directly match native public static final int CHANNEL_IN_LEFT = 0x4; public static final int CHANNEL_IN_RIGHT = 0x8; public static final int CHANNEL_IN_FRONT = 0x10; public static final int CHANNEL_IN_BACK = 0x20; public static final int CHANNEL_IN_LEFT_PROCESSED = 0x40; public static final int CHANNEL_IN_RIGHT_PROCESSED = 0x80; public static final int CHANNEL_IN_FRONT_PROCESSED = 0x100; public static final int CHANNEL_IN_BACK_PROCESSED = 0x200; public static final int CHANNEL_IN_PRESSURE = 0x400; public static final int CHANNEL_IN_X_AXIS = 0x800; public static final int CHANNEL_IN_Y_AXIS = 0x1000; public static final int CHANNEL_IN_Z_AXIS = 0x2000; public static final int CHANNEL_IN_VOICE_UPLINK = 0x4000; public static final int CHANNEL_IN_VOICE_DNLINK = 0x8000; public static final int CHANNEL_IN_MONO = CHANNEL_IN_FRONT; public static final int CHANNEL_IN_STEREO = (CHANNEL_IN_LEFT | CHANNEL_IN_RIGHT);
經常使用的是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
表示一個單精度浮點數表示一個樣本。session
bufferSizeInBytes
表示錄音時音頻數據寫入的buffer的大小。這個數值是經過另外一個方法來獲取的:getMinBufferSize
。getMinBufferSize
是AudioRecord類的靜態方法,返回值就是bufferSizeInBytes
。這裏咱們來看下它的入參:
static public int getMinBufferSize(int sampleRateInHz, int channelConfig, int audioFormat)
sampleRateInHz, channelConfig, audioFormat
三個參數與上面的含義徹底同樣,表明錄音的採樣率、通道以及數據輸出的格式。綜上,AudioRecord的初始化方法以下:
// 獲取音頻數據緩衝段大小 inAudioBufferSize = AudioRecord.getMinBufferSize( Constants.sampleRateInHz, Constants.inputChannelConfig, Constants.audioFormat); // 初始化音頻錄製 audioRecord = new AudioRecord(Constants.audioSource, Constants.sampleRateInHz, Constants.inputChannelConfig, Constants.audioFormat, inAudioBufferSize);
其中,參數設置以下:
// 採樣頻率,44100保證兼容性 public static final int sampleRateInHz = 44100; // 音頻數據格式:PCM 16位每一個樣本,保證設備支持。 public static final int audioFormat = AudioFormat.ENCODING_PCM_16BIT; // 音頻獲取源 public static final int audioSource = MediaRecorder.AudioSource.MIC; // 輸入單聲道 public static final int inputChannelConfig = AudioFormat.CHANNEL_IN_MONO;
public AudioTrack(int streamType, int sampleRateInHz, int channelConfig, int audioFormat, int bufferSizeInBytes, int mode) throws IllegalArgumentException { this(streamType, sampleRateInHz, channelConfig, audioFormat, bufferSizeInBytes, mode, AudioManager.AUDIO_SESSION_ID_GENERATE); }
與AudioRecord相似,AudioTrack的構造器方法依然有不少須要選擇的參數。其中,streamType
表示音頻流播放類型,AudioManager
中列出了可選的類型以下:
/** The audio stream for phone calls */ public static final int STREAM_VOICE_CALL = AudioSystem.STREAM_VOICE_CALL; /** The audio stream for system sounds */ public static final int STREAM_SYSTEM = AudioSystem.STREAM_SYSTEM; /** The audio stream for the phone ring */ public static final int STREAM_RING = AudioSystem.STREAM_RING; /** The audio stream for music playback */ public static final int STREAM_MUSIC = AudioSystem.STREAM_MUSIC; /** The audio stream for alarms */ public static final int STREAM_ALARM = AudioSystem.STREAM_ALARM; /** The audio stream for notifications */ public static final int STREAM_NOTIFICATION = AudioSystem.STREAM_NOTIFICATION; /** @hide The audio stream for phone calls when connected to bluetooth */ public static final int STREAM_BLUETOOTH_SCO = AudioSystem.STREAM_BLUETOOTH_SCO; /** @hide The audio stream for enforced system sounds in certain countries (e.g camera in Japan) */ public static final int STREAM_SYSTEM_ENFORCED = AudioSystem.STREAM_SYSTEM_ENFORCED; /** The audio stream for DTMF Tones */ public static final int STREAM_DTMF = AudioSystem.STREAM_DTMF; /** @hide The audio stream for text to speech (TTS) */ public static final int STREAM_TTS = AudioSystem.STREAM_TTS;
經常使用的有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)
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() { // 獲取音頻數據緩衝段大小 outAudioBufferSize = AudioTrack.getMinBufferSize( Constants.sampleRateInHz, Constants.outputChannelConfig, Constants.audioFormat); // 初始化音頻播放 audioTrack = new AudioTrack(Constants.streamType, Constants.sampleRateInHz, Constants.outputChannelConfig, Constants.audioFormat, outAudioBufferSize, Constants.trackMode); }
其中,參數設置以下:
// 音頻播放端 public static final int streamType = AudioManager.STREAM_VOICE_CALL; // 輸出單聲道 public static final int outputChannelConfig = AudioFormat.CHANNEL_OUT_MONO; // 音頻輸出模式 public static final int trackMode = AudioTrack.MODE_STREAM;
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進行音頻編解碼。主要包括如下步驟:
src/main
下建立jni
文件夾,將上述Speex源碼中include
和libspeex
文件夾拷貝到jni
文件夾下。Android.mk
文件和Application.mk
文件。Android.mk
:LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_LDLIBS :=-llog LOCAL_MODULE := libspeex LOCAL_CFLAGS = -DFIXED_POINT -DUSE_KISS_FFT -DEXPORT="" -UHAVE_CONFIG_H LOCAL_C_INCLUDES := $(LOCAL_PATH)/include LOCAL_SRC_FILES := speex_jni.cpp \ ./libspeex/bits.c \ ./libspeex/cb_search.c \ ./libspeex/exc_10_16_table.c \ ./libspeex/exc_10_32_table.c \ ./libspeex/exc_20_32_table.c \ ./libspeex/exc_5_256_table.c \ ./libspeex/exc_5_64_table.c \ ./libspeex/exc_8_128_table.c \ ./libspeex/filters.c \ ./libspeex/gain_table_lbr.c \ ./libspeex/gain_table.c \ ./libspeex/hexc_10_32_table.c \ ./libspeex/hexc_table.c \ ./libspeex/high_lsp_tables.c \ ./libspeex/kiss_fft.c \ ./libspeex/kiss_fftr.c \ ./libspeex/lpc.c \ ./libspeex/lsp_tables_nb.c \ ./libspeex/lsp.c \ ./libspeex/ltp.c \ ./libspeex/modes_wb.c \ ./libspeex/modes.c \ ./libspeex/nb_celp.c \ ./libspeex/quant_lsp.c \ ./libspeex/sb_celp.c \ ./libspeex/smallft.c \ ./libspeex/speex_callbacks.c \ ./libspeex/speex_header.c \ ./libspeex/speex.c \ ./libspeex/stereo.c \ ./libspeex/vbr.c \ ./libspeex/vorbis_psy.c \ ./libspeex/vq.c \ ./libspeex/window.c \ include $(BUILD_SHARED_LIBRARY)
Application.mk
:
APP_ABI := armeabi armeabi-v7a
jni
中speex
源碼目錄下的include/speex
文件夾下,有一個speex_config_types.h.in
文件,在include/speex
目錄下建立speex_config_types.h
,把speex_config_types.h.in
的內容拷貝過來,而後把@SIZE16@
改爲short
,把@SIZE32@
改爲int
,對應標準C/C++數據類型。這個文件的內容以下:#ifndef __SPEEX_TYPES_H__ #define __SPEEX_TYPES_H__ typedef short spx_int16_t; typedef unsigned short spx_uint16_t; typedef int spx_int32_t; typedef unsigned int spx_uint32_t; #endif
public class Speex { static { try { System.loadLibrary("speex"); } catch (Throwable e) { e.printStackTrace(); } } public native int open(int compression); public native int getFrameSize(); public native int decode(byte encoded[], short lin[], int size); public native int encode(short lin[], int offset, byte encoded[], int size); public native void close(); }
extern "C" JNIEXPORT jint JNICALL Java_com_jd_wly_intercom_audio_Speex_encode (JNIEnv *env, jobject obj, jshortArray lin, jint offset, jbyteArray encoded, jint size) { jshort buffer[enc_frame_size]; jbyte output_buffer[enc_frame_size]; int nsamples = (size-1)/enc_frame_size + 1; int i, tot_bytes = 0; if (!codec_open) return 0; speex_bits_reset(&ebits); for (i = 0; i < nsamples; i++) { env->GetShortArrayRegion(lin, offset + i*enc_frame_size, enc_frame_size, buffer); speex_encode_int(enc_state, buffer, &ebits); } tot_bytes = speex_bits_write(&ebits, (char *)output_buffer, enc_frame_size); env->SetByteArrayRegion(encoded, 0, tot_bytes, output_buffer); return (jint)tot_bytes; }
Android.mk
文件夾下,執行命令ndk-build
:D:\dev\study\intercom\WlyIntercom\app\src\main\jni>ndk-build [armeabi] Compile++ thumb: speex <= speex_jni.cpp [armeabi] Compile thumb : speex <= bits.c [armeabi] Compile thumb : speex <= cb_search.c [armeabi] Compile thumb : speex <= exc_10_16_table.c [armeabi] Compile thumb : speex <= exc_10_32_table.c [armeabi] Compile thumb : speex <= exc_20_32_table.c [armeabi] Compile thumb : speex <= exc_5_256_table.c [armeabi] Compile thumb : speex <= exc_5_64_table.c [armeabi] Compile thumb : speex <= exc_8_128_table.c [armeabi] Compile thumb : speex <= filters.c [armeabi] Compile thumb : speex <= gain_table_lbr.c [armeabi] Compile thumb : speex <= gain_table.c [armeabi] Compile thumb : speex <= hexc_10_32_table.c [armeabi] Compile thumb : speex <= hexc_table.c [armeabi] Compile thumb : speex <= high_lsp_tables.c [armeabi] Compile thumb : speex <= kiss_fft.c [armeabi] Compile thumb : speex <= kiss_fftr.c [armeabi] Compile thumb : speex <= lpc.c [armeabi] Compile thumb : speex <= lsp_tables_nb.c [armeabi] Compile thumb : speex <= lsp.c [armeabi] Compile thumb : speex <= ltp.c [armeabi] Compile thumb : speex <= modes_wb.c [armeabi] Compile thumb : speex <= modes.c [armeabi] Compile thumb : speex <= nb_celp.c [armeabi] Compile thumb : speex <= quant_lsp.c [armeabi] Compile thumb : speex <= sb_celp.c [armeabi] Compile thumb : speex <= smallft.c [armeabi] Compile thumb : speex <= speex_callbacks.c [armeabi] Compile thumb : speex <= speex_header.c [armeabi] Compile thumb : speex <= speex.c [armeabi] Compile thumb : speex <= stereo.c [armeabi] Compile thumb : speex <= vbr.c [armeabi] Compile thumb : speex <= vorbis_psy.c [armeabi] Compile thumb : speex <= vq.c [armeabi] Compile thumb : speex <= window.c [armeabi] StaticLibrary : libstdc++.a [armeabi] SharedLibrary : libspeex.so [armeabi] Install : libspeex.so => libs/armeabi/libspeex.so
生成libs/armeabi/libspeex.so
和對應的obj
文件,如需單獨使用,將上述過程生成的*.so
包拷貝至jniLibs
文件夾中。
encode
方法進行音頻數據的編碼。/** * 將raw原始音頻文件編碼爲Speex格式 * * @param audioData 原始音頻數據 * @return 編碼後的數據 */ public static byte[] raw2spx(short[] audioData) { // 原始數據中包含的整數個encFrameSize int nSamples = audioData.length / encFrameSize; byte[] encodedData = new byte[((audioData.length - 1) / encFrameSize + 1) * encodedFrameSize]; short[] rawByte; // 將原數據轉換成spx壓縮的文件 byte[] encodingData = new byte[encFrameSize]; int readTotal = 0; for (int i = 0; i < nSamples; i++) { rawByte = new short[encFrameSize]; System.arraycopy(audioData, i * encFrameSize, rawByte, 0, encFrameSize); int encodeSize = Speex.getInstance().encode(rawByte, 0, encodingData, rawByte.length); System.arraycopy(encodingData, 0, encodedData, readTotal, encodeSize); readTotal += encodeSize; } rawByte = new short[encFrameSize]; System.arraycopy(audioData, nSamples * encFrameSize, rawByte, 0, audioData.length - nSamples * encFrameSize); int encodeSize = Speex.getInstance().encode(rawByte, 0, encodingData, rawByte.length); System.arraycopy(encodingData, 0, encodedData, readTotal, encodeSize); return encodedData; }
這裏設置了每幀處理160個short
型數據,壓縮比爲5,每幀輸出爲28個byte型數據。Speex壓縮模式特徵以下:
原文綜合考慮音頻質量、壓縮比和算法複雜度,最後選擇了Mode 5。
private static final int DEFAULT_COMPRESSION = 5;
再次說明,本文實現參考了論文:Android real-time audio communications over local wireless,所以系統架構以下圖所示:
數據包要通過Record、Encoder、Transmission、Decoder、Play這一鏈條的處理,這種數據流轉就是對講機核心抽象。鑑於這種場景,本文的實現採用了責任鏈設計模式。責任鏈模式屬於行爲型模式,表徵對對象的某種行爲。
建立型模式,共五種:工廠方法模式、抽象工廠模式、單例模式、建造者模式、原型模式。
結構型模式,共七種:適配器模式、裝飾器模式、代理模式、外觀模式、橋接模式、組合模式、享元模式。
行爲型模式,共十一種:策略模式、模板方法模式、觀察者模式、迭代子模式、責任鏈模式、命令模式、備忘錄模式、狀態模式、訪問者模式、中介者模式、解釋器模式。
責任鏈設計模式的使用場景:在責任鏈模式裏,不少對象裏由每個對象對其下家的引用而鏈接起來造成一條鏈。請求在這個鏈上傳遞,直到鏈上的某一個對象決定處理此請求。發出這個請求的客戶端並不知道鏈上的哪個對象最終處理這個請求,這使得系統能夠在不影響客戶端的狀況下動態地從新組織和分配責任。下面來看下具體的代碼:
首先定義一個JobHandler,表明每一個對象,其中包含抽象方法handleRequest():
/** * 數據處理節點 * * @param <I> 輸入數據類型 * @param <O> 輸出數據類型 * @author yanghao1 */ public abstract class JobHandler<I, O> { private JobHandler<O, ?> nextJobHandler; public JobHandler<O, ?> getNextJobHandler() { return nextJobHandler; } public void setNextJobHandler(JobHandler<O, ?> nextJobHandler) { this.nextJobHandler = nextJobHandler; } public abstract void handleRequest(I audioData); /** * 釋放資源 */ public void free() { } }
JobHandler<I, O>
表示輸入數據類型爲I
,輸出類型爲O
。nextJobHandler
表示下一個處理請求的節點,其類型爲JobHandler<O, ?>
,即輸入數據類型必須爲上一個處理節點的輸出數據類型。
繼承類必須實現抽象方法handleRequest()
,參數類型爲I
,實現對數據包的處理。free()
方法實現資源的釋放,繼承類可根據狀況重寫該方法。這裏分別定義Recorder
、Encoder
、Sender
、Receiver
、Decoder
、Tracker
,均繼承自JobHandler
。
以Recorder
、Encoder
、Sender
爲例說明輸入側數據的處理(這裏僅列出部分代碼,具體代碼參考github地址):
/** * 音頻錄製數據格式ENCODING_PCM_16BIT,返回數據類型爲short[] * * @author yanghao1 */ public class Recorder extends JobHandler<short[], short[]> { @Override public void handleRequest(short[] audioData) { if (audioRecord.getRecordingState() == AudioRecord.RECORDSTATE_STOPPED) { audioRecord.startRecording(); } // 實例化音頻數據緩衝 audioData = new short[inAudioBufferSize]; audioRecord.read(audioData, 0, inAudioBufferSize); getNextJobHandler().handleRequest(audioData); } }
Recorder
完成音頻採集以後,經過getNextJobHandler()
方法獲取對下一個處理節點的引用,而後調用其方法handleRequest()
,而且入參類型爲short[]
。Recorder
的下一個處理節點是Encoder
,在Encoder
的handleRequest()
方法中,實現音頻數據的編碼,其輸入類型爲short[]
,輸出爲byte[]
。
/** * 音頻編碼,輸入類型爲short[],輸出爲byte[] * * @author yanghao1 */ public class Encoder extends JobHandler<short[], byte[]> { @Override public void handleRequest(short[] audioData) { byte[] encodedData = AudioDataUtil.raw2spx(audioData); getNextJobHandler().handleRequest(encodedData); } }
Encoder
的下一個處理節點是Sender
,在Sender
的handleRequest()
方法中,經過多播(組播),將音頻編碼數據發送給局域網內的其它設備。
/** * UDP多播發送 * * @author yanghao1 */ public class Sender extends JobHandler<byte[], byte[]> { @Override public void handleRequest(byte[] audioData) { DatagramPacket datagramPacket = new DatagramPacket( audioData, audioData.length, inetAddress, Constants.MULTI_BROADCAST_PORT); try { multicastSocket.send(datagramPacket); } catch (IOException e) { e.printStackTrace(); } } }
最後,在AudioInput類的構造函數中執行對象之間的關係:
/** * 音頻錄製、編碼、發送線程 * * @author yanghao1 */ public class AudioInput implements Runnable { private Recorder recorder; private Encoder encoder; private Sender sender; private Handler handler; // 錄製狀態 private boolean recording = false; public AudioInput(Handler handler) { this.handler = handler; initJobHandler(); } /** * 初始化錄製、編碼、發送,並指定關聯 */ private void initJobHandler() { recorder = new Recorder(); encoder = new Encoder(); sender = new Sender(handler); recorder.setNextJobHandler(encoder); encoder.setNextJobHandler(sender); } }
即:在界面初始化AudioInput
對應的線程的時候,就完成這些類的實例化,並指定Recorder的下一個處理者是Encoder,Encoder的下一個處理者是Sender。這樣使得整個處理流程很是靈活,好比,若是暫時沒有開發編解碼的過程,在Encoder的handleRequest()
方法中直接指定下一個處理者:
public class Encoder extends JobHandler { @Override public void handleRequest(byte[] audioData) { getNextJobHandler().handleRequest(audioData); } }
一樣的,在初始化AudioOutput
對應的線程時,完成Receiver
、Decoder
、Tracker
的實例化,而且指定Receiver
的下一個處理者是Decoder
、Decoder
的下一個處理者是Tracker
。
在Activity中,分別申明輸入、輸出Runable
、線程池對象、界面更新Handler:
// 界面更新Handler private AudioHandler audioHandler = new AudioHandler(this); // 音頻輸入、輸出Runable private AudioInput audioInput; private AudioOutput audioOutput; // 建立緩衝線程池用於錄音和接收用戶上線消息(錄音線程可能長時間不用,應該讓其超時回收) private ExecutorService inputService = Executors.newCachedThreadPool(); // 建立循環任務線程用於間隔的發送上線消息,獲取局域網內其餘的用戶 private ScheduledExecutorService discoverService = Executors.newScheduledThreadPool(1); // 設置音頻播放線程爲守護線程 private ExecutorService outputService = Executors.newSingleThreadExecutor(new ThreadFactory() { @Override public Thread newThread(@NonNull Runnable r) { Thread thread = Executors.defaultThreadFactory().newThread(r); thread.setDaemon(true); return thread; } });
可能有的同窗會以爲這裏的責任鏈設計模式用法並不是真正的責任鏈,真正的責任鏈模式要求一個具體的處理者對象只能在兩個行爲中選擇一個:一是承擔責任,而是把責任推給下家。不容許出現某一個具體處理者對象在承擔了一部分責任後又把責任向下傳的狀況。
本文中責任鏈設計模式的用法確實不是嚴格的責任鏈模式,但學習的目的不就是活學活用嗎?
上述代碼涉及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
接口。包括:
newCachedThreadPool()
newFixedThreadPool(int)
newScheduledThreadPool(int)
newSingleThreadExecutor()
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
。ThreadPoolExecutor
繼承自AbstractExecutorService
,AbstractExecutorService
實現了ExecutorService
接口。ThreadPoolExecutor
的構造器因爲參數較多,不宜直接暴露給使用者。因此,Executors
中定義 ExecutorService
實例的工廠方法,實際上是經過定義ThreadPoolExecutor
不一樣入參來實現的。下面來看下ThreadPoolExecutor
的構造器方法:
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { if (corePoolSize < 0 || maximumPoolSize <= 0 || maximumPoolSize < corePoolSize || keepAliveTime < 0) throw new IllegalArgumentException(); if (workQueue == null || threadFactory == null || handler == null) throw new NullPointerException(); this.corePoolSize = corePoolSize; this.maximumPoolSize = maximumPoolSize; this.workQueue = workQueue; this.keepAliveTime = unit.toNanos(keepAliveTime); this.threadFactory = threadFactory; this.handler = handler; }
其中,corePoolSize
表示線程池中所保存的核心線程數,包括空閒線程;maximumPoolSize
表示池中容許的最大線程數;keepAliveTime
表示線程池中的空閒線程所能持續的最長時間;unit
表示時間的單位;workQueue
表示任務執行前保存任務的隊列,僅保存由execute
方法提交的Runnable
任務;threadFactory
表示線程建立的工廠,指定線程的特性,好比前面代碼中設置音頻播放線程爲守護線程;handler
表示隊列容量滿以後的處理方法。
ThreadPoolExecutor
對於傳入的任務Runnable
有以下處理流程:
corePoolSize
,即便線程池中有空閒線程,也會建立一個新的線程來執行新添加的任務;corePoolSize
,但緩衝隊列workQueue
未滿,則將新添加的任務放到 workQueue
中,按照 FIFO 的原則依次等待執行(線程池中有線程空閒出來後依次將緩衝隊列中的任務交付給空閒的線程執行);corePoolSize
,且緩衝隊列 workQueue
已滿,但線程池中的線程數量小於maximumPoolSize
,則會建立新的線程來處理被添加的任務;maximumPoolSize
,交由RejectedExecutionHandler handler
處理。ThreadPoolExecutor
主要用於某些特定場合,即上述工廠方法沒法知足的時候,自定義線程池使用。本文使用了三種特性的線程池工廠方法:newCachedThreadPool()
、newScheduledThreadPool(int)
和newSingleThreadExecutor
。
首先,對於錄音線程,因爲對講機用戶大部分時間多是在聽,而不是說。錄音線程可能長時間不用,應該讓其超時回收,因此錄音線程宜使用CachedThreadPool
;
其次,對於發現局域網內的其它用戶的功能,該功能須要不斷循環執行,至關於循環的向局域網內發送心跳信號,所以宜使用ScheduledThreadPool
;
最後,對於音頻播放線程,該線程須要一直在後臺執行,且播放須要串行執行,所以使用SingleThreadExecutor
,並設置爲守護線程,在UI線程(主線程是最後一個用戶線程)結束以後結束。
// 設置音頻播放線程爲守護線程 private ExecutorService outputService = Executors.newSingleThreadExecutor(new ThreadFactory() { @Override public Thread newThread(@NonNull Runnable r) { Thread thread = Executors.defaultThreadFactory().newThread(r); thread.setDaemon(true); return thread; } });
以上。詳細代碼請移步github:intercom 。