本文属于 Android 局域网内的语音对讲项目系列,《通过 UDP 广播实现 Android 局域网 Peer Discovering》实现了局域网内的广播及多播通信,本文将重点说明系统架构,音频信号的实时录制、播放及编解码相关技术。
本文主要包含以下内容:
AudioRecorder 和 AudioTracker 是 Android 中获取实时音频数据的接口。在网络电话、语音对讲等场景中,由于实时性的要求,不能采用文件传输,因此,MediaRecorder 和 MediaPlayer 就无法使用。
AudioRecorder 和 AudioTracker 是 Android 在 Java 层对 libmedia 库的封装,所以效率较高,适合于实时语音相关处理的应用。在使用时,AudioRecorder 和 AudioTracker 的构造器方法入参较多,这里对其进行详细的解释。
- public AudioRecord(int audioSource, int sampleRateInHz, int channelConfig, int audioFormat, int bufferSizeInBytes)
其中,
表示录音来源,在
- audioSource
中列举了不同的音频来源,包括:
- AudioSource
- 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
表示采样频率。音频的采集过程要经过抽样、量化和编码三步。抽样需要关注抽样率。声音是机械波,其特征主要包括频率和振幅(即音调和音量),频率对应时间轴线,振幅对应电平轴线。采样是指间隔固定的时间对波形进行一次记录,采样率就是在 1 秒内采集样本的次数。量化过程就是用数字表示振幅的过程。编码是一个减少信息量的过程,任何数字音频编码方案都是有损的。PCM 编码(脉冲编码调制)是一种保真水平较高的编码方式。在 Android 平台,44100Hz 是唯一目前所有设备都保证支持的采样频率。但比如 22050、16000、11025 也在大多数设备上得到支持。8000 是针对某些低质量的音频通信使用的。
- sampleRateInHz
表示音频通道,即选择单声道、双声道等参数。系统提供的选择如下:
- 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
表示 PCM 16bits 每个样本,所有设备保证支持。
- ENCODING_PCM_16BIT
自然表示 PCM 8bits 每个样本。
- ENCODING_PCM_8BIT
表示一个单精度浮点数表示一个样本。
- ENCODING_PCM_FLOAT
表示录音时音频数据写入的 buffer 的大小。这个数值是通过另一个方法来获取的:
- bufferSizeInBytes
。
- getMinBufferSize
是 AudioRecord 类的静态方法,返回值就是
- getMinBufferSize
。这里我们来看下它的入参:
- bufferSizeInBytes
- static public int getMinBufferSize(int sampleRateInHz, int channelConfig, int audioFormat)
三个参数与上面的含义完全一样,代表录音的采样率、通道以及数据输出的格式。综上,AudioRecord 的初始化方法如下:
- sampleRateInHz, channelConfig, audioFormat
- // 获取音频数据缓冲段大小
- 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
需与 AudioRecord 中的参数保持一致,这里不再介绍。
- audioFormat
与 AudioRecord 中的参数保持对应,比如 AudioRecord 选择了
- channelConfig
(单通道音频输入),这里需要选择
- AudioFormat.CHANNEL_IN_MONO
(单通道音频输出)。
- AudioFormat.CHANNEL_OUT_MONO
表述音频播放缓冲区大小,同样,也需要根据 AudioTrack 的静态方法
- bufferSizeInBytes
来获取。
- 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
,即 AudioTrack 依附到哪个音频会话。
- AudioSessionId
比如,要给 AudioRecord 添加回声消除
,
- AcousticEchoCanceler
的构建方法
- AcousticEchoCanceler
的入参就是 sessionId,通过 AudioRecord 实例的
- create
方法获取。
- 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 是最新支持的方法,通过配置
文件来实现;ndk-build 是传统的方式,通过配置 Android.mk 文件来实现。具体语法参考相关文档,这里不做深入介绍。配置完上述文件之后,需要将 Gradle 关联到原生库,通过 AS 的 Link C++ Project with Gradle 功能实现。
- CMakeLists.txt
完成上述配置之后,正式开始在 Android 中使用 Speex 进行音频编解码。主要包括以下步骤:
下创建
- src/main
文件夹,将上述 Speex 源码中
- jni
和
- 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@
,对应标准 C/C++ 数据类型。这个文件的内容如下:
- int
- #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 个
型数据,压缩比为 5,每帧输出为 28 个 byte 型数据。Speex 压缩模式特征如下:
- short
原文综合考虑音频质量、压缩比和算法复杂度,最后选择了 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
为例说明输入侧数据的处理(这里仅列出部分代码,具体代码参考 github 地址):
- Sender
- /**
- * 音频录制数据格式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);
- }
- }
即:在界面初始化
对应的线程的时候,就完成这些类的实例化,并指定 Recorder 的下一个处理者是 Encoder,Encoder 的下一个处理者是 Sender。这样使得整个处理流程非常灵活,比如,如果暂时没有开发编解码的过程,在 Encoder 的
- AudioInput
方法中直接指定下一个处理者:
- handleRequest()
- public class Encoder extends JobHandler {
- @Override
- public void handleRequest(byte[] audioData) {
- getNextJobHandler().handleRequest(audioData);
- }
- }
同样的,在初始化
对应的线程时,完成
- AudioOutput
、
- Receiver
、
- Decoder
的实例化,并且指定
- Tracker
的下一个处理者是
- Receiver
、
- Decoder
的下一个处理者是
- Decoder
。
- Tracker
在 Activity 中,分别申明输入、输出
、线程池对象、界面更新 Handler:
- Runable
- // 界面更新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)
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序 (FIFO, LIFO, 优先级) 执行。
- newSingleThreadExecutor()
接口与
- Callable
接口类似,
- Runnable
的
- ExecutorService
方法接受
- <T> Future<T> submit(Callable<T> task)
作为入参,在 Java 5 之后,任务分两类:一类是实现了
- Callable
接口的类,一类是实现了
- 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
中,按照 FIFO 的原则依次等待执行(线程池中有线程空闲出来后依次将缓冲队列中的任务交付给空闲的线程执行);
- workQueue
,且缓冲队列
- corePoolSize
已满,但线程池中的线程数量小于
- workQueue
,则会创建新的线程来处理被添加的任务;
- maximumPoolSize
,交由
- maximumPoolSize
处理。
- RejectedExecutionHandler handler
主要用于某些特定场合,即上述工厂方法无法满足的时候,自定义线程池使用。本文使用了三种特性的线程池工厂方法:
- ThreadPoolExecutor
、
- newCachedThreadPool()
和
- newScheduledThreadPool(int)
。
- newSingleThreadExecutor
首先,对于录音线程,由于对讲机用户大部分时间可能是在听,而不是说。录音线程可能长时间不用,应该让其超时回收,所以录音线程宜使用
; 其次,对于发现局域网内的其它用户的功能,该功能需要不断循环执行,相当于循环的向局域网内发送心跳信号,因此宜使用
- CachedThreadPool
; 最后,对于音频播放线程,该线程需要一直在后台执行,且播放需要串行执行,因此使用
- ScheduledThreadPool
,并设置为守护线程,在 UI 线程(主线程是最后一个用户线程)结束之后结束。
- SingleThreadExecutor
- // 设置音频播放线程为守护线程
- 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 。
来源: http://www.cnblogs.com/younghao/p/6748289.html